@tangle-network/agent-profile-materialize 0.15.1 → 0.16.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 (55) hide show
  1. package/dist/candidate-types.d.ts +32 -0
  2. package/dist/candidate-types.js +1 -0
  3. package/dist/candidate.d.ts +9 -0
  4. package/dist/candidate.js +279 -0
  5. package/dist/commands.d.ts +2 -0
  6. package/dist/commands.js +32 -0
  7. package/dist/control-maps.d.ts +34 -0
  8. package/dist/control-maps.js +185 -0
  9. package/dist/execution-controls.d.ts +5 -0
  10. package/dist/execution-controls.js +186 -0
  11. package/dist/harness.d.ts +81 -0
  12. package/dist/harness.js +167 -0
  13. package/dist/hooks.d.ts +6 -0
  14. package/dist/hooks.js +261 -0
  15. package/dist/index.d.ts +23 -623
  16. package/dist/index.js +14 -3733
  17. package/dist/mcp.d.ts +22 -0
  18. package/dist/mcp.js +244 -0
  19. package/dist/plan-application.d.ts +10 -0
  20. package/dist/plan-application.js +152 -0
  21. package/dist/plan-digest.d.ts +18 -0
  22. package/dist/plan-digest.js +90 -0
  23. package/dist/plan-json.d.ts +18 -0
  24. package/dist/plan-json.js +91 -0
  25. package/dist/plan-normalize.d.ts +13 -0
  26. package/dist/plan-normalize.js +156 -0
  27. package/dist/plan-prompt.d.ts +38 -0
  28. package/dist/plan-prompt.js +49 -0
  29. package/dist/plan-support.d.ts +9 -0
  30. package/dist/plan-support.js +22 -0
  31. package/dist/plan-types.d.ts +294 -0
  32. package/dist/plan-types.js +106 -0
  33. package/dist/prime-agent-dir.d.ts +47 -0
  34. package/dist/prime-agent-dir.js +164 -0
  35. package/dist/prime-profile.d.ts +88 -0
  36. package/dist/prime-profile.js +284 -0
  37. package/dist/profile-materializer.d.ts +12 -0
  38. package/dist/profile-materializer.js +59 -0
  39. package/dist/profile-plan.d.ts +14 -0
  40. package/dist/profile-plan.js +86 -0
  41. package/dist/profile-prompt.d.ts +4 -0
  42. package/dist/profile-prompt.js +105 -0
  43. package/dist/profile-support.d.ts +8 -0
  44. package/dist/profile-support.js +6 -0
  45. package/dist/resource-lowering.d.ts +7 -0
  46. package/dist/resource-lowering.js +103 -0
  47. package/dist/resource-resolution.d.ts +27 -0
  48. package/dist/resource-resolution.js +241 -0
  49. package/dist/subagents.d.ts +6 -0
  50. package/dist/subagents.js +133 -0
  51. package/dist/workspace-apply.d.ts +12 -0
  52. package/dist/workspace-apply.js +334 -0
  53. package/dist/workspace-files.d.ts +8 -0
  54. package/dist/workspace-files.js +46 -0
  55. package/package.json +3 -3
