atris 3.35.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { loadManifest, saveManifest } = require('../lib/manifest');
|
|
5
5
|
const { removeBaseContents, writeBaseContents } = require('../lib/company-brain-sync');
|
|
6
|
+
const { collectCloudOrphans } = require('../commands/cloud');
|
|
6
7
|
|
|
7
8
|
const WATCH_IGNORED_DIRS = new Set([
|
|
8
9
|
'.git', '.atris', '.claude', '.cursor', '.next', '.cache',
|
|
@@ -31,6 +32,8 @@ function parseBusinessSyncArgs(args = []) {
|
|
|
31
32
|
const resolveFlag = parseFlagValue(args, '--resolve', null);
|
|
32
33
|
const resolve = resolveFlag || (resolveIdx !== -1 ? positional[resolveIdx + 1] : null);
|
|
33
34
|
const resolvePath = parseFlagValue(args, '--path', parseFlagValue(args, '--file', null));
|
|
35
|
+
const takeRaw = parseFlagValue(args, '--take', null);
|
|
36
|
+
const take = takeRaw ? String(takeRaw).trim().toLowerCase() : null;
|
|
34
37
|
const commandWords = new Set(['status', 'doctor', 'review', 'resolve', 'local', 'cloud', 'both', 'merge']);
|
|
35
38
|
const slug = positional.find((arg) => !commandWords.has(arg)) || null;
|
|
36
39
|
const dryRun = args.includes('--dry-run');
|
|
@@ -39,9 +42,10 @@ function parseBusinessSyncArgs(args = []) {
|
|
|
39
42
|
const watch = args.includes('--watch');
|
|
40
43
|
const intervalSec = Number.parseInt(parseFlagValue(args, '--interval', '60'), 10);
|
|
41
44
|
const debounceSec = Number.parseInt(parseFlagValue(args, '--debounce', '5'), 10);
|
|
45
|
+
const showOrphans = args.includes('--show-orphans');
|
|
42
46
|
const help = args.includes('--help') || args.includes('-h') || positional[0] === 'help';
|
|
43
47
|
|
|
44
|
-
return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, resolvePath, help };
|
|
48
|
+
return { slug, dryRun, timeout, allowDelete, watch, intervalSec, debounceSec, status, review, resolve, resolvePath, take, showOrphans, help };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function readBusinessSlug(cwd = process.cwd()) {
|
|
@@ -322,6 +326,20 @@ function renderLocalSyncStatus(status) {
|
|
|
322
326
|
} else {
|
|
323
327
|
lines.push(' warnings: none');
|
|
324
328
|
}
|
|
329
|
+
if (status.orphanError !== undefined) {
|
|
330
|
+
lines.push(` orphans: ${status.orphanError}`);
|
|
331
|
+
} else if (status.orphanPaths !== undefined) {
|
|
332
|
+
if (status.orphanPaths.length === 0) {
|
|
333
|
+
lines.push(' orphans: none');
|
|
334
|
+
} else {
|
|
335
|
+
const orphanLabel = status.orphanPaths.length === 1 ? 'file' : 'files';
|
|
336
|
+
lines.push(` orphans: ${status.orphanPaths.length} cloud ${orphanLabel} not present locally`);
|
|
337
|
+
status.orphanPaths.slice(0, 20).forEach((p) => lines.push(` - ${p.replace(/^\//, '')}`));
|
|
338
|
+
if (status.orphanPaths.length > 20) {
|
|
339
|
+
lines.push(` ... +${status.orphanPaths.length - 20} more`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
325
343
|
if (status.heartbeat && status.heartbeat.updated_at) {
|
|
326
344
|
lines.push(` watcher: last heartbeat ${status.heartbeat.updated_at} (${status.heartbeat.state || 'unknown'})`);
|
|
327
345
|
} else {
|
|
@@ -334,15 +352,18 @@ function renderLocalSyncStatus(status) {
|
|
|
334
352
|
|
|
335
353
|
function renderBusinessSyncHelp() {
|
|
336
354
|
return [
|
|
337
|
-
'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--review] [--resolve local|cloud|both|merge] [--path <file>] [--timeout 120]',
|
|
355
|
+
'Usage: atris sync [business] [--dry-run] [--watch] [--status] [--show-orphans] [--review] [--take local|cloud] [--resolve local|cloud|both|merge] [--path <file>] [--timeout 120]',
|
|
338
356
|
'',
|
|
339
357
|
'Safe loop:',
|
|
340
358
|
' Pull -> Review -> Publish',
|
|
341
359
|
'',
|
|
342
360
|
'Commands:',
|
|
343
361
|
' atris sync --status Show local sync health',
|
|
362
|
+
' atris sync --status --show-orphans Show local sync health plus cloud files not present locally',
|
|
344
363
|
' atris sync --dry-run Preview pull and publish plans without writing cloud',
|
|
345
|
-
' atris sync --review
|
|
364
|
+
' atris sync --review List conflicts; pick local / cloud / merge per file',
|
|
365
|
+
' atris sync --review --take local|cloud',
|
|
366
|
+
' Non-interactive: take local or cloud for every conflict',
|
|
346
367
|
' atris sync --resolve cloud|local|merge [--path <file>]',
|
|
347
368
|
' atris sync --watch Keep the workspace live with the same safety gates',
|
|
348
369
|
'',
|
|
@@ -353,6 +374,147 @@ function renderBusinessSyncHelp() {
|
|
|
353
374
|
].join('\n');
|
|
354
375
|
}
|
|
355
376
|
|
|
377
|
+
function readTextIfExists(filePath) {
|
|
378
|
+
try {
|
|
379
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
380
|
+
} catch {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildConflictMarkers(localContent = '', remoteContent = '') {
|
|
386
|
+
const local = String(localContent);
|
|
387
|
+
const remote = String(remoteContent);
|
|
388
|
+
const localBody = local.endsWith('\n') || local === '' ? local : `${local}\n`;
|
|
389
|
+
const remoteBody = remote.endsWith('\n') || remote === '' ? remote : `${remote}\n`;
|
|
390
|
+
return `<<<<<<< local\n${localBody}=======\n${remoteBody}>>>>>>> cloud\n`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function listConflictFiles(entries = []) {
|
|
394
|
+
const lines = [];
|
|
395
|
+
const count = entries.length;
|
|
396
|
+
if (count === 0) {
|
|
397
|
+
lines.push('no sync conflicts need review.');
|
|
398
|
+
return `${lines.join('\n')}\n`;
|
|
399
|
+
}
|
|
400
|
+
lines.push(`${count} file${count === 1 ? '' : 's'} conflict`);
|
|
401
|
+
entries.forEach((entry, index) => {
|
|
402
|
+
lines.push(` ${index + 1}. ${entry.targetRel}`);
|
|
403
|
+
});
|
|
404
|
+
lines.push('');
|
|
405
|
+
lines.push('pick local / cloud / merge per file (interactive), or run:');
|
|
406
|
+
lines.push(' atris sync --review --take local');
|
|
407
|
+
lines.push(' atris sync --review --take cloud');
|
|
408
|
+
lines.push('merge writes standard conflict markers into the file (no .remote dump).');
|
|
409
|
+
return `${lines.join('\n')}\n`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function applyReviewChoice(cwd, entry, choice) {
|
|
413
|
+
const strategy = String(choice || '').trim().toLowerCase();
|
|
414
|
+
if (!['local', 'cloud', 'merge'].includes(strategy)) {
|
|
415
|
+
throw new Error(`unknown choice "${choice}". use local, cloud, or merge.`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const targetPath = assertWorkspaceTarget(cwd, entry.targetRel);
|
|
419
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
420
|
+
|
|
421
|
+
if (strategy === 'merge') {
|
|
422
|
+
const localContent = readTextIfExists(entry.localPath);
|
|
423
|
+
const remoteContent = readTextIfExists(entry.remotePath);
|
|
424
|
+
fs.writeFileSync(targetPath, buildConflictMarkers(localContent, remoteContent), 'utf8');
|
|
425
|
+
} else {
|
|
426
|
+
const sourcePath = strategy === 'local' ? entry.localPath : entry.remotePath;
|
|
427
|
+
if (!fs.existsSync(sourcePath)) {
|
|
428
|
+
throw new Error(`missing ${strategy} artifact for ${entry.targetRel}`);
|
|
429
|
+
}
|
|
430
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Review path never leaves sibling .remote dumps next to workspace files.
|
|
434
|
+
cleanupResolvedConflictSidecars(cwd, entry.targetRel, { keepCloud: false });
|
|
435
|
+
cleanupConflictPacketArtifacts(entry);
|
|
436
|
+
return { path: entry.targetRel, choice: strategy };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function promptReviewChoice(entry, ask) {
|
|
440
|
+
const answer = await ask(`${entry.targetRel}: local / cloud / merge? `);
|
|
441
|
+
const choice = String(answer || '').trim().toLowerCase();
|
|
442
|
+
if (!['local', 'cloud', 'merge'].includes(choice)) {
|
|
443
|
+
throw new Error(`invalid choice "${answer}". use local, cloud, or merge.`);
|
|
444
|
+
}
|
|
445
|
+
return choice;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* S2 conflict handler for `atris sync --review`.
|
|
450
|
+
* Lists each conflicting file, then resolves via interactive prompt or `--take local|cloud`.
|
|
451
|
+
* Merge writes standard conflict markers into the target file (no workspace .remote dump).
|
|
452
|
+
*/
|
|
453
|
+
async function runSyncReview(cwd = process.cwd(), options = {}) {
|
|
454
|
+
const take = options.take ? String(options.take).trim().toLowerCase() : null;
|
|
455
|
+
if (take && !['local', 'cloud'].includes(take)) {
|
|
456
|
+
return {
|
|
457
|
+
resolved: [],
|
|
458
|
+
choices: [],
|
|
459
|
+
message: 'use --take local or --take cloud (merge is interactive only).\n',
|
|
460
|
+
exitCode: 1,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const wantedTarget = normalizeResolvePath(options.path || options.resolvePath || null);
|
|
465
|
+
const entries = collectConflictResolutionEntries(cwd)
|
|
466
|
+
.filter((entry) => !wantedTarget || normalizeResolvePath(entry.targetRel) === wantedTarget);
|
|
467
|
+
|
|
468
|
+
if (entries.length === 0) {
|
|
469
|
+
// Fall back to packet summary when no resolvable .local artifacts exist.
|
|
470
|
+
return {
|
|
471
|
+
resolved: [],
|
|
472
|
+
choices: [],
|
|
473
|
+
message: renderLatestConflictReview(cwd),
|
|
474
|
+
exitCode: 0,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const lines = [];
|
|
479
|
+
lines.push(`${entries.length} file${entries.length === 1 ? '' : 's'} conflict`);
|
|
480
|
+
entries.forEach((entry, index) => {
|
|
481
|
+
lines.push(` ${index + 1}. ${entry.targetRel}`);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const interactive = typeof options.ask === 'function';
|
|
485
|
+
if (!take && !interactive) {
|
|
486
|
+
lines.push('');
|
|
487
|
+
lines.push('pick local / cloud / merge per file (interactive), or run:');
|
|
488
|
+
lines.push(' atris sync --review --take local');
|
|
489
|
+
lines.push(' atris sync --review --take cloud');
|
|
490
|
+
lines.push('merge writes standard conflict markers into the file (no .remote dump).');
|
|
491
|
+
lines.push('');
|
|
492
|
+
lines.push(renderLatestConflictReview(cwd).trimEnd());
|
|
493
|
+
lines.push('');
|
|
494
|
+
return { resolved: [], choices: [], message: `${lines.join('\n')}\n`, exitCode: 0 };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const choices = [];
|
|
498
|
+
const resolved = [];
|
|
499
|
+
for (const entry of entries) {
|
|
500
|
+
const choice = take || await promptReviewChoice(entry, options.ask);
|
|
501
|
+
const result = applyReviewChoice(cwd, entry, choice);
|
|
502
|
+
choices.push(result);
|
|
503
|
+
resolved.push(result.path);
|
|
504
|
+
lines.push(` -> ${result.path}: ${result.choice}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const finalizedManifests = cleanupEmptyConflictPackets(cwd, entries);
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push(`resolved ${resolved.length} conflict${resolved.length === 1 ? '' : 's'}.`);
|
|
510
|
+
if (finalizedManifests.length) {
|
|
511
|
+
lines.push(`updated sync manifest for ${finalizedManifests.join(', ')}.`);
|
|
512
|
+
}
|
|
513
|
+
lines.push('next: run `atris sync --dry-run` before publishing.');
|
|
514
|
+
lines.push('');
|
|
515
|
+
return { resolved, choices, message: `${lines.join('\n')}\n`, exitCode: 0 };
|
|
516
|
+
}
|
|
517
|
+
|
|
356
518
|
function renderLatestConflictReview(cwd = process.cwd()) {
|
|
357
519
|
const summaries = listConflictSummaries(cwd);
|
|
358
520
|
if (summaries.length === 0) {
|
|
@@ -782,7 +944,7 @@ async function runSyncCycle(plan, cwd, options = {}) {
|
|
|
782
944
|
}
|
|
783
945
|
}
|
|
784
946
|
|
|
785
|
-
async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
|
|
947
|
+
async function businessSync(args = process.argv.slice(3), cwd = process.cwd(), deps = {}) {
|
|
786
948
|
const options = resolveBusinessSyncOptions(args, cwd);
|
|
787
949
|
|
|
788
950
|
if (options.help) {
|
|
@@ -791,12 +953,32 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
|
|
|
791
953
|
}
|
|
792
954
|
|
|
793
955
|
if (options.status) {
|
|
794
|
-
|
|
956
|
+
const status = collectLocalSyncStatus(cwd, options);
|
|
957
|
+
if (options.showOrphans) {
|
|
958
|
+
try {
|
|
959
|
+
const { orphanPaths } = await collectCloudOrphans(cwd, options.slug, deps);
|
|
960
|
+
status.orphanPaths = orphanPaths;
|
|
961
|
+
} catch (err) {
|
|
962
|
+
status.orphanError = err.message || String(err);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
process.stdout.write(renderLocalSyncStatus(status));
|
|
795
966
|
return;
|
|
796
967
|
}
|
|
797
968
|
|
|
798
969
|
if (options.review) {
|
|
799
|
-
|
|
970
|
+
let ask = null;
|
|
971
|
+
if (!options.take && process.stdin.isTTY) {
|
|
972
|
+
const { promptUser } = require('../utils/auth');
|
|
973
|
+
ask = (question) => promptUser(question);
|
|
974
|
+
}
|
|
975
|
+
const result = await runSyncReview(cwd, {
|
|
976
|
+
take: options.take,
|
|
977
|
+
path: options.resolvePath,
|
|
978
|
+
ask,
|
|
979
|
+
});
|
|
980
|
+
process.stdout.write(result.message);
|
|
981
|
+
if (result.exitCode && result.exitCode !== 0) process.exit(result.exitCode);
|
|
800
982
|
return;
|
|
801
983
|
}
|
|
802
984
|
|
|
@@ -894,8 +1076,10 @@ async function businessSync(args = process.argv.slice(3), cwd = process.cwd()) {
|
|
|
894
1076
|
}
|
|
895
1077
|
|
|
896
1078
|
module.exports = {
|
|
1079
|
+
applyReviewChoice,
|
|
897
1080
|
businessSync,
|
|
898
1081
|
buildBusinessSyncPlan,
|
|
1082
|
+
buildConflictMarkers,
|
|
899
1083
|
canPreviewPush,
|
|
900
1084
|
collectBrainSnapshot,
|
|
901
1085
|
collectWorkspaceSnapshot,
|
|
@@ -903,6 +1087,7 @@ module.exports = {
|
|
|
903
1087
|
collectLocalSyncStatus,
|
|
904
1088
|
collectConflictResolutionEntries,
|
|
905
1089
|
describeWatchFailure,
|
|
1090
|
+
listConflictFiles,
|
|
906
1091
|
parseBusinessSyncArgs,
|
|
907
1092
|
readBusinessSlug,
|
|
908
1093
|
renderLatestConflictReview,
|
|
@@ -910,6 +1095,7 @@ module.exports = {
|
|
|
910
1095
|
renderBusinessSyncHelp,
|
|
911
1096
|
resolveLatestConflict,
|
|
912
1097
|
resolveBusinessSyncOptions,
|
|
1098
|
+
runSyncReview,
|
|
913
1099
|
safeLineMerge,
|
|
914
1100
|
safeMarkdownMerge,
|
|
915
1101
|
shouldIgnoreWatchPath,
|
package/commands/clean.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
4
5
|
|
|
@@ -250,7 +251,7 @@ function findStaleTasks(atrisDir) {
|
|
|
250
251
|
* Detects both out-of-bounds AND drift (symbol moved to different line)
|
|
251
252
|
* Returns { healed: number, unhealable: array }
|
|
252
253
|
*/
|
|
253
|
-
function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
254
|
+
function healBrokenMapRefs(cwd, atrisDir, dryRun = false, homeDir = os.homedir()) {
|
|
254
255
|
const mapFile = path.join(atrisDir, 'MAP.md');
|
|
255
256
|
if (!fs.existsSync(mapFile)) return { healed: 0, unhealable: [] };
|
|
256
257
|
|
|
@@ -261,12 +262,12 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
261
262
|
// Match both `file.js:123` and `file.js:123-456` with surrounding context
|
|
262
263
|
// [^\S\n] = horizontal whitespace only (no newlines)
|
|
263
264
|
// Required delimiter [(,[,—,-] prevents bleeding into adjacent refs on same line
|
|
264
|
-
const refPattern = /(`?)([a-zA-Z0-9_
|
|
265
|
+
const refPattern = /(`?)([a-zA-Z0-9_~\-./\\]+\.(js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-(\d+))?(`?)([^\S\n]*[\(\[—\-][^\S\n]*([^)\]\n]+))?/g;
|
|
265
266
|
|
|
266
267
|
// Function Inventory form: `symbolName()` → `file:line[-line]`
|
|
267
268
|
// Pre-scan to build a (file:line) → symbol map so refs with the symbol BEFORE them still verify.
|
|
268
269
|
const preRefSymbols = {};
|
|
269
|
-
const preRefPattern = /`([a-zA-Z_][a-zA-Z0-9_]*)\s*\(?\s*\)?`\s*(?:→|->)\s*`([a-zA-Z0-9_
|
|
270
|
+
const preRefPattern = /`([a-zA-Z_][a-zA-Z0-9_]*)\s*\(?\s*\)?`\s*(?:→|->)\s*`([a-zA-Z0-9_~\-./\\]+\.(?:js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-\d+)?`/g;
|
|
270
271
|
let preMatch;
|
|
271
272
|
while ((preMatch = preRefPattern.exec(mapContent)) !== null) {
|
|
272
273
|
preRefSymbols[`${preMatch[2]}:${preMatch[3]}`] = preMatch[1];
|
|
@@ -276,7 +277,10 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
276
277
|
const fileCache = {};
|
|
277
278
|
function readFileCached(filePath) {
|
|
278
279
|
if (!fileCache[filePath]) {
|
|
279
|
-
const
|
|
280
|
+
const expandedPath = filePath.startsWith('~/') || filePath.startsWith('~\\')
|
|
281
|
+
? path.join(homeDir, filePath.slice(2))
|
|
282
|
+
: filePath;
|
|
283
|
+
const fullPath = path.isAbsolute(expandedPath) ? expandedPath : path.join(cwd, expandedPath);
|
|
280
284
|
if (!fs.existsSync(fullPath)) return null;
|
|
281
285
|
try {
|
|
282
286
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
@@ -320,7 +324,15 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
320
324
|
// Find where the symbol actually is now
|
|
321
325
|
const newStart = findSymbolLine(file.content, symbol);
|
|
322
326
|
if (!newStart) {
|
|
323
|
-
|
|
327
|
+
// A "symbol" that appears nowhere in the file is often descriptive
|
|
328
|
+
// prose from the annotation ("PATCH /api/...", "mission drain"), not
|
|
329
|
+
// a code identifier. Suppress those for in-bounds refs to existing
|
|
330
|
+
// files; still flag out-of-bounds refs and vanished identifiers
|
|
331
|
+
// (camelCase/snake_case words were plausibly real symbols).
|
|
332
|
+
const identifierLike = /[a-z][^\s]*[A-Z]|_/.test(symbol);
|
|
333
|
+
if (outOfBounds || identifierLike) {
|
|
334
|
+
unhealable.push({ file: filePath, line: startLine, reason: `Symbol "${symbol}" not found` });
|
|
335
|
+
}
|
|
324
336
|
continue;
|
|
325
337
|
}
|
|
326
338
|
|
|
@@ -404,28 +416,30 @@ function findFunctionEnd(lines, startLine) {
|
|
|
404
416
|
function extractSymbol(context) {
|
|
405
417
|
if (!context) return null;
|
|
406
418
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
419
|
+
const original = context;
|
|
420
|
+
|
|
421
|
+
// Clean up the context without stripping parentheses from a symbol ending in ().
|
|
422
|
+
const unquoted = context.trim().replace(/`/g, '');
|
|
423
|
+
const wrapper = unquoted.match(/^([\(\[])/)?.[1];
|
|
424
|
+
let cleaned = unquoted
|
|
425
|
+
.replace(/^[\(\[—\-:]+\s*/, '')
|
|
412
426
|
.trim();
|
|
427
|
+
if (wrapper === '(') cleaned = cleaned.replace(/\)$/, '').trim();
|
|
428
|
+
if (wrapper === '[') cleaned = cleaned.replace(/\]$/, '').trim();
|
|
413
429
|
|
|
414
430
|
if (!cleaned) return null;
|
|
415
431
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const identifierMatch = cleaned.match(/\b([a-zA-Z_][a-zA-Z0-9_]*(?:Atris|Entry|Handler|Cmd|Function|Class)?)\b/i);
|
|
432
|
+
const tokenMatch = cleaned.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\(\))?/);
|
|
433
|
+
if (!tokenMatch) return null;
|
|
419
434
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
435
|
+
const symbol = tokenMatch[1];
|
|
436
|
+
const token = tokenMatch[0];
|
|
437
|
+
const codeShaped = symbol.includes('_') || /[a-z][A-Z]/.test(symbol) || token.endsWith('()');
|
|
438
|
+
if (codeShaped) return symbol;
|
|
423
439
|
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
427
|
-
return firstWord;
|
|
428
|
-
}
|
|
440
|
+
const isSingleWord = cleaned === symbol;
|
|
441
|
+
const isBackticked = original.includes(`\`${symbol}\``);
|
|
442
|
+
if (isSingleWord && isBackticked) return symbol;
|
|
429
443
|
|
|
430
444
|
return null;
|
|
431
445
|
}
|
|
@@ -445,7 +459,7 @@ function findSymbolLine(fileContent, symbol) {
|
|
|
445
459
|
new RegExp(`^\\s*class\\s+${esc}\\b`), // class name
|
|
446
460
|
new RegExp(`^\\s*${esc}\\s*[:(]`), // name: or name(
|
|
447
461
|
new RegExp(`exports\\.${esc}\\s*=`), // exports.name =
|
|
448
|
-
new RegExp(`^\\s*def\\s+${esc}\\s*\\(`),
|
|
462
|
+
new RegExp(`^\\s*(async\\s+)?def\\s+${esc}\\s*\\(`), // [async] def name( (Python)
|
|
449
463
|
];
|
|
450
464
|
|
|
451
465
|
for (const pattern of patterns) {
|
|
@@ -465,15 +479,27 @@ function findSymbolLine(fileContent, symbol) {
|
|
|
465
479
|
*/
|
|
466
480
|
function findStalePages(cwd, atrisDir) {
|
|
467
481
|
const stalePages = [];
|
|
482
|
+
const skippedDirs = new Set(['archive', 'runs', 'qa', 'node_modules', '.git']);
|
|
483
|
+
const currentYear = new Date().getFullYear();
|
|
484
|
+
|
|
485
|
+
function shouldSkipDir(dir, name) {
|
|
486
|
+
if (skippedDirs.has(name)) return true;
|
|
487
|
+
if (/^\d{4}$/.test(name) && path.basename(path.dirname(dir)) === 'logs') {
|
|
488
|
+
return Number(name) < currentYear;
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
468
492
|
|
|
469
493
|
function scanDir(dir) {
|
|
470
494
|
if (!fs.existsSync(dir)) return;
|
|
471
495
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
472
496
|
for (const entry of entries) {
|
|
473
497
|
const fullPath = path.join(dir, entry.name);
|
|
474
|
-
if (entry.isDirectory()
|
|
498
|
+
if (entry.isDirectory()) {
|
|
499
|
+
if (shouldSkipDir(fullPath, entry.name)) continue;
|
|
475
500
|
scanDir(fullPath);
|
|
476
|
-
} else if (entry.isFile()
|
|
501
|
+
} else if (entry.isFile()) {
|
|
502
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
477
503
|
const result = checkPageStaleness(cwd, fullPath);
|
|
478
504
|
if (result) stalePages.push(result);
|
|
479
505
|
}
|