atris 3.35.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 +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +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 +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
package/commands/improve.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* This is the CLI entrypoint for the headline paid capability. The member
|
|
13
13
|
* loop and the /improve skill both call it. If the backend is unreachable
|
|
14
|
-
* or the user is not logged in, it falls back to
|
|
14
|
+
* or the user is not logged in, it falls back to one local mission tick
|
|
15
15
|
* (same loop, local inference) instead of erroring silently.
|
|
16
16
|
*
|
|
17
17
|
* The orchestrator (runImprove) takes injected deps so the network, auth,
|
|
@@ -24,6 +24,11 @@ const path = require('path');
|
|
|
24
24
|
const { spawnSync } = require('child_process');
|
|
25
25
|
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
26
26
|
const { loadCredentials } = require('../utils/auth');
|
|
27
|
+
const pulse = require('../lib/pulse');
|
|
28
|
+
const { cronInstalled } = require('./pulse');
|
|
29
|
+
const close = require('./close');
|
|
30
|
+
const { readUsage } = require('../lib/usage');
|
|
31
|
+
const { knownCommands } = require('../lib/known-commands');
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Expand a leading `~` to the real home directory for LOCAL filesystem
|
|
@@ -40,8 +45,10 @@ function expandHome(p) {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
const SCORECARD_SCHEMA = 'atris.improve_tick.v1';
|
|
48
|
+
const IMPROVE_VITALS_SCHEMA = 'atris.improve_vitals.v1';
|
|
43
49
|
const DEFAULT_TIMEOUT_MS = 300000;
|
|
44
50
|
const VALID_MODES = new Set(['full', 'plan', 'delegate']);
|
|
51
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
54
|
* Resolve the improve endpoint path relative to the configured API base.
|
|
@@ -54,6 +61,22 @@ function improveApiPath(baseUrl) {
|
|
|
54
61
|
return base.endsWith('/api') ? '/improve' : '/api/improve';
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
function hostedApiCannotReachLocalWorkspace(workspace, baseUrl) {
|
|
65
|
+
if (!path.isAbsolute(String(workspace || ''))) return false;
|
|
66
|
+
try {
|
|
67
|
+
if (!fs.statSync(workspace).isDirectory()) return false;
|
|
68
|
+
const hostname = new URL(baseUrl).hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
69
|
+
const loopback = hostname === 'localhost'
|
|
70
|
+
|| hostname.endsWith('.localhost')
|
|
71
|
+
|| hostname === '::1'
|
|
72
|
+
|| hostname === '0.0.0.0'
|
|
73
|
+
|| hostname.startsWith('127.');
|
|
74
|
+
return !loopback;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
function parseImproveArgs(argv = []) {
|
|
58
81
|
const args = Array.isArray(argv) ? [...argv] : [];
|
|
59
82
|
const opts = {
|
|
@@ -133,7 +156,7 @@ function summarizeImproveResponse(data = {}) {
|
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
/**
|
|
136
|
-
* Decide whether to fall back to
|
|
159
|
+
* Decide whether to fall back to one local mission tick. Fallback only when
|
|
137
160
|
* the backend is genuinely unavailable: no auth, or unreachable (status 0).
|
|
138
161
|
* A real HTTP error (insufficient credits 402, server error 5xx) is reported
|
|
139
162
|
* honestly — we never silently run local work and bill nothing on what was a
|
|
@@ -144,9 +167,28 @@ function shouldFallbackLocal({ creds, apiResult } = {}) {
|
|
|
144
167
|
if (!apiResult) return { fallback: false, reason: 'no_result' };
|
|
145
168
|
if (apiResult.ok) return { fallback: false, reason: 'api_ok' };
|
|
146
169
|
if (apiResult.status === 0) return { fallback: true, reason: 'unreachable' };
|
|
170
|
+
// The hosted backend validates workspace_path against its own filesystem,
|
|
171
|
+
// so a local-only folder 403s even for an authed, funded user. That is an
|
|
172
|
+
// unreachable-workspace condition — run the same tick locally instead of
|
|
173
|
+
// dying on it. Other 403s (real permission failures) are still reported.
|
|
174
|
+
if (apiResult.status === 403 && isWorkspaceNotAllowedError(apiResult)) {
|
|
175
|
+
return { fallback: true, reason: 'workspace_not_on_backend' };
|
|
176
|
+
}
|
|
147
177
|
return { fallback: false, reason: `api_error_${apiResult.status}` };
|
|
148
178
|
}
|
|
149
179
|
|
|
180
|
+
const WORKSPACE_NOT_ALLOWED_TEXT = 'workspace_path must be under an allowed directory';
|
|
181
|
+
|
|
182
|
+
function isWorkspaceNotAllowedError(apiResult = {}) {
|
|
183
|
+
const candidates = [
|
|
184
|
+
apiResult.error,
|
|
185
|
+
apiResult.error && apiResult.error.error,
|
|
186
|
+
apiResult.data && apiResult.data.detail,
|
|
187
|
+
apiResult.data && apiResult.data.detail && apiResult.data.detail.error,
|
|
188
|
+
];
|
|
189
|
+
return candidates.some((c) => typeof c === 'string' && c.includes(WORKSPACE_NOT_ALLOWED_TEXT));
|
|
190
|
+
}
|
|
191
|
+
|
|
150
192
|
function buildScorecardRow(summary = {}, meta = {}) {
|
|
151
193
|
return {
|
|
152
194
|
schema: SCORECARD_SCHEMA,
|
|
@@ -249,6 +291,239 @@ function localHourMinute(d = new Date()) {
|
|
|
249
291
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
250
292
|
}
|
|
251
293
|
|
|
294
|
+
function readJsonFile(file, fallback = null) {
|
|
295
|
+
try {
|
|
296
|
+
if (!fs.existsSync(file)) return fallback;
|
|
297
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
298
|
+
} catch {
|
|
299
|
+
return fallback;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readJsonlFile(file) {
|
|
304
|
+
try {
|
|
305
|
+
if (!fs.existsSync(file)) return [];
|
|
306
|
+
return fs.readFileSync(file, 'utf8')
|
|
307
|
+
.split(/\r?\n/)
|
|
308
|
+
.map((line) => line.trim())
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.map((line) => {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(line);
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
} catch {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function timestampMs(value) {
|
|
324
|
+
const ms = Date.parse(String(value || ''));
|
|
325
|
+
return Number.isFinite(ms) ? ms : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function plural(n, word) {
|
|
329
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formatReward(value) {
|
|
333
|
+
const n = Number(value) || 0;
|
|
334
|
+
if (Number.isInteger(n)) return String(n);
|
|
335
|
+
return String(Number(n.toFixed(2)));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function agePhrase(ts, nowMs = Date.now()) {
|
|
339
|
+
const ms = timestampMs(ts);
|
|
340
|
+
if (ms == null) return null;
|
|
341
|
+
const delta = Math.max(0, nowMs - ms);
|
|
342
|
+
if (delta < 60 * 1000) return 'just now';
|
|
343
|
+
if (delta < 60 * 60 * 1000) return `${plural(Math.round(delta / (60 * 1000)), 'minute')} ago`;
|
|
344
|
+
if (delta < DAY_MS) return `${plural(Math.round(delta / (60 * 60 * 1000)), 'hour')} ago`;
|
|
345
|
+
if (delta < 30 * DAY_MS) return `${plural(Math.round(delta / DAY_MS), 'day')} ago`;
|
|
346
|
+
return `on ${new Date(ms).toISOString().slice(0, 10)}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function plainSentence(value) {
|
|
350
|
+
return String(value || '')
|
|
351
|
+
.replace(/\s+/g, ' ')
|
|
352
|
+
.trim()
|
|
353
|
+
.toLowerCase();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function latestByTime(rows, fields) {
|
|
357
|
+
let latest = null;
|
|
358
|
+
let latestMs = -Infinity;
|
|
359
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
360
|
+
for (const field of fields) {
|
|
361
|
+
const ms = timestampMs(row && row[field]);
|
|
362
|
+
if (ms != null && ms > latestMs) {
|
|
363
|
+
latest = row;
|
|
364
|
+
latestMs = ms;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return latest ? { row: latest, ms: latestMs } : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function scoutFindingLanded(row = {}) {
|
|
372
|
+
if (!row || typeof row !== 'object') return false;
|
|
373
|
+
if (row.finding_landed === true || row.finding === true) return true;
|
|
374
|
+
if (Array.isArray(row.findings) && row.findings.length > 0) return true;
|
|
375
|
+
if (row.result && typeof row.result === 'object') {
|
|
376
|
+
if (row.result.finding_landed === true || row.result.finding === true) return true;
|
|
377
|
+
if (Array.isArray(row.result.findings) && row.result.findings.length > 0) return true;
|
|
378
|
+
}
|
|
379
|
+
const landing = row.last_landing || (row.result && row.result.landing) || row.landing;
|
|
380
|
+
if (landing && typeof landing === 'object') {
|
|
381
|
+
const text = `${landing.finding || ''} ${landing.findings || ''}`.trim();
|
|
382
|
+
if (text) return true;
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function collectImproveVitals(options = {}, deps = {}) {
|
|
388
|
+
const root = expandHome(options.workspace || process.cwd());
|
|
389
|
+
const nowDate = options.now ? new Date(options.now) : new Date();
|
|
390
|
+
const nowMs = Number.isFinite(nowDate.getTime()) ? nowDate.getTime() : Date.now();
|
|
391
|
+
const today = localDateKey(new Date(nowMs));
|
|
392
|
+
|
|
393
|
+
const readPulse = deps.readPulseReceipts || pulse.readPulseReceipts;
|
|
394
|
+
const pulsePath = (deps.pulseReceiptsPath || pulse.pulseReceiptsPath)(root);
|
|
395
|
+
const receipts = readPulse(root);
|
|
396
|
+
const finished = (Array.isArray(receipts) ? receipts : []).filter((row) => row && row.phase === 'finished');
|
|
397
|
+
const latestPulse = latestByTime(finished, ['ts']);
|
|
398
|
+
const rewardSince = nowMs - DAY_MS;
|
|
399
|
+
const rewardToday = finished.reduce((sum, row) => {
|
|
400
|
+
const ms = timestampMs(row.ts);
|
|
401
|
+
return ms != null && ms >= rewardSince ? sum + (Number(row.reward) || 0) : sum;
|
|
402
|
+
}, 0);
|
|
403
|
+
const heartbeatAge = latestPulse ? agePhrase(latestPulse.row.ts, nowMs) : null;
|
|
404
|
+
const heartbeatSentence = latestPulse
|
|
405
|
+
? `the scheduled improve heartbeat last beat ${heartbeatAge} and earned ${formatReward(rewardToday)} reward today.`
|
|
406
|
+
: `the scheduled improve heartbeat has not beaten yet and earned ${formatReward(rewardToday)} reward today.`;
|
|
407
|
+
const cronFn = deps.cronInstalled || cronInstalled;
|
|
408
|
+
// Per-repo slots (pr 310): the crontab marker is derived from the root, so
|
|
409
|
+
// the check must ask about THIS repo's markers, not the legacy default.
|
|
410
|
+
const slotMarkers = (() => {
|
|
411
|
+
try { return pulse.resolvePulseSlot(root).markers; } catch { return undefined; }
|
|
412
|
+
})();
|
|
413
|
+
const installed = Boolean(slotMarkers ? cronFn(slotMarkers) : cronFn());
|
|
414
|
+
const installNudge = installed ? null : 'the scheduled improve loop is off. turn it on: atris pulse install --model claude-sonnet-5';
|
|
415
|
+
|
|
416
|
+
const experimentsPath = path.join(root, '.atris', 'state', 'experiments-daily.json');
|
|
417
|
+
const experiments = readJsonFile(experimentsPath, {});
|
|
418
|
+
const history = Array.isArray(experiments && experiments.history) ? experiments.history : [];
|
|
419
|
+
const experimentRanToday = String(experiments && experiments.last_run_date || '') === today;
|
|
420
|
+
const exploitSentence = `${experimentRanToday ? 'todays experiment already ran' : 'no experiment yet today'}, with ${plural(history.length, 'total experiment')}.`;
|
|
421
|
+
|
|
422
|
+
const missionsPath = path.join(root, '.atris', 'state', 'missions.jsonl');
|
|
423
|
+
const missions = readJsonlFile(missionsPath);
|
|
424
|
+
const scoutMissions = missions.filter((row) => {
|
|
425
|
+
const owner = String(row && row.owner || '').toLowerCase();
|
|
426
|
+
return owner === 'scout' || owner === 'signal-scout' || owner.endsWith('-scout');
|
|
427
|
+
});
|
|
428
|
+
const latestScout = latestByTime(scoutMissions, [
|
|
429
|
+
'last_tick_at',
|
|
430
|
+
'last_tick_finished_at',
|
|
431
|
+
'finished_at',
|
|
432
|
+
'updated_at',
|
|
433
|
+
'created_at',
|
|
434
|
+
'started_at',
|
|
435
|
+
]);
|
|
436
|
+
const scoutAge = latestScout ? agePhrase(new Date(latestScout.ms).toISOString(), nowMs) : null;
|
|
437
|
+
const findingLanded = latestScout ? scoutFindingLanded(latestScout.row) : false;
|
|
438
|
+
const exploreSentence = latestScout
|
|
439
|
+
? `the scout last explored ${scoutAge} and ${findingLanded ? 'landed a finding' : 'no finding landed'}.`
|
|
440
|
+
: 'the scout has not explored yet and no finding landed.';
|
|
441
|
+
|
|
442
|
+
const openFlags = (deps.openFlags || close.openFlags)(root, { now: new Date(nowMs) });
|
|
443
|
+
const sweep = (deps.sweepState || close.sweepState)(root, new Date(nowMs), { dryRun: true });
|
|
444
|
+
const overdue = Array.isArray(sweep.overdue) ? sweep.overdue : openFlags.filter((flag) => flag && flag.overdue);
|
|
445
|
+
const topOverdueSentence = overdue[0] ? plainSentence((deps.sweepLine || close.sweepLine)(overdue[0])) : null;
|
|
446
|
+
const excreteSentence = `the excretion loop has ${plural(openFlags.length, 'open loop')} and ${plural(overdue.length, 'overdue loop')}.`;
|
|
447
|
+
|
|
448
|
+
const usageRows = (deps.readUsage || readUsage)(root, { sinceDays: 7, now: new Date(nowMs).toISOString() });
|
|
449
|
+
const known = deps.knownCommands || knownCommands;
|
|
450
|
+
const knownSet = new Set(known);
|
|
451
|
+
const usedThisWeek = new Set((Array.isArray(usageRows) ? usageRows : [])
|
|
452
|
+
.map((row) => row && row.cmd)
|
|
453
|
+
.filter((cmd) => knownSet.has(cmd)));
|
|
454
|
+
const usageSentence = `you used ${usedThisWeek.size} of ${known.length} known commands this week.`;
|
|
455
|
+
|
|
456
|
+
const heartbeat = {
|
|
457
|
+
receipts_path_exists: fs.existsSync(pulsePath),
|
|
458
|
+
last_finished_at: latestPulse ? latestPulse.row.ts : null,
|
|
459
|
+
last_finished_ago: heartbeatAge,
|
|
460
|
+
reward_last_24h: Number(formatReward(rewardToday)),
|
|
461
|
+
cron_installed: installed,
|
|
462
|
+
sentence: plainSentence(heartbeatSentence),
|
|
463
|
+
};
|
|
464
|
+
const exploit = {
|
|
465
|
+
ran_today: experimentRanToday,
|
|
466
|
+
total_experiments: history.length,
|
|
467
|
+
last_run_date: experiments && experiments.last_run_date || null,
|
|
468
|
+
sentence: plainSentence(exploitSentence),
|
|
469
|
+
};
|
|
470
|
+
const explore = {
|
|
471
|
+
total_scout_missions: scoutMissions.length,
|
|
472
|
+
last_tick_at: latestScout ? new Date(latestScout.ms).toISOString() : null,
|
|
473
|
+
last_tick_ago: scoutAge,
|
|
474
|
+
finding_landed: findingLanded,
|
|
475
|
+
sentence: plainSentence(exploreSentence),
|
|
476
|
+
};
|
|
477
|
+
const excrete = {
|
|
478
|
+
open: openFlags.length,
|
|
479
|
+
overdue: overdue.length,
|
|
480
|
+
top_overdue_sentence: topOverdueSentence,
|
|
481
|
+
sentence: plainSentence(excreteSentence),
|
|
482
|
+
};
|
|
483
|
+
const usage = {
|
|
484
|
+
used_this_week: usedThisWeek.size,
|
|
485
|
+
known_commands: known.length,
|
|
486
|
+
sentence: plainSentence(usageSentence),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const sentences = [
|
|
490
|
+
heartbeat.sentence,
|
|
491
|
+
exploit.sentence,
|
|
492
|
+
explore.sentence,
|
|
493
|
+
excrete.sentence,
|
|
494
|
+
...(topOverdueSentence ? [`the top overdue loop says ${topOverdueSentence}`] : []),
|
|
495
|
+
usage.sentence,
|
|
496
|
+
];
|
|
497
|
+
const groups = [
|
|
498
|
+
[heartbeat.sentence, installNudge].filter(Boolean),
|
|
499
|
+
[exploit.sentence],
|
|
500
|
+
[explore.sentence],
|
|
501
|
+
[excrete.sentence, ...(topOverdueSentence ? [`the top overdue loop says ${topOverdueSentence}`] : [])],
|
|
502
|
+
[usage.sentence],
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
schema: IMPROVE_VITALS_SCHEMA,
|
|
507
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
508
|
+
heartbeat,
|
|
509
|
+
exploit,
|
|
510
|
+
explore,
|
|
511
|
+
excrete,
|
|
512
|
+
usage,
|
|
513
|
+
install_nudge: installNudge,
|
|
514
|
+
sentences,
|
|
515
|
+
groups,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function formatImproveVitals(vitals = {}) {
|
|
520
|
+
const groups = Array.isArray(vitals.groups) ? vitals.groups : [];
|
|
521
|
+
return groups
|
|
522
|
+
.map((group) => group.filter(Boolean).map(plainSentence).join('\n'))
|
|
523
|
+
.filter(Boolean)
|
|
524
|
+
.join('\n\n');
|
|
525
|
+
}
|
|
526
|
+
|
|
252
527
|
/**
|
|
253
528
|
* Append a human-readable tick entry to today's journal under ## Notes.
|
|
254
529
|
* The skill contract says every tick lands in the journal; the JSONL
|
|
@@ -288,23 +563,108 @@ function resolveAtrisBin() {
|
|
|
288
563
|
return process.env.ATRIS_BIN || 'atris';
|
|
289
564
|
}
|
|
290
565
|
|
|
566
|
+
// The improve contract is one call -> one tick. Use the mission runtime
|
|
567
|
+
// directly so the tick count and verifier result come back as structured JSON
|
|
568
|
+
// instead of hiding several mission ticks inside one autopilot leg.
|
|
569
|
+
const LOCAL_FALLBACK_ARGS = ['mission', 'run', '--due', '--headless', '--max-ticks', '1', '--complete-on-pass', '--json'];
|
|
570
|
+
|
|
571
|
+
function localFallbackArgs(budgetSec) {
|
|
572
|
+
return LOCAL_FALLBACK_ARGS.concat(['--max-wall', String(Math.max(60, Math.round(budgetSec)))]);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function localSummaryText(tick = {}, mission = {}) {
|
|
576
|
+
const candidates = [
|
|
577
|
+
tick.claude && tick.claude.summary,
|
|
578
|
+
tick.atris2 && tick.atris2.summary,
|
|
579
|
+
tick.drill && tick.drill.summary,
|
|
580
|
+
mission.objective,
|
|
581
|
+
];
|
|
582
|
+
return String(candidates.find((value) => String(value || '').trim()) || 'verified local improvement')
|
|
583
|
+
.replace(/\s+/g, ' ')
|
|
584
|
+
.trim();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Turn one mission-run JSON result into the same stable summary the paid API
|
|
589
|
+
* returns. A local tick earns the conservative local reward (+1) only after
|
|
590
|
+
* its real verifier passes; missing proof is a failed improve call.
|
|
591
|
+
*/
|
|
592
|
+
function summarizeLocalMissionRun(payload = {}) {
|
|
593
|
+
const ticks = Array.isArray(payload.ticks) ? payload.ticks : [];
|
|
594
|
+
const tickCount = Number(payload.tick_count != null ? payload.tick_count : ticks.length);
|
|
595
|
+
if (!payload.ok || payload.action !== 'mission_run') {
|
|
596
|
+
throw new Error(`local improve did not run a mission tick${payload.reason ? `: ${payload.reason}` : ''}`);
|
|
597
|
+
}
|
|
598
|
+
if (tickCount !== 1 || ticks.length !== 1) {
|
|
599
|
+
throw new Error(`local improve must run exactly one tick; got ${tickCount}`);
|
|
600
|
+
}
|
|
601
|
+
const tick = ticks[0] || {};
|
|
602
|
+
if (tick.status !== 'ran') {
|
|
603
|
+
throw new Error(`local improve tick did not run${tick.reason ? `: ${tick.reason}` : ''}`);
|
|
604
|
+
}
|
|
605
|
+
if (tick.claude?.skipped === true || ['caller-session-runner', 'no-claude-mode'].includes(tick.reason)) {
|
|
606
|
+
throw new Error('local improve worker did not run');
|
|
607
|
+
}
|
|
608
|
+
if (tick.verifier_passed !== true) {
|
|
609
|
+
throw new Error('local improve verifier did not pass');
|
|
610
|
+
}
|
|
611
|
+
const mission = payload.mission || {};
|
|
612
|
+
const files = tick.worktree && Array.isArray(tick.worktree.new_since_baseline_sample)
|
|
613
|
+
? tick.worktree.new_since_baseline_sample
|
|
614
|
+
: [];
|
|
615
|
+
return {
|
|
616
|
+
shipped: localSummaryText(tick, mission),
|
|
617
|
+
reward: 1,
|
|
618
|
+
verify: true,
|
|
619
|
+
credits: 0,
|
|
620
|
+
files,
|
|
621
|
+
model: mission.model || mission.runner || null,
|
|
622
|
+
taskId: mission.task_id || mission.current_task_id || null,
|
|
623
|
+
elapsedMs: null,
|
|
624
|
+
scorecardWritten: false,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
291
628
|
function runLocalFallback(opts = {}) {
|
|
292
629
|
const bin = resolveAtrisBin();
|
|
293
630
|
const isScript = bin.endsWith('.js');
|
|
294
631
|
const cmd = isScript ? process.execPath : bin;
|
|
295
|
-
|
|
632
|
+
// Tell autopilot its time budget so it lands gracefully ("budget spent")
|
|
633
|
+
// instead of being SIGKILLed mid-leg and reporting a failed tick. The spawn
|
|
634
|
+
// timeout stays as a backstop, one minute past the budget.
|
|
635
|
+
const budgetSec = Math.max(60, Number(opts.timeoutSec) || 600);
|
|
636
|
+
const argv = (isScript ? [bin] : []).concat(localFallbackArgs(budgetSec));
|
|
296
637
|
const r = spawnSync(cmd, argv, {
|
|
297
638
|
cwd: opts.workspace || process.cwd(),
|
|
298
639
|
encoding: 'utf8',
|
|
299
640
|
env: process.env,
|
|
300
|
-
|
|
301
|
-
|
|
641
|
+
// Mission output is JSON even for the human-facing improve command because
|
|
642
|
+
// proof must be parsed before improve can claim success or write a receipt.
|
|
643
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
644
|
+
timeout: (budgetSec + 60) * 1000,
|
|
302
645
|
});
|
|
646
|
+
let payload = null;
|
|
647
|
+
let summary = null;
|
|
648
|
+
let error = null;
|
|
649
|
+
if (r.status === 0) {
|
|
650
|
+
try {
|
|
651
|
+
payload = JSON.parse(String(r.stdout || '').trim());
|
|
652
|
+
summary = summarizeLocalMissionRun(payload);
|
|
653
|
+
} catch (e) {
|
|
654
|
+
error = e.message;
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
error = String(r.stderr || '').trim().split('\n').filter(Boolean).slice(-1)[0]
|
|
658
|
+
|| `local improve exited ${r.status == null ? 1 : r.status}`;
|
|
659
|
+
}
|
|
303
660
|
return {
|
|
304
|
-
ok: r.status === 0,
|
|
661
|
+
ok: r.status === 0 && Boolean(summary),
|
|
305
662
|
status: r.status == null ? 1 : r.status,
|
|
306
663
|
stdout: r.stdout || '',
|
|
307
664
|
stderr: r.stderr || '',
|
|
665
|
+
payload,
|
|
666
|
+
summary,
|
|
667
|
+
error,
|
|
308
668
|
};
|
|
309
669
|
}
|
|
310
670
|
|
|
@@ -330,23 +690,89 @@ async function runImprove(opts = {}, deps = {}) {
|
|
|
330
690
|
const timeoutSec = Math.round((opts.timeoutMs || DEFAULT_TIMEOUT_MS) / 1000);
|
|
331
691
|
const startedAt = now();
|
|
332
692
|
const creds = loadCreds();
|
|
693
|
+
const shippingTick = (opts.mode || 'full') === 'full' && !opts.dryRun;
|
|
694
|
+
const localFallbackEligible = opts.fallback && shippingTick;
|
|
695
|
+
|
|
696
|
+
const finishLocalFallback = (reason, apiResult = null) => {
|
|
697
|
+
const local = localFn({ workspace, json: opts.json, timeoutSec });
|
|
698
|
+
const finishedAt = now();
|
|
699
|
+
if (!local.ok || !local.summary || local.summary.verify !== true) {
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
source: 'local',
|
|
703
|
+
reason,
|
|
704
|
+
error: local.error || 'local improve verifier did not pass',
|
|
705
|
+
local,
|
|
706
|
+
...(apiResult ? { apiResult } : {}),
|
|
707
|
+
startedAt,
|
|
708
|
+
finishedAt,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const row = buildScorecardRow(local.summary, {
|
|
712
|
+
source: 'local',
|
|
713
|
+
mode: opts.mode || 'full',
|
|
714
|
+
ts: finishedAt,
|
|
715
|
+
member: opts.member,
|
|
716
|
+
});
|
|
717
|
+
let scorecardPath;
|
|
718
|
+
let journalPath;
|
|
719
|
+
try {
|
|
720
|
+
scorecardPath = writeRow(workspace, row);
|
|
721
|
+
journalPath = writeJournal(workspace, local.summary, { source: 'local', member: opts.member });
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return {
|
|
724
|
+
ok: false,
|
|
725
|
+
source: 'local',
|
|
726
|
+
reason,
|
|
727
|
+
error: `local improve receipt write failed: ${e.message}`,
|
|
728
|
+
local,
|
|
729
|
+
...(apiResult ? { apiResult } : {}),
|
|
730
|
+
startedAt,
|
|
731
|
+
finishedAt,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
ok: true,
|
|
736
|
+
source: 'local',
|
|
737
|
+
reason,
|
|
738
|
+
summary: local.summary,
|
|
739
|
+
scorecardPath,
|
|
740
|
+
journalPath,
|
|
741
|
+
receipt: 'written',
|
|
742
|
+
row,
|
|
743
|
+
local,
|
|
744
|
+
...(apiResult ? { apiResult } : {}),
|
|
745
|
+
startedAt,
|
|
746
|
+
finishedAt,
|
|
747
|
+
};
|
|
748
|
+
};
|
|
333
749
|
|
|
334
750
|
// No auth → local fallback (or report if fallback disabled).
|
|
335
751
|
if (!creds || !creds.token) {
|
|
336
|
-
if (!
|
|
752
|
+
if (!localFallbackEligible) {
|
|
337
753
|
return {
|
|
338
754
|
ok: false, source: 'none', reason: 'no_auth',
|
|
339
|
-
error:
|
|
755
|
+
error: shippingTick
|
|
756
|
+
? 'Not logged in and --no-fallback set. Run: atris login'
|
|
757
|
+
: 'Not logged in. Plan, delegate, and dry-run modes require the hosted API; run: atris login',
|
|
340
758
|
startedAt, finishedAt: now(),
|
|
341
759
|
};
|
|
342
760
|
}
|
|
343
|
-
log('not logged in — falling back to
|
|
344
|
-
|
|
345
|
-
|
|
761
|
+
log('not logged in — falling back to one local mission tick');
|
|
762
|
+
return finishLocalFallback('no_auth');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// A hosted backend cannot read an absolute workspace that only exists on
|
|
766
|
+
// this machine. Skip the known-impossible request and use the same verified
|
|
767
|
+
// local mission fallback that its workspace-path 403 would have selected.
|
|
768
|
+
const apiBase = baseFn();
|
|
769
|
+
if (localFallbackEligible && hostedApiCannotReachLocalWorkspace(workspace, apiBase)) {
|
|
770
|
+
log('hosted backend cannot reach local workspace; falling back to one local mission tick');
|
|
771
|
+
return finishLocalFallback('workspace_not_on_backend');
|
|
346
772
|
}
|
|
347
773
|
|
|
348
774
|
// Attempt the paid API tick.
|
|
349
|
-
const apiPath = improveApiPath(
|
|
775
|
+
const apiPath = improveApiPath(apiBase);
|
|
350
776
|
const body = buildImprovePayload({ ...opts, workspace });
|
|
351
777
|
const apiResult = await apiFn(apiPath, {
|
|
352
778
|
method: 'POST',
|
|
@@ -361,7 +787,7 @@ async function runImprove(opts = {}, deps = {}) {
|
|
|
361
787
|
// Only a real, shipping tick earns a receipt. Plan/delegate/dry-run ship
|
|
362
788
|
// nothing, and an error inside an ok envelope (e.g. "workspace not found")
|
|
363
789
|
// is not a shipped change — none of these should write a scorecard/journal.
|
|
364
|
-
const shipped =
|
|
790
|
+
const shipped = shippingTick && !summary.error;
|
|
365
791
|
if (!shipped) {
|
|
366
792
|
return { ok: true, source: 'api', summary, scorecardPath: null, journalPath: null, receipt: 'skipped', startedAt, finishedAt };
|
|
367
793
|
}
|
|
@@ -382,10 +808,9 @@ async function runImprove(opts = {}, deps = {}) {
|
|
|
382
808
|
}
|
|
383
809
|
|
|
384
810
|
const decide = shouldFallbackLocal({ creds, apiResult });
|
|
385
|
-
if (decide.fallback &&
|
|
386
|
-
log(`backend ${decide.reason} — falling back to
|
|
387
|
-
|
|
388
|
-
return { ok: local.ok, source: 'local', reason: decide.reason, local, apiResult, startedAt, finishedAt: now() };
|
|
811
|
+
if (decide.fallback && localFallbackEligible) {
|
|
812
|
+
log(`backend ${decide.reason} — falling back to one local mission tick`);
|
|
813
|
+
return finishLocalFallback(decide.reason, apiResult);
|
|
389
814
|
}
|
|
390
815
|
|
|
391
816
|
// Real, answerable failure (e.g. insufficient credits, server error). Report it.
|
|
@@ -417,9 +842,16 @@ function formatImproveReport(result = {}) {
|
|
|
417
842
|
}
|
|
418
843
|
if (result.source === 'local') {
|
|
419
844
|
lines.push(result.ok ? 'improved (local fallback).' : 'local fallback tick failed.');
|
|
420
|
-
lines.push(` reason: backend ${result.reason} — ran
|
|
421
|
-
if (result.
|
|
422
|
-
|
|
845
|
+
lines.push(` reason: backend ${result.reason} — ran one local mission tick instead`);
|
|
846
|
+
if (result.ok) {
|
|
847
|
+
const s = result.summary || {};
|
|
848
|
+
lines.push(` task: ${s.shipped || '(no description returned)'}`);
|
|
849
|
+
lines.push(` verify: pass`);
|
|
850
|
+
lines.push(` reward: ${s.reward != null ? s.reward : '?'}`);
|
|
851
|
+
if (s.files && s.files.length) lines.push(` files: ${s.files.join(', ')}`);
|
|
852
|
+
if (result.scorecardPath) lines.push(` scorecard: ${path.relative(process.cwd(), result.scorecardPath)}`);
|
|
853
|
+
} else if (result.error) {
|
|
854
|
+
lines.push(` error: ${result.error}`);
|
|
423
855
|
}
|
|
424
856
|
return lines.join('\n');
|
|
425
857
|
}
|
|
@@ -430,16 +862,21 @@ function formatImproveReport(result = {}) {
|
|
|
430
862
|
}
|
|
431
863
|
|
|
432
864
|
function showHelp() {
|
|
433
|
-
console.log(`atris improve
|
|
865
|
+
console.log(`atris improve - show the self-improvement metabolism vitals
|
|
434
866
|
|
|
435
867
|
Usage:
|
|
436
|
-
atris improve
|
|
868
|
+
atris improve
|
|
869
|
+
atris improve --json
|
|
870
|
+
atris improve doctor [--json] [--fix] [--check <kind>]
|
|
871
|
+
atris improve tick [mode] [options]
|
|
872
|
+
atris improve [mode|history] [options]
|
|
437
873
|
|
|
438
874
|
Modes (positional or --mode):
|
|
439
875
|
full plan + build + verify + score (default)
|
|
440
876
|
plan return the plan only, no changes
|
|
441
877
|
delegate queue the tick for a local Claude Code session
|
|
442
878
|
history show the tick history (reward trend, credits, pass rate)
|
|
879
|
+
doctor scan loop receipts and optionally file one repair mission
|
|
443
880
|
|
|
444
881
|
Options:
|
|
445
882
|
--member <name> attribute the tick to a member (the loop's owner)
|
|
@@ -448,17 +885,183 @@ Options:
|
|
|
448
885
|
--no-fallback do not fall back to a local tick if the backend is down
|
|
449
886
|
--workspace <p> workspace path (default: cwd)
|
|
450
887
|
--timeout <sec> request timeout in seconds (default: 300)
|
|
888
|
+
--check <kind> exit 0 when the doctor finding is absent, or 1 when present
|
|
451
889
|
--json machine-readable output (for the member loop)
|
|
452
890
|
-h, --help this help
|
|
453
891
|
|
|
454
892
|
Calls POST /api/improve, which ships one verifiable change and deducts
|
|
455
893
|
Atris credits per successful tick. Writes a per-tick scorecard to
|
|
456
|
-
.atris/state/scorecards.jsonl. Falls back to
|
|
894
|
+
.atris/state/scorecards.jsonl. Falls back to one local mission tick when
|
|
457
895
|
the backend is unreachable or you are not logged in.`);
|
|
458
896
|
}
|
|
459
897
|
|
|
460
|
-
|
|
461
|
-
|
|
898
|
+
function isBareVitalsArgs(argv = []) {
|
|
899
|
+
return argv.length === 0 || (argv.length === 1 && argv[0] === '--json');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const LOOP_DOCTOR_OPEN_STATUSES = new Set(['planning', 'ready', 'running', 'paused', 'blocked']);
|
|
903
|
+
|
|
904
|
+
function loopDoctorKey(finding) {
|
|
905
|
+
return `[loop-doctor:${finding.kind}]`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function openLoopDoctorMission(root, finding) {
|
|
909
|
+
const key = loopDoctorKey(finding);
|
|
910
|
+
const rows = readJsonlFile(path.join(root, '.atris', 'state', 'missions.jsonl'));
|
|
911
|
+
const latest = new Map();
|
|
912
|
+
rows.forEach((row, index) => {
|
|
913
|
+
const mission = row && row.mission && typeof row.mission === 'object' ? row.mission : row;
|
|
914
|
+
if (mission && typeof mission === 'object') latest.set(String(mission.id || `row-${index}`), mission);
|
|
915
|
+
});
|
|
916
|
+
return [...latest.values()].find((mission) => LOOP_DOCTOR_OPEN_STATUSES.has(String(mission.status || '').toLowerCase())
|
|
917
|
+
&& String(mission.objective || '').includes(key)) || null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function formatLoopDoctor(findings, fix = null, closed = []) {
|
|
921
|
+
const lines = [`loop doctor: ${findings.length} finding${findings.length === 1 ? '' : 's'}`];
|
|
922
|
+
findings.forEach((finding, index) => {
|
|
923
|
+
lines.push(`${index + 1}. ${finding.kind}: ${finding.evidence.detail} (${finding.count})`);
|
|
924
|
+
lines.push(` repair: ${finding.suggested_mission.objective}`);
|
|
925
|
+
});
|
|
926
|
+
if (!findings.length) lines.push('the recent improve-loop receipts are clean.');
|
|
927
|
+
if (fix && fix.action === 'mission_started') lines.push(`filed one mission: ${fix.mission.id}`);
|
|
928
|
+
if (fix && fix.action === 'mission_exists') lines.push(`no mission filed: ${fix.mission.id} already covers the top finding.`);
|
|
929
|
+
if (Array.isArray(closed) && closed.length) {
|
|
930
|
+
lines.push(`credited ${closed.length} verified repair${closed.length === 1 ? '' : 's'}: ${closed.map((row) => row.kind).join(', ')}`);
|
|
931
|
+
}
|
|
932
|
+
return lines.join('\n');
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Close the reward loop the doctor opens. When it files a repair it writes a
|
|
936
|
+
// reward-0 loop_doctor row; the fix only earns once its finding is provably
|
|
937
|
+
// gone. This reconciles those open rows against the current findings: a filed
|
|
938
|
+
// row whose kind no longer fires gets one closing credit, so the scorecard
|
|
939
|
+
// reads filed then paid instead of a pile of zero-reward filings. Deduped by
|
|
940
|
+
// mission id so a finding that clears stays closed even if it flickers back.
|
|
941
|
+
function reconcileLoopDoctorRewards(root, findings, now, appendScorecard) {
|
|
942
|
+
// Read the raw rows, not readTickHistory: that filters to improve_tick
|
|
943
|
+
// schema and would drop the loop_doctor rows this reconciliation is about.
|
|
944
|
+
const rows = readJsonlFile(path.join(expandHome(root), '.atris', 'state', 'scorecards.jsonl'));
|
|
945
|
+
const openByMission = new Map();
|
|
946
|
+
const closedMissions = new Set();
|
|
947
|
+
for (const row of rows) {
|
|
948
|
+
if (!row || row.schema !== 'atris.loop_doctor.v1' || !row.mission_id) continue;
|
|
949
|
+
if (row.closed === true) { closedMissions.add(String(row.mission_id)); continue; }
|
|
950
|
+
if (Number(row.reward) === 0) openByMission.set(String(row.mission_id), row);
|
|
951
|
+
}
|
|
952
|
+
const liveKinds = new Set(findings.map((finding) => finding.kind));
|
|
953
|
+
const closed = [];
|
|
954
|
+
for (const [missionId, row] of openByMission) {
|
|
955
|
+
if (closedMissions.has(missionId)) continue;
|
|
956
|
+
if (liveKinds.has(row.kind)) continue; // finding still fires, repair not proven yet
|
|
957
|
+
const closingRow = {
|
|
958
|
+
schema: 'atris.loop_doctor.v1',
|
|
959
|
+
ts: now.toISOString(),
|
|
960
|
+
source: 'loop_doctor',
|
|
961
|
+
kind: row.kind,
|
|
962
|
+
mission_id: missionId,
|
|
963
|
+
reward: 3,
|
|
964
|
+
closed: true,
|
|
965
|
+
note: 'repair verified: the finding it filed against is no longer present',
|
|
966
|
+
};
|
|
967
|
+
appendScorecard(root, closingRow);
|
|
968
|
+
closedMissions.add(missionId);
|
|
969
|
+
closed.push(closingRow);
|
|
970
|
+
}
|
|
971
|
+
return closed;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function runLoopDoctor(argv = [], deps = {}) {
|
|
975
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
976
|
+
const json = args.includes('--json');
|
|
977
|
+
const fixRequested = args.includes('--fix');
|
|
978
|
+
const checkIndex = args.indexOf('--check');
|
|
979
|
+
const checkRequested = checkIndex !== -1;
|
|
980
|
+
const checkKind = checkRequested ? String(args[checkIndex + 1] || '').trim() : null;
|
|
981
|
+
const root = process.cwd();
|
|
982
|
+
const scan = deps.scanLoopReceipts || require('../lib/loop-doctor').scanLoopReceipts;
|
|
983
|
+
const findings = scan({ root, now: deps.now || new Date() });
|
|
984
|
+
let fix = null;
|
|
985
|
+
|
|
986
|
+
if (checkRequested) {
|
|
987
|
+
const finding = checkKind && findings.find((item) => item.kind === checkKind);
|
|
988
|
+
const check = {
|
|
989
|
+
kind: checkKind,
|
|
990
|
+
ok: Boolean(checkKind) && !finding,
|
|
991
|
+
reason: !checkKind ? 'missing finding kind' : (finding ? `${checkKind} is still present` : null),
|
|
992
|
+
};
|
|
993
|
+
const payload = { schema: 'atris.loop_doctor.v1', findings, fix, check };
|
|
994
|
+
if (json) console.log(JSON.stringify(payload));
|
|
995
|
+
else if (check.ok) console.log(`loop doctor check passed: no ${checkKind} finding.`);
|
|
996
|
+
else console.log(`loop doctor check failed: ${check.reason}.`);
|
|
997
|
+
return payload;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Credit any past filing whose finding has since cleared. This runs on plain
|
|
1001
|
+
// `doctor` and `doctor --fix`, never on `--check` (a pure verifier that must
|
|
1002
|
+
// not write), so the reward closes on the next observation after a repair.
|
|
1003
|
+
const reconcileNow = deps.now || new Date();
|
|
1004
|
+
const reconcileAppend = deps.appendScorecardRow || appendScorecardRow;
|
|
1005
|
+
const closed = reconcileLoopDoctorRewards(deps.workspace || root, findings, reconcileNow, reconcileAppend);
|
|
1006
|
+
|
|
1007
|
+
if (fixRequested && findings.length) {
|
|
1008
|
+
const finding = findings[0];
|
|
1009
|
+
const existing = openLoopDoctorMission(root, finding);
|
|
1010
|
+
if (existing) {
|
|
1011
|
+
fix = { action: 'mission_exists', finding_kind: finding.kind, mission: existing };
|
|
1012
|
+
} else {
|
|
1013
|
+
const suggested = finding.suggested_mission;
|
|
1014
|
+
const objective = `${loopDoctorKey(finding)} ${suggested.objective}`;
|
|
1015
|
+
const start = deps.startMission || require('./mission').startMission;
|
|
1016
|
+
const started = start([
|
|
1017
|
+
objective,
|
|
1018
|
+
'--owner', suggested.owner,
|
|
1019
|
+
'--verify', suggested.verifier,
|
|
1020
|
+
'--cadence', suggested.cadence,
|
|
1021
|
+
'--runner', 'auto',
|
|
1022
|
+
'--always-on',
|
|
1023
|
+
], { silent: true });
|
|
1024
|
+
fix = { action: 'mission_started', finding_kind: finding.kind, mission: started.mission };
|
|
1025
|
+
const workspace = deps.workspace || root;
|
|
1026
|
+
const appendScorecard = deps.appendScorecardRow || appendScorecardRow;
|
|
1027
|
+
appendScorecard(workspace, {
|
|
1028
|
+
schema: 'atris.loop_doctor.v1',
|
|
1029
|
+
ts: (deps.now || new Date()).toISOString(),
|
|
1030
|
+
source: 'loop_doctor',
|
|
1031
|
+
kind: finding.kind,
|
|
1032
|
+
mission_id: started.mission.id,
|
|
1033
|
+
reward: 0,
|
|
1034
|
+
note: 'repair filed; reward is earned by the repair tick, not the filing',
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const payload = { schema: 'atris.loop_doctor.v1', findings, fix, closed };
|
|
1040
|
+
if (json) console.log(JSON.stringify(payload));
|
|
1041
|
+
else console.log(formatLoopDoctor(findings, fix, closed));
|
|
1042
|
+
return payload;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function run(argv = [], deps = {}) {
|
|
1046
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
1047
|
+
if (args[0] === 'doctor') {
|
|
1048
|
+
const result = runLoopDoctor(args.slice(1), deps);
|
|
1049
|
+
return result.check && !result.check.ok ? 1 : 0;
|
|
1050
|
+
}
|
|
1051
|
+
if (isBareVitalsArgs(args)) {
|
|
1052
|
+
const vitals = (deps.collectImproveVitals || collectImproveVitals)({ workspace: process.cwd() }, deps);
|
|
1053
|
+
if (args.includes('--json')) console.log(JSON.stringify(vitals));
|
|
1054
|
+
else console.log(formatImproveVitals(vitals));
|
|
1055
|
+
// Keep the live page in step with what the terminal just said.
|
|
1056
|
+
try {
|
|
1057
|
+
const file = require('../lib/improve-vitals-html').writeVitalsHtml(process.cwd(), deps);
|
|
1058
|
+
if (!args.includes('--json')) console.log(`\nlive page: ${file} (open it once, it refreshes itself)`);
|
|
1059
|
+
} catch { /* page is a bonus, never a failure */ }
|
|
1060
|
+
return 0;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const routedArgs = args[0] === 'tick' ? args.slice(1) : args;
|
|
1064
|
+
const opts = parseImproveArgs(routedArgs);
|
|
462
1065
|
if (opts.help) { showHelp(); return 0; }
|
|
463
1066
|
|
|
464
1067
|
if (opts.history) {
|
|
@@ -468,7 +1071,8 @@ async function run(argv = []) {
|
|
|
468
1071
|
return 0;
|
|
469
1072
|
}
|
|
470
1073
|
|
|
471
|
-
const
|
|
1074
|
+
const improveFn = deps.runImprove || runImprove;
|
|
1075
|
+
const result = await improveFn(opts, {
|
|
472
1076
|
log: opts.json ? () => {} : (m) => console.error(` ${m}`),
|
|
473
1077
|
});
|
|
474
1078
|
|
|
@@ -491,11 +1095,23 @@ module.exports = {
|
|
|
491
1095
|
appendScorecardRow,
|
|
492
1096
|
appendTickToJournal,
|
|
493
1097
|
expandHome,
|
|
1098
|
+
collectImproveVitals,
|
|
1099
|
+
formatImproveVitals,
|
|
1100
|
+
isBareVitalsArgs,
|
|
494
1101
|
readTickHistory,
|
|
495
1102
|
summarizeTickHistory,
|
|
496
1103
|
formatTickHistory,
|
|
497
1104
|
improveApiPath,
|
|
498
1105
|
formatImproveReport,
|
|
1106
|
+
runLoopDoctor,
|
|
1107
|
+
reconcileLoopDoctorRewards,
|
|
1108
|
+
formatLoopDoctor,
|
|
1109
|
+
openLoopDoctorMission,
|
|
1110
|
+
loopDoctorKey,
|
|
499
1111
|
runLocalFallback,
|
|
1112
|
+
summarizeLocalMissionRun,
|
|
1113
|
+
LOCAL_FALLBACK_ARGS,
|
|
1114
|
+
localFallbackArgs,
|
|
500
1115
|
SCORECARD_SCHEMA,
|
|
1116
|
+
IMPROVE_VITALS_SCHEMA,
|
|
501
1117
|
};
|