@tangle-network/agent-profile-materialize 0.19.0 → 0.19.2

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
@@ -58,28 +58,74 @@ export declare const CONTEXT_FILE: Record<HarnessId, string>;
58
58
  * bounded by the names being ones no harness or user convention uses.
59
59
  */
60
60
  export declare const MATERIALIZER_RESERVED_PATHS: ReadonlySet<string>;
61
+ /**
62
+ * The context files this package owns by MARKER rather than by name:
63
+ * `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `HERMES.md`.
64
+ *
65
+ * Each is a documented name a repository can already carry, so the name proves
66
+ * nothing. A generated one carries the line {@link markMaterializerContextFile}
67
+ * writes, and only that line lets the next materialization replace it.
68
+ *
69
+ * opencode's context file is absent because it is a RESERVED name this package
70
+ * invented, already owned by the name alone.
71
+ */
72
+ export declare const MATERIALIZER_CONTEXT_PATHS: ReadonlySet<string>;
61
73
  /**
62
74
  * The paths whose generated content is wholly this package's output, per
63
- * {@link PlanFile.ownership}: the reserved names plus `opencode.json`, which
64
- * carries only the registrations this package generates from the profile.
75
+ * {@link PlanFile.ownership}: the reserved names, the context files, and
76
+ * `opencode.json`, which carries only the registrations this package generates
77
+ * from the profile.
78
+ *
79
+ * `opencode.json` and every context file is a documented name a user repository
80
+ * can carry, so the name alone proves nothing and a profile resource may still
81
+ * mount one. An existing file there is replaced only when
82
+ * {@link isMaterializerOutput} recognizes its bytes as this package's own
83
+ * earlier output.
65
84
  *
66
- * `opencode.json` is a documented name a user repository can carry, so the
67
- * name alone proves nothing and a profile resource may still mount it. An
68
- * existing file there is replaced only when {@link isMaterializerOutput}
69
- * recognizes its bytes as this package's own earlier output.
85
+ * Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
86
+ * lowers to. The line a marker would have to add is the reason. A REPLACEMENT
87
+ * is the caller's exact bytes and this package may add none of its own, so that
88
+ * path keeps refusing. A context file carries `prompt.instructions`, which this
89
+ * package already composes — it joins the blocks and appends the final newline
90
+ * — so one more composed line is inside the same authority.
70
91
  *
71
- * Deliberately absent: `.gemini/system.md` (gemini's own documented name),
72
- * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`. A user's repository can carry any of
73
- * those with content this package cannot tell from its own, and clobbering
74
- * one is worse than refusing the turn.
92
+ * That line, not "which channel the harness reads it on", is what separates the
93
+ * two. `GEMINI.md` is owned even though gemini lowers its context file into the
94
+ * system-prompt position as an ADDITION ({@link appendSystemPromptRefusal}),
95
+ * because the intent it carries is still instructions this package composed.
75
96
  */
76
97
  export declare const MATERIALIZER_OWNED_PATHS: ReadonlySet<string>;
77
98
  /** Mark one generated native agent file without changing its harness meaning. */
78
99
  export declare function markMaterializerSubagent(relPath: string, content: string): string;
