ma-agents 2.22.0 → 2.22.2

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.
@@ -8,233 +8,233 @@
8
8
  "skills": {
9
9
  "ai-audit-trail": {
10
10
  "version": "1.0.0",
11
- "installedAt": "2026-03-26T21:09:23.635Z",
12
- "updatedAt": "2026-03-26T21:09:23.635Z",
13
- "installerVersion": "2.21.0",
11
+ "installedAt": "2026-03-27T13:14:33.974Z",
12
+ "updatedAt": "2026-03-27T13:15:20.507Z",
13
+ "installerVersion": "2.22.0",
14
14
  "agentVersion": "1.0.0"
15
15
  },
16
16
  "auto-bug-detection": {
17
17
  "version": "1.0.0",
18
- "installedAt": "2026-03-26T21:09:24.332Z",
19
- "updatedAt": "2026-03-26T21:09:24.332Z",
20
- "installerVersion": "2.21.0",
18
+ "installedAt": "2026-03-27T13:14:34.451Z",
19
+ "updatedAt": "2026-03-27T13:15:20.926Z",
20
+ "installerVersion": "2.22.0",
21
21
  "agentVersion": "1.0.0"
22
22
  },
23
23
  "cmake-best-practices": {
24
24
  "version": "1.0.0",
25
- "installedAt": "2026-03-26T21:09:24.962Z",
26
- "updatedAt": "2026-03-26T21:09:24.962Z",
27
- "installerVersion": "2.21.0",
25
+ "installedAt": "2026-03-27T13:14:35.393Z",
26
+ "updatedAt": "2026-03-27T13:15:21.540Z",
27
+ "installerVersion": "2.22.0",
28
28
  "agentVersion": "1.0.0"
29
29
  },
30
30
  "code-documentation": {
31
31
  "version": "1.0.0",
32
- "installedAt": "2026-03-26T21:09:25.510Z",
33
- "updatedAt": "2026-03-26T21:09:25.510Z",
34
- "installerVersion": "2.21.0",
32
+ "installedAt": "2026-03-27T13:14:36.540Z",
33
+ "updatedAt": "2026-03-27T13:15:23.304Z",
34
+ "installerVersion": "2.22.0",
35
35
  "agentVersion": "1.0.0"
36
36
  },
37
37
  "code-review": {
38
38
  "version": "1.0.0",
39
- "installedAt": "2026-03-26T21:09:26.150Z",
40
- "updatedAt": "2026-03-26T21:09:26.150Z",
41
- "installerVersion": "2.21.0",
39
+ "installedAt": "2026-03-27T13:14:37.539Z",
40
+ "updatedAt": "2026-03-27T13:15:23.811Z",
41
+ "installerVersion": "2.22.0",
42
42
  "agentVersion": "1.0.0"
43
43
  },
44
44
  "commit-message": {
45
45
  "version": "1.0.0",
46
- "installedAt": "2026-03-26T21:09:26.668Z",
47
- "updatedAt": "2026-03-26T21:09:26.668Z",
48
- "installerVersion": "2.21.0",
46
+ "installedAt": "2026-03-27T13:14:38.095Z",
47
+ "updatedAt": "2026-03-27T13:15:24.518Z",
48
+ "installerVersion": "2.22.0",
49
49
  "agentVersion": "1.0.0"
50
50
  },
51
51
  "cpp-best-practices": {
52
52
  "version": "1.0.0",
53
- "installedAt": "2026-03-26T21:09:27.435Z",
54
- "updatedAt": "2026-03-26T21:09:27.435Z",
55
- "installerVersion": "2.21.0",
53
+ "installedAt": "2026-03-27T13:14:38.948Z",
54
+ "updatedAt": "2026-03-27T13:15:25.084Z",
55
+ "installerVersion": "2.22.0",
56
56
  "agentVersion": "1.0.0"
57
57
  },
58
58
  "cpp-concurrency-safety": {
59
59
  "version": "1.0.0",
60
- "installedAt": "2026-03-26T21:09:27.911Z",
61
- "updatedAt": "2026-03-26T21:09:27.911Z",
62
- "installerVersion": "2.21.0",
60
+ "installedAt": "2026-03-27T13:14:39.505Z",
61
+ "updatedAt": "2026-03-27T13:15:26.193Z",
62
+ "installerVersion": "2.22.0",
63
63
  "agentVersion": "1.0.0"
64
64
  },
65
65
  "cpp-const-correctness": {
66
66
  "version": "1.0.0",
67
- "installedAt": "2026-03-26T21:09:28.570Z",
68
- "updatedAt": "2026-03-26T21:09:28.570Z",
69
- "installerVersion": "2.21.0",
67
+ "installedAt": "2026-03-27T13:14:40.580Z",
68
+ "updatedAt": "2026-03-27T13:15:26.990Z",
69
+ "installerVersion": "2.22.0",
70
70
  "agentVersion": "1.0.0"
71
71
  },
72
72
  "cpp-memory-handling": {
73
73
  "version": "1.0.0",
74
- "installedAt": "2026-03-26T21:09:29.999Z",
75
- "updatedAt": "2026-03-26T21:09:29.999Z",
76
- "installerVersion": "2.21.0",
74
+ "installedAt": "2026-03-27T13:14:41.193Z",
75
+ "updatedAt": "2026-03-27T13:15:27.489Z",
76
+ "installerVersion": "2.22.0",
77
77
  "agentVersion": "1.0.0"
78
78
  },
79
79
  "cpp-modern-composition": {
80
80
  "version": "1.0.0",
81
- "installedAt": "2026-03-26T21:09:30.432Z",
82
- "updatedAt": "2026-03-26T21:09:30.432Z",
83
- "installerVersion": "2.21.0",
81
+ "installedAt": "2026-03-27T13:14:42.043Z",
82
+ "updatedAt": "2026-03-27T13:15:29.179Z",
83
+ "installerVersion": "2.22.0",
84
84
  "agentVersion": "1.0.0"
85
85
  },
86
86
  "cpp-robust-interfaces": {
87
87
  "version": "1.0.0",
88
- "installedAt": "2026-03-26T21:09:31.062Z",
89
- "updatedAt": "2026-03-26T21:09:31.062Z",
90
- "installerVersion": "2.21.0",
88
+ "installedAt": "2026-03-27T13:14:43.450Z",
89
+ "updatedAt": "2026-03-27T13:15:29.580Z",
90
+ "installerVersion": "2.22.0",
91
91
  "agentVersion": "1.0.0"
92
92
  },
93
93
  "create-hardened-docker-skill": {
94
94
  "version": "1.0.0",
95
- "installedAt": "2026-03-26T21:09:32.397Z",
96
- "updatedAt": "2026-03-26T21:09:32.397Z",
97
- "installerVersion": "2.21.0",
95
+ "installedAt": "2026-03-27T13:14:44.413Z",
96
+ "updatedAt": "2026-03-27T13:15:29.992Z",
97
+ "installerVersion": "2.22.0",
98
98
  "agentVersion": "1.0.0"
99
99
  },
100
100
  "csharp-best-practices": {
101
101
  "version": "1.0.0",
102
- "installedAt": "2026-03-26T21:09:33.862Z",
103
- "updatedAt": "2026-03-26T21:09:33.862Z",
104
- "installerVersion": "2.21.0",
102
+ "installedAt": "2026-03-27T13:14:46.264Z",
103
+ "updatedAt": "2026-03-27T13:15:30.443Z",
104
+ "installerVersion": "2.22.0",
105
105
  "agentVersion": "1.0.0"
106
106
  },
107
107
  "docker-hardening-verification": {
108
108
  "version": "1.0.0",
109
- "installedAt": "2026-03-26T21:09:34.474Z",
110
- "updatedAt": "2026-03-26T21:09:34.474Z",
111
- "installerVersion": "2.21.0",
109
+ "installedAt": "2026-03-27T13:14:49.221Z",
110
+ "updatedAt": "2026-03-27T13:15:31.093Z",
111
+ "installerVersion": "2.22.0",
112
112
  "agentVersion": "1.0.0"
113
113
  },
114
114
  "docker-image-signing": {
115
115
  "version": "1.0.0",
116
- "installedAt": "2026-03-26T21:09:35.304Z",
117
- "updatedAt": "2026-03-26T21:09:35.304Z",
118
- "installerVersion": "2.21.0",
116
+ "installedAt": "2026-03-27T13:14:50.464Z",
117
+ "updatedAt": "2026-03-27T13:15:31.696Z",
118
+ "installerVersion": "2.22.0",
119
119
  "agentVersion": "1.0.0"
120
120
  },
121
121
  "document-revision-history": {
122
122
  "version": "1.0.0",
123
- "installedAt": "2026-03-26T21:09:35.608Z",
124
- "updatedAt": "2026-03-26T21:09:35.608Z",
125
- "installerVersion": "2.21.0",
123
+ "installedAt": "2026-03-27T13:14:51.335Z",
124
+ "updatedAt": "2026-03-27T13:15:32.112Z",
125
+ "installerVersion": "2.22.0",
126
126
  "agentVersion": "1.0.0"
127
127
  },
128
128
  "git-workflow-skill": {
129
129
  "version": "2.1.0",
130
- "installedAt": "2026-03-26T21:09:36.190Z",
131
- "updatedAt": "2026-03-26T21:09:36.190Z",
132
- "installerVersion": "2.21.0",
130
+ "installedAt": "2026-03-27T13:14:52.667Z",
131
+ "updatedAt": "2026-03-27T13:15:33.074Z",
132
+ "installerVersion": "2.22.0",
133
133
  "agentVersion": "1.0.0"
134
134
  },
135
135
  "js-ts-dependency-mgmt": {
136
136
  "version": "1.0.0",
137
- "installedAt": "2026-03-26T21:09:36.830Z",
138
- "updatedAt": "2026-03-26T21:09:36.830Z",
139
- "installerVersion": "2.21.0",
137
+ "installedAt": "2026-03-27T13:14:53.324Z",
138
+ "updatedAt": "2026-03-27T13:15:33.900Z",
139
+ "installerVersion": "2.22.0",
140
140
  "agentVersion": "1.0.0"
141
141
  },
142
142
  "js-ts-security-skill": {
143
143
  "version": "1.0.0",
144
- "installedAt": "2026-03-26T21:09:37.313Z",
145
- "updatedAt": "2026-03-26T21:09:37.313Z",
146
- "installerVersion": "2.21.0",
144
+ "installedAt": "2026-03-27T13:14:54.684Z",
145
+ "updatedAt": "2026-03-27T13:15:34.746Z",
146
+ "installerVersion": "2.22.0",
147
147
  "agentVersion": "1.0.0"
148
148
  },
149
149
  "logging-best-practices": {
150
150
  "version": "1.0.0",
151
- "installedAt": "2026-03-26T21:09:38.067Z",
152
- "updatedAt": "2026-03-26T21:09:38.067Z",
153
- "installerVersion": "2.21.0",
151
+ "installedAt": "2026-03-27T13:14:55.520Z",
152
+ "updatedAt": "2026-03-27T13:15:35.345Z",
153
+ "installerVersion": "2.22.0",
154
154
  "agentVersion": "1.0.0"
155
155
  },
156
156
  "open-presentation": {
157
157
  "version": "1.0.0",
158
- "installedAt": "2026-03-26T21:09:38.495Z",
159
- "updatedAt": "2026-03-26T21:09:38.495Z",
160
- "installerVersion": "2.21.0",
158
+ "installedAt": "2026-03-27T13:14:56.572Z",
159
+ "updatedAt": "2026-03-27T13:15:36.079Z",
160
+ "installerVersion": "2.22.0",
161
161
  "agentVersion": "1.0.0"
162
162
  },
163
163
  "opentelemetry-best-practices": {
164
164
  "version": "1.0.0",
165
- "installedAt": "2026-03-26T21:09:39.704Z",
166
- "updatedAt": "2026-03-26T21:09:39.704Z",
167
- "installerVersion": "2.21.0",
165
+ "installedAt": "2026-03-27T13:14:57.706Z",
166
+ "updatedAt": "2026-03-27T13:15:36.702Z",
167
+ "installerVersion": "2.22.0",
168
168
  "agentVersion": "1.0.0"
169
169
  },
170
170
  "python-best-practices": {
171
171
  "version": "1.0.0",
172
- "installedAt": "2026-03-26T21:09:40.184Z",
173
- "updatedAt": "2026-03-26T21:09:40.184Z",
174
- "installerVersion": "2.21.0",
172
+ "installedAt": "2026-03-27T13:14:58.358Z",
173
+ "updatedAt": "2026-03-27T13:15:37.234Z",
174
+ "installerVersion": "2.22.0",
175
175
  "agentVersion": "1.0.0"
176
176
  },
177
177
  "python-dependency-mgmt": {
178
178
  "version": "1.0.0",
179
- "installedAt": "2026-03-26T21:09:40.966Z",
180
- "updatedAt": "2026-03-26T21:09:40.966Z",
181
- "installerVersion": "2.21.0",
179
+ "installedAt": "2026-03-27T13:14:59.525Z",
180
+ "updatedAt": "2026-03-27T13:15:37.665Z",
181
+ "installerVersion": "2.22.0",
182
182
  "agentVersion": "1.0.0"
183
183
  },
184
184
  "python-security-skill": {
185
185
  "version": "1.0.0",
186
- "installedAt": "2026-03-26T21:09:41.595Z",
187
- "updatedAt": "2026-03-26T21:09:41.595Z",
188
- "installerVersion": "2.21.0",
186
+ "installedAt": "2026-03-27T13:15:00.170Z",
187
+ "updatedAt": "2026-03-27T13:15:38.500Z",
188
+ "installerVersion": "2.22.0",
189
189
  "agentVersion": "1.0.0"
190
190
  },
191
191
  "self-signed-cert": {
192
192
  "version": "1.0.0",
193
- "installedAt": "2026-03-26T21:09:42.405Z",
194
- "updatedAt": "2026-03-26T21:09:42.405Z",
195
- "installerVersion": "2.21.0",
193
+ "installedAt": "2026-03-27T13:15:01.246Z",
194
+ "updatedAt": "2026-03-27T13:15:39.156Z",
195
+ "installerVersion": "2.22.0",
196
196
  "agentVersion": "1.0.0"
197
197
  },
198
198
  "skill-creator": {
199
199
  "version": "1.0.0",
200
- "installedAt": "2026-03-26T21:09:43.008Z",
201
- "updatedAt": "2026-03-26T21:09:43.008Z",
202
- "installerVersion": "2.21.0",
200
+ "installedAt": "2026-03-27T13:15:02.937Z",
201
+ "updatedAt": "2026-03-27T13:15:39.724Z",
202
+ "installerVersion": "2.22.0",
203
203
  "agentVersion": "1.0.0"
204
204
  },
205
205
  "story-status-lookup": {
206
206
  "version": "1.0.0",
207
- "installedAt": "2026-03-26T21:09:43.969Z",
208
- "updatedAt": "2026-03-26T21:09:43.969Z",
209
- "installerVersion": "2.21.0",
207
+ "installedAt": "2026-03-27T13:15:04.749Z",
208
+ "updatedAt": "2026-03-27T13:15:40.915Z",
209
+ "installerVersion": "2.22.0",
210
210
  "agentVersion": "1.0.0"
211
211
  },
212
212
  "test-accompanied-development": {
213
213
  "version": "1.0.0",
214
- "installedAt": "2026-03-26T21:09:44.449Z",
215
- "updatedAt": "2026-03-26T21:09:44.449Z",
216
- "installerVersion": "2.21.0",
214
+ "installedAt": "2026-03-27T13:15:05.834Z",
215
+ "updatedAt": "2026-03-27T13:15:41.610Z",
216
+ "installerVersion": "2.22.0",
217
217
  "agentVersion": "1.0.0"
218
218
  },
219
219
  "test-generator": {
220
220
  "version": "1.0.0",
221
- "installedAt": "2026-03-26T21:09:45.134Z",
222
- "updatedAt": "2026-03-26T21:09:45.134Z",
223
- "installerVersion": "2.21.0",
221
+ "installedAt": "2026-03-27T13:15:06.270Z",
222
+ "updatedAt": "2026-03-27T13:15:42.077Z",
223
+ "installerVersion": "2.22.0",
224
224
  "agentVersion": "1.0.0"
225
225
  },
226
226
  "vercel-react-best-practices": {
227
227
  "version": "1.0.0",
228
- "installedAt": "2026-03-26T21:09:45.585Z",
229
- "updatedAt": "2026-03-26T21:09:45.585Z",
230
- "installerVersion": "2.21.0",
228
+ "installedAt": "2026-03-27T13:15:07.368Z",
229
+ "updatedAt": "2026-03-27T13:15:42.663Z",
230
+ "installerVersion": "2.22.0",
231
231
  "agentVersion": "1.0.0"
232
232
  },
233
233
  "verify-hardened-docker-skill": {
234
234
  "version": "1.0.0",
235
- "installedAt": "2026-03-26T21:09:46.757Z",
236
- "updatedAt": "2026-03-26T21:09:46.757Z",
237
- "installerVersion": "2.21.0",
235
+ "installedAt": "2026-03-27T13:15:08.132Z",
236
+ "updatedAt": "2026-03-27T13:15:43.636Z",
237
+ "installerVersion": "2.22.0",
238
238
  "agentVersion": "1.0.0"
239
239
  }
240
240
  }
