@namzu/sdk 0.6.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.
- package/CHANGELOG.md +362 -0
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +9 -2
- package/dist/advisory/executor.js.map +1 -1
- package/dist/advisory/executor.test.d.ts +2 -1
- package/dist/advisory/executor.test.d.ts.map +1 -1
- package/dist/advisory/executor.test.js +7 -4
- package/dist/advisory/executor.test.js.map +1 -1
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +2 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +7 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/sse/mapper.test.js +2 -2
- package/dist/constants/compaction/index.d.ts.map +1 -1
- package/dist/constants/compaction/index.js +8 -3
- package/dist/constants/compaction/index.js.map +1 -1
- package/dist/constants/sandbox/index.d.ts +21 -0
- package/dist/constants/sandbox/index.d.ts.map +1 -1
- package/dist/constants/sandbox/index.js +30 -0
- package/dist/constants/sandbox/index.js.map +1 -1
- package/dist/constants/tools/index.d.ts.map +1 -1
- package/dist/constants/tools/index.js +33 -2
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +35 -5
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/persona/assembler.d.ts +1 -0
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +28 -6
- package/dist/persona/assembler.js.map +1 -1
- package/dist/provider/collect.test.js +2 -2
- package/dist/public-runtime.d.ts +5 -4
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +2 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +87 -5
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/execute.test.d.ts +4 -2
- package/dist/registry/tool/execute.test.d.ts.map +1 -1
- package/dist/registry/tool/execute.test.js +112 -3
- package/dist/registry/tool/execute.test.js.map +1 -1
- package/dist/registry/toolset/catalog.d.ts +42 -0
- package/dist/registry/toolset/catalog.d.ts.map +1 -0
- package/dist/registry/toolset/catalog.js +217 -0
- package/dist/registry/toolset/catalog.js.map +1 -0
- package/dist/registry/toolset/catalog.test.d.ts +2 -0
- package/dist/registry/toolset/catalog.test.d.ts.map +1 -0
- package/dist/registry/toolset/catalog.test.js +85 -0
- package/dist/registry/toolset/catalog.test.js.map +1 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.js +147 -0
- package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.js +98 -0
- package/dist/runtime/query/__tests__/executor-concurrency.test.js.map +1 -0
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +38 -3
- package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -1
- package/dist/runtime/query/__tests__/prompt.test.js +47 -2
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
- package/dist/runtime/query/__tests__/stream-recovery.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.js +126 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -0
- package/dist/runtime/query/continuation.d.ts +16 -0
- package/dist/runtime/query/continuation.d.ts.map +1 -0
- package/dist/runtime/query/continuation.js +16 -0
- package/dist/runtime/query/continuation.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +3 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +71 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +19 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +22 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +227 -60
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +21 -1
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +32 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/workspace/__tests__/shared-run.test.d.ts +2 -0
- package/dist/session/workspace/__tests__/shared-run.test.d.ts.map +1 -0
- package/dist/session/workspace/__tests__/shared-run.test.js +147 -0
- package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -0
- package/dist/session/workspace/index.d.ts +2 -0
- package/dist/session/workspace/index.d.ts.map +1 -1
- package/dist/session/workspace/index.js +1 -0
- package/dist/session/workspace/index.js.map +1 -1
- package/dist/session/workspace/shared-run.d.ts +81 -0
- package/dist/session/workspace/shared-run.d.ts.map +1 -0
- package/dist/session/workspace/shared-run.js +251 -0
- package/dist/session/workspace/shared-run.js.map +1 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +36 -6
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/loader.test.d.ts +2 -0
- package/dist/skills/loader.test.d.ts.map +1 -0
- package/dist/skills/loader.test.js +65 -0
- package/dist/skills/loader.test.js.map +1 -0
- package/dist/streaming/coalesce.test.js +1 -1
- package/dist/tools/builtins/__tests__/edit.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +38 -0
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js +22 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/read-file.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/read-file.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/read-file.test.js +24 -0
- package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.js +52 -0
- package/dist/tools/builtins/__tests__/verify-outputs.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/write-file.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/write-file.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/write-file.test.js +74 -0
- package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +40 -7
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +5 -2
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +114 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +13 -13
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +23 -8
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/search-tools.d.ts.map +1 -1
- package/dist/tools/builtins/search-tools.js +4 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/verify-outputs.d.ts +5 -0
- package/dist/tools/builtins/verify-outputs.d.ts.map +1 -0
- package/dist/tools/builtins/verify-outputs.js +103 -0
- package/dist/tools/builtins/verify-outputs.js.map +1 -0
- package/dist/tools/builtins/write-file.d.ts +3 -2
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +72 -12
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/agent.test.d.ts +15 -0
- package/dist/tools/coordinator/__tests__/agent.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/agent.test.js +142 -0
- package/dist/tools/coordinator/__tests__/agent.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/task-list.test.d.ts +13 -0
- package/dist/tools/coordinator/__tests__/task-list.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js +162 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -0
- package/dist/tools/coordinator/agent.d.ts +34 -0
- package/dist/tools/coordinator/agent.d.ts.map +1 -0
- package/dist/tools/coordinator/agent.js +107 -0
- package/dist/tools/coordinator/agent.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +7 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +111 -21
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/base.d.ts +8 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/reactive.d.ts +23 -0
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +14 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/message/index.d.ts +22 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +7 -2
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +2 -9
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +6 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +193 -0
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/skills/index.d.ts +2 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +22 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/toolset/index.d.ts +71 -0
- package/dist/types/toolset/index.d.ts.map +1 -0
- package/dist/types/toolset/index.js +2 -0
- package/dist/types/toolset/index.js.map +1 -0
- package/dist/types/workspace/index.d.ts +1 -0
- package/dist/types/workspace/index.d.ts.map +1 -1
- package/dist/types/workspace/shared-run.d.ts +61 -0
- package/dist/types/workspace/shared-run.d.ts.map +1 -0
- package/dist/types/workspace/shared-run.js +2 -0
- package/dist/types/workspace/shared-run.js.map +1 -0
- package/dist/verification/index.d.ts +1 -0
- package/dist/verification/index.d.ts.map +1 -1
- package/dist/verification/index.js +1 -0
- package/dist/verification/index.js.map +1 -1
- package/dist/verification/presets.d.ts +53 -0
- package/dist/verification/presets.d.ts.map +1 -0
- package/dist/verification/presets.js +70 -0
- package/dist/verification/presets.js.map +1 -0
- package/dist/verification/presets.test.d.ts +16 -0
- package/dist/verification/presets.test.d.ts.map +1 -0
- package/dist/verification/presets.test.js +79 -0
- package/dist/verification/presets.test.js.map +1 -0
- package/package.json +3 -2
- package/src/advisory/executor.test.ts +7 -4
- package/src/advisory/executor.ts +11 -2
- package/src/agents/ReactiveAgent.ts +2 -0
- package/src/agents/SupervisorAgent.ts +7 -0
- package/src/bridge/sse/mapper.test.ts +2 -2
- package/src/constants/compaction/index.ts +8 -3
- package/src/constants/sandbox/index.ts +37 -0
- package/src/constants/tools/index.ts +33 -2
- package/src/manager/run/persistence.ts +34 -6
- package/src/persona/assembler.ts +31 -8
- package/src/provider/collect.test.ts +2 -2
- package/src/public-runtime.ts +14 -1
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +7 -0
- package/src/registry/index.ts +7 -0
- package/src/registry/tool/execute.test.ts +132 -3
- package/src/registry/tool/execute.ts +94 -9
- package/src/registry/toolset/catalog.test.ts +97 -0
- package/src/registry/toolset/catalog.ts +283 -0
- package/src/runtime/query/__tests__/deferred-tools.test.ts +183 -0
- package/src/runtime/query/__tests__/executor-concurrency.test.ts +122 -0
- package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +48 -3
- package/src/runtime/query/__tests__/prompt.test.ts +51 -2
- package/src/runtime/query/__tests__/stream-recovery.test.ts +156 -0
- package/src/runtime/query/continuation.ts +16 -0
- package/src/runtime/query/executor.ts +82 -13
- package/src/runtime/query/index.ts +24 -3
- package/src/runtime/query/iteration/index.ts +263 -68
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/prompt.ts +17 -1
- package/src/runtime/query/tooling.ts +2 -0
- package/src/sandbox/provider/local.ts +33 -0
- package/src/session/workspace/__tests__/shared-run.test.ts +181 -0
- package/src/session/workspace/index.ts +6 -0
- package/src/session/workspace/shared-run.ts +316 -0
- package/src/skills/loader.test.ts +89 -0
- package/src/skills/loader.ts +37 -6
- package/src/streaming/coalesce.test.ts +1 -1
- package/src/tools/builtins/__tests__/edit.test.ts +57 -0
- package/src/tools/builtins/__tests__/payload-budget.test.ts +29 -0
- package/src/tools/builtins/__tests__/read-file.test.ts +31 -0
- package/src/tools/builtins/__tests__/verify-outputs.test.ts +71 -0
- package/src/tools/builtins/__tests__/write-file.test.ts +97 -0
- package/src/tools/builtins/bash.ts +48 -7
- package/src/tools/builtins/edit.ts +162 -27
- package/src/tools/builtins/index.ts +13 -13
- package/src/tools/builtins/read-file.ts +31 -8
- package/src/tools/builtins/search-tools.ts +5 -1
- package/src/tools/builtins/verify-outputs.ts +126 -0
- package/src/tools/builtins/write-file.ts +83 -14
- package/src/tools/coordinator/__tests__/agent.test.ts +172 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +182 -0
- package/src/tools/coordinator/agent.ts +157 -0
- package/src/tools/coordinator/index.ts +128 -22
- package/src/types/agent/base.ts +8 -0
- package/src/types/agent/reactive.ts +25 -0
- package/src/types/agent/supervisor.ts +16 -0
- package/src/types/message/index.ts +32 -2
- package/src/types/provider/chat.ts +2 -9
- package/src/types/run/events.ts +6 -0
- package/src/types/sandbox/index.ts +219 -0
- package/src/types/skills/index.ts +4 -0
- package/src/types/tool/index.ts +24 -0
- package/src/types/toolset/index.ts +86 -0
- package/src/types/workspace/index.ts +9 -0
- package/src/types/workspace/shared-run.ts +65 -0
- package/src/verification/index.ts +1 -0
- package/src/verification/presets.test.ts +112 -0
- 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
|
+
})
|
package/src/skills/loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|