solmate-skills 2.0.12 → 2.0.13

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 (36) hide show
  1. package/AGENTS.md +9 -0
  2. package/CLAUDE.md +13 -3
  3. package/README.md +91 -5
  4. package/USAGE.md +118 -12
  5. package/bin/cli.js +171 -1
  6. package/bin/harness-artifact.js +621 -0
  7. package/bin/harness-artifact.test.js +520 -0
  8. package/bin/harness-check.js +373 -0
  9. package/bin/harness-check.test.js +159 -0
  10. package/bin/test.js +2 -0
  11. package/docs/01_Concept_Design/01_AGENT_HARNESS_REQUIREMENTS_ANALYSIS.md +162 -0
  12. package/docs/03_Technical_Specs/01_AGENT_HARNESS_ARCHITECTURE.md +376 -0
  13. package/docs/03_Technical_Specs/assets/01_agent_harness_data_flow.svg +94 -0
  14. package/docs/04_Logic_Progress/00_BACKLOG.md +339 -0
  15. package/docs/04_Logic_Progress/01_EXECUTION_PLAN.md +160 -0
  16. package/docs/04_Logic_Progress/03_DECISION_LOG.md +98 -0
  17. package/docs/05_QA_Validation/01_TEST_SCENARIOS.md +313 -0
  18. package/docs/05_QA_Validation/02_AGENT_HARNESS_DESIGN_REVIEW.md +100 -0
  19. package/docs/05_QA_Validation/03_AGENT_HARNESS_CONTRACT_QA.md +96 -0
  20. package/manage-collaboration/SKILL.md +2 -0
  21. package/package.json +6 -4
  22. package/rules-docs/SKILL.md +15 -1
  23. package/rules-product/SKILL.md +30 -6
  24. package/rules-product/agents/openai.yaml +2 -2
  25. package/rules-workflow/SKILL.md +32 -7
  26. package/rules-workflow/adapters/claude/solmate-context-reader.md +17 -0
  27. package/rules-workflow/adapters/claude/solmate-implementer.md +20 -0
  28. package/rules-workflow/adapters/claude/solmate-verifier.md +21 -0
  29. package/rules-workflow/agents/openai.yaml +2 -2
  30. package/rules-workflow/resources/agent-harness-contract.md +187 -0
  31. package/rules-workflow/resources/agent-harness-v1.schema.json +432 -0
  32. package/verify-docs/SKILL.md +33 -6
  33. package/verify-docs/agents/openai.yaml +2 -2
  34. package/verify-implementation/SKILL.md +14 -2
  35. package/verify-implementation/agents/openai.yaml +2 -2
  36. package/verify-skills/SKILL.md +27 -0
