@helmiq/crew 0.1.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 (209) hide show
  1. package/defaults/personas/architect.persona.yaml +72 -0
  2. package/defaults/personas/engineer.persona.yaml +137 -0
  3. package/defaults/personas/persona-spec.schema.yaml +149 -0
  4. package/defaults/personas/reviewer.persona.yaml +47 -0
  5. package/defaults/rubrics/adr.rubric.yaml +48 -0
  6. package/defaults/rubrics/code-review.rubric.yaml +39 -0
  7. package/defaults/rubrics/pull-request.rubric.yaml +40 -0
  8. package/dist/actions/actions.test.d.ts +2 -0
  9. package/dist/actions/actions.test.d.ts.map +1 -0
  10. package/dist/actions/actions.test.js +158 -0
  11. package/dist/actions/direct-dispatcher.d.ts +10 -0
  12. package/dist/actions/direct-dispatcher.d.ts.map +1 -0
  13. package/dist/actions/direct-dispatcher.js +27 -0
  14. package/dist/actions/dispatcher.d.ts +11 -0
  15. package/dist/actions/dispatcher.d.ts.map +1 -0
  16. package/dist/actions/dispatcher.js +1 -0
  17. package/dist/actions/index.d.ts +7 -0
  18. package/dist/actions/index.d.ts.map +1 -0
  19. package/dist/actions/index.js +3 -0
  20. package/dist/actions/registry.d.ts +13 -0
  21. package/dist/actions/registry.d.ts.map +1 -0
  22. package/dist/actions/registry.js +40 -0
  23. package/dist/actions/resolver.d.ts +47 -0
  24. package/dist/actions/resolver.d.ts.map +1 -0
  25. package/dist/actions/resolver.js +43 -0
  26. package/dist/cli/cli.test.d.ts +2 -0
  27. package/dist/cli/cli.test.d.ts.map +1 -0
  28. package/dist/cli/cli.test.js +392 -0
  29. package/dist/cli/run.d.ts +45 -0
  30. package/dist/cli/run.d.ts.map +1 -0
  31. package/dist/cli/run.js +236 -0
  32. package/dist/common/errors.d.ts +76 -0
  33. package/dist/common/errors.d.ts.map +1 -0
  34. package/dist/common/errors.js +74 -0
  35. package/dist/config/config.test.d.ts +2 -0
  36. package/dist/config/config.test.d.ts.map +1 -0
  37. package/dist/config/config.test.js +691 -0
  38. package/dist/config/index.d.ts +7 -0
  39. package/dist/config/index.d.ts.map +1 -0
  40. package/dist/config/index.js +4 -0
  41. package/dist/config/loader.d.ts +16 -0
  42. package/dist/config/loader.d.ts.map +1 -0
  43. package/dist/config/loader.js +56 -0
  44. package/dist/config/model-resolver.d.ts +24 -0
  45. package/dist/config/model-resolver.d.ts.map +1 -0
  46. package/dist/config/model-resolver.js +39 -0
  47. package/dist/config/resolver.d.ts +22 -0
  48. package/dist/config/resolver.d.ts.map +1 -0
  49. package/dist/config/resolver.js +115 -0
  50. package/dist/config/schemas.d.ts +266 -0
  51. package/dist/config/schemas.d.ts.map +1 -0
  52. package/dist/config/schemas.js +115 -0
  53. package/dist/context/artifact-reader.d.ts +12 -0
  54. package/dist/context/artifact-reader.d.ts.map +1 -0
  55. package/dist/context/artifact-reader.js +92 -0
  56. package/dist/context/assembler.d.ts +22 -0
  57. package/dist/context/assembler.d.ts.map +1 -0
  58. package/dist/context/assembler.js +126 -0
  59. package/dist/context/code-reader.d.ts +14 -0
  60. package/dist/context/code-reader.d.ts.map +1 -0
  61. package/dist/context/code-reader.js +56 -0
  62. package/dist/context/context.test.d.ts +2 -0
  63. package/dist/context/context.test.d.ts.map +1 -0
  64. package/dist/context/context.test.js +260 -0
  65. package/dist/context/index.d.ts +9 -0
  66. package/dist/context/index.d.ts.map +1 -0
  67. package/dist/context/index.js +5 -0
  68. package/dist/context/section-extractor.d.ts +9 -0
  69. package/dist/context/section-extractor.d.ts.map +1 -0
  70. package/dist/context/section-extractor.js +32 -0
  71. package/dist/context/token-budget.d.ts +11 -0
  72. package/dist/context/token-budget.d.ts.map +1 -0
  73. package/dist/context/token-budget.js +22 -0
  74. package/dist/control/control.test.d.ts +2 -0
  75. package/dist/control/control.test.d.ts.map +1 -0
  76. package/dist/control/control.test.js +137 -0
  77. package/dist/control/id-generator.d.ts +12 -0
  78. package/dist/control/id-generator.d.ts.map +1 -0
  79. package/dist/control/id-generator.js +20 -0
  80. package/dist/control/index.d.ts +5 -0
  81. package/dist/control/index.d.ts.map +1 -0
  82. package/dist/control/index.js +3 -0
  83. package/dist/control/lock-manager.d.ts +13 -0
  84. package/dist/control/lock-manager.d.ts.map +1 -0
  85. package/dist/control/lock-manager.js +72 -0
  86. package/dist/control/run-state.d.ts +16 -0
  87. package/dist/control/run-state.d.ts.map +1 -0
  88. package/dist/control/run-state.js +55 -0
  89. package/dist/engine/composite.d.ts +34 -0
  90. package/dist/engine/composite.d.ts.map +1 -0
  91. package/dist/engine/composite.js +192 -0
  92. package/dist/engine/composite.test.d.ts +2 -0
  93. package/dist/engine/composite.test.d.ts.map +1 -0
  94. package/dist/engine/composite.test.js +1947 -0
  95. package/dist/engine/engine.test.d.ts +2 -0
  96. package/dist/engine/engine.test.d.ts.map +1 -0
  97. package/dist/engine/engine.test.js +334 -0
  98. package/dist/engine/index.d.ts +10 -0
  99. package/dist/engine/index.d.ts.map +1 -0
  100. package/dist/engine/index.js +5 -0
  101. package/dist/engine/llm-client.d.ts +27 -0
  102. package/dist/engine/llm-client.d.ts.map +1 -0
  103. package/dist/engine/llm-client.js +46 -0
  104. package/dist/engine/simple.d.ts +21 -0
  105. package/dist/engine/simple.d.ts.map +1 -0
  106. package/dist/engine/simple.js +59 -0
  107. package/dist/engine/tool-dispatch.d.ts +37 -0
  108. package/dist/engine/tool-dispatch.d.ts.map +1 -0
  109. package/dist/engine/tool-dispatch.js +146 -0
  110. package/dist/engine/tool-dispatch.test.d.ts +2 -0
  111. package/dist/engine/tool-dispatch.test.d.ts.map +1 -0
  112. package/dist/engine/tool-dispatch.test.js +348 -0
  113. package/dist/engine/tool-filter.d.ts +13 -0
  114. package/dist/engine/tool-filter.d.ts.map +1 -0
  115. package/dist/engine/tool-filter.js +25 -0
  116. package/dist/evaluation/evaluation.test.d.ts +2 -0
  117. package/dist/evaluation/evaluation.test.d.ts.map +1 -0
  118. package/dist/evaluation/evaluation.test.js +490 -0
  119. package/dist/evaluation/evaluator.d.ts +19 -0
  120. package/dist/evaluation/evaluator.d.ts.map +1 -0
  121. package/dist/evaluation/evaluator.js +78 -0
  122. package/dist/evaluation/index.d.ts +4 -0
  123. package/dist/evaluation/index.d.ts.map +1 -0
  124. package/dist/evaluation/index.js +2 -0
  125. package/dist/evaluation/scorer.d.ts +38 -0
  126. package/dist/evaluation/scorer.d.ts.map +1 -0
  127. package/dist/evaluation/scorer.js +94 -0
  128. package/dist/index.d.ts +47 -0
  129. package/dist/index.d.ts.map +1 -0
  130. package/dist/index.js +28 -0
  131. package/dist/providers/index.d.ts +2 -0
  132. package/dist/providers/index.d.ts.map +1 -0
  133. package/dist/providers/index.js +1 -0
  134. package/dist/providers/provider-factory.d.ts +11 -0
  135. package/dist/providers/provider-factory.d.ts.map +1 -0
  136. package/dist/providers/provider-factory.js +30 -0
  137. package/dist/publication/frontmatter.d.ts +21 -0
  138. package/dist/publication/frontmatter.d.ts.map +1 -0
  139. package/dist/publication/frontmatter.js +15 -0
  140. package/dist/publication/git-ops.d.ts +18 -0
  141. package/dist/publication/git-ops.d.ts.map +1 -0
  142. package/dist/publication/git-ops.js +74 -0
  143. package/dist/publication/index.d.ts +9 -0
  144. package/dist/publication/index.d.ts.map +1 -0
  145. package/dist/publication/index.js +5 -0
  146. package/dist/publication/provenance-writer.d.ts +27 -0
  147. package/dist/publication/provenance-writer.d.ts.map +1 -0
  148. package/dist/publication/provenance-writer.js +21 -0
  149. package/dist/publication/publication.test.d.ts +2 -0
  150. package/dist/publication/publication.test.d.ts.map +1 -0
  151. package/dist/publication/publication.test.js +235 -0
  152. package/dist/publication/publisher.d.ts +32 -0
  153. package/dist/publication/publisher.d.ts.map +1 -0
  154. package/dist/publication/publisher.js +113 -0
  155. package/dist/publication/secret-scanner.d.ts +6 -0
  156. package/dist/publication/secret-scanner.d.ts.map +1 -0
  157. package/dist/publication/secret-scanner.js +19 -0
  158. package/dist/tools/index.d.ts +4 -0
  159. package/dist/tools/index.d.ts.map +1 -0
  160. package/dist/tools/index.js +2 -0
  161. package/dist/tools/registry.d.ts +15 -0
  162. package/dist/tools/registry.d.ts.map +1 -0
  163. package/dist/tools/registry.js +288 -0
  164. package/dist/tools/registry.test.d.ts +2 -0
  165. package/dist/tools/registry.test.d.ts.map +1 -0
  166. package/dist/tools/registry.test.js +131 -0
  167. package/dist/tools/tool-groups.d.ts +20 -0
  168. package/dist/tools/tool-groups.d.ts.map +1 -0
  169. package/dist/tools/tool-groups.js +48 -0
  170. package/dist/tools/tool-groups.test.d.ts +2 -0
  171. package/dist/tools/tool-groups.test.d.ts.map +1 -0
  172. package/dist/tools/tool-groups.test.js +127 -0
  173. package/dist/types/artifact-store.d.ts +33 -0
  174. package/dist/types/artifact-store.d.ts.map +1 -0
  175. package/dist/types/artifact-store.js +9 -0
  176. package/dist/types/evaluation-rubric.d.ts +18 -0
  177. package/dist/types/evaluation-rubric.d.ts.map +1 -0
  178. package/dist/types/evaluation-rubric.js +1 -0
  179. package/dist/types/index.d.ts +10 -0
  180. package/dist/types/index.d.ts.map +1 -0
  181. package/dist/types/index.js +1 -0
  182. package/dist/types/llm-provider.d.ts +47 -0
  183. package/dist/types/llm-provider.d.ts.map +1 -0
  184. package/dist/types/llm-provider.js +8 -0
  185. package/dist/types/persona-spec.d.ts +79 -0
  186. package/dist/types/persona-spec.d.ts.map +1 -0
  187. package/dist/types/persona-spec.js +1 -0
  188. package/dist/types/project-config.d.ts +28 -0
  189. package/dist/types/project-config.d.ts.map +1 -0
  190. package/dist/types/project-config.js +1 -0
  191. package/dist/types/provenance.d.ts +67 -0
  192. package/dist/types/provenance.d.ts.map +1 -0
  193. package/dist/types/provenance.js +1 -0
  194. package/dist/types/run-state.d.ts +11 -0
  195. package/dist/types/run-state.d.ts.map +1 -0
  196. package/dist/types/run-state.js +1 -0
  197. package/dist/types/tool-runtime.d.ts +43 -0
  198. package/dist/types/tool-runtime.d.ts.map +1 -0
  199. package/dist/types/tool-runtime.js +30 -0
  200. package/dist/workspace/detect.d.ts +11 -0
  201. package/dist/workspace/detect.d.ts.map +1 -0
  202. package/dist/workspace/detect.js +28 -0
  203. package/dist/workspace/detect.test.d.ts +2 -0
  204. package/dist/workspace/detect.test.d.ts.map +1 -0
  205. package/dist/workspace/detect.test.js +53 -0
  206. package/dist/workspace/index.d.ts +2 -0
  207. package/dist/workspace/index.d.ts.map +1 -0
  208. package/dist/workspace/index.js +1 -0
  209. package/package.json +51 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * CrewTool registry -- maps logical tool names used in persona specs