100
+ /**
101
+ * Mark one generated context file so the NEXT materialization can prove this
102
+ * package wrote the bytes it is about to replace.
103
+ *
104
+ * The marker is an HTML comment on line 1. CommonMark closes an HTML block at
105
+ * the line holding `-->`, so every instruction below it parses exactly as it
106
+ * did without the line, and the line itself states no instruction. It is one
107
+ * more line this package composes into a file it already composes: it goes on
108
+ * a context file, which carries `prompt.instructions`, and NEVER on a
109
+ * system-prompt REPLACEMENT, whose bytes are the caller's exact text
110
+ * (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
111
+ * package invented and needs no marker to be owned).
112
+ *
113
+ * It carries the digest of everything BELOW it, which the subagent marker does
114
+ * not need. `CLAUDE.md` is claude-code's own memory file and the agent appends
115
+ * to it mid-session, so a bare marker would still sit on line 1 of a file the
116
+ * agent has since rewritten. Ownership has to mean "these are still our bytes",
117
+ * not "this package wrote here once".
118
+ *
119
+ * A reserved context path — opencode's invented instruction file — is returned
120
+ * unchanged: its NAME already proves the provenance, and nothing else may mount
121
+ * there.
122
+ */
123
+ export declare function markMaterializerContextFile(relPath: string, content: string): string;
79
124
  /**
80
125
  * Whether a generated file occupies a path that the profile materializer owns.
81
126
  *
82
- * Generated native agent files come from the active profile.
127
+ * Generated native agent files and generated context files come from the active
128
+ * profile.
83
129
  * The active profile must replace the prior profile version at the same path.
84
130
  * Otherwise, a profile update stops every resumed session before launch.
85
131
  * Explicit resources at these paths stay shared.
@@ -92,9 +138,11 @@ export declare const OPENCODE_CONFIG_KEYS: ReadonlySet<string>;
92
138
  /**
93
139
  * Whether the file already at owned path `relPath` is this package's own
94
140
  * earlier output. A reserved name proves that by itself. Native agent files
95
- * require the generated marker. An exact unmarked copy of the planned file is
96
- * adopted once for upgrades from materializer versions without that marker.
97
- * `undefined` content means no regular file is there.
141
+ * require the generated marker; context files require the marker AND the body
142
+ * digest it commits to, so an agent that appended to its memory file is no
143
+ * longer holding this package's bytes. An exact unmarked copy of the planned
144
+ * file is adopted once for upgrades from materializer versions without that
145
+ * marker. `undefined` content means no regular file is there.
98
146
  *
99
147
  * An `opencode.json` this package wrote holds the opencode schema id, only the
100
148
  * keys this package writes, and only instruction paths this package invented.
package/dist/harness.js CHANGED
@@ -130,27 +130,54 @@ export const MATERIALIZER_RESERVED_PATHS = new Set([
130
130
  OPENCODE_APPEND_SYSTEM_PROMPT_FILE,
131
131
  CONTEXT_FILE.opencode,
132
132
  ]);
133
+ /** Every path any harness reads as its project context file. */
134
+ const CONTEXT_FILE_PATHS = new Set(Object.values(CONTEXT_FILE));
135
+ /**
136
+ * The context files this package owns by MARKER rather than by name:
137
+ * `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `HERMES.md`.
138
+ *
139
+ * Each is a documented name a repository can already carry, so the name proves
140
+ * nothing. A generated one carries the line {@link markMaterializerContextFile}
141
+ * writes, and only that line lets the next materialization replace it.
142
+ *
143
+ * opencode's context file is absent because it is a RESERVED name this package
144
+ * invented, already owned by the name alone.
145
+ */
146
+ export const MATERIALIZER_CONTEXT_PATHS = new Set([...CONTEXT_FILE_PATHS].filter((path) => !MATERIALIZER_RESERVED_PATHS.has(path)));
133
147
  /**
134
148
  * The paths whose generated content is wholly this package's output, per
135
- * {@link PlanFile.ownership}: the reserved names plus `opencode.json`, which
136
- * carries only the registrations this package generates from the profile.
149
+ * {@link PlanFile.ownership}: the reserved names, the context files, and
150
+ * `opencode.json`, which carries only the registrations this package generates
151
+ * from the profile.
137
152
  *
138
- * `opencode.json` is a documented name a user repository can carry, so the
139
- * name alone proves nothing and a profile resource may still mount it. An
140
- * existing file there is replaced only when {@link isMaterializerOutput}
141
- * recognizes its bytes as this package's own earlier output.
153
+ * `opencode.json` and every context file is a documented name a user repository
154
+ * can carry, so the name alone proves nothing and a profile resource may still
155
+ * mount one. An existing file there is replaced only when
156
+ * {@link isMaterializerOutput} recognizes its bytes as this package's own
157
+ * earlier output.
142
158
  *
143
- * Deliberately absent: `.gemini/system.md` (gemini's own documented name),
144
- * `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`. A user's repository can carry any of
145
- * those with content this package cannot tell from its own, and clobbering
146
- * one is worse than refusing the turn.
159
+ * Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
160
+ * lowers to. The line a marker would have to add is the reason. A REPLACEMENT
161
+ * is the caller's exact bytes and this package may add none of its own, so that
162
+ * path keeps refusing. A context file carries `prompt.instructions`, which this
163
+ * package already composes — it joins the blocks and appends the final newline
164
+ * — so one more composed line is inside the same authority.
165
+ *
166
+ * That line, not "which channel the harness reads it on", is what separates the
167
+ * two. `GEMINI.md` is owned even though gemini lowers its context file into the
168
+ * system-prompt position as an ADDITION ({@link appendSystemPromptRefusal}),
169
+ * because the intent it carries is still instructions this package composed.
147
170
  */
