@microck/canonfig 2.0.0 → 2.1.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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.js +1 -1
  3. package/dist/harness-configuration/adapters/amp.js +87 -0
  4. package/dist/harness-configuration/adapters/antigravity.js +50 -0
  5. package/dist/harness-configuration/adapters/claude.js +43 -0
  6. package/dist/harness-configuration/adapters/codex.js +60 -0
  7. package/dist/harness-configuration/adapters/copilot.js +79 -0
  8. package/dist/harness-configuration/adapters/cursor.js +67 -0
  9. package/dist/harness-configuration/adapters/descriptor.js +10 -0
  10. package/dist/harness-configuration/adapters/devin.js +66 -0
  11. package/dist/harness-configuration/adapters/droid.js +32 -0
  12. package/dist/harness-configuration/adapters/grok.js +44 -0
  13. package/dist/harness-configuration/adapters/hermes.js +105 -0
  14. package/dist/harness-configuration/adapters/index.js +50 -0
  15. package/dist/harness-configuration/adapters/kilo.js +18 -0
  16. package/dist/harness-configuration/adapters/kimi.js +148 -0
  17. package/dist/harness-configuration/adapters/omp.js +64 -0
  18. package/dist/harness-configuration/adapters/open-code-family.js +94 -0
  19. package/dist/harness-configuration/adapters/opencode.js +18 -0
  20. package/dist/harness-configuration/adapters/pi.js +93 -0
  21. package/dist/harness-configuration/adapters/qwen.js +164 -0
  22. package/dist/harness-configuration/adapters/shared-common.js +66 -0
  23. package/dist/harness-configuration/adapters/shared-documents.js +117 -0
  24. package/dist/harness-configuration/adapters/shared-hooks.js +186 -0
  25. package/dist/harness-configuration/adapters/shared-mcp.js +214 -0
  26. package/dist/harness-configuration/adapters/shared.js +4 -0
  27. package/dist/harness-configuration/adapters/tools.js +24 -0
  28. package/dist/harness-configuration/cli-arguments.js +105 -0
  29. package/dist/harness-configuration/cli-output.js +77 -0
  30. package/dist/harness-configuration/cli.js +196 -0
  31. package/dist/harness-configuration/core/compiler.js +175 -0
  32. package/dist/harness-configuration/core/config.js +74 -0
  33. package/dist/harness-configuration/core/diff.js +60 -0
  34. package/dist/harness-configuration/core/doctor.js +40 -0
  35. package/dist/harness-configuration/core/errors.js +13 -0
  36. package/dist/harness-configuration/core/filesystem.js +172 -0
  37. package/dist/harness-configuration/core/frontmatter.js +49 -0
  38. package/dist/harness-configuration/core/hash.js +4 -0
  39. package/dist/harness-configuration/core/path.js +50 -0
  40. package/dist/harness-configuration/core/planner.js +255 -0
  41. package/dist/harness-configuration/core/render-cleanup.js +91 -0
  42. package/dist/harness-configuration/core/render-json.js +195 -0
  43. package/dist/harness-configuration/core/render-text.js +134 -0
  44. package/dist/harness-configuration/core/render-utils.js +202 -0
  45. package/dist/harness-configuration/core/render.js +54 -0
  46. package/dist/harness-configuration/core/scaffold.js +103 -0
  47. package/dist/harness-configuration/core/schema-components.js +167 -0
  48. package/dist/harness-configuration/core/schema-config.js +98 -0
  49. package/dist/harness-configuration/core/schema-runtime.js +143 -0
  50. package/dist/harness-configuration/core/schema-types.js +8 -0
  51. package/dist/harness-configuration/core/schema.js +13 -0
  52. package/dist/harness-configuration/core/state.js +42 -0
  53. package/dist/harness-configuration/core/types.js +5 -0
  54. package/dist/harness-configuration/core/validation.js +113 -0
  55. package/dist/harness-configuration/templates/runtime.js +212 -0
  56. package/dist/runtime/main.js +20 -14
  57. package/package.json +5 -5