@@ -0,0 +1,373 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_BACKLOG = 'docs/04_Logic_Progress/00_BACKLOG.md';
5
+ const RELATED_FIELDS = [
6
+ 'Related Concept Docs',
7
+ 'Related UI Docs',
8
+ 'Related HTML Preview',
9
+ 'Related Technical Docs',
10
+ 'Related QA Docs',
11
+ ];
12
+ const MANDATORY_WORK_TYPES = new Set(['code', 'deploy']);
13
+ const ADVISORY_WORK_TYPES = new Set(['docs', 'prototype']);
14
+
15
+ function escapeRegExp(value) {
16
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+
19
+ function findTaskBlock(content, taskId) {
20
+ const headingPattern = /^#{2,6}\s+\[([ xX])\]\s+([^\n]+)$/gm;
21
+ const headings = [];
22
+ let match;
23
+
24
+ while ((match = headingPattern.exec(content)) !== null) {
25
+ headings.push({ start: match.index, heading: match[2], checked: match[1].toLowerCase() === 'x' });
26
+ }
27
+
28
+ const idPattern = new RegExp(`(^|\\s)${escapeRegExp(taskId)}(?=\\s|:|$)`, 'i');
29
+ const index = headings.findIndex(item => idPattern.test(item.heading));
30
+
31
+ if (index === -1) {
32
+ return null;
33
+ }
34
+
35
+ const end = index + 1 < headings.length ? headings[index + 1].start : content.length;
36
+ return {
37
+ block: content.slice(headings[index].start, end),
38
+ checked: headings[index].checked,
39
+ heading: headings[index].heading,
40
+ };
41
+ }
42
+
43
+ function extractField(block, fieldName) {
44
+ const lines = block.split(/\r?\n/);
45
+ const fieldPattern = new RegExp(`^- ${escapeRegExp(fieldName)}:\\s*(.*)$`);
46
+ const start = lines.findIndex(line => fieldPattern.test(line));
47
+
48
+ if (start === -1) {
49
+ return null;
50
+ }
51
+
52
+ const firstMatch = lines[start].match(fieldPattern);
53
+ const collected = firstMatch[1] ? [firstMatch[1]] : [];
54
+
55
+ for (let index = start + 1; index < lines.length; index += 1) {
56
+ if (/^- [A-Za-z][^:]*:\s*/.test(lines[index])) {
57
+ break;
58
+ }
59
+ collected.push(lines[index]);
60
+ }
61
+
62
+ return collected.join('\n').replace(/^\n+/, '').replace(/\s+$/, '');
63
+ }
64
+
65
+ function extractNestedField(block, fieldName) {
66
+ const lines = block.split(/\r?\n/);
67
+ const fieldPattern = new RegExp(`^(\\s+)- ${escapeRegExp(fieldName)}:\\s*(.*)$`);
68
+ const start = lines.findIndex(line => fieldPattern.test(line));
69
+
70
+ if (start === -1) {
71
+ return null;
72
+ }
73
+
74
+ const firstMatch = lines[start].match(fieldPattern);
75
+ const baseIndent = firstMatch[1].length;
76
+ const collected = firstMatch[2] ? [firstMatch[2]] : [];
77
+
78
+ for (let index = start + 1; index < lines.length; index += 1) {
79
+ const nestedField = lines[index].match(/^(\s+)- [A-Za-z][^:]*:\s*/);
80
+ if (nestedField && nestedField[1].length <= baseIndent) {
81
+ break;
82
+ }
83
+ collected.push(lines[index]);
84
+ }
85
+
86
+ return collected.join('\n').replace(/^\n+/, '').replace(/\s+$/, '');
87
+ }
88
+
89
+ function extractLinks(value) {
90
+ const links = [];
91
+ const pattern = /\[[^\]]+\]\(([^)]+)\)/g;
92
+ let match;
93
+
94
+ while ((match = pattern.exec(value || '')) !== null) {
95
+ links.push(match[1].trim().replace(/^<|>$/g, ''));
96
+ }
97
+
98
+ return links;
99
+ }
100
+
101
+ function extractBacktickValues(value) {
102
+ const values = [];
103
+ const pattern = /`([^`]+)`/g;
104
+ let match;
105
+
106
+ while ((match = pattern.exec(value || '')) !== null) {
107
+ values.push(match[1].trim());
108
+ }
109
+
110
+ return values;
111
+ }
112
+
113
+ function normalizeReference(value) {
114
+ const withoutAnchor = value.split('#')[0].split('?')[0];
115
+ return path.normalize(withoutAnchor).replace(/\\/g, '/').replace(/^\.\//, '');
116
+ }
117
+
118
+ function hasReasonedNA(value) {
119
+ return /N\/A\s*-\s*\S+/i.test(value || '');
120
+ }
121
+
122
+ function isExternalLink(value) {
123
+ return /^[a-z][a-z0-9+.-]*:/i.test(value);
124
+ }
125
+
126
+ function validateRelatedFields(task, backlogPath, errors) {
127
+ const relatedLinks = [];
128
+ const backlogDir = path.dirname(backlogPath);
129
+
130
+ for (const fieldName of RELATED_FIELDS) {
131
+ const field = extractField(task.block, fieldName);
132
+ if (!field) {
133
+ errors.push(`${fieldName} is missing.`);
134
+ continue;
135
+ }
136
+
137
+ const links = extractLinks(field);
138
+ if (links.length === 0) {
139
+ if (!hasReasonedNA(field)) {
140
+ errors.push(`${fieldName} must contain a relative link or \`N/A - reason\`.`);
141
+ }
142
+ continue;
143
+ }
144
+
145
+ for (const link of links) {
146
+ if (isExternalLink(link)) {
147
+ errors.push(`${fieldName} must use a relative project path: ${link}`);
148
+ continue;
149
+ }
150
+
151
+ const normalized = normalizeReference(link);
152
+ const absolute = path.resolve(backlogDir, normalized);
153
+ if (!fs.existsSync(absolute)) {
154
+ errors.push(`${fieldName} points to a missing file: ${link}`);
155
+ continue;
156
+ }
157
+ relatedLinks.push(normalized);
158
+ }
159
+ }
160
+
161
+ return relatedLinks;
162
+ }
163
+
164
+ function validateContextReceipt(task, relatedLinks, errors) {
165
+ const receipt = extractField(task.block, 'Context Receipt');
166
+ if (!receipt) {
167
+ errors.push('Context Receipt is missing.');
168
+ return;
169
+ }
170
+
171
+ const status = extractNestedField(receipt, 'Status');
172
+ if (!status || !/^PASS\b/i.test(status)) {
173
+ errors.push('Context Receipt status must be PASS.');
174
+ }
175
+
176
+ const readField = extractNestedField(receipt, 'Required References Read');
177
+ if (!readField) {
178
+ errors.push('Context Receipt must list Required References Read.');
179
+ } else {
180
+ const readReferences = [...extractLinks(readField), ...extractBacktickValues(readField)]
181
+ .filter(value => !isExternalLink(value))
182
+ .map(normalizeReference);
183
+ const readSet = new Set(readReferences);
184
+
185
+ for (const linkedPath of relatedLinks) {
186
+ if (!readSet.has(linkedPath)) {
187
+ errors.push(`Linked document was not recorded as read: ${linkedPath}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ if (!extractNestedField(receipt, 'Constraints')) {
193
+ errors.push('Context Receipt must include Constraints.');
194
+ }
195
+ if (!extractNestedField(receipt, 'Conflicts')) {
196
+ errors.push('Context Receipt must include Conflicts.');
197
+ }
198
+ }
199
+
200
+ function validateVerificationReceipt(task, backlogPath, errors) {
201
+ const receipt = extractField(task.block, 'Verification Receipt');
202
+ if (!receipt) {
203
+ errors.push('Verification Receipt is missing.');
204
+ return;
205
+ }
206
+
207
+ const status = extractNestedField(receipt, 'Status');
208
+ if (!status || !/^PASS\b/i.test(status)) {
209
+ errors.push('Verification Receipt status must be PASS.');
210
+ }
211
+ if (/\bFAIL\b/i.test(receipt)) {
212
+ errors.push('Verification Receipt contains a FAIL result.');
213
+ }
214
+
215
+ const commands = extractNestedField(receipt, 'Commands and Results');
216
+ if (!commands || !/\bPASS\b/i.test(commands)) {
217
+ errors.push('Verification Receipt must include at least one passing command result.');
218
+ }
219
+
220
+ const unrun = extractNestedField(receipt, 'Unrun Checks');
221
+ if (!unrun) {
222
+ errors.push('Verification Receipt must explain Unrun Checks.');
223
+ }
224
+
225
+ const evidence = extractNestedField(receipt, 'Detailed Evidence');
226
+ const evidenceLinks = extractLinks(evidence || '');
227
+ if (!evidence || evidenceLinks.length === 0) {
228
+ errors.push('Verification Receipt must link Detailed Evidence to a QA document or PR.');
229
+ return;
230
+ }
231
+
232
+ const backlogDir = path.dirname(backlogPath);
233
+ const hasValidEvidence = evidenceLinks.some(link => {
234
+ if (/^https:\/\/github\.com\/.+\/pull\/\d+/i.test(link)) {
235
+ return true;
236
+ }
237
+ if (isExternalLink(link)) {
238
+ return false;
239
+ }
240
+ const absolute = path.resolve(backlogDir, normalizeReference(link));
241
+ return fs.existsSync(absolute) && /(?:^|\/)05_QA_Validation\//.test(absolute.replace(/\\/g, '/'));
242
+ });
243
+
244
+ if (!hasValidEvidence) {
245
+ errors.push('Detailed Evidence must point to an existing QA document or a GitHub PR.');
246
+ }
247
+ }
248
+
249
+ function checkHarnessTask(options) {
250
+ const stage = options.stage;
251
+ const mode = options.mode || 'warning';
252
+ const taskId = options.taskId;
253
+ const backlogPath = path.resolve(options.cwd || process.cwd(), options.backlogPath || DEFAULT_BACKLOG);
254
+ const errors = [];
255
+ const notes = [];
256
+
257
+ if (!['preflight', 'verify'].includes(stage)) {
258
+ return { operationalError: true, valid: false, errors: [`Unknown harness stage: ${stage}`] };
259
+ }
260
+ if (!['warning', 'blocking'].includes(mode)) {
261
+ return { operationalError: true, valid: false, errors: [`Unknown harness mode: ${mode}`] };
262
+ }
263
+ if (!taskId) {
264
+ return { operationalError: true, valid: false, errors: ['A backlog task ID is required.'] };
265
+ }
266
+ if (!fs.existsSync(backlogPath)) {
267
+ return { operationalError: true, valid: false, errors: [`Backlog file not found: ${backlogPath}`] };
268
+ }
269
+
270
+ const content = fs.readFileSync(backlogPath, 'utf8');
271
+ const task = findTaskBlock(content, taskId);
272
+ if (!task) {
273
+ return { operationalError: true, valid: false, errors: [`Task not found in backlog: ${taskId}`] };
274
+ }
275
+
276
+ const workTypeValue = extractField(task.block, 'Work Type');
277
+ const workType = (workTypeValue || '').split(/\s/)[0].toLowerCase();
278
+ if (!workType) {
279
+ errors.push('Work Type is missing. Use code, deploy, docs, or prototype.');
280
+ } else if (ADVISORY_WORK_TYPES.has(workType)) {
281
+ notes.push(`Work Type ${workType} uses advisory receipts.`);
282
+ return {
283
+ stage,
284
+ mode,
285
+ taskId,
286
+ backlogPath,
287
+ workType,
288
+ applicable: false,
289
+ valid: true,
290
+ checked: task.checked,
291
+ errors,
292
+ notes,
293
+ };
294
+ } else if (!MANDATORY_WORK_TYPES.has(workType)) {
295
+ errors.push(`Unsupported Work Type: ${workType}`);
296
+ }
297
+
298
+ for (const fieldName of ['Implementation Preconditions', 'Acceptance Criteria']) {
299
+ if (!extractField(task.block, fieldName)) {
300
+ errors.push(`${fieldName} is missing.`);
301
+ }
302
+ }
303
+
304
+ const relatedLinks = validateRelatedFields(task, backlogPath, errors);
305
+ validateContextReceipt(task, relatedLinks, errors);
306
+
307
+ if (stage === 'verify') {
308
+ validateVerificationReceipt(task, backlogPath, errors);
309
+ }
310
+
311
+ return {
312
+ stage,
313
+ mode,
314
+ taskId,
315
+ backlogPath,
316
+ workType: workType || 'unknown',
317
+ applicable: true,
318
+ valid: errors.length === 0,
319
+ checked: task.checked,
320
+ errors,
321
+ notes,
322
+ };
323
+ }
324
+
325
+ function formatHarnessResult(result) {
326
+ if (result.operationalError) {
327
+ return ['Harness check: ERROR', ...result.errors.map(error => `- ${error}`)].join('\n');
328
+ }
329
+
330
+ let status = 'PASS';
331
+ if (!result.applicable) {
332
+ status = 'ADVISORY';
333
+ } else if (!result.valid) {
334
+ status = result.mode === 'blocking' ? 'BLOCK' : 'WARN';
335
+ }
336
+
337
+ const lines = [
338
+ `Harness ${result.stage}: ${status}`,
339
+ `Task: ${result.taskId}`,
340
+ `Work Type: ${result.workType}`,
341
+ `Mode: ${result.mode}`,
342
+ `Backlog: ${result.backlogPath}`,
343
+ ];
344
+
345
+ for (const note of result.notes || []) {
346
+ lines.push(`- ${note}`);
347
+ }
348
+ for (const error of result.errors || []) {
349
+ lines.push(`- ${error}`);
350
+ }
351
+
352
+ return lines.join('\n');
353
+ }
354
+
355
+ function getHarnessExitCode(result) {
356
+ if (result.operationalError) {
357
+ return 2;
358
+ }
359
+ if (!result.valid && result.mode === 'blocking') {
360
+ return 1;
361
+ }
362
+ return 0;
363
+ }
364
+
365
+ module.exports = {
366
+ DEFAULT_BACKLOG,
367
+ checkHarnessTask,
368
+ extractField,
369
+ extractNestedField,
370
+ findTaskBlock,
371
+ formatHarnessResult,
372
+ getHarnessExitCode,
373
+ };
@@ -0,0 +1,159 @@
1
+ const assert = require('assert');
2
+ const childProcess = require('child_process');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { checkHarnessTask, getHarnessExitCode } = require('./harness-check');
7
+ const cliPath = path.join(__dirname, 'cli.js');
8
+
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'solmate-harness-'));
10
+ const backlogDir = path.join(root, 'docs', '04_Logic_Progress');
11
+ const conceptDir = path.join(root, 'docs', '01_Concept_Design');
12
+ const qaDir = path.join(root, 'docs', '05_QA_Validation');
13
+
14
+ fs.mkdirSync(backlogDir, { recursive: true });
15
+ fs.mkdirSync(conceptDir, { recursive: true });
16
+ fs.mkdirSync(qaDir, { recursive: true });
17
+ fs.writeFileSync(path.join(conceptDir, '03_PRODUCT_SPECS.md'), '# Product Specs\n');
18
+ fs.writeFileSync(path.join(qaDir, '10_FEATURE_QA.md'), '# QA Report\n');
19
+
20
+ const backlogPath = path.join(backlogDir, '00_BACKLOG.md');
21
+ const validTask = `### [ ] TASK-001: Harness fixture
22
+
23
+ - Status: Doing
24
+ - Work Type: code
25
+ - Related Concept Docs:
26
+ - [Product Specs](../01_Concept_Design/03_PRODUCT_SPECS.md) - scope
27
+ - Related UI Docs:
28
+ - N/A - no UI change
29
+ - Related HTML Preview:
30
+ - N/A - no UI change
31
+ - Related Technical Docs:
32
+ - N/A - no technical contract change
33
+ - Related QA Docs:
34
+ - [QA Report](../05_QA_Validation/10_FEATURE_QA.md) - verification evidence
35
+ - Implementation Preconditions:
36
+ - [x] Read all related documents
37
+ - Acceptance Criteria:
38
+ - [x] Harness fixture passes
39
+ - Context Receipt:
40
+ - Status: PASS
41
+ - Required References Read:
42
+ - [Product Specs](../01_Concept_Design/03_PRODUCT_SPECS.md) - constraints extracted
43
+ - [QA Report](../05_QA_Validation/10_FEATURE_QA.md) - criteria extracted
44
+ - Constraints:
45
+ - Keep the fixture minimal
46
+ - Conflicts: None
47
+ - Change Receipt:
48
+ - Files Changed:
49
+ - \`bin/harness-check.test.js\`
50
+ - Verification Receipt:
51
+ - Status: PASS
52
+ - Commands and Results:
53
+ - \`npm test\` - PASS - fixture complete
54
+ - Unrun Checks:
55
+ - N/A - all planned checks ran
56
+ - Detailed Evidence:
57
+ - [QA Report](../05_QA_Validation/10_FEATURE_QA.md) - full evidence
58
+ `;
59
+
60
+ fs.writeFileSync(backlogPath, validTask);
61
+
62
+ const preflight = checkHarnessTask({
63
+ stage: 'preflight',
64
+ mode: 'blocking',
65
+ taskId: 'TASK-001',
66
+ backlogPath,
67
+ });
68
+ assert.strictEqual(preflight.valid, true, preflight.errors.join('\n'));
69
+ assert.strictEqual(getHarnessExitCode(preflight), 0);
70
+
71
+ const verification = checkHarnessTask({
72
+ stage: 'verify',
73
+ mode: 'blocking',
74
+ taskId: 'TASK-001',
75
+ backlogPath,
76
+ });
77
+ assert.strictEqual(verification.valid, true, verification.errors.join('\n'));
78
+
79
+ const cliPass = childProcess.spawnSync(
80
+ process.execPath,
81
+ [cliPath, 'verify', 'TASK-001', '--strict', '--backlog', backlogPath],
82
+ { cwd: root, encoding: 'utf8' },
83
+ );
84
+ assert.strictEqual(cliPass.status, 0, cliPass.stderr || cliPass.stdout);
85
+ assert(cliPass.stdout.includes('Harness verify: PASS'));
86
+
87
+ fs.writeFileSync(backlogPath, validTask.replace(
88
+ ' - [QA Report](../05_QA_Validation/10_FEATURE_QA.md) - criteria extracted\n',
89
+ '',
90
+ ));
91
+ const missingRead = checkHarnessTask({
92
+ stage: 'preflight',
93
+ mode: 'warning',
94
+ taskId: 'TASK-001',
95
+ backlogPath,
96
+ });
97
+ assert.strictEqual(missingRead.valid, false);
98
+ assert.strictEqual(getHarnessExitCode(missingRead), 0);
99
+ assert(missingRead.errors.some(error => error.includes('not recorded as read')));
100
+
101
+ const cliWarning = childProcess.spawnSync(
102
+ process.execPath,
103
+ [cliPath, 'preflight', 'TASK-001', '--backlog', backlogPath],
104
+ { cwd: root, encoding: 'utf8' },
105
+ );
106
+ assert.strictEqual(cliWarning.status, 0, cliWarning.stderr || cliWarning.stdout);
107
+ assert(cliWarning.stdout.includes('Harness preflight: WARN'));
108
+
109
+ const cliBlock = childProcess.spawnSync(
110
+ process.execPath,
111
+ [cliPath, 'preflight', 'TASK-001', '--strict', '--backlog', backlogPath],
112
+ { cwd: root, encoding: 'utf8' },
113
+ );
114
+ assert.strictEqual(cliBlock.status, 1, cliBlock.stderr || cliBlock.stdout);
115
+ assert(cliBlock.stdout.includes('Harness preflight: BLOCK'));
116
+
117
+ fs.writeFileSync(backlogPath, validTask.replace(
118
+ ' - Detailed Evidence:\n - [QA Report](../05_QA_Validation/10_FEATURE_QA.md) - full evidence\n',
119
+ '',
120
+ ));
121
+ const missingEvidence = checkHarnessTask({
122
+ stage: 'verify',
123
+ mode: 'blocking',
124
+ taskId: 'TASK-001',
125
+ backlogPath,
126
+ });
127
+ assert.strictEqual(missingEvidence.valid, false);
128
+ assert.strictEqual(getHarnessExitCode(missingEvidence), 1);
129
+ assert(missingEvidence.errors.some(error => error.includes('Detailed Evidence')));
130
+
131
+ fs.writeFileSync(backlogPath, validTask.replace('Work Type: code', 'Work Type: docs'));
132
+ const docsTask = checkHarnessTask({
133
+ stage: 'verify',
134
+ mode: 'blocking',
135
+ taskId: 'TASK-001',
136
+ backlogPath,
137
+ });
138
+ assert.strictEqual(docsTask.valid, true);
139
+ assert.strictEqual(docsTask.applicable, false);
140
+
141
+ const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'solmate-agent-install-'));
142
+ const customAgentDir = path.join(installRoot, '.claude', 'agents');
143
+ fs.mkdirSync(customAgentDir, { recursive: true });
144
+ fs.writeFileSync(path.join(customAgentDir, 'custom-reviewer.md'), '# Keep this project agent\n');
145
+
146
+ const agentInstall = childProcess.spawnSync(
147
+ process.execPath,
148
+ [cliPath, 'install', 'agents'],
149
+ { cwd: installRoot, encoding: 'utf8' },
150
+ );
151
+ assert.strictEqual(agentInstall.status, 0, agentInstall.stderr || agentInstall.stdout);
152
+ assert(fs.existsSync(path.join(installRoot, '.agent', 'skills', 'rules-workflow', 'resources', 'agent-harness-contract.md')));
153
+ assert(fs.existsSync(path.join(installRoot, '.agent', 'skills', 'rules-workflow', 'resources', 'agent-harness-v1.schema.json')));
154
+ assert(fs.existsSync(path.join(customAgentDir, 'custom-reviewer.md')));
155
+ for (const fileName of ['solmate-context-reader.md', 'solmate-implementer.md', 'solmate-verifier.md']) {
156
+ assert(fs.existsSync(path.join(customAgentDir, fileName)), `${fileName} was not installed`);
157
+ }
158
+
159
+ console.log('harness checks ok');
package/bin/test.js ADDED
@@ -0,0 +1,2 @@
1
+ require('./harness-check.test');
2
+ require('./harness-artifact.test');