@kisev/agentomatic 11.0.0-dev.46.gfbe1e4c6992e
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 +95 -0
- package/README.ru.md +98 -0
- package/dist/agent-profiles.d.ts +125 -0
- package/dist/agent-profiles.js +820 -0
- package/dist/assets/agents/architect.md +67 -0
- package/dist/assets/agents/critic.md +55 -0
- package/dist/assets/agents/manager.md +40 -0
- package/dist/assets/agents/mapper.md +33 -0
- package/dist/assets/agents/review.md +37 -0
- package/dist/assets/agents/worker.md +78 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/briefing.md +10 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/mattermost-triage.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk-stats.md +13 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +13 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/taskmatic.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +301 -0
- package/dist/assets/plugins/memomatic.js +3 -0
- package/dist/assets/plugins/rtk.js +3 -0
- package/dist/assets/plugins/rules-injector.js +3 -0
- package/dist/assets/plugins/zed-bell.js +3 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +39 -0
- package/dist/cli-output.d.ts +27 -0
- package/dist/cli-output.js +296 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1135 -0
- package/dist/config-setup.d.ts +71 -0
- package/dist/config-setup.js +438 -0
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +102 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +638 -0
- package/dist/generate-assets.d.ts +1 -0
- package/dist/generate-assets.js +54 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +141 -0
- package/dist/installer.d.ts +56 -0
- package/dist/installer.js +710 -0
- package/dist/jsonc.d.ts +56 -0
- package/dist/jsonc.js +432 -0
- package/dist/lifecycle.d.ts +77 -0
- package/dist/lifecycle.js +736 -0
- package/dist/package-metadata.d.ts +4 -0
- package/dist/package-metadata.js +42 -0
- package/dist/plugins/memomatic.d.ts +76 -0
- package/dist/plugins/memomatic.js +101 -0
- package/dist/plugins/rtk.d.ts +41 -0
- package/dist/plugins/rtk.js +177 -0
- package/dist/plugins/rules-injector.d.ts +22 -0
- package/dist/plugins/rules-injector.js +131 -0
- package/dist/plugins/zed-bell.d.ts +13 -0
- package/dist/plugins/zed-bell.js +14 -0
- package/dist/reconcile.d.ts +47 -0
- package/dist/reconcile.js +490 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +92 -0
- package/dist/routing.d.ts +133 -0
- package/dist/routing.js +578 -0
- package/dist/runtime/state.d.ts +9 -0
- package/dist/runtime/state.js +193 -0
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/dist/terminal-wizard.d.ts +3 -0
- package/dist/terminal-wizard.js +237 -0
- package/package.json +72 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LifecycleError, type Scope, type SupersededPlan } from "./lifecycle.js";
|
|
2
|
+
export declare class ConfigSetupError extends LifecycleError {
|
|
3
|
+
}
|
|
4
|
+
export declare const CONFIG_TARGETS: readonly ["opencode", "tui", "kilo", "mimo"];
|
|
5
|
+
export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
|
|
6
|
+
export declare const CONFIG_FRAGMENTS: readonly [{
|
|
7
|
+
readonly name: "core-plugin";
|
|
8
|
+
readonly description: "Register @kisev/agentomatic in the plugin array (OpenCode only)";
|
|
9
|
+
readonly targets: readonly ["opencode"];
|
|
10
|
+
}, {
|
|
11
|
+
readonly name: "skills-state-permissions";
|
|
12
|
+
readonly description: "Allow the standard skills XDG state paths without per-run prompts";
|
|
13
|
+
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "lsp-preset";
|
|
16
|
+
readonly description: "Add LSP servers from the shared catalog (OpenCode only)";
|
|
17
|
+
readonly targets: readonly ["opencode"];
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "secrets-guard";
|
|
20
|
+
readonly description: "Deny reads and edits of common secret files";
|
|
21
|
+
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "kilo-display";
|
|
24
|
+
readonly description: "Expand Kilo reasoning, terminal, edit, and tool blocks";
|
|
25
|
+
readonly targets: readonly ["kilo"];
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "tui-schema";
|
|
28
|
+
readonly description: "Add the tui.json schema and stacked diffs";
|
|
29
|
+
readonly targets: readonly ["tui"];
|
|
30
|
+
}];
|
|
31
|
+
export type FragmentName = (typeof CONFIG_FRAGMENTS)[number]["name"];
|
|
32
|
+
export type ConfigSetupSelection = {
|
|
33
|
+
targets: ConfigTargetName[];
|
|
34
|
+
fragments: FragmentName[];
|
|
35
|
+
};
|
|
36
|
+
export type ConfigSetupOperation = {
|
|
37
|
+
target: ConfigTargetName;
|
|
38
|
+
path: string;
|
|
39
|
+
fragment: FragmentName | "file";
|
|
40
|
+
operation: "create" | "update" | "unchanged" | "conflict";
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
export type ConfigSetupPlan = {
|
|
44
|
+
schema_version: 1;
|
|
45
|
+
action: "config-setup";
|
|
46
|
+
scope: Scope;
|
|
47
|
+
root: string;
|
|
48
|
+
package_version: string;
|
|
49
|
+
selection: ConfigSetupSelection;
|
|
50
|
+
targets: Array<{
|
|
51
|
+
target: ConfigTargetName;
|
|
52
|
+
path: string;
|
|
53
|
+
exists: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
operations: ConfigSetupOperation[];
|
|
56
|
+
skipped_fragments: Array<{
|
|
57
|
+
fragment: FragmentName;
|
|
58
|
+
reason: string;
|
|
59
|
+
}>;
|
|
60
|
+
plan_digest: string;
|
|
61
|
+
digest: string;
|
|
62
|
+
confirmation_digest?: string;
|
|
63
|
+
receipt_expires_at?: string;
|
|
64
|
+
superseded_plan?: SupersededPlan;
|
|
65
|
+
requires_restart: boolean;
|
|
66
|
+
confirmable: boolean;
|
|
67
|
+
};
|
|
68
|
+
export declare function normalizeConfigSelection(scope: Scope, value?: Partial<ConfigSetupSelection>): ConfigSetupSelection;
|
|
69
|
+
export declare function defaultConfigSelection(scope: Scope, cwd?: string, home?: string): Promise<ConfigSetupSelection>;
|
|
70
|
+
export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string): Promise<ConfigSetupPlan>;
|
|
71
|
+
export declare function applyConfigSetup(selection: ConfigSetupSelection, scope: Scope, confirmationDigest: string, cwd?: string, home?: string): Promise<ConfigSetupPlan>;
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { requirePackageVersion } from "./package-metadata.js";
|
|
6
|
+
import { applyTransaction, consumeReceipt, deploymentRoot, digest, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, saveReceipt, sha256, withLifecycleLock, } from "./lifecycle.js";
|
|
7
|
+
import { applyJsoncEdits, parseJsonc } from "./jsonc.js";
|
|
8
|
+
const PACKAGE_NAME = "@kisev/agentomatic";
|
|
9
|
+
const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
|
|
10
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const XDG_STATE_GLOB = "~/.local/state/agent-skills/**";
|
|
12
|
+
const OPENCODE_SKILLS_GLOB = "~/.config/opencode/skills/**";
|
|
13
|
+
const SECRET_PATHS = [
|
|
14
|
+
"**/.env",
|
|
15
|
+
"**/.env.*",
|
|
16
|
+
"**/*.env",
|
|
17
|
+
"**/.envrc",
|
|
18
|
+
"**/.npmrc",
|
|
19
|
+
"**/.netrc",
|
|
20
|
+
"**/.pypirc",
|
|
21
|
+
"**/.git-credentials",
|
|
22
|
+
"**/.ssh/**",
|
|
23
|
+
"**/id_rsa",
|
|
24
|
+
"**/id_ed25519",
|
|
25
|
+
"**/*.pem",
|
|
26
|
+
"**/*.key",
|
|
27
|
+
"**/*.p12",
|
|
28
|
+
"**/*.pfx",
|
|
29
|
+
"**/.kube/config",
|
|
30
|
+
"**/kubeconfig*",
|
|
31
|
+
"**/.aws/credentials",
|
|
32
|
+
"**/.docker/config.json",
|
|
33
|
+
"**/auth.json",
|
|
34
|
+
];
|
|
35
|
+
export class ConfigSetupError extends LifecycleError {
|
|
36
|
+
}
|
|
37
|
+
export const CONFIG_TARGETS = ["opencode", "tui", "kilo", "mimo"];
|
|
38
|
+
export const CONFIG_FRAGMENTS = [
|
|
39
|
+
{
|
|
40
|
+
name: "core-plugin",
|
|
41
|
+
description: "Register @kisev/agentomatic in the plugin array (OpenCode only)",
|
|
42
|
+
targets: ["opencode"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "skills-state-permissions",
|
|
46
|
+
description: "Allow the standard skills XDG state paths without per-run prompts",
|
|
47
|
+
targets: ["opencode", "kilo", "mimo"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "lsp-preset",
|
|
51
|
+
description: "Add LSP servers from the shared catalog (OpenCode only)",
|
|
52
|
+
targets: ["opencode"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "secrets-guard",
|
|
56
|
+
description: "Deny reads and edits of common secret files",
|
|
57
|
+
targets: ["opencode", "kilo", "mimo"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "kilo-display",
|
|
61
|
+
description: "Expand Kilo reasoning, terminal, edit, and tool blocks",
|
|
62
|
+
targets: ["kilo"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "tui-schema",
|
|
66
|
+
description: "Add the tui.json schema and stacked diffs",
|
|
67
|
+
targets: ["tui"],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const FRAGMENT_NAMES = CONFIG_FRAGMENTS.map((fragment) => fragment.name);
|
|
71
|
+
const LSP_SERVER_COMMANDS = {
|
|
72
|
+
python: ["basedpyright-langserver", "--stdio"],
|
|
73
|
+
typescript: ["typescript-language-server", "--stdio"],
|
|
74
|
+
yaml: ["yaml-language-server", "--stdio"],
|
|
75
|
+
shell: ["bash-language-server", "start"],
|
|
76
|
+
};
|
|
77
|
+
function lspCatalog() {
|
|
78
|
+
const raw = readFileSync(resolve(packageRoot, "dist", "assets", "lsp-catalog.json"), "utf8");
|
|
79
|
+
const catalog = JSON.parse(raw);
|
|
80
|
+
if (!Array.isArray(catalog.servers))
|
|
81
|
+
throw new ConfigSetupError("invalid_package", "LSP catalog is unavailable");
|
|
82
|
+
return catalog.servers
|
|
83
|
+
.filter((server) => typeof server.name === "string" &&
|
|
84
|
+
Array.isArray(server.extensions) &&
|
|
85
|
+
server.extensions.every((extension) => typeof extension === "string"))
|
|
86
|
+
.filter((server) => LSP_SERVER_COMMANDS[server.name]);
|
|
87
|
+
}
|
|
88
|
+
function mapAllowsAll(value) {
|
|
89
|
+
return Boolean(value &&
|
|
90
|
+
typeof value === "object" &&
|
|
91
|
+
!Array.isArray(value) &&
|
|
92
|
+
value["*"] === "allow");
|
|
93
|
+
}
|
|
94
|
+
function permissionMapEdits(path, entries) {
|
|
95
|
+
return [{ kind: "widen-scalar-map", path, entries }];
|
|
96
|
+
}
|
|
97
|
+
function fragmentEdits(fragment, target, current) {
|
|
98
|
+
const value = current && typeof current === "object" && !Array.isArray(current)
|
|
99
|
+
? current
|
|
100
|
+
: {};
|
|
101
|
+
const permission = value.permission && typeof value.permission === "object" && !Array.isArray(value.permission)
|
|
102
|
+
? value.permission
|
|
103
|
+
: {};
|
|
104
|
+
if (fragment === "core-plugin") {
|
|
105
|
+
return [
|
|
106
|
+
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
|
|
107
|
+
{ kind: "set-if-absent", path: ["plugin"], value: [] },
|
|
108
|
+
{
|
|
109
|
+
kind: "replace-array-value",
|
|
110
|
+
path: ["plugin"],
|
|
111
|
+
from: LEGACY_PACKAGE_NAME,
|
|
112
|
+
to: PACKAGE_NAME,
|
|
113
|
+
},
|
|
114
|
+
{ kind: "append-unique", path: ["plugin"], value: PACKAGE_NAME },
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
if (fragment === "skills-state-permissions") {
|
|
118
|
+
const statePaths = target === "opencode" ? [XDG_STATE_GLOB, OPENCODE_SKILLS_GLOB] : [XDG_STATE_GLOB];
|
|
119
|
+
const edits = [];
|
|
120
|
+
if (!mapAllowsAll(permission.read))
|
|
121
|
+
edits.push(...permissionMapEdits(["permission", "read"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
|
|
122
|
+
if (!mapAllowsAll(permission.edit))
|
|
123
|
+
edits.push(...permissionMapEdits(["permission", "edit"], { [XDG_STATE_GLOB]: "allow" }));
|
|
124
|
+
if (!mapAllowsAll(permission.external_directory))
|
|
125
|
+
edits.push(...permissionMapEdits(["permission", "external_directory"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
|
|
126
|
+
return edits;
|
|
127
|
+
}
|
|
128
|
+
if (fragment === "lsp-preset") {
|
|
129
|
+
return lspCatalog().map((server) => ({
|
|
130
|
+
kind: "set-if-absent",
|
|
131
|
+
path: ["lsp", server.name],
|
|
132
|
+
value: { command: LSP_SERVER_COMMANDS[server.name], extensions: server.extensions },
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
if (fragment === "secrets-guard") {
|
|
136
|
+
const deny = Object.fromEntries(SECRET_PATHS.map((glob) => [glob, "deny"]));
|
|
137
|
+
return [
|
|
138
|
+
...permissionMapEdits(["permission", "read"], {
|
|
139
|
+
...deny,
|
|
140
|
+
"**/.env.example": "allow",
|
|
141
|
+
}),
|
|
142
|
+
...permissionMapEdits(["permission", "edit"], deny),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
if (fragment === "kilo-display") {
|
|
146
|
+
return [
|
|
147
|
+
{ kind: "set-if-absent", path: ["reasoning_display"], value: "expanded" },
|
|
148
|
+
{ kind: "set-if-absent", path: ["terminal_command_display"], value: "expanded" },
|
|
149
|
+
{ kind: "set-if-absent", path: ["code_edit_display"], value: "expanded" },
|
|
150
|
+
{ kind: "set-if-absent", path: ["mcp_tool_display"], value: "expanded" },
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
return [
|
|
154
|
+
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/tui.json" },
|
|
155
|
+
{ kind: "set-if-absent", path: ["diff_style"], value: "stacked" },
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
function applicable(fragment, target) {
|
|
159
|
+
const targets = CONFIG_FRAGMENTS.find((item) => item.name === fragment)?.targets ?? [];
|
|
160
|
+
return targets.includes(target);
|
|
161
|
+
}
|
|
162
|
+
export function normalizeConfigSelection(scope, value = {}) {
|
|
163
|
+
const requestedTargets = value.targets ?? ["opencode"];
|
|
164
|
+
const targets = CONFIG_TARGETS.filter((target) => requestedTargets.includes(target));
|
|
165
|
+
if (targets.length !== requestedTargets.length)
|
|
166
|
+
throw new ConfigSetupError("invalid_selection", "Unknown config target selection");
|
|
167
|
+
if (scope === "project" && (targets.length !== 1 || targets[0] !== "opencode"))
|
|
168
|
+
throw new ConfigSetupError("invalid_selection", "Project scope supports only the opencode target");
|
|
169
|
+
const requestedFragments = value.fragments ?? [];
|
|
170
|
+
const fragments = FRAGMENT_NAMES.filter((fragment) => requestedFragments.includes(fragment));
|
|
171
|
+
if (fragments.length !== requestedFragments.length)
|
|
172
|
+
throw new ConfigSetupError("invalid_selection", "Unknown config fragment selection");
|
|
173
|
+
return { targets, fragments };
|
|
174
|
+
}
|
|
175
|
+
export async function defaultConfigSelection(scope, cwd = process.cwd(), home = homedir()) {
|
|
176
|
+
const exists = async (path) => Boolean(await readRegular(path).catch(() => undefined));
|
|
177
|
+
const targets = ["opencode"];
|
|
178
|
+
if (scope === "global") {
|
|
179
|
+
if (await exists(join(home, ".config", "opencode", "tui.json")))
|
|
180
|
+
targets.push("tui");
|
|
181
|
+
if (await exists(join(home, ".config", "kilo", "kilo.json")))
|
|
182
|
+
targets.push("kilo");
|
|
183
|
+
if (await exists(join(home, ".config", "kilo", "kilo.jsonc")))
|
|
184
|
+
targets.push("kilo");
|
|
185
|
+
if (await exists(join(home, ".config", "mimocode", "mimocode.json")))
|
|
186
|
+
targets.push("mimo");
|
|
187
|
+
if (await exists(join(home, ".config", "mimocode", "mimocode.jsonc")))
|
|
188
|
+
targets.push("mimo");
|
|
189
|
+
}
|
|
190
|
+
return normalizeConfigSelection(scope, {
|
|
191
|
+
targets: [...new Set(targets)],
|
|
192
|
+
fragments: [...FRAGMENT_NAMES],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function resolveTargetFile(target, scope, cwd, home) {
|
|
196
|
+
const existing = async (root, path) => Boolean(await readRegular(join(root, path)).catch(() => undefined));
|
|
197
|
+
const globalRoot = deploymentRoot("global", cwd, home);
|
|
198
|
+
if (target === "opencode") {
|
|
199
|
+
if (scope === "global") {
|
|
200
|
+
for (const path of ["opencode.json", "opencode.jsonc"])
|
|
201
|
+
if (await existing(globalRoot, path))
|
|
202
|
+
return { target, root: globalRoot, path, absolute: join(globalRoot, path), exists: true };
|
|
203
|
+
return {
|
|
204
|
+
target,
|
|
205
|
+
root: globalRoot,
|
|
206
|
+
path: "opencode.jsonc",
|
|
207
|
+
absolute: join(globalRoot, "opencode.jsonc"),
|
|
208
|
+
exists: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const projectRoot = deploymentRoot("project", cwd, home);
|
|
212
|
+
for (const [root, path] of [
|
|
213
|
+
[cwd, "opencode.json"],
|
|
214
|
+
[cwd, "opencode.jsonc"],
|
|
215
|
+
[projectRoot, "opencode.json"],
|
|
216
|
+
[projectRoot, "opencode.jsonc"],
|
|
217
|
+
])
|
|
218
|
+
if (await existing(root, path))
|
|
219
|
+
return { target, root, path, absolute: join(root, path), exists: true };
|
|
220
|
+
return {
|
|
221
|
+
target,
|
|
222
|
+
root: cwd,
|
|
223
|
+
path: "opencode.jsonc",
|
|
224
|
+
absolute: join(cwd, "opencode.jsonc"),
|
|
225
|
+
exists: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (target === "tui")
|
|
229
|
+
return {
|
|
230
|
+
target,
|
|
231
|
+
root: globalRoot,
|
|
232
|
+
path: "tui.json",
|
|
233
|
+
absolute: join(globalRoot, "tui.json"),
|
|
234
|
+
exists: await existing(globalRoot, "tui.json"),
|
|
235
|
+
};
|
|
236
|
+
if (target === "kilo") {
|
|
237
|
+
const root = join(home, ".config", "kilo");
|
|
238
|
+
for (const path of ["kilo.jsonc", "kilo.json"])
|
|
239
|
+
if (await existing(root, path))
|
|
240
|
+
return { target, root, path, absolute: join(root, path), exists: true };
|
|
241
|
+
return { target, root, path: "kilo.jsonc", absolute: join(root, "kilo.jsonc"), exists: false };
|
|
242
|
+
}
|
|
243
|
+
const root = join(home, ".config", "mimocode");
|
|
244
|
+
for (const path of ["mimocode.jsonc", "mimocode.json"])
|
|
245
|
+
if (await existing(root, path))
|
|
246
|
+
return { target, root, path, absolute: join(root, path), exists: true };
|
|
247
|
+
return {
|
|
248
|
+
target,
|
|
249
|
+
root,
|
|
250
|
+
path: "mimocode.jsonc",
|
|
251
|
+
absolute: join(root, "mimocode.jsonc"),
|
|
252
|
+
exists: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
256
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
257
|
+
const operations = [];
|
|
258
|
+
const mutations = [];
|
|
259
|
+
const allowedRoots = new Set([root]);
|
|
260
|
+
const files = [];
|
|
261
|
+
for (const target of CONFIG_TARGETS.filter((name) => selection.targets.includes(name))) {
|
|
262
|
+
let file;
|
|
263
|
+
let current;
|
|
264
|
+
try {
|
|
265
|
+
file = await resolveTargetFile(target, scope, cwd, home);
|
|
266
|
+
current = file.exists ? await readRegular(file.absolute) : undefined;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const reason = error instanceof LifecycleError ? error.message : "target is unreadable";
|
|
270
|
+
for (const fragment of selection.fragments.filter((item) => applicable(item, target)))
|
|
271
|
+
operations.push({ target, path: "unknown", fragment, operation: "conflict", reason });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (file.exists && current === undefined) {
|
|
275
|
+
for (const fragment of selection.fragments.filter((item) => applicable(item, target)))
|
|
276
|
+
operations.push({
|
|
277
|
+
target,
|
|
278
|
+
path: file.absolute,
|
|
279
|
+
fragment,
|
|
280
|
+
operation: "conflict",
|
|
281
|
+
reason: "target is unreadable",
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
files.push(file);
|
|
286
|
+
allowedRoots.add(file.root);
|
|
287
|
+
let text = current ? current.toString("utf8") : "{}\n";
|
|
288
|
+
let changed = false;
|
|
289
|
+
for (const fragment of selection.fragments) {
|
|
290
|
+
if (!applicable(fragment, target))
|
|
291
|
+
continue;
|
|
292
|
+
let parsed = undefined;
|
|
293
|
+
try {
|
|
294
|
+
parsed = parseJsonc(text);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
parsed = undefined;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const applied = applyJsoncEdits(text, fragmentEdits(fragment, target, parsed));
|
|
301
|
+
if (applied.changed) {
|
|
302
|
+
text = applied.text;
|
|
303
|
+
changed = true;
|
|
304
|
+
operations.push({
|
|
305
|
+
target,
|
|
306
|
+
path: file.absolute,
|
|
307
|
+
fragment,
|
|
308
|
+
operation: file.exists ? "update" : "create",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
operations.push({ target, path: file.absolute, fragment, operation: "unchanged" });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
operations.push({
|
|
317
|
+
target,
|
|
318
|
+
path: file.absolute,
|
|
319
|
+
fragment,
|
|
320
|
+
operation: "conflict",
|
|
321
|
+
reason: error instanceof LifecycleError || error instanceof Error
|
|
322
|
+
? error.message
|
|
323
|
+
: String(error),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (changed) {
|
|
328
|
+
const content = Buffer.from(text.endsWith("\n") ? text : `${text}\n`);
|
|
329
|
+
mutations.push({
|
|
330
|
+
...(file.root === root ? {} : { root: file.root }),
|
|
331
|
+
path: file.path,
|
|
332
|
+
operation: "write",
|
|
333
|
+
content,
|
|
334
|
+
mode: 0o644,
|
|
335
|
+
expected: current ? { sha256: sha256(current) } : { absent: true },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const skipped = selection.fragments
|
|
340
|
+
.filter((fragment) => !selection.targets.some((target) => applicable(fragment, target)))
|
|
341
|
+
.map((fragment) => ({ fragment, reason: "no selected target supports this fragment" }));
|
|
342
|
+
const sorted = operations.sort((left, right) => CONFIG_TARGETS.indexOf(left.target) - CONFIG_TARGETS.indexOf(right.target) ||
|
|
343
|
+
FRAGMENT_NAMES.indexOf(left.fragment) -
|
|
344
|
+
FRAGMENT_NAMES.indexOf(right.fragment) ||
|
|
345
|
+
left.path.localeCompare(right.path));
|
|
346
|
+
const base = {
|
|
347
|
+
schema_version: 1,
|
|
348
|
+
action: "config-setup",
|
|
349
|
+
scope,
|
|
350
|
+
root,
|
|
351
|
+
package_version: requirePackageVersion(),
|
|
352
|
+
selection,
|
|
353
|
+
targets: files.map((file) => ({
|
|
354
|
+
target: file.target,
|
|
355
|
+
path: file.absolute,
|
|
356
|
+
exists: file.exists,
|
|
357
|
+
})),
|
|
358
|
+
operations: sorted,
|
|
359
|
+
skipped_fragments: skipped,
|
|
360
|
+
requires_restart: sorted.some((item) => item.operation === "create" || item.operation === "update"),
|
|
361
|
+
confirmable: mutations.length > 0,
|
|
362
|
+
};
|
|
363
|
+
const planDigest = digest(base);
|
|
364
|
+
return {
|
|
365
|
+
plan: { ...base, plan_digest: planDigest, digest: planDigest },
|
|
366
|
+
mutations,
|
|
367
|
+
allowedRoots: [...allowedRoots],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
371
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
372
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
373
|
+
try {
|
|
374
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
375
|
+
const built = await build(selection, scope, cwd, home);
|
|
376
|
+
if (await recoverTransaction(root, stateRoot, built.allowedRoots))
|
|
377
|
+
throw new ConfigSetupError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
378
|
+
if (!built.plan.confirmable)
|
|
379
|
+
return built.plan;
|
|
380
|
+
const receipt = await saveReceipt(stateRoot, "config-setup", scope, root, { plan_digest: built.plan.plan_digest }, Date.now(), built.plan.plan_digest);
|
|
381
|
+
return {
|
|
382
|
+
...built.plan,
|
|
383
|
+
digest: receipt.digest,
|
|
384
|
+
confirmation_digest: receipt.digest,
|
|
385
|
+
receipt_expires_at: receipt.expires_at,
|
|
386
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (error instanceof ConfigSetupError)
|
|
392
|
+
throw error;
|
|
393
|
+
if (error instanceof LifecycleError)
|
|
394
|
+
throw new ConfigSetupError(error.code, error.message);
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
export async function applyConfigSetup(selection, scope, confirmationDigest, cwd = process.cwd(), home = homedir()) {
|
|
399
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
400
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
401
|
+
try {
|
|
402
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
403
|
+
const built = await build(selection, scope, cwd, home);
|
|
404
|
+
if (await recoverTransaction(root, stateRoot, built.allowedRoots))
|
|
405
|
+
throw new ConfigSetupError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
406
|
+
const receipt = (await consumeReceipt(stateRoot, {
|
|
407
|
+
digest: confirmationDigest,
|
|
408
|
+
kind: "config-setup",
|
|
409
|
+
scope,
|
|
410
|
+
root,
|
|
411
|
+
}));
|
|
412
|
+
if (built.plan.plan_digest !== (receipt.plan_digest ?? digest(receipt)))
|
|
413
|
+
throw new ConfigSetupError("stale_plan", "Config setup plan changed after preview");
|
|
414
|
+
if (!built.plan.confirmable)
|
|
415
|
+
throw new ConfigSetupError("invalid_state", "Config setup plan has no applicable changes");
|
|
416
|
+
await applyTransaction(root, stateRoot, built.mutations, {
|
|
417
|
+
validateFinal: async () => {
|
|
418
|
+
for (const mutation of built.mutations) {
|
|
419
|
+
if (mutation.operation !== "write")
|
|
420
|
+
continue;
|
|
421
|
+
const target = join(mutation.root ?? root, mutation.path);
|
|
422
|
+
const content = await readRegular(target);
|
|
423
|
+
if (!content || !content.equals(mutation.content))
|
|
424
|
+
throw new ConfigSetupError("final_validation_failed", `Written config failed final validation: ${mutation.path}`);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
return { ...built.plan, digest: confirmationDigest };
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (error instanceof ConfigSetupError)
|
|
433
|
+
throw error;
|
|
434
|
+
if (error instanceof LifecycleError)
|
|
435
|
+
throw new ConfigSetupError(error.code, error.message);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ExecutionCard } from "./routing.js";
|
|
2
|
+
export declare const CONTRACT_SCHEMA_VERSION: 1;
|
|
3
|
+
export type AgentReportStatus = "COMPLETED" | "BLOCKED" | "FAILED" | "REJECTED_PLAN" | "APPROVED" | "CHANGES_REQUIRED";
|
|
4
|
+
export type MapperReport = {
|
|
5
|
+
mapper_report: {
|
|
6
|
+
schema_version: 1;
|
|
7
|
+
paths: Array<Record<string, string>>;
|
|
8
|
+
callers: Array<Record<string, string>>;
|
|
9
|
+
tests: Array<Record<string, string>>;
|
|
10
|
+
patterns: Array<Record<string, string>>;
|
|
11
|
+
evidence_gaps: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type WorkerReport = {
|
|
15
|
+
worker_report: {
|
|
16
|
+
schema_version: 1;
|
|
17
|
+
status: Extract<AgentReportStatus, "COMPLETED" | "BLOCKED" | "FAILED" | "REJECTED_PLAN">;
|
|
18
|
+
card_id: string | null;
|
|
19
|
+
revision: number | null;
|
|
20
|
+
changed_files: string[];
|
|
21
|
+
checks: Array<{
|
|
22
|
+
command: string;
|
|
23
|
+
status: string;
|
|
24
|
+
}>;
|
|
25
|
+
writes_performed: boolean;
|
|
26
|
+
risks: string[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type ReviewReport = {
|
|
30
|
+
review_report: {
|
|
31
|
+
schema_version: 1;
|
|
32
|
+
status: "APPROVED" | "CHANGES_REQUIRED";
|
|
33
|
+
target: string;
|
|
34
|
+
findings: Array<Record<string, unknown>>;
|
|
35
|
+
evidence: string[];
|
|
36
|
+
checks: string[];
|
|
37
|
+
risks: string[];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export type CriticReport = {
|
|
41
|
+
critic_report: {
|
|
42
|
+
schema_version: 1;
|
|
43
|
+
status: "APPROVED" | "CHANGES_REQUIRED";
|
|
44
|
+
card_id: string;
|
|
45
|
+
revision: number;
|
|
46
|
+
findings: Array<Record<string, unknown>>;
|
|
47
|
+
evidence: string[];
|
|
48
|
+
unrun_checks: string[];
|
|
49
|
+
risks: string[];
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export declare function validateAgentReport(agent: string, report: unknown, card?: ExecutionCard): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { validateExecutionCard } from "./routing.js";
|
|
2
|
+
export const CONTRACT_SCHEMA_VERSION = 1;
|
|
3
|
+
function object(value) {
|
|
4
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
5
|
+
}
|
|
6
|
+
function strings(value) {
|
|
7
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
8
|
+
}
|
|
9
|
+
export function validateAgentReport(agent, report, card) {
|
|
10
|
+
if (!object(report))
|
|
11
|
+
throw new Error("agent report must be an object");
|
|
12
|
+
if (agent === "critic" && !card && "review_report" in report) {
|
|
13
|
+
validateAgentReport("review", report);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const reportKey = agent === "architect" ? "execution_card" : `${agent}_report`;
|
|
17
|
+
if (Object.keys(report).length !== 1 || !(reportKey in report))
|
|
18
|
+
throw new Error(`${agent} report has an invalid envelope`);
|
|
19
|
+
const value = report[reportKey];
|
|
20
|
+
if (!object(value))
|
|
21
|
+
throw new Error(`missing ${agent}_report`);
|
|
22
|
+
if (value.schema_version !== CONTRACT_SCHEMA_VERSION)
|
|
23
|
+
throw new Error(`${agent} report schema version is unsupported`);
|
|
24
|
+
if (agent === "mapper") {
|
|
25
|
+
for (const key of ["paths", "callers", "tests", "patterns"])
|
|
26
|
+
if (!Array.isArray(value[key]))
|
|
27
|
+
throw new Error(`mapper report field is invalid: ${key}`);
|
|
28
|
+
if (Object.keys(value).some((key) => !["schema_version", "paths", "callers", "tests", "patterns", "evidence_gaps"].includes(key)))
|
|
29
|
+
throw new Error("mapper report contains unknown fields");
|
|
30
|
+
if (!strings(value.evidence_gaps))
|
|
31
|
+
throw new Error("mapper report evidence_gaps is invalid");
|
|
32
|
+
}
|
|
33
|
+
else if (agent === "architect") {
|
|
34
|
+
if (value.status === "NEEDS_EVIDENCE")
|
|
35
|
+
return;
|
|
36
|
+
const result = validateExecutionCard(value);
|
|
37
|
+
if (!result.valid)
|
|
38
|
+
throw new Error(`architect execution card is invalid: ${result.failedField}`);
|
|
39
|
+
}
|
|
40
|
+
else if (agent === "worker") {
|
|
41
|
+
if (!strings(value.changed_files) ||
|
|
42
|
+
!Array.isArray(value.checks) ||
|
|
43
|
+
typeof value.writes_performed !== "boolean" ||
|
|
44
|
+
!strings(value.risks))
|
|
45
|
+
throw new Error("worker report is invalid");
|
|
46
|
+
if (Object.keys(value).some((key) => ![
|
|
47
|
+
"schema_version",
|
|
48
|
+
"status",
|
|
49
|
+
"card_id",
|
|
50
|
+
"revision",
|
|
51
|
+
"changed_files",
|
|
52
|
+
"checks",
|
|
53
|
+
"writes_performed",
|
|
54
|
+
"risks",
|
|
55
|
+
].includes(key)))
|
|
56
|
+
throw new Error("worker report contains unknown fields");
|
|
57
|
+
if (value.status === "COMPLETED" &&
|
|
58
|
+
(value.writes_performed !== true ||
|
|
59
|
+
value.checks.some((check) => check.status !== "passed")))
|
|
60
|
+
throw new Error("completed worker report has incomplete checks");
|
|
61
|
+
if (card && value.changed_files.some((path) => !card.write_set.includes(path)))
|
|
62
|
+
throw new Error("worker report writes outside execution card");
|
|
63
|
+
}
|
|
64
|
+
else if (agent === "review" || agent === "critic") {
|
|
65
|
+
if (!["APPROVED", "CHANGES_REQUIRED"].includes(String(value.status)) ||
|
|
66
|
+
!Array.isArray(value.findings) ||
|
|
67
|
+
(typeof value.target !== "string" && agent === "review"))
|
|
68
|
+
throw new Error(`${agent} report is invalid`);
|
|
69
|
+
if (agent === "review" &&
|
|
70
|
+
(!Array.isArray(value.checks) ||
|
|
71
|
+
!strings(value.checks) ||
|
|
72
|
+
!strings(value.risks) ||
|
|
73
|
+
!strings(value.evidence) ||
|
|
74
|
+
!value.target))
|
|
75
|
+
throw new Error("review report fields are invalid");
|
|
76
|
+
if (agent === "critic" &&
|
|
77
|
+
(typeof value.card_id !== "string" ||
|
|
78
|
+
!Number.isInteger(value.revision) ||
|
|
79
|
+
!strings(value.unrun_checks) ||
|
|
80
|
+
!strings(value.risks) ||
|
|
81
|
+
!strings(value.evidence)))
|
|
82
|
+
throw new Error("critic report fields are invalid");
|
|
83
|
+
const allowed = agent === "review"
|
|
84
|
+
? ["schema_version", "status", "target", "findings", "evidence", "checks", "risks"]
|
|
85
|
+
: [
|
|
86
|
+
"schema_version",
|
|
87
|
+
"status",
|
|
88
|
+
"card_id",
|
|
89
|
+
"revision",
|
|
90
|
+
"findings",
|
|
91
|
+
"evidence",
|
|
92
|
+
"unrun_checks",
|
|
93
|
+
"risks",
|
|
94
|
+
];
|
|
95
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
96
|
+
throw new Error(`${agent} report contains unknown fields`);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
throw new Error(`unknown report agent: ${agent}`);
|
|
100
|
+
if (card && (value.card_id !== card.card_id || value.revision !== card.revision))
|
|
101
|
+
throw new Error("agent report does not match execution card");
|
|
102
|
+
}
|