simt-dev 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +169 -80
  3. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  4. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  5. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  7. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  8. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  9. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  10. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  11. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  12. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  13. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  14. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  15. package/ecosystems/bun.json +6 -0
  16. package/ecosystems/cargo.json +25 -0
  17. package/ecosystems/composer.json +6 -0
  18. package/ecosystems/dart.json +24 -0
  19. package/ecosystems/deno.json +21 -0
  20. package/ecosystems/devcontainers.json +6 -0
  21. package/ecosystems/docker-compose.json +6 -0
  22. package/ecosystems/docker.json +6 -0
  23. package/ecosystems/dotnet.json +25 -0
  24. package/ecosystems/elixir.json +23 -0
  25. package/ecosystems/gitsubmodule.json +6 -0
  26. package/ecosystems/go.json +25 -0
  27. package/ecosystems/gradle.json +13 -0
  28. package/ecosystems/maven.json +23 -0
  29. package/ecosystems/node.json +25 -0
  30. package/ecosystems/python.json +20 -0
  31. package/ecosystems/ruby.json +23 -0
  32. package/ecosystems/swift.json +25 -0
  33. package/ecosystems/terraform.json +6 -0
  34. package/ecosystems/uv.json +6 -0
  35. package/harnesses/antigravity.json +162 -0
  36. package/harnesses/claude.json +206 -0
  37. package/harnesses/codex.json +136 -0
  38. package/harnesses/copilot.json +88 -0
  39. package/harnesses/cursor.json +140 -0
  40. package/harnesses/deepseek.json +99 -0
  41. package/harnesses/gemini.json +122 -0
  42. package/harnesses/opencode.json +173 -0
  43. package/package.json +3 -1
  44. package/rules/mdc/intake-questions.mdc +8 -0
  45. package/src/advice/advice.mjs +3 -2
  46. package/src/agenthooks/render.mjs +86 -180
  47. package/src/agents/render.mjs +15 -18
  48. package/src/api.mjs +7 -5
  49. package/src/argv.mjs +19 -0
  50. package/src/bench/ablate.mjs +3 -4
  51. package/src/bench/adapters.json +155 -113
  52. package/src/bench/adapters.mjs +466 -0
  53. package/src/bench/arms.mjs +333 -0
  54. package/src/bench/grade.mjs +12 -26
  55. package/src/bench/graders.mjs +42 -0
  56. package/src/bench/mine.mjs +17 -535
  57. package/src/bench/probe-ledger.mjs +149 -0
  58. package/src/bench/probes.catalog.mjs +918 -0
  59. package/src/bench/probes.mjs +15 -1037
  60. package/src/bench/render.mjs +111 -0
  61. package/src/bench/report.mjs +116 -971
  62. package/src/bench/run.mjs +164 -881
  63. package/src/bench/variants.mjs +4 -5
  64. package/src/bench/workspace.mjs +160 -0
  65. package/src/brief/assemble.mjs +11 -13
  66. package/src/ci/dependabot.mjs +25 -55
  67. package/src/commands/agent.mjs +1 -9
  68. package/src/commands/agenthook.mjs +66 -35
  69. package/src/commands/bench.mjs +2 -2
  70. package/src/commands/ci.mjs +11 -13
  71. package/src/commands/docs.mjs +88 -0
  72. package/src/commands/gate.mjs +10 -13
  73. package/src/commands/harness.mjs +3 -3
  74. package/src/commands/help.mjs +44 -3
  75. package/src/commands/hooks.mjs +20 -20
  76. package/src/commands/ignore.mjs +11 -7
  77. package/src/commands/impact.mjs +4 -8
  78. package/src/commands/index.mjs +49 -9
  79. package/src/commands/init-scan.mjs +90 -0
  80. package/src/commands/init.mjs +122 -104
  81. package/src/commands/lease.mjs +67 -18
  82. package/src/commands/map.mjs +10 -5
  83. package/src/commands/mcp.mjs +2 -17
  84. package/src/commands/repoindex.mjs +8 -8
  85. package/src/commands/rules.mjs +8 -4
  86. package/src/commands/shared.mjs +12 -16
  87. package/src/commands/skills.mjs +5 -6
  88. package/src/commands/skillscompile.mjs +3 -3
  89. package/src/commands/skillsinstall.mjs +5 -4
  90. package/src/commands/slice.mjs +199 -64
  91. package/src/commands/spec.mjs +78 -62
  92. package/src/commands/status.mjs +24 -12
  93. package/src/commands/sync.mjs +14 -14
  94. package/src/commands/tests.mjs +12 -12
  95. package/src/commands/tokens.mjs +19 -24
  96. package/src/commands/upgrade.mjs +8 -4
  97. package/src/commands/why.mjs +6 -13
  98. package/src/compare/hooksstate.mjs +2 -6
  99. package/src/compare/learn.mjs +3 -1
  100. package/src/decisions/index.mjs +3 -2
  101. package/src/docs/commands.mjs +65 -0
  102. package/src/ecosystem/records.mjs +219 -0
  103. package/src/explain.mjs +31 -0
  104. package/src/gates/index.mjs +385 -0
  105. package/src/graph/compile.mjs +8 -4
  106. package/src/graph/load.mjs +2 -1
  107. package/src/graph/spec.mjs +10 -8
  108. package/src/harness/records.mjs +143 -0
  109. package/src/hooks/capability.mjs +5 -3
  110. package/src/hooks/dispatcher.mjs +26 -32
  111. package/src/hooks/gates.mjs +12 -2
  112. package/src/hooks/resolve.mjs +2 -7
  113. package/src/hooks/where.mjs +2 -7
  114. package/src/identity.mjs +2 -11
  115. package/src/index/coverage.mjs +58 -0
  116. package/src/index/extract.mjs +6 -16
  117. package/src/index/gate.mjs +1 -1
  118. package/src/index/govern.mjs +26 -9
  119. package/src/index/impact.mjs +4 -8
  120. package/src/index/map.mjs +14 -15
  121. package/src/index/open.mjs +173 -0
  122. package/src/index/persist.mjs +224 -57
  123. package/src/index/store.mjs +82 -39
  124. package/src/index/tests.mjs +2 -6
  125. package/src/journal.mjs +16 -11
  126. package/src/kernel/card.mjs +37 -63
  127. package/src/kernel/checks.mjs +4 -10
  128. package/src/lease.mjs +102 -7
  129. package/src/ledger/index.mjs +124 -17
  130. package/src/mcp/paginate.mjs +33 -0
  131. package/src/mcp/schemas.mjs +512 -0
  132. package/src/mcp/server.mjs +49 -9
  133. package/src/mcp/tools-facts.mjs +326 -0
  134. package/src/mcp/tools-read.mjs +221 -0
  135. package/src/mcp/tools.mjs +4 -1005
  136. package/src/{bench → measure}/corpus.mjs +1 -1
  137. package/src/measure/metrics.mjs +236 -0
  138. package/src/measure/records.mjs +211 -0
  139. package/src/measure/stats.mjs +538 -0
  140. package/src/measure/task-schema.mjs +125 -0
  141. package/src/measure/tasks.mjs +69 -0
  142. package/src/memory/decide.mjs +6 -15
  143. package/src/memory/lesson-text.mjs +61 -0
  144. package/src/memory/lessons.mjs +29 -82
  145. package/src/name.mjs +7 -0
  146. package/src/output/index.mjs +122 -31
  147. package/src/packs/manage.mjs +15 -9
  148. package/src/plan/index.mjs +1 -1
  149. package/src/plugins.mjs +22 -16
  150. package/src/prbody.mjs +7 -11
  151. package/src/projection/loaders.mjs +7 -692
  152. package/src/projection/manifest.mjs +10 -435
  153. package/src/projection/mdc.mjs +7 -8
  154. package/src/projection/project.mjs +126 -221
  155. package/src/projection/region.mjs +561 -0
  156. package/src/projection/rules.mjs +16 -547
  157. package/src/projection/surfaces.mjs +43 -210
  158. package/src/projection/targets.mjs +5 -5
  159. package/src/repo-root.mjs +13 -1
  160. package/src/resolve/eval.mjs +2 -1
  161. package/src/resolve/fingerprint.mjs +0 -7
  162. package/src/resolve/resolve.mjs +1 -41
  163. package/src/review/critic.mjs +8 -12
  164. package/src/run.mjs +98 -0
  165. package/src/secrets.mjs +14 -22
  166. package/src/state/events.mjs +129 -0
  167. package/src/status-snapshot.mjs +233 -0
  168. package/src/status.mjs +2 -6
  169. package/src/sys/git.mjs +82 -0
  170. package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
  171. package/src/testing/parsers.mjs +265 -0
  172. package/src/testing/patches.mjs +23 -0
  173. package/src/testing/paths.mjs +49 -0
  174. package/src/testing/runners.mjs +186 -0
  175. package/src/testing/vault.mjs +68 -0
  176. package/src/text/frontmatter.mjs +49 -0
  177. package/src/text/tokens.mjs +52 -0
  178. package/src/toolwords.mjs +1 -1
  179. package/src/util/fs.mjs +20 -0
  180. package/src/util/sort.mjs +12 -0
  181. package/src/workspace.mjs +3 -1
  182. package/src/write/plan.mjs +82 -0
  183. package/templates/plan/tracker.json +1 -0
  184. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  185. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  186. package/src/bench/tasks.mjs +0 -36