148
171
  export const MATERIALIZER_OWNED_PATHS = new Set([
149
172
  ...MATERIALIZER_RESERVED_PATHS,
173
+ ...MATERIALIZER_CONTEXT_PATHS,
150
174
  OPENCODE_CONFIG_FILE,
151
175
  ]);
152
176
  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;
153
- const MATERIALIZER_SUBAGENT_MARKER = "# tangle-agent-profile-materialize: generated";
177
+ /** The one provenance token every generated-file marker carries. */
178
+ const MATERIALIZER_MARKER = "tangle-agent-profile-materialize: generated";
179
+ const MATERIALIZER_SUBAGENT_MARKER = `# ${MATERIALIZER_MARKER}`;
180
+ const MATERIALIZER_CONTEXT_MARKER_NOTE = "do not edit; the next profile materialization replaces this file";
154
181
  function materializerSubagentMarkerPrefix(relPath) {
155
182
  return relPath.endsWith(".toml")
156
183
  ? `${MATERIALIZER_SUBAGENT_MARKER}\n`
@@ -169,10 +196,58 @@ export function markMaterializerSubagent(relPath, content) {
169
196
  }
170
197
  return `${materializerSubagentMarkerPrefix(relPath)}${content.slice(4)}`;
171
198
  }
199
+ /** The marker line a generated context file whose body is `body` carries. */
200
+ function materializerContextMarker(body) {
201
+ const digest = createHash("sha256").update(body, "utf8").digest("hex");
202
+ return `<!-- ${MATERIALIZER_MARKER} sha256:${digest}; ${MATERIALIZER_CONTEXT_MARKER_NOTE} -->`;
203
+ }
204
+ /**
205
+ * Mark one generated context file so the NEXT materialization can prove this
206
+ * package wrote the bytes it is about to replace.
207
+ *
208
+ * The marker is an HTML comment on line 1. CommonMark closes an HTML block at
209
+ * the line holding `-->`, so every instruction below it parses exactly as it
210
+ * did without the line, and the line itself states no instruction. It is one
211
+ * more line this package composes into a file it already composes: it goes on
212
+ * a context file, which carries `prompt.instructions`, and NEVER on a
213
+ * system-prompt REPLACEMENT, whose bytes are the caller's exact text
214
+ * (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
215
+ * package invented and needs no marker to be owned).
216
+ *
217
+ * It carries the digest of everything BELOW it, which the subagent marker does
218
+ * not need. `CLAUDE.md` is claude-code's own memory file and the agent appends
219
+ * to it mid-session, so a bare marker would still sit on line 1 of a file the
220
+ * agent has since rewritten. Ownership has to mean "these are still our bytes",
221
+ * not "this package wrote here once".
222
+ *
223
+ * A reserved context path — opencode's invented instruction file — is returned
224
+ * unchanged: its NAME already proves the provenance, and nothing else may mount
225
+ * there.
226
+ */
227
+ export function markMaterializerContextFile(relPath, content) {
228
+ if (!CONTEXT_FILE_PATHS.has(relPath)) {
229
+ throw new Error(`Not a harness context file: ${relPath}`);
230
+ }
231
+ if (!MATERIALIZER_CONTEXT_PATHS.has(relPath))
232
+ return content;
233
+ return `${materializerContextMarker(content)}\n${content}`;
234
+ }
235
+ /**
236
+ * Whether `existing` is a context file this package wrote and nothing edited
237
+ * afterwards: line 1 is exactly the marker computed over every byte below it.
238
+ */
239
+ function isMarkedContextOutput(existing) {
240
+ const lineEnd = existing.indexOf("\n");
241
+ if (lineEnd === -1)
242
+ return false;
243
+ return (existing.slice(0, lineEnd) ===
244
+ materializerContextMarker(existing.slice(lineEnd + 1)));
245
+ }
172
246
  /**
173
247
  * Whether a generated file occupies a path that the profile materializer owns.
174
248
  *
175
- * Generated native agent files come from the active profile.
249
+ * Generated native agent files and generated context files come from the active
250
+ * profile.
176
251
  * The active profile must replace the prior profile version at the same path.
177
252
  * Otherwise, a profile update stops every resumed session before launch.
178
253
  * Explicit resources at these paths stay shared.
@@ -194,9 +269,11 @@ export const OPENCODE_CONFIG_KEYS = new Set([
194
269
  /**
195
270
  * Whether the file already at owned path `relPath` is this package's own
196
271
  * earlier output. A reserved name proves that by itself. Native agent files
197
- * require the generated marker. An exact unmarked copy of the planned file is
198
- * adopted once for upgrades from materializer versions without that marker.
199
- * `undefined` content means no regular file is there.
272
+ * require the generated marker; context files require the marker AND the body
273
+ * digest it commits to, so an agent that appended to its memory file is no
274
+ * longer holding this package's bytes. An exact unmarked copy of the planned
275
+ * file is adopted once for upgrades from materializer versions without that
276
+ * marker. `undefined` content means no regular file is there.
200
277
  *
201
278
  * An `opencode.json` this package wrote holds the opencode schema id, only the
202
279
  * keys this package writes, and only instruction paths this package invented.
@@ -223,6 +300,23 @@ export function isMaterializerOutput(relPath, content, plannedContent) {
223
300
  : `---\n${plannedContent.slice(markerPrefix.length)}`;
224
301
  return existing === legacyContent;
225
302
  }
303
+ if (MATERIALIZER_CONTEXT_PATHS.has(relPath)) {
304
+ const existing = content();
305
+ if (existing === undefined)
306
+ return false;
307
+ if (isMarkedContextOutput(existing))
308
+ return true;
309
+ // A workspace materialized before this marker existed carries the body
310
+ // alone. Adopt it once, and only when it is byte-identical to the body this
311
+ // plan writes, so upgrading the package does not turn every resumed session
312
+ // into a refusal. A body that differs is either a user's file or an earlier
313
+ // profile's, and neither can be told apart without the marker.
314
+ if (plannedContent === undefined)
315
+ return false;
316
+ if (!isMarkedContextOutput(plannedContent))
317
+ return false;
318
+ return existing === plannedContent.slice(plannedContent.indexOf("\n") + 1);
319
+ }
226
320
  if (relPath !== OPENCODE_CONFIG_FILE)
227
321
  return false;
228
322
  const bytes = content();
@@ -287,3 +381,4 @@ export function skillDirForHarness(harness) {
287
381
  assertMaterializerHarness(harness);
288
382
  return HARNESS_SKILL_DIRS[harness];
289
383
  }
384
+ import { createHash } from "node:crypto";
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { applyAgentCandidateWorkspacePlan, materializeCandidateProfile, } from "
3
3
  export type { AgentCandidateWorkspacePlan, AgentCandidateWorkspacePlanReceipt, ApplyAgentCandidateWorkspacePlanOptions, MaterializeCandidateProfileOptions, } from "./candidate-types.js";
4
4
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
5
5
  export type { HarnessId } from "./harness.js";
6
- export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_HARNESSES, skillDirForHarness, } from "./harness.js";
6
+ export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
7
7
  export type { OpenCodeMcpConfig } from "./mcp.js";
8
8
  export { openCodeMcpConfig } from "./mcp.js";
9
9
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Stable public facade for profile materialization and workspace application. */
2
2
  export { applyAgentCandidateWorkspacePlan, materializeCandidateProfile, } from "./candidate.js";
