@namzu/sdk 0.5.0 → 1.0.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 (302) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/dist/advisory/executor.d.ts.map +1 -1
  3. package/dist/advisory/executor.js +9 -2
  4. package/dist/advisory/executor.js.map +1 -1
  5. package/dist/advisory/executor.test.d.ts +2 -1
  6. package/dist/advisory/executor.test.d.ts.map +1 -1
  7. package/dist/advisory/executor.test.js +7 -4
  8. package/dist/advisory/executor.test.js.map +1 -1
  9. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  10. package/dist/agents/ReactiveAgent.js +2 -0
  11. package/dist/agents/ReactiveAgent.js.map +1 -1
  12. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  13. package/dist/agents/SupervisorAgent.js +13 -0
  14. package/dist/agents/SupervisorAgent.js.map +1 -1
  15. package/dist/bridge/sse/mapper.test.js +2 -2
  16. package/dist/constants/compaction/index.d.ts.map +1 -1
  17. package/dist/constants/compaction/index.js +8 -3
  18. package/dist/constants/compaction/index.js.map +1 -1
  19. package/dist/constants/sandbox/index.d.ts +21 -0
  20. package/dist/constants/sandbox/index.d.ts.map +1 -1
  21. package/dist/constants/sandbox/index.js +30 -0
  22. package/dist/constants/sandbox/index.js.map +1 -1
  23. package/dist/constants/tools/index.d.ts.map +1 -1
  24. package/dist/constants/tools/index.js +33 -2
  25. package/dist/constants/tools/index.js.map +1 -1
  26. package/dist/manager/run/persistence.d.ts.map +1 -1
  27. package/dist/manager/run/persistence.js +35 -5
  28. package/dist/manager/run/persistence.js.map +1 -1
  29. package/dist/persona/assembler.d.ts +1 -0
  30. package/dist/persona/assembler.d.ts.map +1 -1
  31. package/dist/persona/assembler.js +28 -6
  32. package/dist/persona/assembler.js.map +1 -1
  33. package/dist/provider/collect.test.js +2 -2
  34. package/dist/public-runtime.d.ts +5 -4
  35. package/dist/public-runtime.d.ts.map +1 -1
  36. package/dist/public-runtime.js +5 -4
  37. package/dist/public-runtime.js.map +1 -1
  38. package/dist/public-tools.d.ts +2 -0
  39. package/dist/public-tools.d.ts.map +1 -1
  40. package/dist/public-tools.js +2 -0
  41. package/dist/public-tools.js.map +1 -1
  42. package/dist/public-types.d.ts +3 -0
  43. package/dist/public-types.d.ts.map +1 -1
  44. package/dist/registry/index.d.ts +2 -0
  45. package/dist/registry/index.d.ts.map +1 -1
  46. package/dist/registry/index.js +1 -0
  47. package/dist/registry/index.js.map +1 -1
  48. package/dist/registry/tool/execute.d.ts.map +1 -1
  49. package/dist/registry/tool/execute.js +87 -5
  50. package/dist/registry/tool/execute.js.map +1 -1
  51. package/dist/registry/tool/execute.test.d.ts +4 -2
  52. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  53. package/dist/registry/tool/execute.test.js +112 -3
  54. package/dist/registry/tool/execute.test.js.map +1 -1
  55. package/dist/registry/toolset/catalog.d.ts +42 -0
  56. package/dist/registry/toolset/catalog.d.ts.map +1 -0
  57. package/dist/registry/toolset/catalog.js +217 -0
  58. package/dist/registry/toolset/catalog.js.map +1 -0
  59. package/dist/registry/toolset/catalog.test.d.ts +2 -0
  60. package/dist/registry/toolset/catalog.test.d.ts.map +1 -0
  61. package/dist/registry/toolset/catalog.test.js +85 -0
  62. package/dist/registry/toolset/catalog.test.js.map +1 -0
  63. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts +2 -0
  64. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts.map +1 -0
  65. package/dist/runtime/query/__tests__/deferred-tools.test.js +147 -0
  66. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -0
  67. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts +2 -0
  68. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts.map +1 -0
  69. package/dist/runtime/query/__tests__/executor-concurrency.test.js +98 -0
  70. package/dist/runtime/query/__tests__/executor-concurrency.test.js.map +1 -0
  71. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +38 -3
  72. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -1
  73. package/dist/runtime/query/__tests__/prompt.test.js +47 -2
  74. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  75. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts +2 -0
  76. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts.map +1 -0
  77. package/dist/runtime/query/__tests__/stream-recovery.test.js +126 -0
  78. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -0
  79. package/dist/runtime/query/continuation.d.ts +16 -0
  80. package/dist/runtime/query/continuation.d.ts.map +1 -0
  81. package/dist/runtime/query/continuation.js +16 -0
  82. package/dist/runtime/query/continuation.js.map +1 -0
  83. package/dist/runtime/query/executor.d.ts +3 -0
  84. package/dist/runtime/query/executor.d.ts.map +1 -1
  85. package/dist/runtime/query/executor.js +71 -3
  86. package/dist/runtime/query/executor.js.map +1 -1
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +19 -3
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts +22 -0
  91. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/index.js +227 -60
  93. package/dist/runtime/query/iteration/index.js.map +1 -1
  94. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  95. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  97. package/dist/runtime/query/prompt.d.ts.map +1 -1
  98. package/dist/runtime/query/prompt.js +21 -1
  99. package/dist/runtime/query/prompt.js.map +1 -1
  100. package/dist/runtime/query/tooling.d.ts +1 -0
  101. package/dist/runtime/query/tooling.d.ts.map +1 -1
  102. package/dist/runtime/query/tooling.js +1 -0
  103. package/dist/runtime/query/tooling.js.map +1 -1
  104. package/dist/sandbox/provider/local.d.ts.map +1 -1
  105. package/dist/sandbox/provider/local.js +32 -1
  106. package/dist/sandbox/provider/local.js.map +1 -1
  107. package/dist/session/workspace/__tests__/shared-run.test.d.ts +2 -0
  108. package/dist/session/workspace/__tests__/shared-run.test.d.ts.map +1 -0
  109. package/dist/session/workspace/__tests__/shared-run.test.js +147 -0
  110. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -0
  111. package/dist/session/workspace/index.d.ts +2 -0
  112. package/dist/session/workspace/index.d.ts.map +1 -1
  113. package/dist/session/workspace/index.js +1 -0
  114. package/dist/session/workspace/index.js.map +1 -1
  115. package/dist/session/workspace/shared-run.d.ts +81 -0
  116. package/dist/session/workspace/shared-run.d.ts.map +1 -0
  117. package/dist/session/workspace/shared-run.js +251 -0
  118. package/dist/session/workspace/shared-run.js.map +1 -0
  119. package/dist/skills/loader.d.ts.map +1 -1
  120. package/dist/skills/loader.js +36 -6
  121. package/dist/skills/loader.js.map +1 -1
  122. package/dist/skills/loader.test.d.ts +2 -0
  123. package/dist/skills/loader.test.d.ts.map +1 -0
  124. package/dist/skills/loader.test.js +65 -0
  125. package/dist/skills/loader.test.js.map +1 -0
  126. package/dist/streaming/coalesce.test.js +1 -1
  127. package/dist/tools/builtins/__tests__/edit.test.d.ts +2 -0
  128. package/dist/tools/builtins/__tests__/edit.test.d.ts.map +1 -0
  129. package/dist/tools/builtins/__tests__/edit.test.js +38 -0
  130. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -0
  131. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts +2 -0
  132. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts.map +1 -0
  133. package/dist/tools/builtins/__tests__/payload-budget.test.js +22 -0
  134. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -0
  135. package/dist/tools/builtins/__tests__/read-file.test.d.ts +2 -0
  136. package/dist/tools/builtins/__tests__/read-file.test.d.ts.map +1 -0
  137. package/dist/tools/builtins/__tests__/read-file.test.js +24 -0
  138. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -0
  139. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts +2 -0
  140. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts.map +1 -0
  141. package/dist/tools/builtins/__tests__/verify-outputs.test.js +52 -0
  142. package/dist/tools/builtins/__tests__/verify-outputs.test.js.map +1 -0
  143. package/dist/tools/builtins/__tests__/write-file.test.d.ts +2 -0
  144. package/dist/tools/builtins/__tests__/write-file.test.d.ts.map +1 -0
  145. package/dist/tools/builtins/__tests__/write-file.test.js +74 -0
  146. package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -0
  147. package/dist/tools/builtins/bash.d.ts.map +1 -1
  148. package/dist/tools/builtins/bash.js +40 -7
  149. package/dist/tools/builtins/bash.js.map +1 -1
  150. package/dist/tools/builtins/edit.d.ts +5 -2
  151. package/dist/tools/builtins/edit.d.ts.map +1 -1
  152. package/dist/tools/builtins/edit.js +114 -18
  153. package/dist/tools/builtins/edit.js.map +1 -1
  154. package/dist/tools/builtins/index.d.ts +1 -0
  155. package/dist/tools/builtins/index.d.ts.map +1 -1
  156. package/dist/tools/builtins/index.js +13 -13
  157. package/dist/tools/builtins/index.js.map +1 -1
  158. package/dist/tools/builtins/read-file.d.ts +1 -0
  159. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  160. package/dist/tools/builtins/read-file.js +23 -8
  161. package/dist/tools/builtins/read-file.js.map +1 -1
  162. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  163. package/dist/tools/builtins/search-tools.js +4 -1
  164. package/dist/tools/builtins/search-tools.js.map +1 -1
  165. package/dist/tools/builtins/verify-outputs.d.ts +5 -0
  166. package/dist/tools/builtins/verify-outputs.d.ts.map +1 -0
  167. package/dist/tools/builtins/verify-outputs.js +103 -0
  168. package/dist/tools/builtins/verify-outputs.js.map +1 -0
  169. package/dist/tools/builtins/write-file.d.ts +3 -2
  170. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  171. package/dist/tools/builtins/write-file.js +72 -12
  172. package/dist/tools/builtins/write-file.js.map +1 -1
  173. package/dist/tools/coordinator/__tests__/agent.test.d.ts +15 -0
  174. package/dist/tools/coordinator/__tests__/agent.test.d.ts.map +1 -0
  175. package/dist/tools/coordinator/__tests__/agent.test.js +142 -0
  176. package/dist/tools/coordinator/__tests__/agent.test.js.map +1 -0
  177. package/dist/tools/coordinator/__tests__/task-list.test.d.ts +13 -0
  178. package/dist/tools/coordinator/__tests__/task-list.test.d.ts.map +1 -0
  179. package/dist/tools/coordinator/__tests__/task-list.test.js +162 -0
  180. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -0
  181. package/dist/tools/coordinator/agent.d.ts +34 -0
  182. package/dist/tools/coordinator/agent.d.ts.map +1 -0
  183. package/dist/tools/coordinator/agent.js +107 -0
  184. package/dist/tools/coordinator/agent.js.map +1 -0
  185. package/dist/tools/coordinator/index.d.ts +7 -0
  186. package/dist/tools/coordinator/index.d.ts.map +1 -1
  187. package/dist/tools/coordinator/index.js +111 -21
  188. package/dist/tools/coordinator/index.js.map +1 -1
  189. package/dist/types/agent/base.d.ts +8 -0
  190. package/dist/types/agent/base.d.ts.map +1 -1
  191. package/dist/types/agent/reactive.d.ts +23 -0
  192. package/dist/types/agent/reactive.d.ts.map +1 -1
  193. package/dist/types/agent/supervisor.d.ts +41 -0
  194. package/dist/types/agent/supervisor.d.ts.map +1 -1
  195. package/dist/types/message/index.d.ts +22 -1
  196. package/dist/types/message/index.d.ts.map +1 -1
  197. package/dist/types/message/index.js +7 -2
  198. package/dist/types/message/index.js.map +1 -1
  199. package/dist/types/provider/chat.d.ts +2 -9
  200. package/dist/types/provider/chat.d.ts.map +1 -1
  201. package/dist/types/run/events.d.ts +6 -0
  202. package/dist/types/run/events.d.ts.map +1 -1
  203. package/dist/types/run/events.js.map +1 -1
  204. package/dist/types/sandbox/index.d.ts +193 -0
  205. package/dist/types/sandbox/index.d.ts.map +1 -1
  206. package/dist/types/sandbox/index.js.map +1 -1
  207. package/dist/types/skills/index.d.ts +2 -0
  208. package/dist/types/skills/index.d.ts.map +1 -1
  209. package/dist/types/tool/index.d.ts +22 -0
  210. package/dist/types/tool/index.d.ts.map +1 -1
  211. package/dist/types/toolset/index.d.ts +71 -0
  212. package/dist/types/toolset/index.d.ts.map +1 -0
  213. package/dist/types/toolset/index.js +2 -0
  214. package/dist/types/toolset/index.js.map +1 -0
  215. package/dist/types/workspace/index.d.ts +1 -0
  216. package/dist/types/workspace/index.d.ts.map +1 -1
  217. package/dist/types/workspace/shared-run.d.ts +61 -0
  218. package/dist/types/workspace/shared-run.d.ts.map +1 -0
  219. package/dist/types/workspace/shared-run.js +2 -0
  220. package/dist/types/workspace/shared-run.js.map +1 -0
  221. package/dist/verification/index.d.ts +1 -0
  222. package/dist/verification/index.d.ts.map +1 -1
  223. package/dist/verification/index.js +1 -0
  224. package/dist/verification/index.js.map +1 -1
  225. package/dist/verification/presets.d.ts +53 -0
  226. package/dist/verification/presets.d.ts.map +1 -0
  227. package/dist/verification/presets.js +70 -0
  228. package/dist/verification/presets.js.map +1 -0
  229. package/dist/verification/presets.test.d.ts +16 -0
  230. package/dist/verification/presets.test.d.ts.map +1 -0
  231. package/dist/verification/presets.test.js +79 -0
  232. package/dist/verification/presets.test.js.map +1 -0
  233. package/package.json +3 -2
  234. package/src/advisory/executor.test.ts +7 -4
  235. package/src/advisory/executor.ts +11 -2
  236. package/src/agents/ReactiveAgent.ts +2 -0
  237. package/src/agents/SupervisorAgent.ts +13 -0
  238. package/src/bridge/sse/mapper.test.ts +2 -2
  239. package/src/constants/compaction/index.ts +8 -3
  240. package/src/constants/sandbox/index.ts +37 -0
  241. package/src/constants/tools/index.ts +33 -2
  242. package/src/manager/run/persistence.ts +34 -6
  243. package/src/persona/assembler.ts +31 -8
  244. package/src/provider/collect.test.ts +2 -2
  245. package/src/public-runtime.ts +14 -1
  246. package/src/public-tools.ts +2 -0
  247. package/src/public-types.ts +7 -0
  248. package/src/registry/index.ts +7 -0
  249. package/src/registry/tool/execute.test.ts +132 -3
  250. package/src/registry/tool/execute.ts +94 -9
  251. package/src/registry/toolset/catalog.test.ts +97 -0
  252. package/src/registry/toolset/catalog.ts +283 -0
  253. package/src/runtime/query/__tests__/deferred-tools.test.ts +183 -0
  254. package/src/runtime/query/__tests__/executor-concurrency.test.ts +122 -0
  255. package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +48 -3
  256. package/src/runtime/query/__tests__/prompt.test.ts +51 -2
  257. package/src/runtime/query/__tests__/stream-recovery.test.ts +156 -0
  258. package/src/runtime/query/continuation.ts +16 -0
  259. package/src/runtime/query/executor.ts +82 -13
  260. package/src/runtime/query/index.ts +24 -3
  261. package/src/runtime/query/iteration/index.ts +263 -68
  262. package/src/runtime/query/iteration/phases/context.ts +10 -0
  263. package/src/runtime/query/prompt.ts +17 -1
  264. package/src/runtime/query/tooling.ts +2 -0
  265. package/src/sandbox/provider/local.ts +33 -0
  266. package/src/session/workspace/__tests__/shared-run.test.ts +181 -0
  267. package/src/session/workspace/index.ts +6 -0
  268. package/src/session/workspace/shared-run.ts +316 -0
  269. package/src/skills/loader.test.ts +89 -0
  270. package/src/skills/loader.ts +37 -6
  271. package/src/streaming/coalesce.test.ts +1 -1
  272. package/src/tools/builtins/__tests__/edit.test.ts +57 -0
  273. package/src/tools/builtins/__tests__/payload-budget.test.ts +29 -0
  274. package/src/tools/builtins/__tests__/read-file.test.ts +31 -0
  275. package/src/tools/builtins/__tests__/verify-outputs.test.ts +71 -0
  276. package/src/tools/builtins/__tests__/write-file.test.ts +97 -0
  277. package/src/tools/builtins/bash.ts +48 -7
  278. package/src/tools/builtins/edit.ts +162 -27
  279. package/src/tools/builtins/index.ts +13 -13
  280. package/src/tools/builtins/read-file.ts +31 -8
  281. package/src/tools/builtins/search-tools.ts +5 -1
  282. package/src/tools/builtins/verify-outputs.ts +126 -0
  283. package/src/tools/builtins/write-file.ts +83 -14
  284. package/src/tools/coordinator/__tests__/agent.test.ts +172 -0
  285. package/src/tools/coordinator/__tests__/task-list.test.ts +182 -0
  286. package/src/tools/coordinator/agent.ts +157 -0
  287. package/src/tools/coordinator/index.ts +128 -22
  288. package/src/types/agent/base.ts +8 -0
  289. package/src/types/agent/reactive.ts +25 -0
  290. package/src/types/agent/supervisor.ts +45 -0
  291. package/src/types/message/index.ts +32 -2
  292. package/src/types/provider/chat.ts +2 -9
  293. package/src/types/run/events.ts +6 -0
  294. package/src/types/sandbox/index.ts +219 -0
  295. package/src/types/skills/index.ts +4 -0
  296. package/src/types/tool/index.ts +24 -0
  297. package/src/types/toolset/index.ts +86 -0
  298. package/src/types/workspace/index.ts +9 -0
  299. package/src/types/workspace/shared-run.ts +65 -0
  300. package/src/verification/index.ts +1 -0
  301. package/src/verification/presets.test.ts +112 -0
  302. package/src/verification/presets.ts +72 -0