package/bin/cli.js CHANGED
@@ -360,7 +360,7 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
360
360
 
361
361
  // Step 3.5: BMAD-METHOD Integration
362
362
  if (installScope === 'project') {
363
- const bmadToolsFilter = ['claude-code', 'cursor', 'cline', 'gemini', 'antigravity'];
363
+ const bmadToolsFilter = agents.filter(a => a.category === 'ide').map(a => a.id);
364
364
 
365
365
  if (yesFlag) {
366
366
  // Warn about IDE agents not supported for BMAD customizations
package/lib/installer.js CHANGED
@@ -254,17 +254,23 @@ async function updateAgentInstructions(agent, projectRoot) {
254
254
  if (!agent.instructionFiles || agent.instructionFiles.length === 0) return;
255
255
 
256
256
  // JSON merge strategy (e.g., OpenCode)
257
+ // OpenCode expects instructions to be plain strings, not objects.
258
+ // We identify our entries by a marker prefix in the string content,
259
+ // and also clean up legacy object-format entries from older versions.
257
260
  if (agent.injectionStrategy?.position === 'json-merge') {
258
261
  const targetKey = agent.injectionStrategy.targetKey || 'instructions';
259
262
  const filePath = path.join(projectRoot, agent.instructionFiles[0]);
260
263
  const agentProjectPath = agent.getProjectPath();
261
264
  const relManifestPath = path.relative(projectRoot, path.join(agentProjectPath, 'MANIFEST.yaml')).replace(/\\/g, '/');
262
- const instructionText = `# AI Agent Skills - Planning Instruction\n\nYou have access to a library of skills in your skills directory. Before starting any task:\n\n1. Read the skill manifest at ${relManifestPath}\n2. Based on the task description, select which skills are relevant\n3. Read only the selected skill files\n4. Then proceed with the task\n\nAlways load skills marked with always_load: true.\nDo not load skills that are not relevant to the current task.`;
265
+ const instructionText = `[${MA_AGENTS_SOURCE}] # AI Agent Skills - Planning Instruction\n\nYou have access to a library of skills in your skills directory. Before starting any task:\n\n1. Read the skill manifest at ${relManifestPath}\n2. Based on the task description, select which skills are relevant\n3. Read only the selected skill files\n4. Then proceed with the task\n\nAlways load skills marked with always_load: true.\nDo not load skills that are not relevant to the current task.`;
266
+
267
+ const isMaEntry = (entry) =>
268
+ (typeof entry === 'string' && entry.startsWith(`[${MA_AGENTS_SOURCE}]`)) ||
269
+ (typeof entry === 'object' && entry != null && entry._source === MA_AGENTS_SOURCE);
263
270
 
264
271
  if (!fs.existsSync(filePath)) {
265
272
  // File absent: create fresh (atomic write)
266
- const data = { [targetKey]: [] };
267
- data[targetKey].push({ _source: MA_AGENTS_SOURCE, text: instructionText });
273
+ const data = { [targetKey]: [instructionText] };
268
274
  const tmpPath = filePath + '.tmp';
269
275
  await fs.outputFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
270
276
  await fs.rename(tmpPath, filePath);
@@ -278,11 +284,9 @@ async function updateAgentInstructions(agent, projectRoot) {
278
284
  if (!Array.isArray(data[targetKey])) {
279
285
  data[targetKey] = [];
280
286
  }
281
- // Filter out stale ma-agents entries, keep user entries (null-safe)
282
- const userEntries = data[targetKey].filter(entry => entry != null && entry._source !== MA_AGENTS_SOURCE);
283
- // Append fresh ma-agents entries
284
- const freshEntries = [{ _source: MA_AGENTS_SOURCE, text: instructionText }];
285
- data[targetKey] = [...userEntries, ...freshEntries];
287
+ // Filter out stale ma-agents entries (string or legacy object format), keep user entries
288
+ const userEntries = data[targetKey].filter(entry => entry != null && !isMaEntry(entry));
289
+ data[targetKey] = [...userEntries, instructionText];
286
290
  // Atomic write: temp file then rename
287
291
  const tmpPath = filePath + '.tmp';
288
292
  await fs.outputFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
package/opencode.json CHANGED
@@ -1,8 +1,5 @@
1
1
  {
2
2
  "instructions": [
3
- {
4
- "_source": "ma-agents",
5
- "text": "# AI Agent Skills - Planning Instruction\n\nYou have access to a library of skills in your skills directory. Before starting any task:\n\n1. Read the skill manifest at .opencode/skills/MANIFEST.yaml\n2. Based on the task description, select which skills are relevant\n3. Read only the selected skill files\n4. Then proceed with the task\n\nAlways load skills marked with always_load: true.\nDo not load skills that are not relevant to the current task."
6
- }
3
+ "[ma-agents] # AI Agent Skills - Planning Instruction\n\nYou have access to a library of skills in your skills directory. Before starting any task:\n\n1. Read the skill manifest at .opencode/skills/MANIFEST.yaml\n2. Based on the task description, select which skills are relevant\n3. Read only the selected skill files\n4. Then proceed with the task\n\nAlways load skills marked with always_load: true.\nDo not load skills that are not relevant to the current task."
7
4
  ]
8
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "2.22.0",
3
+ "version": "2.22.2",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4,6 +4,9 @@
4
4
  *
5
5
  * Validates that updateAgentInstructions() creates a valid opencode.json
6
6
  * from scratch when the file does not yet exist, using the json-merge strategy.
7
+ *
8
+ * Updated: instructions entries are now plain strings (OpenCode schema compliance).
9
+ * ma-agents entries are identified by a [ma-agents] prefix in the string.
7
10
  */
8
11
  'use strict';
9
12
 
@@ -69,6 +72,12 @@ function createOpenCodeAgent(projectRoot) {
69
72
  };
70
73
  }
71
74
 
75
+ // Helper: check if an entry is an ma-agents entry (string with prefix)
76
+ function isMaEntry(entry) {
77
+ return (typeof entry === 'string' && entry.startsWith(`[${MA_AGENTS_SOURCE}]`)) ||
78
+ (typeof entry === 'object' && entry != null && entry._source === MA_AGENTS_SOURCE);
79
+ }
80
+
72
81
  // ─── Creation path tests (file absent) ───────────────────────────────────────
73
82
 
74
83
  console.log('\nStory 9.2 — JSON injection: file-absent creation path');
@@ -126,7 +135,7 @@ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
126
135
  });
127
136
  });
128
137
 
129
- await asyncTest('9.2.5: every entry has _source === MA_AGENTS_SOURCE', async () => {
138
+ await asyncTest('9.2.5: every entry is a plain string (OpenCode schema compliance)', async () => {
130
139
  await withTempDir(async (tmpDir) => {
131
140
  const agent = createOpenCodeAgent(tmpDir);
132
141
  const filePath = path.join(tmpDir, 'opencode.json');
@@ -134,13 +143,13 @@ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
134
143
  await updateFn(agent, tmpDir);
135
144
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
136
145
  for (const entry of parsed.instructions) {
137
- assert.strictEqual(entry._source, MA_AGENTS_SOURCE,
138
- `Entry _source should be "${MA_AGENTS_SOURCE}", got: "${entry._source}"`);
146
+ assert.strictEqual(typeof entry, 'string',
147
+ `Entry should be a plain string, got ${typeof entry}: ${JSON.stringify(entry)}`);
139
148
  }
140
149
  });
141
150
  });
142
151
 
143
- await asyncTest('9.2.6: every entry has a non-empty "text" string', async () => {
152
+ await asyncTest('9.2.6: every entry starts with [ma-agents] prefix', async () => {
144
153
  await withTempDir(async (tmpDir) => {
145
154
  const agent = createOpenCodeAgent(tmpDir);
146
155
  const filePath = path.join(tmpDir, 'opencode.json');
@@ -148,10 +157,8 @@ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
148
157
  await updateFn(agent, tmpDir);
149
158
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
150
159
  for (const entry of parsed.instructions) {
151
- assert.strictEqual(typeof entry.text, 'string',
152
- `Entry "text" should be a string, got: ${typeof entry.text}`);
153
- assert.ok(entry.text.length > 0,
154
- 'Entry "text" should not be empty');
160
+ assert.ok(entry.startsWith(`[${MA_AGENTS_SOURCE}]`),
161
+ `Entry should start with [${MA_AGENTS_SOURCE}], got: "${entry.substring(0, 30)}..."`);
155
162
  }
156
163
  });
157
164
  });
@@ -189,7 +196,7 @@ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
189
196
 
190
197
  await updateFn(agent, tmpDir);
191
198
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
192
- const allText = parsed.instructions.map(e => e.text).join('\n');
199
+ const allText = parsed.instructions.join('\n');
193
200
  assert.ok(allText.includes('MANIFEST.yaml'),
194
201
  'Instruction text should reference MANIFEST.yaml');
195
202
  });
@@ -222,27 +229,28 @@ console.log('\nStory 9.2 — JSON injection: file-absent creation path');
222
229
  });
223
230
  });
