atris 3.37.1 → 3.40.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/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/commands/member.js
CHANGED
|
@@ -7,6 +7,26 @@ 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');
|
|
12
|
+
|
|
13
|
+
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
14
|
+
let dir = path.resolve(startDir);
|
|
15
|
+
while (true) {
|
|
16
|
+
const file = path.join(dir, '.atris', 'business.json');
|
|
17
|
+
if (fs.existsSync(file)) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')).business_id || null;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn(`Warning: could not parse ${file}: ${error.message}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const parent = path.dirname(dir);
|
|
26
|
+
if (parent === dir) return null;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
|
|
11
31
|
function todayLogName() {
|
|
12
32
|
const now = new Date();
|
|
@@ -75,27 +95,6 @@ function parseConfirmFlag(flags) {
|
|
|
75
95
|
return joined.match(/--confirm[=\s]+["']?([^"']+)["']?/)?.[1] || '';
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
function hasFlag(args, name) {
|
|
79
|
-
return args.includes(name);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readFlag(args, name, fallback = '') {
|
|
83
|
-
const prefix = `${name}=`;
|
|
84
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
85
|
-
const arg = args[i];
|
|
86
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return args[i + 1];
|
|
87
|
-
if (String(arg).startsWith(prefix)) return String(arg).slice(prefix.length).replace(/^["']|["']$/g, '');
|
|
88
|
-
}
|
|
89
|
-
return fallback;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function readNumberFlag(args, name, fallback = null) {
|
|
93
|
-
const raw = readFlag(args, name, '');
|
|
94
|
-
if (!raw) return fallback;
|
|
95
|
-
const value = Number(raw);
|
|
96
|
-
return Number.isFinite(value) ? value : null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
98
|
function readRepeatedFlag(args, name) {
|
|
100
99
|
const values = [];
|
|
101
100
|
const prefix = `${name}=`;
|
|
@@ -347,17 +346,12 @@ function emptyMemberGoals(name) {
|
|
|
347
346
|
}
|
|
348
347
|
|
|
349
348
|
function loadMemberGoals(name, paths = memberPaths(name)) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
goals: Array.isArray(parsed.goals) ? parsed.goals : [],
|
|
357
|
-
};
|
|
358
|
-
} catch {
|
|
359
|
-
return emptyMemberGoals(name);
|
|
360
|
-
}
|
|
349
|
+
const parsed = readJson(paths.goalsJson, {}) || {};
|
|
350
|
+
return {
|
|
351
|
+
...emptyMemberGoals(name),
|
|
352
|
+
...parsed,
|
|
353
|
+
goals: Array.isArray(parsed.goals) ? parsed.goals : [],
|
|
354
|
+
};
|
|
361
355
|
}
|
|
362
356
|
|
|
363
357
|
function activeGoal(goalsState, goalId = '') {
|
|
@@ -1393,7 +1387,7 @@ function taskIsClosed(task) {
|
|
|
1393
1387
|
|
|
1394
1388
|
function taskProjectionRows() {
|
|
1395
1389
|
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
1396
|
-
const projection =
|
|
1390
|
+
const projection = readJson(projectionPath);
|
|
1397
1391
|
return Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
1398
1392
|
}
|
|
1399
1393
|
|
|
@@ -1493,7 +1487,7 @@ function sortEvidenceCandidates(candidates) {
|
|
|
1493
1487
|
|
|
1494
1488
|
function readTaskProjectionEvidence(name) {
|
|
1495
1489
|
const projectionPath = path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json');
|
|
1496
|
-
const projection =
|
|
1490
|
+
const projection = readJson(projectionPath);
|
|
1497
1491
|
const tasks = taskProjectionRows();
|
|
1498
1492
|
const candidates = sortEvidenceCandidates(
|
|
1499
1493
|
tasks
|
|
@@ -1645,7 +1639,7 @@ function latestActionableUserLine(thread) {
|
|
|
1645
1639
|
function readMemberRoomEvidence(name) {
|
|
1646
1640
|
const roots = [path.join(process.cwd(), '.obelisk', 'threads')];
|
|
1647
1641
|
const projectsPath = path.join(os.homedir(), '.obelisk', 'projects.json');
|
|
1648
|
-
const projects =
|
|
1642
|
+
const projects = readJson(projectsPath);
|
|
1649
1643
|
if (Array.isArray(projects)) {
|
|
1650
1644
|
const cwd = path.resolve(process.cwd());
|
|
1651
1645
|
const project = projects.find((item) => item?.path && path.resolve(item.path) === cwd && item.id);
|
|
@@ -1658,7 +1652,7 @@ function readMemberRoomEvidence(name) {
|
|
|
1658
1652
|
for (const item of listThreadJsonFiles(root)) {
|
|
1659
1653
|
if (seen.has(item.path)) continue;
|
|
1660
1654
|
seen.add(item.path);
|
|
1661
|
-
const thread =
|
|
1655
|
+
const thread = readJson(item.path);
|
|
1662
1656
|
files_checked += 1;
|
|
1663
1657
|
const context = thread?.atrisContext || {};
|
|
1664
1658
|
const linkedTasks = Array.isArray(context.linkedTasks) ? context.linkedTasks : [];
|
|
@@ -1699,11 +1693,11 @@ function readMemberRoomEvidence(name) {
|
|
|
1699
1693
|
}
|
|
1700
1694
|
|
|
1701
1695
|
function readRecentWakeReceiptEvidence(name) {
|
|
1702
|
-
const latestLoop =
|
|
1696
|
+
const latestLoop = readJson(memberLoopPaths(name).latestPath);
|
|
1703
1697
|
const receiptPath = Array.isArray(latestLoop?.tick_receipts)
|
|
1704
1698
|
? latestLoop.tick_receipts.slice().reverse().find(Boolean)
|
|
1705
1699
|
: null;
|
|
1706
|
-
const latestWake = receiptPath ?
|
|
1700
|
+
const latestWake = receiptPath ? readJson(receiptPath) : null;
|
|
1707
1701
|
return {
|
|
1708
1702
|
latest_loop_path: memberLoopPaths(name).latestPath,
|
|
1709
1703
|
latest_loop_status: latestLoop?.status || null,
|
|
@@ -1845,7 +1839,7 @@ function problemSignalSources(root = process.cwd()) {
|
|
|
1845
1839
|
|
|
1846
1840
|
function rowsFromProblemSignalSource(source) {
|
|
1847
1841
|
if (source.kind === 'jsonl') return readJsonlRowsIfExists(source.path, 80);
|
|
1848
|
-
const row =
|
|
1842
|
+
const row = readJson(source.path);
|
|
1849
1843
|
return row ? [row] : [];
|
|
1850
1844
|
}
|
|
1851
1845
|
|
|
@@ -1966,7 +1960,7 @@ function readMemberSignalSourceConfig(root = process.cwd()) {
|
|
|
1966
1960
|
path.join(root, '.atris', 'member-signal-sources.json'),
|
|
1967
1961
|
];
|
|
1968
1962
|
for (const configPath of candidates) {
|
|
1969
|
-
const config =
|
|
1963
|
+
const config = readJson(configPath);
|
|
1970
1964
|
if (config && typeof config === 'object') return { config, configPath };
|
|
1971
1965
|
}
|
|
1972
1966
|
return { config: null, configPath: null };
|
|
@@ -2394,6 +2388,27 @@ function experimentIsClosed(experiment) {
|
|
|
2394
2388
|
return ['accepted', 'discarded', 'superseded'].includes(String(experiment?.status || '').toLowerCase());
|
|
2395
2389
|
}
|
|
2396
2390
|
|
|
2391
|
+
// A proposal that cannot be reviewed must not gate the member.
|
|
2392
|
+
//
|
|
2393
|
+
// wake returns decision 'wait' for any open experiment, on the theory that the
|
|
2394
|
+
// ball is with a human reviewer. That is right for a real proposal. It is a
|
|
2395
|
+
// permanent deadlock for a FALLBACK proposal: those carry verifier=null and
|
|
2396
|
+
// generation.mode='fallback' (template text emitted when LLM authoring did not
|
|
2397
|
+
// happen), and the review executor correctly refuses to accept anything
|
|
2398
|
+
// unverifiable. So the member waits on a gate nothing can pass -- observed
|
|
2399
|
+
// 2026-07-26 in project-obelisk with validator blocked 52 days, signal-scout 48,
|
|
2400
|
+
// and land-keeper 22, three of six rostered members doing no work at all.
|
|
2401
|
+
//
|
|
2402
|
+
// Deliberately narrow: a proposal WITH a verifier still blocks (a human genuinely
|
|
2403
|
+
// owes it a review), and 'running'/'blocked' still block regardless of shape.
|
|
2404
|
+
function experimentIsUnreviewable(experiment) {
|
|
2405
|
+
if (String(experiment?.status || '').toLowerCase() !== 'proposed') return false;
|
|
2406
|
+
const verifier = experiment?.verifier;
|
|
2407
|
+
const hasVerifier = Array.isArray(verifier) ? verifier.length > 0 : Boolean(String(verifier || '').trim());
|
|
2408
|
+
if (hasVerifier) return false;
|
|
2409
|
+
return String(experiment?.generation?.mode || '').toLowerCase() === 'fallback';
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2397
2412
|
function supersedeOtherOpenExperiments(state, activeGoal, proof) {
|
|
2398
2413
|
const superseded = [];
|
|
2399
2414
|
for (const goal of state.goals || []) {
|
|
@@ -2800,7 +2815,7 @@ function installMemberAliveCron(name, args = []) {
|
|
|
2800
2815
|
stop_command: `atris member alive ${name} --uninstall`,
|
|
2801
2816
|
error,
|
|
2802
2817
|
};
|
|
2803
|
-
|
|
2818
|
+
writeJson(paths.latestPath, payload);
|
|
2804
2819
|
printJsonOrText(payload, [
|
|
2805
2820
|
`${dryRun ? 'Would install' : installed ? 'Installed' : 'Install failed'} hourly alive loop: ${name}`,
|
|
2806
2821
|
`Cron: ${crontabLine}`,
|
|
@@ -2868,14 +2883,6 @@ function writeMemberLoopReceipt(name, payload) {
|
|
|
2868
2883
|
return receiptPath;
|
|
2869
2884
|
}
|
|
2870
2885
|
|
|
2871
|
-
function readJsonIfExists(filePath) {
|
|
2872
|
-
try {
|
|
2873
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2874
|
-
} catch {
|
|
2875
|
-
return null;
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
2886
|
function isPidAlive(pid) {
|
|
2880
2887
|
const numeric = Number(pid);
|
|
2881
2888
|
if (!Number.isFinite(numeric) || numeric <= 0) return false;
|
|
@@ -2887,11 +2894,6 @@ function isPidAlive(pid) {
|
|
|
2887
2894
|
}
|
|
2888
2895
|
}
|
|
2889
2896
|
|
|
2890
|
-
function writeJsonFile(filePath, payload) {
|
|
2891
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2892
|
-
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
2897
|
function safeReadText(filePath, maxBytes = 250000) {
|
|
2896
2898
|
try {
|
|
2897
2899
|
const stat = fs.statSync(filePath);
|
|
@@ -2902,14 +2904,6 @@ function safeReadText(filePath, maxBytes = 250000) {
|
|
|
2902
2904
|
}
|
|
2903
2905
|
}
|
|
2904
2906
|
|
|
2905
|
-
function safeReadJson(filePath) {
|
|
2906
|
-
try {
|
|
2907
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2908
|
-
} catch {
|
|
2909
|
-
return null;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
2907
|
function repoRelative(root, filePath) {
|
|
2914
2908
|
return path.relative(root, filePath).replace(/\\/g, '/');
|
|
2915
2909
|
}
|
|
@@ -3123,7 +3117,7 @@ function autoImproverTaskWaitingForHuman(task, status) {
|
|
|
3123
3117
|
|
|
3124
3118
|
function collectAutoImproverTaskSignals(root) {
|
|
3125
3119
|
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
3126
|
-
const projection =
|
|
3120
|
+
const projection = readJson(projectionPath);
|
|
3127
3121
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3128
3122
|
const openStatuses = new Set(['open', 'todo', 'claimed', 'doing', 'in_progress', 'review', 'blocked']);
|
|
3129
3123
|
const staleTasks = [];
|
|
@@ -3437,7 +3431,7 @@ function autoImproverTaskIsActionable(task) {
|
|
|
3437
3431
|
}
|
|
3438
3432
|
|
|
3439
3433
|
function findExistingAutoImproverTask(title) {
|
|
3440
|
-
const projection =
|
|
3434
|
+
const projection = readJson(path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json'));
|
|
3441
3435
|
const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
|
|
3442
3436
|
const key = lowerCompact(stripAutoImproverTitleNoise(title));
|
|
3443
3437
|
if (!key) return null;
|
|
@@ -3515,9 +3509,9 @@ function createAutoImproverTask(candidate, receiptPath) {
|
|
|
3515
3509
|
|
|
3516
3510
|
function writeAutoImproverReceipt(name, payload, receiptPath = null) {
|
|
3517
3511
|
const finalReceiptPath = receiptPath || path.join(process.cwd(), 'atris', 'runs', `auto-improver-dogfood-${fileSafeStamp()}.json`);
|
|
3518
|
-
|
|
3512
|
+
writeJson(finalReceiptPath, payload);
|
|
3519
3513
|
const latestPath = path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json');
|
|
3520
|
-
|
|
3514
|
+
writeJson(latestPath, { ...payload, receipt_path: finalReceiptPath });
|
|
3521
3515
|
return { receiptPath: finalReceiptPath, latestPath };
|
|
3522
3516
|
}
|
|
3523
3517
|
|
|
@@ -3607,7 +3601,7 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
|
|
|
3607
3601
|
: 'No bounded prevented-fire task was created yet.',
|
|
3608
3602
|
},
|
|
3609
3603
|
};
|
|
3610
|
-
const previousLatest =
|
|
3604
|
+
const previousLatest = readJson(path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json'));
|
|
3611
3605
|
const finalWrite = writeAutoImproverReceipt(name, payload, plannedReceiptPath);
|
|
3612
3606
|
// A no-op scan identical to the previous tick earns a receipt but not a journal entry —
|
|
3613
3607
|
// the cadence loop was appending the same "found: N, prevented: 0" row every ~4 minutes
|
|
@@ -3692,7 +3686,7 @@ function acquireMemberLoopLease(name, { runId, ttlMs }) {
|
|
|
3692
3686
|
return writeLease();
|
|
3693
3687
|
} catch (error) {
|
|
3694
3688
|
if (error.code !== 'EEXIST') throw error;
|
|
3695
|
-
const active =
|
|
3689
|
+
const active = readJson(paths.lockPath);
|
|
3696
3690
|
const expired = active && Number(active.expires_at_ms || 0) <= nowMs;
|
|
3697
3691
|
const deadPid = active && !isPidAlive(active.pid);
|
|
3698
3692
|
if (active && (expired || deadPid)) {
|
|
@@ -3710,7 +3704,7 @@ function acquireMemberLoopLease(name, { runId, ttlMs }) {
|
|
|
3710
3704
|
}
|
|
3711
3705
|
|
|
3712
3706
|
function refreshMemberLoopLease(paths, lease, ttlMs) {
|
|
3713
|
-
|
|
3707
|
+
writeJson(paths.lockPath, {
|
|
3714
3708
|
...lease,
|
|
3715
3709
|
heartbeat_at: stampIso(),
|
|
3716
3710
|
expires_at_ms: Date.now() + ttlMs,
|
|
@@ -3718,7 +3712,7 @@ function refreshMemberLoopLease(paths, lease, ttlMs) {
|
|
|
3718
3712
|
}
|
|
3719
3713
|
|
|
3720
3714
|
function releaseMemberLoopLease(paths, lease) {
|
|
3721
|
-
const active =
|
|
3715
|
+
const active = readJson(paths.lockPath);
|
|
3722
3716
|
if (!active || active.run_id !== lease.run_id || active.pid !== lease.pid) return;
|
|
3723
3717
|
fs.rmSync(paths.lockPath, { force: true });
|
|
3724
3718
|
}
|
|
@@ -3733,11 +3727,11 @@ function activeMemberLoopLease(lease) {
|
|
|
3733
3727
|
function archiveMemberLoopState(name) {
|
|
3734
3728
|
const paths = memberLoopPaths(name);
|
|
3735
3729
|
const archivedAt = stampIso();
|
|
3736
|
-
const previousLease =
|
|
3730
|
+
const previousLease = readJson(paths.lockPath);
|
|
3737
3731
|
const activeLease = activeMemberLoopLease(previousLease);
|
|
3738
3732
|
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
3739
3733
|
if (activeLease) {
|
|
3740
|
-
|
|
3734
|
+
writeJson(paths.stopPath, {
|
|
3741
3735
|
schema: 'atris.member_loop_stop.v1',
|
|
3742
3736
|
member: name,
|
|
3743
3737
|
requested_at: archivedAt,
|
|
@@ -3762,7 +3756,7 @@ function archiveMemberLoopState(name) {
|
|
|
3762
3756
|
stop_path: paths.stopPath,
|
|
3763
3757
|
latest_path: paths.latestPath,
|
|
3764
3758
|
};
|
|
3765
|
-
|
|
3759
|
+
writeJson(paths.latestPath, payload);
|
|
3766
3760
|
return payload;
|
|
3767
3761
|
}
|
|
3768
3762
|
|
|
@@ -4284,6 +4278,16 @@ function memberUpgrade(name) {
|
|
|
4284
4278
|
|
|
4285
4279
|
// --- PUSH subcommand ---
|
|
4286
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
|
+
|
|
4287
4291
|
async function memberPush(name) {
|
|
4288
4292
|
if (!name) {
|
|
4289
4293
|
console.error('Usage: atris member push <name>');
|
|
@@ -4307,6 +4311,9 @@ async function memberPush(name) {
|
|
|
4307
4311
|
const content = fs.readFileSync(memberFile, 'utf8');
|
|
4308
4312
|
const fm = parseFrontmatter(content);
|
|
4309
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();
|
|
4310
4317
|
|
|
4311
4318
|
if (existingAgentId) {
|
|
4312
4319
|
console.log(`Pushing member "${name}" to cloud (updating agent ${existingAgentId})...`);
|
|
@@ -4314,7 +4321,13 @@ async function memberPush(name) {
|
|
|
4314
4321
|
console.log(`Pushing member "${name}" to cloud (creating new agent)...`);
|
|
4315
4322
|
}
|
|
4316
4323
|
|
|
4317
|
-
|
|
4324
|
+
if (!businessId) {
|
|
4325
|
+
console.log('no business binding found; run `atris business init "<name>" --here` to bind this workspace.');
|
|
4326
|
+
}
|
|
4327
|
+
const endpoint = businessId
|
|
4328
|
+
? `/agent/import-member?business_id=${encodeURIComponent(businessId)}`
|
|
4329
|
+
: '/agent/import-member';
|
|
4330
|
+
const result = await apiRequestJson(endpoint, {
|
|
4318
4331
|
method: 'POST',
|
|
4319
4332
|
headers: { 'Content-Type': 'text/markdown' },
|
|
4320
4333
|
body: content,
|
|
@@ -4340,6 +4353,7 @@ async function memberPush(name) {
|
|
|
4340
4353
|
|
|
4341
4354
|
const action = existingAgentId ? 'Updated' : 'Created';
|
|
4342
4355
|
console.log(`${action} successfully. Agent ID: ${agentId}`);
|
|
4356
|
+
if (businessId) console.log(`Bound to business ${businessId}`);
|
|
4343
4357
|
}
|
|
4344
4358
|
|
|
4345
4359
|
// --- PULL subcommand ---
|
|
@@ -7251,52 +7265,218 @@ function workspaceSnapshot(name = null, paths = null) {
|
|
|
7251
7265
|
}
|
|
7252
7266
|
}
|
|
7253
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
|
+
|
|
7254
7472
|
function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeKind(name) } = {}) {
|
|
7255
7473
|
const purpose = missionPurpose(paths);
|
|
7256
7474
|
const steering = readSteeringMemory(paths, name);
|
|
7257
7475
|
const state = loadMemberGoals(name, paths);
|
|
7258
7476
|
const goal = activeGoal(state);
|
|
7259
7477
|
const current = memberOpenExperiment(state);
|
|
7260
|
-
const
|
|
7261
|
-
if (
|
|
7262
|
-
const domainFile = findUnprocessedGeneralistDomainFile(process.cwd());
|
|
7263
|
-
if (domainFile) {
|
|
7264
|
-
return {
|
|
7265
|
-
decision: 'process_domain_file',
|
|
7266
|
-
reason: `generalist_domain_scan:${domainFile.status}`,
|
|
7267
|
-
needs_user: false,
|
|
7268
|
-
ask: null,
|
|
7269
|
-
next_command: `atris member wake ${name} --execute --domain-file ${domainFile.path} --json`,
|
|
7270
|
-
state,
|
|
7271
|
-
goal: goal || null,
|
|
7272
|
-
current_experiment: current || null,
|
|
7273
|
-
checks: {
|
|
7274
|
-
has_member: true,
|
|
7275
|
-
has_mission: true,
|
|
7276
|
-
mission_meaningful: true,
|
|
7277
|
-
has_goal: Boolean(goal),
|
|
7278
|
-
has_open_experiment: Boolean(current),
|
|
7279
|
-
has_unprocessed_domain_file: true,
|
|
7280
|
-
domain_scan_window_minutes: 60,
|
|
7281
|
-
checked_existing_tasks: false,
|
|
7282
|
-
},
|
|
7283
|
-
mission: {
|
|
7284
|
-
north_star: purpose.northStar || null,
|
|
7285
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7286
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7287
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7288
|
-
},
|
|
7289
|
-
steering,
|
|
7290
|
-
evidence: {
|
|
7291
|
-
generalist_domain_scan: domainFile,
|
|
7292
|
-
task_projection: null,
|
|
7293
|
-
nearest_open_loop: null,
|
|
7294
|
-
},
|
|
7295
|
-
workspace: null,
|
|
7296
|
-
domain_file: domainFile,
|
|
7297
|
-
};
|
|
7298
|
-
}
|
|
7299
|
-
}
|
|
7478
|
+
const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
|
|
7479
|
+
if (generalistDecision) return generalistDecision;
|
|
7300
7480
|
const rawDirective = steeringWakeDirective(steering, name, goal);
|
|
7301
7481
|
const directiveClosure = rawDirective ? steeringDirectiveClosure(rawDirective) : null;
|
|
7302
7482
|
const directive = directiveClosure?.all_closed ? null : rawDirective;
|
|
@@ -7339,217 +7519,12 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7339
7519
|
next_command: wakeScores.selected.next_command,
|
|
7340
7520
|
} : null;
|
|
7341
7521
|
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
reason: 'mission_missing_or_placeholder',
|
|
7347
|
-
needs_user: true,
|
|
7348
|
-
ask,
|
|
7349
|
-
next_command: `edit atris/team/${name}/MISSION.md`,
|
|
7350
|
-
state,
|
|
7351
|
-
goal: goal || null,
|
|
7352
|
-
current_experiment: current || null,
|
|
7353
|
-
checks,
|
|
7354
|
-
mission: {
|
|
7355
|
-
north_star: purpose.northStar || null,
|
|
7356
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7357
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7358
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7359
|
-
},
|
|
7360
|
-
steering,
|
|
7361
|
-
evidence,
|
|
7362
|
-
workspace,
|
|
7363
|
-
};
|
|
7364
|
-
}
|
|
7365
|
-
|
|
7366
|
-
const discoveredTask = evidence.problem_discovery?.selected || null;
|
|
7367
|
-
if (discoveredTask?.source === 'log_error_scan' && discoveredTask.autonomous_action === 'create_task') {
|
|
7368
|
-
return {
|
|
7369
|
-
decision: 'create_task',
|
|
7370
|
-
reason: 'autonomous_error_discovery:log_error_scan',
|
|
7371
|
-
needs_user: false,
|
|
7372
|
-
ask: null,
|
|
7373
|
-
next_command: discoveredTask.next_command,
|
|
7374
|
-
state,
|
|
7375
|
-
goal: goal || null,
|
|
7376
|
-
current_experiment: current || null,
|
|
7377
|
-
autonomous_problem: discoveredTask,
|
|
7378
|
-
checks,
|
|
7379
|
-
mission: {
|
|
7380
|
-
north_star: purpose.northStar,
|
|
7381
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7382
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7383
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7384
|
-
},
|
|
7385
|
-
steering,
|
|
7386
|
-
evidence,
|
|
7387
|
-
workspace,
|
|
7388
|
-
};
|
|
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);
|
|
7389
7526
|
}
|
|
7390
|
-
|
|
7391
|
-
if (!goal) {
|
|
7392
|
-
const discoveredProblem = evidence.problem_discovery?.selected || null;
|
|
7393
|
-
if (discoveredProblem) {
|
|
7394
|
-
return {
|
|
7395
|
-
decision: 'set_objective',
|
|
7396
|
-
reason: `autonomous_problem_discovery:${discoveredProblem.source}`,
|
|
7397
|
-
needs_user: false,
|
|
7398
|
-
ask: null,
|
|
7399
|
-
next_command: discoveredProblem.next_command || `atris member wake ${name} --execute --confirm-autonomy-policy`,
|
|
7400
|
-
state,
|
|
7401
|
-
goal: null,
|
|
7402
|
-
current_experiment: null,
|
|
7403
|
-
autonomous_problem: discoveredProblem,
|
|
7404
|
-
checks,
|
|
7405
|
-
mission: {
|
|
7406
|
-
north_star: purpose.northStar,
|
|
7407
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7408
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7409
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7410
|
-
},
|
|
7411
|
-
steering,
|
|
7412
|
-
evidence,
|
|
7413
|
-
workspace,
|
|
7414
|
-
};
|
|
7415
|
-
}
|
|
7416
|
-
return {
|
|
7417
|
-
decision: 'stop',
|
|
7418
|
-
reason: 'no_active_goal',
|
|
7419
|
-
needs_user: false,
|
|
7420
|
-
ask: null,
|
|
7421
|
-
next_command: `atris member goal-from-mission ${name}`,
|
|
7422
|
-
state,
|
|
7423
|
-
goal: null,
|
|
7424
|
-
current_experiment: null,
|
|
7425
|
-
autonomous_problem: null,
|
|
7426
|
-
checks,
|
|
7427
|
-
mission: {
|
|
7428
|
-
north_star: purpose.northStar,
|
|
7429
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7430
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7431
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7432
|
-
},
|
|
7433
|
-
steering,
|
|
7434
|
-
evidence,
|
|
7435
|
-
workspace,
|
|
7436
|
-
};
|
|
7437
|
-
}
|
|
7438
|
-
|
|
7439
|
-
if (blocked) {
|
|
7440
|
-
return {
|
|
7441
|
-
decision: 'ask',
|
|
7442
|
-
reason: 'blocked_experiment',
|
|
7443
|
-
needs_user: true,
|
|
7444
|
-
ask: blocked.block?.ask || 'Needs operator input before another wake.',
|
|
7445
|
-
next_command: `atris member review ${name} ${blocked.id} --discard --proof "..."`,
|
|
7446
|
-
state,
|
|
7447
|
-
goal,
|
|
7448
|
-
current_experiment: blocked,
|
|
7449
|
-
checks,
|
|
7450
|
-
mission: {
|
|
7451
|
-
north_star: purpose.northStar,
|
|
7452
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7453
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7454
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7455
|
-
},
|
|
7456
|
-
steering,
|
|
7457
|
-
evidence,
|
|
7458
|
-
workspace,
|
|
7459
|
-
};
|
|
7460
|
-
}
|
|
7461
|
-
|
|
7462
|
-
if (current) {
|
|
7463
|
-
return {
|
|
7464
|
-
decision: 'wait',
|
|
7465
|
-
reason: `open_experiment_${current.status}`,
|
|
7466
|
-
needs_user: false,
|
|
7467
|
-
ask: null,
|
|
7468
|
-
next_command: `atris member review ${name} ${current.id} --accept --proof "..." --value 4`,
|
|
7469
|
-
state,
|
|
7470
|
-
goal,
|
|
7471
|
-
current_experiment: current,
|
|
7472
|
-
checks,
|
|
7473
|
-
mission: {
|
|
7474
|
-
north_star: purpose.northStar,
|
|
7475
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7476
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7477
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7478
|
-
},
|
|
7479
|
-
steering,
|
|
7480
|
-
evidence,
|
|
7481
|
-
workspace,
|
|
7482
|
-
};
|
|
7483
|
-
}
|
|
7484
|
-
|
|
7485
|
-
if (wakeScores.selected && wakeScores.selected.decision !== 'tick') {
|
|
7486
|
-
const selected = wakeScores.selected;
|
|
7487
|
-
const needsUser = selected.decision === 'ask';
|
|
7488
|
-
return {
|
|
7489
|
-
decision: selected.decision,
|
|
7490
|
-
reason: selected.reason,
|
|
7491
|
-
needs_user: needsUser,
|
|
7492
|
-
ask: needsUser ? (selected.ask || `Need operator input for ${selected.title}.`) : null,
|
|
7493
|
-
next_command: selected.next_command,
|
|
7494
|
-
state,
|
|
7495
|
-
goal,
|
|
7496
|
-
current_experiment: null,
|
|
7497
|
-
checks,
|
|
7498
|
-
mission: {
|
|
7499
|
-
north_star: purpose.northStar,
|
|
7500
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7501
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7502
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7503
|
-
},
|
|
7504
|
-
steering,
|
|
7505
|
-
evidence,
|
|
7506
|
-
workspace,
|
|
7507
|
-
};
|
|
7508
|
-
}
|
|
7509
|
-
|
|
7510
|
-
if (!workspace.clean_for_member && !force) {
|
|
7511
|
-
return {
|
|
7512
|
-
decision: 'wait',
|
|
7513
|
-
reason: 'workspace_dirty_in_member_scope',
|
|
7514
|
-
needs_user: false,
|
|
7515
|
-
ask: null,
|
|
7516
|
-
next_command: `commit/stash files in atris/team/${name}/ (or member scope) — or rerun: atris member wake ${name} --force`,
|
|
7517
|
-
state,
|
|
7518
|
-
goal,
|
|
7519
|
-
current_experiment: null,
|
|
7520
|
-
checks,
|
|
7521
|
-
mission: {
|
|
7522
|
-
north_star: purpose.northStar,
|
|
7523
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7524
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7525
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7526
|
-
},
|
|
7527
|
-
steering,
|
|
7528
|
-
evidence,
|
|
7529
|
-
workspace,
|
|
7530
|
-
};
|
|
7531
|
-
}
|
|
7532
|
-
|
|
7533
|
-
return {
|
|
7534
|
-
decision: 'tick',
|
|
7535
|
-
reason: 'safe_next_bounded_step',
|
|
7536
|
-
needs_user: false,
|
|
7537
|
-
ask: null,
|
|
7538
|
-
next_command: `atris member tick ${name} --goal ${goal.id}`,
|
|
7539
|
-
state,
|
|
7540
|
-
goal,
|
|
7541
|
-
current_experiment: null,
|
|
7542
|
-
checks,
|
|
7543
|
-
mission: {
|
|
7544
|
-
north_star: purpose.northStar,
|
|
7545
|
-
runtime_id: purpose.runtimeMission.id || null,
|
|
7546
|
-
runtime_status: purpose.runtimeMission.status || null,
|
|
7547
|
-
runtime_next: purpose.runtimeMission.next || null,
|
|
7548
|
-
},
|
|
7549
|
-
steering,
|
|
7550
|
-
evidence,
|
|
7551
|
-
workspace,
|
|
7552
|
-
};
|
|
7527
|
+
return null;
|
|
7553
7528
|
}
|
|
7554
7529
|
|
|
7555
7530
|
async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
|
|
@@ -7627,6 +7602,18 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
|
|
|
7627
7602
|
evidence: planned.evidence,
|
|
7628
7603
|
mission: planned.mission,
|
|
7629
7604
|
});
|
|
7605
|
+
// An unreviewable fallback proposal no longer blocks wake (it deadlocked
|
|
7606
|
+
// members for weeks), but it must not accumulate either: one open
|
|
7607
|
+
// proposal per goal stays the invariant, so the stale fallback is
|
|
7608
|
+
// superseded by the one being proposed now.
|
|
7609
|
+
for (const prior of goal.experiments) {
|
|
7610
|
+
if (experimentIsUnreviewable(prior)) {
|
|
7611
|
+
prior.status = 'superseded';
|
|
7612
|
+
prior.superseded_at = now;
|
|
7613
|
+
prior.superseded_by = experiment.id;
|
|
7614
|
+
prior.superseded_reason = 'unreviewable_fallback_replaced_by_next_proposal';
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7630
7617
|
goal.experiments.push(experiment);
|
|
7631
7618
|
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
7632
7619
|
goal.history.push({ at: now, event: 'wake_tick_proposed_experiment', experiment_id: experiment.id });
|
|
@@ -7971,84 +7958,60 @@ function memberAliveAll(...args) {
|
|
|
7971
7958
|
return memberLoopAll('alive', ...args);
|
|
7972
7959
|
}
|
|
7973
7960
|
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
const
|
|
7978
|
-
const
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
if (hasFlag(args, '--uninstall')) {
|
|
7997
|
-
return uninstallMemberAliveCron(name, args);
|
|
7998
|
-
}
|
|
7999
|
-
|
|
8000
|
-
if (status) {
|
|
8001
|
-
const active = readJsonIfExists(paths.lockPath);
|
|
8002
|
-
const latest = readJsonIfExists(paths.latestPath);
|
|
8003
|
-
const payload = {
|
|
8004
|
-
ok: true,
|
|
8005
|
-
action: 'loop_status',
|
|
8006
|
-
member: name,
|
|
8007
|
-
active: Boolean(active && Number(active.expires_at_ms || 0) > Date.now() && isPidAlive(active.pid)),
|
|
8008
|
-
lease: active || null,
|
|
8009
|
-
latest: latest || null,
|
|
8010
|
-
hourly_alive: memberAliveCronStatus(name, paths),
|
|
8011
|
-
lock_path: paths.lockPath,
|
|
8012
|
-
latest_path: paths.latestPath,
|
|
8013
|
-
};
|
|
8014
|
-
printJsonOrText(payload, [
|
|
8015
|
-
`Loop status: ${name}`,
|
|
8016
|
-
`Active: ${payload.active ? 'yes' : 'no'}`,
|
|
8017
|
-
`Hourly: ${payload.hourly_alive.installed ? 'installed' : payload.hourly_alive.script_exists ? 'script ready' : 'not installed'}`,
|
|
8018
|
-
`Latest: ${latest?.receipt_path ? path.relative(process.cwd(), latest.receipt_path) : 'none'}`,
|
|
8019
|
-
], asJson);
|
|
8020
|
-
return;
|
|
8021
|
-
}
|
|
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
|
+
}
|
|
8022
7983
|
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
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
|
+
}
|
|
8051
8012
|
|
|
8013
|
+
// Loop phase: derive the timing plan (tick count, interval, duration, lease ttl) from flags.
|
|
8014
|
+
function memberLoopTimingPlan(name, args, { execute, confirmed }) {
|
|
8052
8015
|
const ticksFlag = readNumberFlag(args, '--ticks', null);
|
|
8053
8016
|
const minutes = readNumberFlag(args, '--minutes', null);
|
|
8054
8017
|
const durationSeconds = readNumberFlag(args, '--duration-seconds', readNumberFlag(args, '--seconds', null));
|
|
@@ -8079,183 +8042,190 @@ async function memberLoop(name, ...args) {
|
|
|
8079
8042
|
forever ? intervalMs + (2 * 60 * 60 * 1000) : durationMs + 60000,
|
|
8080
8043
|
intervalMs + 60000,
|
|
8081
8044
|
);
|
|
8045
|
+
return { ticksFlag, hourly, forever, intervalMs, durationMs, ticks, runId, startedAt, ttlMs };
|
|
8046
|
+
}
|
|
8082
8047
|
|
|
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
|
-
|
|
8109
|
-
|
|
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
|
+
}
|
|
8110
8075
|
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
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');
|
|
8141
8174
|
|
|
8142
|
-
fs.rmSync(paths.stopPath, { force: true });
|
|
8143
|
-
const tickLogPath = path.join(process.cwd(), 'atris', 'runs', `${runId}.jsonl`);
|
|
8144
|
-
fs.mkdirSync(path.dirname(tickLogPath), { recursive: true });
|
|
8145
|
-
const tickResults = [];
|
|
8146
|
-
const decisions = {};
|
|
8147
|
-
let stopped = false;
|
|
8148
|
-
let failed = false;
|
|
8149
|
-
// Stop spinning: if the member can do no real work for this many ticks in a row (waiting on a
|
|
8150
|
-
// human review, blocked, or no goal), break early and surface a clear handoff instead of
|
|
8151
|
-
// burning every remaining tick on the same no-op decision.
|
|
8152
|
-
let earlyExit = null;
|
|
8153
|
-
let consecutiveIdle = 0;
|
|
8154
|
-
const idleBreakThreshold = 2;
|
|
8155
8175
|
// Repeated identical `ask:*` wake decisions (e.g. ask:mission_missing_or_placeholder) mean the
|
|
8156
8176
|
// member is stuck asking the same question every tick regardless of --execute; the idle
|
|
8157
|
-
// early-exit
|
|
8177
|
+
// early-exit only fires in execute mode, so without this a dry-run loop burns every
|
|
8158
8178
|
// remaining tick re-asking. Reuse the same blocked_on_human handoff once we see the ask repeat.
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
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
|
+
}
|
|
8162
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;
|
|
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;
|
|
8163
8213
|
try {
|
|
8164
|
-
for (let index = 0; index < ticks; index += 1) {
|
|
8214
|
+
for (let index = 0; index < plan.ticks; index += 1) {
|
|
8165
8215
|
if (fs.existsSync(paths.stopPath)) {
|
|
8166
|
-
stopped = true;
|
|
8216
|
+
state.stopped = true;
|
|
8167
8217
|
break;
|
|
8168
8218
|
}
|
|
8169
|
-
refreshMemberLoopLease(paths, lease.lease, ttlMs);
|
|
8219
|
+
refreshMemberLoopLease(paths, lease.lease, plan.ttlMs);
|
|
8170
8220
|
const tickStartedAt = stampIso();
|
|
8171
8221
|
try {
|
|
8172
|
-
if (aliveMode) {
|
|
8173
|
-
|
|
8174
|
-
execute,
|
|
8175
|
-
confirmed,
|
|
8176
|
-
force,
|
|
8177
|
-
agent,
|
|
8178
|
-
model,
|
|
8179
|
-
maxWallSeconds: operateMaxWall,
|
|
8180
|
-
autoAcceptLimit,
|
|
8181
|
-
noPrime: index > 0,
|
|
8182
|
-
});
|
|
8183
|
-
const key = `${alive.status || 'alive'}:${alive.reason || 'tick'}`;
|
|
8184
|
-
decisions[key] = (decisions[key] || 0) + 1;
|
|
8185
|
-
const tick = {
|
|
8186
|
-
tick: index + 1,
|
|
8187
|
-
started_at: tickStartedAt,
|
|
8188
|
-
finished_at: alive.finished_at || stampIso(),
|
|
8189
|
-
ok: alive.ok !== false,
|
|
8190
|
-
decision: alive.status || 'alive_tick',
|
|
8191
|
-
reason: alive.reason || null,
|
|
8192
|
-
executed: execute,
|
|
8193
|
-
needs_user: alive.needs_user === true,
|
|
8194
|
-
next_command: alive.next_command || null,
|
|
8195
|
-
has_mission: alive.has_mission === true,
|
|
8196
|
-
has_goal: alive.has_goal === true,
|
|
8197
|
-
has_steering: false,
|
|
8198
|
-
productive: alive.status === 'completed',
|
|
8199
|
-
blocked_on_human: alive.blocked_on_human === true,
|
|
8200
|
-
operate_ok: alive.operate?.ok,
|
|
8201
|
-
auto_accept_accepted: alive.auto_accept?.json?.summary?.accepted ?? alive.auto_accept?.json?.summary?.would_accept,
|
|
8202
|
-
receipt_path: alive.receipt_path || alive.operate?.receipt_path || null,
|
|
8203
|
-
alive,
|
|
8204
|
-
};
|
|
8205
|
-
tickResults.push(tick);
|
|
8206
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8222
|
+
if (options.aliveMode) {
|
|
8223
|
+
memberLoopAliveTick(name, run, index, tickStartedAt);
|
|
8207
8224
|
} else {
|
|
8208
|
-
|
|
8209
|
-
const key = `${wake.decision}:${wake.reason}`;
|
|
8210
|
-
decisions[key] = (decisions[key] || 0) + 1;
|
|
8211
|
-
const tick = {
|
|
8212
|
-
tick: index + 1,
|
|
8213
|
-
started_at: tickStartedAt,
|
|
8214
|
-
finished_at: stampIso(),
|
|
8215
|
-
ok: true,
|
|
8216
|
-
decision: wake.decision,
|
|
8217
|
-
reason: wake.reason,
|
|
8218
|
-
executed: wake.executed,
|
|
8219
|
-
needs_user: wake.needs_user,
|
|
8220
|
-
next_command: wake.next_command,
|
|
8221
|
-
has_mission: wake.checks?.has_mission === true,
|
|
8222
|
-
has_goal: wake.checks?.has_goal === true,
|
|
8223
|
-
has_steering: wake.checks?.has_steering === true,
|
|
8224
|
-
productive: wake.executed === true
|
|
8225
|
-
|| /executed/.test(wake.reason || '')
|
|
8226
|
-
|| ['tick', 'close_loop', 'report_proof', 'create_missing_task', 'create_task', 'set_objective'].includes(wake.decision),
|
|
8227
|
-
blocked_on_human: wake.needs_user === true || /^open_experiment_/.test(wake.reason || ''),
|
|
8228
|
-
current_experiment: wake.current_experiment?.id || null,
|
|
8229
|
-
receipt_path: wake.receipt_path,
|
|
8230
|
-
};
|
|
8231
|
-
tickResults.push(tick);
|
|
8232
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8233
|
-
|
|
8234
|
-
if (wake.decision === 'ask') {
|
|
8235
|
-
if (key === lastAskKey) {
|
|
8236
|
-
consecutiveIdenticalAsk += 1;
|
|
8237
|
-
} else {
|
|
8238
|
-
lastAskKey = key;
|
|
8239
|
-
consecutiveIdenticalAsk = 1;
|
|
8240
|
-
}
|
|
8241
|
-
if (consecutiveIdenticalAsk >= identicalAskBreakThreshold) {
|
|
8242
|
-
earlyExit = {
|
|
8243
|
-
after_tick: tick.tick,
|
|
8244
|
-
decision: tick.decision || null,
|
|
8245
|
-
reason: tick.reason || 'idle',
|
|
8246
|
-
needs_user: tick.needs_user === true,
|
|
8247
|
-
blocked_on_human: true,
|
|
8248
|
-
next_command: tick.next_command || null,
|
|
8249
|
-
stop: 'repeated_identical_ask',
|
|
8250
|
-
};
|
|
8251
|
-
}
|
|
8252
|
-
} else {
|
|
8253
|
-
lastAskKey = null;
|
|
8254
|
-
consecutiveIdenticalAsk = 0;
|
|
8255
|
-
}
|
|
8225
|
+
await memberLoopWakeTick(name, run, index, tickStartedAt);
|
|
8256
8226
|
}
|
|
8257
8227
|
} catch (error) {
|
|
8258
|
-
failed = true;
|
|
8228
|
+
state.failed = true;
|
|
8259
8229
|
const tick = {
|
|
8260
8230
|
tick: index + 1,
|
|
8261
8231
|
started_at: tickStartedAt,
|
|
@@ -8263,19 +8233,19 @@ async function memberLoop(name, ...args) {
|
|
|
8263
8233
|
ok: false,
|
|
8264
8234
|
error: error instanceof Error ? error.message : String(error),
|
|
8265
8235
|
};
|
|
8266
|
-
tickResults.push(tick);
|
|
8267
|
-
fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8236
|
+
state.tickResults.push(tick);
|
|
8237
|
+
fs.appendFileSync(state.tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
|
|
8268
8238
|
break;
|
|
8269
8239
|
}
|
|
8270
|
-
if (earlyExit) break;
|
|
8271
|
-
if (execute) {
|
|
8272
|
-
const last = tickResults[tickResults.length - 1];
|
|
8240
|
+
if (state.earlyExit) break;
|
|
8241
|
+
if (options.execute) {
|
|
8242
|
+
const last = state.tickResults[state.tickResults.length - 1];
|
|
8273
8243
|
if (last && last.productive) {
|
|
8274
|
-
consecutiveIdle = 0;
|
|
8244
|
+
state.consecutiveIdle = 0;
|
|
8275
8245
|
} else if (last) {
|
|
8276
|
-
consecutiveIdle += 1;
|
|
8277
|
-
if (consecutiveIdle >=
|
|
8278
|
-
earlyExit = {
|
|
8246
|
+
state.consecutiveIdle += 1;
|
|
8247
|
+
if (state.consecutiveIdle >= MEMBER_LOOP_IDLE_BREAK_THRESHOLD) {
|
|
8248
|
+
state.earlyExit = {
|
|
8279
8249
|
after_tick: last.tick,
|
|
8280
8250
|
decision: last.decision || null,
|
|
8281
8251
|
reason: last.reason || 'idle',
|
|
@@ -8287,12 +8257,19 @@ async function memberLoop(name, ...args) {
|
|
|
8287
8257
|
}
|
|
8288
8258
|
}
|
|
8289
8259
|
}
|
|
8290
|
-
if (index < ticks - 1 && intervalMs > 0) sleepSync(intervalMs);
|
|
8260
|
+
if (index < plan.ticks - 1 && plan.intervalMs > 0) sleepSync(plan.intervalMs);
|
|
8291
8261
|
}
|
|
8292
8262
|
} finally {
|
|
8293
8263
|
releaseMemberLoopLease(paths, lease.lease);
|
|
8294
8264
|
}
|
|
8265
|
+
}
|
|
8295
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;
|
|
8296
8273
|
const finishedAt = stampIso();
|
|
8297
8274
|
const summary = {
|
|
8298
8275
|
ok: !failed,
|
|
@@ -8334,7 +8311,7 @@ async function memberLoop(name, ...args) {
|
|
|
8334
8311
|
};
|
|
8335
8312
|
const receiptPath = writeMemberLoopReceipt(name, summary);
|
|
8336
8313
|
const payload = { ...summary, receipt_path: receiptPath };
|
|
8337
|
-
|
|
8314
|
+
writeJson(paths.latestPath, payload);
|
|
8338
8315
|
printJsonOrText(payload, [
|
|
8339
8316
|
`${aliveMode ? 'Alive' : 'Loop'}: ${name}`,
|
|
8340
8317
|
`Status: ${payload.status}`,
|
|
@@ -8346,6 +8323,71 @@ async function memberLoop(name, ...args) {
|
|
|
8346
8323
|
], asJson);
|
|
8347
8324
|
}
|
|
8348
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
|
+
|
|
8349
8391
|
async function memberTick(name, ...args) {
|
|
8350
8392
|
const paths = requireMemberDir(name);
|
|
8351
8393
|
const asJson = hasFlag(args, '--json');
|
|
@@ -9069,4 +9111,11 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9069
9111
|
}
|
|
9070
9112
|
}
|
|
9071
9113
|
|
|
9072
|
-
module.exports = {
|
|
9114
|
+
module.exports = {
|
|
9115
|
+
memberCommand,
|
|
9116
|
+
findAllMembers,
|
|
9117
|
+
findWorkspaceBusinessId,
|
|
9118
|
+
parseFrontmatter,
|
|
9119
|
+
wakeBootLines,
|
|
9120
|
+
buildMemberRunStartArgs,
|
|
9121
|
+
};
|