@tangle-network/agent-profile-materialize 0.16.0 → 0.17.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.
@@ -35,8 +35,8 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
35
35
  const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`), "opencode tool");
36
36
  const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, openCodePermissionName, (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
37
37
  if (enforceableTools.length > 0 || permissionEntries.length > 0) {
38
- add("opencode.json", serializeWorkspacePlanJson({
39
- $schema: "https://opencode.ai/config.json",
38
+ add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
39
+ $schema: OPENCODE_CONFIG_SCHEMA,
40
40
  ...(nativeTools.length > 0
41
41
  ? { tools: Object.fromEntries(nativeTools) }
42
42
  : {}),
@@ -180,7 +180,7 @@ function piExecutionControlExtensionSource(enabledTools, excludedTools) {
180
180
  }
181
181
  import { resolveNativeWebTools } from "@tangle-network/agent-interface";
182
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";
183
+ import { CLAUDE_SETTINGS_PATH, OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA, } from "./harness.js";
184
184
  import { registerClaudeSettings } from "./hooks.js";
185
185
  import { serializeWorkspacePlanJson } from "./plan-json.js";
186
186
  import { compareStrings } from "./workspace-files.js";
package/dist/harness.d.ts CHANGED
@@ -4,6 +4,8 @@ export declare function isMaterializerHarness(harness: string): harness is Harne
4
4
  export declare function assertMaterializerHarness(harness: string): asserts harness is HarnessId;
5
5
  export declare const CODEX_SYSTEM_PROMPT_FILE = ".codex/system-prompt.md";
6
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";
7
9
  export declare const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
8
10
  export declare const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
9
11
  export declare function harnessAutoLoadsContextFile(harness: HarnessId): boolean;
@@ -38,30 +40,52 @@ export declare function appendSystemPromptRefusal(harness: HarnessId): string;
38
40
  /** Project context file each harness actually reads from cwd. NEVER a constant. */
39
41
  export declare const CONTEXT_FILE: Record<HarnessId, string>;
40
42
  /**
41
- * The paths whose only possible author is this package, per
42
- * {@link PlanFile.ownership}.
43
+ * The paths a profile resource may never mount.
43
44
  *
44
45
  * Codex names no system-prompt file of its own — `model_instructions_file`
45
46
  * takes whatever path it is given — and opencode names none either, since it
46
47
  * reads exactly the files its config lists. All three names below were chosen
47
48
  * 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
+ * an earlier application into the same workspace, and a mount there could not
50
+ * survive the next application.
49
51
  *
50
52
  * That is an inference from the path name, not verified provenance: nothing
51
53
  * checks the existing bytes are prior materializer output, so a workspace that
52
54
  * carried a foreign file at one of these invented names is overwritten on the
53
55
  * first application. Accepted cost of stateless multi-turn re-application,
54
56
  * bounded by the names being ones no harness or user convention uses.
57
+ */
58
+ export declare const MATERIALIZER_RESERVED_PATHS: ReadonlySet<string>;
59
+ /**
60
+ * The paths whose generated content is wholly this package's output, per
61
+ * {@link PlanFile.ownership}: the reserved names plus `opencode.json`, which
62
+ * carries only the registrations this package generates from the profile.
63
+ *
64
+ * `opencode.json` is a documented name a user repository can carry, so the
65
+ * name alone proves nothing and a profile resource may still mount it. An
66
+ * existing file there is replaced only when {@link isMaterializerOutput}
67
+ * recognizes its bytes as this package's own earlier output.
55
68
  *
56
69
  * 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.
70
+ * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`. A user's repository can carry any of
71
+ * those with content this package cannot tell from its own, and clobbering
72
+ * one is worse than refusing the turn.
63
73
  */
64
74
  export declare const MATERIALIZER_OWNED_PATHS: ReadonlySet<string>;