224
231
 
225
- await asyncTest('9.2.12: file present — user entries preserved, fresh ma-agents entries appended (Story 9.3)', async () => {
232
+ await asyncTest('9.2.12: file present — user entries preserved, fresh ma-agents string appended', async () => {
226
233
  await withTempDir(async (tmpDir) => {
227
234
  const agent = createOpenCodeAgent(tmpDir);
228
235
  const filePath = path.join(tmpDir, 'opencode.json');
229
236
 
230
- // Pre-create a file with a single user entry
231
- const existingContent = { instructions: [{ _source: 'user', text: 'user instruction' }] };
237
+ // Pre-create a file with a single user entry (plain string, no _source)
238
+ const existingContent = { instructions: ['user instruction'] };
232
239
  await fs.writeFile(filePath, JSON.stringify(existingContent, null, 2), 'utf-8');
233
240
 
234
241
  await updateFn(agent, tmpDir);
235
242
 
236
- // Story 9.3 merge: user entry preserved, fresh ma-agents entries appended
237
243
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
238
- const userEntries = parsed.instructions.filter(e => e._source !== MA_AGENTS_SOURCE);
239
- const maEntries = parsed.instructions.filter(e => e._source === MA_AGENTS_SOURCE);
244
+ const userEntries = parsed.instructions.filter(e => !isMaEntry(e));
245
+ const maEntries = parsed.instructions.filter(e => isMaEntry(e));
240
246
  assert.strictEqual(userEntries.length, 1,
241
247
  'Existing user entry should be preserved');
242
- assert.strictEqual(userEntries[0]._source, 'user',
243
- 'User entry _source should remain "user"');
248
+ assert.strictEqual(userEntries[0], 'user instruction',
249
+ 'User entry value should remain unchanged');
244
250
  assert.ok(maEntries.length > 0,
245
251
  'Fresh ma-agents entries should be appended');
252
+ assert.strictEqual(typeof maEntries[0], 'string',
253
+ 'Fresh ma-agents entry should be a plain string');
246
254
  });
247
255
  });
248
256
 
@@ -4,6 +4,9 @@
4
4
  *
5
5
  * Validates that updateAgentInstructions() merges skill instructions into an
6
6
  * existing opencode.json without destroying user configuration.
7
+ *
8
+ * Updated: instructions entries are now plain strings (OpenCode schema compliance).
9
+ * Legacy object-format entries ({ _source, text }) are cleaned up during merge.
7
10
  */
8
11
  'use strict';
9
12
 
@@ -57,13 +60,20 @@ function createOpenCodeAgent(projectRoot) {
57
60
  };
58
61
  }
59
62
 
63
+ // Helper: check if an entry is an ma-agents entry (new string or legacy object format)
64
+ function isMaEntry(entry) {
65
+ return (typeof entry === 'string' && entry.startsWith(`[${MA_AGENTS_SOURCE}]`)) ||
66
+ (typeof entry === 'object' && entry != null && entry._source === MA_AGENTS_SOURCE);
67
+ }
68
+
60
69
  // Helper: create a fixture opencode.json with mixed entries and extra top-level key
70
+ // Includes a legacy object-format ma-agents entry to test backward compat cleanup
61
71
  async function writeFixture(filePath) {
62
72
  const fixture = {
63
73
  theme: 'dark',
64
74
  instructions: [
65
75
  'user instruction',
66
- { text: 'another user entry' },
76
+ 'another user instruction',
67
77
  { _source: MA_AGENTS_SOURCE, text: 'old instruction' }
68
78
  ]
69
79
  };
@@ -84,7 +94,7 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
84
94
  await updateFn(agent, tmpDir);
85
95
 
86
96
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
87
- const userEntries = parsed.instructions.filter(e => e._source !== MA_AGENTS_SOURCE);
97
+ const userEntries = parsed.instructions.filter(e => !isMaEntry(e));
88
98
  assert.strictEqual(userEntries.length, 2,
89
99
  `Expected 2 user entries, got ${userEntries.length}`);
90
100
  });