3
3
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
4
- export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_HARNESSES, skillDirForHarness, } from "./harness.js";
4
+ export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
5
5
  export { openCodeMcpConfig } from "./mcp.js";
6
6
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
7
7
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, resolvePiRuntimePaths, } from "./pi-models-config.js";
@@ -33,19 +33,23 @@ export interface PlanFile {
33
33
  * replaced.
34
34
  *
35
35
  * `"materializer"` = a path whose generated content is wholly this
36
- * package's output: a name this package invented, or `opencode.json`, which
37
- * carries only the registrations generated from the profile. A file already
38
- * there is this materializer's own output from an earlier application into
39
- * the same workspace, and replacing it is how turn two of a resumed session
40
- * gets the current profile instead of the previous one. Application replaces
41
- * it only when the existing bytes prove that provenance — by the name for an
42
- * invented path, by the content for `opencode.json`.
36
+ * package's output: a name this package invented, the generated context file
37
+ * (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `HERMES.md`), or `opencode.json`,
38
+ * which carries only the registrations generated from the profile. A file
39
+ * already there is this materializer's own output from an earlier application
40
+ * into the same workspace, and replacing it is how turn two of a resumed
41
+ * session — or the first turn of a branched one — gets the current profile
42
+ * instead of the previous one. Application replaces it only when the existing
43
+ * bytes prove that provenance — by the name for an invented path, by a marker
44
+ * line committing to the body digest for a context file, by the content for
45
+ * `opencode.json`.
43
46
  *
44
47
  * `"shared"` (the default, and every other file) = a path the user or the
45
- * harness also owns — `CLAUDE.md`, `AGENTS.md`, `.gemini/system.md` are all
46
- * documented, user-authorable names whose content this package cannot tell
47
- * from its own. An existing file there may be the caller's own work, so it is
48
- * never replaced.
48
+ * harness also owns. `.gemini/system.md` is a documented, user-authorable
49
+ * name whose content this package cannot tell from its own, and so is a
50
+ * context file mounted as an explicit profile resource rather than generated.
51
+ * An existing file there may be the caller's own work, so it is never
52
+ * replaced.
49
53
  *
50
54
  * Optional because hand-constructed plans have no path provenance to report;
51
55
  * absent reads as `"shared"`, the safe answer.
@@ -1,4 +1,4 @@
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";
1
+ import { appendSystemPromptRefusal, CODEX_SYSTEM_PROMPT_FILE, CONTEXT_FILE, markMaterializerContextFile, 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, instructionsRoot, builder) {
@@ -90,7 +90,7 @@ export function lowerProfilePrompt(profile, harness, binds, instructionsRoot, bu
90
90
  }
91
91
  else {
92
92
  const instructionPath = CONTEXT_FILE[harness];
93
- addAtRoot(instructionsRoot, instructionPath, `${instructions.join("\n\n")}\n`);
93
+ addAtRoot(instructionsRoot, instructionPath, markMaterializerContextFile(instructionPath, `${instructions.join("\n\n")}\n`));
94
94
  if (harness === "opencode") {
95
95
  openCodeInstructionFiles.push(instructionPath);
96
96
  }
@@ -29,6 +29,43 @@ export function resolveInlineRef(ref) {
29
29
  }
30
30
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
31
31
  const collapse = (s) => s.replace(/\s+/g, " ").trim();
32
+ function frontmatterScalar(value) {
33
+ const trimmed = value.trim();
34
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
35
+ try {
36
+ const parsed = JSON.parse(trimmed);
37
+ if (typeof parsed === "string")
38
+ return parsed;
39
+ }
40
+ catch {
41
+ return trimmed;
42
+ }
43
+ }
44
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
45
+ return trimmed.slice(1, -1).replaceAll("''", "'");
46
+ }
47
+ return trimmed;
48
+ }
49
+ function frontmatterValue(frontmatter, field) {
50
+ const lines = frontmatter.split("\n");
51
+ const index = lines.findIndex((line) => line.startsWith(`${field}:`));
52
+ if (index < 0)
53
+ return undefined;
54
+ const inline = (lines[index] ?? "").slice(field.length + 1).trim();
55
+ if (inline && ![">", "|", ">-", "|-"].includes(inline)) {
56
+ return frontmatterScalar(inline);
57
+ }
58
+ const block = [];
59
+ for (let offset = index + 1; offset < lines.length; offset += 1) {
60
+ const line = lines[offset] ?? "";
61
+ if (/^\s+\S/.test(line))
62
+ block.push(line.trim());
63
+ else if (line.trim() !== "")
64
+ break;
65
+ }
66
+ const value = collapse(block.join(" "));
67
+ return value || undefined;
68
+ }
32
69
  const DEFAULT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
33
70
  const MAX_RESOURCE_BYTES = 10 * 1024 * 1024;
34
71
  const MAX_PROFILE_RESOURCE_BYTES = 32 * 1024 * 1024;
@@ -210,30 +247,17 @@ export function normalizeSkillMd(name, raw) {
210
247
  const m = raw.match(FRONTMATTER_RE);
211
248
  const fm = m?.[1] ?? "";
212
249
  const body = (m?.[2] ?? raw).trim();
213
- let desc = `Skill ${name}. Use when the task matches its domain.`;
214
- if (fm) {
215
- const lines = fm.split("\n");
216
- const i = lines.findIndex((l) => /^description:/.test(l));
217
- if (i >= 0) {
218
- const inline = (lines[i] ?? "").replace(/^description:\s*/, "").trim();
219
- if (inline && ![">", "|", ">-", "|-"].includes(inline))
220
- desc = collapse(inline);
221
- else {
222
- const buf = [];
223
- for (let j = i + 1; j < lines.length; j++) {
224
- const ln = lines[j] ?? "";
225
- if (/^\s+\S/.test(ln))
226
- buf.push(ln.trim());
227
- else if (ln.trim() === "")
228
- continue;
229
- else
230
- break;
231
- }
232
- if (buf.length)
233
- desc = collapse(buf.join(" "));
234
- }
235
- }
250
+ if (fm &&
251
+ frontmatterValue(fm, "name") === name &&
252
+ (frontmatterValue(fm, "description")?.trim().length ?? 0) > 0) {
253
+ return raw;
236
254
  }
255
+ let desc = `Skill ${name}. Use when the task matches its domain.`;
256
+ const declaredDescription = fm
257
+ ? frontmatterValue(fm, "description")
258
+ : undefined;
259
+ if (declaredDescription)
260
+ desc = collapse(declaredDescription);
237
261
  return `---\nname: ${name}\ndescription: ${JSON.stringify(desc)}\n---\n\n${body}\n`;
238
262
  }
239
263
  import { basename } from "node:path";
@@ -1,4 +1,4 @@
1
- export declare const compareStrings: (a: string, b: string) => 0 | 1 | -1;
1
+ export declare const compareStrings: (a: string, b: string) => 1 | -1 | 0;
2
2
  export declare const DEFAULT_FILE_MODE = 420;
3
3
  export declare const PRIVATE_FILE_MODE = 384;
4
4
  export declare const EXECUTABLE_FILE_MODE = 493;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
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
  "repository": {