@tangle-network/agent-profile-materialize 0.17.3 → 0.18.1

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/dist/harness.d.ts CHANGED
@@ -72,12 +72,27 @@ export declare const MATERIALIZER_RESERVED_PATHS: ReadonlySet<string>;
72
72
  * one is worse than refusing the turn.
73
73
  */
74
74
  export declare const MATERIALIZER_OWNED_PATHS: ReadonlySet<string>;
75
+ /** Mark one generated native agent file without changing its harness meaning. */
76
+ export declare function markMaterializerSubagent(relPath: string, content: string): string;
77
+ /**
78
+ * Whether a generated file occupies a path that the profile materializer owns.
79
+ *
80
+ * Generated native agent files come from the active profile.
81
+ * The active profile must replace the prior profile version at the same path.
82
+ * Otherwise, a profile update stops every resumed session before launch.
83
+ * Explicit resources at these paths stay shared.
84
+ * The plan builder applies ownership only when the source is `generated`.
85
+ * An output marker proves ownership before replacement.
86
+ */
87
+ export declare function isMaterializerOwnedPath(relPath: string): boolean;
75
88
  /** The top-level keys this package writes into `opencode.json`. */
76
89
  export declare const OPENCODE_CONFIG_KEYS: ReadonlySet<string>;
77
90
  /**
78
91
  * Whether the file already at owned path `relPath` is this package's own
79
- * earlier output. A reserved name proves that by itself, so `content` is read
80
- * only for `opencode.json`; `undefined` content means no regular file is there.
92
+ * earlier output. A reserved name proves that by itself. Native agent files
93
+ * require the generated marker. An exact unmarked copy of the planned file is
94
+ * adopted once for upgrades from materializer versions without that marker.
95
+ * `undefined` content means no regular file is there.
81
96
  *
82
97
  * An `opencode.json` this package wrote holds the opencode schema id, only the
83
98
  * keys this package writes, and only instruction paths this package invented.
@@ -85,7 +100,7 @@ export declare const OPENCODE_CONFIG_KEYS: ReadonlySet<string>;
85
100
  * and is replaced too; a config with any other key, or with an instruction path
86
101
  * of the user's own, is refused.
87
102
  */
88
- export declare function isMaterializerOutput(relPath: string, content: () => string | undefined): boolean;
103
+ export declare function isMaterializerOutput(relPath: string, content: () => string | undefined, plannedContent?: string): boolean;
89
104
  /**
90
105
  * cwd-relative native skill dir per harness, or null if the harness has no
91
106
  * cwd skill primitive (skills live in a user/global dir the workspace plan
package/dist/harness.js CHANGED
@@ -142,6 +142,40 @@ export const MATERIALIZER_OWNED_PATHS = new Set([
142
142
  ...MATERIALIZER_RESERVED_PATHS,
143
143
  OPENCODE_CONFIG_FILE,
144
144
  ]);
145
+ 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;
146
+ const MATERIALIZER_SUBAGENT_MARKER = "# tangle-agent-profile-materialize: generated";
147
+ function materializerSubagentMarkerPrefix(relPath) {
148
+ return relPath.endsWith(".toml")
149
+ ? `${MATERIALIZER_SUBAGENT_MARKER}\n`
150
+ : `---\n${MATERIALIZER_SUBAGENT_MARKER}\n`;
151
+ }
152
+ /** Mark one generated native agent file without changing its harness meaning. */
153
+ export function markMaterializerSubagent(relPath, content) {
154
+ if (!MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
155
+ throw new Error(`Not a native agent path: ${relPath}`);
156
+ }
157
+ if (relPath.endsWith(".toml")) {
158
+ return `${materializerSubagentMarkerPrefix(relPath)}${content}`;
159
+ }
160
+ if (!content.startsWith("---\n")) {
161
+ throw new Error(`Native Markdown agent has no frontmatter: ${relPath}`);
162
+ }
163
+ return `${materializerSubagentMarkerPrefix(relPath)}${content.slice(4)}`;
164
+ }
165
+ /**
166
+ * Whether a generated file occupies a path that the profile materializer owns.
167
+ *
168
+ * Generated native agent files come from the active profile.
169
+ * The active profile must replace the prior profile version at the same path.
170
+ * Otherwise, a profile update stops every resumed session before launch.
171
+ * Explicit resources at these paths stay shared.
172
+ * The plan builder applies ownership only when the source is `generated`.
173
+ * An output marker proves ownership before replacement.
174
+ */
175
+ export function isMaterializerOwnedPath(relPath) {
176
+ return (MATERIALIZER_OWNED_PATHS.has(relPath) ||
177
+ MATERIALIZER_SUBAGENT_PATH.test(relPath));
178
+ }
145
179
  /** The top-level keys this package writes into `opencode.json`. */