@@ -99,15 +109,15 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
99
109
  await updateFn(agent, tmpDir);
100
110
 
101
111
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
102
- const userEntries = parsed.instructions.filter(e => e._source !== MA_AGENTS_SOURCE);
112
+ const userEntries = parsed.instructions.filter(e => !isMaEntry(e));
103
113
  assert.strictEqual(userEntries[0], 'user instruction',
104
- `First user entry should be plain string "user instruction", got: ${JSON.stringify(userEntries[0])}`);
105
- assert.deepStrictEqual(userEntries[1], { text: 'another user entry' },
106
- `Second user entry should be { text: "another user entry" }, got: ${JSON.stringify(userEntries[1])}`);
114
+ `First user entry should be "user instruction", got: ${JSON.stringify(userEntries[0])}`);
115
+ assert.strictEqual(userEntries[1], 'another user instruction',
116
+ `Second user entry should be "another user instruction", got: ${JSON.stringify(userEntries[1])}`);
107
117
  });
108
118
  });
109
119
 
110
- await asyncTest('9.3.3: stale ma-agents entry is removed', async () => {
120
+ await asyncTest('9.3.3: legacy object-format ma-agents entry is removed', async () => {
111
121
  await withTempDir(async (tmpDir) => {
112
122
  const agent = createOpenCodeAgent(tmpDir);
113
123
  const filePath = path.join(tmpDir, 'opencode.json');
@@ -116,15 +126,15 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
116
126
  await updateFn(agent, tmpDir);
117
127
 
118
128
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
119
- const oldEntry = parsed.instructions.find(
120
- e => typeof e === 'object' && e._source === MA_AGENTS_SOURCE && e.text === 'old instruction'
129
+ const legacyEntry = parsed.instructions.find(
130
+ e => typeof e === 'object' && e != null && e._source === MA_AGENTS_SOURCE
121
131
  );
122
- assert.strictEqual(oldEntry, undefined,
123
- 'Stale ma-agents entry with text "old instruction" should have been removed');
132
+ assert.strictEqual(legacyEntry, undefined,
133
+ 'Legacy object-format ma-agents entry should have been removed');
124
134
  });
125
135
  });
126
136
 
127
- await asyncTest('9.3.4: fresh ma-agents entries are appended', async () => {
137
+ await asyncTest('9.3.4: fresh ma-agents entry is a plain string', async () => {
128
138
  await withTempDir(async (tmpDir) => {
129
139
  const agent = createOpenCodeAgent(tmpDir);
130
140
  const filePath = path.join(tmpDir, 'opencode.json');
@@ -133,16 +143,14 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
133
143
  await updateFn(agent, tmpDir);
134
144
 
135
145
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
136
- const freshEntries = parsed.instructions.filter(
137
- e => typeof e === 'object' && e._source === MA_AGENTS_SOURCE
138
- );
146
+ const freshEntries = parsed.instructions.filter(e => isMaEntry(e));
139
147
  assert.ok(freshEntries.length > 0,
140
148
  'At least one fresh ma-agents entry should be present');
141
149
  for (const entry of freshEntries) {
142
- assert.strictEqual(typeof entry.text, 'string',
143
- `Fresh entry "text" should be a string, got: ${typeof entry.text}`);
144
- assert.ok(entry.text.length > 0,
145
- 'Fresh entry "text" should not be empty');
150
+ assert.strictEqual(typeof entry, 'string',
151
+ `Fresh entry should be a plain string, got ${typeof entry}: ${JSON.stringify(entry)}`);
152
+ assert.ok(entry.startsWith(`[${MA_AGENTS_SOURCE}]`),
153
+ `Fresh entry should start with [${MA_AGENTS_SOURCE}]`);
146
154
  }
147
155
  });
148
156
  });
@@ -157,12 +165,10 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
157
165
 
158
166
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
159
167
  const instructions = parsed.instructions;
160
- // Find the last index of a user entry and first index of ma-agents entry
161
168
  let lastUserIdx = -1;
162
169
  let firstMaIdx = -1;
163
170
  for (let i = 0; i < instructions.length; i++) {
164
- const e = instructions[i];
165
- if (typeof e === 'object' && e._source === MA_AGENTS_SOURCE) {
171
+ if (isMaEntry(instructions[i])) {
166
172
  if (firstMaIdx === -1) firstMaIdx = i;
167
173
  } else {
168
174
  lastUserIdx = i;
@@ -250,9 +256,7 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
250
256
  await updateFn(agent, tmpDir);
251
257
 
252
258
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
253
- const maEntries = parsed.instructions.filter(
254
- e => typeof e === 'object' && e._source === MA_AGENTS_SOURCE
255
- );
259
+ const maEntries = parsed.instructions.filter(e => isMaEntry(e));
256
260
  // Should have same count as after first run (no accumulation)
257
261
  await withTempDir(async (tmpDir2) => {
258
262
  const agent2 = createOpenCodeAgent(tmpDir2);
@@ -260,9 +264,7 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
260
264
  await writeFixture(filePath2);
261
265
  await updateFn(agent2, tmpDir2);
262
266
  const parsed2 = JSON.parse(fs.readFileSync(filePath2, 'utf-8'));
263
- const maEntries2 = parsed2.instructions.filter(
264
- e => typeof e === 'object' && e._source === MA_AGENTS_SOURCE
265
- );
267
+ const maEntries2 = parsed2.instructions.filter(e => isMaEntry(e));
266
268
  assert.strictEqual(maEntries.length, maEntries2.length,
267
269
  `Running installer twice should not accumulate ma-agents entries. Got ${maEntries.length} after 2 runs, ${maEntries2.length} after 1 run`);
268
270
  });
@@ -273,19 +275,36 @@ console.log('\nStory 9.3 — JSON injection: file-present merge path');
273
275
  await withTempDir(async (tmpDir) => {
274
276
  const agent = createOpenCodeAgent(tmpDir);
275
277
  const filePath = path.join(tmpDir, 'opencode.json');
276
- // Hand-edited file with null entry and a valid user entry
277
- const fixture = { instructions: [null, { text: 'user entry' }, { _source: MA_AGENTS_SOURCE, text: 'old ma-entry' }] };
278
+ // Hand-edited file with null entry, a valid user entry, and a legacy ma-agents object
279
+ const fixture = { instructions: [null, 'user entry', { _source: MA_AGENTS_SOURCE, text: 'old ma-entry' }] };
278
280
  await fs.writeFile(filePath, JSON.stringify(fixture, null, 2), 'utf-8');
279
281
 
280
282
  await updateFn(agent, tmpDir);
281
283
 
282
284
  const result = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
283
- // null should be filtered out — only user entry + fresh ma-agents entry remain
285
+ // null should be filtered out — only user entry + fresh ma-agents string remain
284
286
  assert.ok(!result.instructions.includes(null), 'null entry should be removed from instructions array');
285
- const userEntries = result.instructions.filter(e => e != null && e._source !== MA_AGENTS_SOURCE);
287
+ const userEntries = result.instructions.filter(e => e != null && !isMaEntry(e));
286
288
  assert.strictEqual(userEntries.length, 1, 'valid user entry should be preserved');
287
- const maEntries = result.instructions.filter(e => e != null && e._source === MA_AGENTS_SOURCE);
289
+ const maEntries = result.instructions.filter(e => isMaEntry(e));
288
290
  assert.ok(maEntries.length > 0, 'fresh ma-agents entry should be present');
291
+ assert.strictEqual(typeof maEntries[0], 'string', 'fresh ma-agents entry should be a plain string');
292
+ });
293
+ });
294
+
295
+ await asyncTest('9.3.12: all entries in output are plain strings (OpenCode schema compliance)', async () => {
296
+ await withTempDir(async (tmpDir) => {
297
+ const agent = createOpenCodeAgent(tmpDir);
298
+ const filePath = path.join(tmpDir, 'opencode.json');
299
+ await writeFixture(filePath);
300
+
301
+ await updateFn(agent, tmpDir);
302
+
303
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
304
+ for (const entry of parsed.instructions) {
305
+ assert.strictEqual(typeof entry, 'string',
306
+ `All entries should be plain strings for OpenCode compatibility, got ${typeof entry}: ${JSON.stringify(entry)}`);
307
+ }
289
308
  });
290
309
  });
291
310