atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- 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 +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- 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/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- 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/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- 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/radar.js +259 -14
- 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/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -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 +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -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 +2431 -38
- 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/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -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/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -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 +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- 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
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* atris study — on-demand learning feed + tutor-loop routing + waiting-room cards.
|
|
5
|
+
*
|
|
6
|
+
* atris study <topic…> [--personal]
|
|
7
|
+
* atris study --while "<cmd>" [topic…] [--personal]
|
|
8
|
+
*
|
|
9
|
+
* Runs learning-feed ingest in the fixed backend repo, ensures the local feed
|
|
10
|
+
* server is up, opens the feed in the browser, and prints tutor-loop guidance
|
|
11
|
+
* for language topics. --while serves terminal cards while a shell command runs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const { spawn, spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const BACKEND_ROOT = '/Users/keshavrao/arena/atrisos-backend';
|
|
20
|
+
const FEED_URL = 'http://localhost:8777';
|
|
21
|
+
const FEED_STATS_URL = `${FEED_URL}/api/stats`;
|
|
22
|
+
const FEED_NEXT_URL = `${FEED_URL}/api/next`;
|
|
23
|
+
const FEED_ANSWER_URL = `${FEED_URL}/api/answer`;
|
|
24
|
+
const FEED_PORT = 8777;
|
|
25
|
+
const PROBE_TIMEOUT_MS = 2000;
|
|
26
|
+
const SERVER_WAIT_MS = 8000;
|
|
27
|
+
const SERVER_POLL_MS = 400;
|
|
28
|
+
const CARD_WRAP_COLS = 60;
|
|
29
|
+
const NAME_TAG = 'Atris ▸';
|
|
30
|
+
|
|
31
|
+
const LANGUAGE_TOPICS = new Set([
|
|
32
|
+
'spanish', 'french', 'italian', 'portuguese', 'german',
|
|
33
|
+
'japanese', 'mandarin', 'chinese', 'korean', 'hindi',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function showHelp() {
|
|
37
|
+
console.log(`\n atris study — learn a topic on demand (learning feed + tutor loop)\n
|
|
38
|
+
Usage:
|
|
39
|
+
atris study <topic…> [--personal]
|
|
40
|
+
atris study --while "<cmd>" [topic…] [--personal]
|
|
41
|
+
|
|
42
|
+
Adds the topic to your local learning feed, ingests fresh cards, starts the
|
|
43
|
+
feed server if needed, and opens http://localhost:8777 in your browser.
|
|
44
|
+
|
|
45
|
+
--while "<cmd>" run a shell command while serving learning cards in the
|
|
46
|
+
terminal; on exit replays captured stdout/stderr and exits
|
|
47
|
+
with the child's code. Cards use dialogue beats (Atris ▸,
|
|
48
|
+
claim, why; probes: question → space reveal → c/w grade).
|
|
49
|
+
Space advances. q stops cards and live-streams build output.
|
|
50
|
+
Ctrl+C kills the child and replays output so far (exit 130).
|
|
51
|
+
Non-TTY stdin = plain passthrough (no cards). If the feed
|
|
52
|
+
server is down, the command still runs — cards are skipped.
|
|
53
|
+
|
|
54
|
+
--personal also scan workspace briefs for personal-edge cards
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
atris study spanish
|
|
58
|
+
atris study "ai research" --personal
|
|
59
|
+
atris study --while "npm run build"
|
|
60
|
+
atris study --while "npm run build" spanish
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseArgs(argv = []) {
|
|
65
|
+
const opts = { personal: false, help: false, whileCmd: null };
|
|
66
|
+
const topicParts = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const arg = argv[i];
|
|
70
|
+
if (arg === '--help' || arg === '-h') {
|
|
71
|
+
opts.help = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === '--while') {
|
|
75
|
+
i += 1;
|
|
76
|
+
if (i >= argv.length) throw new Error('--while requires a command string');
|
|
77
|
+
opts.whileCmd = argv[i];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === '--personal') {
|
|
81
|
+
opts.personal = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg.startsWith('--')) {
|
|
85
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
86
|
+
}
|
|
87
|
+
topicParts.push(arg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
opts.topic = topicParts.join(' ').trim();
|
|
91
|
+
return opts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireBackend() {
|
|
95
|
+
if (!fs.existsSync(BACKEND_ROOT)) {
|
|
96
|
+
console.error(`✗ Backend repo not found: ${BACKEND_ROOT}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const venvPython = path.join(BACKEND_ROOT, 'venv/bin/python');
|
|
100
|
+
if (!fs.existsSync(venvPython)) {
|
|
101
|
+
console.error(`✗ Backend venv not found: ${venvPython}`);
|
|
102
|
+
console.error(' Create the venv in atrisos-backend before running atris study.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
return venvPython;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectLanguage(topic) {
|
|
109
|
+
const normalized = String(topic || '').toLowerCase().trim();
|
|
110
|
+
if (LANGUAGE_TOPICS.has(normalized)) return normalized;
|
|
111
|
+
for (const word of normalized.split(/\s+/)) {
|
|
112
|
+
if (LANGUAGE_TOPICS.has(word)) return word;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printTutorLoopBlock(backendRoot, language) {
|
|
118
|
+
const tutorDb = path.join(backendRoot, 'experiments/tutor-loop/tutor.db');
|
|
119
|
+
const hasDb = fs.existsSync(tutorDb);
|
|
120
|
+
|
|
121
|
+
console.log('\n── Tutor loop (production drills) ──');
|
|
122
|
+
if (!hasDb) {
|
|
123
|
+
console.log(' Init (run once from backend repo):');
|
|
124
|
+
console.log(' venv/bin/python experiments/tutor-loop/tutor.py init');
|
|
125
|
+
} else {
|
|
126
|
+
console.log(' Tutor DB ready: experiments/tutor-loop/tutor.db');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('\n Daily flow:');
|
|
130
|
+
console.log(' venv/bin/python experiments/tutor-loop/tutor.py tick');
|
|
131
|
+
console.log(' venv/bin/python experiments/tutor-loop/tutor.py reply "<your answers>"');
|
|
132
|
+
console.log(' venv/bin/python experiments/tutor-loop/tutor.py receipt');
|
|
133
|
+
console.log('\n Scan (iMessage replies):');
|
|
134
|
+
console.log(' venv/bin/python experiments/tutor-loop/tutor.py scan');
|
|
135
|
+
|
|
136
|
+
if (language !== 'spanish') {
|
|
137
|
+
console.log('\n curriculum not yet built — spanish ships today');
|
|
138
|
+
}
|
|
139
|
+
console.log('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function probeFeedStats(timeoutMs = PROBE_TIMEOUT_MS) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const req = http.get(FEED_STATS_URL, { timeout: timeoutMs }, (res) => {
|
|
145
|
+
res.resume();
|
|
146
|
+
resolve(res.statusCode >= 200 && res.statusCode < 500);
|
|
147
|
+
});
|
|
148
|
+
req.on('timeout', () => {
|
|
149
|
+
req.destroy();
|
|
150
|
+
resolve(false);
|
|
151
|
+
});
|
|
152
|
+
req.on('error', () => resolve(false));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sleep(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function httpGetJson(url, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
163
|
+
let body = '';
|
|
164
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
165
|
+
res.on('end', () => {
|
|
166
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
167
|
+
resolve(null);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
resolve(JSON.parse(body));
|
|
172
|
+
} catch {
|
|
173
|
+
resolve(null);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
req.on('timeout', () => {
|
|
178
|
+
req.destroy();
|
|
179
|
+
resolve(null);
|
|
180
|
+
});
|
|
181
|
+
req.on('error', () => resolve(null));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function httpPostJson(url, payload, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
const data = JSON.stringify(payload);
|
|
188
|
+
const urlObj = new URL(url);
|
|
189
|
+
const req = http.request({
|
|
190
|
+
hostname: urlObj.hostname,
|
|
191
|
+
port: urlObj.port,
|
|
192
|
+
path: urlObj.pathname,
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
'Content-Length': Buffer.byteLength(data),
|
|
197
|
+
},
|
|
198
|
+
timeout: timeoutMs,
|
|
199
|
+
}, (res) => {
|
|
200
|
+
res.resume();
|
|
201
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
202
|
+
});
|
|
203
|
+
req.on('timeout', () => {
|
|
204
|
+
req.destroy();
|
|
205
|
+
resolve(false);
|
|
206
|
+
});
|
|
207
|
+
req.on('error', () => resolve(false));
|
|
208
|
+
req.write(data);
|
|
209
|
+
req.end();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function spawnFeedServer(venvPython, backendRoot, quiet = false) {
|
|
214
|
+
const logDir = path.join(backendRoot, 'atris/logs');
|
|
215
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
216
|
+
const logPath = path.join(logDir, 'learning-feed.server.log');
|
|
217
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
218
|
+
|
|
219
|
+
const proc = spawn(venvPython, ['experiments/learning-feed/app.py'], {
|
|
220
|
+
cwd: backendRoot,
|
|
221
|
+
detached: true,
|
|
222
|
+
stdio: ['ignore', logFd, logFd],
|
|
223
|
+
});
|
|
224
|
+
proc.unref();
|
|
225
|
+
fs.closeSync(logFd);
|
|
226
|
+
if (!quiet) {
|
|
227
|
+
console.log(' Feed server starting (log: atris/logs/learning-feed.server.log)');
|
|
228
|
+
}
|
|
229
|
+
return proc.pid;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function ensureFeedServer(venvPython, backendRoot) {
|
|
233
|
+
if (await probeFeedStats()) {
|
|
234
|
+
console.log(' Feed server already running.');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
spawnFeedServer(venvPython, backendRoot);
|
|
239
|
+
const deadline = Date.now() + SERVER_WAIT_MS;
|
|
240
|
+
while (Date.now() < deadline) {
|
|
241
|
+
await sleep(SERVER_POLL_MS);
|
|
242
|
+
if (await probeFeedStats()) {
|
|
243
|
+
console.log(` Feed server ready on port ${FEED_PORT}.`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
console.error(`✗ Feed server did not respond on port ${FEED_PORT} within ${SERVER_WAIT_MS / 1000}s.`);
|
|
248
|
+
console.error(' Check atris/logs/learning-feed.server.log in the backend repo.');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function tryEnsureFeedServerSilent(venvPython, backendRoot) {
|
|
253
|
+
if (await probeFeedStats()) return true;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
spawnFeedServer(venvPython, backendRoot, true);
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const deadline = Date.now() + SERVER_WAIT_MS;
|
|
262
|
+
while (Date.now() < deadline) {
|
|
263
|
+
await sleep(SERVER_POLL_MS);
|
|
264
|
+
if (await probeFeedStats()) return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runIngest(venvPython, backendRoot, topic, personal) {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const args = ['experiments/learning-feed/ingest.py', 'learn', topic];
|
|
272
|
+
if (personal) args.push('--personal');
|
|
273
|
+
|
|
274
|
+
console.log(`\n Ingest: venv/bin/python ${args.join(' ')}\n`);
|
|
275
|
+
const proc = spawn(venvPython, args, {
|
|
276
|
+
cwd: backendRoot,
|
|
277
|
+
stdio: 'inherit',
|
|
278
|
+
});
|
|
279
|
+
proc.on('error', reject);
|
|
280
|
+
proc.on('close', (code) => {
|
|
281
|
+
if (code === 0) resolve();
|
|
282
|
+
else reject(new Error(`ingest.py learn exited with code ${code}`));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function openFeed() {
|
|
288
|
+
if (process.platform === 'darwin') {
|
|
289
|
+
spawnSync('open', [FEED_URL], { stdio: 'ignore' });
|
|
290
|
+
} else if (process.platform === 'win32') {
|
|
291
|
+
spawnSync('cmd', ['/c', 'start', '', FEED_URL], { stdio: 'ignore' });
|
|
292
|
+
} else {
|
|
293
|
+
spawnSync('xdg-open', [FEED_URL], { stdio: 'ignore' });
|
|
294
|
+
}
|
|
295
|
+
console.log(`\n Opened ${FEED_URL}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function wrapText(text, width = CARD_WRAP_COLS) {
|
|
299
|
+
const words = String(text || '').trim().split(/\s+/).filter(Boolean);
|
|
300
|
+
if (!words.length) return [''];
|
|
301
|
+
const lines = [];
|
|
302
|
+
let line = words[0];
|
|
303
|
+
for (let i = 1; i < words.length; i += 1) {
|
|
304
|
+
const next = `${line} ${words[i]}`;
|
|
305
|
+
if (next.length <= width) {
|
|
306
|
+
line = next;
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(line);
|
|
309
|
+
line = words[i];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
lines.push(line);
|
|
313
|
+
return lines;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function restoreTty() {
|
|
317
|
+
if (process.stdin.isTTY) {
|
|
318
|
+
process.stdin.setRawMode(false);
|
|
319
|
+
process.stdin.pause();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function replayCaptured(chunks) {
|
|
324
|
+
for (const chunk of chunks) {
|
|
325
|
+
if (chunk.stream === 'stderr') {
|
|
326
|
+
process.stderr.write(chunk.data);
|
|
327
|
+
} else {
|
|
328
|
+
process.stdout.write(chunk.data);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function createWhileChild(cmd) {
|
|
334
|
+
const chunks = [];
|
|
335
|
+
let exitCode = 0;
|
|
336
|
+
let running = true;
|
|
337
|
+
let liveStream = false;
|
|
338
|
+
let replayIndex = 0;
|
|
339
|
+
|
|
340
|
+
const child = spawn(cmd, {
|
|
341
|
+
shell: true,
|
|
342
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
343
|
+
detached: process.platform !== 'win32',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
function append(stream, data) {
|
|
347
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
348
|
+
chunks.push({ stream, data: buf });
|
|
349
|
+
if (liveStream) {
|
|
350
|
+
if (stream === 'stderr') process.stderr.write(buf);
|
|
351
|
+
else process.stdout.write(buf);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
child.stdout.on('data', (data) => append('stdout', data));
|
|
356
|
+
child.stderr.on('data', (data) => append('stderr', data));
|
|
357
|
+
|
|
358
|
+
const done = new Promise((resolve) => {
|
|
359
|
+
child.on('close', (code) => {
|
|
360
|
+
running = false;
|
|
361
|
+
exitCode = code == null ? 1 : code;
|
|
362
|
+
resolve(exitCode);
|
|
363
|
+
});
|
|
364
|
+
child.on('error', () => {
|
|
365
|
+
running = false;
|
|
366
|
+
exitCode = 1;
|
|
367
|
+
resolve(exitCode);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
function startLiveStream() {
|
|
372
|
+
liveStream = true;
|
|
373
|
+
while (replayIndex < chunks.length) {
|
|
374
|
+
const chunk = chunks[replayIndex];
|
|
375
|
+
if (chunk.stream === 'stderr') process.stderr.write(chunk.data);
|
|
376
|
+
else process.stdout.write(chunk.data);
|
|
377
|
+
replayIndex += 1;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function killProcessGroup() {
|
|
382
|
+
if (!child.pid) return;
|
|
383
|
+
try {
|
|
384
|
+
if (process.platform !== 'win32') process.kill(-child.pid, 'SIGTERM');
|
|
385
|
+
else child.kill('SIGTERM');
|
|
386
|
+
} catch {
|
|
387
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
child,
|
|
393
|
+
chunks,
|
|
394
|
+
done,
|
|
395
|
+
get running() { return running; },
|
|
396
|
+
get exitCode() { return exitCode; },
|
|
397
|
+
startLiveStream,
|
|
398
|
+
killProcessGroup,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function fetchNextCard() {
|
|
403
|
+
return httpGetJson(FEED_NEXT_URL, 3000);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function postCardAnswer(cardId, action, dwellMs) {
|
|
407
|
+
await httpPostJson(FEED_ANSWER_URL, {
|
|
408
|
+
card_id: cardId == null ? null : cardId,
|
|
409
|
+
action,
|
|
410
|
+
dwell_ms: Math.max(0, Math.round(dwellMs || 0)),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function fetchFeedStats() {
|
|
415
|
+
return httpGetJson(FEED_STATS_URL, 3000);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderCardScreen(lines) {
|
|
419
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
420
|
+
for (const line of lines) {
|
|
421
|
+
process.stdout.write(`${line}\n`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function cardLines(card, beat) {
|
|
426
|
+
const lines = [NAME_TAG, ''];
|
|
427
|
+
const isProbe = card.type === 'probe';
|
|
428
|
+
|
|
429
|
+
if (isProbe && beat === 0) {
|
|
430
|
+
lines.push(...wrapText('Remember this?'));
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push(' space → reveal q → show build output');
|
|
433
|
+
return lines;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const claim = card.claim || '';
|
|
437
|
+
const why = card.why || '';
|
|
438
|
+
lines.push(...wrapText(claim));
|
|
439
|
+
|
|
440
|
+
if (beat >= 1 || !isProbe) {
|
|
441
|
+
lines.push('');
|
|
442
|
+
lines.push(...wrapText(why));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
lines.push('');
|
|
446
|
+
if (isProbe && beat >= 1) {
|
|
447
|
+
lines.push(' c correct · w wrong · space → next');
|
|
448
|
+
} else if (isProbe) {
|
|
449
|
+
lines.push(' space → reveal');
|
|
450
|
+
} else if (beat === 0) {
|
|
451
|
+
lines.push(' space → why');
|
|
452
|
+
} else {
|
|
453
|
+
lines.push(' space → next card · q → show build output');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return lines;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function waitForKeyOnce() {
|
|
460
|
+
return new Promise((resolve) => {
|
|
461
|
+
const onData = (buf) => {
|
|
462
|
+
process.stdin.removeListener('data', onData);
|
|
463
|
+
resolve(buf);
|
|
464
|
+
};
|
|
465
|
+
process.stdin.on('data', onData);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function waitForKeyOrChildDone(childState) {
|
|
470
|
+
return Promise.race([
|
|
471
|
+
waitForKeyOnce(),
|
|
472
|
+
childState.done.then(() => ({ childDone: true })),
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function runCardLoop(childState) {
|
|
477
|
+
let cardsShown = 0;
|
|
478
|
+
let currentCard = null;
|
|
479
|
+
let beat = 0;
|
|
480
|
+
let cardStart = Date.now();
|
|
481
|
+
let cardsActive = true;
|
|
482
|
+
|
|
483
|
+
process.stdin.setRawMode(true);
|
|
484
|
+
process.stdin.resume();
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
while (childState.running && cardsActive) {
|
|
488
|
+
if (!currentCard) {
|
|
489
|
+
const next = await fetchNextCard();
|
|
490
|
+
if (!next || !next.type) {
|
|
491
|
+
await sleep(400);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
currentCard = next;
|
|
495
|
+
beat = 0;
|
|
496
|
+
cardStart = Date.now();
|
|
497
|
+
cardsShown += 1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
renderCardScreen(cardLines(currentCard, beat));
|
|
501
|
+
|
|
502
|
+
const input = await waitForKeyOrChildDone(childState);
|
|
503
|
+
if (input && input.childDone) break;
|
|
504
|
+
|
|
505
|
+
const keyBuf = Buffer.isBuffer(input) ? input : Buffer.from(String(input || ''));
|
|
506
|
+
const byte = keyBuf[0];
|
|
507
|
+
|
|
508
|
+
if (byte === 3) {
|
|
509
|
+
restoreTty();
|
|
510
|
+
childState.killProcessGroup();
|
|
511
|
+
await sleep(100);
|
|
512
|
+
replayCaptured(childState.chunks);
|
|
513
|
+
process.exit(130);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const key = String.fromCharCode(byte).toLowerCase();
|
|
517
|
+
|
|
518
|
+
if (key === 'q') {
|
|
519
|
+
cardsActive = false;
|
|
520
|
+
childState.startLiveStream();
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const isProbe = currentCard.type === 'probe';
|
|
525
|
+
|
|
526
|
+
if (key === 'c' && isProbe && beat >= 1) {
|
|
527
|
+
await postCardAnswer(currentCard.card_id, 'correct', Date.now() - cardStart);
|
|
528
|
+
currentCard = null;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (key === 'w' && isProbe && beat >= 1) {
|
|
533
|
+
await postCardAnswer(currentCard.card_id, 'wrong', Date.now() - cardStart);
|
|
534
|
+
currentCard = null;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (key !== ' ' && key !== '\r' && key !== '\n') continue;
|
|
539
|
+
|
|
540
|
+
if (isProbe && beat === 0) {
|
|
541
|
+
beat = 1;
|
|
542
|
+
await postCardAnswer(currentCard.card_id, 'reveal', Date.now() - cardStart);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (isProbe && beat >= 1) {
|
|
547
|
+
currentCard = null;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (beat === 0) {
|
|
552
|
+
beat = 1;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (currentCard.card_id && currentCard.type !== 'progress' && currentCard.type !== 'deepdive') {
|
|
557
|
+
await postCardAnswer(currentCard.card_id, 'skip', Date.now() - cardStart);
|
|
558
|
+
}
|
|
559
|
+
currentCard = null;
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
restoreTty();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!cardsActive && childState.running) {
|
|
566
|
+
await childState.done;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return cardsShown;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function printSessionLine(exitCode, cardsShown, statsAvailable) {
|
|
573
|
+
let locked = 0;
|
|
574
|
+
let retention = 0;
|
|
575
|
+
if (statsAvailable) {
|
|
576
|
+
const stats = await fetchFeedStats();
|
|
577
|
+
if (stats) {
|
|
578
|
+
locked = Number(stats.locked_total || 0);
|
|
579
|
+
retention = Number(stats.retention_percent || 0);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const cardPart = `${cardsShown} card${cardsShown === 1 ? '' : 's'}`;
|
|
583
|
+
if (statsAvailable) {
|
|
584
|
+
console.log(`done · exit ${exitCode} · ${cardPart} · ${locked} locked in · ${retention}% retention`);
|
|
585
|
+
} else {
|
|
586
|
+
console.log(`done · exit ${exitCode} · ${cardPart}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function runWhileCapture(cmd, { cards = false, statsAvailable = false } = {}) {
|
|
591
|
+
const childState = createWhileChild(cmd);
|
|
592
|
+
let cardsShown = 0;
|
|
593
|
+
|
|
594
|
+
if (cards) {
|
|
595
|
+
cardsShown = await runCardLoop(childState);
|
|
596
|
+
} else {
|
|
597
|
+
await childState.done;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
restoreTty();
|
|
601
|
+
await printSessionLine(childState.exitCode, cardsShown, statsAvailable);
|
|
602
|
+
replayCaptured(childState.chunks);
|
|
603
|
+
return childState.exitCode;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function runWhileMode(opts) {
|
|
607
|
+
const cmd = opts.whileCmd;
|
|
608
|
+
const topic = opts.topic;
|
|
609
|
+
|
|
610
|
+
if (topic) {
|
|
611
|
+
const venvPython = requireBackend();
|
|
612
|
+
const language = detectLanguage(topic);
|
|
613
|
+
if (language) printTutorLoopBlock(BACKEND_ROOT, language);
|
|
614
|
+
try {
|
|
615
|
+
await runIngest(venvPython, BACKEND_ROOT, topic, opts.personal);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.error(`\n✗ ${err.message || err}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!process.stdin.isTTY) {
|
|
622
|
+
return runWhileCapture(cmd, { cards: false, statsAvailable: false });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let statsAvailable = false;
|
|
626
|
+
let useCards = false;
|
|
627
|
+
|
|
628
|
+
if (fs.existsSync(BACKEND_ROOT) && fs.existsSync(path.join(BACKEND_ROOT, 'venv/bin/python'))) {
|
|
629
|
+
const venvPython = path.join(BACKEND_ROOT, 'venv/bin/python');
|
|
630
|
+
statsAvailable = await tryEnsureFeedServerSilent(venvPython, BACKEND_ROOT);
|
|
631
|
+
useCards = statsAvailable;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return runWhileCapture(cmd, { cards: useCards, statsAvailable });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function run(argv = []) {
|
|
638
|
+
let opts;
|
|
639
|
+
try {
|
|
640
|
+
opts = parseArgs(argv);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(`✗ ${err.message}`);
|
|
643
|
+
showHelp();
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (opts.help) {
|
|
648
|
+
showHelp();
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (opts.whileCmd) {
|
|
653
|
+
if (!opts.whileCmd.trim()) {
|
|
654
|
+
console.error('✗ --while requires a non-empty command string');
|
|
655
|
+
showHelp();
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
return runWhileMode(opts);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (!opts.topic) {
|
|
662
|
+
showHelp();
|
|
663
|
+
return 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const venvPython = requireBackend();
|
|
667
|
+
const language = detectLanguage(opts.topic);
|
|
668
|
+
|
|
669
|
+
if (language) {
|
|
670
|
+
printTutorLoopBlock(BACKEND_ROOT, language);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
await runIngest(venvPython, BACKEND_ROOT, opts.topic, opts.personal);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error(`\n✗ ${err.message || err}`);
|
|
677
|
+
return 1;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
await ensureFeedServer(venvPython, BACKEND_ROOT);
|
|
681
|
+
openFeed();
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
module.exports = {
|
|
686
|
+
run,
|
|
687
|
+
parseArgs,
|
|
688
|
+
detectLanguage,
|
|
689
|
+
showHelp,
|
|
690
|
+
BACKEND_ROOT,
|
|
691
|
+
wrapText,
|
|
692
|
+
createWhileChild,
|
|
693
|
+
};
|