3
+ * to executable CrewTool implementations backed by the tool packages.
4
+ *
5
+ * Each tool's `execute` function adapts the tool package's API,
6
+ * injecting ToolExecutionContext values (repo paths, protected paths, etc.)
7
+ * so the LLM only needs to provide the operation-specific parameters.
8
+ */
9
+ import { readFileFromRepo, writeFileToRepo, listDirectory, searchCodebase, } from '@helmiq/crew-code';
10
+ import { gitBranch, gitCommit, gitPush, gitDiff, gitLog, createPr } from '@helmiq/crew-git';
11
+ import { runCommand, createAllowlist } from '@helmiq/crew-shell';
12
+ export function createToolRegistry(options) {
13
+ const registry = {
14
+ 'read-file': {
15
+ name: 'read-file',
16
+ description: 'Read a file from the target repository',
17
+ parameters: {
18
+ type: 'object',
19
+ properties: {
20
+ path: { type: 'string', description: 'Relative file path within the repository' },
21
+ },
22
+ required: ['path'],
23
+ },
24
+ execute: async (params, ctx) => {
25
+ const { path } = params;
26
+ return readFileFromRepo({ path }, ctx.targetRepoPath);
27
+ },
28
+ },
29
+ 'write-file': {
30
+ name: 'write-file',
31
+ description: 'Write or create a file in the target repository',
32
+ parameters: {
33
+ type: 'object',
34
+ properties: {
35
+ path: { type: 'string', description: 'Relative file path within the repository' },
36
+ content: { type: 'string', description: 'File content to write' },
37
+ },
38
+ required: ['path', 'content'],
39
+ },
40
+ execute: async (params, ctx) => {
41
+ const { path, content } = params;
42
+ return writeFileToRepo({ path, content }, ctx.targetRepoPath, ctx.protectedPaths);
43
+ },
44
+ },
45
+ 'list-directory': {
46
+ name: 'list-directory',
47
+ description: 'List files and directories in the target repository',
48
+ parameters: {
49
+ type: 'object',
50
+ properties: {
51
+ path: {
52
+ type: 'string',
53
+ description: 'Relative directory path (default: repository root)',
54
+ },
55
+ },
56
+ },
57
+ execute: async (params, ctx) => {
58
+ const { path } = (params ?? {});
59
+ return listDirectory({ path }, ctx.targetRepoPath);
60
+ },
61
+ },
62
+ 'search-codebase': {
63
+ name: 'search-codebase',
64
+ description: 'Search the target repository for text patterns',
65
+ parameters: {
66
+ type: 'object',
67
+ properties: {
68
+ pattern: { type: 'string', description: 'Search pattern (regex)' },
69
+ glob: { type: 'string', description: 'File glob filter (e.g. "*.ts")' },
70
+ maxResults: { type: 'number', description: 'Maximum results to return' },
71
+ },
72
+ required: ['pattern'],
73
+ },
74
+ execute: async (params, ctx) => {
75
+ const { pattern, glob, maxResults } = params;
76
+ return searchCodebase({ pattern, glob, maxResults }, ctx.targetRepoPath);
77
+ },
78
+ },
79
+ 'git-branch': {
80
+ name: 'git-branch',
81
+ description: 'Create and checkout a new Git branch in the target repository',
82
+ parameters: {
83
+ type: 'object',
84
+ properties: {
85
+ branchName: { type: 'string', description: 'Name of the branch to create' },
86
+ },
87
+ required: ['branchName'],
88
+ },
89
+ execute: async (params, ctx) => {
90
+ const { branchName } = params;
91
+ const convention = ctx.project.project.key
92
+ ? `^feat/${ctx.project.project.key}-`
93
+ : undefined;
94
+ return gitBranch({ branchName }, ctx.targetRepoPath, convention);
95
+ },
96
+ },
97
+ 'git-commit': {
98
+ name: 'git-commit',
99
+ description: 'Stage files and create a commit in the target repository',
100
+ parameters: {
101
+ type: 'object',
102
+ properties: {
103
+ filePaths: {
104
+ type: 'array',
105
+ items: { type: 'string' },
106
+ description: 'Files to stage and commit',
107
+ },
108
+ message: { type: 'string', description: 'Commit message' },
109
+ },
110
+ required: ['filePaths', 'message'],
111
+ },
112
+ execute: async (params, ctx) => {
113
+ const { filePaths, message } = params;
114
+ return gitCommit({ filePaths, message }, ctx.targetRepoPath);
115
+ },
116
+ },
117
+ 'git-push': {
118
+ name: 'git-push',
119
+ description: 'Push the current branch to the remote',
120
+ parameters: {
121
+ type: 'object',
122
+ properties: {
123
+ branch: { type: 'string', description: 'Branch to push (default: current)' },
124
+ },
125
+ },
126
+ execute: async (params, ctx) => {
127
+ const { branch } = (params ?? {});
128
+ return gitPush({ branch }, ctx.targetRepoPath);
129
+ },
130
+ },
131
+ 'git-diff': {
132
+ name: 'git-diff',
133
+ description: 'Show the diff of changes in the target repository',
134
+ parameters: {
135
+ type: 'object',
136
+ properties: {
137
+ staged: { type: 'boolean', description: 'Show staged changes only' },
138
+ paths: {
139
+ type: 'array',
140
+ items: { type: 'string' },
141
+ description: 'Limit diff to specific files',
142
+ },
143
+ },
144
+ },
145
+ execute: async (params, ctx) => {
146
+ const { staged, paths } = (params ?? {});
147
+ return gitDiff({ staged, paths }, ctx.targetRepoPath);
148
+ },
149
+ },
150
+ 'git-log': {
151
+ name: 'git-log',
152
+ description: 'Show recent commit log in the target repository',
153
+ parameters: {
154
+ type: 'object',
155
+ properties: {
156
+ maxCount: { type: 'number', description: 'Maximum number of entries' },
157
+ path: { type: 'string', description: 'Limit to commits affecting this path' },
158
+ },
159
+ },
160
+ execute: async (params, ctx) => {
161
+ const { maxCount, path } = (params ?? {});
162
+ return gitLog({ maxCount, path }, ctx.targetRepoPath);
163
+ },
164
+ },
165
+ 'create-pr': {
166
+ name: 'create-pr',
167
+ description: 'Create a GitHub pull request. The branch must be pushed to the remote first using git-push; creation will fail if the branch does not exist on the remote.',
168
+ parameters: {
169
+ type: 'object',
170
+ properties: {
171
+ title: { type: 'string', description: 'PR title' },
172
+ body: { type: 'string', description: 'PR description' },
173
+ head: { type: 'string', description: 'Source branch' },
174
+ base: { type: 'string', description: 'Target branch (default: main)' },
175
+ labels: {
176
+ type: 'array',
177
+ items: { type: 'string' },
178
+ description: 'Labels to apply',
179
+ },
180
+ },
181
+ required: ['title', 'body', 'head'],
182
+ },
183
+ execute: async (params, ctx) => {
184
+ const { title, body, head, base, labels } = params;
185
+ const [owner, repo] = ctx.project.source.repo.replace('github:', '').split('/');
186
+ return createPr({
187
+ owner: owner,
188
+ repo: repo,
189
+ title,
190
+ body,
191
+ head,
192
+ base: base ?? 'main',
193
+ labels,
194
+ });
195
+ },
196
+ },
197
+ 'run-command': {
198
+ name: 'run-command',
199
+ description: 'Run a shell command in the target repository (restricted to allowlisted commands)',
200
+ parameters: {
201
+ type: 'object',
202
+ properties: {
203
+ command: { type: 'string', description: 'Command to run (e.g. pnpm)' },
204
+ args: {
205
+ type: 'array',
206
+ items: { type: 'string' },
207
+ description: 'Command arguments (e.g. ["test"])',
208
+ },
209
+ cwd: { type: 'string', description: 'Working directory relative to repo root' },
210
+ },
211
+ required: ['command', 'args'],
212
+ },
213
+ execute: async (params, ctx) => {
214
+ const { command, args, cwd } = params;
215
+ const rules = createAllowlist();
216
+ return runCommand({ command, args, cwd }, ctx.targetRepoPath, rules);
217
+ },
218
+ },
219
+ };
220
+ if (options?.artifactStore) {
221
+ const store = options.artifactStore;
222
+ registry['read-artifact'] = {
223
+ name: 'read-artifact',
224
+ description: 'Read a delivery artifact (requirements, design, review, standards) from the workspace',
225
+ parameters: {
226
+ type: 'object',
227
+ properties: {
228
+ type: {
229
+ type: 'string',
230
+ description: 'Artifact type (e.g. requirements, design, review, standards, backlog)',
231
+ },
232
+ scope: {
233
+ type: 'object',
234
+ description: 'Scope variables for path resolution (e.g. { "epic": "CREW-03" })',
235
+ additionalProperties: { type: 'string' },
236
+ },
237
+ },
238
+ required: ['type', 'scope'],
239
+ },
240
+ execute: async (params) => {
241
+ const { type, scope } = params;
242
+ const result = await store.read(type, scope);
243
+ if (!result) {
244
+ return { found: false, type, scope };
245
+ }
246
+ return {
247
+ found: true,
248
+ path: result.path,
249
+ content: result.content,
250
+ frontmatter: result.frontmatter,
251
+ };
252
+ },
253
+ };
254
+ registry['write-artifact'] = {
255
+ name: 'write-artifact',
256
+ description: 'Write a delivery artifact to the workspace',
257
+ parameters: {
258
+ type: 'object',
259
+ properties: {
260
+ type: {
261
+ type: 'string',
262
+ description: 'Artifact type (e.g. review, design)',
263
+ },
264
+ scope: {
265
+ type: 'object',
266
+ description: 'Scope variables for path resolution (e.g. { "epic": "CREW-03" })',
267
+ additionalProperties: { type: 'string' },
268
+ },
269
+ content: {
270
+ type: 'string',
271
+ description: 'Markdown content to write',
272
+ },
273
+ frontmatter: {
274
+ type: 'object',
275
+ description: 'YAML frontmatter metadata',
276
+ additionalProperties: true,
277
+ },
278
+ },
279
+ required: ['type', 'scope', 'content'],
280
+ },
281
+ execute: async (params) => {
282
+ const { type, scope, content, frontmatter } = params;
283
+ return store.write(type, scope, content, frontmatter ?? {});
284
+ },
285
+ };
286
+ }
287
+ return registry;
288
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/tools/registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createToolRegistry } from './registry.js';
3
+ const mockContext = {
4
+ workspacePath: '/tmp/ws',
5
+ targetRepoPath: '/tmp/target',
6
+ project: {
7
+ project: { name: 'test', key: 'T' },
8
+ workspace: { path: '/tmp/ws', work: 'work/{EPIC_ID}', runs: 'runs' },
9
+ source: { repo: 'github:test/repo', path: '/tmp/target' },
10
+ llm: { default_model: 'test', providers: {} },
11
+ },
12
+ persona: 'engineer',
13
+ task: 'implement-story',
14
+ runId: 'run-test-001',
15
+ protectedPaths: [],
16
+ logger: () => { },
17
+ };
18
+ describe('createToolRegistry', () => {
19
+ it('registers all code, git, and shell tools', () => {
20
+ const registry = createToolRegistry();
21
+ const names = Object.keys(registry).sort();
22
+ expect(names).toEqual([
23
+ 'create-pr',
24
+ 'git-branch',
25
+ 'git-commit',
26
+ 'git-diff',
27
+ 'git-log',
28
+ 'git-push',
29
+ 'list-directory',
30
+ 'read-file',
31
+ 'run-command',
32
+ 'search-codebase',
33
+ 'write-file',
34
+ ]);
35
+ });
36
+ it('does not register artifact tools when no store is provided', () => {
37
+ const registry = createToolRegistry();
38
+ expect(registry['read-artifact']).toBeUndefined();
39
+ expect(registry['write-artifact']).toBeUndefined();
40
+ });
41
+ it('registers artifact tools when an ArtifactStore is provided', () => {
42
+ const store = {
43
+ read: vi.fn(),
44
+ write: vi.fn(),
45
+ list: vi.fn(),
46
+ history: vi.fn(),
47
+ };
48
+ const registry = createToolRegistry({ artifactStore: store });
49
+ expect(registry['read-artifact']).toBeDefined();
50
+ expect(registry['write-artifact']).toBeDefined();
51
+ expect(registry['read-artifact'].name).toBe('read-artifact');
52
+ expect(registry['write-artifact'].name).toBe('write-artifact');
53
+ });
54
+ it('each tool has name, description, parameters, and execute', () => {
55
+ const registry = createToolRegistry();
56
+ for (const [name, tool] of Object.entries(registry)) {
57
+ expect(tool.name).toBe(name);
58
+ expect(tool.description).toBeTruthy();
59
+ expect(tool.parameters).toBeDefined();
60
+ expect(typeof tool.execute).toBe('function');
61
+ }
62
+ });
63
+ });
64
+ describe('artifact tool: read-artifact', () => {
65
+ it('returns artifact content when found', async () => {
66
+ const store = {
67
+ read: vi.fn().mockResolvedValue({
68
+ path: 'work/CREW-03/requirements.md',
69
+ content: '# Requirements',
70
+ frontmatter: { status: 'approved' },
71
+ }),
72
+ write: vi.fn(),
73
+ list: vi.fn(),
74
+ history: vi.fn(),
75
+ };
76
+ const registry = createToolRegistry({ artifactStore: store });
77
+ const result = await registry['read-artifact'].execute({ type: 'requirements', scope: { epic: 'CREW-03' } }, mockContext);
78
+ expect(result).toEqual({
79
+ found: true,
80
+ path: 'work/CREW-03/requirements.md',
81
+ content: '# Requirements',
82
+ frontmatter: { status: 'approved' },
83
+ });
84
+ expect(store.read).toHaveBeenCalledWith('requirements', { epic: 'CREW-03' });
85
+ });
86
+ it('returns found: false when artifact does not exist', async () => {
87
+ const store = {
88
+ read: vi.fn().mockResolvedValue(null),
89
+ write: vi.fn(),
90
+ list: vi.fn(),
91
+ history: vi.fn(),
92
+ };
93
+ const registry = createToolRegistry({ artifactStore: store });
94
+ const result = await registry['read-artifact'].execute({ type: 'design', scope: { epic: 'CREW-99' } }, mockContext);
95
+ expect(result).toEqual({
96
+ found: false,
97
+ type: 'design',
98
+ scope: { epic: 'CREW-99' },
99
+ });
100
+ });
101
+ });
102
+ describe('artifact tool: write-artifact', () => {
103
+ it('writes content to the artifact store', async () => {
104
+ const store = {
105
+ read: vi.fn(),
106
+ write: vi.fn().mockResolvedValue({ path: 'work/CREW-03/review.md', bytesWritten: 128 }),
107
+ list: vi.fn(),
108
+ history: vi.fn(),
109
+ };
110
+ const registry = createToolRegistry({ artifactStore: store });
111
+ const result = await registry['write-artifact'].execute({
112
+ type: 'review',
113
+ scope: { epic: 'CREW-03' },
114
+ content: '# Review\n\nLooks good.',
115
+ frontmatter: { status: 'published', author: 'engineer' },
116
+ }, mockContext);
117
+ expect(result).toEqual({ path: 'work/CREW-03/review.md', bytesWritten: 128 });
118
+ expect(store.write).toHaveBeenCalledWith('review', { epic: 'CREW-03' }, '# Review\n\nLooks good.', { status: 'published', author: 'engineer' });
119
+ });
120
+ it('defaults frontmatter to empty object when not provided', async () => {
121
+ const store = {
122
+ read: vi.fn(),
123
+ write: vi.fn().mockResolvedValue({ path: 'work/CREW-03/review.md', bytesWritten: 64 }),
124
+ list: vi.fn(),
125
+ history: vi.fn(),
126
+ };
127
+ const registry = createToolRegistry({ artifactStore: store });
128
+ await registry['write-artifact'].execute({ type: 'review', scope: { epic: 'CREW-03' }, content: 'content' }, mockContext);
129
+ expect(store.write).toHaveBeenCalledWith('review', { epic: 'CREW-03' }, 'content', {});
130
+ });
131
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tool group definitions that map persona-spec-level logical names
3
+ * to individual tool names in the CrewTool registry.
4
+ *
5
+ * Persona specs use category names (e.g. "code", "git-operations")
6
+ * for readability. The registry uses granular names (e.g. "write-file",
7
+ * "git-branch"). This module bridges the two.
8
+ *
9
+ * Code and git groups are split into read/write halves so that
10
+ * read-only personas (e.g. Reviewer) can deny writes without
11
+ * losing read access.
12
+ */
13
+ export declare const TOOL_GROUPS: Readonly<Record<string, readonly string[]>>;
14
+ /**
15
+ * Expand a set of tool names, replacing any group names with their
16
+ * constituent individual tool names. Names that are not groups pass
17
+ * through unchanged.
18
+ */
19
+ export declare function expandToolNames(names: ReadonlySet<string> | readonly string[]): Set<string>;
20
+ //# sourceMappingURL=tool-groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-groups.d.ts","sourceRoot":"","sources":["../../src/tools/tool-groups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAWnE,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAW3F"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tool group definitions that map persona-spec-level logical names
3
+ * to individual tool names in the CrewTool registry.
4
+ *
5
+ * Persona specs use category names (e.g. "code", "git-operations")
6
+ * for readability. The registry uses granular names (e.g. "write-file",
7
+ * "git-branch"). This module bridges the two.
8
+ *
9
+ * Code and git groups are split into read/write halves so that
10
+ * read-only personas (e.g. Reviewer) can deny writes without
11
+ * losing read access.
12
+ */
13
+ const CODE_READ = ['read-file', 'list-directory', 'search-codebase'];
14
+ const CODE_WRITE = ['write-file'];
15
+ const GIT_READ = ['git-diff', 'git-log'];
16
+ const GIT_WRITE = ['git-branch', 'git-commit', 'git-push', 'create-pr'];
17
+ const GIT_ALL = [...GIT_READ, ...GIT_WRITE];
18
+ export const TOOL_GROUPS = {
19
+ 'read-code': CODE_READ,
20
+ 'write-code': CODE_WRITE,
21
+ code: [...CODE_READ, ...CODE_WRITE],
22
+ 'git-read': GIT_READ,
23
+ 'git-write': GIT_WRITE,
24
+ 'git-operations': GIT_ALL,
25
+ git: GIT_ALL,
26
+ shell: ['run-command'],
27
+ 'read-artifact': ['read-artifact'],
28
+ 'write-artifact': ['write-artifact'],
29
+ };
30
+ /**
31
+ * Expand a set of tool names, replacing any group names with their
32
+ * constituent individual tool names. Names that are not groups pass
33
+ * through unchanged.
34
+ */
35
+ export function expandToolNames(names) {
36
+ const expanded = new Set();
37
+ for (const name of names) {
38
+ const group = TOOL_GROUPS[name];
39
+ if (group) {
40
+ for (const tool of group)
41
+ expanded.add(tool);
42
+ }
43
+ else {
44
+ expanded.add(name);
45
+ }
46
+ }
47
+ return expanded;
48
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-groups.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-groups.test.d.ts","sourceRoot":"","sources":["../../src/tools/tool-groups.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { expandToolNames, TOOL_GROUPS } from './tool-groups.js';
3
+ describe('tool-groups', () => {
4
+ describe('TOOL_GROUPS', () => {
5
+ it('defines read-code group with read-only code I/O tools', () => {
6
+ expect(TOOL_GROUPS['read-code']).toEqual(expect.arrayContaining(['read-file', 'list-directory', 'search-codebase']));
7
+ expect(TOOL_GROUPS['read-code']).not.toContain('write-file');
8
+ });
9
+ it('defines write-code group with write-file only', () => {
10
+ expect(TOOL_GROUPS['write-code']).toEqual(['write-file']);
11
+ });
12
+ it('defines code group as union of read-code and write-code', () => {
13
+ expect(TOOL_GROUPS['code']).toEqual(expect.arrayContaining(['read-file', 'write-file', 'list-directory', 'search-codebase']));
14
+ });
15
+ it('defines git-read group with read-only git tools', () => {
16
+ expect(TOOL_GROUPS['git-read']).toEqual(expect.arrayContaining(['git-diff', 'git-log']));
17
+ expect(TOOL_GROUPS['git-read']).not.toContain('git-branch');
18
+ expect(TOOL_GROUPS['git-read']).not.toContain('git-commit');
19
+ });
20
+ it('defines git-write group with mutating git tools', () => {
21
+ expect(TOOL_GROUPS['git-write']).toEqual(expect.arrayContaining(['git-branch', 'git-commit', 'git-push', 'create-pr']));
22
+ expect(TOOL_GROUPS['git-write']).not.toContain('git-diff');
23
+ expect(TOOL_GROUPS['git-write']).not.toContain('git-log');
24
+ });
25
+ it('defines git-operations as union of git-read and git-write', () => {
26
+ expect(TOOL_GROUPS['git-operations']).toEqual(expect.arrayContaining([
27
+ 'git-branch',
28
+ 'git-commit',
29
+ 'git-push',
30
+ 'git-diff',
31
+ 'git-log',
32
+ 'create-pr',
33
+ ]));
34
+ });
35
+ it('defines git as an alias for git-operations', () => {
36
+ expect(TOOL_GROUPS['git']).toEqual(TOOL_GROUPS['git-operations']);
37
+ });
38
+ it('defines shell group mapping to run-command', () => {
39
+ expect(TOOL_GROUPS['shell']).toEqual(['run-command']);
40
+ });
41
+ });
42
+ describe('expandToolNames', () => {
43
+ it('expands read-code to read-only code tools', () => {
44
+ const result = expandToolNames(new Set(['read-code']));
45
+ expect(result).toEqual(new Set(['read-file', 'list-directory', 'search-codebase']));
46
+ });
47
+ it('expands write-code to write-file only', () => {
48
+ const result = expandToolNames(new Set(['write-code']));
49
+ expect(result).toEqual(new Set(['write-file']));
50
+ });
51
+ it('expands code to the full code I/O set', () => {
52
+ const result = expandToolNames(new Set(['code']));
53
+ expect(result).toEqual(new Set(['read-file', 'write-file', 'list-directory', 'search-codebase']));
54
+ });
55
+ it('expands git-read to read-only git tools', () => {
56
+ const result = expandToolNames(new Set(['git-read']));
57
+ expect(result).toEqual(new Set(['git-diff', 'git-log']));
58
+ });
59
+ it('expands git-write to mutating git tools', () => {
60
+ const result = expandToolNames(new Set(['git-write']));
61
+ expect(result).toEqual(new Set(['git-branch', 'git-commit', 'git-push', 'create-pr']));
62
+ });
63
+ it('passes through names that are not groups', () => {
64
+ const result = expandToolNames(new Set(['read-file', 'custom-tool']));
65
+ expect(result).toEqual(new Set(['read-file', 'custom-tool']));
66
+ });
67
+ it('expands multiple groups and deduplicates', () => {
68
+ const result = expandToolNames(new Set(['code', 'git', 'shell']));
69
+ expect(result).toContain('write-file');
70
+ expect(result).toContain('read-file');
71
+ expect(result).toContain('git-branch');
72
+ expect(result).toContain('git-diff');
73
+ expect(result).toContain('run-command');
74
+ });
75
+ it('handles mix of group names and individual names', () => {
76
+ const result = expandToolNames(new Set(['shell', 'read-file']));
77
+ expect(result).toEqual(new Set(['run-command', 'read-file']));
78
+ });
79
+ it('accepts an array as input', () => {
80
+ const result = expandToolNames(['code', 'shell']);
81
+ expect(result).toContain('write-file');
82
+ expect(result).toContain('run-command');
83
+ });
84
+ it('returns empty set for empty input', () => {
85
+ const result = expandToolNames(new Set());
86
+ expect(result.size).toBe(0);
87
+ });
88
+ it('expands the Engineer implementer sub-agent tools correctly', () => {
89
+ const result = expandToolNames(['code', 'git', 'shell']);
90
+ expect(result).toEqual(new Set([
91
+ 'read-file',
92
+ 'write-file',
93
+ 'list-directory',
94
+ 'search-codebase',
95
+ 'git-branch',
96
+ 'git-commit',
97
+ 'git-push',
98
+ 'git-diff',
99
+ 'git-log',
100
+ 'create-pr',
101
+ 'run-command',
102
+ ]));
103
+ });
104
+ it('expands Reviewer permitted tools without write access', () => {
105
+ const permitted = expandToolNames([
106
+ 'read-artifact',
107
+ 'write-artifact',
108
+ 'read-code',
109
+ 'git-read',
110
+ 'shell',
111
+ ]);
112
+ const denied = expandToolNames(['write-code', 'git-write']);
113
+ expect(permitted).toContain('read-file');
114
+ expect(permitted).toContain('git-diff');
115
+ expect(permitted).toContain('run-command');
116
+ expect(permitted).not.toContain('write-file');
117
+ expect(permitted).not.toContain('git-branch');
118
+ expect(denied).toContain('write-file');
119
+ expect(denied).toContain('git-branch');
120
+ expect(denied).not.toContain('read-file');
121
+ expect(denied).not.toContain('git-diff');
122
+ for (const tool of denied) {
123
+ expect(permitted.has(tool)).toBe(false);
124
+ }
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Abstract artifact store interface.
3
+ *
4
+ * Provides CRUD operations for delivery artifacts (requirements, designs,
5
+ * reviews, etc.) backed by an adapter. The filesystem adapter is the
6
+ * Phase 1 implementation; future adapters (Linear, Jira, GitHub) implement
7
+ * the same contract. See ADR-0011.
8
+ */
9
+ export interface ArtifactRecord {
10
+ path: string;
11
+ content: string;
12
+ frontmatter: Record<string, unknown>;
13
+ }
14
+ export interface ArtifactVersion {
15
+ commitSha: string;
16
+ timestamp: string;
17
+ path: string;
18
+ frontmatter: Record<string, unknown>;
19
+ }
20
+ export interface ArtifactListFilter {
21
+ status?: string;
22
+ }
23
+ export interface ArtifactWriteResult {
24
+ path: string;
25
+ bytesWritten: number;
26
+ }
27
+ export interface ArtifactStore {
28
+ read(artifactType: string, scope: Record<string, string>): Promise<ArtifactRecord | null>;
29
+ write(artifactType: string, scope: Record<string, string>, content: string, frontmatter: Record<string, unknown>): Promise<ArtifactWriteResult>;
30
+ list(artifactType: string, filter?: ArtifactListFilter): Promise<ArtifactRecord[]>;
31
+ history(artifactType: string, scope: Record<string, string>): Promise<ArtifactVersion[]>;
32
+ }
33
+ //# sourceMappingURL=artifact-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-store.d.ts","sourceRoot":"","sources":["../../src/types/artifact-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAE1F,KAAK,CACH,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEhC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAEnF,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CAC1F"}