@@ -0,0 +1,181 @@
1
+ import { mkdtemp, readFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { SharedRunWorkspace } from '../shared-run.js'
6
+
7
+ describe('SharedRunWorkspace', () => {
8
+ it('creates a canonical workspace manifest with runtime-visible paths', async () => {
9
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
10
+ const workspace = await SharedRunWorkspace.create({
11
+ hostRoot,
12
+ runtimeRoot: '/mnt/user-data/outputs/_work',
13
+ label: 'Cowork task',
14
+ now: new Date('2026-05-08T10:00:00.000Z'),
15
+ })
16
+
17
+ const manifest = await workspace.readManifest()
18
+ expect(manifest.kind).toBe('shared-run-workspace')
19
+ expect(manifest.label).toBe('Cowork task')
20
+ expect(manifest.paths).toMatchObject({
21
+ root: '/mnt/user-data/outputs/_work',
22
+ manifest: '/mnt/user-data/outputs/_work/manifest.json',
23
+ sharedContext: '/mnt/user-data/outputs/_work/02_shared_context.md',
24
+ sources: '/mnt/user-data/outputs/_work/sources',
25
+ plans: '/mnt/user-data/outputs/_work/plans',
26
+ agents: '/mnt/user-data/outputs/_work/agents',
27
+ })
28
+ expect(workspace.refs().supervisorBriefPath).toBe(
29
+ '/mnt/user-data/outputs/_work/00_supervisor_brief.md',
30
+ )
31
+ expect(workspace.refs().sharedContextPath).toBe(
32
+ '/mnt/user-data/outputs/_work/02_shared_context.md',
33
+ )
34
+ })
35
+
36
+ it('records source inventory and seeded supervisor brief before workers launch', async () => {
37
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
38
+ const workspace = await SharedRunWorkspace.create({
39
+ hostRoot,
40
+ runtimeRoot: '/mnt/user-data/outputs/_work/',
41
+ })
42
+
43
+ await workspace.writeSourceInventory([
44
+ {
45
+ id: 'file_abc',
46
+ label: 'SBD.docx',
47
+ path: '/mnt/user-data/uploads/file_abc/SBD.docx',
48
+ kind: 'docx',
49
+ sizeBytes: 123,
50
+ },
51
+ ])
52
+ const briefPath = await workspace.seedSupervisorBrief({
53
+ briefText: 'Root task seed brief',
54
+ })
55
+ const agentPath = await workspace.registerAgentWork({
56
+ agentId: 'solution-architecture',
57
+ taskId: 'task_123',
58
+ })
59
+
60
+ expect(briefPath).toBe('/mnt/user-data/outputs/_work/00_supervisor_brief.md')
61
+ expect(agentPath).toBe('/mnt/user-data/outputs/_work/agents/solution-architecture/task_123')
62
+ const inventory = await readFile(join(hostRoot, 'sources', 'inventory.md'), 'utf8')
63
+ expect(inventory).toContain('SBD.docx')
64
+ expect(inventory).toContain('/mnt/user-data/uploads/file_abc/SBD.docx')
65
+ const manifest = await workspace.readManifest()
66
+ expect(manifest.sources).toHaveLength(1)
67
+ expect(manifest.plans[0]?.status).toBe('seeded')
68
+ expect(manifest.agents[0]?.workPath).toBe(agentPath)
69
+ })
70
+
71
+ it('preserves every agent record when workers register concurrently', async () => {
72
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
73
+ const workspace = await SharedRunWorkspace.create({
74
+ hostRoot,
75
+ runtimeRoot: '/mnt/user-data/outputs/_work',
76
+ })
77
+
78
+ await Promise.all(
79
+ Array.from({ length: 9 }, (_, index) =>
80
+ workspace.registerAgentWork({
81
+ agentId: `agent-${index + 1}`,
82
+ taskId: `task_${index + 1}`,
83
+ status: 'running',
84
+ }),
85
+ ),
86
+ )
87
+
88
+ const manifest = await workspace.readManifest()
89
+ expect(manifest.agents).toHaveLength(9)
90
+ expect(manifest.agents.map((agent) => agent.agentId).sort()).toEqual([
91
+ 'agent-1',
92
+ 'agent-2',
93
+ 'agent-3',
94
+ 'agent-4',
95
+ 'agent-5',
96
+ 'agent-6',
97
+ 'agent-7',
98
+ 'agent-8',
99
+ 'agent-9',
100
+ ])
101
+ })
102
+
103
+ it('writes and appends per-worker briefs without losing earlier sections', async () => {
104
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
105
+ const workspace = await SharedRunWorkspace.create({
106
+ hostRoot,
107
+ runtimeRoot: '/mnt/user-data/outputs/_work',
108
+ })
109
+
110
+ const briefPath = await workspace.writeAgentBrief({
111
+ agentId: 'solution-architecture',
112
+ taskId: 'task_abc',
113
+ briefText: '# Worker Brief\n\n## Assignment\n\nDraft the solution architecture section.',
114
+ })
115
+ expect(briefPath).toBe(
116
+ '/mnt/user-data/outputs/_work/agents/solution-architecture/task_abc/00_brief.md',
117
+ )
118
+
119
+ const appended = await workspace.appendAgentBrief({
120
+ agentId: 'solution-architecture',
121
+ taskId: 'task_abc',
122
+ sectionText:
123
+ '## Follow-up 2026-05-10T18:00:00.000Z\n\n### Follow-up message\n\nAdd a risks subsection.',
124
+ })
125
+ expect(appended).toBe(briefPath)
126
+
127
+ const hostBriefPath = join(
128
+ hostRoot,
129
+ 'agents',
130
+ 'solution-architecture',
131
+ 'task_abc',
132
+ '00_brief.md',
133
+ )
134
+ const content = await readFile(hostBriefPath, 'utf8')
135
+ expect(content).toContain('Draft the solution architecture section.')
136
+ expect(content).toContain('Add a risks subsection.')
137
+ // Initial seed must precede the appended section, not the other way around.
138
+ const seedIndex = content.indexOf('Draft the solution architecture')
139
+ const followIndex = content.indexOf('Add a risks subsection.')
140
+ expect(seedIndex).toBeGreaterThan(-1)
141
+ expect(followIndex).toBeGreaterThan(seedIndex)
142
+ })
143
+
144
+ it('writeTaskContext stores the user request verbatim under 01_task_context.md', async () => {
145
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
146
+ const workspace = await SharedRunWorkspace.create({
147
+ hostRoot,
148
+ runtimeRoot: '/mnt/user-data/outputs/_work',
149
+ })
150
+
151
+ const big = '# Task Context\n\n' + 'A'.repeat(40_000)
152
+ const path = await workspace.writeTaskContext(big)
153
+ expect(path).toBe('/mnt/user-data/outputs/_work/01_task_context.md')
154
+ expect(workspace.refs().taskContextPath).toBe(path)
155
+ const content = await readFile(join(hostRoot, '01_task_context.md'), 'utf8')
156
+ // Trailing newline added by the writer is OK; user content must not be truncated.
157
+ expect(content.length).toBeGreaterThanOrEqual(big.length)
158
+ expect(content).toContain('A'.repeat(40_000))
159
+ })
160
+
161
+ it('writeSharedContext stores the shared coordination packet under 02_shared_context.md', async () => {
162
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
163
+ const workspace = await SharedRunWorkspace.create({
164
+ hostRoot,
165
+ runtimeRoot: '/mnt/user-data/outputs/_work',
166
+ })
167
+
168
+ const path = await workspace.writeSharedContext('# Shared Context\n\nRead this first.')
169
+ expect(path).toBe('/mnt/user-data/outputs/_work/02_shared_context.md')
170
+ expect(workspace.refs().sharedContextPath).toBe(path)
171
+ const content = await readFile(join(hostRoot, '02_shared_context.md'), 'utf8')
172
+ expect(content).toContain('Read this first.')
173
+ })
174
+
175
+ it('rejects host paths that escape the shared workspace root', async () => {
176
+ const hostRoot = await mkdtemp(join(tmpdir(), 'namzu-shared-workspace-'))
177
+ const workspace = await SharedRunWorkspace.create({ hostRoot })
178
+
179
+ expect(() => workspace.hostPath('..', 'outside')).toThrow(/escapes root/)
180
+ })
181
+ })
@@ -24,3 +24,9 @@ export { WorkspaceBackendRegistry } from './registry.js'
24
24
 
25
25
  export { GitWorktreeDriver, parseWorktreeList } from './git-worktree.js'
26
26
  export type { ExecFile, ExecFileResult, GitWorktreeDriverConfig } from './git-worktree.js'
27
+
28
+ export { SharedRunWorkspace } from './shared-run.js'
29
+ export type {
30
+ RegisterSharedRunPlanInput,
31
+ SharedRunWorkspaceConfig,
32
+ } from './shared-run.js'
@@ -0,0 +1,316 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
2
+ import { dirname, join, normalize, relative, sep } from 'node:path'
3
+ import { posix } from 'node:path'
4
+ import type {
5
+ SharedRunWorkspaceAgentRecord,
6
+ SharedRunWorkspaceManifest,
7
+ SharedRunWorkspacePlan,
8
+ SharedRunWorkspaceRefs,
9
+ SharedRunWorkspaceSource,
10
+ } from '../../types/workspace/shared-run.js'
11
+
12
+ export interface SharedRunWorkspaceConfig {
13
+ /**
14
+ * Host-visible `_work` directory. The runtime creates and mutates files
15
+ * here before/while agents execute.
16
+ */
17
+ hostRoot: string
18
+ /**
19
+ * Agent-visible `_work` directory. In container sandboxes this is usually
20
+ * `/mnt/user-data/outputs/_work`; in local mode it can be the same
21
+ * relative path agents receive in their runtime notes.
22
+ */
23
+ runtimeRoot?: string
24
+ label?: string
25
+ now?: Date
26
+ }
27
+
28
+ export interface RegisterSharedRunPlanInput {
29
+ id?: string
30
+ briefText: string
31
+ status?: SharedRunWorkspacePlan['status']
32
+ }
33
+
34
+ export class SharedRunWorkspace {
35
+ readonly hostRoot: string
36
+ readonly runtimeRoot: string
37
+ private manifestWriteQueue: Promise<void> = Promise.resolve()
38
+
39
+ private constructor(private readonly config: Required<SharedRunWorkspaceConfig>) {
40
+ this.hostRoot = config.hostRoot
41
+ this.runtimeRoot = trimTrailingSlash(config.runtimeRoot)
42
+ }
43
+
44
+ static async create(config: SharedRunWorkspaceConfig): Promise<SharedRunWorkspace> {
45
+ const workspace = new SharedRunWorkspace({
46
+ hostRoot: config.hostRoot,
47
+ runtimeRoot: config.runtimeRoot ?? config.hostRoot,
48
+ label: config.label ?? '',
49
+ now: config.now ?? new Date(),
50
+ })
51
+ await workspace.ensure()
52
+ return workspace
53
+ }
54
+
55
+ refs(): SharedRunWorkspaceRefs {
56
+ return {
57
+ rootPath: this.runtimePath(),
58
+ manifestPath: this.runtimePath('manifest.json'),
59
+ sharedContextPath: this.runtimePath('02_shared_context.md'),
60
+ sourceInventoryPath: this.runtimePath('sources', 'inventory.md'),
61
+ supervisorBriefPath: this.runtimePath('00_supervisor_brief.md'),
62
+ taskContextPath: this.runtimePath('01_task_context.md'),
63
+ agentsPath: this.runtimePath('agents'),
64
+ }
65
+ }
66
+
67
+ hostPath(...segments: string[]): string {
68
+ return safeJoin(this.hostRoot, segments)
69
+ }
70
+
71
+ runtimePath(...segments: string[]): string {
72
+ if (segments.length === 0) return this.runtimeRoot
73
+ return posix.join(this.runtimeRoot, ...segments.map((segment) => segment.split(sep).join('/')))
74
+ }
75
+
76
+ async ensure(): Promise<void> {
77
+ await Promise.all([
78
+ mkdir(this.hostPath('sources'), { recursive: true }),
79
+ mkdir(this.hostPath('plans', 'root'), { recursive: true }),
80
+ mkdir(this.hostPath('agents'), { recursive: true }),
81
+ ])
82
+ await this.writeManifest((manifest) => manifest)
83
+ }
84
+
85
+ async readManifest(): Promise<SharedRunWorkspaceManifest> {
86
+ const content = await readFile(this.hostPath('manifest.json'), 'utf8').catch(() => '')
87
+ if (!content.trim()) return this.initialManifest()
88
+ return JSON.parse(content) as SharedRunWorkspaceManifest
89
+ }
90
+
91
+ async writeManifest(
92
+ update: (manifest: SharedRunWorkspaceManifest) => SharedRunWorkspaceManifest,
93
+ ): Promise<SharedRunWorkspaceManifest> {
94
+ const operation = this.manifestWriteQueue
95
+ .catch(() => undefined)
96
+ .then(async () => {
97
+ const current = await this.readManifest().catch(() => this.initialManifest())
98
+ const next = update({
99
+ ...current,
100
+ updatedAt: this.nowIso(),
101
+ })
102
+ await writeJsonAtomic(this.hostPath('manifest.json'), next)
103
+ return next
104
+ })
105
+ this.manifestWriteQueue = operation.then(
106
+ () => undefined,
107
+ () => undefined,
108
+ )
109
+ return operation
110
+ }
111
+
112
+ async writeSourceInventory(sources: readonly SharedRunWorkspaceSource[]): Promise<string> {
113
+ const lines = [
114
+ '# Source Inventory',
115
+ '',
116
+ sources.length
117
+ ? 'These are the canonical input references for this run. Prefer this inventory and targeted source reads over rediscovering uploads in every worker.'
118
+ : 'No user-uploaded source files were registered for this run.',
119
+ '',
120
+ ...sources.map((source) =>
121
+ [
122
+ `## ${source.label}`,
123
+ '',
124
+ `- id: ${source.id}`,
125
+ `- path: ${source.path}`,
126
+ source.kind ? `- kind: ${source.kind}` : null,
127
+ typeof source.sizeBytes === 'number' ? `- sizeBytes: ${source.sizeBytes}` : null,
128
+ '',
129
+ ]
130
+ .filter((line): line is string => line !== null)
131
+ .join('\n'),
132
+ ),
133
+ ]
134
+ const content = `${lines.join('\n').trimEnd()}\n`
135
+ await writeFile(this.hostPath('sources', 'inventory.md'), content, 'utf8')
136
+ await this.writeManifest((manifest) => ({
137
+ ...manifest,
138
+ sources: [...sources],
139
+ }))
140
+ return this.runtimePath('sources', 'inventory.md')
141
+ }
142
+
143
+ /**
144
+ * Write the canonical task-context file (`_work/01_task_context.md`).
145
+ * This is the single place the original user request lives in full; child
146
+ * workers read this path instead of receiving the request inline in their
147
+ * prompts.
148
+ */
149
+ async writeTaskContext(text: string): Promise<string> {
150
+ const relativePath = ['01_task_context.md']
151
+ await mkdir(dirnameFor(this.hostPath(...relativePath)), { recursive: true })
152
+ await writeFile(this.hostPath(...relativePath), ensureTrailingNewline(text), 'utf8')
153
+ return this.runtimePath(...relativePath)
154
+ }
155
+
156
+ /**
157
+ * Write the shared coordination packet (`_work/02_shared_context.md`).
158
+ * This is intentionally smaller and more operational than the task context:
159
+ * workers read it first, then open the full task context or source inventory
160
+ * only when their assignment needs raw wording or source-file details.
161
+ */
162
+ async writeSharedContext(text: string): Promise<string> {
163
+ const relativePath = ['02_shared_context.md']
164
+ await mkdir(dirnameFor(this.hostPath(...relativePath)), { recursive: true })
165
+ await writeFile(this.hostPath(...relativePath), ensureTrailingNewline(text), 'utf8')
166
+ return this.runtimePath(...relativePath)
167
+ }
168
+
169
+ /**
170
+ * Write a per-worker brief at `agents/<agentId>/<taskId>/00_brief.md`.
171
+ * Returns the runtime-visible path. Pair with `registerAgentWork` so the
172
+ * agent record and the brief sit under the same scratch directory.
173
+ */
174
+ async writeAgentBrief(input: {
175
+ agentId: string
176
+ taskId?: string
177
+ briefText: string
178
+ }): Promise<string> {
179
+ const taskPart = input.taskId ?? 'pending'
180
+ const relativePath = ['agents', input.agentId, taskPart, '00_brief.md']
181
+ await mkdir(dirnameFor(this.hostPath(...relativePath)), { recursive: true })
182
+ await writeFile(this.hostPath(...relativePath), ensureTrailingNewline(input.briefText), 'utf8')
183
+ return this.runtimePath(...relativePath)
184
+ }
185
+
186
+ /**
187
+ * Append a section to an existing per-worker brief, creating the file if
188
+ * it doesn't yet exist. Use for follow-up turns / continue flows so the
189
+ * brief stays authoritative across resumes — workers re-reading the brief
190
+ * on every entry will see the latest assignment delta, not just the seed
191
+ * text from initial dispatch.
192
+ */
193
+ async appendAgentBrief(input: {
194
+ agentId: string
195
+ taskId?: string
196
+ sectionText: string
197
+ }): Promise<string> {
198
+ const taskPart = input.taskId ?? 'pending'
199
+ const relativePath = ['agents', input.agentId, taskPart, '00_brief.md']
200
+ await mkdir(dirnameFor(this.hostPath(...relativePath)), { recursive: true })
201
+ const target = this.hostPath(...relativePath)
202
+ const existing = await readFile(target, 'utf8').catch(() => '')
203
+ const trimmedExisting = existing.replace(/\n*$/, '')
204
+ const next = trimmedExisting
205
+ ? `${trimmedExisting}\n\n${ensureTrailingNewline(input.sectionText)}`
206
+ : ensureTrailingNewline(input.sectionText)
207
+ await writeFile(target, next, 'utf8')
208
+ return this.runtimePath(...relativePath)
209
+ }
210
+
211
+ async seedSupervisorBrief(input: RegisterSharedRunPlanInput): Promise<string> {
212
+ const id = input.id ?? 'root'
213
+ const relativePath =
214
+ id === 'root' ? ['00_supervisor_brief.md'] : ['plans', id, 'supervisor_brief.md']
215
+ await mkdir(dirnameFor(this.hostPath(...relativePath)), { recursive: true })
216
+ await writeFile(this.hostPath(...relativePath), ensureTrailingNewline(input.briefText), 'utf8')
217
+ const briefPath = this.runtimePath(...relativePath)
218
+ const now = this.nowIso()
219
+ await this.writeManifest((manifest) => ({
220
+ ...manifest,
221
+ plans: upsertBy(manifest.plans, (plan) => plan.id, {
222
+ id,
223
+ briefPath,
224
+ status: input.status ?? 'seeded',
225
+ updatedAt: now,
226
+ }),
227
+ }))
228
+ return briefPath
229
+ }
230
+
231
+ async registerAgentWork(input: {
232
+ agentId: string
233
+ taskId?: string
234
+ status?: SharedRunWorkspaceAgentRecord['status']
235
+ }): Promise<string> {
236
+ const taskPart = input.taskId ?? 'pending'
237
+ const relativePath = ['agents', input.agentId, taskPart]
238
+ await mkdir(this.hostPath(...relativePath), { recursive: true })
239
+ const workPath = this.runtimePath(...relativePath)
240
+ const now = this.nowIso()
241
+ await this.writeManifest((manifest) => ({
242
+ ...manifest,
243
+ agents: upsertBy(manifest.agents, (record) => `${record.agentId}:${record.taskId ?? ''}`, {
244
+ agentId: input.agentId,
245
+ ...(input.taskId ? { taskId: input.taskId } : {}),
246
+ workPath,
247
+ status: input.status ?? 'assigned',
248
+ updatedAt: now,
249
+ }),
250
+ }))
251
+ return workPath
252
+ }
253
+
254
+ private initialManifest(): SharedRunWorkspaceManifest {
255
+ const now = this.config.now.toISOString()
256
+ return {
257
+ schemaVersion: 1,
258
+ kind: 'shared-run-workspace',
259
+ createdAt: now,
260
+ updatedAt: now,
261
+ ...(this.config.label ? { label: this.config.label } : {}),
262
+ paths: {
263
+ root: this.runtimePath(),
264
+ manifest: this.runtimePath('manifest.json'),
265
+ sharedContext: this.runtimePath('02_shared_context.md'),
266
+ sources: this.runtimePath('sources'),
267
+ plans: this.runtimePath('plans'),
268
+ agents: this.runtimePath('agents'),
269
+ },
270
+ sources: [],
271
+ plans: [],
272
+ agents: [],
273
+ }
274
+ }
275
+
276
+ private nowIso(): string {
277
+ return new Date().toISOString()
278
+ }
279
+ }
280
+
281
+ function safeJoin(root: string, segments: readonly string[]): string {
282
+ const fullPath = normalize(join(root, ...segments))
283
+ const rel = relative(root, fullPath)
284
+ if (rel === '' || (!rel.startsWith('..') && !rel.includes(`..${sep}`))) return fullPath
285
+ throw new Error(`SharedRunWorkspace path escapes root: ${segments.join('/')}`)
286
+ }
287
+
288
+ function trimTrailingSlash(value: string): string {
289
+ if (value === '/') return value
290
+ return value.replace(/\/+$/, '')
291
+ }
292
+
293
+ function ensureTrailingNewline(value: string): string {
294
+ return value.endsWith('\n') ? value : `${value}\n`
295
+ }
296
+
297
+ async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
298
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`
299
+ await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
300
+ await rename(tmpPath, path)
301
+ }
302
+
303
+ function dirnameFor(path: string): string {
304
+ return dirname(path)
305
+ }
306
+
307
+ function upsertBy<T>(items: readonly T[], key: (item: T) => string, next: T): T[] {
308
+ const nextKey = key(next)
309
+ let replaced = false
310
+ const updated = items.map((item) => {
311
+ if (key(item) !== nextKey) return item
312
+ replaced = true
313
+ return next
314
+ })
315
+ return replaced ? updated : [...updated, next]
316
+ }
@@ -0,0 +1,89 @@
1
+ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { discoverSkills, loadSkill } from './loader.js'
6
+
7
+ async function writeSkill(parent: string, name: string, content: string): Promise<string> {
8
+ const dir = join(parent, name)
9
+ await mkdir(dir, { recursive: true })
10
+ await writeFile(join(dir, 'SKILL.md'), content, 'utf8')
11
+ return dir
12
+ }
13
+
14
+ describe('Agent Skills loader', () => {
15
+ it('parses standard Agent Skills frontmatter fields', async () => {
16
+ const root = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
17
+ const dir = await writeSkill(
18
+ root,
19
+ 'project-documents',
20
+ [
21
+ '---',
22
+ 'name: project-documents',
23
+ 'description: Draft and edit grounded project documents.',
24
+ 'license: MIT',
25
+ 'compatibility: Requires filesystem tools',
26
+ 'allowed-tools: read write edit',
27
+ 'metadata:',
28
+ ' owner: vandal',
29
+ ' version: "1.0"',
30
+ '---',
31
+ 'Use grounded sources before drafting.',
32
+ '',
33
+ ].join('\n'),
34
+ )
35
+
36
+ const result = await loadSkill(dir, 'metadata')
37
+
38
+ expect(result.skill.metadata).toEqual({
39
+ name: 'project-documents',
40
+ description: 'Draft and edit grounded project documents.',
41
+ license: 'MIT',
42
+ compatibility: 'Requires filesystem tools',
43
+ allowedTools: 'read write edit',
44
+ metadata: {
45
+ owner: 'vandal',
46
+ version: '1.0',
47
+ },
48
+ })
49
+ expect(result.skill.body).toBeUndefined()
50
+ })
51
+
52
+ it('loads the body only when the requested disclosure level is full', async () => {
53
+ const root = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
54
+ const dir = await writeSkill(
55
+ root,
56
+ 'long-form-files',
57
+ [
58
+ '---',
59
+ 'name: long-form-files',
60
+ 'description: Create long files with bounded edit chunks.',
61
+ '---',
62
+ 'Use skeleton-first writes.',
63
+ '',
64
+ ].join('\n'),
65
+ )
66
+
67
+ const metadataOnly = await loadSkill(dir, 'metadata')
68
+ const full = await loadSkill(dir, 'full')
69
+
70
+ expect(metadataOnly.skill.body).toBeUndefined()
71
+ expect(full.skill.body).toBe('Use skeleton-first writes.')
72
+ })
73
+
74
+ it('discovers one-level skill directories deterministically', async () => {
75
+ const root = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
76
+ const a = await writeSkill(
77
+ root,
78
+ 'a-skill',
79
+ ['---', 'name: a-skill', 'description: A skill.', '---', 'A'].join('\n'),
80
+ )
81
+ const b = await writeSkill(
82
+ root,
83
+ 'b-skill',
84
+ ['---', 'name: b-skill', 'description: B skill.', '---', 'B'].join('\n'),
85
+ )
86
+
87
+ expect(await discoverSkills(root)).toEqual([a, b])
88
+ })
89
+ })
@@ -41,14 +41,29 @@ function parseSkillMd(raw: string, dirPath: string): ParsedSkillMd {
41
41
  function parseFlatYaml(raw: string, dirPath: string): SkillMetadata {
42
42
  const lines = raw.split('\n')
43
43
  const kv: Record<string, string> = {}
44
+ const metadata: Record<string, string> = {}
45
+ let section: 'metadata' | undefined
44
46
 
45
47
  for (const line of lines) {
48
+ if (!line.trim() || line.trimStart().startsWith('#')) continue
49
+
50
+ if (/^\s/.test(line)) {
51
+ if (section !== 'metadata') continue
52
+ const colonIdx = line.indexOf(':')
53
+ if (colonIdx === -1) continue
54
+ const key = line.slice(0, colonIdx).trim()
55
+ const value = normalizeYamlScalar(line.slice(colonIdx + 1).trim())
56
+ if (key && value) metadata[key] = value
57
+ continue
58
+ }
59
+
46
60
  const colonIdx = line.indexOf(':')
47
61
  if (colonIdx === -1) continue
48
62
  const key = line.slice(0, colonIdx).trim()
49
- const value = line.slice(colonIdx + 1).trim()
63
+ const value = normalizeYamlScalar(line.slice(colonIdx + 1).trim())
50
64
 
51
- kv[key] = value.replace(/^["']|["']$/g, '')
65
+ section = key === 'metadata' ? 'metadata' : undefined
66
+ if (value) kv[key] = value
52
67
  }
53
68
 
54
69
  if (!kv.name) {
@@ -61,19 +76,35 @@ function parseFlatYaml(raw: string, dirPath: string): SkillMetadata {
61
76
  validateSkillName(kv.name, dirPath)
62
77
  validateDescription(kv.description, dirPath)
63
78
 
64
- const metadata: SkillMetadata = {
79
+ const skillMetadata: SkillMetadata = {
65
80
  name: kv.name,
66
81
  description: kv.description,
67
82
  }
68
83
 
84
+ if (kv.license) {
85
+ skillMetadata.license = kv.license
86
+ }
87
+
69
88
  if (kv.compatibility) {
70
89
  if (kv.compatibility.length > 500) {
71
90
  throw new Error(`SKILL.md at "${dirPath}": compatibility exceeds 500 characters`)
72
91
  }
73
- metadata.compatibility = kv.compatibility
92
+ skillMetadata.compatibility = kv.compatibility
93
+ }
94
+
95
+ if (kv['allowed-tools']) {
96
+ skillMetadata.allowedTools = kv['allowed-tools']
97
+ }
98
+
99
+ if (Object.keys(metadata).length > 0) {
100
+ skillMetadata.metadata = metadata
74
101
  }
75
102
 
76
- return metadata
103
+ return skillMetadata
104
+ }
105
+
106
+ function normalizeYamlScalar(value: string): string {
107
+ return value.replace(/^["']|["']$/g, '').trim()
77
108
  }
78
109
 
79
110
  const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
@@ -164,5 +195,5 @@ export async function discoverSkills(parentDir: string): Promise<string[]> {
164
195
  logger.debug('Skills directory not found', { parentDir })
165
196
  }
166
197
 
167
- return dirs
198
+ return dirs.sort()
168
199
  }
@@ -98,7 +98,7 @@ describe('coalesce()', () => {
98
98
  iteration: 0,
99
99
  messageId: MID,
100
100
  toolUseId: TUID,
101
- toolName: 'Read',
101
+ toolName: 'read',
102
102
  },
103
103
  { type: 'text_delta', runId: RID, iteration: 0, messageId: MID, text: 'c' },
104
104
  ]