atris 3.15.11 → 3.15.13
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 +12 -1
- package/atris/features/company-brain-sync/validate.md +21 -0
- package/commands/integrations.js +9 -1
- package/commands/pull.js +51 -8
- package/commands/push.js +67 -11
- package/commands/wiki.js +40 -1
- package/lib/todo-fallback.js +34 -9
- package/lib/wiki.js +84 -4
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -55,11 +55,22 @@ CHECK → atris review (verify + cleanup)
|
|
|
55
55
|
|
|
56
56
|
This workspace has a compiled agent brain.
|
|
57
57
|
|
|
58
|
+
On session start, activate it first:
|
|
59
|
+
`atris brain activate --root /Users/keshavrao/arena/atris-cli --verify`
|
|
60
|
+
|
|
58
61
|
Load these first:
|
|
62
|
+
- `atris/now.md`
|
|
59
63
|
- `atris/brain/STATUS.md`
|
|
60
64
|
- `atris/brain/self_improvement_ledger.md`
|
|
65
|
+
- `atris/wiki/concepts/sync-language.md`
|
|
66
|
+
- `atris/skills/activation/SKILL.md`
|
|
61
67
|
- `atris/MAP.md`
|
|
62
|
-
- `atris
|
|
68
|
+
- `atris/TODO.md`
|
|
69
|
+
|
|
70
|
+
First-message rule: follow the sync-language contract before writing to the operator.
|
|
71
|
+
Purpose: optimize for decision-speed; lead with the move, then use descriptions only when they help the operator act.
|
|
72
|
+
Shape: `<operator>, today is about <move>` -> `I picked this because <why now>` -> `Ready: <draft/proof/context>` -> `Go deeper: <paths>`.
|
|
73
|
+
Definitions: operator = current person or agent; move = one concrete high-leverage workflow; why now = business reason; ready = prepared action or proof; paths = 2-4 optional deeper views.
|
|
63
74
|
|
|
64
75
|
Re-run after meaningful work:
|
|
65
76
|
`atris brain compile --root /Users/keshavrao/arena/atris-cli`
|
|
@@ -207,6 +207,27 @@ Validated behavior:
|
|
|
207
207
|
|
|
208
208
|
## Still Needed
|
|
209
209
|
|
|
210
|
+
- Publish `atris@3.15.12`.
|
|
211
|
+
- Deploy the backend workspace snapshot patch from `atrisos-backend`.
|
|
212
|
+
- After deploy, run a Pallet smoke:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm i -g atris@3.15.12
|
|
216
|
+
cd ~/arena/atris-business/pallet
|
|
217
|
+
atris pull pallet --keep-local --only atris/wiki --timeout 120
|
|
218
|
+
atris push pallet --only atris/wiki --dry-run
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- DoorDash post-publish smoke:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
cd ~/arena/atris-business/doordash
|
|
225
|
+
atris wiki verify
|
|
226
|
+
atris push doordash --dry-run
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- Packaged CLI smoke already passed locally for `3.15.12`: tarball install exposes `atris pull --no-manifest` and `atris wiki verify`.
|
|
230
|
+
|
|
210
231
|
This feature is safer and usable, but the "perfect sync" bar still requires more command-path fixtures and model-assisted semantic merge support.
|
|
211
232
|
|
|
212
233
|
Missing validation:
|
package/commands/integrations.js
CHANGED
|
@@ -326,6 +326,7 @@ function imessageDoctor() {
|
|
|
326
326
|
chat_db_readable: false,
|
|
327
327
|
sqlite3_available: false,
|
|
328
328
|
osascript_available: false,
|
|
329
|
+
messages_automation: false,
|
|
329
330
|
};
|
|
330
331
|
const issues = [];
|
|
331
332
|
|
|
@@ -346,10 +347,17 @@ function imessageDoctor() {
|
|
|
346
347
|
|
|
347
348
|
checks.osascript_available = spawnSync('osascript', ['-e', 'return "ok"'], { encoding: 'utf8' }).status === 0;
|
|
348
349
|
if (!checks.osascript_available) issues.push('osascript is not available.');
|
|
350
|
+
if (checks.osascript_available) {
|
|
351
|
+
checks.messages_automation = spawnSync('osascript', ['-e', 'tell application "Messages" to count services'], {
|
|
352
|
+
encoding: 'utf8',
|
|
353
|
+
timeout: 4000,
|
|
354
|
+
}).status === 0;
|
|
355
|
+
}
|
|
356
|
+
if (!checks.messages_automation) issues.push('Messages automation permission is not available yet.');
|
|
349
357
|
|
|
350
358
|
if (!checks.macos) issues.push('Local iMessage requires macOS.');
|
|
351
359
|
|
|
352
|
-
const connected = checks.macos && checks.chat_db_exists && checks.chat_db_readable && checks.sqlite3_available && checks.osascript_available;
|
|
360
|
+
const connected = checks.macos && checks.chat_db_exists && checks.chat_db_readable && checks.sqlite3_available && checks.osascript_available && checks.messages_automation;
|
|
353
361
|
return {
|
|
354
362
|
connected,
|
|
355
363
|
provider: 'local_imessage',
|
package/commands/pull.js
CHANGED
|
@@ -76,7 +76,7 @@ async function pullAtris() {
|
|
|
76
76
|
let arg = process.argv[3];
|
|
77
77
|
|
|
78
78
|
if (arg === '--help') {
|
|
79
|
-
console.log('Usage: atris pull [business] [--into <path>] [--only <prefix>] [--keep-local] [--timeout <seconds>] [--dry-run]');
|
|
79
|
+
console.log('Usage: atris pull [business] [--into <path>] [--only <prefix>] [--keep-local] [--timeout <seconds>] [--dry-run] [--no-manifest]');
|
|
80
80
|
console.log('');
|
|
81
81
|
console.log(' Pull is force-overwrite by default. Cloud is the source of truth.');
|
|
82
82
|
console.log(' Local files that conflict with cloud are replaced by the cloud version.');
|
|
@@ -87,6 +87,7 @@ async function pullAtris() {
|
|
|
87
87
|
console.log(' atris pull doordash --only atris/wiki/');
|
|
88
88
|
console.log(' atris pull --keep-local Preserve conflicting local edits as .remote files (legacy)');
|
|
89
89
|
console.log(' atris pull --dry-run Preview pull changes without writing local files');
|
|
90
|
+
console.log(' atris pull --no-manifest Pull for inspection without changing this machine\'s sync anchor');
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -176,6 +177,7 @@ async function pullBusiness(slug) {
|
|
|
176
177
|
const force = !process.argv.includes('--keep-local');
|
|
177
178
|
const failOnConflict = process.argv.includes('--fail-on-conflict');
|
|
178
179
|
const dryRun = process.argv.includes('--dry-run');
|
|
180
|
+
const noManifest = process.argv.includes('--no-manifest');
|
|
179
181
|
|
|
180
182
|
// Parse --only flag: comma-separated directory prefixes to filter
|
|
181
183
|
// Supports both --only=team/,context/ and --only team/,context/
|
|
@@ -485,6 +487,19 @@ async function pullBusiness(slug) {
|
|
|
485
487
|
let files = result.data.files || [];
|
|
486
488
|
if (files.length === 0) {
|
|
487
489
|
console.log(' Workspace is empty.');
|
|
490
|
+
const inScopeLocalBeforePull = Object.keys(localFilesBeforePull).filter((p) => {
|
|
491
|
+
if (!onlyPrefixes) return true;
|
|
492
|
+
const rel = p.replace(/^\//, '');
|
|
493
|
+
return onlyPrefixes.some((pref) => rel.startsWith(pref));
|
|
494
|
+
}).length;
|
|
495
|
+
if (!force && inScopeLocalBeforePull > 0) {
|
|
496
|
+
console.error('');
|
|
497
|
+
console.error(' Pull stopped: cloud returned zero files while local has in-scope content.');
|
|
498
|
+
console.error(' This usually means the snapshot endpoint is unhealthy or still warming.');
|
|
499
|
+
console.error(' No local files or sync manifest were changed.');
|
|
500
|
+
await emit('status_unknown', { error_detail: 'empty snapshot with local in-scope content' });
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
488
503
|
// Don't early-return in force mode: we still need to fall through to the
|
|
489
504
|
// mirror sweep so a genuinely-emptied cloud can clear local files. The
|
|
490
505
|
// sweep itself has a safety guard that refuses to wipe local content
|
|
@@ -551,11 +566,35 @@ async function pullBusiness(slug) {
|
|
|
551
566
|
// Compute local file hashes
|
|
552
567
|
const localFiles = localFilesBeforePull;
|
|
553
568
|
|
|
569
|
+
function isPullPathInScope(filePath) {
|
|
570
|
+
if (!onlyPrefixes) return true;
|
|
571
|
+
const rel = filePath.replace(/^\//, '');
|
|
572
|
+
return onlyPrefixes.some((pref) => rel.startsWith(pref));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function filterFilesToPullScope(filesMap = {}) {
|
|
576
|
+
if (!onlyPrefixes) return filesMap;
|
|
577
|
+
return Object.fromEntries(
|
|
578
|
+
Object.entries(filesMap).filter(([p]) => isPullPathInScope(p))
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function filterManifestToPullScope(existingManifest) {
|
|
583
|
+
if (!onlyPrefixes || !existingManifest || !existingManifest.files) return existingManifest;
|
|
584
|
+
return {
|
|
585
|
+
...existingManifest,
|
|
586
|
+
files: filterFilesToPullScope(existingManifest.files),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
554
590
|
// If output dir is empty (fresh clone) or --force, treat as first sync — pull everything
|
|
555
|
-
const
|
|
591
|
+
const scopedLocalFiles = filterFilesToPullScope(localFiles);
|
|
592
|
+
const scopedRemoteFiles = filterFilesToPullScope(remoteFiles);
|
|
593
|
+
const scopedManifest = filterManifestToPullScope(manifest);
|
|
594
|
+
const effectiveManifest = (Object.keys(scopedLocalFiles).length === 0 || force) ? null : scopedManifest;
|
|
556
595
|
|
|
557
596
|
// Three-way compare
|
|
558
|
-
const diff = threeWayCompare(
|
|
597
|
+
const diff = threeWayCompare(scopedLocalFiles, scopedRemoteFiles, effectiveManifest);
|
|
559
598
|
|
|
560
599
|
if (dryRun) {
|
|
561
600
|
console.log('');
|
|
@@ -806,12 +845,16 @@ async function pullBusiness(slug) {
|
|
|
806
845
|
}
|
|
807
846
|
manifestFiles = merged;
|
|
808
847
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
848
|
+
if (!noManifest) {
|
|
849
|
+
const newManifest = buildManifest(manifestFiles, commitHash, { workspaceRoot: outputDir });
|
|
850
|
+
saveManifest(resolvedSlug || slug, newManifest);
|
|
851
|
+
writeBaseContents(outputDir, remoteContent);
|
|
852
|
+
removeBaseContents(outputDir, diff.deletedRemote);
|
|
853
|
+
}
|
|
813
854
|
|
|
814
|
-
// Save business config in the output dir so push/status work without args
|
|
855
|
+
// Save business config in the output dir so push/status work without args.
|
|
856
|
+
// Inspection pulls should not re-bind the global sync anchor, but the pulled
|
|
857
|
+
// folder still benefits from a local business marker for navigation.
|
|
815
858
|
const atrisDir = path.join(outputDir, '.atris');
|
|
816
859
|
fs.mkdirSync(atrisDir, { recursive: true });
|
|
817
860
|
fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
|
package/commands/push.js
CHANGED
|
@@ -89,6 +89,21 @@ function buildPushChangePlan({
|
|
|
89
89
|
return { filesToPush, deletedPaths, unchangedCount };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function shouldRetrySyncIndividually(result, filesToPush) {
|
|
93
|
+
if (!result || result.ok) return false;
|
|
94
|
+
if (!Array.isArray(filesToPush) || filesToPush.length <= 1) return false;
|
|
95
|
+
return result.status !== 403 && result.status !== 409;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isMassDeletePlan({ deletedPaths = [], filesToPush = [], unchangedCount = 0 } = {}) {
|
|
99
|
+
const deleteCount = deletedPaths.length;
|
|
100
|
+
if (deleteCount === 0) return false;
|
|
101
|
+
const survivingCount = filesToPush.length + unchangedCount;
|
|
102
|
+
if (deleteCount >= 10 && survivingCount === 0) return true;
|
|
103
|
+
if (deleteCount >= 25) return true;
|
|
104
|
+
return deleteCount >= 10 && deleteCount > survivingCount;
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
async function pushAtris() {
|
|
93
108
|
const elapsedMs = startTimer();
|
|
94
109
|
let slug = process.argv[3];
|
|
@@ -107,7 +122,7 @@ async function pushAtris() {
|
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
if (!slug || slug === '--help') {
|
|
110
|
-
console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete]');
|
|
125
|
+
console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete] [--delete-all]');
|
|
111
126
|
console.log('');
|
|
112
127
|
console.log(' Push requires a fresh pull. If cloud has changed since your last pull,');
|
|
113
128
|
console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
|
|
@@ -116,13 +131,15 @@ async function pushAtris() {
|
|
|
116
131
|
console.log(' atris push pallet Push pallet/ or atris/pallet/');
|
|
117
132
|
console.log(' atris push pallet --only team/nate Only push files in team/nate/');
|
|
118
133
|
console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
|
|
119
|
-
console.log(' atris push --delete Allow cloud deletes shown by --dry-run');
|
|
134
|
+
console.log(' atris push --delete Allow small cloud deletes shown by --dry-run');
|
|
135
|
+
console.log(' atris push --delete-all Extra confirmation for mass-delete recovery');
|
|
120
136
|
process.exit(0);
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
const force = process.argv.includes('--force');
|
|
124
140
|
const dryRun = process.argv.includes('--dry-run');
|
|
125
141
|
const allowDelete = process.argv.includes('--delete');
|
|
142
|
+
const allowMassDelete = process.argv.includes('--delete-all');
|
|
126
143
|
const allowCrossRootManifest = process.argv.includes('--allow-cross-root-manifest');
|
|
127
144
|
|
|
128
145
|
// Parse --only
|
|
@@ -343,7 +360,7 @@ async function pushAtris() {
|
|
|
343
360
|
|
|
344
361
|
if (filesToPush.length === 0 && deletedPaths.length === 0) {
|
|
345
362
|
console.log('\n Already up to date.\n');
|
|
346
|
-
await emit('success', { files_unchanged:
|
|
363
|
+
await emit('success', { files_unchanged: unchangedCount });
|
|
347
364
|
return;
|
|
348
365
|
}
|
|
349
366
|
|
|
@@ -384,6 +401,24 @@ async function pushAtris() {
|
|
|
384
401
|
process.exit(1);
|
|
385
402
|
}
|
|
386
403
|
|
|
404
|
+
if (deletedPaths.length > 0 && allowDelete && !allowMassDelete && isMassDeletePlan({ deletedPaths, filesToPush, unchangedCount })) {
|
|
405
|
+
console.log('');
|
|
406
|
+
console.log(` ✗ Refusing mass delete of ${deletedPaths.length} cloud files with --delete alone.`);
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log(' This looks like a missing local folder or wrong source root.');
|
|
409
|
+
console.log(' No cloud files were deleted.');
|
|
410
|
+
console.log('');
|
|
411
|
+
console.log(' First inspect the plan:');
|
|
412
|
+
console.log(` atris push ${resolvedSlug || slug} --dry-run`);
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log(' If this is truly an intentional full cleanup, rerun with both flags:');
|
|
415
|
+
console.log(` atris push ${resolvedSlug || slug} --delete --delete-all`);
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(' If this is surprising, pull cloud truth first:');
|
|
418
|
+
console.log(` atris pull ${resolvedSlug || slug} --keep-local --timeout 120`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
387
422
|
let pushed = 0;
|
|
388
423
|
let deleted = 0;
|
|
389
424
|
let skipped = [];
|
|
@@ -398,6 +433,7 @@ async function pushAtris() {
|
|
|
398
433
|
let failedToLand = [];
|
|
399
434
|
const landedPaths = new Set();
|
|
400
435
|
let result = { ok: true };
|
|
436
|
+
let batchFailureDetail = null;
|
|
401
437
|
|
|
402
438
|
// Server-canonical path format for the /sync endpoint: NO leading slash.
|
|
403
439
|
// The warm runner's _safe_path rejects `/atris/...` with "Absolute path
|
|
@@ -410,6 +446,10 @@ async function pushAtris() {
|
|
|
410
446
|
return s.startsWith('/') ? s : `/${s}`;
|
|
411
447
|
};
|
|
412
448
|
const wireFiles = (files) => files.map((f) => ({ path: toWirePath(f.path), content: f.content }));
|
|
449
|
+
const syncFiles = (files) => apiRequestJson(
|
|
450
|
+
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
451
|
+
{ method: 'POST', token: creds.token, body: { files: wireFiles(files) }, headers: { 'X-Atris-Actor-Source': 'cli' } }
|
|
452
|
+
);
|
|
413
453
|
|
|
414
454
|
// Inspect per-file results from a /sync response. Treat "written" and
|
|
415
455
|
// "unchanged" as success; everything else (including missing-from-results,
|
|
@@ -451,10 +491,7 @@ async function pushAtris() {
|
|
|
451
491
|
|
|
452
492
|
if (filesToPush.length > 0) {
|
|
453
493
|
// Push files to server (strip leading slash — server requires workspace-relative paths)
|
|
454
|
-
result = await
|
|
455
|
-
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
456
|
-
{ method: 'POST', token: creds.token, body: { files: wireFiles(filesToPush) }, headers: { 'X-Atris-Actor-Source': 'cli' } }
|
|
457
|
-
);
|
|
494
|
+
result = await syncFiles(filesToPush);
|
|
458
495
|
|
|
459
496
|
if (!result.ok) {
|
|
460
497
|
if (result.status === 403) {
|
|
@@ -469,10 +506,7 @@ async function pushAtris() {
|
|
|
469
506
|
skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
|
|
470
507
|
|
|
471
508
|
if (allowed.length > 0) {
|
|
472
|
-
const retry = await
|
|
473
|
-
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
474
|
-
{ method: 'POST', token: creds.token, body: { files: wireFiles(allowed) }, headers: { 'X-Atris-Actor-Source': 'cli' } }
|
|
475
|
-
);
|
|
509
|
+
const retry = await syncFiles(allowed);
|
|
476
510
|
if (retry.ok) {
|
|
477
511
|
recordSyncResults(allowed, retry);
|
|
478
512
|
pushed = landedPaths.size;
|
|
@@ -490,6 +524,23 @@ async function pushAtris() {
|
|
|
490
524
|
console.error('\n Computer is sleeping. Wake it first.');
|
|
491
525
|
await emit('cold_wake', { error_detail: 'computer sleeping (409)' });
|
|
492
526
|
process.exit(1);
|
|
527
|
+
} else if (shouldRetrySyncIndividually(result, filesToPush)) {
|
|
528
|
+
batchFailureDetail = `${result.status || 'unknown'}: ${result.errorMessage || result.error || 'batch sync failed'}`;
|
|
529
|
+
console.log('');
|
|
530
|
+
console.log(` Batch push failed (${result.errorMessage || result.error || result.status}). Retrying one file at a time...`);
|
|
531
|
+
for (const f of filesToPush) {
|
|
532
|
+
const single = await syncFiles([f]);
|
|
533
|
+
if (single.ok) {
|
|
534
|
+
recordSyncResults([f], single);
|
|
535
|
+
} else {
|
|
536
|
+
failedToLand.push({
|
|
537
|
+
path: f.path,
|
|
538
|
+
status: single.status || 'error',
|
|
539
|
+
error: single.errorMessage || single.error || '',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
pushed = landedPaths.size;
|
|
493
544
|
} else {
|
|
494
545
|
console.error(`\n Push failed: ${result.errorMessage || result.error || result.status}`);
|
|
495
546
|
await emit('status_unknown', { error_detail: `sync status ${result.status}` });
|
|
@@ -631,6 +682,9 @@ async function pushAtris() {
|
|
|
631
682
|
} else if (deleteFailed.length > 0) {
|
|
632
683
|
finalOutcome = 'status_unknown';
|
|
633
684
|
finalDetail = `${deleteFailed.length} delete(s) failed (statuses: ${[...new Set(deleteFailed.map(f => f.status))].join(',')})`;
|
|
685
|
+
} else if (batchFailureDetail) {
|
|
686
|
+
finalOutcome = 'status_unknown';
|
|
687
|
+
finalDetail = `batch sync failed but individual retry landed all files (${batchFailureDetail})`;
|
|
634
688
|
} else if (_rateLimitedDeletes > 0) {
|
|
635
689
|
finalOutcome = 'rate_limited';
|
|
636
690
|
finalDetail = `${_rateLimitedDeletes} delete(s) hit 429 (recovered)`;
|
|
@@ -652,8 +706,10 @@ async function pushAtris() {
|
|
|
652
706
|
module.exports = {
|
|
653
707
|
pushAtris,
|
|
654
708
|
buildPushChangePlan,
|
|
709
|
+
shouldRetrySyncIndividually,
|
|
655
710
|
resolvePushSourceDir,
|
|
656
711
|
canonicalWorkspaceRoot,
|
|
657
712
|
basenameOfManifestPath,
|
|
658
713
|
isBusinessWorkspaceRoot,
|
|
714
|
+
isMassDeletePlan,
|
|
659
715
|
};
|
package/commands/wiki.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
buildLintPrompt,
|
|
17
17
|
writeWikiStatus,
|
|
18
18
|
appendWikiLog,
|
|
19
|
+
validateAgentReadableWikiPages,
|
|
19
20
|
} = require('../lib/wiki');
|
|
20
21
|
|
|
21
22
|
function autoDetectSlug() {
|
|
@@ -331,6 +332,32 @@ function wikiLog(mode, slug, limit) {
|
|
|
331
332
|
console.log('');
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
function wikiVerify(mode, slug) {
|
|
336
|
+
const wikiMode = mode === 'private' ? 'private' : 'public';
|
|
337
|
+
const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
|
|
338
|
+
if (!wikiDir) {
|
|
339
|
+
console.error(`No local wiki found at ${getWikiRoot(wikiMode)}.`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const report = validateAgentReadableWikiPages(process.cwd(), wikiMode);
|
|
344
|
+
console.log('');
|
|
345
|
+
console.log(`Agent-readable wiki contract: ${report.ok ? 'pass' : 'fail'}`);
|
|
346
|
+
console.log(` pages: ${report.pageCount}`);
|
|
347
|
+
console.log(` findings: ${report.findingCount}`);
|
|
348
|
+
if (!report.ok) {
|
|
349
|
+
for (const finding of report.findings.slice(0, 20)) {
|
|
350
|
+
console.log(` - ${finding.page}: ${finding.code} - ${finding.message}`);
|
|
351
|
+
}
|
|
352
|
+
if (report.findings.length > 20) {
|
|
353
|
+
console.log(` ... +${report.findings.length - 20} more`);
|
|
354
|
+
}
|
|
355
|
+
console.log('');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
console.log('');
|
|
359
|
+
}
|
|
360
|
+
|
|
334
361
|
async function wikiCommand(subcommand, ...args) {
|
|
335
362
|
const { mode, args: cleanArgs } = parseModeArgs(args);
|
|
336
363
|
|
|
@@ -392,8 +419,18 @@ async function wikiCommand(subcommand, ...args) {
|
|
|
392
419
|
await loopAtris(cleanArgs);
|
|
393
420
|
break;
|
|
394
421
|
}
|
|
422
|
+
case 'verify':
|
|
423
|
+
case 'contract': {
|
|
424
|
+
const slug = mode === 'cloud' ? (cleanArgs[0] || autoDetectSlug()) : null;
|
|
425
|
+
if (mode === 'cloud') {
|
|
426
|
+
console.error('Cloud wiki verify is local-first. Run after `atris pull --only atris/wiki --no-manifest` or inside a workspace.');
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
wikiVerify(mode, slug);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
395
432
|
default:
|
|
396
|
-
console.log('Usage: atris wiki <ingest|query|lint|search|log|loop> [business] [args]');
|
|
433
|
+
console.log('Usage: atris wiki <ingest|query|lint|search|log|loop|verify> [business] [args]');
|
|
397
434
|
console.log('');
|
|
398
435
|
console.log(' ingest <path> Local-first ingest into atris/wiki/');
|
|
399
436
|
console.log(' query "question" Local-first query against atris/wiki/');
|
|
@@ -401,6 +438,7 @@ async function wikiCommand(subcommand, ...args) {
|
|
|
401
438
|
console.log(' search [business] <term> Search local atris/wiki/index.md');
|
|
402
439
|
console.log(' log [business] [N] Show recent atris/wiki/log.md entries');
|
|
403
440
|
console.log(' loop Run local wiki upkeep analysis and refresh STATUS/log');
|
|
441
|
+
console.log(' verify Check agent-readable source/verification metadata');
|
|
404
442
|
console.log('');
|
|
405
443
|
console.log('Flags:');
|
|
406
444
|
console.log(' --cloud Route ingest/query/lint to the cloud workspace');
|
|
@@ -418,4 +456,5 @@ module.exports = {
|
|
|
418
456
|
wikiLint,
|
|
419
457
|
wikiSearch,
|
|
420
458
|
wikiLog,
|
|
459
|
+
wikiVerify,
|
|
421
460
|
};
|
package/lib/todo-fallback.js
CHANGED
|
@@ -19,9 +19,25 @@ function parseTodoFile(todoPath) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function tagsFromText(text) {
|
|
23
|
+
const allTags = [...String(text || '').matchAll(/\[(\w+)\]/g)].map(m => m[1]);
|
|
24
|
+
return {
|
|
25
|
+
allTags,
|
|
26
|
+
tag: allTags.includes('endgame') ? 'endgame' : (allTags[0] || null),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanTaskTitle(text) {
|
|
31
|
+
const raw = String(text || '').trim();
|
|
32
|
+
const withoutTags = raw.replace(/\s*\[\w+\]/g, '').trim();
|
|
33
|
+
const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[—-]\s*)?(.*)$/);
|
|
34
|
+
if (!bold) return withoutTags;
|
|
35
|
+
return [bold[1], bold[2]].filter(Boolean).join(' — ').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
function parseSection(content, sectionName) {
|
|
23
39
|
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
24
|
-
const match = content.match(new RegExp(
|
|
40
|
+
const match = content.match(new RegExp(`(?:^|\\n)##\\s+${escaped}\\s*\\n([\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
|
|
25
41
|
if (!match) return [];
|
|
26
42
|
|
|
27
43
|
const body = (match[1] || '').trim();
|
|
@@ -34,14 +50,13 @@ function parseSection(content, sectionName) {
|
|
|
34
50
|
for (const rawLine of lines) {
|
|
35
51
|
const line = rawLine.trimEnd();
|
|
36
52
|
|
|
37
|
-
const taskMatch = line.match(/^- \*\*([
|
|
53
|
+
const taskMatch = line.match(/^- \*\*([^*:\n]+):\*\*\s*(.+)$/);
|
|
38
54
|
if (taskMatch) {
|
|
39
55
|
if (current) tasks.push(current);
|
|
40
|
-
const allTags =
|
|
41
|
-
const tag = allTags.includes('endgame') ? 'endgame' : (allTags[0] || null);
|
|
56
|
+
const { allTags, tag } = tagsFromText(taskMatch[2]);
|
|
42
57
|
current = {
|
|
43
58
|
id: taskMatch[1],
|
|
44
|
-
title: taskMatch[2]
|
|
59
|
+
title: cleanTaskTitle(taskMatch[2]),
|
|
45
60
|
tag,
|
|
46
61
|
tags: allTags,
|
|
47
62
|
claimed: null,
|
|
@@ -52,14 +67,24 @@ function parseSection(content, sectionName) {
|
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
const checkMatch = line.match(/^- \[[ x]\]\s+(.+)$/);
|
|
55
|
-
if (checkMatch
|
|
56
|
-
|
|
70
|
+
if (checkMatch) {
|
|
71
|
+
if (current) {
|
|
72
|
+
tasks.push(current);
|
|
73
|
+
current = null;
|
|
74
|
+
}
|
|
75
|
+
const { allTags, tag } = tagsFromText(checkMatch[1]);
|
|
76
|
+
tasks.push({ id: null, title: cleanTaskTitle(checkMatch[1]), tag, tags: allTags, claimed: null, stage: null, verify: null });
|
|
57
77
|
continue;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
const plainMatch = line.match(/^- (.+)$/);
|
|
61
|
-
if (plainMatch && !
|
|
62
|
-
|
|
81
|
+
if (plainMatch && !plainMatch[1].startsWith('**')) {
|
|
82
|
+
if (current) {
|
|
83
|
+
tasks.push(current);
|
|
84
|
+
current = null;
|
|
85
|
+
}
|
|
86
|
+
const { allTags, tag } = tagsFromText(plainMatch[1]);
|
|
87
|
+
tasks.push({ id: null, title: cleanTaskTitle(plainMatch[1]), tag, tags: allTags, claimed: null, stage: null, verify: null });
|
|
63
88
|
continue;
|
|
64
89
|
}
|
|
65
90
|
|
package/lib/wiki.js
CHANGED
|
@@ -457,7 +457,20 @@ function readWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
|
457
457
|
|
|
458
458
|
function normalizeSourcePath(projectRoot, source) {
|
|
459
459
|
if (!source || /^https?:\/\//i.test(source)) return null;
|
|
460
|
+
// Source receipts may be external system labels such as "hubspot",
|
|
461
|
+
// "chorus", or "greenhouse". They are valid evidence labels but not local
|
|
462
|
+
// files, so stale-file checks should not mark them missing.
|
|
463
|
+
if (!/[/.]/.test(source)) return null;
|
|
460
464
|
if (path.isAbsolute(source)) return path.normalize(source);
|
|
465
|
+
if (source.startsWith('context/')) {
|
|
466
|
+
const atrisContextPath = path.normalize(path.join(projectRoot, 'atris', source));
|
|
467
|
+
if (fs.existsSync(atrisContextPath)) return atrisContextPath;
|
|
468
|
+
}
|
|
469
|
+
if (source.startsWith('team/')) {
|
|
470
|
+
const atrisTeamPath = path.normalize(path.join(projectRoot, 'atris', source));
|
|
471
|
+
if (fs.existsSync(atrisTeamPath)) return atrisTeamPath;
|
|
472
|
+
}
|
|
473
|
+
if (/^(chorus|hubspot|greenhouse)\//.test(source)) return null;
|
|
461
474
|
return path.normalize(path.join(projectRoot, source));
|
|
462
475
|
}
|
|
463
476
|
|
|
@@ -503,9 +516,20 @@ function findStaleWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
|
503
516
|
.filter(Boolean);
|
|
504
517
|
}
|
|
505
518
|
|
|
506
|
-
function extractWikiLinks(content) {
|
|
519
|
+
function extractWikiLinks(content, wikiRoot = WIKI_ROOT) {
|
|
507
520
|
const matches = content.match(/\[\[((?:atris\/wiki|\.atris\/presidio)\/[^\]]+?)\]\]/g) || [];
|
|
508
|
-
|
|
521
|
+
const links = matches.map((match) => match.slice(2, -2));
|
|
522
|
+
const markdownMatches = content.matchAll(/\[[^\]]+\]\(([^)]+\.md)\)/g);
|
|
523
|
+
for (const match of markdownMatches) {
|
|
524
|
+
const raw = String(match[1] || '').trim();
|
|
525
|
+
if (!raw || /^https?:\/\//i.test(raw) || raw.startsWith('#')) continue;
|
|
526
|
+
if (raw.startsWith(`${wikiRoot}/`)) {
|
|
527
|
+
links.push(raw);
|
|
528
|
+
} else if (!raw.startsWith('/') && !raw.startsWith('..')) {
|
|
529
|
+
links.push(`${wikiRoot}/${raw}`.replace(/\/+/g, '/'));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return links;
|
|
509
533
|
}
|
|
510
534
|
|
|
511
535
|
function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
|
|
@@ -519,8 +543,10 @@ function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
|
|
|
519
543
|
inboundLinks.set(page.relativePath, 0);
|
|
520
544
|
}
|
|
521
545
|
|
|
546
|
+
const indexLinks = new Set(extractWikiLinks(indexContent, wikiRoot).map((link) => link.replace(/\\/g, '/')));
|
|
547
|
+
|
|
522
548
|
for (const page of pages) {
|
|
523
|
-
const links = extractWikiLinks(page.content);
|
|
549
|
+
const links = extractWikiLinks(page.content, wikiRoot);
|
|
524
550
|
for (const link of links) {
|
|
525
551
|
const normalized = link.replace(/\\/g, '/');
|
|
526
552
|
if (normalized !== page.relativePath && inboundLinks.has(normalized)) {
|
|
@@ -531,13 +557,66 @@ function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
|
|
|
531
557
|
|
|
532
558
|
return pages
|
|
533
559
|
.filter((page) => {
|
|
534
|
-
const indexed = indexContent.includes(`[[${page.relativePath}]]`);
|
|
560
|
+
const indexed = indexContent.includes(`[[${page.relativePath}]]`) || indexLinks.has(page.relativePath);
|
|
535
561
|
const inboundCount = inboundLinks.get(page.relativePath) || 0;
|
|
536
562
|
return !indexed && inboundCount === 0;
|
|
537
563
|
})
|
|
538
564
|
.map((page) => page.relativePath);
|
|
539
565
|
}
|
|
540
566
|
|
|
567
|
+
function hasFrontmatterKey(frontmatter, key) {
|
|
568
|
+
return Object.prototype.hasOwnProperty.call(frontmatter || {}, key);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isIsoDate(value) {
|
|
572
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isConfidence(value) {
|
|
576
|
+
const n = Number(value);
|
|
577
|
+
return Number.isFinite(n) && n >= 0 && n <= 1;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function validateAgentReadableWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
581
|
+
const findings = [];
|
|
582
|
+
const pages = readWikiPages(projectRoot, mode);
|
|
583
|
+
const stalePages = new Set(findStaleWikiPages(projectRoot, mode).map((item) => item.page));
|
|
584
|
+
|
|
585
|
+
for (const page of pages) {
|
|
586
|
+
const fm = page.frontmatter || {};
|
|
587
|
+
const sources = Array.isArray(fm.sources) ? fm.sources.filter(Boolean) : [];
|
|
588
|
+
|
|
589
|
+
if (sources.length === 0) {
|
|
590
|
+
findings.push({ page: page.relativePath, code: 'missing-sources', message: 'missing source receipt in frontmatter sources' });
|
|
591
|
+
}
|
|
592
|
+
if (!isIsoDate(fm.last_compiled)) {
|
|
593
|
+
findings.push({ page: page.relativePath, code: 'missing-last-compiled', message: 'missing YYYY-MM-DD last_compiled' });
|
|
594
|
+
}
|
|
595
|
+
if (!isIsoDate(fm.last_verified)) {
|
|
596
|
+
findings.push({ page: page.relativePath, code: 'missing-last-verified', message: 'missing YYYY-MM-DD last_verified' });
|
|
597
|
+
}
|
|
598
|
+
if (!isConfidence(fm.confidence)) {
|
|
599
|
+
findings.push({ page: page.relativePath, code: 'missing-confidence', message: 'confidence must be a number from 0 to 1' });
|
|
600
|
+
}
|
|
601
|
+
if (!hasFrontmatterKey(fm, 'dependencies') || !Array.isArray(fm.dependencies)) {
|
|
602
|
+
findings.push({ page: page.relativePath, code: 'missing-dependencies', message: 'dependencies must be present as a list, even if empty' });
|
|
603
|
+
}
|
|
604
|
+
if (!String(fm.actionability || '').trim()) {
|
|
605
|
+
findings.push({ page: page.relativePath, code: 'missing-actionability', message: 'missing actionability summary' });
|
|
606
|
+
}
|
|
607
|
+
if (stalePages.has(page.relativePath)) {
|
|
608
|
+
findings.push({ page: page.relativePath, code: 'stale-source', message: 'source changed since last_compiled or source is missing' });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
ok: findings.length === 0,
|
|
614
|
+
pageCount: pages.length,
|
|
615
|
+
findingCount: findings.length,
|
|
616
|
+
findings,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
541
620
|
function findSuggestedSources(projectRoot = process.cwd(), limit = 3) {
|
|
542
621
|
const candidates = [
|
|
543
622
|
'README.md',
|
|
@@ -743,6 +822,7 @@ module.exports = {
|
|
|
743
822
|
readWikiPages,
|
|
744
823
|
findStaleWikiPages,
|
|
745
824
|
findWikiOrphans,
|
|
825
|
+
validateAgentReadableWikiPages,
|
|
746
826
|
findSuggestedSources,
|
|
747
827
|
stageWikiIngest,
|
|
748
828
|
writeWikiStatus,
|