atris 3.35.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
package/commands/play.js
CHANGED
|
@@ -328,6 +328,7 @@ function playWorkspaceRoot(taskDb, workspaceArg) {
|
|
|
328
328
|
|
|
329
329
|
function nextCommands(task, player) {
|
|
330
330
|
const helper = player || 'player';
|
|
331
|
+
const readyResult = 'Players can now claim AgentXP proof faster because the verifier names the finished artifact.';
|
|
331
332
|
if (!task) {
|
|
332
333
|
return [
|
|
333
334
|
`atris task delegate "AgentXP first rep: one proof-backed mission" --to ${player} --tag agent-xp`,
|
|
@@ -339,7 +340,7 @@ function nextCommands(task, player) {
|
|
|
339
340
|
if (task.status === 'open') {
|
|
340
341
|
return [
|
|
341
342
|
`atris task claim ${ref} --as ${helper}`,
|
|
342
|
-
`atris task ready ${ref} --as ${helper} --proof "AGENTXP_PROOF.md + test -s AGENTXP_PROOF.md passed"`,
|
|
343
|
+
`atris task ready ${ref} --as ${helper} --proof "AGENTXP_PROOF.md + test -s AGENTXP_PROOF.md passed" --result "${readyResult}"`,
|
|
343
344
|
`atris task accept ${ref} --as ${player} --proof "<human review>"`,
|
|
344
345
|
'atris xp card --local',
|
|
345
346
|
...globalSyncCommands(player),
|
|
@@ -349,7 +350,7 @@ function nextCommands(task, player) {
|
|
|
349
350
|
if (task.status === 'claimed') {
|
|
350
351
|
const actor = task.claimed_by || helper;
|
|
351
352
|
return [
|
|
352
|
-
`atris task ready ${ref} --as ${actor} --proof "AGENTXP_PROOF.md + test -s AGENTXP_PROOF.md passed"`,
|
|
353
|
+
`atris task ready ${ref} --as ${actor} --proof "AGENTXP_PROOF.md + test -s AGENTXP_PROOF.md passed" --result "${readyResult}"`,
|
|
353
354
|
`atris task accept ${ref} --as ${player} --proof "<human review>"`,
|
|
354
355
|
'atris xp card --local',
|
|
355
356
|
...globalSyncCommands(player),
|
package/commands/probe.js
CHANGED
|
@@ -28,6 +28,7 @@ const https = require('https');
|
|
|
28
28
|
const http = require('http');
|
|
29
29
|
const { getApiBaseUrl } = require('../utils/api');
|
|
30
30
|
const { loadCredentials } = require('../utils/auth');
|
|
31
|
+
const { encodeToolResult } = require('../lib/tool-result-encode');
|
|
31
32
|
|
|
32
33
|
// G2's honest blocked message (tool_policy_bench.MAX_TURNS_EXHAUSTED_MESSAGE)
|
|
33
34
|
// starts with this — an answer that is only this marker is a dead-end.
|
|
@@ -182,6 +183,26 @@ function runTerminal(base, token, command, businessId) {
|
|
|
182
183
|
return postJson(`${base}/ai-computer/terminal`, token, body, 80000);
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
// CLI-231: local executor for atris2 mission ticks. Same result shape as the
|
|
187
|
+
// /ai-computer/terminal endpoint ({ stdout, stderr, exit_code }), but the
|
|
188
|
+
// command runs in the mission's OWN workspace directory instead of the hosted
|
|
189
|
+
// ai-computer. Without this, a mission tick in a local workspace read the
|
|
190
|
+
// remote computer's files and reported local members (e.g. maze) as missing.
|
|
191
|
+
function runLocalTerminal(command, cwd) {
|
|
192
|
+
const { exec } = require('child_process');
|
|
193
|
+
const { envWithNodeDir } = require('../lib/spawn-env');
|
|
194
|
+
const env = envWithNodeDir(process.env);
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
exec(command, { cwd, env, timeout: 60000, maxBuffer: 4 * 1024 * 1024, shell: '/bin/bash' }, (error, stdout, stderr) => {
|
|
197
|
+
resolve({
|
|
198
|
+
stdout: String(stdout || ''),
|
|
199
|
+
stderr: String(stderr || (error && error.message) || ''),
|
|
200
|
+
exit_code: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
function parseArgs(argv) {
|
|
186
207
|
const a = { business: null, model: 'atris:fast', memberId: null, memberSlug: null, calendar: false, message: null };
|
|
187
208
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -215,6 +236,7 @@ async function runAtris2Turn(opts = {}) {
|
|
|
215
236
|
idleMs = 180000,
|
|
216
237
|
connectTimeoutMs = 60000,
|
|
217
238
|
signal = null,
|
|
239
|
+
localCwd = null,
|
|
218
240
|
} = opts;
|
|
219
241
|
const t0 = Date.now();
|
|
220
242
|
const out = { ok: false, text: '', engine: null, tools_run: 0, cli_ops: [], unsupported: [], error: null, duration_ms: 0 };
|
|
@@ -299,7 +321,9 @@ async function runAtris2Turn(opts = {}) {
|
|
|
299
321
|
out = { status: 'error', error: 'unsupported op or unsafe path' };
|
|
300
322
|
unsupported.push(`file_op:${op}`);
|
|
301
323
|
} else {
|
|
302
|
-
const term =
|
|
324
|
+
const term = localCwd
|
|
325
|
+
? await runLocalTerminal(cmd, localCwd)
|
|
326
|
+
: await runTerminal(base, token, cmd, business);
|
|
303
327
|
const ok = term.exit_code === 0;
|
|
304
328
|
out = ok
|
|
305
329
|
? { status: 'ok', stdout: String(term.stdout || '').slice(0, 12000) }
|
|
@@ -316,7 +340,9 @@ async function runAtris2Turn(opts = {}) {
|
|
|
316
340
|
out = { status: 'error', error: `unsupported atris cli op: ${op || '?'}` };
|
|
317
341
|
unsupported.push(`cli_op:${op || '?'}`);
|
|
318
342
|
} else {
|
|
319
|
-
const term =
|
|
343
|
+
const term = localCwd
|
|
344
|
+
? await runLocalTerminal(cmd, localCwd)
|
|
345
|
+
: await runTerminal(base, token, cmd, business);
|
|
320
346
|
out = atrisCliResult(cmd, term);
|
|
321
347
|
cliOps.push(op || '?');
|
|
322
348
|
}
|
|
@@ -325,7 +351,7 @@ async function runAtris2Turn(opts = {}) {
|
|
|
325
351
|
unsupported.push(`tool:${name || '?'}`);
|
|
326
352
|
}
|
|
327
353
|
await postJson(`${base}/atris2/turn/tool-result`, token,
|
|
328
|
-
{ call_id: ev.call_id || ev.id,
|
|
354
|
+
{ call_id: ev.call_id || ev.id, result_base64: encodeToolResult(out) }, 30000);
|
|
329
355
|
resetIdle();
|
|
330
356
|
}).catch((e) => reject(e));
|
|
331
357
|
} else if (et === 'result') {
|
|
@@ -441,4 +467,5 @@ module.exports = {
|
|
|
441
467
|
fileOpCommand,
|
|
442
468
|
atrisCliCommand,
|
|
443
469
|
atrisCliResult,
|
|
470
|
+
runLocalTerminal,
|
|
444
471
|
};
|
package/commands/pulse.js
CHANGED
|
@@ -49,7 +49,7 @@ Options:
|
|
|
49
49
|
--json Print machine-readable output
|
|
50
50
|
--no-claude Do not spawn Claude-backed mission work
|
|
51
51
|
--no-verify Skip verifier command
|
|
52
|
-
--verify "<cmd>" Verifier for changed-work ticks (default: npm test)
|
|
52
|
+
--verify "<cmd>" Verifier for changed-work ticks (default: npm test if package.json exists, else none)
|
|
53
53
|
--cadence "<cron>" Cron cadence for install
|
|
54
54
|
--days <n> Auto-expire installed heartbeat after n days
|
|
55
55
|
--hours <n> Auto-expire installed heartbeat after n hours
|
|
@@ -76,8 +76,6 @@ function emit(obj, asJson) {
|
|
|
76
76
|
return obj;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const STATE_HOME = path.join(os.homedir(), '.atris', 'overnight', 'atris-cli-self-improve');
|
|
80
|
-
|
|
81
79
|
// --- engine: run one mission-run-due tick via the real CLI ---
|
|
82
80
|
|
|
83
81
|
function runMissionEngine(root, { noClaude = false, timeoutMs = 600000 } = {}) {
|
|
@@ -166,25 +164,51 @@ function gitSnapshot(root) {
|
|
|
166
164
|
return { head, dirty: new Set(gitChangedFiles(root)) };
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
// 30 min: the full suite takes ~8 min alone but 15+ under concurrent mission
|
|
168
|
+
// load; a 10-min cap turned green suites into false verify FAILs (tick 159).
|
|
169
|
+
function runVerify(root, verifyCmd, timeoutMs = 1800000) {
|
|
170
170
|
if (!verifyCmd) return { passed: null, cmd: null };
|
|
171
|
+
const { envWithNodeDir } = require('../lib/spawn-env');
|
|
171
172
|
const r = spawnSync(verifyCmd, {
|
|
172
173
|
cwd: root,
|
|
173
174
|
shell: true,
|
|
174
175
|
encoding: 'utf8',
|
|
175
176
|
timeout: timeoutMs,
|
|
176
|
-
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1', ATRIS_AGENT_PROOF_ONLY: '0' },
|
|
177
|
+
env: envWithNodeDir({ ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1', ATRIS_AGENT_PROOF_ONLY: '0' }),
|
|
177
178
|
});
|
|
178
179
|
return { passed: r.status === 0, cmd: verifyCmd, status: r.status };
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
function refreshOrbPolicyLessons(root, ingestion, now = new Date()) {
|
|
183
|
+
if (!ingestion || ingestion.historical_failures < 1) return null;
|
|
184
|
+
const {
|
|
185
|
+
loadHistory,
|
|
186
|
+
mineProofPolicy,
|
|
187
|
+
writePolicyLessons,
|
|
188
|
+
syncLessonsMd,
|
|
189
|
+
} = require('../lib/policy-lessons');
|
|
190
|
+
const mined = mineProofPolicy(loadHistory(root), {
|
|
191
|
+
now,
|
|
192
|
+
orbWindowDays: ingestion.summary && ingestion.summary.days,
|
|
193
|
+
});
|
|
194
|
+
const statePath = writePolicyLessons(root, mined);
|
|
195
|
+
const lessonsMd = syncLessonsMd(root, mined);
|
|
196
|
+
const orbLesson = mined.lessons.find((lesson) => lesson.id === 'orb-job-failures') || null;
|
|
197
|
+
return {
|
|
198
|
+
status: orbLesson ? orbLesson.status : null,
|
|
199
|
+
state_path: statePath,
|
|
200
|
+
lessons_md_path: lessonsMd.path,
|
|
201
|
+
lessons_md_written: lessonsMd.written,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
// --- atris pulse tick ---
|
|
182
206
|
|
|
183
207
|
function tickCommand(args, root = process.cwd()) {
|
|
184
208
|
const asJson = wantsJson(args);
|
|
185
209
|
const noClaude = hasFlag(args, '--no-claude');
|
|
186
210
|
const noVerify = hasFlag(args, '--no-verify');
|
|
187
|
-
const verifyCmd = noVerify ? null : readFlag(args, '--verify',
|
|
211
|
+
const verifyCmd = noVerify ? null : readFlag(args, '--verify', pulse.defaultVerifyCmd(root));
|
|
188
212
|
const startedAt = Date.now();
|
|
189
213
|
|
|
190
214
|
// Detect a previous tick that died mid-run before we take the lock.
|
|
@@ -199,6 +223,20 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
199
223
|
}
|
|
200
224
|
|
|
201
225
|
const tickIndex = pulse.nextTickIndex(root);
|
|
226
|
+
let orbIngestion = {
|
|
227
|
+
summary: null,
|
|
228
|
+
improvement_candidates: [],
|
|
229
|
+
scorecards_written: 0,
|
|
230
|
+
historical_failures: 0,
|
|
231
|
+
};
|
|
232
|
+
let orbPolicy = null;
|
|
233
|
+
let orbIngestError = null;
|
|
234
|
+
try {
|
|
235
|
+
orbIngestion = pulse.ingestOrbScorecard(root);
|
|
236
|
+
orbPolicy = refreshOrbPolicyLessons(root, orbIngestion);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
orbIngestError = String(error && error.message ? error.message : error);
|
|
239
|
+
}
|
|
202
240
|
const signalExitCode = { SIGINT: 130, SIGTERM: 143 };
|
|
203
241
|
let interrupted = false;
|
|
204
242
|
const finishInterrupted = (signal) => {
|
|
@@ -225,18 +263,31 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
225
263
|
tickIndex,
|
|
226
264
|
phase: 'started',
|
|
227
265
|
prevTickStale: priorStale.stale,
|
|
266
|
+
orbScorecard: orbIngestion.summary,
|
|
267
|
+
improvementCandidates: orbIngestion.improvement_candidates,
|
|
268
|
+
orbIngestError,
|
|
228
269
|
}));
|
|
229
270
|
|
|
230
271
|
let engine;
|
|
231
272
|
let verify = { passed: null, cmd: verifyCmd };
|
|
232
273
|
try {
|
|
233
|
-
|
|
274
|
+
// Non-git workspaces made producedWork permanently false (empty git delta),
|
|
275
|
+
// so every tick scored 0 no matter what the engine wrote. Fall back to a
|
|
276
|
+
// filesystem snapshot when git is absent.
|
|
277
|
+
const isRepo = fs.existsSync(path.join(root, '.git'));
|
|
278
|
+
const before = isRepo ? gitSnapshot(root) : pulse.fsSnapshot(root);
|
|
234
279
|
engine = runEngine(root, { noClaude, autopilotFallback: !hasFlag(args, '--no-autopilot') });
|
|
235
|
-
const after = gitSnapshot(root);
|
|
280
|
+
const after = isRepo ? gitSnapshot(root) : pulse.fsSnapshot(root);
|
|
236
281
|
// This tick's ACTUAL contribution: files it newly dirtied, or a new commit.
|
|
237
282
|
// Pre-existing dirt is excluded so reward isn't re-credited every tick.
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
let changedFiles;
|
|
284
|
+
let committed = false;
|
|
285
|
+
if (isRepo) {
|
|
286
|
+
changedFiles = [...after.dirty].filter((f) => !before.dirty.has(f));
|
|
287
|
+
committed = Boolean(before.head && after.head && before.head !== after.head);
|
|
288
|
+
} else {
|
|
289
|
+
changedFiles = pulse.diffFsSnapshots(before, after);
|
|
290
|
+
}
|
|
240
291
|
const producedWork = committed || changedFiles.length > 0;
|
|
241
292
|
|
|
242
293
|
// Verify only matters when the tick produced work; a no-op tick skips it.
|
|
@@ -270,6 +321,9 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
270
321
|
elapsedMs,
|
|
271
322
|
prevTickStale: priorStale.stale,
|
|
272
323
|
reward,
|
|
324
|
+
orbScorecard: orbIngestion.summary,
|
|
325
|
+
improvementCandidates: orbIngestion.improvement_candidates,
|
|
326
|
+
orbIngestError,
|
|
273
327
|
});
|
|
274
328
|
pulse.appendPulseReceipt(root, receipt);
|
|
275
329
|
|
|
@@ -283,10 +337,33 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
283
337
|
changedFiles,
|
|
284
338
|
elapsedMs,
|
|
285
339
|
model: process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
340
|
+
orbScorecard: orbIngestion.summary,
|
|
341
|
+
improvementCandidates: orbIngestion.improvement_candidates,
|
|
286
342
|
}));
|
|
287
343
|
scorecardWritten = true;
|
|
288
344
|
}
|
|
289
345
|
|
|
346
|
+
// Closure sweep rides the heartbeat: escalate overdue open loops once a
|
|
347
|
+
// day without a human invoking atris close sweep. Never fails the tick.
|
|
348
|
+
let closure = null;
|
|
349
|
+
try {
|
|
350
|
+
const { scanState, sweepState } = require('./close');
|
|
351
|
+
const scanned = scanState(root);
|
|
352
|
+
const swept = sweepState(root);
|
|
353
|
+
closure = {
|
|
354
|
+
open: swept.open,
|
|
355
|
+
overdue: swept.overdue.length,
|
|
356
|
+
escalated_today: swept.escalated_today,
|
|
357
|
+
scanned: {
|
|
358
|
+
opened: scanned.opened.length,
|
|
359
|
+
auto_closed: scanned.auto_closed.length,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
} catch { /* closure ledger optional */ }
|
|
363
|
+
|
|
364
|
+
// Rewrite the live vitals page so an open browser tab shows this tick.
|
|
365
|
+
try { require('../lib/improve-vitals-html').writeVitalsHtml(root); } catch { /* page is a bonus */ }
|
|
366
|
+
|
|
290
367
|
const out = {
|
|
291
368
|
ok: true,
|
|
292
369
|
action: 'pulse_tick',
|
|
@@ -300,11 +377,23 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
300
377
|
prev_tick_stale: priorStale.stale,
|
|
301
378
|
elapsed_ms: elapsedMs,
|
|
302
379
|
receipts_path: pulse.pulseReceiptsPath(root),
|
|
380
|
+
orb_scorecard: orbIngestion.summary,
|
|
381
|
+
orb_improvement_candidates: orbIngestion.improvement_candidates,
|
|
382
|
+
orb_scorecards_written: orbIngestion.scorecards_written,
|
|
383
|
+
orb_policy_lesson: orbPolicy,
|
|
303
384
|
};
|
|
385
|
+
if (orbIngestError) out.orb_ingest_error = orbIngestError;
|
|
386
|
+
if (closure) out.closure = closure;
|
|
304
387
|
if (!asJson) {
|
|
305
388
|
const ghost = priorStale.stale ? ` (recovered ghost tick #${priorStale.tick_index || '?'})` : '';
|
|
306
389
|
const r = reward > 0 ? `+${reward}` : String(reward);
|
|
307
390
|
process.stdout.write(`pulse tick #${tickIndex}: ${what} - verify ${verify.passed === null ? 'n/a' : verify.passed ? 'pass' : 'FAIL'} - reward ${r}${ghost}\n`);
|
|
391
|
+
if (orbIngestion.summary && orbIngestion.summary.fail > 0) {
|
|
392
|
+
process.stdout.write(`orb: ${orbIngestion.summary.fail} failed job${orbIngestion.summary.fail === 1 ? '' : 's'} in the ${orbIngestion.summary.days}-day window; ${orbIngestion.scorecards_written} new improvement candidate${orbIngestion.scorecards_written === 1 ? '' : 's'} recorded.\n`);
|
|
393
|
+
}
|
|
394
|
+
if (closure && closure.overdue > 0) {
|
|
395
|
+
process.stdout.write(`closure: ${closure.overdue} open loop${closure.overdue === 1 ? '' : 's'} overdue, run: atris close sweep\n`);
|
|
396
|
+
}
|
|
308
397
|
}
|
|
309
398
|
return emit(out, asJson);
|
|
310
399
|
} catch (err) {
|
|
@@ -318,8 +407,21 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
318
407
|
what: `tick crashed: ${err && err.message ? err.message : String(err)}`,
|
|
319
408
|
elapsedMs: Date.now() - startedAt,
|
|
320
409
|
reward: -1,
|
|
410
|
+
orbScorecard: orbIngestion.summary,
|
|
411
|
+
improvementCandidates: orbIngestion.improvement_candidates,
|
|
412
|
+
orbIngestError,
|
|
321
413
|
}));
|
|
322
|
-
const out = {
|
|
414
|
+
const out = {
|
|
415
|
+
ok: false,
|
|
416
|
+
action: 'pulse_tick',
|
|
417
|
+
tick_index: tickIndex,
|
|
418
|
+
error: err && err.message ? err.message : String(err),
|
|
419
|
+
orb_scorecard: orbIngestion.summary,
|
|
420
|
+
orb_improvement_candidates: orbIngestion.improvement_candidates,
|
|
421
|
+
orb_scorecards_written: orbIngestion.scorecards_written,
|
|
422
|
+
orb_policy_lesson: orbPolicy,
|
|
423
|
+
};
|
|
424
|
+
if (orbIngestError) out.orb_ingest_error = orbIngestError;
|
|
323
425
|
if (!asJson) process.stdout.write(`pulse tick #${tickIndex} crashed: ${out.error}\n`);
|
|
324
426
|
return emit(out, asJson);
|
|
325
427
|
} finally {
|
|
@@ -331,34 +433,89 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
331
433
|
|
|
332
434
|
// --- atris pulse status ---
|
|
333
435
|
|
|
334
|
-
function
|
|
436
|
+
function crontabHasActiveMarker(crontab, markers = pulse.PULSE_MARKER) {
|
|
437
|
+
const markerList = (Array.isArray(markers) ? markers : [markers]).filter(Boolean);
|
|
438
|
+
return String(crontab || '')
|
|
439
|
+
.split('\n')
|
|
440
|
+
.map((line) => line.trim())
|
|
441
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
442
|
+
.some((line) => markerList.some((marker) => line.includes(marker)));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function cronInstalled(markers = pulse.PULSE_MARKER, commandSpawn = spawnSync) {
|
|
335
446
|
try {
|
|
336
|
-
const r =
|
|
447
|
+
const r = commandSpawn('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
|
|
337
448
|
if (r.status !== 0) return false;
|
|
338
|
-
return
|
|
449
|
+
return crontabHasActiveMarker(r.stdout, markers);
|
|
339
450
|
} catch {
|
|
340
451
|
return false;
|
|
341
452
|
}
|
|
342
453
|
}
|
|
343
454
|
|
|
344
|
-
function
|
|
455
|
+
function pulseExpiryBreadcrumbPath(root) {
|
|
456
|
+
return path.join(root, '.atris', 'state', 'pulse-expired.json');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function readPulseExpiryBreadcrumb(root) {
|
|
460
|
+
try {
|
|
461
|
+
const raw = fs.readFileSync(pulseExpiryBreadcrumbPath(root), 'utf8').trim();
|
|
462
|
+
const candidates = [];
|
|
463
|
+
if (raw) candidates.push(raw, ...raw.split('\n').reverse());
|
|
464
|
+
for (const candidate of candidates) {
|
|
465
|
+
try {
|
|
466
|
+
const parsed = JSON.parse(candidate);
|
|
467
|
+
if (parsed
|
|
468
|
+
&& typeof parsed.expired_at === 'string'
|
|
469
|
+
&& typeof parsed.state_home === 'string'
|
|
470
|
+
&& typeof parsed.renew_command === 'string') {
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
473
|
+
} catch {}
|
|
474
|
+
}
|
|
475
|
+
} catch {}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function clearPulseExpiryBreadcrumb(root) {
|
|
480
|
+
const file = pulseExpiryBreadcrumbPath(root);
|
|
481
|
+
try {
|
|
482
|
+
if (!fs.existsSync(file)) return false;
|
|
483
|
+
fs.rmSync(file, { force: true });
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function statusCommand(args, root = process.cwd(), options = {}) {
|
|
345
491
|
const asJson = wantsJson(args);
|
|
492
|
+
const slot = pulse.resolvePulseSlot(root, options.homeDir ? { homeDir: options.homeDir } : {});
|
|
346
493
|
const receipts = pulse.readPulseReceipts(root);
|
|
347
494
|
const summary = pulse.summarizePulse(receipts);
|
|
348
|
-
const installed = cronInstalled();
|
|
495
|
+
const installed = cronInstalled(slot.markers, options.spawnSync || spawnSync);
|
|
496
|
+
const expiryBreadcrumb = installed ? null : readPulseExpiryBreadcrumb(root);
|
|
497
|
+
const writeOutput = options.writeOutput || ((text) => process.stdout.write(text));
|
|
349
498
|
const out = {
|
|
350
499
|
ok: true,
|
|
351
500
|
action: 'pulse_status',
|
|
352
501
|
cron_installed: installed,
|
|
502
|
+
expiry_breadcrumb: expiryBreadcrumb,
|
|
503
|
+
state_home: slot.activeStateHome,
|
|
504
|
+
marker: slot.activeMarker,
|
|
505
|
+
install_state_home: slot.stateHome,
|
|
506
|
+
install_marker: slot.marker,
|
|
507
|
+
legacy_slot: slot.legacyMatches,
|
|
353
508
|
...summary,
|
|
354
509
|
};
|
|
355
510
|
if (!asJson) {
|
|
356
|
-
|
|
357
|
-
`pulse: ${installed ? 'cron
|
|
511
|
+
writeOutput([
|
|
512
|
+
`pulse: ${installed ? 'cron installed' : 'cron not installed (run: atris pulse install)'}`,
|
|
513
|
+
expiryBreadcrumb ? `heartbeat expired on ${expiryBreadcrumb.expired_at}; renew with: ${expiryBreadcrumb.renew_command}` : '',
|
|
514
|
+
`state home: ${slot.activeStateHome} marker: ${slot.activeMarker}`,
|
|
358
515
|
`ticks: ${summary.total_ticks} | reward: ${summary.reward_sum} | verify pass/fail: ${summary.verify_pass}/${summary.verify_fail}`,
|
|
359
|
-
`last tick: ${summary.last_tick_ts || 'never'} (verify ${summary.last_verify_passed === null ? 'n/a' : summary.last_verify_passed ? 'pass' : '
|
|
360
|
-
summary.stale.stale ?
|
|
361
|
-
summary.orphan_ticks.length ?
|
|
516
|
+
`last tick: ${summary.last_tick_ts || 'never'} (verify ${summary.last_verify_passed === null ? 'n/a' : summary.last_verify_passed ? 'pass' : 'fail'})`,
|
|
517
|
+
summary.stale.stale ? `stale: ${summary.stale.reason}${summary.stale.tick_index ? ` (ghost tick #${summary.stale.tick_index})` : ''}` : 'liveness: fresh',
|
|
518
|
+
summary.orphan_ticks.length ? `orphan (crashed) ticks: ${summary.orphan_ticks.join(', ')}` : '',
|
|
362
519
|
].filter(Boolean).join('\n') + '\n');
|
|
363
520
|
}
|
|
364
521
|
return emit(out, asJson);
|
|
@@ -366,9 +523,9 @@ function statusCommand(args, root = process.cwd()) {
|
|
|
366
523
|
|
|
367
524
|
// --- atris pulse install / uninstall ---
|
|
368
525
|
|
|
369
|
-
function resolveAtrisBin() {
|
|
526
|
+
function resolveAtrisBin(commandSpawn = spawnSync) {
|
|
370
527
|
// Prefer a globally linked `atris`; fall back to this checkout's bin.
|
|
371
|
-
const which =
|
|
528
|
+
const which = commandSpawn('which', ['atris'], { encoding: 'utf8', timeout: 8000 });
|
|
372
529
|
if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
|
|
373
530
|
return `${process.execPath} ${path.join(__dirname, '..', 'bin', 'atris.js')}`;
|
|
374
531
|
}
|
|
@@ -376,17 +533,17 @@ function resolveAtrisBin() {
|
|
|
376
533
|
// The dirs holding the binaries the engine spawns by bare name (claude, node,
|
|
377
534
|
// git, atris). Baked into the cron script's PATH so a minimal cron environment
|
|
378
535
|
// can still find them. Missing tools just contribute nothing.
|
|
379
|
-
function resolveEngineBinDirs(extraBins = []) {
|
|
536
|
+
function resolveEngineBinDirs(extraBins = [], commandSpawn = spawnSync) {
|
|
380
537
|
const dirs = new Set([path.dirname(process.execPath)]); // node
|
|
381
538
|
for (const tool of ['ax', 'claude', 'git', 'atris']) {
|
|
382
|
-
const r =
|
|
539
|
+
const r = commandSpawn('which', [tool], { encoding: 'utf8', timeout: 8000 });
|
|
383
540
|
if (r.status === 0 && r.stdout.trim()) dirs.add(path.dirname(r.stdout.trim()));
|
|
384
541
|
}
|
|
385
542
|
for (const bin of [process.env.ATRIS_RUNNER_BIN, process.env.ATRIS_CLAUDE_BIN, ...extraBins]) {
|
|
386
543
|
const configured = String(bin || '').trim();
|
|
387
544
|
if (configured && configured.includes(path.sep)) dirs.add(path.dirname(configured));
|
|
388
545
|
if (configured && !configured.includes(path.sep)) {
|
|
389
|
-
const r =
|
|
546
|
+
const r = commandSpawn('which', [configured], { encoding: 'utf8', timeout: 8000 });
|
|
390
547
|
if (r.status === 0 && r.stdout.trim()) dirs.add(path.dirname(r.stdout.trim()));
|
|
391
548
|
}
|
|
392
549
|
}
|
|
@@ -394,8 +551,19 @@ function resolveEngineBinDirs(extraBins = []) {
|
|
|
394
551
|
return Array.from(dirs);
|
|
395
552
|
}
|
|
396
553
|
|
|
397
|
-
function
|
|
554
|
+
function stripMarkedCrontabLines(crontab, markers) {
|
|
555
|
+
const markerList = Array.isArray(markers) ? markers : [markers];
|
|
556
|
+
return String(crontab || '')
|
|
557
|
+
.split('\n')
|
|
558
|
+
.filter((line) => line && !markerList.some((marker) => marker && line.includes(marker)))
|
|
559
|
+
.join('\n');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function installCommand(args, root = process.cwd(), options = {}) {
|
|
398
563
|
const asJson = wantsJson(args);
|
|
564
|
+
const commandSpawn = options.spawnSync || spawnSync;
|
|
565
|
+
const writeOutput = options.writeOutput || ((text) => process.stdout.write(text));
|
|
566
|
+
const slot = pulse.resolvePulseSlot(root, options.homeDir ? { homeDir: options.homeDir } : {});
|
|
399
567
|
const cadenceInput = readFlag(args, '--cadence', pulse.DEFAULT_CADENCE_CRON);
|
|
400
568
|
let cron;
|
|
401
569
|
try {
|
|
@@ -429,23 +597,24 @@ function installCommand(args, root = process.cwd()) {
|
|
|
429
597
|
if (!asJson) process.stdout.write(`pulse install failed: ${out.detail}\n`);
|
|
430
598
|
return emit(out, asJson);
|
|
431
599
|
}
|
|
432
|
-
const verifyCmd = readFlag(args, '--verify',
|
|
600
|
+
const verifyCmd = readFlag(args, '--verify', pulse.defaultVerifyCmd(root));
|
|
433
601
|
const model = readFlag(args, '--model', process.env.ATRIS_RUNNER_MODEL || process.env.ATRIS_CLAUDE_MODEL || DEFAULT_CLAUDE_RUNNER_MODEL);
|
|
434
602
|
const runnerProfile = readFlag(args, '--runner-profile', process.env.ATRIS_RUNNER_PROFILE || '');
|
|
435
603
|
const runnerBin = readFlag(args, '--runner-bin', process.env.ATRIS_RUNNER_BIN || process.env.ATRIS_CLAUDE_BIN || '');
|
|
436
604
|
const runnerCommandTemplate = readFlag(args, '--runner-template', process.env.ATRIS_RUNNER_COMMAND_TEMPLATE || process.env.ATRIS_CLAUDE_COMMAND_TEMPLATE || '');
|
|
437
605
|
const deadlineEpoch = Math.floor(Date.now() / 1000) + expiry.seconds;
|
|
438
606
|
|
|
439
|
-
fs.mkdirSync(
|
|
440
|
-
const scriptPath = path.join(
|
|
607
|
+
fs.mkdirSync(slot.stateHome, { recursive: true });
|
|
608
|
+
const scriptPath = path.join(slot.stateHome, 'tick.sh');
|
|
441
609
|
// Resolve the real bin dirs the engine spawns by bare name, so cron's minimal
|
|
442
610
|
// PATH doesn't silently break the worker spawn (claude lives in ~/.local/bin).
|
|
443
|
-
const pathDirs = resolveEngineBinDirs([runnerBin]);
|
|
611
|
+
const pathDirs = resolveEngineBinDirs([runnerBin], commandSpawn);
|
|
444
612
|
const script = pulse.buildTickScript({
|
|
445
613
|
root,
|
|
446
|
-
atrisBin: resolveAtrisBin(),
|
|
447
|
-
stateHome:
|
|
614
|
+
atrisBin: resolveAtrisBin(commandSpawn),
|
|
615
|
+
stateHome: slot.stateHome,
|
|
448
616
|
deadlineEpoch,
|
|
617
|
+
marker: slot.marker,
|
|
449
618
|
model,
|
|
450
619
|
runnerProfile,
|
|
451
620
|
runnerBin,
|
|
@@ -455,18 +624,23 @@ function installCommand(args, root = process.cwd()) {
|
|
|
455
624
|
});
|
|
456
625
|
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
457
626
|
|
|
458
|
-
const line = pulse.buildCrontabLine({ cron, scriptPath });
|
|
459
|
-
// Append our line to the existing crontab
|
|
460
|
-
|
|
627
|
+
const line = pulse.buildCrontabLine({ cron, scriptPath, marker: slot.marker });
|
|
628
|
+
// Append our line to the existing crontab. Strip this repo's new marker and,
|
|
629
|
+
// when the legacy tick.sh belongs to this root, strip the old single-slot marker too.
|
|
630
|
+
const existing = commandSpawn('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
|
|
461
631
|
const prior = existing.status === 0 ? String(existing.stdout || '') : '';
|
|
462
|
-
const cleaned = prior
|
|
632
|
+
const cleaned = stripMarkedCrontabLines(prior, slot.markers);
|
|
463
633
|
const next = `${cleaned ? cleaned + '\n' : ''}${line}\n`;
|
|
464
|
-
const apply =
|
|
634
|
+
const apply = commandSpawn('crontab', ['-'], { input: next, encoding: 'utf8', timeout: 10000 });
|
|
635
|
+
const expiryBreadcrumbCleared = apply.status === 0 && clearPulseExpiryBreadcrumb(root);
|
|
465
636
|
|
|
466
637
|
const out = {
|
|
467
638
|
ok: apply.status === 0,
|
|
468
639
|
action: 'pulse_install',
|
|
469
640
|
script_path: scriptPath,
|
|
641
|
+
state_home: slot.stateHome,
|
|
642
|
+
marker: slot.marker,
|
|
643
|
+
legacy_migrated: slot.legacyMatches,
|
|
470
644
|
crontab_line: line,
|
|
471
645
|
cadence: cron,
|
|
472
646
|
cadence_input: cadenceInput,
|
|
@@ -477,34 +651,50 @@ function installCommand(args, root = process.cwd()) {
|
|
|
477
651
|
runner_profile: runnerProfile || null,
|
|
478
652
|
runner_bin: runnerBin || null,
|
|
479
653
|
runner_template_configured: Boolean(runnerCommandTemplate),
|
|
654
|
+
expiry_breadcrumb_cleared: expiryBreadcrumbCleared,
|
|
480
655
|
};
|
|
481
656
|
if (!asJson) {
|
|
482
657
|
if (apply.status === 0) {
|
|
483
|
-
|
|
658
|
+
writeOutput([
|
|
484
659
|
`pulse installed. heartbeat fires '${cron}' against ${root}.`,
|
|
485
660
|
`script: ${scriptPath}`,
|
|
486
661
|
`auto-expires in ${expiry.hours ? `${expiry.hours} hours` : `${expiry.days} days`}. stop early: atris pulse uninstall`,
|
|
487
662
|
].join('\n') + '\n');
|
|
488
663
|
} else {
|
|
489
|
-
|
|
664
|
+
writeOutput(`pulse install failed to write crontab: ${apply.stderr || apply.status}\nscript written to ${scriptPath}; add this line to your crontab manually:\n${line}\n`);
|
|
490
665
|
}
|
|
491
666
|
}
|
|
492
667
|
return emit(out, asJson);
|
|
493
668
|
}
|
|
494
669
|
|
|
495
|
-
function uninstallCommand(args) {
|
|
670
|
+
function uninstallCommand(args, root = process.cwd()) {
|
|
496
671
|
const asJson = wantsJson(args);
|
|
672
|
+
const slot = pulse.resolvePulseSlot(root);
|
|
497
673
|
const existing = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 10000 });
|
|
498
674
|
if (existing.status !== 0) {
|
|
499
|
-
const out = {
|
|
675
|
+
const out = {
|
|
676
|
+
ok: true,
|
|
677
|
+
action: 'pulse_uninstall',
|
|
678
|
+
removed: false,
|
|
679
|
+
reason: 'no_crontab',
|
|
680
|
+
state_home: slot.activeStateHome,
|
|
681
|
+
marker: slot.activeMarker,
|
|
682
|
+
};
|
|
500
683
|
if (!asJson) process.stdout.write('pulse: no crontab to clean.\n');
|
|
501
684
|
return emit(out, asJson);
|
|
502
685
|
}
|
|
503
686
|
const prior = String(existing.stdout || '');
|
|
504
|
-
const had = prior.includes(
|
|
505
|
-
const cleaned = prior
|
|
687
|
+
const had = slot.markers.some((marker) => prior.includes(marker));
|
|
688
|
+
const cleaned = stripMarkedCrontabLines(prior, slot.markers);
|
|
506
689
|
const apply = spawnSync('crontab', ['-'], { input: cleaned ? cleaned + '\n' : '', encoding: 'utf8', timeout: 10000 });
|
|
507
|
-
const out = {
|
|
690
|
+
const out = {
|
|
691
|
+
ok: apply.status === 0,
|
|
692
|
+
action: 'pulse_uninstall',
|
|
693
|
+
removed: had,
|
|
694
|
+
state_home: slot.activeStateHome,
|
|
695
|
+
marker: slot.activeMarker,
|
|
696
|
+
legacy_slot: slot.legacyMatches,
|
|
697
|
+
};
|
|
508
698
|
if (!asJson) process.stdout.write(had ? 'pulse uninstalled (crontab line removed).\n' : 'pulse: no heartbeat line found.\n');
|
|
509
699
|
return emit(out, asJson);
|
|
510
700
|
}
|
|
@@ -553,6 +743,10 @@ module.exports = {
|
|
|
553
743
|
tickCommand,
|
|
554
744
|
statusCommand,
|
|
555
745
|
cronInstalled,
|
|
746
|
+
crontabHasActiveMarker,
|
|
747
|
+
pulseExpiryBreadcrumbPath,
|
|
748
|
+
readPulseExpiryBreadcrumb,
|
|
749
|
+
clearPulseExpiryBreadcrumb,
|
|
556
750
|
installCommand,
|
|
557
751
|
uninstallCommand,
|
|
558
752
|
runCommand,
|
|
@@ -561,5 +755,6 @@ module.exports = {
|
|
|
561
755
|
runEngine,
|
|
562
756
|
gitChangedFiles,
|
|
563
757
|
runVerify,
|
|
564
|
-
|
|
758
|
+
refreshOrbPolicyLessons,
|
|
759
|
+
stripMarkedCrontabLines,
|
|
565
760
|
};
|