atris 3.15.11 → 3.15.12
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 +25 -6
- package/commands/push.js +35 -9
- 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
|
|
@@ -806,12 +821,16 @@ async function pullBusiness(slug) {
|
|
|
806
821
|
}
|
|
807
822
|
manifestFiles = merged;
|
|
808
823
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
824
|
+
if (!noManifest) {
|
|
825
|
+
const newManifest = buildManifest(manifestFiles, commitHash, { workspaceRoot: outputDir });
|
|
826
|
+
saveManifest(resolvedSlug || slug, newManifest);
|
|
827
|
+
writeBaseContents(outputDir, remoteContent);
|
|
828
|
+
removeBaseContents(outputDir, diff.deletedRemote);
|
|
829
|
+
}
|
|
813
830
|
|
|
814
|
-
// Save business config in the output dir so push/status work without args
|
|
831
|
+
// Save business config in the output dir so push/status work without args.
|
|
832
|
+
// Inspection pulls should not re-bind the global sync anchor, but the pulled
|
|
833
|
+
// folder still benefits from a local business marker for navigation.
|
|
815
834
|
const atrisDir = path.join(outputDir, '.atris');
|
|
816
835
|
fs.mkdirSync(atrisDir, { recursive: true });
|
|
817
836
|
fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
|
package/commands/push.js
CHANGED
|
@@ -89,6 +89,12 @@ 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
|
+
|
|
92
98
|
async function pushAtris() {
|
|
93
99
|
const elapsedMs = startTimer();
|
|
94
100
|
let slug = process.argv[3];
|
|
@@ -343,7 +349,7 @@ async function pushAtris() {
|
|
|
343
349
|
|
|
344
350
|
if (filesToPush.length === 0 && deletedPaths.length === 0) {
|
|
345
351
|
console.log('\n Already up to date.\n');
|
|
346
|
-
await emit('success', { files_unchanged:
|
|
352
|
+
await emit('success', { files_unchanged: unchangedCount });
|
|
347
353
|
return;
|
|
348
354
|
}
|
|
349
355
|
|
|
@@ -398,6 +404,7 @@ async function pushAtris() {
|
|
|
398
404
|
let failedToLand = [];
|
|
399
405
|
const landedPaths = new Set();
|
|
400
406
|
let result = { ok: true };
|
|
407
|
+
let batchFailureDetail = null;
|
|
401
408
|
|
|
402
409
|
// Server-canonical path format for the /sync endpoint: NO leading slash.
|
|
403
410
|
// The warm runner's _safe_path rejects `/atris/...` with "Absolute path
|
|
@@ -410,6 +417,10 @@ async function pushAtris() {
|
|
|
410
417
|
return s.startsWith('/') ? s : `/${s}`;
|
|
411
418
|
};
|
|
412
419
|
const wireFiles = (files) => files.map((f) => ({ path: toWirePath(f.path), content: f.content }));
|
|
420
|
+
const syncFiles = (files) => apiRequestJson(
|
|
421
|
+
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
422
|
+
{ method: 'POST', token: creds.token, body: { files: wireFiles(files) }, headers: { 'X-Atris-Actor-Source': 'cli' } }
|
|
423
|
+
);
|
|
413
424
|
|
|
414
425
|
// Inspect per-file results from a /sync response. Treat "written" and
|
|
415
426
|
// "unchanged" as success; everything else (including missing-from-results,
|
|
@@ -451,10 +462,7 @@ async function pushAtris() {
|
|
|
451
462
|
|
|
452
463
|
if (filesToPush.length > 0) {
|
|
453
464
|
// 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
|
-
);
|
|
465
|
+
result = await syncFiles(filesToPush);
|
|
458
466
|
|
|
459
467
|
if (!result.ok) {
|
|
460
468
|
if (result.status === 403) {
|
|
@@ -469,10 +477,7 @@ async function pushAtris() {
|
|
|
469
477
|
skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
|
|
470
478
|
|
|
471
479
|
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
|
-
);
|
|
480
|
+
const retry = await syncFiles(allowed);
|
|
476
481
|
if (retry.ok) {
|
|
477
482
|
recordSyncResults(allowed, retry);
|
|
478
483
|
pushed = landedPaths.size;
|
|
@@ -490,6 +495,23 @@ async function pushAtris() {
|
|
|
490
495
|
console.error('\n Computer is sleeping. Wake it first.');
|
|
491
496
|
await emit('cold_wake', { error_detail: 'computer sleeping (409)' });
|
|
492
497
|
process.exit(1);
|
|
498
|
+
} else if (shouldRetrySyncIndividually(result, filesToPush)) {
|
|
499
|
+
batchFailureDetail = `${result.status || 'unknown'}: ${result.errorMessage || result.error || 'batch sync failed'}`;
|
|
500
|
+
console.log('');
|
|
501
|
+
console.log(` Batch push failed (${result.errorMessage || result.error || result.status}). Retrying one file at a time...`);
|
|
502
|
+
for (const f of filesToPush) {
|
|
503
|
+
const single = await syncFiles([f]);
|
|
504
|
+
if (single.ok) {
|
|
505
|
+
recordSyncResults([f], single);
|
|
506
|
+
} else {
|
|
507
|
+
failedToLand.push({
|
|
508
|
+
path: f.path,
|
|
509
|
+
status: single.status || 'error',
|
|
510
|
+
error: single.errorMessage || single.error || '',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
pushed = landedPaths.size;
|
|
493
515
|
} else {
|
|
494
516
|
console.error(`\n Push failed: ${result.errorMessage || result.error || result.status}`);
|
|
495
517
|
await emit('status_unknown', { error_detail: `sync status ${result.status}` });
|
|
@@ -631,6 +653,9 @@ async function pushAtris() {
|
|
|
631
653
|
} else if (deleteFailed.length > 0) {
|
|
632
654
|
finalOutcome = 'status_unknown';
|
|
633
655
|
finalDetail = `${deleteFailed.length} delete(s) failed (statuses: ${[...new Set(deleteFailed.map(f => f.status))].join(',')})`;
|
|
656
|
+
} else if (batchFailureDetail) {
|
|
657
|
+
finalOutcome = 'status_unknown';
|
|
658
|
+
finalDetail = `batch sync failed but individual retry landed all files (${batchFailureDetail})`;
|
|
634
659
|
} else if (_rateLimitedDeletes > 0) {
|
|
635
660
|
finalOutcome = 'rate_limited';
|
|
636
661
|
finalDetail = `${_rateLimitedDeletes} delete(s) hit 429 (recovered)`;
|
|
@@ -652,6 +677,7 @@ async function pushAtris() {
|
|
|
652
677
|
module.exports = {
|
|
653
678
|
pushAtris,
|
|
654
679
|
buildPushChangePlan,
|
|
680
|
+
shouldRetrySyncIndividually,
|
|
655
681
|
resolvePushSourceDir,
|
|
656
682
|
canonicalWorkspaceRoot,
|
|
657
683
|
basenameOfManifestPath,
|
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,
|