atris 3.38.0 → 3.41.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 +25 -6
- package/atris/PERSONA.md +8 -4
- package/atris.md +7 -0
- package/ax +2 -1
- package/bin/atris.js +31 -6
- package/commands/agent-spawn.js +13 -11
- package/commands/autoland.js +28 -77
- package/commands/bench.js +10 -12
- package/commands/business.js +345 -0
- package/commands/chat-scan.js +5 -7
- package/commands/codex-goal.js +8 -10
- package/commands/computer.js +20 -0
- package/commands/console.js +19 -3
- package/commands/decide.js +166 -0
- package/commands/deck.js +1 -4
- package/commands/drill.js +14 -24
- package/commands/engine.js +196 -8
- package/commands/gm.js +8 -6
- package/commands/harvest.js +1 -4
- package/commands/init.js +23 -3
- package/commands/land.js +8 -14
- package/commands/launchpad.js +1 -14
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/member.js +558 -574
- package/commands/mission.js +456 -237
- package/commands/pack.js +2746 -164
- package/commands/play.js +6 -4
- package/commands/probe.js +2 -2
- package/commands/pulse.js +15 -16
- package/commands/release.js +10 -9
- package/commands/router.js +5 -4
- package/commands/site-deploy.js +885 -0
- package/commands/site.js +11 -2
- package/commands/slop.js +14 -2
- package/commands/stream.js +4 -18
- package/commands/task.js +880 -558
- package/commands/taste.js +101 -0
- package/commands/team.js +176 -3
- package/commands/vercel.js +4 -2
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -22
- package/commands/wiki.js +1 -4
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +1 -14
- package/commands/xp.js +27 -24
- package/lib/accept-verify-gate.js +5 -1
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +116 -1
- package/lib/autoland.js +66 -0
- package/lib/bench/runner.js +19 -1
- package/lib/context-gatherer.js +7 -1
- package/lib/engine-registry.js +141 -20
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +65 -15
- package/lib/git-spawn.js +15 -0
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +0 -2
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +4 -1
- package/lib/official-cli-integration.js +47 -2
- package/lib/orb-context.js +8 -1
- package/lib/pack-capabilities.js +685 -0
- package/lib/router-brain.js +51 -1
- package/lib/runner-command.js +0 -6
- package/lib/self-drive.js +44 -13
- package/lib/task-db.js +137 -3
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +66 -0
- package/lib/wish-audit.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/zip.js +95 -7
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/commands/member.js
CHANGED
|
@@ -7,6 +7,8 @@ const { loadCredentials } = require('../utils/auth');
|
|
|
7
7
|
const { apiRequestJson } = require('../utils/api');
|
|
8
8
|
const { runAliveTick } = require('../lib/member-alive');
|
|
9
9
|
const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
10
|
+
const { readJson, writeJson } = require('../lib/json-file');
|
|
11
|
+
const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
|
|
10
12
|
|
|
11
13
|
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
12
14
|
let dir = path.resolve(startDir);
|
|
@@ -93,27 +95,6 @@ function parseConfirmFlag(flags) {
|
|
|
93
95
|
return joined.match(/--confirm[=\s]+["']?([^"']+)["']?/)?.[1] || '';
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function hasFlag(args, name) {
|
|
97
|
-
return args.includes(name);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function readFlag(args, name, fallback = '') {
|
|
101
|
-
const prefix = `${name}=`;
|
|
102
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
103
|
-
const arg = args[i];
|
|
104
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return args[i + 1];
|
|
105
|
-
if (String(arg).startsWith(prefix)) return String(arg).slice(prefix.length).replace(/^["']|["']$/g, '');
|
|
106
|
-
}
|
|
107
|
-
return fallback;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function readNumberFlag(args, name, fallback = null) {
|
|
111
|
-
const raw = readFlag(args, name, '');
|
|
112
|
-
if (!raw) return fallback;
|
|
113
|
-
const value = Number(raw);
|
|
114
|
-
return Number.isFinite(value) ? value : null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
98
|
function readRepeatedFlag(args, name) {
|
|
118
99
|
const values = [];
|
|
119
100
|
const prefix = `${name}=`;
|
|
@@ -365,17 +346,12 @@ function emptyMemberGoals(name) {
|
|
|
365
346
|
}
|
|
366
347
|
|
|
367
348
|
function loadMemberGoals(name, paths = memberPaths(name)) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
goals: Array.isArray(parsed.goals) ? parsed.goals : [],
|
|
375
|
-
};
|
|
376
|
-
} catch {
|
|
377
|
-
return emptyMemberGoals(name);
|
|
378
|
-
}
|
|
349
|
+
const parsed = readJson(paths.goalsJson, {}) || {};
|
|
350
|
+
return {
|
|
351
|
+
...emptyMemberGoals(name),
|
|
352
|
+
...parsed,
|
|
353
|
+
goals: Array.isArray(parsed.goals) ? parsed.goals : [],
|
|
354
|
+
};
|
|
379
355
|
}
|
|
380
356
|
|
|
381
357
|
function activeGoal(goalsState, goalId = '') {
|
|
@@ -1411,7 +1387,7 @@ function taskIsClosed(task) {
|
|
|
1411
1387
|
|
|
1412
1388
|
function taskProjectionRows() {
|
|
1413
1389
|
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
1414
|
-
const projection =
|
|
1390
|
+
const projection = readJson(projectionPath);
|
|
1415
1391
|
return Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
1416
1392
|
}
|
|
1417
1393
|
|
|
@@ -1511,7 +1487,7 @@ function sortEvidenceCandidates(candidates) {
|
|
|
1511
1487
|
|
|
1512
1488
|
function readTaskProjectionEvidence(name) {
|
|
1513
1489
|
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
1514
|
-
const projection =
|
|
1490
|
+
const projection = readJson(projectionPath);
|
|
1515
1491
|
const tasks = taskProjectionRows();
|
|
1516
1492
|
const candidates = sortEvidenceCandidates(
|
|
1517
1493
|
tasks
|
|
@@ -1663,7 +1639,7 @@ function latestActionableUserLine(thread) {
|
|
|
1663
1639
|
function readMemberRoomEvidence(name) {
|
|
1664
1640
|
const roots = [path.join(process.cwd(), '.obelisk', 'threads')];
|
|
1665
1641
|
const projectsPath = path.join(os.homedir(), '.obelisk', 'projects.json');
|
|
1666
|
-
const projects =
|
|
1642
|
+
const projects = readJson(projectsPath);
|
|
1667
1643
|
if (Array.isArray(projects)) {
|
|
1668
1644
|
const cwd = path.resolve(process.cwd());
|
|
1669
1645
|
const project = projects.find((item) => item?.path && path.resolve(item.path) === cwd && item.id);
|
|
@@ -1676,7 +1652,7 @@ function readMemberRoomEvidence(name) {
|
|
|
1676
1652
|
for (const item of listThreadJsonFiles(root)) {
|
|
1677
1653
|
if (seen.has(item.path)) continue;
|
|
1678
1654
|
seen.add(item.path);
|
|
1679
|
-
const thread =
|
|
1655
|
+
const thread = readJson(item.path);
|
|
1680
1656
|
files_checked += 1;
|
|
1681
1657
|
const context = thread?.atrisContext || {};
|
|
1682
1658
|
const linkedTasks = Array.isArray(context.linkedTasks) ? context.linkedTasks : [];
|
|
@@ -1717,11 +1693,11 @@ function readMemberRoomEvidence(name) {
|
|
|
1717
1693
|
}
|
|
1718
1694
|
|
|
1719
1695
|
function readRecentWakeReceiptEvidence(name) {
|
|
1720
|
-
const latestLoop =
|
|
1696
|
+
const latestLoop = readJson(memberLoopPaths(name).latestPath);
|
|
1721
1697
|
const receiptPath = Array.isArray(latestLoop?.tick_receipts)
|
|
1722
1698
|
? latestLoop.tick_receipts.slice().reverse().find(Boolean)
|
|
1723
1699
|
: null;
|
|
1724
|
-
const latestWake = receiptPath ?
|
|
1700
|
+
const latestWake = receiptPath ? readJson(receiptPath) : null;
|
|
1725
1701
|
return {
|
|
1726
1702
|
latest_loop_path: memberLoopPaths(name).latestPath,
|
|
1727
1703
|
latest_loop_status: latestLoop?.status || null,
|
|
@@ -1863,7 +1839,7 @@ function problemSignalSources(root = process.cwd()) {
|
|
|
1863
1839
|
|
|
1864
1840
|
function rowsFromProblemSignalSource(source) {
|
|
1865
1841
|
if (source.kind === 'jsonl') return readJsonlRowsIfExists(source.path, 80);
|
|
1866
|
-
const row =
|
|
1842
|
+
const row = readJson(source.path);
|
|
1867
1843
|
return row ? [row] : [];
|
|
1868
1844
|
}
|
|
1869
1845
|
|
|
@@ -1984,7 +1960,7 @@ function readMemberSignalSourceConfig(root = process.cwd()) {
|
|
|
1984
1960
|
path.join(root, '.atris', 'member-signal-sources.json'),
|
|
1985
1961
|
];
|
|
1986
1962
|
for (const configPath of candidates) {
|
|
1987
|
-
const config =
|
|
1963
|
+
const config = readJson(configPath);
|
|
1988
1964
|
if (config && typeof config === 'object') return { config, configPath };
|
|
1989
1965
|
}
|
|
1990
1966
|
return { config: null, configPath: null };
|
|
@@ -2839,7 +2815,7 @@ function installMemberAliveCron(name, args = []) {
|
|
|
2839
2815
|
stop_command: `atris member alive ${name} --uninstall`,
|
|
2840
2816
|
error,
|
|
2841
2817
|
};
|
|
2842
|
-
|
|
2818
|
+
writeJson(paths.latestPath, payload);
|
|
2843
2819
|
printJsonOrText(payload, [
|
|
2844
2820
|
`${dryRun ? 'Would install' : installed ? 'Installed' : 'Install failed'} hourly alive loop: ${name}`,
|
|
2845
2821
|
`Cron: ${crontabLine}`,
|
|
@@ -2907,14 +2883,6 @@ function writeMemberLoopReceipt(name, payload) {
|
|
|
2907
2883
|
return receiptPath;
|
|
2908
2884
|
}
|
|
2909
2885
|
|
|
2910
|
-
function readJsonIfExists(filePath) {
|
|
2911
|
-
try {
|
|
2912
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2913
|
-
} catch {
|
|
2914
|
-
return null;
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
2886
|
function isPidAlive(pid) {
|
|
2919
2887
|
const numeric = Number(pid);
|
|
2920
2888
|
if (!Number.isFinite(numeric) || numeric <= 0) return false;
|
|
@@ -2926,11 +2894,6 @@ function isPidAlive(pid) {
|
|
|
2926
2894
|
}
|
|
2927
2895
|
}
|
|
2928
2896
|
|
|
2929
|
-
function writeJsonFile(filePath, payload) {
|
|
2930
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2931
|
-
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
2897
|
function safeReadText(filePath, maxBytes = 250000) {
|
|
2935
2898
|
try {
|
|
2936
2899
|
const stat = fs.statSync(filePath);
|
|
@@ -2941,14 +2904,6 @@ function safeReadText(filePath, maxBytes = 250000) {
|
|
|
2941
2904
|
}
|
|
2942
2905
|
}
|
|
2943
2906
|
|
|
2944
|
-
function safeReadJson(filePath) {
|
|
2945
|
-
try {
|
|
2946
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2947
|
-
} catch {
|
|
2948
|
-
return null;
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
2907
|
function repoRelative(root, filePath) {
|
|
2953
2908
|
return path.relative(root, filePath).replace(/\\/g, '/');
|
|
2954
2909
|
}
|
|
@@ -3162,7 +3117,7 @@ function autoImproverTaskWaitingForHuman(task, status) {
|
|
|
3162
3117
|
|
|
3163
3118
|
function collectAutoImproverTaskSignals(root) {
|
|
3164
3119
|
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
3165
|
-
const projection =
|
|
3120
|
+
const projection = readJson(projectionPath);
|
|
3166
3121
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3167
3122
|
const openStatuses = new Set(['open', 'todo', 'claimed', 'doing', 'in_progress', 'review', 'blocked']);
|
|
3168
3123
|
const staleTasks = [];
|
|
@@ -3476,7 +3431,7 @@ function autoImproverTaskIsActionable(task) {
|
|
|
3476
3431
|
}
|
|
3477
3432
|
|
|
3478
3433
|
function findExistingAutoImproverTask(title) {
|
|
3479
|
-
const projection =
|
|
3434
|
+
const projection = readJson(path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json'));
|
|
3480
3435
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3481
3436
|
const key = lowerCompact(stripAutoImproverTitleNoise(title));
|
|
3482
3437
|
if (!key) return null;
|
|
@@ -3554,9 +3509,9 @@ function createAutoImproverTask(candidate, receiptPath) {
|
|
|
3554
3509
|
|
|
3555
3510
|
function writeAutoImproverReceipt(name, payload, receiptPath = null) {
|
|
3556
3511
|
const finalReceiptPath = receiptPath || path.join(process.cwd(), 'atris', 'runs', `auto-improver-dogfood-${fileSafeStamp()}.json`);
|
|
3557
|
-
|
|
3512
|
+
writeJson(finalReceiptPath, payload);
|
|
3558
3513
|
const latestPath = path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json');
|
|
3559
|
-
|
|
3514
|
+
writeJson(latestPath, { ...payload, receipt_path: finalReceiptPath });
|
|
3560
3515
|
return { receiptPath: finalReceiptPath, latestPath };
|
|
3561
3516
|
}
|
|
3562
3517
|
|
|
@@ -3646,7 +3601,7 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
|
|
|
3646
3601
|
: 'No bounded prevented-fire task was created yet.',
|
|
3647
3602
|
},
|
|
3648
3603
|
};
|
|
3649
|
-
const previousLatest =
|
|
3604
|
+
const previousLatest = readJson(path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json'));
|
|
3650
3605
|
const finalWrite = writeAutoImproverReceipt(name, payload, plannedReceiptPath);
|
|
3651
3606
|
// A no-op scan identical to the previous tick earns a receipt but not a journal entry —
|
|
3652
3607
|
// the cadence loop was appending the same "found: N, prevented: 0" row every ~4 minutes
|
|
@@ -3731,7 +3686,7 @@ function acquireMemberLoopLease(name, { runId, ttlMs }) {
|
|
|
3731
3686
|
return writeLease();
|
|
3732
3687
|
} catch (error) {
|
|
3733
3688
|
if (error.code !== 'EEXIST') throw error;
|
|
3734
|
-
const active =
|
|
3689
|
+
const active = readJson(paths.lockPath);
|
|
3735
3690
|
const expired = active && Number(active.expires_at_ms || 0) <= nowMs;
|
|
3736
3691
|
const deadPid = active && !isPidAlive(active.pid);
|
|
3737
3692
|
if (active && (expired || deadPid)) {
|
|
@@ -3749,7 +3704,7 @@ function acquireMemberLoopLease(name, { runId, ttlMs }) {
|
|
|
3749
3704
|
}
|
|
3750
3705
|
|
|
3751
3706
|
function refreshMemberLoopLease(paths, lease, ttlMs) {
|
|
3752
|
-
|
|
3707
|
+
writeJson(paths.lockPath, {
|
|
3753
3708
|
...lease,
|
|
3754
3709
|
heartbeat_at: stampIso(),
|
|
3755
3710
|
expires_at_ms: Date.now() + ttlMs,
|
|
@@ -3757,7 +3712,7 @@ function refreshMemberLoopLease(paths, lease, ttlMs) {
|
|
|
3757
3712
|
}
|
|
3758
3713
|
|
|
3759
3714
|
function releaseMemberLoopLease(paths, lease) {
|
|
3760
|
-
const active =
|
|
3715
|
+
const active = readJson(paths.lockPath);
|
|
3761
3716
|
if (!active || active.run_id !== lease.run_id || active.pid !== lease.pid) return;
|
|
3762
3717
|
fs.rmSync(paths.lockPath, { force: true });
|
|
3763
3718
|
}
|
|
@@ -3772,11 +3727,11 @@ function activeMemberLoopLease(lease) {
|
|
|
3772
3727
|
function archiveMemberLoopState(name) {
|
|
3773
3728
|
const paths = memberLoopPaths(name);
|
|
3774
3729
|
const archivedAt = stampIso();
|
|
3775
|
-
const previousLease =
|
|
3730
|
+
const previousLease = readJson(paths.lockPath);
|
|
3776
3731
|
const activeLease = activeMemberLoopLease(previousLease);
|
|
3777
3732
|
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
3778
3733
|
if (activeLease) {
|
|
3779
|
-
|
|
3734
|
+
writeJson(paths.stopPath, {
|
|
3780
3735
|
schema: 'atris.member_loop_stop.v1',
|
|
3781
3736
|
member: name,
|
|
3782
3737
|
requested_at: archivedAt,
|
|
@@ -3801,7 +3756,7 @@ function archiveMemberLoopState(name) {
|
|
|
3801
3756
|
stop_path: paths.stopPath,
|
|
3802
3757
|
latest_path: paths.latestPath,
|
|
3803
3758
|
};
|
|
3804
|
-
|
|
3759
|
+
writeJson(paths.latestPath, payload);
|
|
3805
3760
|
return payload;
|
|
3806
3761
|
}
|
|
3807
3762
|
|
|
@@ -4323,6 +4278,16 @@ function memberUpgrade(name) {
|
|
|
4323
4278
|
|
|
4324
4279
|
// --- PUSH subcommand ---
|
|
4325
4280
|
|
|
4281
|
+
function readMemberPushBusinessBinding(root = process.cwd()) {
|
|
4282
|
+
const file = path.join(root, '.atris', 'business.json');
|
|
4283
|
+
if (!fs.existsSync(file)) return null;
|
|
4284
|
+
try {
|
|
4285
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
throw new Error(`Failed to read .atris/business.json: ${e.message || e}`);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4326
4291
|
async function memberPush(name) {
|
|
4327
4292
|
if (!name) {
|
|
4328
4293
|
console.error('Usage: atris member push <name>');
|
|
@@ -4346,6 +4311,9 @@ async function memberPush(name) {
|
|
|
4346
4311
|
const content = fs.readFileSync(memberFile, 'utf8');
|
|
4347
4312
|
const fm = parseFrontmatter(content);
|
|
4348
4313
|
const existingAgentId = fm && fm['agent-id'];
|
|
4314
|
+
const taskDb = require('../lib/task-db');
|
|
4315
|
+
const binding = readMemberPushBusinessBinding(taskDb.workspaceRoot());
|
|
4316
|
+
const businessId = String(binding && (binding.business_id || binding.id) || '').trim();
|
|
4349
4317
|
|
|
4350
4318
|
if (existingAgentId) {
|
|
4351
4319
|
console.log(`Pushing member "${name}" to cloud (updating agent ${existingAgentId})...`);
|
|
@@ -4353,7 +4321,9 @@ async function memberPush(name) {
|
|
|
4353
4321
|
console.log(`Pushing member "${name}" to cloud (creating new agent)...`);
|
|
4354
4322
|
}
|
|
4355
4323
|
|
|
4356
|
-
|
|
4324
|
+
if (!businessId) {
|
|
4325
|
+
console.log('no business binding found; run `atris business init "<name>" --here` to bind this workspace.');
|
|
4326
|
+
}
|
|
4357
4327
|
const endpoint = businessId
|
|
4358
4328
|
? `/agent/import-member?business_id=${encodeURIComponent(businessId)}`
|
|
4359
4329
|
: '/agent/import-member';
|
|
@@ -7295,52 +7265,218 @@ function workspaceSnapshot(name = null, paths = null) {
|
|
|
7295
7265
|
}
|
|
7296
7266
|
}
|
|
7297
7267
|
|
|
7268
|
+
// Shared mission summary every wake decision carries. The mission-gate and generalist
|
|
7269
|
+
// rules coerce a falsy North Star to null (pre-gate we cannot trust the mission file yet).
|
|
7270
|
+
function wakeMissionSummary(purpose, { coerceNorthStarNull = false } = {}) {
|
|
7271
|
+
return {
|
|
7272
|
+
north_star: coerceNorthStarNull ? (purpose.northStar || null) : purpose.northStar,
|
|
7273
|
+
runtime_id: purpose.runtimeMission.id || null,
|
|
7274
|
+
runtime_status: purpose.runtimeMission.status || null,
|
|
7275
|
+
runtime_next: purpose.runtimeMission.next || null,
|
|
7276
|
+
};
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7279
|
+
// Merge a rule's verdict (decision/reason/ask/next_command + who owns it) with the
|
|
7280
|
+
// context every decision reports. Key order matters: callers serialize this to JSON.
|
|
7281
|
+
function composeWakeDecision(ctx, verdict) {
|
|
7282
|
+
const result = {
|
|
7283
|
+
decision: verdict.decision,
|
|
7284
|
+
reason: verdict.reason,
|
|
7285
|
+
needs_user: Boolean(verdict.needs_user),
|
|
7286
|
+
ask: verdict.ask || null,
|
|
7287
|
+
next_command: verdict.next_command,
|
|
7288
|
+
state: ctx.state,
|
|
7289
|
+
goal: verdict.goal,
|
|
7290
|
+
current_experiment: verdict.current_experiment,
|
|
7291
|
+
};
|
|
7292
|
+
if ('autonomous_problem' in verdict) result.autonomous_problem = verdict.autonomous_problem;
|
|
7293
|
+
result.checks = ctx.checks;
|
|
7294
|
+
result.mission = verdict.mission || wakeMissionSummary(ctx.purpose);
|
|
7295
|
+
result.steering = ctx.steering;
|
|
7296
|
+
result.evidence = ctx.evidence;
|
|
7297
|
+
result.workspace = ctx.workspace;
|
|
7298
|
+
return result;
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
// Rule: a generalist with a meaningful mission processes dropped domain files before
|
|
7302
|
+
// anything else. Fires before evidence collection, so it returns a complete decision
|
|
7303
|
+
// with its own checks and no workspace snapshot.
|
|
7304
|
+
function wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current }) {
|
|
7305
|
+
if (lowerCompact(name) !== 'generalist' || !purpose.meaningful) return null;
|
|
7306
|
+
const domainFile = findUnprocessedGeneralistDomainFile(process.cwd());
|
|
7307
|
+
if (!domainFile) return null;
|
|
7308
|
+
return {
|
|
7309
|
+
decision: 'process_domain_file',
|
|
7310
|
+
reason: `generalist_domain_scan:${domainFile.status}`,
|
|
7311
|
+
needs_user: false,
|
|
7312
|
+
ask: null,
|
|
7313
|
+
next_command: `atris member wake ${name} --execute --domain-file ${domainFile.path} --json`,
|
|
7314
|
+
state,
|
|
7315
|
+
goal: goal || null,
|
|
7316
|
+
current_experiment: current || null,
|
|
7317
|
+
checks: {
|
|
7318
|
+
has_member: true,
|
|
7319
|
+
has_mission: true,
|
|
7320
|
+
mission_meaningful: true,
|
|
7321
|
+
has_goal: Boolean(goal),
|
|
7322
|
+
has_open_experiment: Boolean(current),
|
|
7323
|
+
has_unprocessed_domain_file: true,
|
|
7324
|
+
domain_scan_window_minutes: 60,
|
|
7325
|
+
checked_existing_tasks: false,
|
|
7326
|
+
},
|
|
7327
|
+
mission: wakeMissionSummary(purpose, { coerceNorthStarNull: true }),
|
|
7328
|
+
steering,
|
|
7329
|
+
evidence: {
|
|
7330
|
+
generalist_domain_scan: domainFile,
|
|
7331
|
+
task_projection: null,
|
|
7332
|
+
nearest_open_loop: null,
|
|
7333
|
+
},
|
|
7334
|
+
workspace: null,
|
|
7335
|
+
domain_file: domainFile,
|
|
7336
|
+
};
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
// Rule: a member without a meaningful mission asks the operator before waking itself.
|
|
7340
|
+
function wakeRuleMissionGate(ctx) {
|
|
7341
|
+
if (ctx.purpose.meaningful) return null;
|
|
7342
|
+
return {
|
|
7343
|
+
decision: 'ask',
|
|
7344
|
+
reason: 'mission_missing_or_placeholder',
|
|
7345
|
+
needs_user: true,
|
|
7346
|
+
ask: `Define atris/team/${ctx.name}/MISSION.md with a concrete North Star before this member wakes itself.`,
|
|
7347
|
+
next_command: `edit atris/team/${ctx.name}/MISSION.md`,
|
|
7348
|
+
goal: ctx.goal || null,
|
|
7349
|
+
current_experiment: ctx.current || null,
|
|
7350
|
+
mission: wakeMissionSummary(ctx.purpose, { coerceNorthStarNull: true }),
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
// Rule: log-error discovery that asks for a task gets one opened autonomously.
|
|
7355
|
+
function wakeRuleAutonomousErrorTask(ctx) {
|
|
7356
|
+
const discoveredTask = ctx.evidence.problem_discovery?.selected || null;
|
|
7357
|
+
if (!discoveredTask) return null;
|
|
7358
|
+
if (discoveredTask.source !== 'log_error_scan' || discoveredTask.autonomous_action !== 'create_task') return null;
|
|
7359
|
+
return {
|
|
7360
|
+
decision: 'create_task',
|
|
7361
|
+
reason: 'autonomous_error_discovery:log_error_scan',
|
|
7362
|
+
next_command: discoveredTask.next_command,
|
|
7363
|
+
goal: ctx.goal || null,
|
|
7364
|
+
current_experiment: ctx.current || null,
|
|
7365
|
+
autonomous_problem: discoveredTask,
|
|
7366
|
+
};
|
|
7367
|
+
}
|
|
7368
|
+
|
|
7369
|
+
// Rule: no active goal — adopt a discovered problem as the objective, else stop.
|
|
7370
|
+
function wakeRuleNeedsGoal(ctx) {
|
|
7371
|
+
if (ctx.goal) return null;
|
|
7372
|
+
const discoveredProblem = ctx.evidence.problem_discovery?.selected || null;
|
|
7373
|
+
if (discoveredProblem) {
|
|
7374
|
+
return {
|
|
7375
|
+
decision: 'set_objective',
|
|
7376
|
+
reason: `autonomous_problem_discovery:${discoveredProblem.source}`,
|
|
7377
|
+
next_command: discoveredProblem.next_command || `atris member wake ${ctx.name} --execute --confirm-autonomy-policy`,
|
|
7378
|
+
goal: null,
|
|
7379
|
+
current_experiment: null,
|
|
7380
|
+
autonomous_problem: discoveredProblem,
|
|
7381
|
+
};
|
|
7382
|
+
}
|
|
7383
|
+
return {
|
|
7384
|
+
decision: 'stop',
|
|
7385
|
+
reason: 'no_active_goal',
|
|
7386
|
+
next_command: `atris member goal-from-mission ${ctx.name}`,
|
|
7387
|
+
goal: null,
|
|
7388
|
+
current_experiment: null,
|
|
7389
|
+
autonomous_problem: null,
|
|
7390
|
+
};
|
|
7391
|
+
}
|
|
7392
|
+
|
|
7393
|
+
// Rule: a blocked experiment needs operator input before another wake.
|
|
7394
|
+
function wakeRuleBlockedExperiment(ctx) {
|
|
7395
|
+
if (!ctx.blocked) return null;
|
|
7396
|
+
return {
|
|
7397
|
+
decision: 'ask',
|
|
7398
|
+
reason: 'blocked_experiment',
|
|
7399
|
+
needs_user: true,
|
|
7400
|
+
ask: ctx.blocked.block?.ask || 'Needs operator input before another wake.',
|
|
7401
|
+
next_command: `atris member review ${ctx.name} ${ctx.blocked.id} --discard --proof "..."`,
|
|
7402
|
+
goal: ctx.goal,
|
|
7403
|
+
current_experiment: ctx.blocked,
|
|
7404
|
+
};
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
// Rule: an open reviewable proposal parks the member until review. Skip the wait when
|
|
7408
|
+
// the only thing "open" is an unreviewable fallback proposal; otherwise the member is
|
|
7409
|
+
// parked forever on a gate no reviewer can clear.
|
|
7410
|
+
function wakeRuleOpenReviewableProposal(ctx) {
|
|
7411
|
+
if (!ctx.current || experimentIsUnreviewable(ctx.current)) return null;
|
|
7412
|
+
return {
|
|
7413
|
+
decision: 'wait',
|
|
7414
|
+
reason: `open_experiment_${ctx.current.status}`,
|
|
7415
|
+
next_command: `atris member review ${ctx.name} ${ctx.current.id} --accept --proof "..." --value 4`,
|
|
7416
|
+
goal: ctx.goal,
|
|
7417
|
+
current_experiment: ctx.current,
|
|
7418
|
+
};
|
|
7419
|
+
}
|
|
7420
|
+
|
|
7421
|
+
// Rule: the scored candidate list can steer the wake somewhere other than a tick.
|
|
7422
|
+
function wakeRuleScoredNonTickCandidate(ctx) {
|
|
7423
|
+
const selected = ctx.wakeScores.selected;
|
|
7424
|
+
if (!selected || selected.decision === 'tick') return null;
|
|
7425
|
+
const needsUser = selected.decision === 'ask';
|
|
7426
|
+
return {
|
|
7427
|
+
decision: selected.decision,
|
|
7428
|
+
reason: selected.reason,
|
|
7429
|
+
needs_user: needsUser,
|
|
7430
|
+
ask: needsUser ? (selected.ask || `Need operator input for ${selected.title}.`) : null,
|
|
7431
|
+
next_command: selected.next_command,
|
|
7432
|
+
goal: ctx.goal,
|
|
7433
|
+
current_experiment: null,
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
|
|
7437
|
+
// Rule: dirty files inside the member's own scope gate the wake unless forced.
|
|
7438
|
+
function wakeRuleDirtyMemberScope(ctx) {
|
|
7439
|
+
if (ctx.workspace.clean_for_member || ctx.force) return null;
|
|
7440
|
+
return {
|
|
7441
|
+
decision: 'wait',
|
|
7442
|
+
reason: 'workspace_dirty_in_member_scope',
|
|
7443
|
+
next_command: `commit/stash files in atris/team/${ctx.name}/ (or member scope) — or rerun: atris member wake ${ctx.name} --force`,
|
|
7444
|
+
goal: ctx.goal,
|
|
7445
|
+
current_experiment: null,
|
|
7446
|
+
};
|
|
7447
|
+
}
|
|
7448
|
+
|
|
7449
|
+
// Rule: nothing blocks the member, so take the safe next bounded step.
|
|
7450
|
+
function wakeRuleSafeBoundedTick(ctx) {
|
|
7451
|
+
return {
|
|
7452
|
+
decision: 'tick',
|
|
7453
|
+
reason: 'safe_next_bounded_step',
|
|
7454
|
+
next_command: `atris member tick ${ctx.name} --goal ${ctx.goal.id}`,
|
|
7455
|
+
goal: ctx.goal,
|
|
7456
|
+
current_experiment: null,
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
|
|
7460
|
+
// Ordered decision list: the first rule that speaks wins. Order is the contract.
|
|
7461
|
+
const WAKE_DECISION_RULES = [
|
|
7462
|
+
wakeRuleMissionGate,
|
|
7463
|
+
wakeRuleAutonomousErrorTask,
|
|
7464
|
+
wakeRuleNeedsGoal,
|
|
7465
|
+
wakeRuleBlockedExperiment,
|
|
7466
|
+
wakeRuleOpenReviewableProposal,
|
|
7467
|
+
wakeRuleScoredNonTickCandidate,
|
|
7468
|
+
wakeRuleDirtyMemberScope,
|
|
7469
|
+
wakeRuleSafeBoundedTick,
|
|
7470
|
+
];
|
|
7471
|
+
|
|
7298
7472
|
function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeKind(name) } = {}) {
|
|
7299
7473
|
const purpose = missionPurpose(paths);
|
|
7300
7474
|
const steering = readSteeringMemory(paths, name);
|
|
7301
7475
|
const state = loadMemberGoals(name, paths);
|
|
7302
7476
|
const goal = activeGoal(state);
|
|
7303
7477
|
const current = memberOpenExperiment(state);
|
|
7304
|
-
const
|
|
7305
|
-
if (
|
|
7306
|
-
const domainFile = findUnprocessedGeneralistDomainFile(process.cwd());
|
|
7307
|
-
if (domainFile) {
|
|
7308
|
-
return {
|
|
7309
|
-
decision: 'process_domain_file',
|
|
7310
|
-
reason: `generalist_domain_scan:${domainFile.status}`,
|
|
7311
|
-
needs_user: false,
|
|
7312
|
-
ask: null,
|
|
7313
|
-
next_command: `atris member wake ${name} --execute --domain-file ${domainFile.path} --json`,
|
|
7314
|
-
state,
|
|
7315
|
-
goal: goal || null,
|
|
7316
|
-
current_experiment: current || null,
|
|
7317
|
-
checks: {
|
|
7318
|
-
has_member: true,
|
|
7319
|
-
has_mission: true,
|
|
7320
|
-
mission_meaningful: true,
|
|
7321
|
-
has_goal: Boolean(goal),
|
|
7322
|
-
has_open_experiment: Boolean(current),
|
|
7323
|
-
has_unprocessed_domain_file: true,
|
|
7324
|
-
domain_scan_window_minutes: 60,
|
|
7325
|
-
checked_existing_tasks: false,
|
|
7326
|
-
},
|
|
7327
|
-
mission: {
|
|
7328
|
-
north_star: purpose.northStar || null,
|
|
7329
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7330
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7331
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7332
|
-
},
|
|
7333
|
-
steering,
|
|
7334
|
-
evidence: {
|
|
7335
|
-
generalist_domain_scan: domainFile,
|
|
7336
|
-
task_projection: null,
|
|
7337
|
-
nearest_open_loop: null,
|
|
7338
|
-
},
|
|
7339
|
-
workspace: null,
|
|
7340
|
-
domain_file: domainFile,
|
|
7341
|
-
};
|
|
7342
|
-
}
|
|
7343
|
-
}
|
|
7478
|
+
const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
|
|
7479
|
+
if (generalistDecision) return generalistDecision;
|
|
7344
7480
|
const rawDirective = steeringWakeDirective(steering, name, goal);
|
|
7345
7481
|
const directiveClosure = rawDirective ? steeringDirectiveClosure(rawDirective) : null;
|
|
7346
7482
|
const directive = directiveClosure?.all_closed ? null : rawDirective;
|
|
@@ -7383,219 +7519,12 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7383
7519
|
next_command: wakeScores.selected.next_command,
|
|
7384
7520
|
} : null;
|
|
7385
7521
|
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
reason: 'mission_missing_or_placeholder',
|
|
7391
|
-
needs_user: true,
|
|
7392
|
-
ask,
|
|
7393
|
-
next_command: `edit atris/team/${name}/MISSION.md`,
|
|
7394
|
-
state,
|
|
7395
|
-
goal: goal || null,
|
|
7396
|
-
current_experiment: current || null,
|
|
7397
|
-
checks,
|
|
7398
|
-
mission: {
|
|
7399
|
-
north_star: purpose.northStar || null,
|
|
7400
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7401
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7402
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7403
|
-
},
|
|
7404
|
-
steering,
|
|
7405
|
-
evidence,
|
|
7406
|
-
workspace,
|
|
7407
|
-
};
|
|
7408
|
-
}
|
|
7409
|
-
|
|
7410
|
-
const discoveredTask = evidence.problem_discovery?.selected || null;
|
|
7411
|
-
if (discoveredTask?.source === 'log_error_scan' && discoveredTask.autonomous_action === 'create_task') {
|
|
7412
|
-
return {
|
|
7413
|
-
decision: 'create_task',
|
|
7414
|
-
reason: 'autonomous_error_discovery:log_error_scan',
|
|
7415
|
-
needs_user: false,
|
|
7416
|
-
ask: null,
|
|
7417
|
-
next_command: discoveredTask.next_command,
|
|
7418
|
-
state,
|
|
7419
|
-
goal: goal || null,
|
|
7420
|
-
current_experiment: current || null,
|
|
7421
|
-
autonomous_problem: discoveredTask,
|
|
7422
|
-
checks,
|
|
7423
|
-
mission: {
|
|
7424
|
-
north_star: purpose.northStar,
|
|
7425
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7426
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7427
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7428
|
-
},
|
|
7429
|
-
steering,
|
|
7430
|
-
evidence,
|
|
7431
|
-
workspace,
|
|
7432
|
-
};
|
|
7433
|
-
}
|
|
7434
|
-
|
|
7435
|
-
if (!goal) {
|
|
7436
|
-
const discoveredProblem = evidence.problem_discovery?.selected || null;
|
|
7437
|
-
if (discoveredProblem) {
|
|
7438
|
-
return {
|
|
7439
|
-
decision: 'set_objective',
|
|
7440
|
-
reason: `autonomous_problem_discovery:${discoveredProblem.source}`,
|
|
7441
|
-
needs_user: false,
|
|
7442
|
-
ask: null,
|
|
7443
|
-
next_command: discoveredProblem.next_command || `atris member wake ${name} --execute --confirm-autonomy-policy`,
|
|
7444
|
-
state,
|
|
7445
|
-
goal: null,
|
|
7446
|
-
current_experiment: null,
|
|
7447
|
-
autonomous_problem: discoveredProblem,
|
|
7448
|
-
checks,
|
|
7449
|
-
mission: {
|
|
7450
|
-
north_star: purpose.northStar,
|
|
7451
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7452
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7453
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7454
|
-
},
|
|
7455
|
-
steering,
|
|
7456
|
-
evidence,
|
|
7457
|
-
workspace,
|
|
7458
|
-
};
|
|
7459
|
-
}
|
|
7460
|
-
return {
|
|
7461
|
-
decision: 'stop',
|
|
7462
|
-
reason: 'no_active_goal',
|
|
7463
|
-
needs_user: false,
|
|
7464
|
-
ask: null,
|
|
7465
|
-
next_command: `atris member goal-from-mission ${name}`,
|
|
7466
|
-
state,
|
|
7467
|
-
goal: null,
|
|
7468
|
-
current_experiment: null,
|
|
7469
|
-
autonomous_problem: null,
|
|
7470
|
-
checks,
|
|
7471
|
-
mission: {
|
|
7472
|
-
north_star: purpose.northStar,
|
|
7473
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7474
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7475
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7476
|
-
},
|
|
7477
|
-
steering,
|
|
7478
|
-
evidence,
|
|
7479
|
-
workspace,
|
|
7480
|
-
};
|
|
7522
|
+
const ctx = { name, force, purpose, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
|
|
7523
|
+
for (const rule of WAKE_DECISION_RULES) {
|
|
7524
|
+
const verdict = rule(ctx);
|
|
7525
|
+
if (verdict) return composeWakeDecision(ctx, verdict);
|
|
7481
7526
|
}
|
|
7482
|
-
|
|
7483
|
-
if (blocked) {
|
|
7484
|
-
return {
|
|
7485
|
-
decision: 'ask',
|
|
7486
|
-
reason: 'blocked_experiment',
|
|
7487
|
-
needs_user: true,
|
|
7488
|
-
ask: blocked.block?.ask || 'Needs operator input before another wake.',
|
|
7489
|
-
next_command: `atris member review ${name} ${blocked.id} --discard --proof "..."`,
|
|
7490
|
-
state,
|
|
7491
|
-
goal,
|
|
7492
|
-
current_experiment: blocked,
|
|
7493
|
-
checks,
|
|
7494
|
-
mission: {
|
|
7495
|
-
north_star: purpose.northStar,
|
|
7496
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7497
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7498
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7499
|
-
},
|
|
7500
|
-
steering,
|
|
7501
|
-
evidence,
|
|
7502
|
-
workspace,
|
|
7503
|
-
};
|
|
7504
|
-
}
|
|
7505
|
-
|
|
7506
|
-
// Skip the wait when the only thing "open" is an unreviewable fallback proposal;
|
|
7507
|
-
// otherwise the member is parked forever on a gate no reviewer can clear.
|
|
7508
|
-
if (current && !experimentIsUnreviewable(current)) {
|
|
7509
|
-
return {
|
|
7510
|
-
decision: 'wait',
|
|
7511
|
-
reason: `open_experiment_${current.status}`,
|
|
7512
|
-
needs_user: false,
|
|
7513
|
-
ask: null,
|
|
7514
|
-
next_command: `atris member review ${name} ${current.id} --accept --proof "..." --value 4`,
|
|
7515
|
-
state,
|
|
7516
|
-
goal,
|
|
7517
|
-
current_experiment: current,
|
|
7518
|
-
checks,
|
|
7519
|
-
mission: {
|
|
7520
|
-
north_star: purpose.northStar,
|
|
7521
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7522
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7523
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7524
|
-
},
|
|
7525
|
-
steering,
|
|
7526
|
-
evidence,
|
|
7527
|
-
workspace,
|
|
7528
|
-
};
|
|
7529
|
-
}
|
|
7530
|
-
|
|
7531
|
-
if (wakeScores.selected && wakeScores.selected.decision !== 'tick') {
|
|
7532
|
-
const selected = wakeScores.selected;
|
|
7533
|
-
const needsUser = selected.decision === 'ask';
|
|
7534
|
-
return {
|
|
7535
|
-
decision: selected.decision,
|
|
7536
|
-
reason: selected.reason,
|
|
7537
|
-
needs_user: needsUser,
|
|
7538
|
-
ask: needsUser ? (selected.ask || `Need operator input for ${selected.title}.`) : null,
|
|
7539
|
-
next_command: selected.next_command,
|
|
7540
|
-
state,
|
|
7541
|
-
goal,
|
|
7542
|
-
current_experiment: null,
|
|
7543
|
-
checks,
|
|
7544
|
-
mission: {
|
|
7545
|
-
north_star: purpose.northStar,
|
|
7546
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7547
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7548
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7549
|
-
},
|
|
7550
|
-
steering,
|
|
7551
|
-
evidence,
|
|
7552
|
-
workspace,
|
|
7553
|
-
};
|
|
7554
|
-
}
|
|
7555
|
-
|
|
7556
|
-
if (!workspace.clean_for_member && !force) {
|
|
7557
|
-
return {
|
|
7558
|
-
decision: 'wait',
|
|
7559
|
-
reason: 'workspace_dirty_in_member_scope',
|
|
7560
|
-
needs_user: false,
|
|
7561
|
-
ask: null,
|
|
7562
|
-
next_command: `commit/stash files in atris/team/${name}/ (or member scope) — or rerun: atris member wake ${name} --force`,
|
|
7563
|
-
state,
|
|
7564
|
-
goal,
|
|
7565
|
-
current_experiment: null,
|
|
7566
|
-
checks,
|
|
7567
|
-
mission: {
|
|
7568
|
-
north_star: purpose.northStar,
|
|
7569
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7570
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7571
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7572
|
-
},
|
|
7573
|
-
steering,
|
|
7574
|
-
evidence,
|
|
7575
|
-
workspace,
|
|
7576
|
-
};
|
|
7577
|
-
}
|
|
7578
|
-
|
|
7579
|
-
return {
|
|
7580
|
-
decision: 'tick',
|
|
7581
|
-
reason: 'safe_next_bounded_step',
|
|
7582
|
-
needs_user: false,
|
|
7583
|
-
ask: null,
|
|
7584
|
-
next_command: `atris member tick ${name} --goal ${goal.id}`,
|
|
7585
|
-
state,
|
|
7586
|
-
goal,
|
|
7587
|
-
current_experiment: null,
|
|
7588
|
-
checks,
|
|
7589
|
-
mission: {
|
|
7590
|
-
north_star: purpose.northStar,
|
|
7591
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7592
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7593
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7594
|
-
},
|
|
7595
|
-
steering,
|
|
7596
|
-
evidence,
|
|
7597
|
-
workspace,
|
|
7598
|
-
};
|
|
7527
|
+
return null;
|
|
7599
7528
|
}
|
|
7600
7529
|
|
|
7601
7530
|
async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
|
|
@@ -8029,84 +7958,60 @@ function memberAliveAll(...args) {
|
|
|
8029
7958
|
return memberLoopAll('alive', ...args);
|
|
8030
7959
|
}
|
|
8031
7960
|
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
const
|
|
8036
|
-
const
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
if (hasFlag(args, '--uninstall')) {
|
|
8055
|
-
return uninstallMemberAliveCron(name, args);
|
|
8056
|
-
}
|
|
8057
|
-
|
|
8058
|
-
if (status) {
|
|
8059
|
-
const active = readJsonIfExists(paths.lockPath);
|
|
8060
|
-
const latest = readJsonIfExists(paths.latestPath);
|
|
8061
|
-
const payload = {
|
|
8062
|
-
ok: true,
|
|
8063
|
-
action: 'loop_status',
|
|
8064
|
-
member: name,
|
|
8065
|
-
active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now() && isPidAlive(active.pid)),
|
|
8066
|
-
lease: active || null,
|
|
8067
|
-
latest: latest || null,
|
|
8068
|
-
hourly_alive: memberAliveCronStatus(name, paths),
|
|
8069
|
-
lock_path: paths.lockPath,
|
|
8070
|
-
latest_path: paths.latestPath,
|
|
8071
|
-
};
|
|
8072
|
-
printJsonOrText(payload, [
|
|
8073
|
-
`Loop status: ${name}`,
|
|
8074
|
-
`Active: ${payload.active ? 'yes' : 'no'}`,
|
|
8075
|
-
`Hourly: ${payload.hourly_alive.installed ? 'installed' : payload.hourly_alive.script_exists ? 'script ready' : 'not installed'}`,
|
|
8076
|
-
`Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
|
|
8077
|
-
], asJson);
|
|
8078
|
-
return;
|
|
8079
|
-
}
|
|
7961
|
+
// Loop phase: report lease + latest-receipt state without touching anything.
|
|
7962
|
+
function memberLoopStatusReport(name, paths, asJson) {
|
|
7963
|
+
const active = readJson(paths.lockPath);
|
|
7964
|
+
const latest = readJson(paths.latestPath);
|
|
7965
|
+
const payload = {
|
|
7966
|
+
ok: true,
|
|
7967
|
+
action: 'loop_status',
|
|
7968
|
+
member: name,
|
|
7969
|
+
active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now() && isPidAlive(active.pid)),
|
|
7970
|
+
lease: active || null,
|
|
7971
|
+
latest: latest || null,
|
|
7972
|
+
hourly_alive: memberAliveCronStatus(name, paths),
|
|
7973
|
+
lock_path: paths.lockPath,
|
|
7974
|
+
latest_path: paths.latestPath,
|
|
7975
|
+
};
|
|
7976
|
+
printJsonOrText(payload, [
|
|
7977
|
+
`Loop status: ${name}`,
|
|
7978
|
+
`Active: ${payload.active ? 'yes' : 'no'}`,
|
|
7979
|
+
`Hourly: ${payload.hourly_alive.installed ? 'installed' : payload.hourly_alive.script_exists ? 'script ready' : 'not installed'}`,
|
|
7980
|
+
`Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
|
|
7981
|
+
], asJson);
|
|
7982
|
+
}
|
|
8080
7983
|
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
7984
|
+
// Loop phase: drop a stop file so the running loop breaks at its next tick boundary.
|
|
7985
|
+
function memberLoopRequestStop(name, paths, asJson) {
|
|
7986
|
+
const requestedAt = stampIso();
|
|
7987
|
+
const stopPayload = {
|
|
7988
|
+
schema: 'atris.member_loop_stop.v1',
|
|
7989
|
+
member: name,
|
|
7990
|
+
requested_at: requestedAt,
|
|
7991
|
+
pid: process.pid,
|
|
7992
|
+
};
|
|
7993
|
+
writeJson(paths.stopPath, stopPayload);
|
|
7994
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
7995
|
+
ok: true,
|
|
7996
|
+
action: 'loop_stop',
|
|
7997
|
+
member: name,
|
|
7998
|
+
requested_at: requestedAt,
|
|
7999
|
+
stop_path: paths.stopPath,
|
|
8000
|
+
});
|
|
8001
|
+
printJsonOrText({
|
|
8002
|
+
ok: true,
|
|
8003
|
+
action: 'loop_stop',
|
|
8004
|
+
member: name,
|
|
8005
|
+
stop_path: paths.stopPath,
|
|
8006
|
+
receipt_path: receiptPath,
|
|
8007
|
+
}, [
|
|
8008
|
+
`Stop requested for ${name}.`,
|
|
8009
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
8010
|
+
], asJson);
|
|
8011
|
+
}
|
|
8109
8012
|
|
|
8013
|
+
// Loop phase: derive the timing plan (tick count, interval, duration, lease ttl) from flags.
|
|
8014
|
+
function memberLoopTimingPlan(name, args, { execute, confirmed }) {
|
|
8110
8015
|
const ticksFlag = readNumberFlag(args, '--ticks', null);
|
|
8111
8016
|
const minutes = readNumberFlag(args, '--minutes', null);
|
|
8112
8017
|
const durationSeconds = readNumberFlag(args, '--duration-seconds', readNumberFlag(args, '--seconds', null));
|
|
@@ -8137,183 +8042,190 @@ async function memberLoop(name, ...args) {
|
|
|
8137
8042
|
forever ? intervalMs + (2 * 60 * 60 * 1000) : durationMs + 60000,
|
|
8138
8043
|
intervalMs + 60000,
|
|
8139
8044
|
);
|
|
8045
|
+
return { ticksFlag, hourly, forever, intervalMs, durationMs, ticks, runId, startedAt, ttlMs };
|
|
8046
|
+
}
|
|
8140
8047
|
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8048
|
+
// Loop phase: refuse to execute without the autonomy confirmation, with a receipt saying why.
|
|
8049
|
+
function memberLoopRefuseUnconfirmed(name, paths, { asJson, startedAt }) {
|
|
8050
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
8051
|
+
ok: false,
|
|
8052
|
+
action: 'loop',
|
|
8053
|
+
member: name,
|
|
8054
|
+
status: 'blocked',
|
|
8055
|
+
reason: 'execute_requires_confirm_autonomy_policy',
|
|
8056
|
+
mode: 'execute',
|
|
8057
|
+
started_at: startedAt,
|
|
8058
|
+
finished_at: stampIso(),
|
|
8059
|
+
});
|
|
8060
|
+
const payload = {
|
|
8061
|
+
ok: false,
|
|
8062
|
+
action: 'loop',
|
|
8063
|
+
member: name,
|
|
8064
|
+
status: 'blocked',
|
|
8065
|
+
reason: 'execute_requires_confirm_autonomy_policy',
|
|
8066
|
+
receipt_path: receiptPath,
|
|
8067
|
+
};
|
|
8068
|
+
writeJson(paths.latestPath, payload);
|
|
8069
|
+
printJsonOrText(payload, [
|
|
8070
|
+
`Loop blocked for ${name}: execute requires --confirm-autonomy-policy.`,
|
|
8071
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
8072
|
+
], asJson);
|
|
8073
|
+
process.exitCode = 1;
|
|
8074
|
+
}
|
|
8168
8075
|
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8076
|
+
// Loop phase: another loop holds the lease, so skip this run and leave a receipt behind.
|
|
8077
|
+
function memberLoopSkipAlreadyActive(name, paths, lease, { asJson, execute, startedAt }) {
|
|
8078
|
+
const receiptPath = writeMemberLoopReceipt(name, {
|
|
8079
|
+
ok: true,
|
|
8080
|
+
action: 'loop',
|
|
8081
|
+
member: name,
|
|
8082
|
+
status: 'skipped',
|
|
8083
|
+
reason: 'loop_already_active',
|
|
8084
|
+
mode: execute ? 'execute' : 'dry_run',
|
|
8085
|
+
active_lease: lease.lease || null,
|
|
8086
|
+
started_at: startedAt,
|
|
8087
|
+
finished_at: stampIso(),
|
|
8088
|
+
});
|
|
8089
|
+
const payload = {
|
|
8090
|
+
ok: true,
|
|
8091
|
+
action: 'loop',
|
|
8092
|
+
member: name,
|
|
8093
|
+
status: 'skipped',
|
|
8094
|
+
reason: 'loop_already_active',
|
|
8095
|
+
ticks: 0,
|
|
8096
|
+
receipt_path: receiptPath,
|
|
8097
|
+
active_lease: lease.lease || null,
|
|
8098
|
+
};
|
|
8099
|
+
writeJson(paths.latestPath, payload);
|
|
8100
|
+
printJsonOrText(payload, [
|
|
8101
|
+
`Loop skipped for ${name}: another loop is active.`,
|
|
8102
|
+
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
8103
|
+
], asJson);
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
// Tick phase (alive mode): run the full operate pipeline once and record what it did.
|
|
8107
|
+
function memberLoopAliveTick(name, run, index, tickStartedAt) {
|
|
8108
|
+
const { options, state } = run;
|
|
8109
|
+
const alive = runAliveTick(name, {
|
|
8110
|
+
execute: options.execute,
|
|
8111
|
+
confirmed: options.confirmed,
|
|
8112
|
+
force: options.force,
|
|
8113
|
+
agent: options.agent,
|
|
8114
|
+
model: options.model,
|
|
8115
|
+
maxWallSeconds: options.operateMaxWall,
|
|
8116
|
+
autoAcceptLimit: options.autoAcceptLimit,
|
|
8117
|
+
noPrime: index > 0,
|
|
8118
|
+
});
|
|
8119
|
+
const key = `${alive.status || 'alive'}:${alive.reason || 'tick'}`;
|
|
8120
|
+
state.decisions[key] = (state.decisions[key] || 0) + 1;
|
|
8121
|
+
const tick = {
|
|
8122
|
+
tick: index + 1,
|
|
8123
|
+
started_at: tickStartedAt,
|
|
8124
|
+
finished_at: alive.finished_at || stampIso(),
|
|
8125
|
+
ok: alive.ok !== false,
|
|
8126
|
+
decision: alive.status || 'alive_tick',
|
|
8127
|
+
reason: alive.reason || null,
|
|
8128
|
+
executed: options.execute,
|
|
8129
|
+
needs_user: alive.needs_user === true,
|
|
8130
|
+
next_command: alive.next_command || null,
|
|
8131
|
+
has_mission: alive.has_mission === true,
|
|
8132
|
+
has_goal: alive.has_goal === true,
|
|
8133
|
+
has_steering: false,
|
|
8134
|
+
productive: alive.status === 'completed',
|
|
8135
|
+
blocked_on_human: alive.blocked_on_human === true,
|
|
8136
|
+
operate_ok: alive.operate?.ok,
|
|
8137
|
+
auto_accept_accepted: alive.auto_accept?.json?.summary?.accepted ?? alive.auto_accept?.json?.summary?.would_accept,
|
|
8138
|
+
receipt_path: alive.receipt_path || alive.operate?.receipt_path || null,
|
|
8139
|
+
alive,
|
|
8140
|
+
};
|
|
8141
|
+
state.tickResults.push(tick);
|
|
8142
|
+
fs.appendFileSync(state.tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8143
|
+
}
|
|
8144
|
+
|
|
8145
|
+
// Tick phase (wake mode): take one wake decision, record it, and notice a member stuck
|
|
8146
|
+
// re-asking the same question so the loop can hand off instead of burning remaining ticks.
|
|
8147
|
+
async function memberLoopWakeTick(name, run, index, tickStartedAt) {
|
|
8148
|
+
const { options, state } = run;
|
|
8149
|
+
const wake = await runMemberWake(name, { execute: options.execute, confirmed: options.confirmed, force: options.force });
|
|
8150
|
+
const key = `${wake.decision}:${wake.reason}`;
|
|
8151
|
+
state.decisions[key] = (state.decisions[key] || 0) + 1;
|
|
8152
|
+
const tick = {
|
|
8153
|
+
tick: index + 1,
|
|
8154
|
+
started_at: tickStartedAt,
|
|
8155
|
+
finished_at: stampIso(),
|
|
8156
|
+
ok: true,
|
|
8157
|
+
decision: wake.decision,
|
|
8158
|
+
reason: wake.reason,
|
|
8159
|
+
executed: wake.executed,
|
|
8160
|
+
needs_user: wake.needs_user,
|
|
8161
|
+
next_command: wake.next_command,
|
|
8162
|
+
has_mission: wake.checks?.has_mission === true,
|
|
8163
|
+
has_goal: wake.checks?.has_goal === true,
|
|
8164
|
+
has_steering: wake.checks?.has_steering === true,
|
|
8165
|
+
productive: wake.executed === true
|
|
8166
|
+
|| /executed/.test(wake.reason || '')
|
|
8167
|
+
|| ['tick', 'close_loop', 'report_proof', 'create_missing_task', 'create_task', 'set_objective'].includes(wake.decision),
|
|
8168
|
+
blocked_on_human: wake.needs_user === true || /^open_experiment_/.test(wake.reason || ''),
|
|
8169
|
+
current_experiment: wake.current_experiment?.id || null,
|
|
8170
|
+
receipt_path: wake.receipt_path,
|
|
8171
|
+
};
|
|
8172
|
+
state.tickResults.push(tick);
|
|
8173
|
+
fs.appendFileSync(state.tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8199
8174
|
|
|
8200
|
-
fs.rmSync(paths.stopPath, { force: true });
|
|
8201
|
-
const tickLogPath = path.join(process.cwd(), 'atris', 'runs', `${runId}.jsonl`);
|
|
8202
|
-
fs.mkdirSync(path.dirname(tickLogPath), { recursive: true });
|
|
8203
|
-
const tickResults = [];
|
|
8204
|
-
const decisions = {};
|
|
8205
|
-
let stopped = false;
|
|
8206
|
-
let failed = false;
|
|
8207
|
-
// Stop spinning: if the member can do no real work for this many ticks in a row (waiting on a
|
|
8208
|
-
// human review, blocked, or no goal), break early and surface a clear handoff instead of
|
|
8209
|
-
// burning every remaining tick on the same no-op decision.
|
|
8210
|
-
let earlyExit = null;
|
|
8211
|
-
let consecutiveIdle = 0;
|
|
8212
|
-
const idleBreakThreshold = 2;
|
|
8213
8175
|
// Repeated identical `ask:*` wake decisions (e.g. ask:mission_missing_or_placeholder) mean the
|
|
8214
8176
|
// member is stuck asking the same question every tick regardless of --execute; the idle
|
|
8215
|
-
// early-exit
|
|
8177
|
+
// early-exit only fires in execute mode, so without this a dry-run loop burns every
|
|
8216
8178
|
// remaining tick re-asking. Reuse the same blocked_on_human handoff once we see the ask repeat.
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8179
|
+
if (wake.decision === 'ask') {
|
|
8180
|
+
if (key === state.lastAskKey) {
|
|
8181
|
+
state.consecutiveIdenticalAsk += 1;
|
|
8182
|
+
} else {
|
|
8183
|
+
state.lastAskKey = key;
|
|
8184
|
+
state.consecutiveIdenticalAsk = 1;
|
|
8185
|
+
}
|
|
8186
|
+
if (state.consecutiveIdenticalAsk >= MEMBER_LOOP_IDENTICAL_ASK_BREAK_THRESHOLD) {
|
|
8187
|
+
state.earlyExit = {
|
|
8188
|
+
after_tick: tick.tick,
|
|
8189
|
+
decision: tick.decision || null,
|
|
8190
|
+
reason: tick.reason || 'idle',
|
|
8191
|
+
needs_user: tick.needs_user === true,
|
|
8192
|
+
blocked_on_human: true,
|
|
8193
|
+
next_command: tick.next_command || null,
|
|
8194
|
+
stop: 'repeated_identical_ask',
|
|
8195
|
+
};
|
|
8196
|
+
}
|
|
8197
|
+
} else {
|
|
8198
|
+
state.lastAskKey = null;
|
|
8199
|
+
state.consecutiveIdenticalAsk = 0;
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
|
|
8203
|
+
// Stop spinning: if the member can do no real work for this many ticks in a row (waiting on a
|
|
8204
|
+
// human review, blocked, or no goal), break early and surface a clear handoff instead of
|
|
8205
|
+
// burning every remaining tick on the same no-op decision.
|
|
8206
|
+
const MEMBER_LOOP_IDLE_BREAK_THRESHOLD = 2;
|
|
8207
|
+
const MEMBER_LOOP_IDENTICAL_ASK_BREAK_THRESHOLD = 2;
|
|
8220
8208
|
|
|
8209
|
+
// Loop phase: control. Hold the lease, honor the stop file, pace ticks by interval, and
|
|
8210
|
+
// break early when the member is idling instead of faking activity.
|
|
8211
|
+
async function memberLoopRunTicks(name, paths, lease, run) {
|
|
8212
|
+
const { options, plan, state } = run;
|
|
8221
8213
|
try {
|
|
8222
|
-
for (let index = 0; index < ticks; index += 1) {
|
|
8214
|
+
for (let index = 0; index < plan.ticks; index += 1) {
|
|
8223
8215
|
if (fs.existsSync(paths.stopPath)) {
|
|
8224
|
-
stopped = true;
|
|
8216
|
+
state.stopped = true;
|
|
8225
8217
|
break;
|
|
8226
8218
|
}
|
|
8227
|
-
refreshMemberLoopLease(paths, lease.lease, ttlMs);
|
|
8219
|
+
refreshMemberLoopLease(paths, lease.lease, plan.ttlMs);
|
|
8228
8220
|
const tickStartedAt = stampIso();
|
|
8229
8221
|
try {
|
|
8230
|
-
if (aliveMode) {
|
|
8231
|
-
|
|
8232
|
-
execute,
|
|
8233
|
-
confirmed,
|
|
8234
|
-
force,
|
|
8235
|
-
agent,
|
|
8236
|
-
model,
|
|
8237
|
-
maxWallSeconds: operateMaxWall,
|
|
8238
|
-
autoAcceptLimit,
|
|
8239
|
-
noPrime: index > 0,
|
|
8240
|
-
});
|
|
8241
|
-
const key = `${alive.status || 'alive'}:${alive.reason || 'tick'}`;
|
|
8242
|
-
decisions[key] = (decisions[key] || 0) + 1;
|
|
8243
|
-
const tick = {
|
|
8244
|
-
tick: index + 1,
|
|
8245
|
-
started_at: tickStartedAt,
|
|
8246
|
-
finished_at: alive.finished_at || stampIso(),
|
|
8247
|
-
ok: alive.ok !== false,
|
|
8248
|
-
decision: alive.status || 'alive_tick',
|
|
8249
|
-
reason: alive.reason || null,
|
|
8250
|
-
executed: execute,
|
|
8251
|
-
needs_user: alive.needs_user === true,
|
|
8252
|
-
next_command: alive.next_command || null,
|
|
8253
|
-
has_mission: alive.has_mission === true,
|
|
8254
|
-
has_goal: alive.has_goal === true,
|
|
8255
|
-
has_steering: false,
|
|
8256
|
-
productive: alive.status === 'completed',
|
|
8257
|
-
blocked_on_human: alive.blocked_on_human === true,
|
|
8258
|
-
operate_ok: alive.operate?.ok,
|
|
8259
|
-
auto_accept_accepted: alive.auto_accept?.json?.summary?.accepted ?? alive.auto_accept?.json?.summary?.would_accept,
|
|
8260
|
-
receipt_path: alive.receipt_path || alive.operate?.receipt_path || null,
|
|
8261
|
-
alive,
|
|
8262
|
-
};
|
|
8263
|
-
tickResults.push(tick);
|
|
8264
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8222
|
+
if (options.aliveMode) {
|
|
8223
|
+
memberLoopAliveTick(name, run, index, tickStartedAt);
|
|
8265
8224
|
} else {
|
|
8266
|
-
|
|
8267
|
-
const key = `${wake.decision}:${wake.reason}`;
|
|
8268
|
-
decisions[key] = (decisions[key] || 0) + 1;
|
|
8269
|
-
const tick = {
|
|
8270
|
-
tick: index + 1,
|
|
8271
|
-
started_at: tickStartedAt,
|
|
8272
|
-
finished_at: stampIso(),
|
|
8273
|
-
ok: true,
|
|
8274
|
-
decision: wake.decision,
|
|
8275
|
-
reason: wake.reason,
|
|
8276
|
-
executed: wake.executed,
|
|
8277
|
-
needs_user: wake.needs_user,
|
|
8278
|
-
next_command: wake.next_command,
|
|
8279
|
-
has_mission: wake.checks?.has_mission === true,
|
|
8280
|
-
has_goal: wake.checks?.has_goal === true,
|
|
8281
|
-
has_steering: wake.checks?.has_steering === true,
|
|
8282
|
-
productive: wake.executed === true
|
|
8283
|
-
|| /executed/.test(wake.reason || '')
|
|
8284
|
-
|| ['tick', 'close_loop', 'report_proof', 'create_missing_task', 'create_task', 'set_objective'].includes(wake.decision),
|
|
8285
|
-
blocked_on_human: wake.needs_user === true || /^open_experiment_/.test(wake.reason || ''),
|
|
8286
|
-
current_experiment: wake.current_experiment?.id || null,
|
|
8287
|
-
receipt_path: wake.receipt_path,
|
|
8288
|
-
};
|
|
8289
|
-
tickResults.push(tick);
|
|
8290
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8291
|
-
|
|
8292
|
-
if (wake.decision === 'ask') {
|
|
8293
|
-
if (key === lastAskKey) {
|
|
8294
|
-
consecutiveIdenticalAsk += 1;
|
|
8295
|
-
} else {
|
|
8296
|
-
lastAskKey = key;
|
|
8297
|
-
consecutiveIdenticalAsk = 1;
|
|
8298
|
-
}
|
|
8299
|
-
if (consecutiveIdenticalAsk >= identicalAskBreakThreshold) {
|
|
8300
|
-
earlyExit = {
|
|
8301
|
-
after_tick: tick.tick,
|
|
8302
|
-
decision: tick.decision || null,
|
|
8303
|
-
reason: tick.reason || 'idle',
|
|
8304
|
-
needs_user: tick.needs_user === true,
|
|
8305
|
-
blocked_on_human: true,
|
|
8306
|
-
next_command: tick.next_command || null,
|
|
8307
|
-
stop: 'repeated_identical_ask',
|
|
8308
|
-
};
|
|
8309
|
-
}
|
|
8310
|
-
} else {
|
|
8311
|
-
lastAskKey = null;
|
|
8312
|
-
consecutiveIdenticalAsk = 0;
|
|
8313
|
-
}
|
|
8225
|
+
await memberLoopWakeTick(name, run, index, tickStartedAt);
|
|
8314
8226
|
}
|
|
8315
8227
|
} catch (error) {
|
|
8316
|
-
failed = true;
|
|
8228
|
+
state.failed = true;
|
|
8317
8229
|
const tick = {
|
|
8318
8230
|
tick: index + 1,
|
|
8319
8231
|
started_at: tickStartedAt,
|
|
@@ -8321,19 +8233,19 @@ async function memberLoop(name, ...args) {
|
|
|
8321
8233
|
ok: false,
|
|
8322
8234
|
error: error instanceof Error ? error.message : String(error),
|
|
8323
8235
|
};
|
|
8324
|
-
tickResults.push(tick);
|
|
8325
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8236
|
+
state.tickResults.push(tick);
|
|
8237
|
+
fs.appendFileSync(state.tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8326
8238
|
break;
|
|
8327
8239
|
}
|
|
8328
|
-
if (earlyExit) break;
|
|
8329
|
-
if (execute) {
|
|
8330
|
-
const last = tickResults[tickResults.length - 1];
|
|
8240
|
+
if (state.earlyExit) break;
|
|
8241
|
+
if (options.execute) {
|
|
8242
|
+
const last = state.tickResults[state.tickResults.length - 1];
|
|
8331
8243
|
if (last && last.productive) {
|
|
8332
|
-
consecutiveIdle = 0;
|
|
8244
|
+
state.consecutiveIdle = 0;
|
|
8333
8245
|
} else if (last) {
|
|
8334
|
-
consecutiveIdle += 1;
|
|
8335
|
-
if (consecutiveIdle >=
|
|
8336
|
-
earlyExit = {
|
|
8246
|
+
state.consecutiveIdle += 1;
|
|
8247
|
+
if (state.consecutiveIdle >= MEMBER_LOOP_IDLE_BREAK_THRESHOLD) {
|
|
8248
|
+
state.earlyExit = {
|
|
8337
8249
|
after_tick: last.tick,
|
|
8338
8250
|
decision: last.decision || null,
|
|
8339
8251
|
reason: last.reason || 'idle',
|
|
@@ -8345,12 +8257,19 @@ async function memberLoop(name, ...args) {
|
|
|
8345
8257
|
}
|
|
8346
8258
|
}
|
|
8347
8259
|
}
|
|
8348
|
-
if (index < ticks - 1 && intervalMs > 0) sleepSync(intervalMs);
|
|
8260
|
+
if (index < plan.ticks - 1 && plan.intervalMs > 0) sleepSync(plan.intervalMs);
|
|
8349
8261
|
}
|
|
8350
8262
|
} finally {
|
|
8351
8263
|
releaseMemberLoopLease(paths, lease.lease);
|
|
8352
8264
|
}
|
|
8265
|
+
}
|
|
8353
8266
|
|
|
8267
|
+
// Loop phase: result recording. One summary object, one receipt, one latest pointer, one report.
|
|
8268
|
+
function memberLoopRecordResult(name, paths, lease, run) {
|
|
8269
|
+
const { options, plan, state } = run;
|
|
8270
|
+
const { execute, aliveMode, asJson } = options;
|
|
8271
|
+
const { hourly, forever, runId, ticks, intervalMs, durationMs, startedAt } = plan;
|
|
8272
|
+
const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath } = state;
|
|
8354
8273
|
const finishedAt = stampIso();
|
|
8355
8274
|
const summary = {
|
|
8356
8275
|
ok: !failed,
|
|
@@ -8392,7 +8311,7 @@ async function memberLoop(name, ...args) {
|
|
|
8392
8311
|
};
|
|
8393
8312
|
const receiptPath = writeMemberLoopReceipt(name, summary);
|
|
8394
8313
|
const payload = { ...summary, receipt_path: receiptPath };
|
|
8395
|
-
|
|
8314
|
+
writeJson(paths.latestPath, payload);
|
|
8396
8315
|
printJsonOrText(payload, [
|
|
8397
8316
|
`${aliveMode ? 'Alive' : 'Loop'}: ${name}`,
|
|
8398
8317
|
`Status: ${payload.status}`,
|
|
@@ -8404,6 +8323,71 @@ async function memberLoop(name, ...args) {
|
|
|
8404
8323
|
], asJson);
|
|
8405
8324
|
}
|
|
8406
8325
|
|
|
8326
|
+
// Driver: the phases above do the work; this reads as setup -> gate -> run -> record.
|
|
8327
|
+
async function memberLoop(name, ...args) {
|
|
8328
|
+
if (name === '--all' || name === 'all') return memberLoopAll('loop', ...args);
|
|
8329
|
+
requireMemberDir(name);
|
|
8330
|
+
const asJson = hasFlag(args, '--json');
|
|
8331
|
+
const aliveMode = hasFlag(args, '--alive');
|
|
8332
|
+
const execute = hasFlag(args, '--execute');
|
|
8333
|
+
const confirmed = hasFlag(args, '--confirm-autonomy-policy');
|
|
8334
|
+
const force = hasFlag(args, '--force');
|
|
8335
|
+
const agentFlag = String(readFlag(args, '--agent', '') || '').trim().toLowerCase();
|
|
8336
|
+
const agent = agentFlag === 'claude' ? 'claude' : agentFlag === 'codex' ? 'codex' : undefined;
|
|
8337
|
+
const model = String(readFlag(args, '--model', '') || '').trim() || undefined;
|
|
8338
|
+
const operateMaxWall = Math.max(60, Math.min(1800, Number(readNumberFlag(args, '--operate-max-wall', readNumberFlag(args, '--max-wall', 900)))));
|
|
8339
|
+
const autoAcceptLimit = Math.max(1, Math.floor(Number(readNumberFlag(args, '--auto-accept-limit', 8))));
|
|
8340
|
+
const options = { asJson, aliveMode, execute, confirmed, force, agent, model, operateMaxWall, autoAcceptLimit };
|
|
8341
|
+
const paths = memberLoopPaths(name);
|
|
8342
|
+
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
8343
|
+
|
|
8344
|
+
if (hasFlag(args, '--install')) {
|
|
8345
|
+
return installMemberAliveCron(name, args);
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
if (hasFlag(args, '--uninstall')) {
|
|
8349
|
+
return uninstallMemberAliveCron(name, args);
|
|
8350
|
+
}
|
|
8351
|
+
|
|
8352
|
+
if (hasFlag(args, '--status')) {
|
|
8353
|
+
return memberLoopStatusReport(name, paths, asJson);
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8356
|
+
if (hasFlag(args, '--stop')) {
|
|
8357
|
+
return memberLoopRequestStop(name, paths, asJson);
|
|
8358
|
+
}
|
|
8359
|
+
|
|
8360
|
+
const plan = memberLoopTimingPlan(name, args, { execute, confirmed });
|
|
8361
|
+
|
|
8362
|
+
if (execute && !confirmed) {
|
|
8363
|
+
return memberLoopRefuseUnconfirmed(name, paths, { asJson, startedAt: plan.startedAt });
|
|
8364
|
+
}
|
|
8365
|
+
|
|
8366
|
+
const lease = acquireMemberLoopLease(name, { runId: plan.runId, ttlMs: plan.ttlMs });
|
|
8367
|
+
if (!lease.acquired) {
|
|
8368
|
+
return memberLoopSkipAlreadyActive(name, paths, lease, { asJson, execute, startedAt: plan.startedAt });
|
|
8369
|
+
}
|
|
8370
|
+
|
|
8371
|
+
fs.rmSync(paths.stopPath, { force: true });
|
|
8372
|
+
const tickLogPath = path.join(process.cwd(), 'atris', 'runs', `${plan.runId}.jsonl`);
|
|
8373
|
+
fs.mkdirSync(path.dirname(tickLogPath), { recursive: true });
|
|
8374
|
+
const state = {
|
|
8375
|
+
tickResults: [],
|
|
8376
|
+
decisions: {},
|
|
8377
|
+
stopped: false,
|
|
8378
|
+
failed: false,
|
|
8379
|
+
earlyExit: null,
|
|
8380
|
+
consecutiveIdle: 0,
|
|
8381
|
+
lastAskKey: null,
|
|
8382
|
+
consecutiveIdenticalAsk: 0,
|
|
8383
|
+
tickLogPath,
|
|
8384
|
+
};
|
|
8385
|
+
|
|
8386
|
+
const run = { options, plan, state };
|
|
8387
|
+
await memberLoopRunTicks(name, paths, lease, run);
|
|
8388
|
+
memberLoopRecordResult(name, paths, lease, run);
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8407
8391
|
async function memberTick(name, ...args) {
|
|
8408
8392
|
const paths = requireMemberDir(name);
|
|
8409
8393
|
const asJson = hasFlag(args, '--json');
|