75
+ /** The top-level keys this package writes into `opencode.json`. */
76
+ export declare const OPENCODE_CONFIG_KEYS: ReadonlySet<string>;
77
+ /**
78
+ * 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.
81
+ *
82
+ * An `opencode.json` this package wrote holds the opencode schema id, only the
83
+ * keys this package writes, and only instruction paths this package invented.
84
+ * A user config that holds nothing else is indistinguishable from that output
85
+ * and is replaced too; a config with any other key, or with an instruction path
86
+ * of the user's own, is refused.
87
+ */
88
+ export declare function isMaterializerOutput(relPath: string, content: () => string | undefined): boolean;
65
89
  /**
66
90
  * cwd-relative native skill dir per harness, or null if the harness has no
67
91
  * cwd skill primitive (skills live in a user/global dir the workspace plan
package/dist/harness.js CHANGED
@@ -21,6 +21,8 @@ export function assertMaterializerHarness(harness) {
21
21
  }
22
22
  export const CODEX_SYSTEM_PROMPT_FILE = ".codex/system-prompt.md";
23
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";
24
26
  export const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
25
27
  export const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
26
28
  export function harnessAutoLoadsContextFile(harness) {
@@ -101,34 +103,95 @@ export const CONTEXT_FILE = {
101
103
  openclaw: "AGENTS.md",
102
104
  };
103
105
  /**
104
- * The paths whose only possible author is this package, per
105
- * {@link PlanFile.ownership}.
106
+ * The paths a profile resource may never mount.
106
107
  *
107
108
  * Codex names no system-prompt file of its own — `model_instructions_file`
108
109
  * takes whatever path it is given — and opencode names none either, since it
109
110
  * reads exactly the files its config lists. All three names below were chosen
110
111
  * 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
+ * an earlier application into the same workspace, and a mount there could not
113
+ * survive the next application.
112
114
  *
113
115
  * That is an inference from the path name, not verified provenance: nothing
114
116
  * checks the existing bytes are prior materializer output, so a workspace that
115
117
  * carried a foreign file at one of these invented names is overwritten on the
116
118
  * first application. Accepted cost of stateless multi-turn re-application,
117
119
  * 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
120
  */