@@ -0,0 +1,143 @@
1
+ export class SchemaValidationError extends Error {
2
+ issues;
3
+ constructor(issues) {
4
+ super(issues
5
+ .map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
6
+ .join("\n"));
7
+ this.name = "SchemaValidationError";
8
+ this.issues = issues;
9
+ }
10
+ }
11
+ export class Validator {
12
+ issues = [];
13
+ issue(path, message) {
14
+ this.issues.push({ path: [...path], message });
15
+ }
16
+ finish(value) {
17
+ if (this.issues.length > 0)
18
+ throw new SchemaValidationError(this.issues);
19
+ return value;
20
+ }
21
+ }
22
+ export function schema(parser) {
23
+ return {
24
+ parse(input) {
25
+ const validator = new Validator();
26
+ return validator.finish(parser(input, validator, []));
27
+ },
28
+ safeParse(input) {
29
+ try {
30
+ return { success: true, data: this.parse(input) };
31
+ }
32
+ catch (error) {
33
+ if (error instanceof SchemaValidationError) {
34
+ return { success: false, error };
35
+ }
36
+ throw error;
37
+ }
38
+ },
39
+ };
40
+ }
41
+ export function isRecord(input) {
42
+ return input !== null && typeof input === "object" && !Array.isArray(input);
43
+ }
44
+ export function objectValue(input, validator, path) {
45
+ if (!isRecord(input)) {
46
+ validator.issue(path, "Expected an object.");
47
+ return {};
48
+ }
49
+ return input;
50
+ }
51
+ export function stringValue(input, validator, path, options = {}) {
52
+ if (typeof input !== "string") {
53
+ validator.issue(path, "Expected a string.");
54
+ return "";
55
+ }
56
+ if (options.min !== undefined && input.length < options.min) {
57
+ validator.issue(path, `Expected at least ${options.min} character(s).`);
58
+ }
59
+ if (options.pattern && !options.pattern.test(input)) {
60
+ validator.issue(path, "Invalid format.");
61
+ }
62
+ return input;
63
+ }
64
+ export function optionalString(input, validator, path) {
65
+ return input === undefined ? undefined : stringValue(input, validator, path);
66
+ }
67
+ export function booleanValue(input, validator, path, fallback) {
68
+ if (input === undefined)
69
+ return fallback;
70
+ if (typeof input !== "boolean") {
71
+ validator.issue(path, "Expected a boolean.");
72
+ return fallback;
73
+ }
74
+ return input;
75
+ }
76
+ export function positiveInteger(input, validator, path, max) {
77
+ if (input === undefined)
78
+ return undefined;
79
+ if (typeof input !== "number"
80
+ || !Number.isInteger(input)
81
+ || input <= 0
82
+ || (max !== undefined && input > max)) {
83
+ validator.issue(path, max === undefined
84
+ ? "Expected a positive integer."
85
+ : `Expected a positive integer no greater than ${max}.`);
86
+ return undefined;
87
+ }
88
+ return input;
89
+ }
90
+ export function enumValue(input, values, validator, path, fallback) {
91
+ if (typeof input === "string" && values.includes(input)) {
92
+ return input;
93
+ }
94
+ validator.issue(path, `Expected one of: ${values.join(", ")}.`);
95
+ return fallback;
96
+ }
97
+ export function stringArray(input, validator, path, fallback = []) {
98
+ if (input === undefined)
99
+ return [...fallback];
100
+ if (!Array.isArray(input)) {
101
+ validator.issue(path, "Expected an array of strings.");
102
+ return [...fallback];
103
+ }
104
+ return input.map((value, index) => stringValue(value, validator, [...path, index], { min: 1 }));
105
+ }
106
+ export function relativePath(input, validator, path, fallback) {
107
+ if (input === undefined && fallback !== undefined)
108
+ return fallback;
109
+ const value = stringValue(input, validator, path, { min: 1 });
110
+ if (value.startsWith("/")
111
+ || value.startsWith("\\")
112
+ || /^[A-Za-z]:[\\/]/u.test(value)
113
+ || value.split(/[\\/]+/u).includes("..")) {
114
+ validator.issue(path, "Path must stay inside .canonfig/.");
115
+ }
116
+ return value;
117
+ }
118
+ const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
119
+ export function idValue(input, validator, path) {
120
+ return stringValue(input, validator, path, {
121
+ min: 1,
122
+ pattern: ID_PATTERN,
123
+ });
124
+ }
125
+ export function secretValue(input, validator, path) {
126
+ if (typeof input === "string")
127
+ return input;
128
+ const value = objectValue(input, validator, path);
129
+ const fromEnv = stringValue(value.fromEnv, validator, [...path, "fromEnv"], { min: 1 });
130
+ const fallback = optionalString(value.default, validator, [...path, "default"]);
131
+ return fallback === undefined
132
+ ? { fromEnv }
133
+ : { fromEnv, default: fallback };
134
+ }
135
+ export function secretRecord(input, validator, path) {
136
+ if (input === undefined)
137
+ return {};
138
+ const value = objectValue(input, validator, path);
139
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
140
+ key,
141
+ secretValue(item, validator, [...path, key]),
142
+ ]));
143
+ }
@@ -0,0 +1,8 @@
1
+ export const HOOK_EVENTS = [
2
+ "session_start", "session_end", "prompt_submit", "before_agent", "after_agent",
3
+ "before_tool", "after_tool", "before_compact", "after_compact", "stop",
4
+ "subagent_start", "subagent_stop",
5
+ ];
6
+ export const CAPABILITIES = [
7
+ "read", "write", "search", "shell", "web", "mcp", "subagent", "test", "git",
8
+ ];
@@ -0,0 +1,13 @@
1
+ import { TARGET_IDS } from "./types.js";
2
+ import { enumValue, schema, } from "./schema-runtime.js";
3
+ import { parseAgent, parseCommand, parseHook, parseMcpServer, parseRule, } from "./schema-components.js";
4
+ import { parseConfig } from "./schema-config.js";
5
+ export * from "./schema-types.js";
6
+ export * from "./schema-runtime.js";
7
+ export const TargetIdSchema = schema((input, validator, path) => enumValue(input, TARGET_IDS, validator, path, "codex"));
8
+ export const McpServerSchema = schema(parseMcpServer);
9
+ export const HookSchema = schema(parseHook);
10
+ export const RuleSchema = schema(parseRule);
11
+ export const AgentSchema = schema(parseAgent);
12
+ export const CommandSchema = schema(parseCommand);
13
+ export const CanonfigConfigSchema = schema(parseConfig);
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CANONFIG_DIR, STATE_FILENAME } from "./config.js";
4
+ import { CanonfigError } from "./errors.js";
5
+ import { assertNoSymlinkPathComponents, atomicWrite } from "./filesystem.js";
6
+ export const HARNESS_CONFIGURATION_VERSION = "1";
7
+ export function emptyState() {
8
+ return { version: 1, generatedAt: new Date(0).toISOString(), canonfigVersion: HARNESS_CONFIGURATION_VERSION, artifacts: {} };
9
+ }
10
+ export async function loadState(root) {
11
+ const statePath = path.join(root, CANONFIG_DIR, STATE_FILENAME);
12
+ try {
13
+ const raw = await fs.readFile(statePath, "utf8");
14
+ const parsed = JSON.parse(raw);
15
+ if (parsed.version !== 1 || !parsed.artifacts || typeof parsed.artifacts !== "object") {
16
+ throw new CanonfigError("STATE_INVALID", `Unsupported state file: ${path.relative(root, statePath)}`);
17
+ }
18
+ return parsed;
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return emptyState();
23
+ if (error instanceof CanonfigError)
24
+ throw error;
25
+ throw new CanonfigError("STATE_INVALID", `Could not read ${path.relative(root, statePath)}: ${String(error)}`, error);
26
+ }
27
+ }
28
+ export async function writeState(root, state) {
29
+ const statePath = path.join(root, CANONFIG_DIR, STATE_FILENAME);
30
+ if (Object.keys(state.artifacts).length === 0) {
31
+ await assertNoSymlinkPathComponents(root, path.dirname(statePath));
32
+ try {
33
+ await fs.unlink(statePath);
34
+ }
35
+ catch (error) {
36
+ if (error.code !== "ENOENT")
37
+ throw error;
38
+ }
39
+ return;
40
+ }
41
+ await atomicWrite(statePath, `${JSON.stringify(state, null, 2)}\n`, 0o600, root);
42
+ }
@@ -0,0 +1,5 @@
1
+ export const TARGET_IDS = [
2
+ "codex", "claude-code", "amp", "oh-my-pi", "pi", "factory-droid",
3
+ "cursor", "devin", "opencode", "grok-build", "antigravity", "copilot-cli",
4
+ "kimi", "kilo", "hermes", "qwen",
5
+ ];
@@ -0,0 +1,113 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parseSkill } from "./frontmatter.js";
4
+ import { walkFiles } from "./filesystem.js";
5
+ import { assertSafeRelativePath } from "./path.js";
6
+ async function exists(filePath) {
7
+ try {
8
+ await fs.access(filePath);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function validateProject(root, config) {
16
+ const diagnostics = [];
17
+ const canonfigDir = path.join(root, ".canonfig");
18
+ const referenced = [
19
+ { kind: "instruction", relative: config.instructions.root },
20
+ ...config.instructions.rules.map((rule) => ({ kind: `rule ${rule.id}`, relative: rule.file })),
21
+ ...config.agents.map((agent) => ({ kind: `agent ${agent.id}`, relative: agent.file })),
22
+ ...config.commands.map((command) => ({ kind: `command ${command.id}`, relative: command.file })),
23
+ ];
24
+ for (const item of referenced) {
25
+ const safe = assertSafeRelativePath(item.relative);
26
+ if (!await exists(path.join(canonfigDir, safe))) {
27
+ diagnostics.push({
28
+ level: "error",
29
+ code: "SOURCE_MISSING",
30
+ path: `.canonfig/${safe}`,
31
+ message: `Missing source for ${item.kind}: .canonfig/${safe}`,
32
+ });
33
+ }
34
+ }
35
+ const skillNames = new Map();
36
+ for (const rootRelative of config.skills.roots) {
37
+ const safeRoot = assertSafeRelativePath(rootRelative);
38
+ const absoluteRoot = path.join(canonfigDir, safeRoot);
39
+ if (!await exists(absoluteRoot)) {
40
+ diagnostics.push({
41
+ level: "info",
42
+ code: "SKILL_ROOT_MISSING",
43
+ path: `.canonfig/${safeRoot}`,
44
+ message: `Skill root .canonfig/${safeRoot} does not exist; it contributes no skills.`,
45
+ });
46
+ continue;
47
+ }
48
+ const manifests = (await walkFiles(absoluteRoot)).filter((file) => path.basename(file) === "SKILL.md");
49
+ for (const manifest of manifests.sort()) {
50
+ const relative = `.canonfig/${safeRoot}/${manifest}`;
51
+ try {
52
+ const source = await fs.readFile(path.join(absoluteRoot, manifest), "utf8");
53
+ const parsed = parseSkill(source);
54
+ const directoryName = path.basename(path.dirname(manifest));
55
+ if (parsed.data.name !== directoryName) {
56
+ diagnostics.push({
57
+ level: "warning",
58
+ code: "SKILL_NAME_DIRECTORY_MISMATCH",
59
+ path: relative,
60
+ message: `Skill name ${parsed.data.name} does not match its directory ${directoryName}.`,
61
+ });
62
+ }
63
+ const previous = skillNames.get(parsed.data.name);
64
+ if (previous) {
65
+ diagnostics.push({
66
+ level: "error",
67
+ code: "SKILL_NAME_DUPLICATE",
68
+ path: relative,
69
+ message: `Skill name ${parsed.data.name} is duplicated by ${previous} and ${relative}.`,
70
+ });
71
+ }
72
+ else {
73
+ skillNames.set(parsed.data.name, relative);
74
+ }
75
+ }
76
+ catch (error) {
77
+ diagnostics.push({
78
+ level: "error",
79
+ code: "SKILL_INVALID",
80
+ path: relative,
81
+ message: `Invalid Agent Skill manifest ${relative}: ${error instanceof Error ? error.message : String(error)}`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ for (const hook of config.hooks) {
87
+ if (hook.matcher.inputRegex) {
88
+ try {
89
+ new RegExp(hook.matcher.inputRegex);
90
+ }
91
+ catch (error) {
92
+ diagnostics.push({
93
+ level: "error",
94
+ code: "HOOK_REGEX_INVALID",
95
+ message: `Hook ${hook.id} has an invalid inputRegex: ${error instanceof Error ? error.message : String(error)}`,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ for (const [name, server] of Object.entries(config.mcp.servers)) {
101
+ const values = server.transport === "stdio" ? Object.values(server.env) : Object.values(server.headers);
102
+ for (const value of values) {
103
+ if (typeof value !== "string" && value.default !== undefined) {
104
+ diagnostics.push({
105
+ level: "warning",
106
+ code: "SECRET_DEFAULT_PRESENT",
107
+ message: `MCP server ${name} gives ${value.fromEnv} a default value. Generated files may therefore contain a credential-like literal.`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ return diagnostics;
113
+ }
@@ -0,0 +1,212 @@
1
+ export const AMP_PLUGIN_EVENT_MAP = {
2
+ before_tool: "tool.call",
3
+ after_tool: "tool.result",
4
+ session_start: "session.start",
5
+ before_agent: "agent.start",
6
+ after_agent: "agent.end",
7
+ };
8
+ export const PI_PLUGIN_EVENT_MAP = {
9
+ before_tool: "tool_call",
10
+ after_tool: "tool_result",
11
+ session_start: "session_start",
12
+ session_end: "session_shutdown",
13
+ before_agent: "before_agent_start",
14
+ before_compact: "session_before_compact",
15
+ stop: "agent_end",
16
+ };
17
+ export function hookRegistryJson(hooks) {
18
+ return `${JSON.stringify({ version: 1, hooks }, null, 2)}\n`;
19
+ }
20
+ export function hookRunnerSource() {
21
+ return `#!/usr/bin/env node
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { spawnSync } from "node:child_process";
26
+
27
+ const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
28
+ const repoRoot = path.resolve(runtimeDir, "../..");
29
+ let registry;
30
+ try { registry = JSON.parse(fs.readFileSync(path.join(runtimeDir, "hooks.json"), "utf8")); }
31
+ catch { process.exit(0); }
32
+ const hooks = Array.isArray(registry?.hooks) ? registry.hooks : [];
33
+ const args = process.argv.slice(2);
34
+ const arg = (name) => { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; };
35
+ const hookId = arg("--hook");
36
+ const target = arg("--target") || "unknown";
37
+ const event = arg("--event") || "unknown";
38
+ const hook = hooks.find((candidate) => candidate.id === hookId && candidate.enabled !== false);
39
+ if (!hook) process.exit(0);
40
+
41
+ let rawText = "";
42
+ try { rawText = fs.readFileSync(0, "utf8"); } catch { rawText = ""; }
43
+ let raw;
44
+ try { raw = rawText.trim() ? JSON.parse(rawText) : {}; } catch { raw = { raw: rawText }; }
45
+ const toolName = String(raw.tool_name ?? raw.toolName ?? raw.tool ?? raw.name ?? raw.input?.tool ?? raw.input?.toolName ?? "");
46
+ const toolInput = raw.tool_input ?? raw.toolInput ?? raw.input ?? raw.args ?? {};
47
+ const serialized = JSON.stringify({ toolName, toolInput, raw });
48
+
49
+ const capabilityPatterns = {
50
+ shell: /(bash|shell|terminal|execute|exec|command|run_command)/i,
51
+ read: /(read|view|cat|glob|grep|search|list|find)/i,
52
+ write: /(write|edit|patch|create|delete|move|replace)/i,
53
+ search: /(grep|glob|search|find|list)/i,
54
+ web: /(web|fetch|browser|http)/i,
55
+ mcp: /(mcp|server)/i,
56
+ subagent: /(task|agent|subagent)/i,
57
+ test: /(test|spec|check|verify)/i,
58
+ git: /(git|commit|branch|diff|push|pull)/i,
59
+ };
60
+ const matchesTool = !hook.matcher?.tools?.length || hook.matcher.tools.some((name) => {
61
+ if (name === toolName) return true;
62
+ try { return new RegExp(name).test(toolName); } catch { return false; }
63
+ });
64
+ const matchesCapability = !hook.matcher?.capabilities?.length || hook.matcher.capabilities.some((capability) => capabilityPatterns[capability]?.test(toolName));
65
+ let matchesInput = true;
66
+ if (hook.matcher?.inputRegex) {
67
+ try { matchesInput = new RegExp(hook.matcher.inputRegex).test(serialized); }
68
+ catch (error) { console.error(\`Invalid inputRegex for hook \${hook.id}: \${error.message}\`); process.exit(2); }
69
+ }
70
+ if (!matchesTool || !matchesCapability || !matchesInput) process.exit(0);
71
+
72
+ const normalized = {
73
+ version: 1,
74
+ hookId: hook.id,
75
+ target,
76
+ event,
77
+ repositoryRoot: repoRoot,
78
+ toolName,
79
+ toolInput,
80
+ raw,
81
+ };
82
+ const [command, ...commandArgs] = hook.run;
83
+ const result = spawnSync(command, commandArgs, {
84
+ cwd: repoRoot,
85
+ env: {
86
+ ...process.env,
87
+ CANONFIG_ROOT: repoRoot,
88
+ CANONFIG_HOOK_ID: hook.id,
89
+ CANONFIG_TARGET: target,
90
+ CANONFIG_EVENT: event,
91
+ CANONFIG_TOOL_NAME: toolName,
92
+ },
93
+ input: JSON.stringify(normalized),
94
+ encoding: "utf8",
95
+ timeout: hook.timeoutMs,
96
+ maxBuffer: 16 * 1024 * 1024,
97
+ });
98
+
99
+ let output;
100
+ try { output = result.stdout?.trim() ? JSON.parse(result.stdout) : undefined; } catch { output = undefined; }
101
+ const reason = String(output?.reason ?? result.stderr?.trim() ?? result.error?.message ?? \`Canonfig hook \${hook.id} failed\`);
102
+ const denied = result.status === 2 || output?.decision === "deny" || output?.permission === "deny" || output?.block === true;
103
+ const failed = result.error || result.status === null || (result.status !== 0 && result.status !== 2);
104
+ if (denied || (failed && hook.onFailure === "block")) {
105
+ console.error(reason);
106
+ process.exit(2);
107
+ }
108
+ if (failed && hook.onFailure === "warn") console.error(reason);
109
+ if (output?.message) console.log(String(output.message));
110
+ else if (result.stdout && !output) process.stdout.write(result.stdout);
111
+ process.exit(0);
112
+ `;
113
+ }
114
+ function pluginPreamble(target) {
115
+ return `// Generated by Canonfig. Edit .canonfig/harness.yaml instead.
116
+ import fs from "node:fs";
117
+ import path from "node:path";
118
+ import { fileURLToPath } from "node:url";
119
+ import { execFile } from "node:child_process";
120
+
121
+ function findRoot() {
122
+ let current = path.dirname(fileURLToPath(import.meta.url));
123
+ while (true) {
124
+ if (fs.existsSync(path.join(current, ".canonfig", ".runtime", "hook-runner.mjs"))) return current;
125
+ const parent = path.dirname(current);
126
+ if (parent === current) return process.cwd();
127
+ current = parent;
128
+ }
129
+ }
130
+ const root = findRoot();
131
+ function runCanonfig(hookId, event, payload, timeoutMs) {
132
+ return new Promise((resolve) => {
133
+ const runner = path.join(root, ".canonfig", ".runtime", "hook-runner.mjs");
134
+ const child = execFile(
135
+ process.execPath,
136
+ [runner, "--hook", hookId, "--target", "${target}", "--event", event],
137
+ {
138
+ cwd: root,
139
+ encoding: "utf8",
140
+ timeout: Math.max(1, timeoutMs + 1000),
141
+ maxBuffer: 16 * 1024 * 1024,
142
+ },
143
+ (error, stdout, stderr) => {
144
+ const reason = String(stderr || stdout || error?.message || "Blocked by Canonfig hook").trim();
145
+ resolve({ blocked: error !== null, reason });
146
+ },
147
+ );
148
+ child.stdin?.on("error", () => {});
149
+ child.stdin?.end(JSON.stringify(payload ?? {}));
150
+ });
151
+ }
152
+ `;
153
+ }
154
+ export function ampPluginSource(hooks, agents = []) {
155
+ const enabled = hooks.filter((hook) => hook.enabled);
156
+ const registrations = enabled.map((hook) => {
157
+ const event = AMP_PLUGIN_EVENT_MAP[hook.event];
158
+ if (!event)
159
+ return "";
160
+ const rejection = event === "tool.call"
161
+ ? "if (result.blocked) return { action: \"reject-and-continue\", message: result.reason };"
162
+ : "if (result.blocked) throw new Error(result.reason);";
163
+ return ` amp.on(${JSON.stringify(event)}, async (payload) => { const result = await runCanonfig(${JSON.stringify(hook.id)}, ${JSON.stringify(hook.event)}, payload, ${hook.timeoutMs}); ${rejection} });`;
164
+ }).filter(Boolean);
165
+ const agentRegistrations = agents.flatMap(({ agent, content, tools }) => {
166
+ const variable = `canonfigAgent_${agent.id.replaceAll(/[^A-Za-z0-9_$]/g, "_")}`;
167
+ const toolName = `canonfig_${agent.id.replaceAll(/[^A-Za-z0-9_]/g, "_")}_subagent`;
168
+ const definition = [
169
+ ` const ${variable} = amp.createAgent({`,
170
+ ` name: ${JSON.stringify(agent.id)},`,
171
+ ...(agent.model === "inherit" ? [] : [` model: ${JSON.stringify(agent.model)},`]),
172
+ ` instructions: ${JSON.stringify(content.trim())},`,
173
+ ` tools: ${tools?.length ? JSON.stringify(tools) : JSON.stringify("all")},`,
174
+ ` display: { label: ${JSON.stringify(agent.id)} },`,
175
+ " });",
176
+ "",
177
+ " amp.registerTool({",
178
+ ` name: ${JSON.stringify(toolName)},`,
179
+ ` description: ${JSON.stringify(agent.description)},`,
180
+ " inputSchema: { type: \"object\", properties: { request: { type: \"string\" } }, required: [\"request\"] },",
181
+ " async execute(input, ctx) {",
182
+ " const request = typeof input.request === \"string\" ? input.request : \"\";",
183
+ " if (!request.trim()) return \"Missing subagent request.\";",
184
+ ` const result = await ${variable}.run(request, { parentThreadID: ctx.thread.id, timeoutMs: 10 * 60 * 1000 });`,
185
+ " return result.text;",
186
+ " },",
187
+ " });",
188
+ ];
189
+ return definition;
190
+ });
191
+ return `${pluginPreamble("amp")}\nimport type { PluginAPI } from "@ampcode/plugin";\n\nexport default function canonfig(amp: PluginAPI) {\n${[...registrations, ...agentRegistrations].join("\n")}\n}\n`;
192
+ }
193
+ export function piPluginSource(target, hooks) {
194
+ const registrations = hooks.filter((hook) => hook.enabled).map((hook) => {
195
+ const event = PI_PLUGIN_EVENT_MAP[hook.event];
196
+ if (!event)
197
+ return "";
198
+ const result = hook.event === "before_tool"
199
+ ? "if (result.blocked) return { block: true, reason: result.reason };"
200
+ : "if (result.blocked) throw new Error(result.reason);";
201
+ return ` pi.on(${JSON.stringify(event)}, async (payload) => { const result = await runCanonfig(${JSON.stringify(hook.id)}, ${JSON.stringify(hook.event)}, payload, ${hook.timeoutMs}); ${result} });`;
202
+ }).filter(Boolean).join("\n");
203
+ const packageName = target === "pi" ? "@earendil-works/pi-coding-agent" : "@oh-my-pi/pi-coding-agent";
204
+ return `${pluginPreamble(target)}\nimport type { ExtensionAPI } from ${JSON.stringify(packageName)};\n\nexport default function canonfig(pi: ExtensionAPI) {\n${registrations}\n}\n`;
205
+ }
206
+ export function openCodePluginSource(target, hooks) {
207
+ const before = hooks.filter((hook) => hook.enabled && hook.event === "before_tool");
208
+ const after = hooks.filter((hook) => hook.enabled && hook.event === "after_tool");
209
+ const beforeBody = before.map((hook) => ` { const result = await runCanonfig(${JSON.stringify(hook.id)}, "before_tool", { ...input, ...output }, ${hook.timeoutMs}); if (result.blocked) throw new Error(result.reason); }`).join("\n");
210
+ const afterBody = after.map((hook) => ` { const result = await runCanonfig(${JSON.stringify(hook.id)}, "after_tool", { ...input, ...output }, ${hook.timeoutMs}); if (result.blocked) throw new Error(result.reason); }`).join("\n");
211
+ return `${pluginPreamble(target)}\nexport const CanonfigPlugin = async () => ({\n "tool.execute.before": async (input, output) => {\n${beforeBody}\n },\n "tool.execute.after": async (input, output) => {\n${afterBody}\n },\n});\n`;
212
+ }
@@ -2,6 +2,7 @@
2
2
  import { NodeRuntime } from "@effect/platform-node";
3
3
  import { Effect } from "effect";
4
4
  import { evaluateCli, runCli } from "../cli/cli.js";
5
+ import { isHarnessConfigurationCommand, runHarnessConfigurationCli, } from "../harness-configuration/cli.js";
5
6
  const warningListeners = process.listeners("warning");
6
7
  process.removeAllListeners("warning");
7
8
  process.on("warning", (warning) => {
@@ -19,20 +20,25 @@ const nodeCliIo = {
19
20
  },
20
21
  };
21
22
  const arguments_ = process.argv.slice(2);
22
- const outcome = evaluateCli(arguments_);
23
- if (outcome._tag === "Command") {
24
- NodeRuntime.runMain(Effect.promise(() => import("./layers.js")).pipe(Effect.flatMap(({ runtimeLayer }) => runCli(arguments_, nodeCliIo).pipe(Effect.andThen(outcome.command._tag === "SourceServe"
25
- ? Effect.never
26
- : Effect.void), Effect.provide(runtimeLayer())))));
23
+ if (isHarnessConfigurationCommand(arguments_)) {
24
+ NodeRuntime.runMain(Effect.promise(() => runHarnessConfigurationCli(arguments_.slice(1), nodeCliIo)));
27
25
  }
28
26
  else {
29
- NodeRuntime.runMain(Effect.sync(() => {
30
- if (outcome._tag === "Help" || outcome._tag === "Version") {
31
- nodeCliIo.writeStdout(`${outcome.text}\n`);
32
- }
33
- else {
34
- nodeCliIo.writeStderr(`${outcome.message}\n`);
35
- }
36
- nodeCliIo.setExitCode(outcome.exitCode);
37
- }));
27
+ const outcome = evaluateCli(arguments_);
28
+ if (outcome._tag === "Command") {
29
+ NodeRuntime.runMain(Effect.promise(() => import("./layers.js")).pipe(Effect.flatMap(({ runtimeLayer }) => runCli(arguments_, nodeCliIo).pipe(Effect.andThen(outcome.command._tag === "SourceServe"
30
+ ? Effect.never
31
+ : Effect.void), Effect.provide(runtimeLayer())))));
32
+ }
33
+ else {
34
+ NodeRuntime.runMain(Effect.sync(() => {
35
+ if (outcome._tag === "Help" || outcome._tag === "Version") {
36
+ nodeCliIo.writeStdout(`${outcome.text}\n`);
37
+ }
38
+ else {
39
+ nodeCliIo.writeStderr(`${outcome.message}\n`);
40
+ }
41
+ nodeCliIo.setExitCode(outcome.exitCode);
42
+ }));
43
+ }
38
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microck/canonfig",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "one-way configuration synchronizer: publish a canonical AI agent setup from one Source Machine and converge Linux, macOS, and Windows Follower Machines",
5
5
  "author": "Microck <contact@micr.dev>",
6
6
  "license": "MIT",
@@ -47,15 +47,15 @@
47
47
  "website:build": "npm run build --workspace @canonfig/website"
48
48
  },
49
49
  "dependencies": {
50
- "@effect/platform-node": "4.0.0-rc.109",
51
- "@effect/sql-sqlite-node": "4.0.0-rc.109",
52
- "effect": "4.0.0-rc.109",
50
+ "@effect/platform-node": "4.0.0-rc.112",
51
+ "@effect/sql-sqlite-node": "4.0.0-rc.112",
52
+ "effect": "4.0.0-rc.112",
53
53
  "selfsigned": "^5.5.0",
54
54
  "smol-toml": "1.6.1",
55
55
  "yaml": "^2.9.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@effect/vitest": "4.0.0-rc.109",
58
+ "@effect/vitest": "4.0.0-rc.112",
59
59
  "@oxlint/plugins": "1.78.0",
60
60
  "@types/node": "24.10.1",
61
61
  "oxlint": "1.78.0",