@tangle-network/agent-profile-materialize 1.1.6 → 1.1.8

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 (95) hide show
  1. package/dist/index.d.ts +1226 -29
  2. package/dist/index.js +4987 -23
  3. package/package.json +7 -5
  4. package/dist/candidate-types.d.ts +0 -36
  5. package/dist/candidate-types.js +0 -1
  6. package/dist/candidate.d.ts +0 -9
  7. package/dist/candidate.js +0 -283
  8. package/dist/commands.d.ts +0 -2
  9. package/dist/commands.js +0 -32
  10. package/dist/control-maps.d.ts +0 -58
  11. package/dist/control-maps.js +0 -277
  12. package/dist/execution-controls.d.ts +0 -5
  13. package/dist/execution-controls.js +0 -199
  14. package/dist/harness-prompt.d.ts +0 -22
  15. package/dist/harness-prompt.js +0 -64
  16. package/dist/harness.d.ts +0 -131
  17. package/dist/harness.js +0 -416
  18. package/dist/hooks.d.ts +0 -6
  19. package/dist/hooks.js +0 -250
  20. package/dist/managed-workspace-bind.d.ts +0 -4
  21. package/dist/managed-workspace-bind.js +0 -112
  22. package/dist/managed-workspace-stage.d.ts +0 -25
  23. package/dist/managed-workspace-stage.js +0 -199
  24. package/dist/managed-workspace-types.d.ts +0 -38
  25. package/dist/managed-workspace-types.js +0 -1
  26. package/dist/managed-workspace.d.ts +0 -37
  27. package/dist/managed-workspace.js +0 -290
  28. package/dist/mcp-config-ownership.d.ts +0 -13
  29. package/dist/mcp-config-ownership.js +0 -79
  30. package/dist/mcp-opencode.d.ts +0 -25
  31. package/dist/mcp-opencode.js +0 -52
  32. package/dist/mcp-timeouts.d.ts +0 -7
  33. package/dist/mcp-timeouts.js +0 -48
  34. package/dist/mcp.d.ts +0 -4
  35. package/dist/mcp.js +0 -246
  36. package/dist/model-id.d.ts +0 -55
  37. package/dist/model-id.js +0 -67
  38. package/dist/pi-models-config.d.ts +0 -32
  39. package/dist/pi-models-config.js +0 -65
  40. package/dist/plan-application.d.ts +0 -10
  41. package/dist/plan-application.js +0 -190
  42. package/dist/plan-digest.d.ts +0 -18
  43. package/dist/plan-digest.js +0 -116
  44. package/dist/plan-json.d.ts +0 -13
  45. package/dist/plan-json.js +0 -91
  46. package/dist/plan-normalize.d.ts +0 -12
  47. package/dist/plan-normalize.js +0 -168
  48. package/dist/plan-prompt.d.ts +0 -38
  49. package/dist/plan-prompt.js +0 -49
  50. package/dist/plan-support.d.ts +0 -9
  51. package/dist/plan-support.js +0 -22
  52. package/dist/plan-types.d.ts +0 -341
  53. package/dist/plan-types.js +0 -105
  54. package/dist/prime-agent-dir.d.ts +0 -64
  55. package/dist/prime-agent-dir.js +0 -173
  56. package/dist/prime-fork.d.ts +0 -155
  57. package/dist/prime-fork.js +0 -262
  58. package/dist/prime-profile.d.ts +0 -88
  59. package/dist/prime-profile.js +0 -297
  60. package/dist/profile-bundle-cache.d.ts +0 -37
  61. package/dist/profile-bundle-cache.js +0 -320
  62. package/dist/profile-bundle-key.d.ts +0 -69
  63. package/dist/profile-bundle-key.js +0 -62
  64. package/dist/profile-bundle-lru.d.ts +0 -56
  65. package/dist/profile-bundle-lru.js +0 -220
  66. package/dist/profile-materializer.d.ts +0 -24
  67. package/dist/profile-materializer.js +0 -114
  68. package/dist/profile-plan.d.ts +0 -16
  69. package/dist/profile-plan.js +0 -90
  70. package/dist/profile-prompt.d.ts +0 -4
  71. package/dist/profile-prompt.js +0 -107
  72. package/dist/profile-support.d.ts +0 -8
  73. package/dist/profile-support.js +0 -6
  74. package/dist/resource-lowering.d.ts +0 -6
  75. package/dist/resource-lowering.js +0 -103
  76. package/dist/resource-resolution.d.ts +0 -27
  77. package/dist/resource-resolution.js +0 -265
  78. package/dist/secret-concurrent-revision.d.ts +0 -30
  79. package/dist/secret-concurrent-revision.js +0 -291
  80. package/dist/secret-session-view.d.ts +0 -36
  81. package/dist/secret-session-view.js +0 -179
  82. package/dist/subagents.d.ts +0 -6
  83. package/dist/subagents.js +0 -137
  84. package/dist/tool-bin-path.d.ts +0 -23
  85. package/dist/tool-bin-path.js +0 -61
  86. package/dist/workspace-apply-files.d.ts +0 -57
  87. package/dist/workspace-apply-files.js +0 -193
  88. package/dist/workspace-apply-publish.d.ts +0 -22
  89. package/dist/workspace-apply-publish.js +0 -178
  90. package/dist/workspace-apply.d.ts +0 -11
  91. package/dist/workspace-apply.js +0 -124
  92. package/dist/workspace-files.d.ts +0 -8
  93. package/dist/workspace-files.js +0 -46
  94. package/dist/workspace-launch.d.ts +0 -3
  95. package/dist/workspace-launch.js +0 -13
