sinapse-ai 1.25.1 → 1.25.3
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/.sinapse-ai/data/entity-registry.yaml +39 -14
- package/.sinapse-ai/install-manifest.yaml +3 -3
- package/CHANGELOG.md +2 -8
- package/README.en.md +38 -30
- package/README.md +33 -25
- package/bin/commands/install.js +6 -2
- package/bin/commands/update.js +6 -2
- package/bin/lib/command-generator.js +60 -36
- package/bin/lib/global-provider-adapters.js +244 -49
- package/bin/lib/provider-contract.js +23 -0
- package/bin/lib/provider-parity.js +60 -9
- package/docs/getting-started.md +13 -5
- package/docs/guides/codex-config.md +35 -51
- package/docs/guides/ide-integration.md +22 -23
- package/docs/guides/project-status-feature.md +35 -55
- package/docs/guides/user-guide.md +21 -10
- package/docs/installation/README.md +4 -4
- package/docs/installation/faq.md +84 -70
- package/docs/installation/linux.md +11 -14
- package/docs/installation/macos.md +10 -4
- package/docs/installation/troubleshooting.md +10 -4
- package/docs/installation/v4-quick-start.md +13 -13
- package/docs/installation/windows.md +11 -15
- package/docs/pt/guides/project-status-feature.md +35 -55
- package/docs/pt/guides/user-guide.md +21 -11
- package/docs/pt/installation/README.md +2 -4
- package/docs/pt/installation/faq.md +54 -89
- package/docs/pt/installation/linux.md +9 -27
- package/docs/pt/installation/macos.md +8 -12
- package/docs/pt/installation/troubleshooting.md +10 -8
- package/docs/pt/installation/v4-quick-start.md +16 -16
- package/docs/pt/installation/windows.md +8 -21
- package/docs/troubleshooting.md +9 -7
- package/package.json +1 -1
- package/scripts/validate-article-vii.js +305 -20
- package/scripts/validate-install-docs.js +87 -3
|
@@ -2,17 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const {
|
|
7
|
+
MASTER_ALIAS_ENTRY_POINTS,
|
|
8
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
9
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
10
|
+
SUPREME_PUBLIC_ID,
|
|
11
|
+
} = require('./provider-contract');
|
|
5
12
|
|
|
6
13
|
const NOFOLLOW_READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
7
14
|
|
|
15
|
+
function isPathInside(root, candidate) {
|
|
16
|
+
const relative = path.relative(root, candidate);
|
|
17
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureSafeDirectoryWithinHome(home, directory) {
|
|
21
|
+
const absoluteHome = path.resolve(home);
|
|
22
|
+
const absoluteDirectory = path.resolve(directory);
|
|
23
|
+
if (!isPathInside(absoluteHome, absoluteDirectory)) {
|
|
24
|
+
throw new Error(`Refusing provider write outside HOME: ${absoluteDirectory}`);
|
|
25
|
+
}
|
|
26
|
+
fs.mkdirSync(absoluteHome, { recursive: true });
|
|
27
|
+
const homeStat = fs.lstatSync(absoluteHome);
|
|
28
|
+
if (homeStat.isSymbolicLink() || !homeStat.isDirectory()) {
|
|
29
|
+
throw new Error(`Refusing provider write through unsafe HOME: ${absoluteHome}`);
|
|
30
|
+
}
|
|
31
|
+
const realHome = fs.realpathSync.native(absoluteHome);
|
|
32
|
+
let current = absoluteHome;
|
|
33
|
+
const relative = path.relative(absoluteHome, absoluteDirectory);
|
|
34
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
35
|
+
current = path.join(current, segment);
|
|
36
|
+
if (!fs.existsSync(current)) fs.mkdirSync(current);
|
|
37
|
+
const stat = fs.lstatSync(current);
|
|
38
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
39
|
+
throw new Error(`Refusing provider write through link/reparse path: ${current}`);
|
|
40
|
+
}
|
|
41
|
+
const realCurrent = fs.realpathSync.native(current);
|
|
42
|
+
if (!isPathInside(realHome, realCurrent)) {
|
|
43
|
+
throw new Error(`Refusing provider write outside HOME through reparse path: ${current}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return absoluteDirectory;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertSafeFileWithinHome(home, filePath) {
|
|
50
|
+
ensureSafeDirectoryWithinHome(home, path.dirname(filePath));
|
|
51
|
+
if (fs.existsSync(filePath)) {
|
|
52
|
+
const stat = fs.lstatSync(filePath);
|
|
53
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
54
|
+
throw new Error(`Refusing provider write through unsafe file: ${filePath}`);
|
|
55
|
+
}
|
|
56
|
+
return stat;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
8
61
|
function readRegularFileNoFollowSync(filePath, encoding = null) {
|
|
9
62
|
let handle;
|
|
10
63
|
try {
|
|
11
64
|
handle = fs.openSync(filePath, NOFOLLOW_READ_FLAGS);
|
|
12
|
-
|
|
65
|
+
const opened = fs.fstatSync(handle);
|
|
66
|
+
const after = fs.lstatSync(filePath);
|
|
67
|
+
if (!opened.isFile() || after.isSymbolicLink() || !after.isFile()) return null;
|
|
68
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino) return null;
|
|
13
69
|
return fs.readFileSync(handle, encoding || undefined);
|
|
14
70
|
} catch (error) {
|
|
15
|
-
if (['ENOENT', 'ELOOP', 'EISDIR'].includes(error.code)) return null;
|
|
71
|
+
if (['ENOENT', 'ELOOP', 'EISDIR', 'EINVAL', 'UNKNOWN'].includes(error.code)) return null;
|
|
16
72
|
throw error;
|
|
17
73
|
} finally {
|
|
18
74
|
if (handle !== undefined) fs.closeSync(handle);
|
|
@@ -36,8 +92,11 @@ function renderCodexToml(name, description, instructions) {
|
|
|
36
92
|
|
|
37
93
|
function markGlobalAgent(markdown, fileName = '') {
|
|
38
94
|
let content = markdown;
|
|
39
|
-
if (fileName ===
|
|
40
|
-
content = content.replace(
|
|
95
|
+
if (fileName === `${SUPREME_ORCHESTRATOR_ID}.md`) {
|
|
96
|
+
content = content.replace(
|
|
97
|
+
new RegExp(`^name:\\s*${SUPREME_ORCHESTRATOR_ID}\\s*$`, 'm'),
|
|
98
|
+
`name: ${SUPREME_PUBLIC_ID}`,
|
|
99
|
+
);
|
|
41
100
|
}
|
|
42
101
|
if (content.includes('SINAPSE-MANAGED:global-agent')) return content;
|
|
43
102
|
return `${content.trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-agent -->\n`;
|
|
@@ -54,137 +113,273 @@ function renderGlobalSkill(skillId) {
|
|
|
54
113
|
generic ? '# SINAPSE Global Agent Activator' : '# SINAPSE Global Orchestrator', '',
|
|
55
114
|
generic
|
|
56
115
|
? 'Resolve the requested ID through the native agent catalog of the active provider; reject unknown IDs without guessing.'
|
|
57
|
-
:
|
|
116
|
+
: `Activate the public adapter \`${SUPREME_PUBLIC_ID}\`, backed by canonical source ID \`${SUPREME_ORCHESTRATOR_ID}\`, and preserve its delegation-only authority.`,
|
|
58
117
|
'Canonical global persona and task sources live under `~/.sinapse/`.',
|
|
59
118
|
'Use native Codex delegation and never start a nested CLI.', '',
|
|
60
119
|
].join('\n');
|
|
61
120
|
}
|
|
62
121
|
|
|
63
|
-
function
|
|
64
|
-
|
|
122
|
+
function captureParentBinding(home, parentPath) {
|
|
123
|
+
ensureSafeDirectoryWithinHome(home, parentPath);
|
|
124
|
+
const handle = fs.openSync(parentPath, fs.constants.O_RDONLY);
|
|
125
|
+
const opened = fs.fstatSync(handle);
|
|
126
|
+
const current = fs.lstatSync(parentPath);
|
|
127
|
+
if (!opened.isDirectory() || current.isSymbolicLink() || !current.isDirectory()
|
|
128
|
+
|| opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
129
|
+
fs.closeSync(handle);
|
|
130
|
+
throw new Error(`Refusing provider write through unstable parent: ${parentPath}`);
|
|
131
|
+
}
|
|
132
|
+
return { handle, dev: opened.dev, ino: opened.ino, realPath: fs.realpathSync.native(parentPath) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertParentBinding(home, parentPath, binding) {
|
|
136
|
+
ensureSafeDirectoryWithinHome(home, parentPath);
|
|
137
|
+
const current = fs.lstatSync(parentPath);
|
|
138
|
+
const opened = fs.fstatSync(binding.handle);
|
|
139
|
+
const realPath = fs.realpathSync.native(parentPath);
|
|
140
|
+
if (current.isSymbolicLink() || !current.isDirectory() || !opened.isDirectory()
|
|
141
|
+
|| current.dev !== binding.dev || current.ino !== binding.ino
|
|
142
|
+
|| opened.dev !== binding.dev || opened.ino !== binding.ino
|
|
143
|
+
|| realPath !== binding.realPath) {
|
|
144
|
+
throw new Error(`Refusing provider publish after parent path changed: ${parentPath}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function unlinkFileWithBinding(home, filePath, binding, expected, beforeDelete) {
|
|
149
|
+
if (typeof beforeDelete === 'function') beforeDelete({ filePath, parentPath: path.dirname(filePath) });
|
|
150
|
+
assertParentBinding(home, path.dirname(filePath), binding);
|
|
151
|
+
let current;
|
|
152
|
+
try {
|
|
153
|
+
current = fs.lstatSync(filePath);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.code === 'ENOENT') return false;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (current.isSymbolicLink() || !current.isFile()
|
|
159
|
+
|| current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
160
|
+
throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
fs.unlinkSync(filePath);
|
|
163
|
+
assertParentBinding(home, path.dirname(filePath), binding);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeFileAtomically(filePath, content, home = path.dirname(filePath), options = {}) {
|
|
168
|
+
const destinationIdentity = assertSafeFileWithinHome(home, filePath);
|
|
169
|
+
const parentPath = path.dirname(filePath);
|
|
170
|
+
const binding = captureParentBinding(home, parentPath);
|
|
171
|
+
const temporaryPath = path.join(
|
|
172
|
+
parentPath,
|
|
173
|
+
`.${path.basename(filePath)}.${crypto.randomBytes(24).toString('hex')}.tmp`,
|
|
174
|
+
);
|
|
65
175
|
let handle;
|
|
176
|
+
let temporaryIdentity;
|
|
177
|
+
let temporaryExists = false;
|
|
66
178
|
try {
|
|
67
179
|
handle = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
180
|
+
temporaryIdentity = fs.fstatSync(handle);
|
|
181
|
+
temporaryExists = true;
|
|
68
182
|
fs.writeFileSync(handle, content, 'utf8');
|
|
69
183
|
fs.fsyncSync(handle);
|
|
70
184
|
fs.closeSync(handle);
|
|
71
185
|
handle = undefined;
|
|
72
|
-
|
|
186
|
+
if (typeof options.beforePublish === 'function') {
|
|
187
|
+
options.beforePublish({ filePath, parentPath, temporaryPath });
|
|
188
|
+
}
|
|
189
|
+
assertParentBinding(home, parentPath, binding);
|
|
190
|
+
if (options.exclusive) {
|
|
191
|
+
fs.linkSync(temporaryPath, filePath);
|
|
192
|
+
assertParentBinding(home, parentPath, binding);
|
|
193
|
+
unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
|
|
194
|
+
temporaryExists = false;
|
|
195
|
+
} else {
|
|
196
|
+
const currentDestination = assertSafeFileWithinHome(home, filePath);
|
|
197
|
+
if (destinationIdentity && (!currentDestination
|
|
198
|
+
|| currentDestination.dev !== destinationIdentity.dev
|
|
199
|
+
|| currentDestination.ino !== destinationIdentity.ino)) {
|
|
200
|
+
throw new Error(`Refusing provider overwrite after file identity changed: ${filePath}`);
|
|
201
|
+
}
|
|
202
|
+
if (!destinationIdentity && currentDestination) {
|
|
203
|
+
throw new Error(`Refusing provider overwrite of a newly appeared file: ${filePath}`);
|
|
204
|
+
}
|
|
205
|
+
assertParentBinding(home, parentPath, binding);
|
|
206
|
+
fs.renameSync(temporaryPath, filePath);
|
|
207
|
+
temporaryExists = false;
|
|
208
|
+
}
|
|
209
|
+
assertParentBinding(home, parentPath, binding);
|
|
73
210
|
} finally {
|
|
74
211
|
if (handle !== undefined) fs.closeSync(handle);
|
|
75
|
-
|
|
212
|
+
let parentStillBound = false;
|
|
213
|
+
try {
|
|
214
|
+
assertParentBinding(home, parentPath, binding);
|
|
215
|
+
parentStillBound = true;
|
|
216
|
+
} catch { /* never traverse the temporary pathname after an ancestor swap */ }
|
|
217
|
+
if (parentStillBound && temporaryExists) {
|
|
218
|
+
try {
|
|
219
|
+
unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
|
|
220
|
+
} catch { /* fail closed: leave an untrusted temporary path untouched */ }
|
|
221
|
+
}
|
|
222
|
+
fs.closeSync(binding.handle);
|
|
76
223
|
}
|
|
77
224
|
}
|
|
78
225
|
|
|
79
|
-
function writeGlobalSkillIfManaged(skillPath, content) {
|
|
80
|
-
|
|
226
|
+
function writeGlobalSkillIfManaged(skillPath, content, home = path.dirname(skillPath), options = {}) {
|
|
227
|
+
ensureSafeDirectoryWithinHome(home, path.dirname(skillPath));
|
|
81
228
|
|
|
82
229
|
let handle;
|
|
83
230
|
try {
|
|
231
|
+
try {
|
|
232
|
+
const existingStat = fs.lstatSync(skillPath);
|
|
233
|
+
if (existingStat.isSymbolicLink() || !existingStat.isFile()) return false;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code !== 'ENOENT') throw error;
|
|
236
|
+
}
|
|
84
237
|
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
85
238
|
handle = fs.openSync(skillPath, fs.constants.O_RDONLY | noFollow);
|
|
86
239
|
} catch (error) {
|
|
87
240
|
if (error.code === 'ELOOP') return false;
|
|
88
241
|
if (error.code !== 'ENOENT') throw error;
|
|
89
242
|
try {
|
|
90
|
-
|
|
243
|
+
writeFileAtomically(skillPath, content, home, { ...options, exclusive: true });
|
|
244
|
+
return true;
|
|
91
245
|
} catch (createError) {
|
|
92
|
-
if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content);
|
|
246
|
+
if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content, home, options);
|
|
93
247
|
throw createError;
|
|
94
248
|
}
|
|
95
|
-
try {
|
|
96
|
-
fs.writeFileSync(handle, content, 'utf8');
|
|
97
|
-
return true;
|
|
98
|
-
} finally {
|
|
99
|
-
fs.closeSync(handle);
|
|
100
|
-
}
|
|
101
249
|
}
|
|
102
250
|
|
|
103
251
|
try {
|
|
104
|
-
|
|
252
|
+
const opened = fs.fstatSync(handle);
|
|
253
|
+
const current = fs.lstatSync(skillPath);
|
|
254
|
+
if (!opened.isFile() || current.isSymbolicLink() || !current.isFile()) return false;
|
|
255
|
+
if (current.dev !== opened.dev || current.ino !== opened.ino) return false;
|
|
105
256
|
const existing = fs.readFileSync(handle, 'utf8');
|
|
106
257
|
if (!existing.includes('SINAPSE-MANAGED:global-skill')) return false;
|
|
107
258
|
fs.closeSync(handle);
|
|
108
259
|
handle = undefined;
|
|
109
|
-
writeFileAtomically(skillPath, content);
|
|
260
|
+
writeFileAtomically(skillPath, content, home);
|
|
110
261
|
return true;
|
|
111
262
|
} finally {
|
|
112
263
|
if (handle !== undefined) fs.closeSync(handle);
|
|
113
264
|
}
|
|
114
265
|
}
|
|
115
266
|
|
|
116
|
-
function
|
|
267
|
+
function isValidGlobalSkill(skillId, skillPath) {
|
|
268
|
+
const content = readRegularFileNoFollowSync(skillPath, 'utf8');
|
|
269
|
+
return content !== null && content === renderGlobalSkill(skillId);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function removeStaleManagedAgents(targetDir, expectedFiles, extension, options = {}) {
|
|
117
273
|
if (!fs.existsSync(targetDir)) return [];
|
|
274
|
+
const home = options.home || path.dirname(targetDir);
|
|
275
|
+
const binding = captureParentBinding(home, targetDir);
|
|
118
276
|
const expected = new Set(expectedFiles);
|
|
119
277
|
const removed = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
278
|
+
try {
|
|
279
|
+
for (const file of fs.readdirSync(targetDir).filter((name) => name.endsWith(extension))) {
|
|
280
|
+
if (expected.has(file)) continue;
|
|
281
|
+
const filePath = path.join(targetDir, file);
|
|
282
|
+
const identity = fs.lstatSync(filePath);
|
|
283
|
+
const content = readRegularFileNoFollowSync(filePath, 'utf8');
|
|
284
|
+
if (content === null) continue;
|
|
285
|
+
const explicitlyManaged = content.includes('SINAPSE-MANAGED:global-agent');
|
|
286
|
+
const legacyManaged = content.includes('ACTIVATION-NOTICE: This command activates an agent from sinapse.')
|
|
287
|
+
&& /\.sinapse[\\/]sinapse[\\/]agents[\\/]/.test(content)
|
|
288
|
+
&& content.includes('Load the squad manifest');
|
|
289
|
+
if (!explicitlyManaged && !legacyManaged) continue;
|
|
290
|
+
if (unlinkFileWithBinding(home, filePath, binding, identity, options.beforeDelete)) removed.push(file);
|
|
291
|
+
}
|
|
292
|
+
} finally {
|
|
293
|
+
fs.closeSync(binding.handle);
|
|
132
294
|
}
|
|
133
295
|
return removed;
|
|
134
296
|
}
|
|
135
297
|
|
|
136
|
-
function
|
|
298
|
+
function removeManagedFileWithBinding(home, filePath, beforeDelete) {
|
|
299
|
+
let identity;
|
|
300
|
+
try {
|
|
301
|
+
identity = fs.lstatSync(filePath);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error.code === 'ENOENT') return false;
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
const content = readRegularFileNoFollowSync(filePath, 'utf8');
|
|
307
|
+
if (content === null || !content.includes('SINAPSE-MANAGED:global-agent')) return false;
|
|
308
|
+
const afterRead = fs.lstatSync(filePath);
|
|
309
|
+
if (afterRead.isSymbolicLink() || !afterRead.isFile()
|
|
310
|
+
|| afterRead.dev !== identity.dev || afterRead.ino !== identity.ino) {
|
|
311
|
+
throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
|
|
312
|
+
}
|
|
313
|
+
const binding = captureParentBinding(home, path.dirname(filePath));
|
|
314
|
+
try {
|
|
315
|
+
return unlinkFileWithBinding(home, filePath, binding, identity, beforeDelete);
|
|
316
|
+
} finally {
|
|
317
|
+
fs.closeSync(binding.handle);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir, testHooks = {} }) {
|
|
137
322
|
let commandFiles = fs.existsSync(commandsDir)
|
|
138
323
|
? fs.readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()
|
|
139
324
|
: [];
|
|
140
|
-
if (commandFiles.includes(
|
|
141
|
-
const aliasFiles = new Set(
|
|
325
|
+
if (commandFiles.includes(`${SUPREME_ORCHESTRATOR_ID}.md`)) {
|
|
326
|
+
const aliasFiles = new Set(MASTER_ALIAS_ENTRY_POINTS.map((id) => `${id}.md`));
|
|
142
327
|
commandFiles = commandFiles.filter((file) => !aliasFiles.has(file));
|
|
143
328
|
}
|
|
144
|
-
const written = {
|
|
329
|
+
const written = {
|
|
330
|
+
claude: [],
|
|
331
|
+
claudeSkills: [],
|
|
332
|
+
claudeAvailableSkills: [],
|
|
333
|
+
codex: [],
|
|
334
|
+
skills: [],
|
|
335
|
+
availableSkills: [],
|
|
336
|
+
};
|
|
145
337
|
|
|
146
338
|
if (llmChoice === 'claude-code' || llmChoice === 'both') {
|
|
147
339
|
const targetDir = path.join(home, '.claude', 'agents');
|
|
148
|
-
|
|
149
|
-
removeStaleManagedAgents(targetDir, commandFiles, '.md');
|
|
340
|
+
ensureSafeDirectoryWithinHome(home, targetDir);
|
|
341
|
+
removeStaleManagedAgents(targetDir, commandFiles, '.md', { home, beforeDelete: testHooks.beforeStaleAgentDelete });
|
|
150
342
|
for (const file of commandFiles) {
|
|
151
343
|
const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
|
|
152
|
-
|
|
344
|
+
writeFileAtomically(path.join(targetDir, file), markdown, home);
|
|
153
345
|
written.claude.push(file);
|
|
154
346
|
}
|
|
155
347
|
const skillsRoot = path.join(home, '.claude', 'skills');
|
|
156
|
-
for (const skillId of
|
|
348
|
+
for (const skillId of GLOBAL_PROVIDER_SKILL_IDS) {
|
|
157
349
|
const skillPath = path.join(skillsRoot, skillId, 'SKILL.md');
|
|
158
|
-
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
|
|
350
|
+
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId), home, { beforePublish: testHooks.beforeSkillPublish })) {
|
|
159
351
|
written.claudeSkills.push(skillId);
|
|
160
352
|
}
|
|
353
|
+
if (isValidGlobalSkill(skillId, skillPath)) written.claudeAvailableSkills.push(skillId);
|
|
161
354
|
}
|
|
162
355
|
}
|
|
163
356
|
if (llmChoice === 'codex' || llmChoice === 'both') {
|
|
164
357
|
const targetDir = path.join(home, '.codex', 'agents');
|
|
165
|
-
|
|
358
|
+
ensureSafeDirectoryWithinHome(home, targetDir);
|
|
166
359
|
removeStaleManagedAgents(
|
|
167
360
|
targetDir,
|
|
168
361
|
commandFiles.map((file) => file.replace(/\.md$/, '.toml')),
|
|
169
362
|
'.toml',
|
|
363
|
+
{ home, beforeDelete: testHooks.beforeStaleAgentDelete },
|
|
170
364
|
);
|
|
171
365
|
for (const file of commandFiles) {
|
|
172
366
|
const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
|
|
173
367
|
const id = file.replace(/\.md$/, '');
|
|
174
368
|
const metadata = parseAgentMarkdown(markdown, id);
|
|
175
369
|
const tomlName = `${id}.toml`;
|
|
176
|
-
|
|
370
|
+
writeFileAtomically(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), home);
|
|
177
371
|
const staleMarkdown = path.join(targetDir, file);
|
|
178
|
-
|
|
372
|
+
removeManagedFileWithBinding(home, staleMarkdown, testHooks.beforeStaleMarkdownDelete);
|
|
179
373
|
written.codex.push(tomlName);
|
|
180
374
|
}
|
|
181
375
|
const skillsRoot = path.join(home, '.agents', 'skills');
|
|
182
|
-
for (const skillId of
|
|
376
|
+
for (const skillId of GLOBAL_PROVIDER_SKILL_IDS) {
|
|
183
377
|
const skillDir = path.join(skillsRoot, skillId);
|
|
184
378
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
185
|
-
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
|
|
379
|
+
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId), home, { beforePublish: testHooks.beforeSkillPublish })) {
|
|
186
380
|
written.skills.push(skillId);
|
|
187
381
|
}
|
|
382
|
+
if (isValidGlobalSkill(skillId, skillPath)) written.availableSkills.push(skillId);
|
|
188
383
|
}
|
|
189
384
|
}
|
|
190
385
|
return written;
|
|
@@ -196,4 +391,4 @@ function getGlobalCommandStagingDir({ llmChoice, sinapseHome, claudeCommandsDir
|
|
|
196
391
|
return path.join(sinapseHome, '.generated', 'agents');
|
|
197
392
|
}
|
|
198
393
|
|
|
199
|
-
module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, writeGlobalSkillIfManaged, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
|
|
394
|
+
module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, readRegularFileNoFollowSync, writeFileAtomically, writeGlobalSkillIfManaged, isValidGlobalSkill, ensureSafeDirectoryWithinHome, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CANONICAL_AGENT_COUNT = 172;
|
|
4
|
+
const SUPREME_ORCHESTRATOR_ID = 'snps-orqx';
|
|
5
|
+
const SUPREME_PUBLIC_ID = SUPREME_ORCHESTRATOR_ID.replace(/^snps/, 'sinapse');
|
|
6
|
+
const MASTER_ALIAS_ENTRY_POINTS = Object.freeze([
|
|
7
|
+
SUPREME_PUBLIC_ID.replace(/-orqx$/, ''),
|
|
8
|
+
SUPREME_PUBLIC_ID,
|
|
9
|
+
SUPREME_ORCHESTRATOR_ID.replace(/-orqx$/, ''),
|
|
10
|
+
]);
|
|
11
|
+
const GLOBAL_PROVIDER_SKILL_IDS = Object.freeze([
|
|
12
|
+
...MASTER_ALIAS_ENTRY_POINTS,
|
|
13
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
14
|
+
'sinapse-agent',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
CANONICAL_AGENT_COUNT,
|
|
19
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
20
|
+
SUPREME_PUBLIC_ID,
|
|
21
|
+
MASTER_ALIAS_ENTRY_POINTS,
|
|
22
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
23
|
+
};
|
|
@@ -1,22 +1,73 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const {
|
|
4
|
+
CANONICAL_AGENT_COUNT,
|
|
5
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
6
|
+
} = require('./provider-contract');
|
|
7
|
+
|
|
8
|
+
function normalizeCanonicalCatalog(canonicalCatalog, extension) {
|
|
9
|
+
if (!canonicalCatalog || typeof canonicalCatalog === 'number') {
|
|
10
|
+
throw new Error('Provider adapter parity requires the canonical agent ID catalog');
|
|
11
|
+
}
|
|
12
|
+
const entries = [...canonicalCatalog].map((entry) => (typeof entry === 'string'
|
|
13
|
+
? { id: entry.replace(/\.md$/, ''), file: entry }
|
|
14
|
+
: entry));
|
|
15
|
+
const sourceIds = entries.map((entry) => entry.id);
|
|
16
|
+
const sourceSet = new Set(sourceIds);
|
|
17
|
+
if (sourceSet.size !== sourceIds.length) {
|
|
18
|
+
const duplicates = [...sourceSet]
|
|
19
|
+
.filter((id) => sourceIds.filter((item) => item === id).length > 1);
|
|
20
|
+
throw new Error(`Canonical agent catalog contains duplicate IDs: ${duplicates.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
if (entries.length !== CANONICAL_AGENT_COUNT) {
|
|
23
|
+
throw new Error(`Canonical agent catalog must contain exactly ${CANONICAL_AGENT_COUNT} unique IDs, found ${entries.length}`);
|
|
24
|
+
}
|
|
25
|
+
const files = entries.map((entry) => entry.file.replace(/\.md$/, extension));
|
|
26
|
+
return { count: CANONICAL_AGENT_COUNT, files: new Set(files) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describeCatalogDivergence(provider, actualFiles, canonical) {
|
|
30
|
+
const actual = new Set(actualFiles);
|
|
31
|
+
const problems = [];
|
|
32
|
+
if (actualFiles.length !== canonical.count) {
|
|
33
|
+
problems.push(`${provider}: ${actualFiles.length}/${canonical.count}`);
|
|
34
|
+
}
|
|
35
|
+
if (canonical.files) {
|
|
36
|
+
if (actual.size !== actualFiles.length) {
|
|
37
|
+
const duplicates = [...actual].filter((file) => actualFiles.filter((item) => item === file).length > 1);
|
|
38
|
+
problems.push(`${provider} duplicate: ${duplicates.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const missing = [...canonical.files].filter((file) => !actual.has(file));
|
|
41
|
+
const orphaned = [...actual].filter((file) => !canonical.files.has(file));
|
|
42
|
+
if (missing.length) problems.push(`${provider} missing: ${missing.join(', ')}`);
|
|
43
|
+
if (orphaned.length) problems.push(`${provider} orphaned: ${orphaned.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
return problems;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertProviderAdapterParity(llmChoice, adapters, canonicalCatalog) {
|
|
4
49
|
if (!['claude-code', 'codex', 'both'].includes(llmChoice)) {
|
|
5
50
|
throw new Error(`Invalid LLM choice for provider parity: ${llmChoice}`);
|
|
6
51
|
}
|
|
7
|
-
const
|
|
52
|
+
const problems = [];
|
|
53
|
+
const claudeCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.md');
|
|
54
|
+
const codexCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.toml');
|
|
8
55
|
if (llmChoice === 'claude-code' || llmChoice === 'both') {
|
|
9
|
-
|
|
56
|
+
problems.push(...describeCatalogDivergence('Claude Code', adapters.claude, claudeCatalog));
|
|
57
|
+
const claudeSkills = new Set(adapters.claudeAvailableSkills || adapters.claudeSkills || []);
|
|
58
|
+
const missingAliases = GLOBAL_PROVIDER_SKILL_IDS.filter((alias) => !claudeSkills.has(alias));
|
|
59
|
+
if (missingAliases.length) problems.push(`Claude Code alias skills missing: ${missingAliases.join(', ')}`);
|
|
10
60
|
}
|
|
11
61
|
if (llmChoice === 'codex' || llmChoice === 'both') {
|
|
12
|
-
|
|
62
|
+
problems.push(...describeCatalogDivergence('Codex', adapters.codex, codexCatalog));
|
|
63
|
+
const codexSkills = new Set(adapters.availableSkills || adapters.skills || []);
|
|
64
|
+
const missingAliases = GLOBAL_PROVIDER_SKILL_IDS.filter((alias) => !codexSkills.has(alias));
|
|
65
|
+
if (missingAliases.length) problems.push(`Codex alias skills missing: ${missingAliases.join(', ')}`);
|
|
13
66
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const details = divergent.map(([provider, count]) => `${provider}: ${count}/${canonicalCount}`).join(', ');
|
|
17
|
-
throw new Error(`Provider adapter parity failed (${details})`);
|
|
67
|
+
if (problems.length) {
|
|
68
|
+
throw new Error(`Provider adapter parity failed (${problems.join('; ')})`);
|
|
18
69
|
}
|
|
19
|
-
return
|
|
70
|
+
return claudeCatalog.count;
|
|
20
71
|
}
|
|
21
72
|
|
|
22
73
|
module.exports = { assertProviderAdapterParity };
|
package/docs/getting-started.md
CHANGED
|
@@ -27,14 +27,22 @@ Se algo falhar: `npx sinapse-ai doctor --fix` corrige automaticamente.
|
|
|
27
27
|
|
|
28
28
|
## Ativar primeiro agente
|
|
29
29
|
|
|
30
|
-
No
|
|
30
|
+
No Claude Code:
|
|
31
31
|
|
|
32
|
-
```
|
|
32
|
+
```text
|
|
33
33
|
@developer
|
|
34
34
|
*help
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
No Codex, use o roteador ou a ativação direta:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
$snps
|
|
41
|
+
$sinapse-agent developer
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`@developer` e `*help` pertencem à superfície do Claude Code. No Codex, `$snps`
|
|
45
|
+
roteia a solicitação e `$sinapse-agent developer` ativa o agente de implementação.
|
|
38
46
|
|
|
39
47
|
## Próximos passos
|
|
40
48
|
|
|
@@ -61,9 +69,9 @@ npx sinapse-ai uninstall # remove tudo
|
|
|
61
69
|
## Conceitos chave (em 30 segundos)
|
|
62
70
|
|
|
63
71
|
- **Squad**: equipe de agentes especializados (ex: squad-design tem 15 agentes de design/UX)
|
|
64
|
-
- **Agente**: persona de IA com
|
|
72
|
+
- **Agente**: persona de IA com especialização e comandos específicos (`@developer` no Claude Code; `$sinapse-agent developer` no Codex)
|
|
65
73
|
- **Hook**: enforcement runtime que bloqueia violações da Constitution (ex: PR sem story)
|
|
66
|
-
- **Constitution**:
|
|
74
|
+
- **Constitution**: 11 artigos que governam o framework (ver `.sinapse-ai/constitution.md`)
|
|
67
75
|
- **Story**: arquivo `.md` que documenta requisitos antes do código (Documentation-First)
|
|
68
76
|
|
|
69
77
|
---
|