146
180
  export const OPENCODE_CONFIG_KEYS = new Set([
147
181
  "$schema",
@@ -152,8 +186,10 @@ export const OPENCODE_CONFIG_KEYS = new Set([
152
186
  ]);
153
187
  /**
154
188
  * Whether the file already at owned path `relPath` is this package's own
155
- * earlier output. A reserved name proves that by itself, so `content` is read
156
- * only for `opencode.json`; `undefined` content means no regular file is there.
189
+ * earlier output. A reserved name proves that by itself. Native agent files
190
+ * require the generated marker. An exact unmarked copy of the planned file is
191
+ * adopted once for upgrades from materializer versions without that marker.
192
+ * `undefined` content means no regular file is there.
157
193
  *
158
194
  * An `opencode.json` this package wrote holds the opencode schema id, only the
159
195
  * keys this package writes, and only instruction paths this package invented.
@@ -161,9 +197,25 @@ export const OPENCODE_CONFIG_KEYS = new Set([
161
197
  * and is replaced too; a config with any other key, or with an instruction path
162
198
  * of the user's own, is refused.
163
199
  */
164
- export function isMaterializerOutput(relPath, content) {
200
+ export function isMaterializerOutput(relPath, content, plannedContent) {
165
201
  if (MATERIALIZER_RESERVED_PATHS.has(relPath))
166
202
  return true;
203
+ if (MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
204
+ const existing = content();
205
+ if (existing === undefined)
206
+ return false;
207
+ const markerPrefix = materializerSubagentMarkerPrefix(relPath);
208
+ if (existing.startsWith(markerPrefix))
209
+ return true;
210
+ if (plannedContent === undefined)
211
+ return false;
212
+ if (!plannedContent.startsWith(markerPrefix))
213
+ return false;
214
+ const legacyContent = relPath.endsWith(".toml")
215
+ ? plannedContent.slice(markerPrefix.length)
216
+ : `---\n${plannedContent.slice(markerPrefix.length)}`;
217
+ return existing === legacyContent;
218
+ }
167
219
  if (relPath !== OPENCODE_CONFIG_FILE)
168
220
  return false;
169
221
  const bytes = content();
@@ -1,4 +1,4 @@
1
- import { MATERIALIZER_OWNED_PATHS, MATERIALIZER_RESERVED_PATHS, } from "./harness.js";
1
+ import { isMaterializerOwnedPath, MATERIALIZER_RESERVED_PATHS, } from "./harness.js";
2
2
  import { workspacePlanConfigValue } from "./plan-json.js";
3
3
  import { mergeJsonFiles } from "./plan-normalize.js";
4
4
  import { formatUnsupported } from "./plan-support.js";
@@ -24,7 +24,7 @@ export function createProfilePlanBuilder(harness) {
24
24
  relPath: normalizedPath,
25
25
  content,
26
26
  source,
27
- ...(source === "generated" && MATERIALIZER_OWNED_PATHS.has(normalizedPath)
27
+ ...(source === "generated" && isMaterializerOwnedPath(normalizedPath)
28
28
  ? { ownership: "materializer" }
29
29
  : {}),
30
30
  ...(fileMode === undefined ? {} : { mode: validateFileMode(fileMode) }),
@@ -1,4 +1,4 @@
1
- import { appendSystemPromptRefusal, CODEX_SYSTEM_PROMPT_FILE, CONTEXT_FILE, OPENCODE_APPEND_SYSTEM_PROMPT_FILE, OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA, OPENCODE_PRIMARY_AGENT_PREFIX, systemPromptRefusal, } from "./harness.js";
1
+ import { appendSystemPromptRefusal, CODEX_SYSTEM_PROMPT_FILE, CONTEXT_FILE, markMaterializerSubagent, OPENCODE_APPEND_SYSTEM_PROMPT_FILE, OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA, OPENCODE_PRIMARY_AGENT_PREFIX, systemPromptRefusal, } from "./harness.js";
2
2
  import { sha256 } from "./plan-digest.js";
3
3
  import { serializeWorkspacePlanJson } from "./plan-json.js";
4
4
  export function lowerProfilePrompt(profile, harness, binds, builder) {
@@ -34,7 +34,7 @@ export function lowerProfilePrompt(profile, harness, binds, builder) {
34
34
  throw new Error(`opencode: subagent name "${primaryAgentName}" is reserved for the profile primary agent`);
35
35
  }
36
36
  plan.systemPrompt = systemPrompt;
37
- add(primaryAgentPath, ["---", "mode: primary", "---", systemPrompt, ""].join("\n"));
37
+ add(primaryAgentPath, markMaterializerSubagent(primaryAgentPath, ["---", "mode: primary", "---", systemPrompt, ""].join("\n")));
38
38
  plan.flags.push("--agent", primaryAgentName);
39
39
  }
40
40
  else {
package/dist/subagents.js CHANGED
@@ -27,7 +27,8 @@ export function materializeSubagent(h, name, sa, plan, add, unsupported) {
27
27
  fm.push(`maxTurns: ${sa.maxSteps}`);
28
28
  reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
29
29
  fm.push("---", "", sa.prompt ?? sa.description ?? name);
30
- add(`.claude/agents/${name}.md`, `${fm.join("\n")}\n`);
30
+ const relPath = `.claude/agents/${name}.md`;
31
+ add(relPath, markMaterializerSubagent(relPath, `${fm.join("\n")}\n`));
31
32
  break;
32
33
  }
33
34
  case "opencode": {
@@ -53,7 +54,8 @@ export function materializeSubagent(h, name, sa, plan, add, unsupported) {
53
54
  "maxSteps",
54
55
  ]), unsupported);
55
56
  fm.push("---", "", sa.prompt ?? "");
56
- add(`.opencode/agents/${name}.md`, `${fm.join("\n")}\n`);
57
+ const relPath = `.opencode/agents/${name}.md`;
58
+ add(relPath, markMaterializerSubagent(relPath, `${fm.join("\n")}\n`));
57
59
  break;
58
60
  }
59
61
  case "gemini": {
@@ -63,7 +65,8 @@ export function materializeSubagent(h, name, sa, plan, add, unsupported) {
63
65
  .map(([tool]) => tool)
64
66
  : undefined;
65
67
  reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model", "tools", "maxSteps"]), unsupported);
66
- add(`.gemini/agents/${name}.md`, [
68
+ const relPath = `.gemini/agents/${name}.md`;
69
+ add(relPath, markMaterializerSubagent(relPath, [
67
70
  "---",
68
71
  `name: ${name}`,
69
72
  `description: ${JSON.stringify(sa.description ?? name)}`,
@@ -75,7 +78,7 @@ export function materializeSubagent(h, name, sa, plan, add, unsupported) {
75
78
  "",
76
79
  sa.prompt ?? "",
77
80
  "",
78
- ].join("\n"));
81
+ ].join("\n")));
79
82
  break;
80
83
  }
81
84
  case "codex": {
@@ -88,7 +91,7 @@ export function materializeSubagent(h, name, sa, plan, add, unsupported) {
88
91
  if (sa.model)
89
92
  lines.push(`model = ${tomlStr(sa.model)}`);
90
93
  reportUnsupportedSubagentFields(h, name, sa, new Set(["description", "prompt", "model"]), unsupported);
91
- add(relPath, `${lines.join("\n")}\n`);
94
+ add(relPath, markMaterializerSubagent(relPath, `${lines.join("\n")}\n`));
92
95
  registerCodexAgent(plan, name, relPath, sa.description ?? name);
93
96
  break;
94
97
  }
@@ -130,4 +133,5 @@ function reportUnsupportedSubagentFields(harness, name, subagent, supported, uns
130
133
  }
131
134
  }
132
135
  }
136
+ import { markMaterializerSubagent } from "./harness.js";
133
137
  import { tomlStr } from "./plan-json.js";
@@ -62,7 +62,7 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
62
62
  // the workspace, not authorship.
63
63
  const mayReplace = (file) => existingFiles === "allow-identical" &&
64
64
  file.ownership === "materializer" &&
65
- isMaterializerOutput(file.relPath, () => regularFileContent(file.absPath));
65
+ isMaterializerOutput(file.relPath, () => regularFileContent(file.absPath), file.content);
66
66
  // Validate the complete plan before the first write so a late traversal or
67
67
  // existing symlink cannot leave a partially materialized workspace.
68
68
  for (const file of filesToApply) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.17.3",
3
+ "version": "0.18.1",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,13 @@
20
20
  "dist"
21
21
  ],
22
22
  "devDependencies": {
23
- "@tangle-network/agent-interface": "1.0.0",
23
+ "@tangle-network/agent-interface": "2.0.0",
24
24
  "@types/node": "25.6.0",
25
25
  "typescript": "^6.0.3",
26
26
  "vitest": "^4.1.10"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tangle-network/agent-interface": "^1.0.0"
29
+ "@tangle-network/agent-interface": "^1.0.0 || ^2.0.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",