package/dist/harness.d.ts DELETED
@@ -1,131 +0,0 @@
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_CONFIG_FILE = "opencode.json";
8
- export declare const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
9
- export declare const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
10
- export declare const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
11
- /** Whether the shared materializer may place its context file in the harness agent directory. */
12
- export declare function harnessAutoLoadsAgentContextFile(harness: HarnessId): boolean;
13
- /**
14
- * Why a harness refuses a system-prompt REPLACEMENT.
15
- *
16
- * The reason names the control that does exist, so the refusal is actionable
17
- * without a capability-matrix lookup — a bare "unsupported" sends the caller
18
- * looking for a flag that was never there.
19
- */
20
- export declare function systemPromptRefusal(harness: HarnessId): string;
21
- /**
22
- * Why a harness refuses an ADDITION to the system prompt.
23
- *
24
- * gemini puts its context file in the system-prompt position, so the temptation
25
- * is to lower an addition there. That is refused because GEMINI.md is the ONE
26
- * file it auto-loads: an addition written there is byte-identical to the same
27
- * text under `prompt.instructions`, and no consumer could tell which intent the
28
- * caller expressed. opencode is not refused for the same situation, because it
29
- * loads exactly the files its config names and can therefore carry the two
30
- * intents in separate, separately-ordered files.
31
- */
32
- export declare function appendSystemPromptRefusal(harness: HarnessId): string;
33
- /** Project context file each harness actually reads from cwd. NEVER a constant. */
34
- export declare const CONTEXT_FILE: Record<HarnessId, string>;
35
- /**
36
- * The paths a profile resource may never mount.
37
- *
38
- * Codex names no system-prompt file of its own — `model_instructions_file`
39
- * takes whatever path it is given — and opencode names none either, since it
40
- * reads exactly the files its config lists. All three names below were chosen
41
- * here, so a file already at one of them is this materializer's own output from
42
- * an earlier application into the same workspace, and a mount there could not
43
- * survive the next application.
44
- *
45
- * That is an inference from the path name, not verified provenance: nothing
46
- * checks the existing bytes are prior materializer output, so a workspace that
47
- * carried a foreign file at one of these invented names is overwritten on the
48
- * first application. Accepted cost of stateless multi-turn re-application,
49
- * bounded by the names being ones no harness or user convention uses.
50
- */
51
- export declare const MATERIALIZER_RESERVED_PATHS: ReadonlySet<string>;
52
- /**
53
- * The context files this package owns by MARKER rather than by name:
54
- * `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `HERMES.md`.
55
- *
56
- * Each is a documented name a repository can already carry, so the name proves
57
- * nothing. A generated one carries the line {@link markMaterializerContextFile}
58
- * writes, and only that line lets the next materialization replace it.
59
- *
60
- * opencode's context file is absent because it is a RESERVED name this package
61
- * invented, already owned by the name alone.
62
- */
63
- export declare const MATERIALIZER_CONTEXT_PATHS: ReadonlySet<string>;
64
- /** Mark one generated native agent file without changing its harness meaning. */
65
- export declare function markMaterializerSubagent(relPath: string, content: string): string;
66
- /**
67
- * Mark one generated context file so the NEXT materialization can prove this
68
- * package wrote the bytes it is about to replace.
69
- *
70
- * The marker is an HTML comment on line 1. CommonMark closes an HTML block at
71
- * the line holding `-->`, so every instruction below it parses exactly as it
72
- * did without the line, and the line itself states no instruction. It is one
73
- * more line this package composes into a file it already composes: it goes on
74
- * a context file, which carries `prompt.instructions`, and NEVER on a
75
- * system-prompt REPLACEMENT, whose bytes are the caller's exact text
76
- * (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
77
- * package invented and needs no marker to be owned).
78
- *
79
- * It carries the digest of the generated body below it, which the subagent
80
- * marker does not need. `CLAUDE.md` is claude-code's own memory file and the
81
- * agent appends to it mid-session, so a bare marker would still sit on line 1
82
- * of a file the agent has since rewritten. The digest tells which leading bytes
83
- * are still this package's own: the next materialization replaces exactly
84
- * those and keeps what the agent appended below them
85
- * ({@link generatedContextAppendix}). The marker's wording is part of every
86
- * marker already written into live workspaces, so it stays as it is.
87
- *
88
- * A reserved context path — opencode's invented instruction file — is returned
89
- * unchanged: its NAME already proves the provenance, and nothing else may mount
90
- * there.
91
- */
92
- export declare function markMaterializerContextFile(relPath: string, content: string): string;
93
- /**
94
- * Whether a generated file occupies a path that the profile materializer owns.
95
- *
96
- * Generated native agent files and generated context files come from the active
97
- * profile.
98
- * The active profile must replace the prior profile version at the same path.
99
- * Otherwise, a profile update stops every resumed session before launch.
100
- * Explicit resources at these paths stay shared.
101
- * The plan builder applies ownership only when the source is `generated`.
102
- * An output marker proves ownership before replacement.
103
- */
104
- export declare function isMaterializerOwnedPath(relPath: string): boolean;
105
- /**
106
- * The bytes to publish at owned path `relPath` over the file already there, or
107
- * `undefined` when that file is not this package's earlier output and must be
108
- * refused (`undefined` content: no regular file there). For a context file the
109
- * replacement is the planned file followed by whatever the agent appended
110
- * below the generated body (claude-code appends its memory to `CLAUDE.md`). A
111
- * body written before the marker existed is adopted once, only when it is
112
- * byte-identical to the body this plan writes.
113
- */
114
- export declare function materializerReplacement(relPath: string, content: () => string | undefined, plannedContent: string): string | undefined;
115
- /**
116
- * cwd-relative native skill dir per harness, or null if the harness has no
117
- * cwd skill primitive (skills live in a user/global dir the workspace plan
118
- * cannot reach). Public so consumers composing a profile can (a) render the
119
- * exact path the platform will mount a skill at into their prompts instead
120
- * of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
121
- * ahead of materialization instead of catching the materialize-time throw.
122
- */
123
- export declare const HARNESS_SKILL_DIRS: Readonly<Record<HarnessId, string | null>>;
124
- /**
125
- * Resolve the cwd-relative directory `resources.skills` refs materialize
126
- * into on `harness`. `null` means the harness cannot receive cwd skills, the
127
- * same condition that surfaces as an `unsupported`
128
- * "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
129
- * materialize time, exposed here as branchable data.
130
- */
131
- export declare function skillDirForHarness(harness: HarnessId): string | null;
package/dist/harness.js DELETED
@@ -1,416 +0,0 @@
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_CONFIG_FILE = "opencode.json";
25
- export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
26
- export const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
27
- export const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
28
- /** Whether the shared materializer may place its context file in the harness agent directory. */
29
- export function harnessAutoLoadsAgentContextFile(harness) {
30
- assertMaterializerHarness(harness);
31
- // Prime uses the fork-specific materializePrimeProfileControls path. It
32
- // does not consume the shared agent-root plan produced by this predicate.
33
- return harness === "pi";
34
- }
35
- /**
36
- * Why a harness refuses a system-prompt REPLACEMENT.
37
- *
38
- * The reason names the control that does exist, so the refusal is actionable
39
- * without a capability-matrix lookup — a bare "unsupported" sends the caller
40
- * looking for a flag that was never there.
41
- */
42
- export function systemPromptRefusal(harness) {
43
- switch (harness) {
44
- case "opencode":
45
- // The control exists, so the reason names the two declarations that reach
46
- // it rather than claiming opencode has none.
47
- 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"]';
48
- default:
49
- return `${harness}: no verified exact system-prompt replacement control`;
50
- }
51
- }
52
- /**
53
- * Why a harness refuses an ADDITION to the system prompt.
54
- *
55
- * gemini puts its context file in the system-prompt position, so the temptation
56
- * is to lower an addition there. That is refused because GEMINI.md is the ONE
57
- * file it auto-loads: an addition written there is byte-identical to the same
58
- * text under `prompt.instructions`, and no consumer could tell which intent the
59
- * caller expressed. opencode is not refused for the same situation, because it
60
- * loads exactly the files its config names and can therefore carry the two
61
- * intents in separate, separately-ordered files.
62
- */
63
- export function appendSystemPromptRefusal(harness) {
64
- switch (harness) {
65
- case "gemini":
66
- 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";
67
- case "codex":
68
- return "codex: no verified additive system-prompt control — its AGENTS.md lands in a developer/user message, not the system channel";
69
- default:
70
- return `${harness}: no verified additive system-prompt control`;
71
- }
72
- }
73
- /** Project context file each harness actually reads from cwd. NEVER a constant. */
74
- export const CONTEXT_FILE = {
75
- "claude-code": "CLAUDE.md",
76
- nanoclaw: "CLAUDE.md",
77
- codex: "AGENTS.md",
78
- opencode: ".opencode/profile-instructions.md",
79
- "kimi-code": "AGENTS.md",
80
- pi: "AGENTS.md",
81
- // prime (PrimeIntellect-ai/prime-agent v0.7.0, be9e2fa0) walks cwd and its
82
- // ancestors for AGENTS.md/CLAUDE.md, cwd first (fork resource-loader.ts:59
83
- // candidates, :94-109 walk from cwd inside loadProjectContextFiles).
84
- prime: "AGENTS.md",
85
- gemini: "GEMINI.md", // current default; AGENTS.md only via .gemini/settings.json context.fileName
86
- hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
87
- openclaw: "AGENTS.md",
88
- };
89
- /**
90
- * The paths a profile resource may never mount.
91
- *
92
- * Codex names no system-prompt file of its own — `model_instructions_file`
93
- * takes whatever path it is given — and opencode names none either, since it
94
- * reads exactly the files its config lists. All three names below were chosen
95
- * here, so a file already at one of them is this materializer's own output from
96
- * an earlier application into the same workspace, and a mount there could not
97
- * survive the next application.
98
- *
99
- * That is an inference from the path name, not verified provenance: nothing
100
- * checks the existing bytes are prior materializer output, so a workspace that
101
- * carried a foreign file at one of these invented names is overwritten on the
102
- * first application. Accepted cost of stateless multi-turn re-application,
103
- * bounded by the names being ones no harness or user convention uses.
104
- */
105
- export const MATERIALIZER_RESERVED_PATHS = new Set([
106
- CODEX_SYSTEM_PROMPT_FILE,
107
- OPENCODE_APPEND_SYSTEM_PROMPT_FILE,
108
- CONTEXT_FILE.opencode,
109
- ]);
110
- /** Every path any harness reads as its project context file. */
111
- const CONTEXT_FILE_PATHS = new Set(Object.values(CONTEXT_FILE));
112
- /**
113
- * The context files this package owns by MARKER rather than by name:
114
- * `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `HERMES.md`.
115
- *
116
- * Each is a documented name a repository can already carry, so the name proves
117
- * nothing. A generated one carries the line {@link markMaterializerContextFile}
118
- * writes, and only that line lets the next materialization replace it.
119
- *
120
- * opencode's context file is absent because it is a RESERVED name this package
121
- * invented, already owned by the name alone.
122
- */
123
- export const MATERIALIZER_CONTEXT_PATHS = new Set([...CONTEXT_FILE_PATHS].filter((path) => !MATERIALIZER_RESERVED_PATHS.has(path)));
124
- /**
125
- * The paths whose generated content is wholly this package's output, per
126
- * {@link PlanFile.ownership}: the reserved names, the context files, and
127
- * `opencode.json`, which carries only the registrations this package generates
128
- * from the profile.
129
- *
130
- * `opencode.json` and every context file is a documented name a user repository
131
- * can carry, so the name alone proves nothing and a profile resource may still
132
- * mount one. An existing file there is replaced only when
133
- * {@link materializerReplacement} recognizes its bytes as this package's own
134
- * earlier output.
135
- *
136
- * Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
137
- * lowers to. The line a marker would have to add is the reason. A REPLACEMENT
138
- * is the caller's exact bytes and this package may add none of its own, so that
139
- * path keeps refusing. A context file carries `prompt.instructions`, which this
140
- * package already composes — it joins the blocks and appends the final newline
141
- * — so one more composed line is inside the same authority.
142
- *
143
- * That line, not "which channel the harness reads it on", is what separates the
144
- * two. `GEMINI.md` is owned even though gemini lowers its context file into the
145
- * system-prompt position as an ADDITION ({@link appendSystemPromptRefusal}),
146
- * because the intent it carries is still instructions this package composed.
147
- */
148
- const MATERIALIZER_OWNED_PATHS = new Set([
149
- ...MATERIALIZER_RESERVED_PATHS,
150
- ...MATERIALIZER_CONTEXT_PATHS,
151
- OPENCODE_CONFIG_FILE,
152
- ]);
153
- const MATERIALIZER_SUBAGENT_PATH = /^(?:(?:\.claude|\.opencode|\.gemini)\/agents\/[a-z0-9][a-z0-9_-]*\.md|\.codex\/agents\/[a-z0-9][a-z0-9_-]*\.toml)$/u;
154
- /** The one provenance token every generated-file marker carries. */
155
- const MATERIALIZER_MARKER = "tangle-agent-profile-materialize: generated";
156
- const MATERIALIZER_SUBAGENT_MARKER = `# ${MATERIALIZER_MARKER}`;
157
- const MATERIALIZER_CONTEXT_MARKER_NOTE = "do not edit; the next profile materialization replaces this file";
158
- function materializerSubagentMarkerPrefix(relPath) {
159
- return relPath.endsWith(".toml")
160
- ? `${MATERIALIZER_SUBAGENT_MARKER}\n`
161
- : `---\n${MATERIALIZER_SUBAGENT_MARKER}\n`;
162
- }
163
- /** Mark one generated native agent file without changing its harness meaning. */
164
- export function markMaterializerSubagent(relPath, content) {
165
- if (!MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
166
- throw new Error(`Not a native agent path: ${relPath}`);
167
- }
168
- if (relPath.endsWith(".toml")) {
169
- return `${materializerSubagentMarkerPrefix(relPath)}${content}`;
170
- }
171
- if (!content.startsWith("---\n")) {
172
- throw new Error(`Native Markdown agent has no frontmatter: ${relPath}`);
173
- }
174
- return `${materializerSubagentMarkerPrefix(relPath)}${content.slice(4)}`;
175
- }
176
- /** The marker line a generated context file whose body is `body` carries. */
177
- function materializerContextMarker(body) {
178
- const digest = createHash("sha256").update(body, "utf8").digest("hex");
179
- return `<!-- ${MATERIALIZER_MARKER} sha256:${digest}; ${MATERIALIZER_CONTEXT_MARKER_NOTE} -->`;
180
- }
181
- /**
182
- * Mark one generated context file so the NEXT materialization can prove this
183
- * package wrote the bytes it is about to replace.
184
- *
185
- * The marker is an HTML comment on line 1. CommonMark closes an HTML block at
186
- * the line holding `-->`, so every instruction below it parses exactly as it
187
- * did without the line, and the line itself states no instruction. It is one
188
- * more line this package composes into a file it already composes: it goes on
189
- * a context file, which carries `prompt.instructions`, and NEVER on a
190
- * system-prompt REPLACEMENT, whose bytes are the caller's exact text
191
- * (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
192
- * package invented and needs no marker to be owned).
193
- *
194
- * It carries the digest of the generated body below it, which the subagent
195
- * marker does not need. `CLAUDE.md` is claude-code's own memory file and the
196
- * agent appends to it mid-session, so a bare marker would still sit on line 1
197
- * of a file the agent has since rewritten. The digest tells which leading bytes
198
- * are still this package's own: the next materialization replaces exactly
199
- * those and keeps what the agent appended below them
200
- * ({@link generatedContextAppendix}). The marker's wording is part of every
201
- * marker already written into live workspaces, so it stays as it is.
202
- *
203
- * A reserved context path — opencode's invented instruction file — is returned
204
- * unchanged: its NAME already proves the provenance, and nothing else may mount
205
- * there.
206
- */
207
- export function markMaterializerContextFile(relPath, content) {
208
- if (!CONTEXT_FILE_PATHS.has(relPath)) {
209
- throw new Error(`Not a harness context file: ${relPath}`);
210
- }
211
- if (!MATERIALIZER_CONTEXT_PATHS.has(relPath))
212
- return content;
213
- return `${materializerContextMarker(content)}\n${content}`;
214
- }
215
- const MATERIALIZER_CONTEXT_MARKER_HEAD = `<!-- ${MATERIALIZER_MARKER} sha256:`;
216
- const MATERIALIZER_CONTEXT_MARKER_TAIL = `; ${MATERIALIZER_CONTEXT_MARKER_NOTE} -->`;
217
- const SHA256_HEX = /^[0-9a-f]{64}$/u;
218
- /**
219
- * Text an agent appended below this package's generated context body: `""`
220
- * for exact output, `undefined` when no marker is on line 1 or no leading part
221
- * of the body matches its digest (an edit inside the generated part). Each
222
- * line boundary is a candidate end; one incremental hash walks the body once.
223
- */
224
- function generatedContextAppendix(existing) {
225
- const lineEnd = existing.indexOf("\n");
226
- if (lineEnd === -1)
227
- return undefined;
228
- const marker = existing.slice(0, lineEnd);
229
- if (!marker.startsWith(MATERIALIZER_CONTEXT_MARKER_HEAD) ||
230
- !marker.endsWith(MATERIALIZER_CONTEXT_MARKER_TAIL)) {
231
- return undefined;
232
- }
233
- const digest = marker.slice(MATERIALIZER_CONTEXT_MARKER_HEAD.length, marker.length - MATERIALIZER_CONTEXT_MARKER_TAIL.length);
234
- if (!SHA256_HEX.test(digest))
235
- return undefined;
236
- const body = existing.slice(lineEnd + 1);
237
- // No cut at 0: a generated body is never empty, so it never adopts a whole file.
238
- const boundaries = [];
239
- for (let newline = body.indexOf("\n"); newline !== -1; newline = body.indexOf("\n", newline + 1)) {
240
- boundaries.push(newline, newline + 1);
241
- }
242
- if (boundaries.length === 0 ||
243
- boundaries[boundaries.length - 1] !== body.length) {
244
- boundaries.push(body.length);
245
- }
246
- const hash = createHash("sha256");
247
- let hashed = 0;
248
- for (const end of boundaries) {
249
- hash.update(body.slice(hashed, end), "utf8");
250
- hashed = end;
251
- if (hash.copy().digest("hex") === digest)
252
- return body.slice(end);
253
- }
254
- return undefined;
255
- }
256
- /** The planned context file, then the agent's appended text on its own line. */
257
- function withContextAppendix(plannedContent, appendix) {
258
- if (appendix === "" ||
259
- plannedContent.endsWith("\n") ||
260
- appendix.startsWith("\n")) {
261
- return `${plannedContent}${appendix}`;
262
- }
263
- return `${plannedContent}\n${appendix}`;
264
- }
265
- /**
266
- * Whether a generated file occupies a path that the profile materializer owns.
267
- *
268
- * Generated native agent files and generated context files come from the active
269
- * profile.
270
- * The active profile must replace the prior profile version at the same path.
271
- * Otherwise, a profile update stops every resumed session before launch.
272
- * Explicit resources at these paths stay shared.
273
- * The plan builder applies ownership only when the source is `generated`.
274
- * An output marker proves ownership before replacement.
275
- */
276
- export function isMaterializerOwnedPath(relPath) {
277
- return (MATERIALIZER_OWNED_PATHS.has(relPath) ||
278
- MATERIALIZER_SUBAGENT_PATH.test(relPath));
279
- }
280
- /** The top-level keys this package writes into `opencode.json`. */
281
- const OPENCODE_CONFIG_KEYS = new Set([
282
- "$schema",
283
- "instructions",
284
- "mcp",
285
- "tools",
286
- "permission",
287
- ]);
288
- /**
289
- * The bytes to publish at owned path `relPath` over the file already there, or
290
- * `undefined` when that file is not this package's earlier output and must be
291
- * refused (`undefined` content: no regular file there). For a context file the
292
- * replacement is the planned file followed by whatever the agent appended
293
- * below the generated body (claude-code appends its memory to `CLAUDE.md`). A
294
- * body written before the marker existed is adopted once, only when it is
295
- * byte-identical to the body this plan writes.
296
- */
297
- export function materializerReplacement(relPath, content, plannedContent) {
298
- if (!MATERIALIZER_CONTEXT_PATHS.has(relPath)) {
299
- return isMaterializerOutput(relPath, content, plannedContent)
300
- ? plannedContent
301
- : undefined;
302
- }
303
- const existing = content();
304
- if (existing === undefined)
305
- return undefined;
306
- const appendix = generatedContextAppendix(existing);
307
- if (appendix === "")
308
- return plannedContent;
309
- // Appended text rides only on a marked plan, which the next version recognizes.
310
- if (generatedContextAppendix(plannedContent) !== "")
311
- return undefined;
312
- if (appendix !== undefined) {
313
- return withContextAppendix(plannedContent, appendix);
314
- }
315
- return existing === plannedContent.slice(plannedContent.indexOf("\n") + 1)
316
- ? plannedContent
317
- : undefined;
318
- }
319
- /**
320
- * Whether the file already at owned path `relPath`, other than a context file,
321
- * is this package's own earlier output. A reserved name proves that by itself.
322
- * Native agent files require the generated marker. An exact unmarked copy of
323
- * the planned native agent file is adopted once for upgrades from materializer
324
- * versions without that marker. `undefined` content means no regular file is
325
- * there.
326
- *
327
- * An `opencode.json` this package wrote holds the opencode schema id, only the
328
- * keys this package writes, and only instruction paths this package invented;
329
- * a user config holding nothing else is replaced too, any other key or own
330
- * instruction path is refused.
331
- */
332
- function isMaterializerOutput(relPath, content, plannedContent) {
333
- if (MATERIALIZER_RESERVED_PATHS.has(relPath))
334
- return true;
335
- if (relPath === ".pi/mcp.json" || relPath === ".kimi-code/mcp.json")
336
- return isMaterializerMcpConfig(relPath, content);
337
- if (MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
338
- const existing = content();
339
- if (existing === undefined)
340
- return false;
341
- const markerPrefix = materializerSubagentMarkerPrefix(relPath);
342
- if (existing.startsWith(markerPrefix))
343
- return true;
344
- if (!plannedContent.startsWith(markerPrefix))
345
- return false;
346
- const legacyContent = relPath.endsWith(".toml")
347
- ? plannedContent.slice(markerPrefix.length)
348
- : `---\n${plannedContent.slice(markerPrefix.length)}`;
349
- return existing === legacyContent;
350
- }
351
- if (relPath !== OPENCODE_CONFIG_FILE)
352
- return false;
353
- const bytes = content();
354
- if (bytes === undefined)
355
- return false;
356
- let parsed;
357
- try {
358
- parsed = JSON.parse(bytes);
359
- }
360
- catch {
361
- return false;
362
- }
363
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
364
- return false;
365
- }
366
- const config = parsed;
367
- if (config.$schema !== OPENCODE_CONFIG_SCHEMA)
368
- return false;
369
- if (!Object.keys(config).every((key) => OPENCODE_CONFIG_KEYS.has(key))) {
370
- return false;
371
- }
372
- const instructions = config.instructions;
373
- if (instructions === undefined)
374
- return true;
375
- return (Array.isArray(instructions) &&
376
- instructions.every((entry) => entry === OPENCODE_APPEND_SYSTEM_PROMPT_FILE ||
377
- entry === CONTEXT_FILE.opencode));
378
- }
379
- /**
380
- * cwd-relative native skill dir per harness, or null if the harness has no
381
- * cwd skill primitive (skills live in a user/global dir the workspace plan
382
- * cannot reach). Public so consumers composing a profile can (a) render the
383
- * exact path the platform will mount a skill at into their prompts instead
384
- * of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
385
- * ahead of materialization instead of catching the materialize-time throw.
386
- */
387
- export const HARNESS_SKILL_DIRS = {
388
- "claude-code": ".claude/skills",
389
- nanoclaw: ".claude/skills",
390
- codex: ".codex/skills",
391
- opencode: ".opencode/skills",
392
- "kimi-code": ".kimi/skills",
393
- pi: ".pi/skills",
394
- // prime auto-discovers project skills from <cwd>/<configDir>/skills where the
395
- // fork ships configDir ".prime/agent" (fork package.json piConfig, config.ts:494;
396
- // package-manager.ts:885 projectBaseDir, :2175-2180 projectDirs.skills,
397
- // :2209-2213 collectAutoSkillEntries). The loader recurses into
398
- // <name>/SKILL.md subdirs (skills.ts:328-372).
399
- prime: ".prime/agent/skills",
400
- gemini: ".gemini/skills",
401
- hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
402
- openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
403
- };
404
- /**
405
- * Resolve the cwd-relative directory `resources.skills` refs materialize
406
- * into on `harness`. `null` means the harness cannot receive cwd skills, the
407
- * same condition that surfaces as an `unsupported`
408
- * "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
409
- * materialize time, exposed here as branchable data.
410
- */
411
- export function skillDirForHarness(harness) {
412
- assertMaterializerHarness(harness);
413
- return HARNESS_SKILL_DIRS[harness];
414
- }
415
- import { createHash } from "node:crypto";
416
- import { isMaterializerMcpConfig } from "./mcp-config-ownership.js";
package/dist/hooks.d.ts DELETED
@@ -1,6 +0,0 @@
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[], extraEnv: Record<string, string>, envSources?: 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";