atris 3.15.13 → 3.15.22
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/AGENTS.md +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
package/commands/pull.js
CHANGED
|
@@ -7,7 +7,7 @@ const { loadConfig } = require('../utils/config');
|
|
|
7
7
|
const { getLogPath } = require('../lib/file-ops');
|
|
8
8
|
const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
|
|
9
9
|
const { loadBusinesses, businessMatchesSlug } = require('./business');
|
|
10
|
-
const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare } = require('../lib/manifest');
|
|
10
|
+
const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
|
|
11
11
|
const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
12
12
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
13
13
|
const { resolveSafeOutputDir } = require('../lib/workspace-safety');
|
|
@@ -37,6 +37,34 @@ function isBusinessWorkspaceRoot(dir) {
|
|
|
37
37
|
return fs.existsSync(path.join(dir, '.atris', 'business.json')) && fs.existsSync(path.join(dir, 'atris'));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function normalizePullFilePath(filePath) {
|
|
41
|
+
if (!filePath) return null;
|
|
42
|
+
return `/${String(filePath).replace(/^\/+/, '')}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mergeSmartPullFiles(hashFiles = [], batchFiles = [], changedPaths = []) {
|
|
46
|
+
const contentMap = {};
|
|
47
|
+
for (const f of batchFiles) {
|
|
48
|
+
const normalizedPath = normalizePullFilePath(f && f.path);
|
|
49
|
+
if (normalizedPath) contentMap[normalizedPath] = { ...f, path: normalizedPath };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const missingContent = changedPaths.filter((p) => {
|
|
53
|
+
const item = contentMap[p];
|
|
54
|
+
return !item || typeof item.content !== 'string';
|
|
55
|
+
});
|
|
56
|
+
if (missingContent.length > 0) {
|
|
57
|
+
return { ok: false, missingContent };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const files = hashFiles.map((f) => {
|
|
61
|
+
const normalizedPath = normalizePullFilePath(f && f.path);
|
|
62
|
+
const normalizedHashOnly = normalizedPath ? { ...f, path: normalizedPath } : f;
|
|
63
|
+
return contentMap[normalizedPath] || normalizedHashOnly;
|
|
64
|
+
});
|
|
65
|
+
return { ok: true, files };
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
function syncTimestamp() {
|
|
41
69
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
42
70
|
}
|
|
@@ -75,7 +103,7 @@ function buildPullConflictReviewPacket(outputDir, conflictChanges, remoteContent
|
|
|
75
103
|
async function pullAtris() {
|
|
76
104
|
let arg = process.argv[3];
|
|
77
105
|
|
|
78
|
-
if (arg === '--help') {
|
|
106
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
79
107
|
console.log('Usage: atris pull [business] [--into <path>] [--only <prefix>] [--keep-local] [--timeout <seconds>] [--dry-run] [--no-manifest]');
|
|
80
108
|
console.log('');
|
|
81
109
|
console.log(' Pull is force-overwrite by default. Cloud is the source of truth.');
|
|
@@ -221,8 +249,8 @@ async function pullBusiness(slug) {
|
|
|
221
249
|
// correct workspace for THIS business — i.e. it has a `.atris/business.json`
|
|
222
250
|
// whose slug matches `slug`. Any other signal (a stray `atris/` folder, a
|
|
223
251
|
// business.json for a different business, etc.) is NOT enough: pulling
|
|
224
|
-
// atris-labs
|
|
225
|
-
// one directory and write
|
|
252
|
+
// atris-labs on top of another business workspace would mix two businesses into
|
|
253
|
+
// one directory and write one manifest over the other (or vice
|
|
226
254
|
// versa), causing the next sync to do strange things.
|
|
227
255
|
//
|
|
228
256
|
// Fallback: create a fresh ./{slug}/ subdir. Always safe — even if cwd is
|
|
@@ -253,9 +281,9 @@ async function pullBusiness(slug) {
|
|
|
253
281
|
// for a stray cwd to cause atris to delete user files.
|
|
254
282
|
({ dir: outputDir } = resolveSafeOutputDir(outputDir, { slug, op: 'pull into' }));
|
|
255
283
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
284
|
+
// Pull the whole business workspace by default. Older versions silently
|
|
285
|
+
// narrowed bound workspace pulls to atris/, which made staging snapshots and
|
|
286
|
+
// the real workspace diverge while both reported "synced".
|
|
259
287
|
|
|
260
288
|
// Resolve business ID — always refresh from API to avoid stale workspace_id
|
|
261
289
|
let businessId, workspaceId, businessName, resolvedSlug;
|
|
@@ -379,13 +407,16 @@ async function pullBusiness(slug) {
|
|
|
379
407
|
// Diff against manifest to find changed files
|
|
380
408
|
const remoteHashes = {};
|
|
381
409
|
for (const f of hashResult.data.files) {
|
|
382
|
-
|
|
410
|
+
const normalizedPath = normalizePullFilePath(f.path);
|
|
411
|
+
if (isIgnoredSyncPath(normalizedPath)) continue;
|
|
412
|
+
if (normalizedPath && f.hash) remoteHashes[normalizedPath] = f.hash;
|
|
383
413
|
}
|
|
384
414
|
const changedPaths = [];
|
|
385
415
|
const manifestFiles = manifest.files || {};
|
|
386
416
|
for (const [p, hash] of Object.entries(remoteHashes)) {
|
|
387
417
|
const prev = manifestFiles[p];
|
|
388
|
-
|
|
418
|
+
const missingLocally = !localFilesBeforePull[p];
|
|
419
|
+
if (!prev || prev.hash !== hash || missingLocally) changedPaths.push(p);
|
|
389
420
|
}
|
|
390
421
|
|
|
391
422
|
if (changedPaths.length === 0) {
|
|
@@ -419,17 +450,16 @@ async function pullBusiness(slug) {
|
|
|
419
450
|
|
|
420
451
|
if (batchResult.ok && batchResult.data && batchResult.data.files) {
|
|
421
452
|
process.stdout.write(`\r Fetched ${batchResult.data.files.length} files in ${phase2Sec}s.${' '.repeat(10)}\n`);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
453
|
+
const merged = mergeSmartPullFiles(hashResult.data.files, batchResult.data.files, changedPaths);
|
|
454
|
+
if (merged.ok) {
|
|
455
|
+
result = { ok: true, data: { files: merged.files } };
|
|
456
|
+
} else {
|
|
457
|
+
process.stdout.write(`\r Batch missing ${merged.missingContent.length} file(s), fetching full snapshot...${' '.repeat(10)}\n`);
|
|
458
|
+
const contentUrl = `/business/${businessId}/workspaces/${workspaceId}/snapshot?include_content=true${pathsParam}`;
|
|
459
|
+
result = await apiRequestJson(contentUrl, { method: 'GET', token: creds.token, timeoutMs });
|
|
460
|
+
const fullSec = Math.floor((Date.now() - startPhase2) / 1000);
|
|
461
|
+
process.stdout.write(`\r Fetched in ${fullSec}s.${' '.repeat(20)}\n`);
|
|
426
462
|
}
|
|
427
|
-
// Build merged file list: all hash-only entries + inject content for changed ones
|
|
428
|
-
const mergedFiles = hashResult.data.files.map(f => {
|
|
429
|
-
const withContent = contentMap[f.path];
|
|
430
|
-
return withContent || f;
|
|
431
|
-
});
|
|
432
|
-
result = { ok: true, data: { files: mergedFiles } };
|
|
433
463
|
} else {
|
|
434
464
|
// Batch not available — fall back to full snapshot
|
|
435
465
|
process.stdout.write(`\r Batch unavailable, fetching full snapshot...${' '.repeat(10)}\n`);
|
|
@@ -544,6 +574,9 @@ async function pullBusiness(slug) {
|
|
|
544
574
|
const crypto = require('crypto');
|
|
545
575
|
for (const file of files) {
|
|
546
576
|
if (!file.path || file.binary) continue;
|
|
577
|
+
const normalizedPath = normalizePullFilePath(file.path);
|
|
578
|
+
if (!normalizedPath) continue;
|
|
579
|
+
if (isIgnoredSyncPath(normalizedPath)) continue;
|
|
547
580
|
// An empty string IS valid content (a real, zero-byte file). The earlier
|
|
548
581
|
// version excluded `content === ''` from the hasContent path, which made
|
|
549
582
|
// empty files masquerade as hash-only entries; they'd then be recorded in
|
|
@@ -555,11 +588,11 @@ async function pullBusiness(slug) {
|
|
|
555
588
|
if (hasContent) {
|
|
556
589
|
// Full content available — hash from raw bytes (matches computeLocalHashes)
|
|
557
590
|
const rawBytes = Buffer.from(file.content, 'utf-8');
|
|
558
|
-
remoteFiles[
|
|
559
|
-
remoteContent[
|
|
591
|
+
remoteFiles[normalizedPath] = { hash: crypto.createHash('sha256').update(rawBytes).digest('hex'), size: rawBytes.length };
|
|
592
|
+
remoteContent[normalizedPath] = file.content;
|
|
560
593
|
} else if (file.hash) {
|
|
561
594
|
// Hash-only entry from smart pull — trust the cloud-reported hash
|
|
562
|
-
remoteFiles[
|
|
595
|
+
remoteFiles[normalizedPath] = { hash: file.hash, size: file.size || 0 };
|
|
563
596
|
}
|
|
564
597
|
}
|
|
565
598
|
|
|
@@ -598,8 +631,12 @@ async function pullBusiness(slug) {
|
|
|
598
631
|
|
|
599
632
|
if (dryRun) {
|
|
600
633
|
console.log('');
|
|
601
|
-
for (const p of [...diff.toPull, ...diff.newRemote]) {
|
|
602
|
-
const label = diff.newRemote.includes(p)
|
|
634
|
+
for (const p of [...diff.toPull, ...diff.newRemote, ...diff.deletedLocal]) {
|
|
635
|
+
const label = diff.newRemote.includes(p)
|
|
636
|
+
? 'new on computer'
|
|
637
|
+
: diff.deletedLocal.includes(p)
|
|
638
|
+
? 'missing locally, restored from computer'
|
|
639
|
+
: 'updated on computer';
|
|
603
640
|
const icon = diff.newRemote.includes(p) ? '+' : '\u2193';
|
|
604
641
|
console.log(` ${icon} ${p.replace(/^\//, '')} ${label} (dry run)`);
|
|
605
642
|
}
|
|
@@ -611,7 +648,7 @@ async function pullBusiness(slug) {
|
|
|
611
648
|
}
|
|
612
649
|
|
|
613
650
|
const parts = [];
|
|
614
|
-
const pullCount = diff.toPull.length + diff.newRemote.length;
|
|
651
|
+
const pullCount = diff.toPull.length + diff.newRemote.length + diff.deletedLocal.length;
|
|
615
652
|
if (pullCount > 0) parts.push(`${pullCount} would be pulled`);
|
|
616
653
|
if (diff.deletedRemote.length > 0) parts.push(`${diff.deletedRemote.length} local file${diff.deletedRemote.length === 1 ? '' : 's'} would be removed`);
|
|
617
654
|
if (diff.unchanged.length > 0) parts.push(`${diff.unchanged.length} unchanged`);
|
|
@@ -632,13 +669,17 @@ async function pullBusiness(slug) {
|
|
|
632
669
|
console.log('');
|
|
633
670
|
|
|
634
671
|
// Pull files that changed remotely (and we didn't change locally)
|
|
635
|
-
for (const p of [...diff.toPull, ...diff.newRemote]) {
|
|
672
|
+
for (const p of [...diff.toPull, ...diff.newRemote, ...diff.deletedLocal]) {
|
|
636
673
|
const content = remoteContent[p];
|
|
637
674
|
if (!content && content !== '') continue;
|
|
638
675
|
const localPath = path.join(outputDir, p.replace(/^\//, ''));
|
|
639
676
|
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
640
677
|
fs.writeFileSync(localPath, content);
|
|
641
|
-
const label = diff.newRemote.includes(p)
|
|
678
|
+
const label = diff.newRemote.includes(p)
|
|
679
|
+
? 'new on computer'
|
|
680
|
+
: diff.deletedLocal.includes(p)
|
|
681
|
+
? 'missing locally, restored from computer'
|
|
682
|
+
: 'updated on computer';
|
|
642
683
|
const icon = diff.newRemote.includes(p) ? '+' : '\u2193';
|
|
643
684
|
console.log(` ${icon} ${p.replace(/^\//, '')} ${label}`);
|
|
644
685
|
pulled++;
|
|
@@ -736,7 +777,9 @@ async function pullBusiness(slug) {
|
|
|
736
777
|
// If there's no prior manifest (first pull), there's nothing to sweep —
|
|
737
778
|
// threeWayCompare already handled newRemote/conflicts/newLocal, and we
|
|
738
779
|
// have no basis for claiming ownership of any local-only file.
|
|
739
|
-
const managedPaths = manifest && manifest.files
|
|
780
|
+
const managedPaths = manifest && manifest.files
|
|
781
|
+
? Object.keys(manifest.files).filter((p) => !isIgnoredSyncPath(p))
|
|
782
|
+
: [];
|
|
740
783
|
const sweepCandidates = managedPaths.filter(isInScope).filter((p) => localFiles[p]);
|
|
741
784
|
const inScopeLocal = Object.keys(localFiles).filter(isInScope);
|
|
742
785
|
|
|
@@ -1059,4 +1102,4 @@ async function pullMemberJournal(token, agentId, memberName, memberDir) {
|
|
|
1059
1102
|
return synced;
|
|
1060
1103
|
}
|
|
1061
1104
|
|
|
1062
|
-
module.exports = { buildPullConflictReviewPacket, pullAtris };
|
|
1105
|
+
module.exports = { buildPullConflictReviewPacket, mergeSmartPullFiles, normalizePullFilePath, pullAtris };
|
package/commands/push.js
CHANGED
|
@@ -4,7 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
|
|
7
|
-
const { loadManifest, saveManifest, buildManifest, computeLocalHashes } = require('../lib/manifest');
|
|
7
|
+
const { loadManifest, saveManifest, buildManifest, computeLocalHashes, isIgnoredSyncPath, filterSyncFiles } = require('../lib/manifest');
|
|
8
8
|
const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
9
9
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
10
10
|
const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
|
|
@@ -59,6 +59,7 @@ function buildPushChangePlan({
|
|
|
59
59
|
baseFiles = {},
|
|
60
60
|
onlyPrefixes = null,
|
|
61
61
|
readFileContent,
|
|
62
|
+
isLocalFilePresent,
|
|
62
63
|
} = {}) {
|
|
63
64
|
const filesToPush = [];
|
|
64
65
|
const deletedPaths = [];
|
|
@@ -79,7 +80,9 @@ function buildPushChangePlan({
|
|
|
79
80
|
for (const filePath of Object.keys(baseFiles)) {
|
|
80
81
|
if (!pathInScope(filePath, onlyPrefixes)) continue;
|
|
81
82
|
if (basenameOfManifestPath(filePath).startsWith('.')) continue;
|
|
83
|
+
if (isIgnoredSyncPath(filePath)) continue;
|
|
82
84
|
if (!localFiles[filePath]) {
|
|
85
|
+
if (isLocalFilePresent && isLocalFilePresent(filePath)) continue;
|
|
83
86
|
deletedPaths.push(filePath);
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -104,11 +107,153 @@ function isMassDeletePlan({ deletedPaths = [], filesToPush = [], unchangedCount
|
|
|
104
107
|
return deleteCount >= 10 && deleteCount > survivingCount;
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
function cleanManifestPath(filePath) {
|
|
111
|
+
const s = String(filePath || '').replace(/\\/g, '/');
|
|
112
|
+
return s.startsWith('/') ? s : `/${s}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildCloudHashMap(files = []) {
|
|
116
|
+
const cloudHashes = {};
|
|
117
|
+
for (const f of files) {
|
|
118
|
+
const normalizedPath = f && f.path ? cleanManifestPath(f.path) : null;
|
|
119
|
+
if (normalizedPath && f.hash) cloudHashes[normalizedPath] = f.hash;
|
|
120
|
+
}
|
|
121
|
+
return cloudHashes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isSyncReviewArtifactPath(filePath) {
|
|
125
|
+
const cleaned = cleanManifestPath(filePath);
|
|
126
|
+
return ['.remote', '.local', '.base', '.cloud'].some((suffix) => cleaned.endsWith(suffix));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isNestedWorkspacePollutionPath(filePath, slug) {
|
|
130
|
+
const cleaned = cleanManifestPath(filePath);
|
|
131
|
+
const cleanSlug = String(slug || '').trim().replace(/^\/+|\/+$/g, '');
|
|
132
|
+
if (!cleanSlug) return false;
|
|
133
|
+
return cleaned === `/${cleanSlug}` || cleaned.startsWith(`/${cleanSlug}/`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function analyzePushSafety({
|
|
137
|
+
filesToPush = [],
|
|
138
|
+
deletedPaths = [],
|
|
139
|
+
unchangedCount = 0,
|
|
140
|
+
onlyPrefixes = null,
|
|
141
|
+
slug = null,
|
|
142
|
+
allowBroadWorkspace = false,
|
|
143
|
+
allowNestedWorkspace = false,
|
|
144
|
+
allowSyncArtifacts = false,
|
|
145
|
+
} = {}) {
|
|
146
|
+
const changedPaths = [
|
|
147
|
+
...filesToPush.map((file) => cleanManifestPath(file.path)),
|
|
148
|
+
...deletedPaths.map((filePath) => cleanManifestPath(filePath)),
|
|
149
|
+
];
|
|
150
|
+
const changeCount = changedPaths.length;
|
|
151
|
+
const rootChanged = changedPaths.filter((filePath) => filePath.split('/').filter(Boolean).length === 1);
|
|
152
|
+
const topLevels = new Set(changedPaths.map((filePath) => filePath.split('/').filter(Boolean)[0]).filter(Boolean));
|
|
153
|
+
const nestedWorkspacePaths = changedPaths.filter((filePath) => isNestedWorkspacePollutionPath(filePath, slug));
|
|
154
|
+
const syncArtifactPaths = changedPaths.filter(isSyncReviewArtifactPath);
|
|
155
|
+
const reasons = [];
|
|
156
|
+
|
|
157
|
+
if (nestedWorkspacePaths.length > 0 && !allowNestedWorkspace) {
|
|
158
|
+
reasons.push(`nested workspace folder detected (${nestedWorkspacePaths.length} path${nestedWorkspacePaths.length === 1 ? '' : 's'})`);
|
|
159
|
+
}
|
|
160
|
+
if (syncArtifactPaths.length > 0 && !allowSyncArtifacts) {
|
|
161
|
+
reasons.push(`sync review artifacts detected (${syncArtifactPaths.length} path${syncArtifactPaths.length === 1 ? '' : 's'})`);
|
|
162
|
+
}
|
|
163
|
+
if (!onlyPrefixes && !allowBroadWorkspace) {
|
|
164
|
+
if (changeCount >= 25) {
|
|
165
|
+
reasons.push(`large unscoped workspace change (${changeCount} paths)`);
|
|
166
|
+
} else if (rootChanged.length >= 5) {
|
|
167
|
+
reasons.push(`many root files changed (${rootChanged.length} paths)`);
|
|
168
|
+
} else if (topLevels.size >= 8 && changeCount >= 10) {
|
|
169
|
+
reasons.push(`many top-level areas changed (${topLevels.size} areas)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
ok: reasons.length === 0,
|
|
175
|
+
reasons,
|
|
176
|
+
changedPaths,
|
|
177
|
+
changeCount,
|
|
178
|
+
unchangedCount,
|
|
179
|
+
rootChanged,
|
|
180
|
+
topLevelCount: topLevels.size,
|
|
181
|
+
nestedWorkspacePaths,
|
|
182
|
+
syncArtifactPaths,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderPushSafetyBlock(report, slug) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(' Quest gate: review before publish');
|
|
190
|
+
lines.push(' ✗ Refusing unsafe workspace push.');
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push(` Planned change: ${report.changeCount} path${report.changeCount === 1 ? '' : 's'}, ${report.unchangedCount} unchanged.`);
|
|
193
|
+
for (const reason of report.reasons) {
|
|
194
|
+
lines.push(` Reason: ${reason}.`);
|
|
195
|
+
}
|
|
196
|
+
const examples = [
|
|
197
|
+
...report.nestedWorkspacePaths,
|
|
198
|
+
...report.syncArtifactPaths,
|
|
199
|
+
...report.changedPaths,
|
|
200
|
+
];
|
|
201
|
+
const uniqueExamples = [...new Set(examples)].slice(0, 8);
|
|
202
|
+
if (uniqueExamples.length > 0) {
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(' First paths to review:');
|
|
205
|
+
uniqueExamples.forEach((filePath) => lines.push(` - ${filePath.replace(/^\//, '')}`));
|
|
206
|
+
}
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(' Safe moves:');
|
|
209
|
+
lines.push(` atris push ${slug || ''} --dry-run --only atris/now.md`.trimEnd());
|
|
210
|
+
lines.push(' atris sync --review');
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(' If this broad publish is intentional, rerun with:');
|
|
213
|
+
lines.push(' --allow-broad-workspace');
|
|
214
|
+
if (report.nestedWorkspacePaths.length > 0) lines.push(' --allow-nested-workspace');
|
|
215
|
+
if (report.syncArtifactPaths.length > 0) lines.push(' --allow-sync-artifacts');
|
|
216
|
+
lines.push('');
|
|
217
|
+
return `${lines.join('\n')}\n`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parsePushTimeoutSec(argv = process.argv, defaultSec = 120) {
|
|
221
|
+
let raw = null;
|
|
222
|
+
const eqArg = argv.find(a => a.startsWith('--timeout='));
|
|
223
|
+
if (eqArg) raw = eqArg.slice('--timeout='.length);
|
|
224
|
+
else {
|
|
225
|
+
const idx = argv.indexOf('--timeout');
|
|
226
|
+
if (idx !== -1 && argv[idx + 1]) raw = argv[idx + 1];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const parsed = raw == null ? defaultSec : Number.parseInt(raw, 10);
|
|
230
|
+
if (!Number.isFinite(parsed)) return defaultSec;
|
|
231
|
+
return Math.max(5, Math.min(300, parsed));
|
|
232
|
+
}
|
|
233
|
+
|
|
107
234
|
async function pushAtris() {
|
|
108
235
|
const elapsedMs = startTimer();
|
|
109
236
|
let slug = process.argv[3];
|
|
110
237
|
let _coldWake = false;
|
|
111
238
|
|
|
239
|
+
if (process.argv.includes('--help') || process.argv.includes('-h') || slug === 'help') {
|
|
240
|
+
console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(' Push requires a fresh pull. If cloud has changed since your last pull,');
|
|
243
|
+
console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(' atris push Push from current folder (auto-detect business)');
|
|
246
|
+
console.log(' atris push example-co Push example-co/ or atris/example-co/');
|
|
247
|
+
console.log(' atris push example-co --only team/nate Only push files in team/nate/');
|
|
248
|
+
console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
|
|
249
|
+
console.log(' atris push --delete Allow small cloud deletes shown by --dry-run');
|
|
250
|
+
console.log(' atris push --delete-all Extra confirmation for mass-delete recovery');
|
|
251
|
+
console.log(' --allow-broad-workspace Publish a large unscoped workspace plan after review');
|
|
252
|
+
console.log(' --allow-nested-workspace Explicitly allow nested <slug>/ paths');
|
|
253
|
+
console.log(' --allow-sync-artifacts Explicitly allow *.remote/*.local/*.base/*.cloud files');
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
112
257
|
// Auto-detect business from .atris/business.json in current dir
|
|
113
258
|
if (!slug || slug.startsWith('-')) {
|
|
114
259
|
const bizFile = path.join(process.cwd(), '.atris', 'business.json');
|
|
@@ -128,11 +273,14 @@ async function pushAtris() {
|
|
|
128
273
|
console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
|
|
129
274
|
console.log('');
|
|
130
275
|
console.log(' atris push Push from current folder (auto-detect business)');
|
|
131
|
-
console.log(' atris push
|
|
132
|
-
console.log(' atris push
|
|
276
|
+
console.log(' atris push example-co Push example-co/ or atris/example-co/');
|
|
277
|
+
console.log(' atris push example-co --only team/nate Only push files in team/nate/');
|
|
133
278
|
console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
|
|
134
279
|
console.log(' atris push --delete Allow small cloud deletes shown by --dry-run');
|
|
135
280
|
console.log(' atris push --delete-all Extra confirmation for mass-delete recovery');
|
|
281
|
+
console.log(' --allow-broad-workspace Publish a large unscoped workspace plan after review');
|
|
282
|
+
console.log(' --allow-nested-workspace Explicitly allow nested <slug>/ paths');
|
|
283
|
+
console.log(' --allow-sync-artifacts Explicitly allow *.remote/*.local/*.base/*.cloud files');
|
|
136
284
|
process.exit(0);
|
|
137
285
|
}
|
|
138
286
|
|
|
@@ -140,7 +288,11 @@ async function pushAtris() {
|
|
|
140
288
|
const dryRun = process.argv.includes('--dry-run');
|
|
141
289
|
const allowDelete = process.argv.includes('--delete');
|
|
142
290
|
const allowMassDelete = process.argv.includes('--delete-all');
|
|
291
|
+
const allowBroadWorkspace = process.argv.includes('--allow-broad-workspace');
|
|
292
|
+
const allowNestedWorkspace = process.argv.includes('--allow-nested-workspace');
|
|
293
|
+
const allowSyncArtifacts = process.argv.includes('--allow-sync-artifacts');
|
|
143
294
|
const allowCrossRootManifest = process.argv.includes('--allow-cross-root-manifest');
|
|
295
|
+
const timeoutSec = parsePushTimeoutSec(process.argv);
|
|
144
296
|
|
|
145
297
|
// Parse --only
|
|
146
298
|
let onlyRaw = null;
|
|
@@ -167,7 +319,7 @@ async function pushAtris() {
|
|
|
167
319
|
const sourceDir = resolvePushSourceDir({ slug });
|
|
168
320
|
if (!sourceDir) {
|
|
169
321
|
console.error(`No local folder found for "${slug}".`);
|
|
170
|
-
console.error('Run from inside a pulled folder, or: atris push
|
|
322
|
+
console.error('Run from inside a pulled folder, or: atris push example-co --from ./path');
|
|
171
323
|
process.exit(1);
|
|
172
324
|
}
|
|
173
325
|
|
|
@@ -176,9 +328,8 @@ async function pushAtris() {
|
|
|
176
328
|
// Refuse to walk/upload dangerous paths ($HOME, /, /Users, system dirs).
|
|
177
329
|
assertSafeWorkspaceRoot(sourceDir, { slug, op: 'push from' });
|
|
178
330
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
331
|
+
// Push the whole business workspace by default. Brain-only sync must be
|
|
332
|
+
// explicit with --only atris/ so root workspace files cannot be missed.
|
|
182
333
|
|
|
183
334
|
// Resolve business — always refresh from API
|
|
184
335
|
let businessId, workspaceId, businessName, resolvedSlug;
|
|
@@ -280,11 +431,8 @@ async function pushAtris() {
|
|
|
280
431
|
{ method: 'GET', token: creds.token, timeoutMs: 60000 }
|
|
281
432
|
);
|
|
282
433
|
if (snapshotResult.ok && snapshotResult.data && Array.isArray(snapshotResult.data.files)) {
|
|
283
|
-
const cloudHashes =
|
|
284
|
-
|
|
285
|
-
if (f.path && f.hash) cloudHashes[f.path] = f.hash;
|
|
286
|
-
}
|
|
287
|
-
const manifestFiles = (manifest && manifest.files) || {};
|
|
434
|
+
const cloudHashes = filterSyncFiles(buildCloudHashMap(snapshotResult.data.files));
|
|
435
|
+
const manifestFiles = filterSyncFiles((manifest && manifest.files) || {});
|
|
288
436
|
const driftFiles = [];
|
|
289
437
|
// Direction 1: cloud has files the manifest doesn't know about, OR
|
|
290
438
|
// cloud's hash differs from what we last pulled (someone changed it).
|
|
@@ -350,12 +498,13 @@ async function pushAtris() {
|
|
|
350
498
|
|
|
351
499
|
// Compare local hashes to manifest — NO server call needed
|
|
352
500
|
// Files where local hash differs from manifest = changed locally
|
|
353
|
-
const baseFiles = (manifest && manifest.files) ? manifest.files : {};
|
|
501
|
+
const baseFiles = filterSyncFiles((manifest && manifest.files) ? manifest.files : {});
|
|
354
502
|
const { filesToPush, deletedPaths, unchangedCount } = buildPushChangePlan({
|
|
355
503
|
localFiles,
|
|
356
504
|
baseFiles,
|
|
357
505
|
onlyPrefixes,
|
|
358
506
|
readFileContent: (filePath) => fs.readFileSync(path.join(sourceDir, filePath.replace(/^\//, '')), 'utf8'),
|
|
507
|
+
isLocalFilePresent: (filePath) => fs.existsSync(path.join(sourceDir, filePath.replace(/^\//, ''))),
|
|
359
508
|
});
|
|
360
509
|
|
|
361
510
|
if (filesToPush.length === 0 && deletedPaths.length === 0) {
|
|
@@ -364,6 +513,23 @@ async function pushAtris() {
|
|
|
364
513
|
return;
|
|
365
514
|
}
|
|
366
515
|
|
|
516
|
+
const safetyReport = analyzePushSafety({
|
|
517
|
+
filesToPush,
|
|
518
|
+
deletedPaths,
|
|
519
|
+
unchangedCount,
|
|
520
|
+
onlyPrefixes,
|
|
521
|
+
slug: resolvedSlug || slug,
|
|
522
|
+
allowBroadWorkspace,
|
|
523
|
+
allowNestedWorkspace,
|
|
524
|
+
allowSyncArtifacts,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
if (!safetyReport.ok && !dryRun) {
|
|
528
|
+
process.stdout.write(renderPushSafetyBlock(safetyReport, resolvedSlug || slug));
|
|
529
|
+
await emit('blocked', { error_detail: safetyReport.reasons.join('; ') });
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
|
|
367
533
|
// Dry run — show what would be pushed without pushing
|
|
368
534
|
if (dryRun) {
|
|
369
535
|
console.log('');
|
|
@@ -383,6 +549,10 @@ async function pushAtris() {
|
|
|
383
549
|
console.log(' Deletes require an explicit real push with --delete.');
|
|
384
550
|
console.log(' If these deletes are unexpected, run `atris pull --keep-local` first.\n');
|
|
385
551
|
}
|
|
552
|
+
if (!safetyReport.ok) {
|
|
553
|
+
process.stdout.write(renderPushSafetyBlock(safetyReport, resolvedSlug || slug));
|
|
554
|
+
console.log(' Dry run only: nothing was sent.\n');
|
|
555
|
+
}
|
|
386
556
|
return;
|
|
387
557
|
}
|
|
388
558
|
|
|
@@ -447,8 +617,14 @@ async function pushAtris() {
|
|
|
447
617
|
};
|
|
448
618
|
const wireFiles = (files) => files.map((f) => ({ path: toWirePath(f.path), content: f.content }));
|
|
449
619
|
const syncFiles = (files) => apiRequestJson(
|
|
450
|
-
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
451
|
-
{
|
|
620
|
+
`/business/${businessId}/workspaces/${workspaceId}/sync?timeout=${timeoutSec}`,
|
|
621
|
+
{
|
|
622
|
+
method: 'POST',
|
|
623
|
+
token: creds.token,
|
|
624
|
+
body: { files: wireFiles(files) },
|
|
625
|
+
headers: { 'X-Atris-Actor-Source': 'cli' },
|
|
626
|
+
timeoutMs: (timeoutSec + 15) * 1000,
|
|
627
|
+
}
|
|
452
628
|
);
|
|
453
629
|
|
|
454
630
|
// Inspect per-file results from a /sync response. Treat "written" and
|
|
@@ -596,7 +772,7 @@ async function pushAtris() {
|
|
|
596
772
|
}
|
|
597
773
|
if (deleteFailed.length > 0) {
|
|
598
774
|
console.log('');
|
|
599
|
-
console.log(` ⚠ ${deleteFailed.length} delete
|
|
775
|
+
console.log(` ⚠ ${deleteFailed.length} ${deleteFailed.length === 1 ? 'delete' : 'deletes'} failed (NOT marked as deleted in manifest):`);
|
|
600
776
|
deleteFailed.slice(0, 10).forEach((f) => console.log(` ${f.status} ${f.path.replace(/^\//, '')}`));
|
|
601
777
|
if (deleteFailed.length > 10) console.log(` ... +${deleteFailed.length - 10} more`);
|
|
602
778
|
}
|
|
@@ -625,7 +801,7 @@ async function pushAtris() {
|
|
|
625
801
|
// These did NOT land on cloud even though the HTTP call returned 200.
|
|
626
802
|
if (failedToLand.length > 0) {
|
|
627
803
|
console.log('');
|
|
628
|
-
console.log(` ⚠ ${failedToLand.length} file
|
|
804
|
+
console.log(` ⚠ ${failedToLand.length} ${failedToLand.length === 1 ? 'file' : 'files'} did NOT land on cloud (server returned 200 but`);
|
|
629
805
|
console.log(` dropped or rejected these files):`);
|
|
630
806
|
const shown = failedToLand.slice(0, 15);
|
|
631
807
|
for (const f of shown) {
|
|
@@ -710,6 +886,12 @@ module.exports = {
|
|
|
710
886
|
resolvePushSourceDir,
|
|
711
887
|
canonicalWorkspaceRoot,
|
|
712
888
|
basenameOfManifestPath,
|
|
889
|
+
buildCloudHashMap,
|
|
713
890
|
isBusinessWorkspaceRoot,
|
|
714
891
|
isMassDeletePlan,
|
|
892
|
+
parsePushTimeoutSec,
|
|
893
|
+
analyzePushSafety,
|
|
894
|
+
isNestedWorkspacePollutionPath,
|
|
895
|
+
isSyncReviewArtifactPath,
|
|
896
|
+
renderPushSafetyBlock,
|
|
715
897
|
};
|
package/commands/review.js
CHANGED
|
@@ -29,9 +29,16 @@ function findReviewEngine() {
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
function printMissingReviewEngine(jsonMode) {
|
|
33
|
+
if (jsonMode) {
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
ok: false,
|
|
36
|
+
error: 'Review engine not found.',
|
|
37
|
+
expected: 'atris/business/claude-code-review/workspace/review_engine.py',
|
|
38
|
+
specialists: ['Security', 'Testing', 'Performance', 'Maintainability', 'Database', 'Async'],
|
|
39
|
+
install: 'copy review_engine.py to your project',
|
|
40
|
+
}, null, 2));
|
|
41
|
+
} else {
|
|
35
42
|
console.error('Review engine not found.');
|
|
36
43
|
console.error('Expected at: atris/business/claude-code-review/workspace/review_engine.py');
|
|
37
44
|
console.error('');
|
|
@@ -39,9 +46,20 @@ function runReview(args) {
|
|
|
39
46
|
console.error(' Security, Testing, Performance, Maintainability, Database, Async');
|
|
40
47
|
console.error('');
|
|
41
48
|
console.error('Install: copy review_engine.py to your project');
|
|
42
|
-
process.exit(1);
|
|
43
49
|
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function exitReviewError(message, jsonMode, extra = {}) {
|
|
54
|
+
if (jsonMode) {
|
|
55
|
+
console.log(JSON.stringify({ ok: false, error: message, ...extra }, null, 2));
|
|
56
|
+
} else {
|
|
57
|
+
console.error(message);
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
44
61
|
|
|
62
|
+
function runReview(args) {
|
|
45
63
|
// Parse args
|
|
46
64
|
let file = null;
|
|
47
65
|
let diffRef = null;
|
|
@@ -61,6 +79,11 @@ function runReview(args) {
|
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
const enginePath = findReviewEngine();
|
|
83
|
+
if (!enginePath) {
|
|
84
|
+
printMissingReviewEngine(jsonMode);
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
// Build command
|
|
65
88
|
const cmdArgs = ['python3', enginePath];
|
|
66
89
|
if (file) {
|
|
@@ -74,11 +97,12 @@ function runReview(args) {
|
|
|
74
97
|
|
|
75
98
|
if (allMode) {
|
|
76
99
|
// Audit all Python services
|
|
77
|
-
console.log('Auditing all Python services...\n');
|
|
100
|
+
if (!jsonMode) console.log('Auditing all Python services...\n');
|
|
78
101
|
const servicesDir = path.join(process.cwd(), 'backend', 'services');
|
|
79
102
|
if (!fs.existsSync(servicesDir)) {
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
exitReviewError('No backend/services/ directory found.', jsonMode, {
|
|
104
|
+
expected: 'backend/services/',
|
|
105
|
+
});
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
const files = fs.readdirSync(servicesDir).filter(f => f.endsWith('.py'));
|
|
@@ -107,6 +131,19 @@ function runReview(args) {
|
|
|
107
131
|
|
|
108
132
|
issues.sort((a, b) => a.score - b.score);
|
|
109
133
|
|
|
134
|
+
if (jsonMode) {
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
ok: true,
|
|
137
|
+
action: 'code_review_all',
|
|
138
|
+
services: files.length,
|
|
139
|
+
clean: cleanCount,
|
|
140
|
+
with_findings: issues.length,
|
|
141
|
+
total_findings: totalFindings,
|
|
142
|
+
issues,
|
|
143
|
+
}, null, 2));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
110
147
|
console.log(`AUDIT: ${files.length} services | ${cleanCount} clean | ${issues.length} with findings\n`);
|
|
111
148
|
if (issues.length > 0) {
|
|
112
149
|
console.log(`${'Service'.padEnd(40)} ${'Score'.padStart(6)} ${'Findings'.padStart(8)} Top Issue`);
|
|
@@ -131,15 +168,16 @@ function runReview(args) {
|
|
|
131
168
|
|
|
132
169
|
async function reviewCommand(...args) {
|
|
133
170
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
134
|
-
console.log('Usage: atris review [file] [options]');
|
|
171
|
+
console.log('Usage: atris code-review [file] [options] (alias: atris cr)');
|
|
135
172
|
console.log('');
|
|
136
|
-
console.log(' atris review Review staged changes');
|
|
137
|
-
console.log(' atris review <file.py> Review a specific file');
|
|
138
|
-
console.log(' atris review --diff HEAD~1 Review last commit');
|
|
139
|
-
console.log(' atris review --all Audit all backend services');
|
|
140
|
-
console.log(' atris review --json Machine-readable output');
|
|
173
|
+
console.log(' atris code-review Review staged changes');
|
|
174
|
+
console.log(' atris code-review <file.py> Review a specific file');
|
|
175
|
+
console.log(' atris code-review --diff HEAD~1 Review last commit');
|
|
176
|
+
console.log(' atris code-review --all Audit all backend services');
|
|
177
|
+
console.log(' atris code-review --json Machine-readable output');
|
|
141
178
|
console.log('');
|
|
142
179
|
console.log('6 specialists: Security, Testing, Performance, Maintainability, Database, Async');
|
|
180
|
+
console.log('Note: this is distinct from `atris review` (the workflow validator step).');
|
|
143
181
|
return;
|
|
144
182
|
}
|
|
145
183
|
|