atris 3.15.13 → 3.15.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +84 -8
- package/README.md +5 -1
- package/atris/AGENTS.md +46 -1
- package/atris/CLAUDE.md +36 -1
- package/atris/GEMINI.md +14 -1
- package/atris/atris.md +12 -1
- package/atris/atrisDev.md +3 -2
- package/atris/context/README.md +11 -0
- package/atris/features/company-brain-sync/validate.md +5 -5
- package/atris/learnings.jsonl +1 -0
- package/atris/policies/atris-design.md +2 -0
- package/atris/skills/aeo/SKILL.md +2 -2
- package/atris/skills/atris/SKILL.md +15 -62
- package/atris/skills/design/SKILL.md +2 -0
- package/atris/skills/imessage/SKILL.md +19 -2
- package/atris/skills/loop/SKILL.md +6 -5
- package/atris/skills/magic-inbox/SKILL.md +1 -1
- package/atris/team/_template/MEMBER.md +23 -1
- package/atris/team/brainstormer/START_HERE.md +6 -0
- package/atris/team/executor/MEMBER.md +13 -0
- package/atris/team/executor/START_HERE.md +6 -0
- package/atris/team/launcher/START_HERE.md +6 -0
- package/atris/team/mission-lead/MEMBER.md +39 -0
- package/atris/team/mission-lead/MISSION.md +33 -0
- package/atris/team/mission-lead/START_HERE.md +6 -0
- package/atris/team/navigator/MEMBER.md +11 -0
- package/atris/team/navigator/START_HERE.md +6 -0
- package/atris/team/opus-overnight/MEMBER.md +39 -0
- package/atris/team/opus-overnight/MISSION.md +61 -0
- package/atris/team/opus-overnight/START_HERE.md +6 -0
- package/atris/team/opus-overnight/STEERING.md +35 -0
- package/atris/team/researcher/START_HERE.md +6 -0
- package/atris/team/validator/MEMBER.md +26 -6
- package/atris/team/validator/START_HERE.md +6 -0
- package/atris/wiki/concepts/agent-activation-contract.md +79 -0
- package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
- package/atris/wiki/index.md +27 -0
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
- package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
- package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
- package/atris.md +49 -13
- package/bin/atris.js +660 -22
- package/commands/activate.js +12 -3
- package/commands/aeo.js +1 -1
- package/commands/align.js +10 -10
- package/commands/analytics.js +9 -4
- package/commands/app.js +2 -0
- package/commands/apps.js +276 -0
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +74 -5
- package/commands/brain.js +536 -61
- package/commands/brainstorm.js +12 -12
- package/commands/business-sync.js +142 -24
- package/commands/clean.js +9 -6
- package/commands/codex-goal.js +311 -0
- package/commands/errors.js +11 -1
- package/commands/feedback.js +55 -17
- package/commands/fork.js +2 -2
- package/commands/gm.js +376 -0
- package/commands/init.js +80 -3
- package/commands/integrations.js +524 -0
- package/commands/learn.js +25 -16
- package/commands/lesson.js +41 -0
- package/commands/lifecycle.js +2 -2
- package/commands/member.js +2416 -9
- package/commands/mission.js +1776 -0
- package/commands/now.js +48 -7
- package/commands/play.js +425 -0
- package/commands/publish.js +2 -1
- package/commands/pull.js +72 -29
- package/commands/push.js +199 -17
- package/commands/review.js +51 -13
- package/commands/skill.js +2 -2
- package/commands/soul.js +19 -13
- package/commands/status.js +6 -1
- package/commands/sync.js +5 -4
- package/commands/task.js +1041 -147
- package/commands/terminal.js +5 -5
- package/commands/verify.js +7 -5
- package/commands/visualize.js +7 -0
- package/commands/wiki.js +53 -16
- package/commands/workflow.js +298 -54
- package/commands/workspace-clean.js +1 -1
- package/commands/worktree.js +468 -0
- package/commands/xp.js +1608 -0
- package/lib/manifest.js +34 -4
- package/lib/scorecard.js +3 -2
- package/lib/task-db.js +408 -27
- package/lib/todo-fallback.js +28 -2
- package/lib/todo.js +5 -3
- package/package.json +23 -2
- package/utils/update-check.js +51 -1
package/commands/activate.js
CHANGED
|
@@ -55,16 +55,25 @@ function activateAtris() {
|
|
|
55
55
|
// Sort descending (most recent first)
|
|
56
56
|
allLogs.sort().reverse();
|
|
57
57
|
|
|
58
|
-
// Extract C# items from logs until we have 3
|
|
58
|
+
// Extract C# items from logs until we have 3. Dedupe by ID across files
|
|
59
|
+
// since per-day numbering reuses C1, C2, etc. — the same ID appearing
|
|
60
|
+
// in two day-files would otherwise render as duplicate rows.
|
|
61
|
+
const seenIds = new Set();
|
|
59
62
|
for (const logPath of allLogs) {
|
|
60
63
|
if (recentCompletions.length >= 3) break;
|
|
61
64
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
62
65
|
const completedSection = content.match(/## Completed ✅\n([\s\S]*?)(?=\n## |$)/);
|
|
63
66
|
if (completedSection) {
|
|
64
|
-
|
|
67
|
+
// Match `- **C#: Title**` (title between the bold markers). If extra
|
|
68
|
+
// prose follows after `**`, ignore it — the activation panel shows
|
|
69
|
+
// titles only, truncated to 59 chars.
|
|
70
|
+
const matches = completedSection[1].matchAll(/- \*\*C(\d+):\s*([^*\n]+?)\*\*/gm);
|
|
65
71
|
for (const match of matches) {
|
|
66
72
|
if (recentCompletions.length >= 3) break;
|
|
67
|
-
|
|
73
|
+
const id = `C${match[1]}`;
|
|
74
|
+
if (seenIds.has(id)) continue;
|
|
75
|
+
seenIds.add(id);
|
|
76
|
+
recentCompletions.push({ id, desc: match[2].trim(), file: path.basename(logPath) });
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
79
|
}
|
package/commands/aeo.js
CHANGED
|
@@ -88,7 +88,7 @@ function printHelp() {
|
|
|
88
88
|
console.log('');
|
|
89
89
|
console.log('Examples:');
|
|
90
90
|
console.log(' atris aeo init');
|
|
91
|
-
console.log(' atris aeo draft "what is
|
|
91
|
+
console.log(' atris aeo draft "what is example-co" --queries "what is example-co,best freight platform"');
|
|
92
92
|
console.log(' atris aeo draft "how does atris work" --workspace doordash --slug atris-overview');
|
|
93
93
|
}
|
|
94
94
|
|
package/commands/align.js
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* USAGE:
|
|
14
14
|
* atris align # auto-detect business from .atris/business.json
|
|
15
|
-
* atris align
|
|
16
|
-
* atris align
|
|
17
|
-
* atris align
|
|
18
|
-
* atris align
|
|
15
|
+
* atris align example-co # explicit business slug
|
|
16
|
+
* atris align example-co --dry-run # show diff, do nothing
|
|
17
|
+
* atris align example-co --fix # local is canonical: delete EC2 extras, push local-only
|
|
18
|
+
* atris align example-co --fix --from cloud # cloud is canonical: pull EC2-only, delete local extras
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
@@ -489,15 +489,15 @@ async function alignAtris() {
|
|
|
489
489
|
if (!slug || slug.startsWith('-')) slug = null;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
if (!slug || slug === '--help' || slug === '-h') {
|
|
492
|
+
if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
|
|
493
493
|
console.log('Usage: atris align [business] [--fix] [--hard] [--from cloud|local] [--dry-run]');
|
|
494
494
|
console.log('');
|
|
495
495
|
console.log(' atris align Diff current workspace against cloud (auto-detect)');
|
|
496
|
-
console.log(' atris align
|
|
497
|
-
console.log(' atris align
|
|
498
|
-
console.log(' atris align
|
|
499
|
-
console.log(' atris align
|
|
500
|
-
console.log(' atris align
|
|
496
|
+
console.log(' atris align example-co Diff example-co workspace');
|
|
497
|
+
console.log(' atris align example-co --fix Fix drift (local is canonical by default)');
|
|
498
|
+
console.log(' atris align example-co --fix --hard Force-push: nuke cloud cruft, upload local. Skips diff. Fast.');
|
|
499
|
+
console.log(' atris align example-co --fix --from cloud Cloud is canonical: pull EC2-only, delete local extras');
|
|
500
|
+
console.log(' atris align example-co --dry-run Show what would change, do nothing');
|
|
501
501
|
process.exit(0);
|
|
502
502
|
}
|
|
503
503
|
|
package/commands/analytics.js
CHANGED
|
@@ -67,12 +67,17 @@ function analyticsAtris() {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Parse timestamps for productivity hours
|
|
71
|
-
|
|
70
|
+
// Parse timestamps for productivity hours. Match the journal heading
|
|
71
|
+
// format `### Title — HH:MM` and the legacy `**HH:MM:SS**` form.
|
|
72
|
+
const timestampMatches = content.match(/(?:—|--)\s*(\d{2}):\d{2}(?::\d{2})?\b|\*\*(\d{2}):\d{2}(?::\d{2})?\*\*/g);
|
|
72
73
|
if (timestampMatches) {
|
|
73
74
|
timestampMatches.forEach(ts => {
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const m = ts.match(/(\d{2}):/);
|
|
76
|
+
if (!m) return;
|
|
77
|
+
const hour = parseInt(m[1], 10);
|
|
78
|
+
if (Number.isFinite(hour) && hour >= 0 && hour < 24) {
|
|
79
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
80
|
+
}
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
});
|
package/commands/app.js
CHANGED
package/commands/apps.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function findAppsPackRoot(startDir = process.cwd()) {
|
|
6
|
+
const explicit = process.env.ATRIS_APPS_PACK;
|
|
7
|
+
if (explicit) {
|
|
8
|
+
const resolved = path.resolve(explicit);
|
|
9
|
+
if (fs.existsSync(path.join(resolved, 'scripts', 'app_use.py'))) return resolved;
|
|
10
|
+
}
|
|
11
|
+
let current = path.resolve(startDir);
|
|
12
|
+
while (true) {
|
|
13
|
+
const candidate = path.join(current, 'atris', 'apps-pack');
|
|
14
|
+
if (fs.existsSync(path.join(candidate, 'scripts', 'app_use.py'))) return candidate;
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current) break;
|
|
17
|
+
current = parent;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function appsUsageLines() {
|
|
23
|
+
return [
|
|
24
|
+
'Usage: atris apps <command>',
|
|
25
|
+
'',
|
|
26
|
+
'Commands:',
|
|
27
|
+
' list [--json] List available local apps',
|
|
28
|
+
' run <slug> [--lines N] Run an app and print data/latest.md or --json status',
|
|
29
|
+
' owner <slug> [--json] Show owner view: launch, usage, learning, next actions',
|
|
30
|
+
' status [--json] Show local app health',
|
|
31
|
+
' queue Show app improvement queue',
|
|
32
|
+
' rate <slug> <up|down> [note] Record output feedback',
|
|
33
|
+
' smoke Run fresh-checkout smoke test',
|
|
34
|
+
' doctor [--strict] Audit pack health, smoke, and source cleanliness',
|
|
35
|
+
' handoff [--json] Print app operator checklist and receipt paths',
|
|
36
|
+
' overnight Run the bounded overnight app loop',
|
|
37
|
+
' overnight-install [--start] Install bounded macOS LaunchAgent',
|
|
38
|
+
' overnight-agent [status|stop] Inspect or stop macOS LaunchAgent',
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printAppsHelp() {
|
|
43
|
+
console.log('');
|
|
44
|
+
for (const line of appsUsageLines()) console.log(line);
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function wantsJson(subcommand, rawArgs) {
|
|
49
|
+
return subcommand === '--json' || rawArgs.includes('--json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeInvocation(subcommand, rawArgs) {
|
|
53
|
+
if (subcommand === '--json') {
|
|
54
|
+
return { subcommand: 'list', rawArgs: ['--json', ...rawArgs] };
|
|
55
|
+
}
|
|
56
|
+
return { subcommand, rawArgs };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function exitAppsError(message, json, options = {}) {
|
|
60
|
+
const payload = {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: message,
|
|
63
|
+
...(options.extra || {}),
|
|
64
|
+
};
|
|
65
|
+
if (options.usage) payload.usage = appsUsageLines();
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
68
|
+
} else {
|
|
69
|
+
console.error(message);
|
|
70
|
+
if (options.usage) printAppsHelp();
|
|
71
|
+
}
|
|
72
|
+
process.exit(options.code || 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseScalar(value) {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (trimmed === '[]') return [];
|
|
78
|
+
if (trimmed === 'true') return true;
|
|
79
|
+
if (trimmed === 'false') return false;
|
|
80
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
81
|
+
const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
|
|
82
|
+
return quoted ? quoted[1] : trimmed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readAppManifest(appDir) {
|
|
86
|
+
const text = fs.readFileSync(path.join(appDir, 'APP.md'), 'utf8');
|
|
87
|
+
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
|
88
|
+
const manifest = {};
|
|
89
|
+
if (!match) return manifest;
|
|
90
|
+
let currentList = null;
|
|
91
|
+
for (const line of match[1].split('\n')) {
|
|
92
|
+
const listItem = line.match(/^\s+-\s+(.*)$/);
|
|
93
|
+
if (listItem && currentList) {
|
|
94
|
+
manifest[currentList].push(parseScalar(listItem[1]));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/^\s+/.test(line)) {
|
|
98
|
+
if (currentList && manifest[currentList].length === 0) delete manifest[currentList];
|
|
99
|
+
currentList = null;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const field = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
103
|
+
if (!field) continue;
|
|
104
|
+
const [, key, rawValue] = field;
|
|
105
|
+
if (rawValue === '') {
|
|
106
|
+
manifest[key] = [];
|
|
107
|
+
currentList = key;
|
|
108
|
+
} else {
|
|
109
|
+
manifest[key] = parseScalar(rawValue);
|
|
110
|
+
currentList = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return manifest;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function listAppManifests(packRoot) {
|
|
117
|
+
const appsDir = path.join(packRoot, 'apps');
|
|
118
|
+
return fs.readdirSync(appsDir)
|
|
119
|
+
.filter((name) => fs.existsSync(path.join(appsDir, name, 'APP.md')))
|
|
120
|
+
.sort()
|
|
121
|
+
.map((slug) => ({
|
|
122
|
+
slug,
|
|
123
|
+
path: path.join(appsDir, slug, 'APP.md'),
|
|
124
|
+
latest_output: path.join(appsDir, slug, 'data', 'latest.md'),
|
|
125
|
+
...readAppManifest(path.join(appsDir, slug)),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function popOption(args, name, fallback = null) {
|
|
130
|
+
const eqPrefix = `${name}=`;
|
|
131
|
+
const eqIndex = args.findIndex((arg) => arg.startsWith(eqPrefix));
|
|
132
|
+
if (eqIndex >= 0) {
|
|
133
|
+
const value = args[eqIndex].slice(eqPrefix.length);
|
|
134
|
+
args.splice(eqIndex, 1);
|
|
135
|
+
return value || fallback;
|
|
136
|
+
}
|
|
137
|
+
const index = args.indexOf(name);
|
|
138
|
+
if (index >= 0) {
|
|
139
|
+
const value = args[index + 1];
|
|
140
|
+
args.splice(index, value === undefined ? 1 : 2);
|
|
141
|
+
return value || fallback;
|
|
142
|
+
}
|
|
143
|
+
return fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runPackScript(packRoot, script, args) {
|
|
147
|
+
const result = spawnSync('python3', [script, ...args], {
|
|
148
|
+
cwd: packRoot,
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
env: process.env,
|
|
151
|
+
});
|
|
152
|
+
if (result.error) {
|
|
153
|
+
console.error(`✗ Failed to run python3 ${script}: ${result.error.message}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
process.exit(result.status ?? 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function appsCommand(subcommand, ...rawArgs) {
|
|
160
|
+
const jsonRequested = wantsJson(subcommand, rawArgs);
|
|
161
|
+
const normalized = normalizeInvocation(subcommand, rawArgs);
|
|
162
|
+
subcommand = normalized.subcommand;
|
|
163
|
+
rawArgs = normalized.rawArgs;
|
|
164
|
+
|
|
165
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
166
|
+
printAppsHelp();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const packRoot = findAppsPackRoot();
|
|
170
|
+
if (!packRoot) {
|
|
171
|
+
if (jsonRequested) {
|
|
172
|
+
exitAppsError('No Atris app pack found.', true, {
|
|
173
|
+
extra: { expected: 'atris/apps-pack/ in this workspace, or ATRIS_APPS_PACK' },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
console.error('✗ No Atris app pack found.');
|
|
177
|
+
console.error(' Expected atris/apps-pack/ in this workspace, or set ATRIS_APPS_PACK.');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const args = [...rawArgs];
|
|
182
|
+
const workspace = path.resolve(popOption(args, '--workspace', process.cwd()));
|
|
183
|
+
const lines = popOption(args, '--lines', null);
|
|
184
|
+
const note = popOption(args, '--note', null);
|
|
185
|
+
const hours = popOption(args, '--hours', null);
|
|
186
|
+
const intervalMinutes = popOption(args, '--interval-minutes', null);
|
|
187
|
+
const maxCycles = popOption(args, '--max-cycles', null);
|
|
188
|
+
const label = popOption(args, '--label', null);
|
|
189
|
+
const python = popOption(args, '--python', null);
|
|
190
|
+
const json = args.includes('--json');
|
|
191
|
+
if (json) args.splice(args.indexOf('--json'), 1);
|
|
192
|
+
const noRun = args.includes('--no-run');
|
|
193
|
+
if (noRun) args.splice(args.indexOf('--no-run'), 1);
|
|
194
|
+
|
|
195
|
+
if (subcommand === 'list') {
|
|
196
|
+
if (json) {
|
|
197
|
+
console.log(JSON.stringify({ pack: packRoot, apps: listAppManifests(packRoot) }, null, 2));
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
runPackScript(packRoot, 'scripts/app_use.py', ['--list']);
|
|
201
|
+
}
|
|
202
|
+
if (subcommand === 'status') runPackScript(packRoot, 'scripts/app_status.py', json ? ['--json'] : []);
|
|
203
|
+
if (subcommand === 'queue') runPackScript(packRoot, 'scripts/app_improvement_queue.py', []);
|
|
204
|
+
if (subcommand === 'smoke') runPackScript(packRoot, 'scripts/install_smoke.py', []);
|
|
205
|
+
if (subcommand === 'doctor') runPackScript(packRoot, 'scripts/install_smoke.py', []);
|
|
206
|
+
if (subcommand === 'handoff') {
|
|
207
|
+
const command = ['--workspace', workspace];
|
|
208
|
+
if (json) command.push('--json');
|
|
209
|
+
runPackScript(packRoot, 'scripts/app_operator_checklist.py', command);
|
|
210
|
+
}
|
|
211
|
+
if (subcommand === 'overnight') {
|
|
212
|
+
const command = ['--workspace', workspace];
|
|
213
|
+
if (hours) command.push('--hours', hours);
|
|
214
|
+
if (intervalMinutes) command.push('--interval-minutes', intervalMinutes);
|
|
215
|
+
if (maxCycles) command.push('--max-cycles', maxCycles);
|
|
216
|
+
runPackScript(packRoot, 'scripts/app_overnight.py', command);
|
|
217
|
+
}
|
|
218
|
+
if (subcommand === 'overnight-install') {
|
|
219
|
+
const command = ['--workspace', workspace];
|
|
220
|
+
if (hours) command.push('--hours', hours);
|
|
221
|
+
if (intervalMinutes) command.push('--interval-minutes', intervalMinutes);
|
|
222
|
+
if (maxCycles) command.push('--max-cycles', maxCycles);
|
|
223
|
+
if (label) command.push('--label', label);
|
|
224
|
+
if (python) command.push('--python', python);
|
|
225
|
+
if (args.includes('--start')) command.push('--start');
|
|
226
|
+
if (args.includes('--dry-run')) command.push('--dry-run');
|
|
227
|
+
runPackScript(packRoot, 'scripts/install_overnight_launch_agent.py', command);
|
|
228
|
+
}
|
|
229
|
+
if (subcommand === 'overnight-agent') {
|
|
230
|
+
const command = [args.shift() || 'status'];
|
|
231
|
+
if (label) command.push('--label', label);
|
|
232
|
+
if (args.includes('--dry-run')) command.push('--dry-run');
|
|
233
|
+
if (json) command.push('--json');
|
|
234
|
+
runPackScript(packRoot, 'scripts/control_overnight_launch_agent.py', command);
|
|
235
|
+
}
|
|
236
|
+
if (subcommand === 'run') {
|
|
237
|
+
const slug = args.shift();
|
|
238
|
+
if (!slug) {
|
|
239
|
+
exitAppsError('Usage: atris apps run <slug> [--workspace <path>] [--lines N]', json);
|
|
240
|
+
}
|
|
241
|
+
const command = [slug, '--workspace', workspace];
|
|
242
|
+
if (lines) command.push('--lines', lines);
|
|
243
|
+
if (json) command.push('--json');
|
|
244
|
+
if (noRun) command.push('--no-run');
|
|
245
|
+
runPackScript(packRoot, 'scripts/app_use.py', command);
|
|
246
|
+
}
|
|
247
|
+
if (subcommand === 'owner') {
|
|
248
|
+
const slug = args.shift();
|
|
249
|
+
if (!slug) {
|
|
250
|
+
exitAppsError('Usage: atris apps owner <slug> [--workspace <path>] [--lines N]', json);
|
|
251
|
+
}
|
|
252
|
+
const command = [slug, '--workspace', workspace];
|
|
253
|
+
if (lines) command.push('--lines', lines);
|
|
254
|
+
if (json) command.push('--json');
|
|
255
|
+
if (noRun) command.push('--no-run');
|
|
256
|
+
runPackScript(packRoot, 'scripts/app_owner.py', command);
|
|
257
|
+
}
|
|
258
|
+
if (subcommand === 'rate') {
|
|
259
|
+
const slug = args.shift();
|
|
260
|
+
const rating = args.shift();
|
|
261
|
+
const ratingNote = note || args.join(' ');
|
|
262
|
+
if (!slug || !['up', 'down'].includes(rating)) {
|
|
263
|
+
console.error('Usage: atris apps rate <slug> <up|down> [note]');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
runPackScript(packRoot, 'scripts/app_rate.py', [slug, rating, ratingNote || '']);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exitAppsError(`Unknown apps command: ${subcommand}`, json, { usage: true });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
appsCommand,
|
|
274
|
+
findAppsPackRoot,
|
|
275
|
+
listAppManifests,
|
|
276
|
+
};
|
package/commands/auth.js
CHANGED
|
@@ -341,7 +341,7 @@ async function accountsCmd() {
|
|
|
341
341
|
}
|
|
342
342
|
profiles.forEach(p => deleteProfile(p));
|
|
343
343
|
deleteCredentials();
|
|
344
|
-
console.log(`✓ Removed ${profiles.length} account
|
|
344
|
+
console.log(`✓ Removed ${profiles.length} ${profiles.length === 1 ? 'account' : 'accounts'}.`);
|
|
345
345
|
process.exit(0);
|
|
346
346
|
}
|
|
347
347
|
if (!target) {
|
package/commands/autopilot.js
CHANGED
|
@@ -39,6 +39,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
|
|
|
39
39
|
|
|
40
40
|
for (const t of todo.backlog) {
|
|
41
41
|
if (t.tags && t.tags.includes('unverified')) continue;
|
|
42
|
+
if (shouldSkipEndgameAtPicker(cwd, t)) continue;
|
|
42
43
|
if (t.tag === 'endgame' && !skipped.has(t.title)) {
|
|
43
44
|
suggestions.push({
|
|
44
45
|
task: t.title,
|
|
@@ -125,6 +126,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
|
|
|
125
126
|
// --- Backlog tasks ---
|
|
126
127
|
for (const t of todo.backlog) {
|
|
127
128
|
if (t.tags && t.tags.includes('unverified')) continue;
|
|
129
|
+
if (shouldSkipEndgameAtPicker(cwd, t)) continue;
|
|
128
130
|
if (skipped.has(t.title)) continue;
|
|
129
131
|
const remaining = todo.backlog.filter(b => !(b.tags && b.tags.includes('unverified'))).length;
|
|
130
132
|
suggestions.push({
|
|
@@ -697,6 +699,12 @@ function writeLesson(cwd, slug, status, explanation) {
|
|
|
697
699
|
}
|
|
698
700
|
|
|
699
701
|
let content = fs.readFileSync(lessonsPath, 'utf8');
|
|
702
|
+
// Same-day dedup: if an identical line already exists, skip the write. A
|
|
703
|
+
// cron firing every 13min produced 5 identical no-verify-field lessons in
|
|
704
|
+
// one day (2026-05-08) before the picker-side fix landed — pure noise. The
|
|
705
|
+
// append-only contract still holds across days because today's date is in
|
|
706
|
+
// the line.
|
|
707
|
+
if (content.includes(lessonLine)) return;
|
|
700
708
|
// Append after the --- separator
|
|
701
709
|
if (content.includes('---\n')) {
|
|
702
710
|
content = content.replace(/---\n/, `---\n\n${lessonLine}\n`);
|
|
@@ -784,7 +792,7 @@ function getVerifyCommand(cwd, taskTitle) {
|
|
|
784
792
|
const todoPath = path.join(cwd, 'atris', 'TODO.md');
|
|
785
793
|
if (fs.existsSync(todoPath)) {
|
|
786
794
|
const todo = parseTodo(todoPath);
|
|
787
|
-
const task = [...todo.inProgress, ...todo.backlog, ...todo.completed]
|
|
795
|
+
const task = [...todo.inProgress, ...(todo.review || []), ...todo.backlog, ...todo.completed]
|
|
788
796
|
.find(t => t.title === taskTitle);
|
|
789
797
|
if (task && task.verify) return { cmd: task.verify, explicit: true };
|
|
790
798
|
}
|
|
@@ -813,6 +821,14 @@ function detectDefaultVerify(cwd) {
|
|
|
813
821
|
if (fs.existsSync(path.join(cwd, 'pytest.ini')) ||
|
|
814
822
|
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
815
823
|
fs.existsSync(path.join(cwd, 'setup.py'))) {
|
|
824
|
+
// Prefer a repo-curated fast lane over bare `pytest`. Large repos (e.g.
|
|
825
|
+
// atrisos-backend) ship a critical-path runner because the full suite is
|
|
826
|
+
// unsafe to run unsupervised (CLAUDE.md: "NEVER run pytest tests/ ... eats
|
|
827
|
+
// 10GB+ RAM"). lessons.md 2026-05-10 verify-failed: bare `pytest` failed
|
|
828
|
+
// a reactive-signal verify and halted the loop.
|
|
829
|
+
for (const fast of ['backend/scripts/test_fast.sh', 'scripts/test_fast.sh', 'test_fast.sh']) {
|
|
830
|
+
if (fs.existsSync(path.join(cwd, fast))) return `bash ${fast}`;
|
|
831
|
+
}
|
|
816
832
|
return 'pytest';
|
|
817
833
|
}
|
|
818
834
|
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
|
|
@@ -1177,10 +1193,16 @@ function runTaskOnce(context, options = {}) {
|
|
|
1177
1193
|
// task is already done — either way, halt. This is the keystone that makes
|
|
1178
1194
|
// Verify load-bearing. The cmd is captured here and reused post-execute so
|
|
1179
1195
|
// an agent cannot swap the rubric mid-tick.
|
|
1196
|
+
//
|
|
1197
|
+
// Timeout: 300s. Many endgame Verify clauses chain a fast-suite run
|
|
1198
|
+
// (test_fast.sh ~60s) plus extra assertions. At 60s the gate timed out
|
|
1199
|
+
// before the chain could finish, the catch branch labeled it "falsifiable",
|
|
1200
|
+
// and the loop executed already-done work. 300s lets the standard
|
|
1201
|
+
// pytest+fast-suite shape complete cleanly.
|
|
1180
1202
|
const skipFalsifiability = options.skipFalsifiability === true;
|
|
1181
1203
|
if (!skipFalsifiability && verifyResult.explicit && context.kind === 'endgame' && verifyCmd) {
|
|
1182
1204
|
try {
|
|
1183
|
-
execSync(verifyCmd, { cwd, stdio: 'pipe', timeout:
|
|
1205
|
+
execSync(verifyCmd, { cwd, stdio: 'pipe', timeout: 300000 });
|
|
1184
1206
|
writeLesson(cwd, 'verify-not-falsifiable', 'fail',
|
|
1185
1207
|
`Verify \`${verifyCmd}\` passed before work started on "${context.task}". Either the rubric is trivial or the task is already done. Tick halted.`);
|
|
1186
1208
|
return {
|
|
@@ -1466,7 +1488,8 @@ function readEndgameState(cwd) {
|
|
|
1466
1488
|
pickedAt: pickedMatch ? pickedMatch[1].trim() : null,
|
|
1467
1489
|
horizon: horizonMatch ? horizonMatch[1].trim() : '',
|
|
1468
1490
|
remaining: todo.backlog.filter(t => t.tag === 'endgame').length
|
|
1469
|
-
+ todo.inProgress.filter(t => t.tag === 'endgame').length
|
|
1491
|
+
+ todo.inProgress.filter(t => t.tag === 'endgame').length
|
|
1492
|
+
+ (todo.review || []).filter(t => t.tag === 'endgame').length,
|
|
1470
1493
|
completed: todo.completed.filter(t => t.tag === 'endgame').length,
|
|
1471
1494
|
};
|
|
1472
1495
|
} catch {
|
|
@@ -2207,6 +2230,42 @@ function isLessonResolvedLegacy(lessonLine, cwd) {
|
|
|
2207
2230
|
* system already wrote down about itself. A `fail` lesson with `isLessonResolved
|
|
2208
2231
|
* === false` means grep confirms the bug pattern is still present — actionable.
|
|
2209
2232
|
*/
|
|
2233
|
+
/**
|
|
2234
|
+
* Returns true if a recent (within `windowDays`) `verify-not-falsifiable`
|
|
2235
|
+
* lesson references this exact task title. The falsifiability gate halts the
|
|
2236
|
+
* tick when the Verify clause already passes before work starts (task is
|
|
2237
|
+
* already shipped or rubric is trivial), but nothing in TODO.md changes —
|
|
2238
|
+
* so the next tick re-picks the same task and burns another 90s+ halting in
|
|
2239
|
+
* the same place. Reading the lesson log breaks the loop without requiring
|
|
2240
|
+
* a TODO.md hand-edit (which is the structurally-broken file we route around
|
|
2241
|
+
* per feedback_todo_md_is_the_problem).
|
|
2242
|
+
*/
|
|
2243
|
+
function hasRecentVerifyPrePass(cwd, taskTitle, windowDays = 7) {
|
|
2244
|
+
if (!taskTitle) return false;
|
|
2245
|
+
const lessons = parseLessons(cwd);
|
|
2246
|
+
if (lessons.length === 0) return false;
|
|
2247
|
+
const cutoff = new Date();
|
|
2248
|
+
cutoff.setDate(cutoff.getDate() - windowDays);
|
|
2249
|
+
const cutoffDate = cutoff.toISOString().split('T')[0];
|
|
2250
|
+
const needle = `"${taskTitle}"`;
|
|
2251
|
+
for (const l of lessons) {
|
|
2252
|
+
if (l.id !== 'verify-not-falsifiable') continue;
|
|
2253
|
+
if (l.date < cutoffDate) continue;
|
|
2254
|
+
if (l.body.includes(needle)) return true;
|
|
2255
|
+
}
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function shouldSkipEndgameAtPicker(cwd, task) {
|
|
2260
|
+
if (!task || task.tag !== 'endgame') return false;
|
|
2261
|
+
// Endgame tasks must declare an explicit **Verify:** field. runTaskOnce would
|
|
2262
|
+
// halt them, so the picker must not downgrade them into generic backlog work.
|
|
2263
|
+
if (!task.verify) return true;
|
|
2264
|
+
// If Verify already passed before work started recently, the task is already
|
|
2265
|
+
// shipped or the rubric is trivial. Keep it out of all picker paths.
|
|
2266
|
+
return hasRecentVerifyPrePass(cwd, task.title);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2210
2269
|
function pickUnresolvedFailLesson(cwd) {
|
|
2211
2270
|
const lessons = parseLessons(cwd);
|
|
2212
2271
|
if (lessons.length === 0) return null;
|
|
@@ -2245,6 +2304,11 @@ function pickUnresolvedFailLesson(cwd) {
|
|
|
2245
2304
|
return candidates[0];
|
|
2246
2305
|
}
|
|
2247
2306
|
|
|
2307
|
+
function getLessonVerdict(lessonLine) {
|
|
2308
|
+
const match = lessonLine.match(/\*\*\[\d{4}-\d{2}-\d{2}\]\s+[\w-]+\*\*\s*[—-]\s*(pass|fail)\b/i);
|
|
2309
|
+
return match ? match[1].toLowerCase() : null;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2248
2312
|
/**
|
|
2249
2313
|
* Propose 3 candidate next horizons for the autopilot loop. Combines
|
|
2250
2314
|
* `getIdleTickCount` + `getRecentSignals` into a prompt asking the LLM
|
|
@@ -2286,6 +2350,7 @@ Based on these signals, propose exactly 3 candidate next horizons for the loop t
|
|
|
2286
2350
|
- A real, concrete horizon tied to what the signals actually reveal (no placeholders, no "candidate 1", no TODO/FIXME stubs).
|
|
2287
2351
|
- Something the loop can actually work on in this repo right now.
|
|
2288
2352
|
- Distinct from the other two candidates.
|
|
2353
|
+
- Not a restatement of a \`pass\` lesson; pass lessons are shipped constraints, not open work.
|
|
2289
2354
|
|
|
2290
2355
|
Output STRICT JSON ONLY — no prose, no markdown code fences, no commentary. The output must be a single JSON array with exactly 3 objects, each shaped:
|
|
2291
2356
|
|
|
@@ -2353,7 +2418,7 @@ Reply with the JSON array and nothing else.`;
|
|
|
2353
2418
|
throw new Error(`proposeCandidateHorizons: expected at least 1 valid candidate, got ${candidates.length}`);
|
|
2354
2419
|
}
|
|
2355
2420
|
|
|
2356
|
-
// Filter out candidates derived from resolved lessons
|
|
2421
|
+
// Filter out candidates derived from shipped/resolved lessons.
|
|
2357
2422
|
const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
|
|
2358
2423
|
const filtered = [];
|
|
2359
2424
|
for (const c of candidates) {
|
|
@@ -2362,12 +2427,16 @@ Reply with the JSON array and nothing else.`;
|
|
|
2362
2427
|
for (const lessonLine of signals.recentLessons) {
|
|
2363
2428
|
const slugMatch = lessonLine.match(/\*\*\[\d{4}-\d{2}-\d{2}\]\s+([\w-]+)\*\*/);
|
|
2364
2429
|
if (!slugMatch) continue;
|
|
2365
|
-
|
|
2430
|
+
const alreadyResolved = lessonLine.includes('[resolved]');
|
|
2366
2431
|
const slug = slugMatch[1];
|
|
2367
2432
|
// Fuzzy match: check if slug keywords appear in the candidate text
|
|
2368
2433
|
const slugWords = slug.split('-').filter(w => w.length > 2);
|
|
2369
2434
|
const matchCount = slugWords.filter(w => combinedText.includes(w)).length;
|
|
2370
2435
|
if (matchCount < Math.ceil(slugWords.length * 0.5)) continue;
|
|
2436
|
+
if (alreadyResolved || getLessonVerdict(lessonLine) === 'pass') {
|
|
2437
|
+
droppedByLesson = true;
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2371
2440
|
// Candidate matches this lesson — check if the lesson is resolved
|
|
2372
2441
|
if (isLessonResolved(lessonLine, cwd)) {
|
|
2373
2442
|
// Tag lesson [resolved] in lessons.md
|