ma-agents 2.22.1 → 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.
- package/.opencode/skills/.ma-agents.json +66 -66
- package/lib/installer.js +12 -8
- package/opencode.json +1 -4
- package/package.json +1 -1
- package/test/opencode-json-injection.test.js +25 -17
- package/test/opencode-json-merge.test.js +52 -33
|
@@ -8,232 +8,232 @@
|
|
|
8
8
|
"skills": {
|
|
9
9
|
"ai-audit-trail": {
|
|
10
10
|
"version": "1.0.0",
|
|
11
|
-
"installedAt": "2026-03-
|
|
12
|
-
"updatedAt": "2026-03-
|
|
11
|
+
"installedAt": "2026-03-27T13:14:33.974Z",
|
|
12
|
+
"updatedAt": "2026-03-27T13:15:20.507Z",
|
|
13
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-
|
|
19
|
-
"updatedAt": "2026-03-
|
|
18
|
+
"installedAt": "2026-03-27T13:14:34.451Z",
|
|
19
|
+
"updatedAt": "2026-03-27T13:15:20.926Z",
|
|
20
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-
|
|
26
|
-
"updatedAt": "2026-03-
|
|
25
|
+
"installedAt": "2026-03-27T13:14:35.393Z",
|
|
26
|
+
"updatedAt": "2026-03-27T13:15:21.540Z",
|
|
27
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-
|
|
33
|
-
"updatedAt": "2026-03-
|
|
32
|
+
"installedAt": "2026-03-27T13:14:36.540Z",
|
|
33
|
+
"updatedAt": "2026-03-27T13:15:23.304Z",
|
|
34
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-
|
|
40
|
-
"updatedAt": "2026-03-
|
|
39
|
+
"installedAt": "2026-03-27T13:14:37.539Z",
|
|
40
|
+
"updatedAt": "2026-03-27T13:15:23.811Z",
|
|
41
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-
|
|
47
|
-
"updatedAt": "2026-03-
|
|
46
|
+
"installedAt": "2026-03-27T13:14:38.095Z",
|
|
47
|
+
"updatedAt": "2026-03-27T13:15:24.518Z",
|
|
48
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-
|
|
54
|
-
"updatedAt": "2026-03-
|
|
53
|
+
"installedAt": "2026-03-27T13:14:38.948Z",
|
|
54
|
+
"updatedAt": "2026-03-27T13:15:25.084Z",
|
|
55
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-
|
|
61
|
-
"updatedAt": "2026-03-
|
|
60
|
+
"installedAt": "2026-03-27T13:14:39.505Z",
|
|
61
|
+
"updatedAt": "2026-03-27T13:15:26.193Z",
|
|
62
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-
|
|
68
|
-
"updatedAt": "2026-03-
|
|
67
|
+
"installedAt": "2026-03-27T13:14:40.580Z",
|
|
68
|
+
"updatedAt": "2026-03-27T13:15:26.990Z",
|
|
69
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-
|
|
75
|
-
"updatedAt": "2026-03-
|
|
74
|
+
"installedAt": "2026-03-27T13:14:41.193Z",
|
|
75
|
+
"updatedAt": "2026-03-27T13:15:27.489Z",
|
|
76
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-
|
|
82
|
-
"updatedAt": "2026-03-
|
|
81
|
+
"installedAt": "2026-03-27T13:14:42.043Z",
|
|
82
|
+
"updatedAt": "2026-03-27T13:15:29.179Z",
|
|
83
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-
|
|
89
|
-
"updatedAt": "2026-03-
|
|
88
|
+
"installedAt": "2026-03-27T13:14:43.450Z",
|
|
89
|
+
"updatedAt": "2026-03-27T13:15:29.580Z",
|
|
90
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-
|
|
96
|
-
"updatedAt": "2026-03-
|
|
95
|
+
"installedAt": "2026-03-27T13:14:44.413Z",
|
|
96
|
+
"updatedAt": "2026-03-27T13:15:29.992Z",
|
|
97
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-
|
|
103
|
-
"updatedAt": "2026-03-
|
|
102
|
+
"installedAt": "2026-03-27T13:14:46.264Z",
|
|
103
|
+
"updatedAt": "2026-03-27T13:15:30.443Z",
|
|
104
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-
|
|
110
|
-
"updatedAt": "2026-03-
|
|
109
|
+
"installedAt": "2026-03-27T13:14:49.221Z",
|
|
110
|
+
"updatedAt": "2026-03-27T13:15:31.093Z",
|
|
111
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-
|
|
117
|
-
"updatedAt": "2026-03-
|
|
116
|
+
"installedAt": "2026-03-27T13:14:50.464Z",
|
|
117
|
+
"updatedAt": "2026-03-27T13:15:31.696Z",
|
|
118
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-
|
|
124
|
-
"updatedAt": "2026-03-
|
|
123
|
+
"installedAt": "2026-03-27T13:14:51.335Z",
|
|
124
|
+
"updatedAt": "2026-03-27T13:15:32.112Z",
|
|
125
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-
|
|
131
|
-
"updatedAt": "2026-03-
|
|
130
|
+
"installedAt": "2026-03-27T13:14:52.667Z",
|
|
131
|
+
"updatedAt": "2026-03-27T13:15:33.074Z",
|
|
132
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-
|
|
138
|
-
"updatedAt": "2026-03-
|
|
137
|
+
"installedAt": "2026-03-27T13:14:53.324Z",
|
|
138
|
+
"updatedAt": "2026-03-27T13:15:33.900Z",
|
|
139
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-
|
|
145
|
-
"updatedAt": "2026-03-
|
|
144
|
+
"installedAt": "2026-03-27T13:14:54.684Z",
|
|
145
|
+
"updatedAt": "2026-03-27T13:15:34.746Z",
|
|
146
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-
|
|
152
|
-
"updatedAt": "2026-03-
|
|
151
|
+
"installedAt": "2026-03-27T13:14:55.520Z",
|
|
152
|
+
"updatedAt": "2026-03-27T13:15:35.345Z",
|
|
153
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-
|
|
159
|
-
"updatedAt": "2026-03-
|
|
158
|
+
"installedAt": "2026-03-27T13:14:56.572Z",
|
|
159
|
+
"updatedAt": "2026-03-27T13:15:36.079Z",
|
|
160
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-
|
|
166
|
-
"updatedAt": "2026-03-
|
|
165
|
+
"installedAt": "2026-03-27T13:14:57.706Z",
|
|
166
|
+
"updatedAt": "2026-03-27T13:15:36.702Z",
|
|
167
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-
|
|
173
|
-
"updatedAt": "2026-03-
|
|
172
|
+
"installedAt": "2026-03-27T13:14:58.358Z",
|
|
173
|
+
"updatedAt": "2026-03-27T13:15:37.234Z",
|
|
174
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-
|
|
180
|
-
"updatedAt": "2026-03-
|
|
179
|
+
"installedAt": "2026-03-27T13:14:59.525Z",
|
|
180
|
+
"updatedAt": "2026-03-27T13:15:37.665Z",
|
|
181
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-
|
|
187
|
-
"updatedAt": "2026-03-
|
|
186
|
+
"installedAt": "2026-03-27T13:15:00.170Z",
|
|
187
|
+
"updatedAt": "2026-03-27T13:15:38.500Z",
|
|
188
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-
|
|
194
|
-
"updatedAt": "2026-03-
|
|
193
|
+
"installedAt": "2026-03-27T13:15:01.246Z",
|
|
194
|
+
"updatedAt": "2026-03-27T13:15:39.156Z",
|
|
195
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-
|
|
201
|
-
"updatedAt": "2026-03-
|
|
200
|
+
"installedAt": "2026-03-27T13:15:02.937Z",
|
|
201
|
+
"updatedAt": "2026-03-27T13:15:39.724Z",
|
|
202
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-
|
|
208
|
-
"updatedAt": "2026-03-
|
|
207
|
+
"installedAt": "2026-03-27T13:15:04.749Z",
|
|
208
|
+
"updatedAt": "2026-03-27T13:15:40.915Z",
|
|
209
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-
|
|
215
|
-
"updatedAt": "2026-03-
|
|
214
|
+
"installedAt": "2026-03-27T13:15:05.834Z",
|
|
215
|
+
"updatedAt": "2026-03-27T13:15:41.610Z",
|
|
216
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-
|
|
222
|
-
"updatedAt": "2026-03-
|
|
221
|
+
"installedAt": "2026-03-27T13:15:06.270Z",
|
|
222
|
+
"updatedAt": "2026-03-27T13:15:42.077Z",
|
|
223
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-
|
|
229
|
-
"updatedAt": "2026-03-
|
|
228
|
+
"installedAt": "2026-03-27T13:15:07.368Z",
|
|
229
|
+
"updatedAt": "2026-03-27T13:15:42.663Z",
|
|
230
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-
|
|
236
|
-
"updatedAt": "2026-03-
|
|
235
|
+
"installedAt": "2026-03-27T13:15:08.132Z",
|
|
236
|
+
"updatedAt": "2026-03-27T13:15:43.636Z",
|
|
237
237
|
"installerVersion": "2.22.0",
|
|
238
238
|
"agentVersion": "1.0.0"
|
|
239
239
|
}
|
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 =
|
|
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
|
|
282
|
-
const userEntries = data[targetKey].filter(entry => entry != null && entry
|
|
283
|
-
|
|
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
|
@@ -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
|
|
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
|
|
138
|
-
`Entry
|
|
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
|
|
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.
|
|
152
|
-
`Entry
|
|
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.
|
|
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
|
|
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: [
|
|
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
|
|
239
|
-
const maEntries = parsed.instructions.filter(e => e
|
|
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]
|
|
243
|
-
'User entry
|
|
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
|
-
|
|
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
|
|
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
|
|
112
|
+
const userEntries = parsed.instructions.filter(e => !isMaEntry(e));
|
|
103
113
|
assert.strictEqual(userEntries[0], 'user instruction',
|
|
104
|
-
`First user entry should be
|
|
105
|
-
assert.
|
|
106
|
-
`Second user entry should be
|
|
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:
|
|
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
|
|
120
|
-
e => typeof e === 'object' && e
|
|
129
|
+
const legacyEntry = parsed.instructions.find(
|
|
130
|
+
e => typeof e === 'object' && e != null && e._source === MA_AGENTS_SOURCE
|
|
121
131
|
);
|
|
122
|
-
assert.strictEqual(
|
|
123
|
-
'
|
|
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
|
|
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
|
|
143
|
-
`Fresh entry
|
|
144
|
-
assert.ok(entry.
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
277
|
-
const fixture = { instructions: [null,
|
|
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
|
|
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
|
|
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
|
|
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
|
|