@@ -0,0 +1,186 @@
1
+ export function materializeExecutionControls(h, tools, permissions, modes, plan, add, unsupported) {
2
+ const toolEntries = Object.entries(tools ?? {}).sort(([left], [right]) => compareStrings(left, right));
3
+ const permissionEntries = Object.entries(permissions ?? {}).sort(([left], [right]) => compareStrings(left, right));
4
+ switch (h) {
5
+ case "claude-code": {
6
+ if (toolEntries.length > 0 || permissionEntries.length > 0) {
7
+ const nativeTools = mapSupportedBooleanEntries(toolEntries, claudeToolName, (name) => unsupported("tools", `claude-code: no verified native tool mapping for "${name}"`), "claude-code tool");
8
+ const nativePermissions = mapSupportedPermissionEntries(permissionEntries, claudeToolName, (name) => unsupported("permissions", `claude-code: no verified native permission mapping for "${name}"`), "claude-code permission");
9
+ const rules = claudePermissionRules(nativePermissions);
10
+ for (const [tool, enabled] of nativeTools) {
11
+ rules[enabled ? "allow" : "deny"].push(tool);
12
+ }
13
+ add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({
14
+ permissions: sealClaudePermissionRules(rules),
15
+ }));
16
+ registerClaudeSettings(plan);
17
+ }
18
+ break;
19
+ }
20
+ case "opencode": {
21
+ const enforceableTools = toolEntries.filter(([name, enabled]) => {
22
+ if (normalizedControlName(name) !== "lsp" || !enabled)
23
+ return true;
24
+ unsupported("tools", "opencode: the LSP tool is disabled by the runtime and cannot be enabled exactly");
25
+ return false;
26
+ });
27
+ const enforceablePermissions = permissionEntries.filter(([name, policy]) => {
28
+ if (normalizedControlName(name) !== "read")
29
+ return true;
30
+ if (!permissionAllowsEverything(policy)) {
31
+ unsupported("permissions", "opencode: read access cannot be denied or approval-gated");
32
+ }
33
+ return false;
34
+ });
35
+ const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`), "opencode tool");
36
+ const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, openCodePermissionName, (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
37
+ if (enforceableTools.length > 0 || permissionEntries.length > 0) {
38
+ add("opencode.json", serializeWorkspacePlanJson({
39
+ $schema: "https://opencode.ai/config.json",
40
+ ...(nativeTools.length > 0
41
+ ? { tools: Object.fromEntries(nativeTools) }
42
+ : {}),
43
+ ...(nativePermissions.length > 0
44
+ ? { permission: Object.fromEntries(nativePermissions) }
45
+ : {}),
46
+ }));
47
+ }
48
+ break;
49
+ }
50
+ case "codex": {
51
+ const searchEntries = [];
52
+ for (const [name, enabled] of toolEntries) {
53
+ const posture = resolveNativeWebTools({ [name]: enabled });
54
+ if (posture.search === enabled && posture.fetch === undefined) {
55
+ searchEntries.push([name, enabled]);
56
+ continue;
57
+ }
58
+ if (posture.fetch === enabled && posture.search === undefined) {
59
+ if (enabled) {
60
+ unsupported("tools", `codex: native web fetch is unavailable for "${name}"`);
61
+ }
62
+ continue;
63
+ }
64
+ unsupported("tools", `codex: no verified primary-process tool mapping for "${name}"`);
65
+ }
66
+ const nativeSearch = mapSupportedBooleanEntries(searchEntries, () => "web_search", () => undefined, "codex tool");
67
+ if (nativeSearch[0]) {
68
+ plan.flags.push("-c", `tools.web_search=${nativeSearch[0][1]}`);
69
+ }
70
+ if (permissionEntries.length > 0) {
71
+ unsupported("permissions", "codex: no verified primary-process permission policy mapping");
72
+ }
73
+ break;
74
+ }
75
+ case "pi": {
76
+ const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`), "pi tool");
77
+ const nativePermissions = mapSupportedPermissionEntries(permissionEntries, piToolId, (name) => unsupported("permissions", `pi: permission name "${name}" is empty after trimming`), "pi permission");
78
+ const enabled = new Set(nativeTools
79
+ .filter(([, value]) => value)
80
+ .map(([name]) => name)
81
+ .sort(compareStrings));
82
+ const excluded = new Set(nativeTools
83
+ .filter(([, value]) => !value)
84
+ .map(([name]) => name)
85
+ .sort(compareStrings));
86
+ for (const [name, policy] of nativePermissions) {
87
+ if (typeof policy !== "string") {
88
+ unsupported("permissions", `pi: conditional permission policy for "${name}" has no verified headless mapping`);
89
+ continue;
90
+ }
91
+ if (policy === "ask") {
92
+ unsupported("permissions", `pi: permission policy "ask" for "${name}" requires an interactive decision surface`);
93
+ continue;
94
+ }
95
+ if (policy === "allow") {
96
+ if (excluded.has(name)) {
97
+ throw new Error(`pi execution control "${name}" conflicts between disabled and allow`);
98
+ }
99
+ continue;
100
+ }
101
+ if (enabled.has(name)) {
102
+ throw new Error(`pi execution control "${name}" conflicts between enabled and deny`);
103
+ }
104
+ excluded.add(name);
105
+ }
106
+ if (enabled.size > 0 || excluded.size > 0) {
107
+ add(PI_EXECUTION_CONTROL_EXTENSION_PATH, piExecutionControlExtensionSource([...enabled], [...excluded]));
108
+ plan.flags.push("--extension", PI_EXECUTION_CONTROL_EXTENSION_PATH);
109
+ }
110
+ if (excluded.size > 0) {
111
+ plan.flags.push("--exclude-tools", [...excluded].sort(compareStrings).join(","));
112
+ }
113
+ break;
114
+ }
115
+ default:
116
+ if (toolEntries.length > 0) {
117
+ unsupported("tools", `${h}: no verified primary-process tool availability mapping`);
118
+ }
119
+ if (permissionEntries.length > 0) {
120
+ unsupported("permissions", `${h}: no verified primary-process permission policy mapping`);
121
+ }
122
+ }
123
+ if (modes && Object.keys(modes).length > 0) {
124
+ unsupported("modes", `${h}: resolve one named AgentProfile mode before materialization`);
125
+ }
126
+ }
127
+ function piExecutionControlExtensionSource(enabledTools, excludedTools) {
128
+ return [
129
+ 'import { writeSync } from "node:fs";',
130
+ `const enabledTools = Object.freeze(${JSON.stringify(enabledTools)});`,
131
+ `const excludedTools = Object.freeze(${JSON.stringify(excludedTools)});`,
132
+ "",
133
+ "function failProfile(message) {",
134
+ " try {",
135
+ ' writeSync(2, "Pi profile execution controls failed: " + message + "\\n");',
136
+ " } finally {",
137
+ " process.exit(78);",
138
+ " }",
139
+ "}",
140
+ "",
141
+ "function resolveTool(reference, available, required) {",
142
+ " if (available.includes(reference)) return reference;",
143
+ " const folded = reference.toLowerCase();",
144
+ " const matches = available.filter((name) => name.toLowerCase() === folded);",
145
+ " if (matches.length === 1) return matches[0];",
146
+ " if (matches.length > 1) {",
147
+ " failProfile('tool \"' + reference + '\" is ambiguous: ' + matches.join(\", \"));",
148
+ " }",
149
+ " if (required) {",
150
+ " failProfile('requested tool \"' + reference + '\" is unavailable');",
151
+ " }",
152
+ " return undefined;",
153
+ "}",
154
+ "",
155
+ "function applyControls(pi) {",
156
+ " const available = pi.getAllTools().map((tool) => tool.name);",
157
+ " for (const reference of excludedTools) {",
158
+ " const remaining = resolveTool(reference, available, false);",
159
+ " if (remaining !== undefined) {",
160
+ " failProfile(",
161
+ " 'excluded tool \"' + reference + '\" remains available as \"' + remaining +",
162
+ " '\"; pass its exact registered Pi name',",
163
+ " );",
164
+ " }",
165
+ " }",
166
+ " const enabled = enabledTools.map((name) =>",
167
+ " resolveTool(name, available, true),",
168
+ " );",
169
+ " const active = new Set(pi.getActiveTools());",
170
+ " for (const name of enabled) active.add(name);",
171
+ " pi.setActiveTools([...active]);",
172
+ "}",
173
+ "",
174
+ "export default function applyProfileExecutionControls(pi) {",
175
+ ' pi.on("before_agent_start", () => applyControls(pi));',
176
+ ' pi.on("turn_start", () => applyControls(pi));',
177
+ "}",
178
+ "",
179
+ ].join("\n");
180
+ }
181
+ import { resolveNativeWebTools } from "@tangle-network/agent-interface";
182
+ import { claudePermissionRules, claudeToolName, mapSupportedBooleanEntries, mapSupportedPermissionEntries, normalizedControlName, openCodePermissionName, openCodeToolName, PI_EXECUTION_CONTROL_EXTENSION_PATH, permissionAllowsEverything, piToolId, sealClaudePermissionRules, } from "./control-maps.js";
183
+ import { CLAUDE_SETTINGS_PATH } from "./harness.js";
184
+ import { registerClaudeSettings } from "./hooks.js";
185
+ import { serializeWorkspacePlanJson } from "./plan-json.js";
186
+ import { compareStrings } from "./workspace-files.js";
@@ -0,0 +1,81 @@
1
+ export declare const MATERIALIZER_HARNESSES: readonly ["claude-code", "nanoclaw", "codex", "opencode", "kimi-code", "pi", "prime", "gemini", "hermes", "openclaw"];
2
+ export type HarnessId = (typeof MATERIALIZER_HARNESSES)[number];
3
+ export declare function isMaterializerHarness(harness: string): harness is HarnessId;
4
+ export declare function assertMaterializerHarness(harness: string): asserts harness is HarnessId;
5
+ export declare const CODEX_SYSTEM_PROMPT_FILE = ".codex/system-prompt.md";
6
+ export declare const OPENCODE_APPEND_SYSTEM_PROMPT_FILE = ".opencode/agent-system-prompt.md";
7
+ export declare const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
8
+ export declare const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
9
+ export declare function harnessAutoLoadsContextFile(harness: HarnessId): boolean;
10
+ /**
11
+ * The project context file this harness reads from cwd — its stable, documented
12
+ * ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
13
+ * bare TUI's persona, which has no per-message system channel) targets the same
14
+ * file the materializer writes instructions to, instead of keeping its own copy
15
+ * of this per-harness map.
16
+ */
17
+ export declare function harnessContextFile(harness: HarnessId): string;
18
+ /**
19
+ * Why a harness refuses a system-prompt REPLACEMENT.
20
+ *
21
+ * The reason names the control that does exist, so the refusal is actionable
22
+ * without a capability-matrix lookup — a bare "unsupported" sends the caller
23
+ * looking for a flag that was never there.
24
+ */
25
+ export declare function systemPromptRefusal(harness: HarnessId): string;
26
+ /**
27
+ * Why a harness refuses an ADDITION to the system prompt.
28
+ *
29
+ * gemini puts its context file in the system-prompt position, so the temptation
30
+ * is to lower an addition there. That is refused because GEMINI.md is the ONE
31
+ * file it auto-loads: an addition written there is byte-identical to the same
32
+ * text under `prompt.instructions`, and no consumer could tell which intent the
33
+ * caller expressed. opencode is not refused for the same situation, because it
34
+ * loads exactly the files its config names and can therefore carry the two
35
+ * intents in separate, separately-ordered files.
36
+ */
37
+ export declare function appendSystemPromptRefusal(harness: HarnessId): string;
38
+ /** Project context file each harness actually reads from cwd. NEVER a constant. */
39
+ export declare const CONTEXT_FILE: Record<HarnessId, string>;
40
+ /**
41
+ * The paths whose only possible author is this package, per
42
+ * {@link PlanFile.ownership}.
43
+ *
44
+ * Codex names no system-prompt file of its own — `model_instructions_file`
45
+ * takes whatever path it is given — and opencode names none either, since it
46
+ * reads exactly the files its config lists. All three names below were chosen
47
+ * here, so a file already at one of them is this materializer's own output from
48
+ * an earlier application into the same workspace.
49
+ *
50
+ * That is an inference from the path name, not verified provenance: nothing
51
+ * checks the existing bytes are prior materializer output, so a workspace that
52
+ * carried a foreign file at one of these invented names is overwritten on the
53
+ * first application. Accepted cost of stateless multi-turn re-application,
54
+ * bounded by the names being ones no harness or user convention uses.
55
+ *
56
+ * Deliberately absent: `.gemini/system.md` (gemini's own documented name),
57
+ * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`, `opencode.json`. A user's repository
58
+ * can carry any of those, and clobbering one is worse than refusing the turn.
59
+ * For `opencode.json` this means a profile change that alters the
60
+ * `instructions[]` membership between turns of one workspace refuses loudly
61
+ * (the consolidated config no longer matches byte-for-byte) rather than
62
+ * replacing a possibly user-authored config.
63
+ */
64
+ export declare const MATERIALIZER_OWNED_PATHS: ReadonlySet<string>;
65
+ /**
66
+ * cwd-relative native skill dir per harness, or null if the harness has no
67
+ * cwd skill primitive (skills live in a user/global dir the workspace plan
68
+ * cannot reach). Public so consumers composing a profile can (a) render the
69
+ * exact path the platform will mount a skill at into their prompts instead
70
+ * of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
71
+ * ahead of materialization instead of catching the materialize-time throw.
72
+ */
73
+ export declare const HARNESS_SKILL_DIRS: Readonly<Record<HarnessId, string | null>>;
74
+ /**
75
+ * Resolve the cwd-relative directory `resources.skills` refs materialize
76
+ * into on `harness`. `null` means the harness cannot receive cwd skills, the
77
+ * same condition that surfaces as an `unsupported`
78
+ * "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
79
+ * materialize time, exposed here as branchable data.
80
+ */
81
+ export declare function skillDirForHarness(harness: HarnessId): string | null;
@@ -0,0 +1,167 @@
1
+ export const MATERIALIZER_HARNESSES = [
2
+ "claude-code",
3
+ "nanoclaw",
4
+ "codex",
5
+ "opencode",
6
+ "kimi-code",
7
+ "pi",
8
+ "prime",
9
+ "gemini",
10
+ "hermes",
11
+ "openclaw",
12
+ ];
13
+ const MATERIALIZER_HARNESS_SET = new Set(MATERIALIZER_HARNESSES);
14
+ export function isMaterializerHarness(harness) {
15
+ return MATERIALIZER_HARNESS_SET.has(harness);
16
+ }
17
+ export function assertMaterializerHarness(harness) {
18
+ if (!isMaterializerHarness(harness)) {
19
+ throw new Error(`Unsupported profile materialization harness "${harness}"`);
20
+ }
21
+ }
22
+ export const CODEX_SYSTEM_PROMPT_FILE = ".codex/system-prompt.md";
23
+ export const OPENCODE_APPEND_SYSTEM_PROMPT_FILE = ".opencode/agent-system-prompt.md";
24
+ export const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
25
+ export const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
26
+ export function harnessAutoLoadsContextFile(harness) {
27
+ assertMaterializerHarness(harness);
28
+ return harness !== "opencode";
29
+ }
30
+ /**
31
+ * The project context file this harness reads from cwd — its stable, documented
32
+ * ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
33
+ * bare TUI's persona, which has no per-message system channel) targets the same
34
+ * file the materializer writes instructions to, instead of keeping its own copy
35
+ * of this per-harness map.
36
+ */
37
+ export function harnessContextFile(harness) {
38
+ assertMaterializerHarness(harness);
39
+ // The map is keyed by HarnessId, but callers reach this from runtime strings
40
+ // (a registry spec name, a request field). A miss would otherwise return
41
+ // undefined behind a `string` return type and surface as an undefined write
42
+ // path much later.
43
+ const path = CONTEXT_FILE[harness];
44
+ if (!path) {
45
+ throw new Error(`No project context file is known for harness "${harness}"`);
46
+ }
47
+ return path;
48
+ }
49
+ /**
50
+ * Why a harness refuses a system-prompt REPLACEMENT.
51
+ *
52
+ * The reason names the control that does exist, so the refusal is actionable
53
+ * without a capability-matrix lookup — a bare "unsupported" sends the caller
54
+ * looking for a flag that was never there.
55
+ */
56
+ export function systemPromptRefusal(harness) {
57
+ switch (harness) {
58
+ case "opencode":
59
+ // The control exists, so the reason names the two declarations that reach
60
+ // it rather than claiming opencode has none.
61
+ return 'opencode: the only system-prompt replacement is per-agent (`agent.<name>.prompt`), which binds to the agent selected at launch and a workspace plan cannot guarantee; an executor that applies the plan\'s "--agent" flag to its own spawn declares binds: ["systemPrompt"], and one that composes opencode\'s config itself declares binds: ["opencode.agent-prompt"]';
62
+ default:
63
+ return `${harness}: no verified exact system-prompt replacement control`;
64
+ }
65
+ }
66
+ /**
67
+ * Why a harness refuses an ADDITION to the system prompt.
68
+ *
69
+ * gemini puts its context file in the system-prompt position, so the temptation
70
+ * is to lower an addition there. That is refused because GEMINI.md is the ONE
71
+ * file it auto-loads: an addition written there is byte-identical to the same
72
+ * text under `prompt.instructions`, and no consumer could tell which intent the
73
+ * caller expressed. opencode is not refused for the same situation, because it
74
+ * loads exactly the files its config names and can therefore carry the two
75
+ * intents in separate, separately-ordered files.
76
+ */
77
+ export function appendSystemPromptRefusal(harness) {
78
+ switch (harness) {
79
+ case "gemini":
80
+ return "gemini: the only additive system-prompt channel is GEMINI.md memory, the single context file it auto-loads, which `prompt.instructions` already materializes — use it instead";
81
+ case "codex":
82
+ return "codex: no verified additive system-prompt control — its AGENTS.md lands in a developer/user message, not the system channel";
83
+ default:
84
+ return `${harness}: no verified additive system-prompt control`;
85
+ }
86
+ }
87
+ /** Project context file each harness actually reads from cwd. NEVER a constant. */
88
+ export const CONTEXT_FILE = {
89
+ "claude-code": "CLAUDE.md",
90
+ nanoclaw: "CLAUDE.md",
91
+ codex: "AGENTS.md",
92
+ opencode: ".opencode/profile-instructions.md",
93
+ "kimi-code": "AGENTS.md",
94
+ pi: "AGENTS.md",
95
+ // prime (PrimeIntellect-ai/prime-agent v0.7.0, be9e2fa0) walks cwd and its
96
+ // ancestors for AGENTS.md/CLAUDE.md, cwd first (fork resource-loader.ts:59
97
+ // candidates, :94-109 walk from cwd inside loadProjectContextFiles).
98
+ prime: "AGENTS.md",
99
+ gemini: "GEMINI.md", // current default; AGENTS.md only via .gemini/settings.json context.fileName
100
+ hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
101
+ openclaw: "AGENTS.md",
102
+ };
103
+ /**
104
+ * The paths whose only possible author is this package, per
105
+ * {@link PlanFile.ownership}.
106
+ *
107
+ * Codex names no system-prompt file of its own — `model_instructions_file`
108
+ * takes whatever path it is given — and opencode names none either, since it
109
+ * reads exactly the files its config lists. All three names below were chosen
110
+ * here, so a file already at one of them is this materializer's own output from
111
+ * an earlier application into the same workspace.
112
+ *
113
+ * That is an inference from the path name, not verified provenance: nothing
114
+ * checks the existing bytes are prior materializer output, so a workspace that
115
+ * carried a foreign file at one of these invented names is overwritten on the
116
+ * first application. Accepted cost of stateless multi-turn re-application,
117
+ * bounded by the names being ones no harness or user convention uses.
118
+ *
119
+ * Deliberately absent: `.gemini/system.md` (gemini's own documented name),
120
+ * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`, `opencode.json`. A user's repository
121
+ * can carry any of those, and clobbering one is worse than refusing the turn.
122
+ * For `opencode.json` this means a profile change that alters the
123
+ * `instructions[]` membership between turns of one workspace refuses loudly
124
+ * (the consolidated config no longer matches byte-for-byte) rather than
125
+ * replacing a possibly user-authored config.
126
+ */
127
+ export const MATERIALIZER_OWNED_PATHS = new Set([
128
+ CODEX_SYSTEM_PROMPT_FILE,
129
+ OPENCODE_APPEND_SYSTEM_PROMPT_FILE,
130
+ CONTEXT_FILE.opencode,
131
+ ]);
132
+ /**
133
+ * cwd-relative native skill dir per harness, or null if the harness has no
134
+ * cwd skill primitive (skills live in a user/global dir the workspace plan
135
+ * cannot reach). Public so consumers composing a profile can (a) render the
136
+ * exact path the platform will mount a skill at into their prompts instead
137
+ * of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
138
+ * ahead of materialization instead of catching the materialize-time throw.
139
+ */
140
+ export const HARNESS_SKILL_DIRS = {
141
+ "claude-code": ".claude/skills",
142
+ nanoclaw: ".claude/skills",
143
+ codex: ".codex/skills",
144
+ opencode: ".opencode/skills",
145
+ "kimi-code": ".kimi/skills",
146
+ pi: ".pi/skills",
147
+ // prime auto-discovers project skills from <cwd>/<configDir>/skills where the
148
+ // fork ships configDir ".prime/agent" (fork package.json piConfig, config.ts:494;
149
+ // package-manager.ts:885 projectBaseDir, :2175-2180 projectDirs.skills,
150
+ // :2209-2213 collectAutoSkillEntries). The loader recurses into
151
+ // <name>/SKILL.md subdirs (skills.ts:328-372).
152
+ prime: ".prime/agent/skills",
153
+ gemini: ".gemini/skills",
154
+ hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
155
+ openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
156
+ };
157
+ /**
158
+ * Resolve the cwd-relative directory `resources.skills` refs materialize
159
+ * into on `harness`. `null` means the harness cannot receive cwd skills, the
160
+ * same condition that surfaces as an `unsupported`
161
+ * "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
162
+ * materialize time, exposed here as branchable data.
163
+ */
164
+ export function skillDirForHarness(harness) {
165
+ assertMaterializerHarness(harness);
166
+ return HARNESS_SKILL_DIRS[harness];
167
+ }
@@ -0,0 +1,6 @@
1
+ export declare function materializeHooks(h: HarnessId, hooks: Record<string, AgentProfileHookCommand[]>, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void, plan: WorkspacePlan): void;
2
+ export declare function registerClaudeSettings(plan: WorkspacePlan): void;
3
+ export declare function hookWrapperSource(executable: string, args: string[], env: Record<string, string>): string;
4
+ import type { AgentProfileHookCommand } from "@tangle-network/agent-interface";
5
+ import type { AddPlanFile } from "./plan-json.js";
6
+ import type { HarnessId, Unsupported, WorkspacePlan } from "./plan-types.js";