@@ -9,17 +9,14 @@
9
9
  // misconfigured the command needs to be able to ask what it exposes without hanging, and
10
10
  // a command that only speaks when spoken to cannot answer that.
11
11
 
12
- import { readFile } from 'node:fs/promises';
13
- import { fileURLToPath } from 'node:url';
14
- import { dirname, join } from 'node:path';
15
-
16
12
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
17
13
  import { TOOLS } from '../mcp/tools.mjs';
18
14
  import { PROTOCOL_VERSION, SUPPORTED_VERSIONS, serve } from '../mcp/server.mjs';
19
15
  import { CLI } from '../name.mjs';
16
+ import { packageVersion } from '../package-version.mjs';
20
17
 
21
18
  export async function mcpCommand(positional, flags) {
22
- const { version } = await packageVersion();
19
+ const version = packageVersion();
23
20
 
24
21
  if (flags.list === true) {
25
22
  if (isJson(flags)) {
@@ -62,16 +59,4 @@ export async function mcpCommand(positional, flags) {
62
59
  } catch (error) {
63
60
  return fail(`mcp transport failed: ${error.message}`, EXIT.FAIL, `run: ${CLI} mcp --list to check the surface loads`);
64
61
  }
65
- }
66
-
67
- async function packageVersion() {
68
- try {
69
- const here = dirname(fileURLToPath(import.meta.url));
70
- const pkg = JSON.parse(await readFile(join(here, '..', '..', 'package.json'), 'utf8'));
71
- return { version: pkg.version };
72
- } catch {
73
- // A missing version must not stop the server: the client already knows which
74
- // build it launched, and a hard failure here would look like a protocol bug.
75
- return { version: 'unknown' };
76
- }
77
62
  }
@@ -2,12 +2,11 @@
2
2
  // content: it is gitignored, and a second run with the same blobs writes the same
3
3
  // rows (ADR-089).
4
4
 
5
- import { execFile } from 'node:child_process';
6
-
7
5
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
8
6
  import { CLI } from '../name.mjs';
9
7
  import { JSONL_FILE, SQLITE_FILE, reindex } from '../index/store.mjs';
10
8
  import { commandRoot } from '../repo-root.mjs';
9
+ import { git } from '../sys/git.mjs';
11
10
 
12
11
  export async function indexCommand(args, flags) {
13
12
  if (args.length > 0) return fail(`unknown index argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} index`);
@@ -51,10 +50,11 @@ export async function indexCommand(args, flags) {
51
50
  return EXIT.OK;
52
51
  }
53
52
 
54
- function isIgnored(root, file) {
55
- return new Promise((resolve) => {
56
- execFile('git', ['check-ignore', '-q', '--', file], { cwd: root }, (error) => {
57
- resolve(!error);
58
- });
59
- });
53
+ async function isIgnored(root, file) {
54
+ try {
55
+ await git(root, ['check-ignore', '-q', '--', file]);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
60
  }
@@ -18,7 +18,8 @@ import { readdir } from 'node:fs/promises';
18
18
  import { repoContext } from '../context.mjs';
19
19
  import { join } from 'node:path';
20
20
  import { renderDecisionsIndex } from '../decisions/index-doc.mjs';
21
- import { commitJournal, journaledWriteFile } from '../journal.mjs';
21
+ import { applyPlan, change } from '../write/plan.mjs';
22
+ import { readBefore } from '../diff.mjs';
22
23
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
23
24
  import { loadRules, select } from '../projection/project.mjs';
24
25
  import { loadMdcRules } from '../projection/mdc.mjs';
@@ -529,9 +530,12 @@ export async function decisionsCommand(args, flags) {
529
530
  // table - the index is not internal state even though it is generated.
530
531
  if (flags.yes === true) {
531
532
  const rootDir = root ?? process.cwd();
532
- const journal = [];
533
- await journaledWriteFile(rootDir, journal, join(rootDir, 'docs', 'decisions-index.md'), renderDecisionsIndex(rows));
534
- await commitJournal(rootDir, 'rules decisions', journal);
533
+ const index = join(rootDir, 'docs', 'decisions-index.md');
534
+ await applyPlan(
535
+ rootDir,
536
+ [change({ path: index, before: await readBefore(index), after: renderDecisionsIndex(rows), owner: 'rules decisions', reason: 'the index follows the decision files' })],
537
+ { mode: { apply: true }, label: 'rules decisions' },
538
+ );
535
539
  emit(`wrote docs/decisions-index.md (${rows.length} decisions)`);
536
540
  return EXIT.OK;
537
541
  }
@@ -29,6 +29,7 @@ import {
29
29
  } from '../projection/shared.mjs';
30
30
  import { buildManifest, declaredShared, manifestPath, manifestText, readManifest, writeManifest } from '../projection/manifest.mjs';
31
31
  import { desiredRegion } from '../projection/rules.mjs';
32
+ import { plainNumber } from '../util/fs.mjs';
32
33
 
33
34
  /**
34
35
  * Everything `plan` and `install` both need, computed once.
@@ -76,9 +77,9 @@ async function gather(flags) {
76
77
  */
77
78
  function divergentNote(d, total) {
78
79
  if (d.reason === 'coverage') {
79
- return `${d.id} carried by ${plain(d.roots.length)} of ${plain(total)} repositories, so each region keeps its own`;
80
+ return `${d.id} carried by ${plainNumber(d.roots.length)} of ${plainNumber(total)} repositories, so each region keeps its own`;
80
81
  }
81
- return `${d.id} rendered differently in ${plain(d.roots.length)} repositories, so each region keeps its own`;
82
+ return `${d.id} rendered differently in ${plainNumber(d.roots.length)} repositories, so each region keeps its own`;
82
83
  }
83
84
 
84
85
  /** Why a selected rule cannot leave any region, in the words its reason deserves. */
@@ -98,7 +99,7 @@ function excludedNote({ id, why }) {
98
99
  function capNote(target, bytes) {
99
100
  if (target.path === null || target.cap === null || bytes <= target.cap.bytes) return null;
100
101
  return [
101
- `${target.surface} ${plain(bytes)} B exceeds the ${plain(target.cap.bytes)} B it allows for ${target.cap.appliesTo}`,
102
+ `${target.surface} ${plainNumber(bytes)} B exceeds the ${plainNumber(target.cap.bytes)} B it allows for ${target.cap.appliesTo}`,
102
103
  ` ${target.cap.why}`,
103
104
  ` the rules above that limit are not read; move rules this workspace does not share back into each region, or drop the shared copy for this surface`,
104
105
  ].join('\n');
@@ -157,10 +158,10 @@ export async function sharedPlanCommand(_args, flags) {
157
158
  }
158
159
 
159
160
  const lines = [
160
- `shared ${target.root} ${plain(roots.length)} repositories (${target.source})`,
161
- `rules ${plain(shared.length)} of ${plain(candidates)} ids carried once ${plain(bytes)} B`,
161
+ `shared ${target.root} ${plainNumber(roots.length)} repositories (${target.source})`,
162
+ `rules ${plainNumber(shared.length)} of ${plainNumber(candidates)} ids carried once ${plainNumber(bytes)} B`,
162
163
  ];
163
- for (const r of shared) lines.push(` ${r.id} ${r.priority ?? 'unclassified'} ${plain(Buffer.byteLength(r.rendered))} B ${r.source}`);
164
+ for (const r of shared) lines.push(` ${r.id} ${r.priority ?? 'unclassified'} ${plainNumber(Buffer.byteLength(r.rendered))} B ${r.source}`);
164
165
  if (divergent.length > 0) {
165
166
  // Named, not summarised: a reader who expected a python rule here and did not get it
166
167
  // is owed the reason, and "divergent: 2" is not a reason.
@@ -171,9 +172,9 @@ export async function sharedPlanCommand(_args, flags) {
171
172
  lines.push(`left in every region ${excluded.length} rule(s) no root can hand over:`);
172
173
  for (const e of excluded) lines.push(` ${excludedNote(e)}`);
173
174
  }
174
- lines.push(`saves ${plain(save)} B across ${plain(roots.length)} regions`);
175
+ lines.push(`saves ${plainNumber(save)} B across ${plainNumber(roots.length)} regions`);
175
176
  for (const r of rows) {
176
- lines.push(` ${r.name} ${plain(r.bytes)} B ${r.mode} ${r.rules.length > 0 ? r.rules.join(', ') : '(carries nothing extra)'}`);
177
+ lines.push(` ${r.name} ${plainNumber(r.bytes)} B ${r.mode} ${r.rules.length > 0 ? r.rules.join(', ') : '(carries nothing extra)'}`);
177
178
  }
178
179
  lines.push('writes');
179
180
  for (const t of targets) {
@@ -297,7 +298,7 @@ export async function sharedInstallCommand(_args, flags) {
297
298
  }
298
299
  emit(
299
300
  [
300
- `shared ${plain(shared.length)} rule(s), ${plain(Buffer.byteLength(sharedBody(shared)))} B`,
301
+ `shared ${plainNumber(shared.length)} rule(s), ${plainNumber(Buffer.byteLength(sharedBody(shared)))} B`,
301
302
  ...states.map((s) =>
302
303
  s.path === null
303
304
  ? ` paste ${s.surface} no user-level file is recorded for it`
@@ -326,8 +327,8 @@ export async function sharedInstallCommand(_args, flags) {
326
327
  }
327
328
 
328
329
  const lines = [
329
- `shared installed for ${plain(roots.length)} repositories`,
330
- `rules ${plain(shared.length)} carried once ${shared.map((r) => r.id).join(', ')}`,
330
+ `shared installed for ${plainNumber(roots.length)} repositories`,
331
+ `rules ${plainNumber(shared.length)} carried once ${shared.map((r) => r.id).join(', ')}`,
331
332
  ];
332
333
  for (const w of written) {
333
334
  // `inject`'s word for a file with no region in it is "added"; the word for a file
@@ -417,11 +418,6 @@ function describeShared(state, targets) {
417
418
  return `${targets.length} user-level file(s) carry every rule this repository handed over`;
418
419
  }
419
420
 
420
- /** `1234567`, never `1,234,567`: two machines must print the same bytes (ADR-013). */
421
- function plain(n) {
422
- return String(n);
423
- }
424
-
425
421
  /** The directory name, for a line that has to name several repositories at once. */
426
422
  function basename(path) {
427
423
  return path.split('/').filter(Boolean).pop() ?? path;
@@ -20,12 +20,14 @@ import { bodyFile, sourcePath } from '../resolve/body.mjs';
20
20
  import { unknownSkills, scoreCorpus } from '../resolve/eval.mjs';
21
21
  import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
22
22
  import { fingerprint } from '../resolve/fingerprint.mjs';
23
- import { tokenize, rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
23
+ import { rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
24
+ import { tokenize } from '../text/tokens.mjs';
24
25
  import { localSkillNodes } from '../resolve/local.mjs';
25
26
  import { nearMatches } from '../resolve/near.mjs';
26
27
  import { CLI } from '../name.mjs';
27
28
  import { parsePathList } from '../argv.mjs';
28
29
  import { commandRoot } from '../repo-root.mjs';
30
+ import { gitText } from '../sys/git.mjs';
29
31
 
30
32
  export { parsePathList };
31
33
 
@@ -34,11 +36,8 @@ const DEFAULT_BUDGET = 20480;
34
36
  /** git status paths, so path signals come from the real diff. */
35
37
  export async function dirtyPaths(root) {
36
38
  try {
37
- const { execFile } = await import('node:child_process');
38
- const { promisify } = await import('node:util');
39
- const run = promisify(execFile);
40
- const { stdout } = await run('git', ['-C', root, 'status', '--porcelain=v1'], { maxBuffer: 8 << 20 });
41
- return stdout
39
+ const stdout = await gitText(root, ['status', '--porcelain=v1'], { maxBuffer: 8 << 20 });
40
+ return (stdout ?? '')
42
41
  .split('\n')
43
42
  .filter(Boolean)
44
43
  .map((line) => line.slice(3).trim())
@@ -14,9 +14,9 @@ import {
14
14
  evaluateSkillTriggers,
15
15
  proposeSkillDescription,
16
16
  } from '../graph/compile.mjs';
17
- import { loadRunRecords } from '../bench/report.mjs';
18
- import { extractPatchFiles } from '../bench/grade.mjs';
19
- import { resolveVaultDir } from '../bench/mine.mjs';
17
+ import { loadRunRecords } from '../measure/records.mjs';
18
+ import { extractPatchFiles } from '../testing/patches.mjs';
19
+ import { resolveVaultDir } from '../testing/vault.mjs';
20
20
  import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
21
21
  import { nearMatches } from '../resolve/near.mjs';
22
22
  import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
@@ -39,6 +39,7 @@ import { CLI } from '../name.mjs';
39
39
  import { MANIFEST_FILE } from '../name.mjs';
40
40
  import { nameProblems } from '../graph/spec.mjs';
41
41
  import { commandRoot } from '../repo-root.mjs';
42
+ import { parseFrontmatter } from '../text/frontmatter.mjs';
42
43
 
43
44
  /**
44
45
  * Where skills live in this repo. The estate already has a convention - a
@@ -498,13 +499,13 @@ async function dirEntries(root) {
498
499
  */
499
500
  export async function wrapFlat(src, name) {
500
501
  const text = await readFile(src, 'utf8');
501
- const fm = /^---\n([\s\S]*?)\n---\n?/.exec(text);
502
- if (!fm) return `---\nname: ${name}\n---\n\n${text.trim()}\n`;
503
- const keys = fm[1]
502
+ const { front, block } = parseFrontmatter(text);
503
+ if (front === null) return `---\nname: ${name}\n---\n\n${text.trim()}\n`;
504
+ const keys = front
504
505
  .split('\n')
505
506
  .filter((l) => !/^name:/.test(l))
506
507
  .join('\n');
507
- return `---\nname: ${name}\n${keys}\n---\n\n${text.slice(fm[0].length).trim()}\n`;
508
+ return `---\nname: ${name}\n${keys}\n---\n\n${text.slice(block.length).trim()}\n`;
508
509
  }
509
510
 
510
511
  async function fileSha(path) {