atris 3.40.0 → 3.42.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/bin/atris.js +13 -1
- package/commands/activate.js +1 -4
- package/commands/computer.js +20 -0
- package/commands/improve.js +221 -0
- package/commands/mission.js +2 -1
- package/commands/site-deploy.js +19 -4
- package/commands/slop.js +2 -1
- package/commands/stream.js +2 -1
- package/commands/strings.js +2 -1
- package/commands/team.js +95 -2
- package/lib/lesson-preflight.js +2 -1
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -1709,6 +1709,18 @@ function showWelcomeVisualization() {
|
|
|
1709
1709
|
console.log(row('tidy', tidyBits.join(', ')));
|
|
1710
1710
|
}
|
|
1711
1711
|
|
|
1712
|
+
// The guarantee gauge, only when it has bad news: landings a human had to
|
|
1713
|
+
// fix this week. Zero is the healthy state and stays silent; no git history
|
|
1714
|
+
// (not a repo, git missing) also stays silent.
|
|
1715
|
+
try {
|
|
1716
|
+
const rev = require('../commands/improve').collectRevisionSignals(cwd, { days: 7 });
|
|
1717
|
+
if (rev.revised > 0) {
|
|
1718
|
+
console.log(` ${rev.revised} landing${rev.revised === 1 ? '' : 's'} this week needed a human fix; run atris improve revisions`);
|
|
1719
|
+
}
|
|
1720
|
+
} catch {
|
|
1721
|
+
// Silent: the gauge never blocks boot.
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1712
1724
|
let wikiNudge = '';
|
|
1713
1725
|
try {
|
|
1714
1726
|
wikiNudge = require('../lib/wiki').wikiMetabolismNudge(cwd);
|
|
@@ -2732,7 +2744,7 @@ if (command === 'init') {
|
|
|
2732
2744
|
const subcommand = process.argv[3];
|
|
2733
2745
|
const args = process.argv.slice(4);
|
|
2734
2746
|
require('../commands/business').businessCommand(subcommand, ...args)
|
|
2735
|
-
.then(() => process.exit(0))
|
|
2747
|
+
.then(() => process.exit(process.exitCode || 0))
|
|
2736
2748
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2737
2749
|
} else if (command === 'soul') {
|
|
2738
2750
|
const args = process.argv.slice(3);
|
package/commands/activate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
3
4
|
const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
|
|
4
5
|
const { detectWorkspaceState, loadContext } = require('../lib/state-detection');
|
|
5
6
|
const { readWikiStatus } = require('../lib/wiki');
|
|
@@ -13,10 +14,6 @@ const CLARITY_FIELDS = [
|
|
|
13
14
|
{ key: 'leash', label: 'Leash' },
|
|
14
15
|
];
|
|
15
16
|
|
|
16
|
-
function escapeRegExp(value) {
|
|
17
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
17
|
function readClarityMarkdownProfile(mdPath) {
|
|
21
18
|
try {
|
|
22
19
|
const content = fs.readFileSync(mdPath, 'utf8');
|
package/commands/computer.js
CHANGED
|
@@ -4384,6 +4384,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
4384
4384
|
default:
|
|
4385
4385
|
console.error(`Unknown subcommand: ${sub}`);
|
|
4386
4386
|
console.log('Run: atris computer --help');
|
|
4387
|
+
process.exitCode = 1;
|
|
4387
4388
|
}
|
|
4388
4389
|
}
|
|
4389
4390
|
|
|
@@ -4399,4 +4400,23 @@ module.exports = {
|
|
|
4399
4400
|
extractAttachedWorkspaceMismatch,
|
|
4400
4401
|
contextForAttachedWorkspaceMismatch,
|
|
4401
4402
|
printRecruitingLocalSyncOutcome,
|
|
4403
|
+
// Hermetic parsing/formatting layer, exported for test/computer.test.js.
|
|
4404
|
+
parseComputerOptions,
|
|
4405
|
+
parseComputerCreateArgs,
|
|
4406
|
+
computerCreateArgsHaveName,
|
|
4407
|
+
normalizeComputerType,
|
|
4408
|
+
formatComputerTypeList,
|
|
4409
|
+
parseComputerDeleteArgs,
|
|
4410
|
+
parseComputerCardArgs,
|
|
4411
|
+
renderComputerCard,
|
|
4412
|
+
renderComputerCardMarkdown,
|
|
4413
|
+
formatLeaseAge,
|
|
4414
|
+
formatWorkspaceRef,
|
|
4415
|
+
workspaceMatchesInput,
|
|
4416
|
+
resolveWorkspaceFromList,
|
|
4417
|
+
workspaceMatchesComputerType,
|
|
4418
|
+
looksLikeWorkspaceId,
|
|
4419
|
+
shellQuote,
|
|
4420
|
+
withoutRecruitingWrapperFlags,
|
|
4421
|
+
formatCloudSelection,
|
|
4402
4422
|
};
|
package/commands/improve.js
CHANGED
|
@@ -348,6 +348,13 @@ function plural(n, word) {
|
|
|
348
348
|
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// Spell small counts as words so the vitals read as prose; digits above nine.
|
|
352
|
+
const SMALL_NUMBER_WORDS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
|
|
353
|
+
|
|
354
|
+
function countWord(n) {
|
|
355
|
+
return n >= 0 && n < SMALL_NUMBER_WORDS.length ? SMALL_NUMBER_WORDS[n] : String(n);
|
|
356
|
+
}
|
|
357
|
+
|
|
351
358
|
function formatReward(value) {
|
|
352
359
|
const n = Number(value) || 0;
|
|
353
360
|
if (Number.isInteger(n)) return String(n);
|
|
@@ -505,6 +512,26 @@ function collectImproveVitals(options = {}, deps = {}) {
|
|
|
505
512
|
sentence: plainSentence(usageSentence),
|
|
506
513
|
};
|
|
507
514
|
|
|
515
|
+
// The guarantee gauge: agent landings vs human fixes over the last 14 days.
|
|
516
|
+
// Collection is bounded by --since so a huge repo only walks a fortnight.
|
|
517
|
+
// No git history (not a repo, git missing) means the line is omitted silently.
|
|
518
|
+
let guarantee = null;
|
|
519
|
+
try {
|
|
520
|
+
const collect = deps.collectRevisionSignals || collectRevisionSignals;
|
|
521
|
+
const rev = collect(root, { days: 14, now: nowMs });
|
|
522
|
+
const landingsPhrase = `${countWord(rev.landings)} landing${rev.landings === 1 ? '' : 's'} this fortnight`;
|
|
523
|
+
const fixPhrase = `${countWord(rev.revised)} needed a human fix`;
|
|
524
|
+
guarantee = {
|
|
525
|
+
days: rev.days,
|
|
526
|
+
landings: rev.landings,
|
|
527
|
+
revised: rev.revised,
|
|
528
|
+
rate: rev.rate,
|
|
529
|
+
sentence: plainSentence(`${landingsPhrase}, ${fixPhrase}.`),
|
|
530
|
+
};
|
|
531
|
+
} catch {
|
|
532
|
+
guarantee = null;
|
|
533
|
+
}
|
|
534
|
+
|
|
508
535
|
const sentences = [
|
|
509
536
|
heartbeat.sentence,
|
|
510
537
|
exploit.sentence,
|
|
@@ -512,6 +539,7 @@ function collectImproveVitals(options = {}, deps = {}) {
|
|
|
512
539
|
excrete.sentence,
|
|
513
540
|
...(topOverdueSentence ? [`the top overdue loop says ${topOverdueSentence}`] : []),
|
|
514
541
|
usage.sentence,
|
|
542
|
+
...(guarantee ? [guarantee.sentence] : []),
|
|
515
543
|
];
|
|
516
544
|
const groups = [
|
|
517
545
|
[heartbeat.sentence, installNudge].filter(Boolean),
|
|
@@ -519,6 +547,7 @@ function collectImproveVitals(options = {}, deps = {}) {
|
|
|
519
547
|
[explore.sentence],
|
|
520
548
|
[excrete.sentence, ...(topOverdueSentence ? [`the top overdue loop says ${topOverdueSentence}`] : [])],
|
|
521
549
|
[usage.sentence],
|
|
550
|
+
...(guarantee ? [[guarantee.sentence]] : []),
|
|
522
551
|
];
|
|
523
552
|
|
|
524
553
|
return {
|
|
@@ -529,6 +558,7 @@ function collectImproveVitals(options = {}, deps = {}) {
|
|
|
529
558
|
explore,
|
|
530
559
|
excrete,
|
|
531
560
|
usage,
|
|
561
|
+
guarantee,
|
|
532
562
|
install_nudge: installNudge,
|
|
533
563
|
sentences,
|
|
534
564
|
groups,
|
|
@@ -887,6 +917,190 @@ function formatImproveReport(result = {}) {
|
|
|
887
917
|
return lines.join('\n');
|
|
888
918
|
}
|
|
889
919
|
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
// atris improve revisions — the gauge for the north-star metric:
|
|
922
|
+
// operator revisions after landing = 0. an agent landing is a commit carrying
|
|
923
|
+
// the atris co-author trailer (atris-builder[bot]); if a human commit touches
|
|
924
|
+
// any of the same files within 72 hours, that landing failed the guarantee.
|
|
925
|
+
//
|
|
926
|
+
// renames are NOT followed: `git log --follow` is per-file and would cost one
|
|
927
|
+
// subprocess per file per landing, so a post-landing rename reads as "no
|
|
928
|
+
// overlap". that undercounts revisions slightly; accepted on purpose.
|
|
929
|
+
|
|
930
|
+
const REVISIONS_SCHEMA = 'atris.improve_revisions.v1';
|
|
931
|
+
const REVISION_WINDOW_HOURS = 72;
|
|
932
|
+
const REVISION_WINDOW_MS = REVISION_WINDOW_HOURS * 60 * 60 * 1000;
|
|
933
|
+
const AGENT_TRAILER_MARKER = 'atris-builder[bot]';
|
|
934
|
+
const DEFAULT_REVISIONS_DAYS = 14;
|
|
935
|
+
|
|
936
|
+
function parseRevisionsArgs(argv = []) {
|
|
937
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
938
|
+
const opts = { days: DEFAULT_REVISIONS_DAYS, json: false, help: false };
|
|
939
|
+
for (let i = 0; i < args.length; i++) {
|
|
940
|
+
const a = args[i];
|
|
941
|
+
if (a === '--help' || a === '-h') { opts.help = true; continue; }
|
|
942
|
+
if (a === '--json') { opts.json = true; continue; }
|
|
943
|
+
if (a === '--days') { const v = Number(args[++i]); if (Number.isFinite(v) && v > 0) opts.days = Math.round(v); continue; }
|
|
944
|
+
if (a.startsWith('--days=')) { const v = Number(a.split('=')[1]); if (Number.isFinite(v) && v > 0) opts.days = Math.round(v); continue; }
|
|
945
|
+
}
|
|
946
|
+
return opts;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function gitLines(cwdRoot, gitArgs) {
|
|
950
|
+
const r = spawnSync('git', gitArgs, { cwd: cwdRoot, encoding: 'utf8' });
|
|
951
|
+
if (r.status !== 0) {
|
|
952
|
+
const err = new Error(String(r.stderr || `git ${gitArgs[0]} exited ${r.status}`).trim());
|
|
953
|
+
err.gitFailed = true;
|
|
954
|
+
throw err;
|
|
955
|
+
}
|
|
956
|
+
return String(r.stdout || '');
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Files changed by one commit. Merge commits are attributed by their
|
|
961
|
+
* first-parent diff (what the merge actually brought onto the mainline);
|
|
962
|
+
* plain commits use diff-tree. Root commits list their initial files.
|
|
963
|
+
*/
|
|
964
|
+
function commitFiles(cwdRoot, commit) {
|
|
965
|
+
const parents = commit.parents;
|
|
966
|
+
const out = parents.length >= 2
|
|
967
|
+
? gitLines(cwdRoot, ['diff', '--name-only', `${commit.hash}^1`, commit.hash])
|
|
968
|
+
: gitLines(cwdRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', '--root', commit.hash]);
|
|
969
|
+
return out.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Read the last N days of history and pair every agent landing with the
|
|
974
|
+
* human commits that touched the same files within the 72-hour window.
|
|
975
|
+
*/
|
|
976
|
+
function collectRevisionSignals(root, options = {}) {
|
|
977
|
+
const days = Number.isFinite(options.days) && options.days > 0 ? Math.round(options.days) : DEFAULT_REVISIONS_DAYS;
|
|
978
|
+
const nowMs = options.now != null ? new Date(options.now).getTime() : Date.now();
|
|
979
|
+
const sinceIso = new Date(nowMs - days * DAY_MS).toISOString();
|
|
980
|
+
|
|
981
|
+
let raw = '';
|
|
982
|
+
try {
|
|
983
|
+
raw = gitLines(root, ['log', `--since=${sinceIso}`, '--date=iso-strict', '--pretty=format:%H%x1f%P%x1f%aI%x1f%s%x1f%B%x1e']);
|
|
984
|
+
} catch (e) {
|
|
985
|
+
// a repo with no commits yet exits non-zero on `git log`; that is the
|
|
986
|
+
// empty-history case, not an error. anything else (not a repo) rethrows.
|
|
987
|
+
if (!/does not have any commits|bad default revision|unknown revision/i.test(e.message)) throw e;
|
|
988
|
+
raw = '';
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const commits = raw.split('\x1e')
|
|
992
|
+
.map((chunk) => chunk.replace(/^\n/, ''))
|
|
993
|
+
.filter((chunk) => chunk.trim())
|
|
994
|
+
.map((chunk) => {
|
|
995
|
+
const [hash, parents, at, subject, body] = chunk.split('\x1f');
|
|
996
|
+
return {
|
|
997
|
+
hash: String(hash || '').trim(),
|
|
998
|
+
parents: String(parents || '').trim().split(/\s+/).filter(Boolean),
|
|
999
|
+
at: String(at || '').trim(),
|
|
1000
|
+
ms: timestampMs(at),
|
|
1001
|
+
subject: String(subject || '').trim(),
|
|
1002
|
+
isAgent: String(body || '').includes(AGENT_TRAILER_MARKER),
|
|
1003
|
+
};
|
|
1004
|
+
})
|
|
1005
|
+
.filter((c) => c.hash && c.ms != null);
|
|
1006
|
+
|
|
1007
|
+
const landings = commits.filter((c) => c.isAgent);
|
|
1008
|
+
// A merge that carries agent work onto the mainline is a landing action,
|
|
1009
|
+
// not a human correction; counting merges as revisions made the first live
|
|
1010
|
+
// reading say 85 percent on a healthy repo. Only single-parent human
|
|
1011
|
+
// commits count as revision signals.
|
|
1012
|
+
const humans = commits.filter((c) => !c.isAgent && c.parents.length < 2);
|
|
1013
|
+
|
|
1014
|
+
const filesCache = new Map();
|
|
1015
|
+
const filesOf = (commit) => {
|
|
1016
|
+
if (!filesCache.has(commit.hash)) filesCache.set(commit.hash, commitFiles(root, commit));
|
|
1017
|
+
return filesCache.get(commit.hash);
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const revisions = [];
|
|
1021
|
+
for (const landing of landings) {
|
|
1022
|
+
const landedFiles = new Set(filesOf(landing));
|
|
1023
|
+
if (!landedFiles.size) continue;
|
|
1024
|
+
const revisedBy = [];
|
|
1025
|
+
const touched = new Set();
|
|
1026
|
+
for (const human of humans) {
|
|
1027
|
+
if (human.ms <= landing.ms || human.ms > landing.ms + REVISION_WINDOW_MS) continue;
|
|
1028
|
+
const overlap = filesOf(human).filter((f) => landedFiles.has(f));
|
|
1029
|
+
if (!overlap.length) continue;
|
|
1030
|
+
revisedBy.push({ hash: human.hash, subject: human.subject, at: human.at });
|
|
1031
|
+
overlap.forEach((f) => touched.add(f));
|
|
1032
|
+
}
|
|
1033
|
+
if (revisedBy.length) {
|
|
1034
|
+
revisions.push({
|
|
1035
|
+
landing: { hash: landing.hash, subject: landing.subject, at: landing.at },
|
|
1036
|
+
revised_by: revisedBy,
|
|
1037
|
+
files: [...touched].sort(),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return {
|
|
1043
|
+
schema: REVISIONS_SCHEMA,
|
|
1044
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
1045
|
+
days,
|
|
1046
|
+
window_hours: REVISION_WINDOW_HOURS,
|
|
1047
|
+
landings: landings.length,
|
|
1048
|
+
revised: revisions.length,
|
|
1049
|
+
rate: landings.length ? revisions.length / landings.length : 0,
|
|
1050
|
+
revisions,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function listFilesPhrase(files = []) {
|
|
1055
|
+
const shown = files.slice(0, 3);
|
|
1056
|
+
const rest = files.length - shown.length;
|
|
1057
|
+
return rest > 0 ? `${shown.join(', ')} and ${plural(rest, 'more file')}` : shown.join(', ');
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function formatRevisionsReport(summary = {}) {
|
|
1061
|
+
const days = summary.days || DEFAULT_REVISIONS_DAYS;
|
|
1062
|
+
if (!summary.landings) {
|
|
1063
|
+
return `no agent landings found in the last ${plural(days, 'day')}. nothing to measure yet.`;
|
|
1064
|
+
}
|
|
1065
|
+
const lines = [];
|
|
1066
|
+
lines.push(`agent landings in the last ${plural(days, 'day')}: ${summary.landings}.`);
|
|
1067
|
+
lines.push(`landings a human then revised: ${summary.revised}.`);
|
|
1068
|
+
lines.push(`revision rate: ${Math.round((summary.rate || 0) * 100)} percent. the target is zero.`);
|
|
1069
|
+
for (const item of Array.isArray(summary.revisions) ? summary.revisions : []) {
|
|
1070
|
+
lines.push('');
|
|
1071
|
+
lines.push(`an agent landed "${plainSentence(item.landing.subject)}". a human then changed ${listFilesPhrase(item.files)} within ${REVISION_WINDOW_HOURS} hours.`);
|
|
1072
|
+
}
|
|
1073
|
+
return lines.join('\n');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function showRevisionsHelp() {
|
|
1077
|
+
console.log(`atris improve revisions - measure operator revisions after landing
|
|
1078
|
+
|
|
1079
|
+
Usage:
|
|
1080
|
+
atris improve revisions [--days N] [--json]
|
|
1081
|
+
|
|
1082
|
+
Reads git history for the last N days (default ${DEFAULT_REVISIONS_DAYS}). A commit with the
|
|
1083
|
+
atris co-author trailer is an agent landing; a later human commit touching
|
|
1084
|
+
the same files within ${REVISION_WINDOW_HOURS} hours is a revision signal. The north-star
|
|
1085
|
+
metric is a revision rate of zero.`);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function runRevisions(argv = []) {
|
|
1089
|
+
const opts = parseRevisionsArgs(argv);
|
|
1090
|
+
if (opts.help) { showRevisionsHelp(); return 0; }
|
|
1091
|
+
let summary;
|
|
1092
|
+
try {
|
|
1093
|
+
summary = collectRevisionSignals(process.cwd(), { days: opts.days });
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
console.log('this folder has no readable git history, so there are no landings to measure.');
|
|
1096
|
+
if (!e.gitFailed) console.error(` ${e.message}`);
|
|
1097
|
+
return 1;
|
|
1098
|
+
}
|
|
1099
|
+
if (opts.json) console.log(JSON.stringify(summary));
|
|
1100
|
+
else console.log(formatRevisionsReport(summary));
|
|
1101
|
+
return 0;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
890
1104
|
function showHelp() {
|
|
891
1105
|
console.log(`atris improve - show the self-improvement metabolism vitals
|
|
892
1106
|
|
|
@@ -894,6 +1108,7 @@ Usage:
|
|
|
894
1108
|
atris improve
|
|
895
1109
|
atris improve --json
|
|
896
1110
|
atris improve doctor [--json] [--fix] [--check <kind>]
|
|
1111
|
+
atris improve revisions [--days N] [--json]
|
|
897
1112
|
atris improve tick [mode] [options]
|
|
898
1113
|
atris improve [mode|history] [options]
|
|
899
1114
|
|
|
@@ -903,6 +1118,7 @@ Modes (positional or --mode):
|
|
|
903
1118
|
delegate queue the tick for a local Claude Code session
|
|
904
1119
|
history show the tick history (reward trend, credits, pass rate)
|
|
905
1120
|
doctor scan loop receipts and optionally file one repair mission
|
|
1121
|
+
revisions measure operator revisions after agent landings (target: zero)
|
|
906
1122
|
|
|
907
1123
|
Options:
|
|
908
1124
|
--member <name> attribute the tick to a member (the loop's owner)
|
|
@@ -1074,6 +1290,9 @@ async function run(argv = [], deps = {}) {
|
|
|
1074
1290
|
const result = runLoopDoctor(args.slice(1), deps);
|
|
1075
1291
|
return result.check && !result.check.ok ? 1 : 0;
|
|
1076
1292
|
}
|
|
1293
|
+
if (args[0] === 'revisions') {
|
|
1294
|
+
return runRevisions(args.slice(1));
|
|
1295
|
+
}
|
|
1077
1296
|
if (isBareVitalsArgs(args)) {
|
|
1078
1297
|
const vitals = (deps.collectImproveVitals || collectImproveVitals)({ workspace: process.cwd() }, deps);
|
|
1079
1298
|
if (args.includes('--json')) console.log(JSON.stringify(vitals));
|
|
@@ -1129,6 +1348,8 @@ module.exports = {
|
|
|
1129
1348
|
formatTickHistory,
|
|
1130
1349
|
improveApiPath,
|
|
1131
1350
|
runLoopDoctor,
|
|
1351
|
+
collectRevisionSignals,
|
|
1352
|
+
formatRevisionsReport,
|
|
1132
1353
|
runLocalFallback,
|
|
1133
1354
|
summarizeLocalMissionRun,
|
|
1134
1355
|
LOCAL_FALLBACK_ARGS,
|
package/commands/mission.js
CHANGED
|
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
9
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
9
10
|
const {
|
|
10
11
|
appendBriefRecord,
|
|
11
12
|
stampBriefOutcome,
|
|
@@ -1901,7 +1902,7 @@ function missionLastLanding(mission, root = process.cwd()) {
|
|
|
1901
1902
|
|
|
1902
1903
|
function missionLandingChangedIsGenericTick(mission, changed) {
|
|
1903
1904
|
const text = String(changed || '').trim();
|
|
1904
|
-
const objective =
|
|
1905
|
+
const objective = escapeRegExp(mission?.objective || '');
|
|
1905
1906
|
if (!objective) return false;
|
|
1906
1907
|
return new RegExp(`^${objective} recorded tick \\d+\\.$`).test(text);
|
|
1907
1908
|
}
|
package/commands/site-deploy.js
CHANGED
|
@@ -221,10 +221,21 @@ function parseResponseData(result) {
|
|
|
221
221
|
try { return JSON.parse(text); } catch { return null; }
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function validationDetail(items) {
|
|
225
|
+
const messages = items.map((item) => {
|
|
226
|
+
if (!item || typeof item !== 'object') return String(item);
|
|
227
|
+
const message = item.msg || item.message || item.type || 'validation failed';
|
|
228
|
+
const location = Array.isArray(item.loc) ? item.loc.join('.') : '';
|
|
229
|
+
return location ? `${location}: ${message}` : message;
|
|
230
|
+
});
|
|
231
|
+
return messages.join('; ');
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
function errorDetail(result) {
|
|
225
235
|
const data = parseResponseData(result);
|
|
226
236
|
if (data && typeof data === 'object') {
|
|
227
237
|
const detail = data.detail || data.error || data.message;
|
|
238
|
+
if (Array.isArray(detail)) return validationDetail(detail);
|
|
228
239
|
if (detail) return typeof detail === 'string' ? detail : JSON.stringify(detail);
|
|
229
240
|
}
|
|
230
241
|
return responseText(result).trim() || 'request failed';
|
|
@@ -314,9 +325,13 @@ async function uploadPages(sitesUrl, slug, pages, token, deps = {}) {
|
|
|
314
325
|
const url = `${sitesUrl}/${slug}/pages`;
|
|
315
326
|
for (let start = 0; start < pages.length; start += BATCH_SIZE) {
|
|
316
327
|
const batch = pages.slice(start, start + BATCH_SIZE);
|
|
317
|
-
const result = await requestJson(
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
const result = await requestJson(
|
|
329
|
+
'PUT',
|
|
330
|
+
url,
|
|
331
|
+
token,
|
|
332
|
+
batch.map((page) => pagePayload(page, fileSystem)),
|
|
333
|
+
deps,
|
|
334
|
+
);
|
|
320
335
|
if (result.status < 200 || result.status >= 300) throw requestError('PUT', url, result);
|
|
321
336
|
for (const page of batch) log(` published ${page.path} (${formatBytes(page.size)})`);
|
|
322
337
|
}
|
|
@@ -846,13 +861,13 @@ async function run(argv, deps = {}) {
|
|
|
846
861
|
log(`\n deploying ${pages.length} file${pages.length === 1 ? '' : 's'} to ${liveUrl}`);
|
|
847
862
|
try {
|
|
848
863
|
await createSite(sitesUrl, options.name, options.spa, credentials.token, { ...deps, log });
|
|
864
|
+
await registerSubdomain(options.name, { ...deps, log });
|
|
849
865
|
await uploadPages(sitesUrl, options.name, pages, credentials.token, { ...deps, log });
|
|
850
866
|
} catch (error) {
|
|
851
867
|
errorLog(` deploy failed: ${error.message}`);
|
|
852
868
|
return 1;
|
|
853
869
|
}
|
|
854
870
|
|
|
855
|
-
await registerSubdomain(options.name, { ...deps, log });
|
|
856
871
|
log(`\n live at ${liveUrl}`);
|
|
857
872
|
return 0;
|
|
858
873
|
}
|
package/commands/slop.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { execFileSync } = require('child_process');
|
|
21
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
21
22
|
|
|
22
23
|
const SCAN_EXTS = new Set(['.css', '.scss', '.sass', '.less', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte', '.astro',
|
|
23
24
|
'.md', '.mdx', '.txt']); // prose too: the voice doctrine (em-dash, hype-copy) is enforceable, not just advice
|
|
@@ -418,7 +419,7 @@ function findDeadCode(root = process.cwd(), opts = {}) {
|
|
|
418
419
|
const allRepoJs = [...new Set([...listJsFiles(root)])];
|
|
419
420
|
const mentionedBy = (file, predicate) => {
|
|
420
421
|
const stem = path.basename(file).replace(/\.(js|mjs|cjs)$/, '');
|
|
421
|
-
const re = new RegExp(`['"\`/]${stem
|
|
422
|
+
const re = new RegExp(`['"\`/]${escapeRegExp(stem)}(?:\\.js)?['"\`]`);
|
|
422
423
|
return allRepoJs.some((f) => {
|
|
423
424
|
if (f === file || !predicate(f)) return false;
|
|
424
425
|
try { return re.test(fs.readFileSync(f, 'utf8')); } catch { return false; }
|
package/commands/stream.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
6
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
6
7
|
const { runGit: spawnGit } = require('../lib/git-spawn');
|
|
7
8
|
const { collectBoard } = require('./land');
|
|
8
9
|
const { listWorktrees } = require('./worktree');
|
|
@@ -236,7 +237,7 @@ function sanitizeAgent(value) {
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
function stripLeadingAgent(summary, agent) {
|
|
239
|
-
const cleanAgent =
|
|
240
|
+
const cleanAgent = escapeRegExp(agent || '');
|
|
240
241
|
if (!cleanAgent) return summary;
|
|
241
242
|
return String(summary || '').replace(new RegExp(`^${cleanAgent}\\s+`, 'i'), '');
|
|
242
243
|
}
|
package/commands/strings.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const { gitChangedLines } = require('./slop'); // reuse the diff parser — DRY
|
|
25
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
25
26
|
|
|
26
27
|
const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.vue', '.svelte', '.astro', '.html']);
|
|
27
28
|
const TEXT_EXTS = new Set([...CODE_EXTS, '.md', '.mdx', '.txt']);
|
|
@@ -232,7 +233,7 @@ function check(argv) {
|
|
|
232
233
|
}
|
|
233
234
|
const regAbs = path.resolve(process.cwd(), REGISTRY_FILE);
|
|
234
235
|
|
|
235
|
-
const matchers = reg.terms.map((t) => ({ ...t, re: new RegExp(`\\b${t.ban
|
|
236
|
+
const matchers = reg.terms.map((t) => ({ ...t, re: new RegExp(`\\b${escapeRegExp(t.ban)}\\b`, 'i') }));
|
|
236
237
|
const findings = [];
|
|
237
238
|
for (const file of files) {
|
|
238
239
|
if (path.resolve(file) === regAbs) continue; // never flag the registry itself
|
package/commands/team.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
|
|
5
6
|
const { canonicalEngineName } = require('../lib/engine-registry');
|
|
@@ -115,12 +116,83 @@ function renderTeamRoster(roster) {
|
|
|
115
116
|
.join('\n');
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// The pruning pass keeps the team lean like a real company: it flags members
|
|
120
|
+
// with no recent signal, and it never deletes anything. A signal is the newest
|
|
121
|
+
// of MEMBER.md, any logs/*.md, or a mission the member owns that is still
|
|
122
|
+
// active or running.
|
|
123
|
+
const PRUNE_ACTIVE_MISSION_STATUSES = new Set(['active', 'running']);
|
|
124
|
+
const DEFAULT_PRUNE_DAYS = 30;
|
|
125
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
126
|
+
|
|
127
|
+
function newestSignalMs(member) {
|
|
128
|
+
const times = [];
|
|
129
|
+
const stamp = (file) => {
|
|
130
|
+
try { times.push(fs.statSync(file).mtimeMs); } catch { /* missing file is just no signal */ }
|
|
131
|
+
};
|
|
132
|
+
if (member?.path) stamp(member.path);
|
|
133
|
+
if (member?.dir) {
|
|
134
|
+
const logsDir = path.join(member.dir, 'logs');
|
|
135
|
+
let entries = [];
|
|
136
|
+
try { entries = fs.readdirSync(logsDir); } catch { entries = []; }
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.endsWith('.md')) stamp(path.join(logsDir, entry));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return times.length ? Math.max(...times) : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectTeamPrune(deps = {}) {
|
|
145
|
+
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
146
|
+
const days = Number.isFinite(deps.days) && deps.days > 0 ? deps.days : DEFAULT_PRUNE_DAYS;
|
|
147
|
+
const nowMs = typeof deps.now === 'function' ? deps.now() : Date.now();
|
|
148
|
+
const activeOwners = new Set();
|
|
149
|
+
for (const mission of collectMissions(root, deps)) {
|
|
150
|
+
if (!PRUNE_ACTIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) continue;
|
|
151
|
+
const owner = String(mission?.owner || mission?.member || '').trim().toLowerCase();
|
|
152
|
+
if (owner) activeOwners.add(owner);
|
|
153
|
+
}
|
|
154
|
+
const quiet = [];
|
|
155
|
+
let activeCount = 0;
|
|
156
|
+
for (const member of collectMembers(root, deps)) {
|
|
157
|
+
const name = String(member?.name || '').trim().toLowerCase();
|
|
158
|
+
if (!name) continue;
|
|
159
|
+
const signalMs = newestSignalMs(member);
|
|
160
|
+
if (activeOwners.has(name) || (signalMs && nowMs - signalMs < days * DAY_MS)) {
|
|
161
|
+
activeCount += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
quiet.push({
|
|
165
|
+
name,
|
|
166
|
+
days_quiet: signalMs ? Math.floor((nowMs - signalMs) / DAY_MS) : null,
|
|
167
|
+
last_signal: signalMs ? new Date(signalMs).toISOString() : null,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
quiet.sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
return { quiet, active_count: activeCount };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderTeamPrune(report, days = DEFAULT_PRUNE_DAYS) {
|
|
175
|
+
if (!report.quiet.length && !report.active_count) {
|
|
176
|
+
return 'no team members yet. create one with: atris member create <name> --role="..."';
|
|
177
|
+
}
|
|
178
|
+
if (!report.quiet.length) {
|
|
179
|
+
return `everyone on the team has a signal newer than ${days} days. nothing to prune.`;
|
|
180
|
+
}
|
|
181
|
+
const lines = report.quiet.map((entry) => (entry.days_quiet === null
|
|
182
|
+
? `${entry.name} has no recorded activity; keep, hand off, or retire.`
|
|
183
|
+
: `${entry.name} has been quiet for ${entry.days_quiet} days; keep, hand off, or retire.`));
|
|
184
|
+
lines.push(`${report.active_count} member${report.active_count === 1 ? ' is' : 's are'} still active. nothing was deleted; this is a report.`);
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
118
188
|
function helpText() {
|
|
119
189
|
return [
|
|
120
190
|
'atris team - one team view: every member, their role, and any engine running their work',
|
|
121
191
|
'atris team presence - show who is awake and what they are doing',
|
|
192
|
+
'atris team prune - flag members with no recent activity; deletes nothing',
|
|
122
193
|
'',
|
|
123
194
|
'usage: atris team [roster|presence] [--json]',
|
|
195
|
+
'usage: atris team prune [--days N] [--json]',
|
|
124
196
|
].join('\n');
|
|
125
197
|
}
|
|
126
198
|
|
|
@@ -129,6 +201,27 @@ function teamCommand(args = [], deps = {}) {
|
|
|
129
201
|
(deps.write || process.stdout.write.bind(process.stdout))(`${helpText()}\n`);
|
|
130
202
|
return 0;
|
|
131
203
|
}
|
|
204
|
+
if (args[0] === 'prune') {
|
|
205
|
+
const rest = args.slice(1);
|
|
206
|
+
let days = DEFAULT_PRUNE_DAYS;
|
|
207
|
+
let json = false;
|
|
208
|
+
let bad = false;
|
|
209
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
210
|
+
const arg = rest[i];
|
|
211
|
+
if (arg === '--json') { json = true; continue; }
|
|
212
|
+
if (arg === '--days') { i += 1; days = Number(rest[i]); continue; }
|
|
213
|
+
if (arg.startsWith('--days=')) { days = Number(arg.slice('--days='.length)); continue; }
|
|
214
|
+
bad = true;
|
|
215
|
+
}
|
|
216
|
+
if (bad || !Number.isFinite(days) || days <= 0) {
|
|
217
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team prune [--days N] [--json]\n');
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
const report = deps.prune || collectTeamPrune({ ...deps, days });
|
|
221
|
+
const output = json ? JSON.stringify(report, null, 2) : renderTeamPrune(report, days);
|
|
222
|
+
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
132
225
|
const rosterArgs = args.filter((arg) => arg !== 'roster');
|
|
133
226
|
if (args[0] !== 'presence' && rosterArgs.every((arg) => arg === '--json')) {
|
|
134
227
|
const roster = deps.roster || collectTeamRoster(deps);
|
|
@@ -139,7 +232,7 @@ function teamCommand(args = [], deps = {}) {
|
|
|
139
232
|
return 0;
|
|
140
233
|
}
|
|
141
234
|
if (args[0] !== 'presence' || args.some((arg, index) => index > 0 && arg !== '--json')) {
|
|
142
|
-
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence] [--json]\n');
|
|
235
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence|prune] [--json]\n');
|
|
143
236
|
return 2;
|
|
144
237
|
}
|
|
145
238
|
const presence = deps.presence || collectTeamPresence(deps);
|
|
@@ -150,4 +243,4 @@ function teamCommand(args = [], deps = {}) {
|
|
|
150
243
|
return 0;
|
|
151
244
|
}
|
|
152
245
|
|
|
153
|
-
module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamRoster, renderTeamRoster, teamCommand };
|
|
246
|
+
module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamPrune, collectTeamRoster, renderTeamPrune, renderTeamRoster, teamCommand };
|
package/lib/lesson-preflight.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const escapeRegExp = require('./escape-regexp');
|
|
5
6
|
|
|
6
7
|
const ACTIVE_STATUSES = new Set(['observed', 'open', 'attempted']);
|
|
7
8
|
const KEYWORD_STOP_WORDS = new Set([
|
|
@@ -80,7 +81,7 @@ function slugKeywords(slug) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function keywordAppears(text, keyword) {
|
|
83
|
-
const escaped = keyword
|
|
84
|
+
const escaped = escapeRegExp(keyword);
|
|
84
85
|
return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i').test(text);
|
|
85
86
|
}
|
|
86
87
|
|