atris 3.1.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +65 -131
- package/README.md +29 -4
- package/atris/GETTING_STARTED.md +65 -131
- package/atris/PERSONA.md +5 -1
- package/atris/atris.md +122 -153
- package/atris/skills/aeo/SKILL.md +117 -0
- package/atris/skills/atris/SKILL.md +49 -25
- package/atris/skills/create-member/SKILL.md +29 -9
- package/atris/skills/endgame/SKILL.md +9 -0
- package/atris/skills/improve/SKILL.md +2 -2
- package/atris/skills/research-search/SKILL.md +167 -0
- package/atris/skills/research-search/arxiv_search.py +157 -0
- package/atris/skills/research-search/program.md +48 -0
- package/atris/skills/research-search/results.tsv +6 -0
- package/atris/skills/research-search/scholar_search.py +154 -0
- package/atris/skills/tidy/SKILL.md +36 -21
- package/atris/team/_template/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +35 -1
- package/atris.md +118 -178
- package/bin/atris.js +37 -6
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
- package/cli/atris_code.py +889 -0
- package/cli/runtime_guard.py +693 -0
- package/commands/align.js +15 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +948 -42
- package/commands/business.js +691 -11
- package/commands/computer.js +1979 -43
- package/commands/context-sync.js +5 -0
- package/commands/experiments.js +1 -1
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +40 -1
- package/commands/push.js +44 -0
- package/commands/release.js +183 -0
- package/commands/research.js +52 -0
- package/commands/serve.js +1 -0
- package/commands/sync.js +372 -87
- package/commands/verify.js +53 -4
- package/commands/wiki.js +71 -26
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/reward-config.js +24 -0
- package/lib/scorecard.js +58 -6
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +235 -60
- package/package.json +4 -2
- package/utils/api.js +19 -0
- package/utils/auth.js +25 -1
- package/utils/config.js +24 -0
- package/utils/update-check.js +16 -0
package/commands/sync.js
CHANGED
|
@@ -3,7 +3,17 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const TEMPLATE_ROOT_DIR = path.join(__dirname, '..', 'templates');
|
|
7
|
+
const WORKSPACE_TEMPLATES = {
|
|
8
|
+
business: {
|
|
9
|
+
dir: path.join(TEMPLATE_ROOT_DIR, 'business-starter'),
|
|
10
|
+
label: 'business environment',
|
|
11
|
+
},
|
|
12
|
+
research: {
|
|
13
|
+
dir: path.join(TEMPLATE_ROOT_DIR, 'research-canonical'),
|
|
14
|
+
label: 'research lab environment',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Walk a directory and return relative file paths.
|
|
@@ -25,7 +35,7 @@ function _walkTemplateDir(dir, base = dir) {
|
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
|
-
* Substitute
|
|
38
|
+
* Substitute workspace metadata in template content.
|
|
29
39
|
*/
|
|
30
40
|
function _substituteParams(content, params) {
|
|
31
41
|
return content
|
|
@@ -33,7 +43,129 @@ function _substituteParams(content, params) {
|
|
|
33
43
|
.replace(/\{\{slug\}\}/g, params.slug || 'business')
|
|
34
44
|
.replace(/\{\{owner_email\}\}/g, params.owner_email || '')
|
|
35
45
|
.replace(/\{\{business_id\}\}/g, params.business_id || '')
|
|
36
|
-
.replace(/\{\{workspace_id\}\}/g, params.workspace_id || '')
|
|
46
|
+
.replace(/\{\{workspace_id\}\}/g, params.workspace_id || '')
|
|
47
|
+
.replace(/\{\{today\}\}/g, params.today || new Date().toISOString().slice(0, 10))
|
|
48
|
+
.replace(/\{\{workspace_template\}\}/g, params.workspace_template || 'business');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sync the canonical skill set from atris-cli/atris/skills/* into a
|
|
53
|
+
* workspace's atris/skills/* (plus ensure .claude/skills/ symlinks).
|
|
54
|
+
*
|
|
55
|
+
* Shared by business mode (via syncWorkspaceTemplate) and legacy/dev mode
|
|
56
|
+
* (via syncAtris). Single source of truth = atris-cli/atris/skills/.
|
|
57
|
+
*
|
|
58
|
+
* Returns the number of files updated (0 if everything was up to date).
|
|
59
|
+
*/
|
|
60
|
+
function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
61
|
+
const packageSkillsDir = path.join(__dirname, '..', 'atris', 'skills');
|
|
62
|
+
const userSkillsDir = path.join(targetAtrisDir, 'skills');
|
|
63
|
+
const claudeSkillsBaseDir = path.join(path.dirname(targetAtrisDir), '.claude', 'skills');
|
|
64
|
+
const verbose = opts.verbose !== false;
|
|
65
|
+
let updated = 0;
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(packageSkillsDir)) return 0;
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(userSkillsDir)) fs.mkdirSync(userSkillsDir, { recursive: true });
|
|
70
|
+
if (!fs.existsSync(claudeSkillsBaseDir)) fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f => {
|
|
73
|
+
try { return fs.statSync(path.join(packageSkillsDir, f)).isDirectory(); }
|
|
74
|
+
catch { return false; }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const skill of skillFolders) {
|
|
78
|
+
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
79
|
+
const destSkillDir = path.join(userSkillsDir, skill);
|
|
80
|
+
const symlinkPath = path.join(claudeSkillsBaseDir, skill);
|
|
81
|
+
|
|
82
|
+
const syncRecursive = (src, dest, skillName, basePath = '') => {
|
|
83
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
84
|
+
for (const entry of fs.readdirSync(src)) {
|
|
85
|
+
const srcPath = path.join(src, entry);
|
|
86
|
+
const destPath = path.join(dest, entry);
|
|
87
|
+
const relPath = basePath ? `${basePath}/${entry}` : entry;
|
|
88
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
89
|
+
syncRecursive(srcPath, destPath, skillName, relPath);
|
|
90
|
+
} else {
|
|
91
|
+
const srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
92
|
+
const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
|
|
93
|
+
if (srcContent !== destContent) {
|
|
94
|
+
fs.writeFileSync(destPath, srcContent);
|
|
95
|
+
if (entry.endsWith('.sh')) fs.chmodSync(destPath, 0o755);
|
|
96
|
+
if (verbose) console.log(`✓ Updated atris/skills/${skillName}/${relPath}`);
|
|
97
|
+
updated++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
syncRecursive(srcSkillDir, destSkillDir, skill);
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(symlinkPath)) {
|
|
106
|
+
const relativePath = path.relative(claudeSkillsBaseDir, destSkillDir);
|
|
107
|
+
try {
|
|
108
|
+
fs.symlinkSync(relativePath, symlinkPath);
|
|
109
|
+
if (verbose) console.log(`✓ Linked .claude/skills/${skill}`);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
fs.mkdirSync(symlinkPath, { recursive: true });
|
|
112
|
+
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
113
|
+
if (fs.existsSync(skillFile)) {
|
|
114
|
+
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
115
|
+
}
|
|
116
|
+
if (verbose) console.log(`✓ Copied .claude/skills/${skill} (symlink failed)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return updated;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveWorkspaceTemplate(templateName = 'business') {
|
|
125
|
+
const normalized = String(templateName || 'business').toLowerCase();
|
|
126
|
+
if (normalized === 'research-lab' || normalized === 'researchlab' || normalized === 'lab') {
|
|
127
|
+
return { key: 'research', ...WORKSPACE_TEMPLATES.research };
|
|
128
|
+
}
|
|
129
|
+
const template = WORKSPACE_TEMPLATES[normalized];
|
|
130
|
+
if (!template) return null;
|
|
131
|
+
return { key: normalized, ...template };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureWorkspaceStateFiles(targetRoot, params, options = {}) {
|
|
135
|
+
const dryRun = options.dryRun === true;
|
|
136
|
+
const metaDir = path.join(targetRoot, '.atris');
|
|
137
|
+
const stateDir = path.join(metaDir, 'state');
|
|
138
|
+
const created = [];
|
|
139
|
+
|
|
140
|
+
const files = [
|
|
141
|
+
{
|
|
142
|
+
relPath: '_sync.json',
|
|
143
|
+
content: `${JSON.stringify({
|
|
144
|
+
workspace_slug: params.slug || 'business',
|
|
145
|
+
business_id: params.business_id || '',
|
|
146
|
+
workspace_id: params.workspace_id || '',
|
|
147
|
+
workspace_template: params.workspace_template || 'business',
|
|
148
|
+
status: 'initialized-local',
|
|
149
|
+
updated_at: new Date().toISOString(),
|
|
150
|
+
source: 'workspace template bootstrap',
|
|
151
|
+
}, null, 2)}\n`,
|
|
152
|
+
},
|
|
153
|
+
{ relPath: 'events.jsonl', content: '' },
|
|
154
|
+
{ relPath: 'episodes.jsonl', content: '' },
|
|
155
|
+
{ relPath: 'scorecards.jsonl', content: '' },
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const fullPath = path.join(stateDir, file.relPath);
|
|
160
|
+
if (fs.existsSync(fullPath)) continue;
|
|
161
|
+
created.push(path.join('.atris', 'state', file.relPath));
|
|
162
|
+
if (!dryRun) {
|
|
163
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(fullPath, file.content);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return created;
|
|
37
169
|
}
|
|
38
170
|
|
|
39
171
|
/**
|
|
@@ -43,36 +175,44 @@ function _substituteParams(content, params) {
|
|
|
43
175
|
* Default: NEVER overwrites existing files (preserves customizations).
|
|
44
176
|
* --force: overwrites existing canonical files (bumps to latest).
|
|
45
177
|
*/
|
|
46
|
-
function
|
|
178
|
+
function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
|
|
179
|
+
const template = resolveWorkspaceTemplate(options.templateName || bizMeta.workspace_template || 'business');
|
|
180
|
+
if (!template) {
|
|
181
|
+
console.error(`✗ Unknown workspace template: ${options.templateName || bizMeta.workspace_template}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
47
185
|
const params = {
|
|
48
186
|
slug: bizMeta.slug || 'business',
|
|
49
187
|
name: bizMeta.name || bizMeta.slug || 'this business',
|
|
50
188
|
owner_email: bizMeta.owner_email || '',
|
|
51
189
|
business_id: bizMeta.business_id || '',
|
|
52
190
|
workspace_id: bizMeta.workspace_id || '',
|
|
191
|
+
today: new Date().toISOString().slice(0, 10),
|
|
192
|
+
workspace_template: template.key,
|
|
53
193
|
};
|
|
54
194
|
const force = options.force != null ? options.force : process.argv.includes('--force');
|
|
55
195
|
const dryRun = options.dryRun != null ? options.dryRun : process.argv.includes('--dry-run');
|
|
56
196
|
const targetAtrisDir = path.join(targetRoot, 'atris');
|
|
57
197
|
|
|
58
|
-
if (!fs.existsSync(
|
|
59
|
-
console.error(`✗
|
|
198
|
+
if (!fs.existsSync(template.dir)) {
|
|
199
|
+
console.error(`✗ Workspace template directory not found: ${template.dir}`);
|
|
60
200
|
console.error(' Your atris-cli installation may be incomplete.');
|
|
61
201
|
process.exit(1);
|
|
62
202
|
}
|
|
63
203
|
|
|
64
204
|
console.log('');
|
|
65
|
-
console.log(`Updating ${params.name} (${params.slug}) from
|
|
205
|
+
console.log(`Updating ${params.name} (${params.slug}) from ${template.label} templates...`);
|
|
66
206
|
console.log(` Target: ${targetAtrisDir}/`);
|
|
67
|
-
console.log(` Source: ${
|
|
207
|
+
console.log(` Source: ${template.dir}`);
|
|
68
208
|
console.log('');
|
|
69
209
|
|
|
70
|
-
const templateFiles = _walkTemplateDir(
|
|
210
|
+
const templateFiles = _walkTemplateDir(template.dir).sort();
|
|
71
211
|
let added = 0, updated = 0, skipped = 0, preserved = 0;
|
|
72
212
|
const addedList = [], updatedList = [], preservedList = [];
|
|
73
213
|
|
|
74
214
|
for (const relPath of templateFiles) {
|
|
75
|
-
const templatePath = path.join(
|
|
215
|
+
const templatePath = path.join(template.dir, relPath);
|
|
76
216
|
const targetPath = path.join(targetAtrisDir, relPath);
|
|
77
217
|
let templateContent;
|
|
78
218
|
try { templateContent = fs.readFileSync(templatePath, 'utf-8'); } catch { continue; }
|
|
@@ -97,10 +237,22 @@ function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
|
|
|
97
237
|
}
|
|
98
238
|
}
|
|
99
239
|
|
|
240
|
+
const stateAddedList = ensureWorkspaceStateFiles(targetRoot, params, { dryRun });
|
|
241
|
+
|
|
242
|
+
// Skills: sync the canonical skill set from atris-cli package into the
|
|
243
|
+
// customer workspace. Business-starter template ships skill infra (README,
|
|
244
|
+
// folders) but skill files live in atris-cli/atris/skills/ — single source
|
|
245
|
+
// of truth. Any new skill (e.g. AEO) auto-propagates to every customer.
|
|
246
|
+
let skillsUpdated = 0;
|
|
247
|
+
if (!dryRun) {
|
|
248
|
+
skillsUpdated = syncPackageSkills(targetAtrisDir, { verbose: false });
|
|
249
|
+
}
|
|
250
|
+
|
|
100
251
|
console.log(` Added: ${added}`);
|
|
101
252
|
console.log(` Updated: ${updated} ${force ? '' : '(--force to enable)'}`);
|
|
102
253
|
console.log(` Preserved: ${preserved} (existing customizations kept)`);
|
|
103
254
|
console.log(` Skipped: ${skipped} (already match template)`);
|
|
255
|
+
console.log(` Skills: ${skillsUpdated} updated from atris-cli/atris/skills/`);
|
|
104
256
|
console.log('');
|
|
105
257
|
|
|
106
258
|
if (addedList.length > 0) {
|
|
@@ -115,10 +267,15 @@ function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
|
|
|
115
267
|
if (updatedList.length > 15) console.log(` ... +${updatedList.length - 15} more`);
|
|
116
268
|
console.log('');
|
|
117
269
|
}
|
|
270
|
+
if (stateAddedList.length > 0) {
|
|
271
|
+
console.log(' State files:');
|
|
272
|
+
stateAddedList.forEach(p => console.log(` + ${p}`));
|
|
273
|
+
console.log('');
|
|
274
|
+
}
|
|
118
275
|
|
|
119
276
|
if (dryRun) {
|
|
120
277
|
console.log(' (--dry-run, no changes made)');
|
|
121
|
-
} else if (added === 0 && updated === 0) {
|
|
278
|
+
} else if (added === 0 && updated === 0 && stateAddedList.length === 0) {
|
|
122
279
|
ensureWikiScaffold(targetRoot);
|
|
123
280
|
console.log(' ✓ Already up to date');
|
|
124
281
|
} else {
|
|
@@ -127,13 +284,22 @@ function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
|
|
|
127
284
|
}
|
|
128
285
|
}
|
|
129
286
|
|
|
287
|
+
function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
|
|
288
|
+
return syncWorkspaceTemplate(targetRoot, bizMeta, {
|
|
289
|
+
...options,
|
|
290
|
+
templateName: options.templateName || bizMeta.workspace_template || 'business',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
130
294
|
function syncAtris() {
|
|
131
295
|
// Business mode detection: if .atris/business.json exists, use canonical templates
|
|
132
296
|
const bizFile = path.join(process.cwd(), '.atris', 'business.json');
|
|
133
297
|
if (fs.existsSync(bizFile)) {
|
|
134
298
|
try {
|
|
135
299
|
const bizMeta = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
|
|
136
|
-
return
|
|
300
|
+
return syncWorkspaceTemplate(process.cwd(), bizMeta, {
|
|
301
|
+
templateName: bizMeta.workspace_template || 'business',
|
|
302
|
+
});
|
|
137
303
|
} catch (e) {
|
|
138
304
|
console.error(`✗ Failed to read .atris/business.json: ${e.message}`);
|
|
139
305
|
process.exit(1);
|
|
@@ -241,80 +407,8 @@ function syncAtris() {
|
|
|
241
407
|
console.log('✓ Migrated TASK_CONTEXTS.md to TODO.md');
|
|
242
408
|
}
|
|
243
409
|
|
|
244
|
-
// Sync all skills from package to user's project
|
|
245
|
-
|
|
246
|
-
const userSkillsDir = path.join(targetDir, 'skills');
|
|
247
|
-
const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
|
|
248
|
-
|
|
249
|
-
if (fs.existsSync(packageSkillsDir)) {
|
|
250
|
-
// Ensure directories exist
|
|
251
|
-
if (!fs.existsSync(userSkillsDir)) {
|
|
252
|
-
fs.mkdirSync(userSkillsDir, { recursive: true });
|
|
253
|
-
}
|
|
254
|
-
if (!fs.existsSync(claudeSkillsBaseDir)) {
|
|
255
|
-
fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Get all skill folders from package
|
|
259
|
-
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f => {
|
|
260
|
-
const skillPath = path.join(packageSkillsDir, f);
|
|
261
|
-
return fs.statSync(skillPath).isDirectory();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
for (const skill of skillFolders) {
|
|
265
|
-
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
266
|
-
const destSkillDir = path.join(userSkillsDir, skill);
|
|
267
|
-
const symlinkPath = path.join(claudeSkillsBaseDir, skill);
|
|
268
|
-
|
|
269
|
-
// Recursive sync function for skills (handles subdirs like hooks/)
|
|
270
|
-
const syncRecursive = (src, dest, skillName, basePath = '') => {
|
|
271
|
-
if (!fs.existsSync(dest)) {
|
|
272
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
273
|
-
}
|
|
274
|
-
const entries = fs.readdirSync(src);
|
|
275
|
-
for (const entry of entries) {
|
|
276
|
-
const srcPath = path.join(src, entry);
|
|
277
|
-
const destPath = path.join(dest, entry);
|
|
278
|
-
const relPath = basePath ? `${basePath}/${entry}` : entry;
|
|
279
|
-
|
|
280
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
281
|
-
syncRecursive(srcPath, destPath, skillName, relPath);
|
|
282
|
-
} else {
|
|
283
|
-
const srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
284
|
-
const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
|
|
285
|
-
if (srcContent !== destContent) {
|
|
286
|
-
fs.writeFileSync(destPath, srcContent);
|
|
287
|
-
// Preserve executable permission for shell scripts
|
|
288
|
-
if (entry.endsWith('.sh')) {
|
|
289
|
-
fs.chmodSync(destPath, 0o755);
|
|
290
|
-
}
|
|
291
|
-
console.log(`✓ Updated atris/skills/${skillName}/${relPath}`);
|
|
292
|
-
updated++;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
syncRecursive(srcSkillDir, destSkillDir, skill);
|
|
299
|
-
|
|
300
|
-
// Create symlink if doesn't exist
|
|
301
|
-
if (!fs.existsSync(symlinkPath)) {
|
|
302
|
-
const relativePath = path.join('..', '..', 'atris', 'skills', skill);
|
|
303
|
-
try {
|
|
304
|
-
fs.symlinkSync(relativePath, symlinkPath);
|
|
305
|
-
console.log(`✓ Linked .claude/skills/${skill}`);
|
|
306
|
-
} catch (e) {
|
|
307
|
-
// Fallback: copy instead of symlink
|
|
308
|
-
fs.mkdirSync(symlinkPath, { recursive: true });
|
|
309
|
-
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
310
|
-
if (fs.existsSync(skillFile)) {
|
|
311
|
-
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
312
|
-
}
|
|
313
|
-
console.log(`✓ Copied .claude/skills/${skill} (symlink failed)`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
410
|
+
// Sync all skills from package to user's project via shared helper.
|
|
411
|
+
updated += syncPackageSkills(targetDir, { verbose: true });
|
|
318
412
|
|
|
319
413
|
// Update .claude/skills/atris/SKILL.md (legacy - now handled above, keeping for compatibility)
|
|
320
414
|
const claudeSkillsDir = path.join(process.cwd(), '.claude', 'skills', 'atris');
|
|
@@ -571,4 +665,195 @@ function syncSkills({ silent = false } = {}) {
|
|
|
571
665
|
return updated;
|
|
572
666
|
}
|
|
573
667
|
|
|
574
|
-
|
|
668
|
+
/**
|
|
669
|
+
* Discover atris-managed projects under a root directory.
|
|
670
|
+
* A project is any directory whose immediate child `atris/` folder contains `atris.md`.
|
|
671
|
+
* Skips noise: node_modules, .git, .claude, dist, build, _archive, worktrees.
|
|
672
|
+
*/
|
|
673
|
+
function _findAtrisProjects(rootDir, maxDepth = 8) {
|
|
674
|
+
const skip = new Set([
|
|
675
|
+
'node_modules', '.git', '.claude', '.next', 'dist', 'build',
|
|
676
|
+
'_archive', 'worktrees', '.codex', '.venv', 'venv', '__pycache__',
|
|
677
|
+
]);
|
|
678
|
+
const found = [];
|
|
679
|
+
function walk(dir, depth) {
|
|
680
|
+
if (depth > maxDepth) return;
|
|
681
|
+
const atrisMd = path.join(dir, 'atris', 'atris.md');
|
|
682
|
+
if (fs.existsSync(atrisMd)) found.push(dir);
|
|
683
|
+
let entries;
|
|
684
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
685
|
+
catch { return; }
|
|
686
|
+
for (const e of entries) {
|
|
687
|
+
if (!e.isDirectory()) continue;
|
|
688
|
+
if (skip.has(e.name)) continue;
|
|
689
|
+
if (e.name.startsWith('.')) continue;
|
|
690
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
walk(path.resolve(rootDir), 0);
|
|
694
|
+
return found;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Sync canonical atris.md (and core docs) across every atris project under cwd.
|
|
699
|
+
* Walks the subtree, finds every `atris/atris.md`, copies the package source.
|
|
700
|
+
*
|
|
701
|
+
* Flags: --dry-run (preview only), --yes/--force (skip confirm).
|
|
702
|
+
* Skips business workspaces (they use syncWorkspaceTemplate via syncAtris).
|
|
703
|
+
*/
|
|
704
|
+
// Canonical files shipped from the package root. Must match syncAtris's filesToSync.
|
|
705
|
+
const SYNC_ALL_FILES = [
|
|
706
|
+
{ source: 'atris.md', target: 'atris.md' },
|
|
707
|
+
{ source: 'atris/atrisDev.md', target: 'atrisDev.md' },
|
|
708
|
+
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
709
|
+
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
710
|
+
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Pure function: build the sync plan for every atris project under root.
|
|
715
|
+
* No console output, no writes. Returns { projects, plan } for inspection.
|
|
716
|
+
* Plan entries: { projectRoot, isBusiness, isCustomized, changes }.
|
|
717
|
+
*
|
|
718
|
+
* Exported for tests — the production syncAtrisAll wraps this.
|
|
719
|
+
*/
|
|
720
|
+
function buildSyncAllPlan({ root, pkgRoot, filesToSync = SYNC_ALL_FILES } = {}) {
|
|
721
|
+
const projects = _findAtrisProjects(root);
|
|
722
|
+
const plan = [];
|
|
723
|
+
for (const projectRoot of projects) {
|
|
724
|
+
const atrisDir = path.join(projectRoot, 'atris');
|
|
725
|
+
const bizFile = path.join(projectRoot, '.atris', 'business.json');
|
|
726
|
+
const isSelf = path.resolve(projectRoot) === path.resolve(pkgRoot);
|
|
727
|
+
const isInBusinessDir = projectRoot.split(path.sep).includes('atris-business');
|
|
728
|
+
let isBusiness = isInBusinessDir || (fs.existsSync(bizFile) && !isSelf);
|
|
729
|
+
if (isBusiness && !isInBusinessDir) {
|
|
730
|
+
try {
|
|
731
|
+
const head = fs.readFileSync(path.join(atrisDir, 'atris.md'), 'utf8').slice(0, 300);
|
|
732
|
+
if (!/^#\s+Atris Boot Protocol/i.test(head)) isBusiness = false;
|
|
733
|
+
} catch {}
|
|
734
|
+
}
|
|
735
|
+
let isCustomized = false;
|
|
736
|
+
if (!isSelf && !isBusiness) {
|
|
737
|
+
try {
|
|
738
|
+
const head = fs.readFileSync(path.join(atrisDir, 'atris.md'), 'utf8').slice(0, 500);
|
|
739
|
+
const isNewCanonical = /^#\s+atris\s*\n\nAtris exists because/m.test(head);
|
|
740
|
+
const isOldGeneric = /^#\s+atris\.md\s*\n\n>\s+Drop this file anywhere/m.test(head);
|
|
741
|
+
if (!isNewCanonical && !isOldGeneric) isCustomized = true;
|
|
742
|
+
} catch {}
|
|
743
|
+
}
|
|
744
|
+
const changes = [];
|
|
745
|
+
for (const { source, target } of filesToSync) {
|
|
746
|
+
const sourceFile = path.join(pkgRoot, source);
|
|
747
|
+
const targetFile = path.join(atrisDir, target);
|
|
748
|
+
if (!fs.existsSync(sourceFile)) continue;
|
|
749
|
+
const newContent = fs.readFileSync(sourceFile, 'utf8');
|
|
750
|
+
const currentContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, 'utf8') : '';
|
|
751
|
+
if (currentContent !== newContent) changes.push(target);
|
|
752
|
+
}
|
|
753
|
+
plan.push({ projectRoot, isBusiness, isCustomized, changes });
|
|
754
|
+
}
|
|
755
|
+
return { projects, plan };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function syncAtrisAll({ dryRun = false, force = false } = {}) {
|
|
759
|
+
const root = process.cwd();
|
|
760
|
+
const pkgRoot = path.join(__dirname, '..');
|
|
761
|
+
|
|
762
|
+
console.log('');
|
|
763
|
+
console.log(`Scanning ${root} for atris projects...`);
|
|
764
|
+
|
|
765
|
+
const filesToSync = SYNC_ALL_FILES;
|
|
766
|
+
const { projects, plan: initialPlan } = buildSyncAllPlan({ root, pkgRoot, filesToSync });
|
|
767
|
+
|
|
768
|
+
if (projects.length === 0) {
|
|
769
|
+
console.log('No atris projects found under current directory.');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const plan = initialPlan;
|
|
774
|
+
|
|
775
|
+
// Report.
|
|
776
|
+
console.log(`Found ${projects.length} project(s).`);
|
|
777
|
+
console.log('');
|
|
778
|
+
let wouldUpdate = 0, unchanged = 0, skipped = 0;
|
|
779
|
+
for (const p of plan) {
|
|
780
|
+
const rel = path.relative(root, p.projectRoot) || '.';
|
|
781
|
+
if (p.isBusiness) {
|
|
782
|
+
console.log(` ⏭ ${rel} (business workspace — run "atris update" in that dir)`);
|
|
783
|
+
skipped++;
|
|
784
|
+
} else if (p.isCustomized) {
|
|
785
|
+
console.log(` ⏭ ${rel} (customized atris.md — review manually)`);
|
|
786
|
+
skipped++;
|
|
787
|
+
} else if (p.changes.length === 0) {
|
|
788
|
+
console.log(` · ${rel} (up to date)`);
|
|
789
|
+
unchanged++;
|
|
790
|
+
} else {
|
|
791
|
+
console.log(` → ${rel} — ${p.changes.length} file(s): ${p.changes.join(', ')}`);
|
|
792
|
+
wouldUpdate++;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
console.log('');
|
|
796
|
+
|
|
797
|
+
if (dryRun) {
|
|
798
|
+
console.log(`Dry run: ${wouldUpdate} project(s) would update, ${unchanged} unchanged, ${skipped} skipped.`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (wouldUpdate === 0) {
|
|
803
|
+
console.log('Nothing to sync.');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Confirm unless forced.
|
|
808
|
+
if (!force) {
|
|
809
|
+
const readline = require('readline');
|
|
810
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
811
|
+
return new Promise((resolve) => {
|
|
812
|
+
rl.question(`Sync ${wouldUpdate} project(s)? (y/N) `, (answer) => {
|
|
813
|
+
rl.close();
|
|
814
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
815
|
+
console.log('Cancelled.');
|
|
816
|
+
resolve();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
_executeSyncAll(plan, pkgRoot, filesToSync, root);
|
|
820
|
+
resolve();
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
_executeSyncAll(plan, pkgRoot, filesToSync, root);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function _executeSyncAll(plan, pkgRoot, filesToSync, root) {
|
|
829
|
+
let updated = 0;
|
|
830
|
+
for (const p of plan) {
|
|
831
|
+
if (p.isBusiness || p.isCustomized || p.changes.length === 0) continue;
|
|
832
|
+
const atrisDir = path.join(p.projectRoot, 'atris');
|
|
833
|
+
for (const { source, target } of filesToSync) {
|
|
834
|
+
const sourceFile = path.join(pkgRoot, source);
|
|
835
|
+
const targetFile = path.join(atrisDir, target);
|
|
836
|
+
if (!fs.existsSync(sourceFile)) continue;
|
|
837
|
+
const newContent = fs.readFileSync(sourceFile, 'utf8');
|
|
838
|
+
const currentContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, 'utf8') : '';
|
|
839
|
+
if (currentContent === newContent) continue;
|
|
840
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
841
|
+
fs.copyFileSync(sourceFile, targetFile);
|
|
842
|
+
}
|
|
843
|
+
updated++;
|
|
844
|
+
}
|
|
845
|
+
console.log('');
|
|
846
|
+
console.log(`✓ Synced ${updated} project(s).`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
module.exports = {
|
|
850
|
+
syncAtris,
|
|
851
|
+
syncAtrisAll,
|
|
852
|
+
buildSyncAllPlan,
|
|
853
|
+
SYNC_ALL_FILES,
|
|
854
|
+
syncSkills,
|
|
855
|
+
syncBusinessCanonical,
|
|
856
|
+
syncWorkspaceTemplate,
|
|
857
|
+
resolveWorkspaceTemplate,
|
|
858
|
+
ensureWorkspaceStateFiles,
|
|
859
|
+
};
|
package/commands/verify.js
CHANGED
|
@@ -444,11 +444,11 @@ function verifyChange(cwd, change) {
|
|
|
444
444
|
};
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
//
|
|
447
|
+
// No specific check possible — refuse to auto-pass
|
|
448
448
|
return {
|
|
449
|
-
pass:
|
|
449
|
+
pass: false,
|
|
450
450
|
description: change.description,
|
|
451
|
-
details: '
|
|
451
|
+
details: 'No verifiable check for this change type. Add an explicit verify command.'
|
|
452
452
|
};
|
|
453
453
|
}
|
|
454
454
|
|
|
@@ -479,6 +479,55 @@ function checkMapForFiles(atrisDir, changes) {
|
|
|
479
479
|
return { documented: documented >= fileChanges.length / 2 };
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* atris verify <slug> --section <name>
|
|
484
|
+
*
|
|
485
|
+
* Extract the first fenced bash block under "## <name>" in
|
|
486
|
+
* atris/features/<slug>/validate.md and execute it. Returns the exit code
|
|
487
|
+
* from bash. Used as the machine-checkable Verify command in TODO.md tasks.
|
|
488
|
+
*
|
|
489
|
+
* Contract (per atris.md): the rubric must be read-only, deterministic, and
|
|
490
|
+
* reference only the working tree. The command fails loudly when the rubric
|
|
491
|
+
* or section is missing — that prevents silent "trivial Verify" regressions.
|
|
492
|
+
*/
|
|
493
|
+
function verifyRubric(slug, section, opts = {}) {
|
|
494
|
+
const cwd = opts.cwd || process.cwd();
|
|
495
|
+
if (!slug || !section) {
|
|
496
|
+
console.error('Usage: atris verify <feature-slug> --section <name>');
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
const validateFile = path.join(cwd, 'atris', 'features', slug, 'validate.md');
|
|
500
|
+
if (!fs.existsSync(validateFile)) {
|
|
501
|
+
console.error(`✗ No rubric at ${path.relative(cwd, validateFile)}`);
|
|
502
|
+
return 2;
|
|
503
|
+
}
|
|
504
|
+
const content = fs.readFileSync(validateFile, 'utf8');
|
|
505
|
+
// Match "## <section>" (case-insensitive, anchored), skipping optional
|
|
506
|
+
// prose until the first ```bash or ```sh fence. Extract until the closing ```.
|
|
507
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
508
|
+
const pattern = new RegExp(
|
|
509
|
+
`^##\\s+${escaped}\\s*$[\\s\\S]*?\\n\`\`\`(?:bash|sh)?\\s*\\n([\\s\\S]*?)\\n\`\`\``,
|
|
510
|
+
'mi'
|
|
511
|
+
);
|
|
512
|
+
const match = content.match(pattern);
|
|
513
|
+
if (!match) {
|
|
514
|
+
console.error(`✗ No fenced bash block under "## ${section}" in ${path.relative(cwd, validateFile)}`);
|
|
515
|
+
return 2;
|
|
516
|
+
}
|
|
517
|
+
const script = match[1];
|
|
518
|
+
const os = require('os');
|
|
519
|
+
const tmpFile = path.join(os.tmpdir(), `atris-verify-${Date.now()}-${Math.floor(Math.random() * 1e6)}.sh`);
|
|
520
|
+
fs.writeFileSync(tmpFile, `#!/usr/bin/env bash\nset -e\n${script}\n`);
|
|
521
|
+
fs.chmodSync(tmpFile, 0o755);
|
|
522
|
+
try {
|
|
523
|
+
const proc = spawnSync('bash', [tmpFile], { cwd, stdio: opts.silent ? 'pipe' : 'inherit' });
|
|
524
|
+
return typeof proc.status === 'number' ? proc.status : 1;
|
|
525
|
+
} finally {
|
|
526
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
482
530
|
module.exports = {
|
|
483
|
-
verifyAtris
|
|
531
|
+
verifyAtris,
|
|
532
|
+
verifyRubric
|
|
484
533
|
};
|