127
- export const MATERIALIZER_OWNED_PATHS = new Set([
121
+ export const MATERIALIZER_RESERVED_PATHS = new Set([
128
122
  CODEX_SYSTEM_PROMPT_FILE,
129
123
  OPENCODE_APPEND_SYSTEM_PROMPT_FILE,
130
124
  CONTEXT_FILE.opencode,
131
125
  ]);
126
+ /**
127
+ * The paths whose generated content is wholly this package's output, per
128
+ * {@link PlanFile.ownership}: the reserved names plus `opencode.json`, which
129
+ * carries only the registrations this package generates from the profile.
130
+ *
131
+ * `opencode.json` is a documented name a user repository can carry, so the
132
+ * name alone proves nothing and a profile resource may still mount it. An
133
+ * existing file there is replaced only when {@link isMaterializerOutput}
134
+ * recognizes its bytes as this package's own earlier output.
135
+ *
136
+ * Deliberately absent: `.gemini/system.md` (gemini's own documented name),
137
+ * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`. A user's repository can carry any of
138
+ * those with content this package cannot tell from its own, and clobbering
139
+ * one is worse than refusing the turn.
140
+ */
141
+ export const MATERIALIZER_OWNED_PATHS = new Set([
142
+ ...MATERIALIZER_RESERVED_PATHS,
143
+ OPENCODE_CONFIG_FILE,
144
+ ]);
145
+ /** The top-level keys this package writes into `opencode.json`. */
146
+ export const OPENCODE_CONFIG_KEYS = new Set([
147
+ "$schema",
148
+ "instructions",
149
+ "mcp",
150
+ "tools",
151
+ "permission",
152
+ ]);
153
+ /**
154
+ * 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.
157
+ *
158
+ * An `opencode.json` this package wrote holds the opencode schema id, only the
159
+ * keys this package writes, and only instruction paths this package invented.
160
+ * A user config that holds nothing else is indistinguishable from that output
161
+ * and is replaced too; a config with any other key, or with an instruction path
162
+ * of the user's own, is refused.
163
+ */
164
+ export function isMaterializerOutput(relPath, content) {
165
+ if (MATERIALIZER_RESERVED_PATHS.has(relPath))
166
+ return true;
167
+ if (relPath !== OPENCODE_CONFIG_FILE)
168
+ return false;
169
+ const bytes = content();
170
+ if (bytes === undefined)
171
+ return false;
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(bytes);
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
180
+ return false;
181
+ }
182
+ const config = parsed;
183
+ if (config.$schema !== OPENCODE_CONFIG_SCHEMA)
184
+ return false;
185
+ if (!Object.keys(config).every((key) => OPENCODE_CONFIG_KEYS.has(key))) {
186
+ return false;
187
+ }
188
+ const instructions = config.instructions;
189
+ if (instructions === undefined)
190
+ return true;
191
+ return (Array.isArray(instructions) &&
192
+ instructions.every((entry) => entry === OPENCODE_APPEND_SYSTEM_PROMPT_FILE ||
193
+ entry === CONTEXT_FILE.opencode));
194
+ }
132
195
  /**
133
196
  * cwd-relative native skill dir per harness, or null if the harness has no
134
197
  * cwd skill primitive (skills live in a user/global dir the workspace plan
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export { HARNESS_SKILL_DIRS, harnessAutoLoadsContextFile, harnessContextFile, is
7
7
  export type { OpenCodeMcpConfig } from "./mcp.js";
8
8
  export { openCodeMcpConfig } from "./mcp.js";
9
9
  export { modelIdsMatch, qualifyModelId } from "./model-id.js";
10
+ export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, type PiModelsConfigFile, type PiModelsConfigInput, } from "./pi-models-config.js";
10
11
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
11
12
  export { agentCandidateProfilePlanMaterial, canonicalizeAgentCandidateProfilePlan, captureAgentCandidateProfilePlan, hashWorkspacePlan, } from "./plan-digest.js";
12
13
  export { takeSystemPromptBinding } from "./plan-prompt.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.
4
4
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_HARNESSES, skillDirForHarness, } from "./harness.js";
5
5
  export { openCodeMcpConfig } from "./mcp.js";
6
6
  export { modelIdsMatch, qualifyModelId } from "./model-id.js";
7
+ export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, } from "./pi-models-config.js";
7
8
  export { applyWorkspacePlan, applyWorkspacePlanWithSecretProvider, } from "./plan-application.js";
8
9
  export { agentCandidateProfilePlanMaterial, canonicalizeAgentCandidateProfilePlan, captureAgentCandidateProfilePlan, hashWorkspacePlan, } from "./plan-digest.js";
9
10
  export { takeSystemPromptBinding } from "./plan-prompt.js";
package/dist/mcp.js CHANGED
@@ -47,8 +47,8 @@ export function materializeMcp(h, mcp, plan, add, unsupported) {
47
47
  }
48
48
  case "opencode":
49
49
  // opencode.json "mcp" block (local|remote).
50
- add("opencode.json", serializeWorkspacePlanJson({
51
- $schema: "https://opencode.ai/config.json",
50
+ add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
51
+ $schema: OPENCODE_CONFIG_SCHEMA,
52
52
  mcp: openCodeMcpConfig(mcp),
53
53
  }));
54
54
  break;
@@ -238,6 +238,7 @@ export function openCodeMcpConfig(mcp) {
238
238
  }
239
239
  return out;
240
240
  }
241
+ import { OPENCODE_CONFIG_FILE, OPENCODE_CONFIG_SCHEMA } from "./harness.js";
241
242
  import { sha256 } from "./plan-digest.js";
242
243
  import { concatWorkspacePlanArguments, joinWorkspacePlanArguments, serializeWorkspacePlanJson, tomlProfileConfigValue, tomlStr, } from "./plan-json.js";
243
244
  import { setPlanEnvironment } from "./profile-plan.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One custom-model shape for upstream Pi and the Prime fork.
3
+ *
4
+ * Both agents read the same `models.json` provider schema.
5
+ * Callers own the file lifecycle, but they must not build different payloads.
6
+ */
7
+ export interface PiModelsConfigInput {
8
+ /** Provider key in models.json and the matching `--provider` value. */
9
+ providerName: string;
10
+ /** Model id in models.json and the matching `--model` value. */
11
+ modelId: string;
12
+ /** Provider endpoint. OpenAI-compatible endpoints include `/v1`. */
13
+ baseUrl: string;
14
+ /** Literal in-box credential or an environment reference. */
15
+ apiKey: string;
16
+ /** Pi wire protocol. */
17
+ api?: "anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses";
18
+ contextWindow?: number;
19
+ maxTokens?: number;
20
+ reasoning?: boolean;
21
+ }
22
+ export interface PiModelsConfigFile {
23
+ path: string;
24
+ content: string;
25
+ mode: number;
26
+ }
27
+ export declare const PI_MODELS_CONFIG_PATH = ".pi/agent/models.json";
28
+ export declare function buildPiModelsConfig(input: PiModelsConfigInput): Record<string, unknown>;
29
+ /** Build the owner-only Pi file used by headless and interactive processes. */
30
+ export declare function buildPiModelsConfigFile(input: PiModelsConfigInput): PiModelsConfigFile;
@@ -0,0 +1,46 @@
1
+ export const PI_MODELS_CONFIG_PATH = ".pi/agent/models.json";
2
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
3
+ export function buildPiModelsConfig(input) {
4
+ for (const [field, value] of [
5
+ ["providerName", input.providerName],
6
+ ["modelId", input.modelId],
7
+ ["baseUrl", input.baseUrl],
8
+ ["apiKey", input.apiKey],
9
+ ]) {
10
+ if (typeof value !== "string" || value.trim().length === 0) {
11
+ throw new Error(`pi models.json requires a non-empty "${field}"; ` +
12
+ "the caller must supply provider, model, baseUrl, and apiKey");
13
+ }
14
+ }
15
+ return {
16
+ providers: {
17
+ [input.providerName]: {
18
+ name: input.providerName,
19
+ baseUrl: input.baseUrl,
20
+ api: input.api ?? "openai-completions",
21
+ apiKey: input.apiKey,
22
+ models: [
23
+ {
24
+ id: input.modelId,
25
+ name: input.modelId,
26
+ reasoning: input.reasoning ?? false,
27
+ input: ["text", "image"],
28
+ contextWindow: input.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
29
+ ...(input.maxTokens !== undefined
30
+ ? { maxTokens: input.maxTokens }
31
+ : {}),
32
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ };
38
+ }
39
+ /** Build the owner-only Pi file used by headless and interactive processes. */
40
+ export function buildPiModelsConfigFile(input) {
41
+ return {
42
+ path: PI_MODELS_CONFIG_PATH,
43
+ content: `${JSON.stringify(buildPiModelsConfig(input), null, 2)}\n`,
44
+ mode: 0o600,
45
+ };
46
+ }
@@ -28,16 +28,20 @@ export interface PlanFile {
28
28
  * Who owns the PATH — which decides whether an existing file there may be
29
29
  * replaced.
30
30
  *
31
- * `"materializer"` = a path this package invented. No harness convention
32
- * names it and no user authors it, so a file already there can only be this
33
- * materializer's own output from an earlier application into the same
34
- * workspace, and replacing it is how turn two of a resumed session gets the
35
- * current profile instead of the previous one.
31
+ * `"materializer"` = a path whose generated content is wholly this
32
+ * package's output: a name this package invented, or `opencode.json`, which
33
+ * carries only the registrations generated from the profile. A file already
34
+ * there is this materializer's own output from an earlier application into
35
+ * the same workspace, and replacing it is how turn two of a resumed session
36
+ * gets the current profile instead of the previous one. Application replaces
37
+ * it only when the existing bytes prove that provenance — by the name for an
38
+ * invented path, by the content for `opencode.json`.
36
39
  *
37
40
  * `"shared"` (the default, and every other file) = a path the user or the
38
- * harness also owns — `CLAUDE.md`, `AGENTS.md`, `opencode.json`,
39
- * `.gemini/system.md` are all documented, user-authorable names. An existing
40
- * file there may be the caller's own work, so it is never replaced.
41
+ * harness also owns — `CLAUDE.md`, `AGENTS.md`, `.gemini/system.md` are all
42
+ * documented, user-authorable names whose content this package cannot tell
43
+ * from its own. An existing file there may be the caller's own work, so it is
44
+ * never replaced.
41
45
  *
42
46
  * Optional because hand-constructed plans have no path provenance to report;
43
47
  * absent reads as `"shared"`, the safe answer.
@@ -1,4 +1,4 @@
1
- import { MATERIALIZER_OWNED_PATHS } from "./harness.js";
1
+ import { MATERIALIZER_OWNED_PATHS, 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";
@@ -47,7 +47,7 @@ export function createProfilePlanBuilder(harness) {
47
47
  if (previousDimension) {
48
48
  throw new Error(`Duplicate profile resource path: ${normalizedPath} (${previousDimension}, ${dimension})`);
49
49
  }
50
- if (MATERIALIZER_OWNED_PATHS.has(normalizedPath)) {
50
+ if (MATERIALIZER_RESERVED_PATHS.has(normalizedPath)) {
51
51
  throw new Error(`Profile resource path is reserved by the materializer: ${normalizedPath} (${dimension}). ` +
52
52
  "This package writes that path itself and replaces it on re-application, so a mount " +
53
53
  "there cannot survive; choose another path.");
@@ -1,4 +1,4 @@
1
- import { appendSystemPromptRefusal, CODEX_SYSTEM_PROMPT_FILE, CONTEXT_FILE, OPENCODE_APPEND_SYSTEM_PROMPT_FILE, OPENCODE_PRIMARY_AGENT_PREFIX, systemPromptRefusal, } from "./harness.js";
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";
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) {
@@ -97,8 +97,8 @@ export function lowerProfilePrompt(profile, harness, binds, builder) {
97
97
  }
98
98
  }
99
99
  if (openCodeInstructionFiles.length > 0) {
100
- add("opencode.json", serializeWorkspacePlanJson({
101
- $schema: "https://opencode.ai/config.json",
100
+ add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
101
+ $schema: OPENCODE_CONFIG_SCHEMA,
102
102
  instructions: openCodeInstructionFiles,
103
103
  }));
104
104
  }
@@ -37,11 +37,14 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
37
37
  runtimeOnlyFiles: privateRuntimeFiles,
38
38
  };
39
39
  }
40
- // A path this package invented can only hold this package's own earlier
41
- // output, so re-applying into the same workspace replaces it rather than
42
- // failing the turn. `reject` still refuses: that policy is "nothing may
43
- // pre-exist here", which is a statement about the workspace, not authorship.
44
- const mayReplace = (file) => existingFiles === "allow-identical" && file.ownership === "materializer";
40
+ // An owned path holds this package's own earlier output, so re-applying
41
+ // into the same workspace replaces it rather than failing the turn — once
42
+ // the bytes already there prove that provenance. `reject` still refuses:
43
+ // that policy is "nothing may pre-exist here", which is a statement about
44
+ // the workspace, not authorship.
45
+ const mayReplace = (file) => existingFiles === "allow-identical" &&
46
+ file.ownership === "materializer" &&
47
+ isMaterializerOutput(file.relPath, () => regularFileContent(file.absPath));
45
48
  // Validate the complete plan before the first write so a late traversal or
46
49
  // existing symlink cannot leave a partially materialized workspace.
47
50
  for (const file of files) {
@@ -170,6 +173,11 @@ function workspaceFileExists(pathValue) {
170
173
  throw error;
171
174
  }
172
175
  }
176
+ function regularFileContent(pathValue) {
177
+ return lstatSync(pathValue).isFile()
178
+ ? readFileSync(pathValue, "utf8")
179
+ : undefined;
180
+ }
173
181
  function workspaceFileMatches(file) {
174
182
  const stats = lstatSync(file.absPath);
175
183
  return (stats.isFile() &&
@@ -327,6 +335,7 @@ export function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = ha
327
335
  import { createHash, randomUUID } from "node:crypto";
328
336
  import { closeSync, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
329
337
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
338
+ import { isMaterializerOutput } from "./harness.js";
330
339
  import { hashWorkspacePlan } from "./plan-digest.js";
331
340
  import { normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
332
341
  import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",