atris 3.74.4 → 3.74.6
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/FOR_AGENTS.md +12 -12
- package/atris/AGENTS.md +7 -7
- package/atris/CLAUDE.md +10 -10
- package/atris/policies/atris-backend.md +1 -1
- package/atris/policies/atris-design.md +71 -71
- package/atris/policies/design-seed.md +28 -28
- package/atris/policies/writing.md +1 -1
- package/atris/skills/apps/SKILL.md +3 -3
- package/atris/skills/atris-feedback/SKILL.md +2 -2
- package/atris/skills/autopilot/SKILL.md +10 -10
- package/atris/skills/blocks/SKILL.md +16 -16
- package/atris/skills/calendar/SKILL.md +1 -1
- package/atris/skills/clawhub/atris/SKILL.md +4 -4
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
- package/atris/skills/create-app/SKILL.md +18 -18
- package/atris/skills/create-member/SKILL.md +11 -11
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/drive/SKILL.md +25 -25
- package/atris/skills/email-agent/SKILL.md +11 -11
- package/atris/skills/endgame/SKILL.md +33 -33
- package/atris/skills/magic-inbox/SKILL.md +15 -15
- package/atris/skills/notion/SKILL.md +1 -1
- package/atris/skills/ramp/SKILL.md +7 -7
- package/atris/skills/research-search/SKILL.md +7 -7
- package/atris/skills/slides/SKILL.md +7 -7
- package/atris/skills/tidy/SKILL.md +11 -11
- package/atris/skills/wake/SKILL.md +11 -11
- package/atris/skills/writing/SKILL.md +5 -5
- package/atris/team/_template/SOUL.md +3 -3
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/launcher/MEMBER.md +2 -2
- package/atris/team/mission-lead/MISSION.md +7 -7
- package/atris/team/opus-overnight/MISSION.md +16 -16
- package/atris/team/opus-overnight/STEERING.md +5 -5
- package/atris.md +9 -0
- package/bin/atris.js +16 -3
- package/commands/campaign.js +17 -2
- package/commands/card.js +19 -12
- package/commands/engine.js +92 -17
- package/commands/gm.js +30 -0
- package/commands/idea.js +263 -0
- package/commands/init.js +11 -2
- package/commands/land.js +39 -18
- package/commands/mission.js +4 -3
- package/commands/ninety-nine.js +288 -0
- package/commands/one-lap.js +46 -27
- package/commands/reel.js +12 -7
- package/commands/sync.js +10 -2
- package/commands/task.js +4 -2
- package/commands/xp.js +68 -15
- package/commands/zen.js +10 -3
- package/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- package/lib/career-xp-lock.js +61 -0
- package/lib/career-xp-root.js +61 -0
- package/lib/claude-boot-block.js +7 -4
- package/lib/engine-registry.js +139 -1
- package/lib/fleet.js +11 -2
- package/lib/gm-bet.js +734 -42
- package/lib/gm-card.js +431 -0
- package/lib/gm-phone-page.js +486 -717
- package/lib/gm-phone.js +33 -34
- package/lib/gm-season.js +14 -0
- package/lib/headless-chrome.js +39 -0
- package/lib/idea-gate.js +33 -0
- package/lib/idea-inbox.js +220 -0
- package/lib/idea-ledger.js +196 -0
- package/lib/idea-tick.js +384 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +1 -0
- package/lib/ninety-nine-sync.js +156 -0
- package/lib/ninety-nine.js +247 -0
- package/lib/opening-lines.js +94 -0
- package/lib/roster-markdown.js +39 -1
- package/lib/roster-models.js +1 -1
- package/lib/roster-swap.js +353 -0
- package/lib/spawn-env.js +29 -2
- package/lib/task-db.js +8 -4
- package/lib/wish-audit.js +14 -0
- package/lib/yes-lock.js +365 -256
- package/lib/zen-apply.js +6 -3
- package/lib/zen.js +28 -1
- package/package.json +1 -1
- package/templates/business-starter/CLAUDE.md +7 -7
- package/templates/business-starter/MAP.md +1 -1
- package/templates/business-starter/TODO.md +1 -1
- package/templates/business-starter/atris.md +2 -2
- package/templates/business-starter/context/README.md +6 -6
- package/templates/business-starter/context/live-workspace.md +1 -1
- package/templates/business-starter/goals.md +1 -1
- package/templates/business-starter/instructions.md +1 -1
- package/templates/business-starter/memory.md +4 -4
- package/templates/business-starter/persona.md +3 -3
- package/templates/business-starter/policies/LESSONS.md +1 -1
- package/templates/business-starter/reports/README.md +1 -1
- package/templates/business-starter/skills/README.md +1 -1
- package/templates/business-starter/team/README.md +1 -1
- package/templates/business-starter/team/START_HERE.md +1 -1
- package/templates/business-starter/team/_template/SOUL.md +3 -3
- package/templates/business-starter/team/comms/SOUL.md +2 -2
- package/templates/business-starter/team/ops/SOUL.md +4 -4
- package/templates/business-starter/team/research/SOUL.md +3 -3
- package/templates/business-starter/wiki/index.md +1 -1
- package/templates/research-canonical/CLAUDE.md +7 -7
- package/templates/research-canonical/MAP.md +1 -1
- package/templates/research-canonical/TODO.md +1 -1
- package/templates/research-canonical/atris.md +2 -2
- package/templates/research-canonical/context/README.md +8 -8
- package/templates/research-canonical/context/live-workspace.md +1 -1
- package/templates/research-canonical/goals.md +2 -2
- package/templates/research-canonical/instructions.md +1 -1
- package/templates/research-canonical/memory.md +4 -4
- package/templates/research-canonical/persona.md +1 -1
- package/templates/research-canonical/policies/LESSONS.md +1 -1
- package/templates/research-canonical/reports/README.md +1 -1
- package/templates/research-canonical/skills/README.md +1 -1
- package/templates/research-canonical/team/README.md +1 -1
- package/templates/research-canonical/team/eval/SOUL.md +1 -1
- package/templates/research-canonical/team/experiment/SOUL.md +2 -2
- package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
- package/templates/research-canonical/team/literature/SOUL.md +2 -2
- package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
- package/templates/research-canonical/wiki/index.md +1 -1
- package/utils/auth.js +1 -0
package/commands/xp.js
CHANGED
|
@@ -7,6 +7,9 @@ const crypto = require('crypto');
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
|
|
9
9
|
const { treeHashFor } = require('../lib/tree-hash');
|
|
10
|
+
const { careerXpRoot, careerXpLinkedRoots } = require('../lib/career-xp-root');
|
|
11
|
+
const { withCareerXpLock } = require('../lib/career-xp-lock');
|
|
12
|
+
const { normalizeActor, taskBuilder } = require('../lib/review-integrity');
|
|
10
13
|
|
|
11
14
|
const DEFAULT_GRAPH_DAYS = 365;
|
|
12
15
|
const MAX_SYNC_GRAPH_DAYS = 370;
|
|
@@ -22,15 +25,15 @@ const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json
|
|
|
22
25
|
const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
23
26
|
|
|
24
27
|
// Default single-workspace XP root. Local XP reads task_episodes.jsonl (written
|
|
25
|
-
// by task-db at the
|
|
26
|
-
// it, so
|
|
28
|
+
// by task-db at the main checkout's workspace root) and writes its projection
|
|
29
|
+
// beside it, so reads must resolve the same shared root, otherwise
|
|
27
30
|
// `xp --local` from a subdir (e.g. backend/) read the wrong/empty episodes and
|
|
28
|
-
// split the projection into a nested .atris. Explicit --workspace
|
|
29
|
-
//
|
|
31
|
+
// split the projection into a nested .atris. Explicit --workspace selects the
|
|
32
|
+
// repository and nested workspace, then also resolves its shared ledger. Falls
|
|
30
33
|
// back to raw cwd if the resolver can't load.
|
|
31
34
|
function defaultXpWorkspace() {
|
|
32
35
|
try {
|
|
33
|
-
return require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
|
|
36
|
+
return careerXpRoot(require('../lib/mission-root').resolveWorkspaceRoot(process.cwd()));
|
|
34
37
|
} catch {
|
|
35
38
|
return process.cwd();
|
|
36
39
|
}
|
|
@@ -921,7 +924,7 @@ function receiptFromTaskEpisode(episode, treeHash) {
|
|
|
921
924
|
);
|
|
922
925
|
const proof = String(episode?.proof || '').trim();
|
|
923
926
|
const reward = asNumber(episode?.career_xp?.reward ?? episode?.reward?.value, 0);
|
|
924
|
-
if (!episodeId || (!rejected && (!eligible || !proof || reward <= 0))) return null;
|
|
927
|
+
if (!episodeId || (!rejected && (!eligible || !proof || reward <= 0 || !hasIndependentXpReview(episode)))) return null;
|
|
925
928
|
|
|
926
929
|
return {
|
|
927
930
|
schema: 'atris.career_xp_receipt.v1',
|
|
@@ -1047,8 +1050,55 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
|
|
|
1047
1050
|
};
|
|
1048
1051
|
}
|
|
1049
1052
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1053
|
+
// The eligible flag records acceptance intent, not reviewer independence.
|
|
1054
|
+
// Apply the same actor evidence to new episodes and historical recovery.
|
|
1055
|
+
function hasIndependentXpReview(episode) {
|
|
1056
|
+
const builder = taskBuilder(episode?.state);
|
|
1057
|
+
if (!builder) return false;
|
|
1058
|
+
const reviewer = normalizeActor(episode.action?.actor);
|
|
1059
|
+
if (reviewer && reviewer !== builder) return true;
|
|
1060
|
+
const metadata = episode.state.metadata || {};
|
|
1061
|
+
const verifier = normalizeActor(metadata.certified_verified_by);
|
|
1062
|
+
const reviewActors = Array.isArray(metadata.agent_review_actors) ? metadata.agent_review_actors.map(normalizeActor) : [];
|
|
1063
|
+
return metadata.agent_certified === true && metadata.machine_verified === true
|
|
1064
|
+
&& Boolean(verifier) && verifier !== builder
|
|
1065
|
+
&& verifier === normalizeActor(metadata.independent_review_by)
|
|
1066
|
+
&& reviewActors.includes(verifier);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function readLinkedXpFile(filePath, options = {}) {
|
|
1070
|
+
try {
|
|
1071
|
+
return readJsonl(filePath, options);
|
|
1072
|
+
} catch {
|
|
1073
|
+
console.error(`warning: skipped unreadable linked xp file at ${filePath}`);
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Historical linked ledgers predate the shared writer. Require explicit
|
|
1079
|
+
// eligibility here; receipt conversion also enforces independent review.
|
|
1080
|
+
function recoverLinkedTaskEpisodes(workspace) {
|
|
1081
|
+
return careerXpLinkedRoots(workspace).flatMap(root => readLinkedXpFile(path.join(root, TASK_EPISODES_FILE), { allowPartialTail: true }))
|
|
1082
|
+
.filter(episode => episode?.state?.status === 'done' && episode?.career_xp?.eligible === true
|
|
1083
|
+
&& String(episode.proof || '').trim()
|
|
1084
|
+
&& asNumber(episode.career_xp.reward ?? episode.reward?.value) > 0
|
|
1085
|
+
&& hasIndependentXpReview(episode));
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function recoverLinkedReceipts(workspace) {
|
|
1089
|
+
return careerXpLinkedRoots(workspace).flatMap(root => {
|
|
1090
|
+
const chain = normalizeReceiptChain(readLinkedXpFile(path.join(root, CAREER_XP_RECEIPTS_FILE)), { allowLegacyUpgrade: false });
|
|
1091
|
+
return chain.integrity.status === 'verified' ? chain.receipts : [];
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function collectLocalXpProjectionState(args = [], { write = true, resolveSharedRoot = true } = {}) {
|
|
1096
|
+
const selected = path.resolve(readInlineFirstFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1097
|
+
const workspace = resolveSharedRoot ? careerXpRoot(selected) : selected;
|
|
1098
|
+
return withCareerXpLock(workspace, () => collectLockedXpProjectionState(workspace, { write, resolveSharedRoot }));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function collectLockedXpProjectionState(workspace, { write, resolveSharedRoot }) {
|
|
1052
1102
|
const episodePath = path.join(workspace, TASK_EPISODES_FILE);
|
|
1053
1103
|
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
1054
1104
|
const projectionPath = path.join(workspace, CAREER_XP_PROJECTION_FILE);
|
|
@@ -1075,12 +1125,13 @@ function collectLocalXpProjectionState(args = [], { write = true } = {}) {
|
|
|
1075
1125
|
&& fileSize(episodePath) > 0
|
|
1076
1126
|
&& fileSize(receiptsPath) === 0;
|
|
1077
1127
|
const tail = readTaskEpisodeTail(episodePath, cursorPath, { forceReset: replayFromStart });
|
|
1078
|
-
const
|
|
1128
|
+
const recovered = resolveSharedRoot ? recoverLinkedTaskEpisodes(workspace) : [];
|
|
1129
|
+
const episodes = [...tail.episodes, ...recovered];
|
|
1130
|
+
const treeHash = write && episodes.length ? treeHashFor(workspace) : null;
|
|
1079
1131
|
const seen = new Set(existingChain.receipts.map(receipt => receipt.receipt_id).filter(Boolean));
|
|
1080
1132
|
let previousHash = existingChain.integrity.head_hash || null;
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
.filter(Boolean)
|
|
1133
|
+
const recoveredReceipts = resolveSharedRoot ? recoverLinkedReceipts(workspace) : [];
|
|
1134
|
+
const newReceipts = [...recoveredReceipts, ...episodes.map(episode => receiptFromTaskEpisode(episode, treeHash)).filter(Boolean)]
|
|
1084
1135
|
.filter((receipt) => {
|
|
1085
1136
|
if (seen.has(receipt.receipt_id)) return false;
|
|
1086
1137
|
seen.add(receipt.receipt_id);
|
|
@@ -1381,12 +1432,14 @@ function collectAllLocalXpProjection(args = []) {
|
|
|
1381
1432
|
const searchRoots = defaultAllSearchRoots(args).filter(root => fs.existsSync(root));
|
|
1382
1433
|
const explicitWorkspaces = readFlagValues(args, '--workspace');
|
|
1383
1434
|
const discovered = searchRoots.flatMap(root => discoverCareerXpWorkspaces(root));
|
|
1384
|
-
|
|
1435
|
+
// Local recovery folds old checkout episodes into the durable main ledger.
|
|
1436
|
+
// Canonicalize before aggregation so copies can never earn twice.
|
|
1437
|
+
const workspaces = uniquePaths([...explicitWorkspaces, ...discovered].map(careerXpRoot));
|
|
1385
1438
|
const projections = workspaces.map((workspace) => {
|
|
1386
1439
|
try {
|
|
1387
1440
|
return {
|
|
1388
1441
|
workspace_root: workspace,
|
|
1389
|
-
projection:
|
|
1442
|
+
projection: collectLocalXpProjectionState(['--workspace', workspace]).projection,
|
|
1390
1443
|
};
|
|
1391
1444
|
} catch (error) {
|
|
1392
1445
|
return {
|
|
@@ -1641,7 +1694,7 @@ LIMIT 25
|
|
|
1641
1694
|
}
|
|
1642
1695
|
|
|
1643
1696
|
function buildCareerXpSessionCapsule(args = []) {
|
|
1644
|
-
const workspace = path.resolve(readInlineFirstFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1697
|
+
const workspace = careerXpRoot(path.resolve(readInlineFirstFlag(args, '--workspace', defaultXpWorkspace())));
|
|
1645
1698
|
const sinceInput = readInlineFirstFlag(args, '--since', 'today');
|
|
1646
1699
|
const untilInput = readInlineFirstFlag(args, '--until', null);
|
|
1647
1700
|
const since = parseSessionBoundary(sinceInput, startOfToday());
|
package/commands/zen.js
CHANGED
|
@@ -57,9 +57,16 @@ function makeOut(calm) {
|
|
|
57
57
|
return { dim, line, rake };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// Name the three biggest places so a large count reads as "mostly agent
|
|
61
|
+
// notes in atris/" rather than an unexplained total.
|
|
62
|
+
function teamLine(n, places = []) {
|
|
61
63
|
if (!n) return 'no unsaved team notes.';
|
|
62
|
-
|
|
64
|
+
const head = n === 1 ? '1 unsaved team note left in place' : `${n} unsaved team notes left in place`;
|
|
65
|
+
if (!places.length) return `${head}.`;
|
|
66
|
+
const named = places.slice(0, 3).map((p) => `${p.place} ${p.count}`);
|
|
67
|
+
const rest = places.length - named.length;
|
|
68
|
+
if (rest) named.push(`and ${rest} more ${rest === 1 ? 'place' : 'places'}`);
|
|
69
|
+
return `${head}: ${named.join(', ')}.`;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
function printYours(yours, line, dim) {
|
|
@@ -141,7 +148,7 @@ function printApply(result, out) {
|
|
|
141
148
|
line(' almost zen. still out of place:');
|
|
142
149
|
for (const text of result.remaining) line(dim(` - ${text}`));
|
|
143
150
|
}
|
|
144
|
-
line(dim(` ${teamLine(result.team_files)}`));
|
|
151
|
+
line(dim(` ${teamLine(result.team_files, result.team_places)}`));
|
|
145
152
|
if (result.saved.length) {
|
|
146
153
|
line(dim(' nothing was thrown away. see what zen saved: git for-each-ref refs/archive refs/heads/zen-backup'));
|
|
147
154
|
}
|
package/lib/campaign-keep.js
CHANGED
|
@@ -20,15 +20,30 @@ function shellQuote(text) {
|
|
|
20
20
|
return `'${String(text).replace(/'/g, `'\\''`)}'`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// The job a timer runs. Campaign wake is the default; `atris idea on` passes
|
|
24
|
+
// its own (idea tick every ten minutes).
|
|
25
|
+
const CAMPAIGN_WAKE_JOB = Object.freeze({
|
|
26
|
+
name: 'campaign-wake',
|
|
27
|
+
args: ['campaign', 'wake', '--json'],
|
|
28
|
+
every_seconds: 60,
|
|
29
|
+
marker: MARKER,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function cronSchedule(seconds) {
|
|
33
|
+
const minutes = Math.max(1, Math.round(Number(seconds || 60) / 60));
|
|
34
|
+
return minutes === 1 ? '* * * * *' : `*/${minutes} * * * *`;
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
// One timer per checkout: the label and marker carry a short hash of its root.
|
|
24
|
-
function planKeep({ root, home, platform = process.platform, homeDir = os.homedir(), nodeBin = 'node', atrisBin, envPath = process.env.PATH || '' }) {
|
|
38
|
+
function planKeep({ root, home, platform = process.platform, homeDir = os.homedir(), nodeBin = 'node', atrisBin, envPath = process.env.PATH || '', job = CAMPAIGN_WAKE_JOB }) {
|
|
25
39
|
const slot = crypto.createHash('sha256').update(root).digest('hex').slice(0, 8);
|
|
40
|
+
const every = Number(job.every_seconds) || 60;
|
|
26
41
|
// node by name on the saved PATH, so upgrading node does not break the timer.
|
|
27
|
-
const command = ['/usr/bin/env', nodeBin, atrisBin,
|
|
28
|
-
const logPath = path.join(home,
|
|
29
|
-
const base = { root, slot, log_path: logPath, command: command.join(' '), every_seconds:
|
|
42
|
+
const command = ['/usr/bin/env', nodeBin, atrisBin, ...job.args];
|
|
43
|
+
const logPath = path.join(home, job.log || `${job.name}.log`);
|
|
44
|
+
const base = { root, slot, log_path: logPath, command: command.join(' '), every_seconds: every };
|
|
30
45
|
if (platform === 'darwin') {
|
|
31
|
-
const label = `ai.atris.
|
|
46
|
+
const label = `ai.atris.${job.name}.${slot}`;
|
|
32
47
|
const file = path.join(homeDir, 'Library', 'LaunchAgents', `${label}.plist`);
|
|
33
48
|
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
34
49
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -40,7 +55,7 @@ function planKeep({ root, home, platform = process.platform, homeDir = os.homedi
|
|
|
40
55
|
<key>WorkingDirectory</key><string>${xml(root)}</string>
|
|
41
56
|
<key>EnvironmentVariables</key>
|
|
42
57
|
<dict><key>PATH</key><string>${xml(envPath)}</string><key>ATRIS_SKIP_UPDATE_CHECK</key><string>1</string></dict>
|
|
43
|
-
<key>StartInterval</key><integer
|
|
58
|
+
<key>StartInterval</key><integer>${every}</integer>
|
|
44
59
|
<key>StandardOutPath</key><string>${xml(logPath)}</string>
|
|
45
60
|
<key>StandardErrorPath</key><string>${xml(logPath)}</string>
|
|
46
61
|
</dict>
|
|
@@ -48,8 +63,8 @@ function planKeep({ root, home, platform = process.platform, homeDir = os.homedi
|
|
|
48
63
|
`;
|
|
49
64
|
return { ...base, kind: 'launchd', label, file, contents };
|
|
50
65
|
}
|
|
51
|
-
const marker = `${MARKER}_${slot}`;
|
|
52
|
-
const line =
|
|
66
|
+
const marker = `${job.marker || MARKER}_${slot}`;
|
|
67
|
+
const line = `${cronSchedule(every)} cd ${shellQuote(root)} && PATH=${shellQuote(envPath)} ATRIS_SKIP_UPDATE_CHECK=1 ${command.map(shellQuote).join(' ')} >> ${shellQuote(logPath)} 2>&1 # ${marker}`;
|
|
53
68
|
return { ...base, kind: 'cron', marker, crontab_line: line };
|
|
54
69
|
}
|
|
55
70
|
|
package/lib/campaign-loop.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const fs = require('fs');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const crypto = require('crypto');
|
|
10
11
|
const { spawn } = require('child_process');
|
|
@@ -37,6 +38,18 @@ function proposalId(objective) {
|
|
|
37
38
|
return `m-${slug || 'mission'}-${crypto.randomBytes(3).toString('hex')}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function briefPath(root, campaignId) {
|
|
42
|
+
return path.join(root, 'atris', 'campaigns', campaignId, 'BRIEF.md');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readBrief(root, campaignId) {
|
|
46
|
+
try {
|
|
47
|
+
return fs.readFileSync(briefPath(root, campaignId), 'utf8').trim();
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function rankCampaignMembers(view, rsiSummary = null, events = null) {
|
|
41
54
|
const seats = Array.isArray(view && view.seats) ? view.seats.map((s) => s.member).filter(Boolean) : [];
|
|
42
55
|
const candidates = [...new Set([view && view.owner, ...seats].filter(Boolean))];
|
|
@@ -99,7 +112,7 @@ function completionOrderedMissions(view, events) {
|
|
|
99
112
|
|
|
100
113
|
// What the owner's engine reads before proposing: the goal, how it is judged,
|
|
101
114
|
// what has been tried, and how it went.
|
|
102
|
-
function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummary = null, events = null) {
|
|
115
|
+
function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummary = null, events = null, brief = '') {
|
|
103
116
|
const tried = view.missions.slice(-12).map((m) => `- ${m.id}: ${m.status}${m.outcome ? ` (${m.outcome})` : ''}${m.cost_usd !== null ? `, $${m.cost_usd}` : ''}${typeof m.reward === 'number' ? `, reward ${m.reward >= 0 ? '+' : ''}${m.reward}` : ''}`).join('\n') || '- none yet';
|
|
104
117
|
const ranked = rankCampaignMembers(view, rsiSummary, events);
|
|
105
118
|
const memberLine = ranked.map((r) => {
|
|
@@ -113,6 +126,7 @@ function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummar
|
|
|
113
126
|
`You plan the next missions for an Atris campaign. Answer with JSON only.`,
|
|
114
127
|
``,
|
|
115
128
|
`Campaign: ${view.title}`,
|
|
129
|
+
brief ? `Brief:\n${brief}` : '',
|
|
116
130
|
`Judge (the campaign is done when this command exits 0): ${view.judge}`,
|
|
117
131
|
`Last judge reading: ${view.last_judge ? `${view.last_judge.passed ? 'passed' : 'failed'}${typeof view.last_judge.reading === 'number' ? ` (${view.last_judge.reading} of 1)` : ''}: ${view.answer.text || ''}` : 'never run'}`,
|
|
118
132
|
judgeOutput ? `Last judge output:\n\`\`\`\n${judgeOutput.replace(/```/g, "'''")}\n\`\`\`` : '',
|
|
@@ -231,7 +245,8 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
231
245
|
});
|
|
232
246
|
rsiSummary = summarizeStoreRsi(store);
|
|
233
247
|
} catch {}
|
|
234
|
-
const
|
|
248
|
+
const brief = (deps.brief ? deps.brief(id) : readBrief(root, id)).trim();
|
|
249
|
+
const prompt = proposalPrompt(current, count, latestJudgeOutput(events), deps.lessons ? deps.lessons(id) : '', rsiSummary, events, brief);
|
|
235
250
|
const answer = await ask(root, current, prompt);
|
|
236
251
|
const proposals = parseProposals(answer.text, current, count, rsiSummary, events);
|
|
237
252
|
// Planning costs money too. It rides on the first mission it produced as a
|
|
@@ -242,8 +257,10 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
242
257
|
if (typeof answer.cost_usd === 'number') plan.cost_usd = answer.cost_usd;
|
|
243
258
|
proposals.forEach((proposal, index) => {
|
|
244
259
|
const missionId = proposalId(proposal.objective);
|
|
260
|
+
const key = claimKey(proposal.objective);
|
|
261
|
+
if (brief && !proposal.objective.includes(brief)) proposal.objective = `${proposal.objective}\n\n${brief}`;
|
|
245
262
|
append(id, 'mission_proposed', { mission_id: missionId, member: proposal.member, objective: proposal.objective, verify: proposal.verify, estimate_usd: proposal.estimate_usd || undefined }, proposal.member);
|
|
246
|
-
append(id, 'mission_claimed', { mission_id: missionId, claim_key:
|
|
263
|
+
append(id, 'mission_claimed', { mission_id: missionId, claim_key: key }, proposal.member);
|
|
247
264
|
if (index === 0) append(id, 'step', { mission_id: missionId, ...plan }, current.owner);
|
|
248
265
|
});
|
|
249
266
|
return proposals;
|
|
@@ -264,8 +281,10 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
264
281
|
async function startAndRun(id, work) {
|
|
265
282
|
const current = view(id);
|
|
266
283
|
const reserve = work.estimate_usd || defaultReserve(current);
|
|
284
|
+
const brief = (deps.brief ? deps.brief(id) : readBrief(root, id)).trim();
|
|
285
|
+
const objective = brief && !work.objective.includes(brief) ? `${work.objective}\n\n${brief}` : work.objective;
|
|
267
286
|
const started = await cli([
|
|
268
|
-
'mission', 'start',
|
|
287
|
+
'mission', 'start', objective, '--mission-id', work.mission_id, '--owner', work.member,
|
|
269
288
|
'--runner', runner, '--verify', work.verify || current.judge, '--campaign', id, '--reserve', String(reserve),
|
|
270
289
|
'--max-wall', String(missionMinutes * 60), '--json',
|
|
271
290
|
]);
|
|
@@ -419,4 +438,4 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
419
438
|
return { step, tick, run, propose };
|
|
420
439
|
}
|
|
421
440
|
|
|
422
|
-
module.exports = { ACTIVITY_LINE, askOwner, makeLoop, parseProposals, proposalPrompt, rankCampaignMembers, readingKey, runCli };
|
|
441
|
+
module.exports = { ACTIVITY_LINE, askOwner, briefPath, makeLoop, parseProposals, proposalPrompt, rankCampaignMembers, readBrief, readingKey, runCli };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { sleepSync } = require('./sleep-sync');
|
|
7
|
+
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return error.code === 'EPERM';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Callers resolve the shared root before entering. Do not acquire recursively.
|
|
19
|
+
function withCareerXpLock(workspace, action, { timeoutMs = 10000 } = {}) {
|
|
20
|
+
const file = path.join(workspace, '.atris', 'state', 'career_xp.lock');
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
const holder = JSON.stringify({ pid: process.pid, host: os.hostname() });
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
for (;;) {
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = fs.openSync(file, 'wx');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code !== 'EEXIST') throw error;
|
|
30
|
+
}
|
|
31
|
+
if (fd !== undefined) {
|
|
32
|
+
try {
|
|
33
|
+
fs.writeFileSync(fd, holder);
|
|
34
|
+
return action();
|
|
35
|
+
} finally {
|
|
36
|
+
fs.closeSync(fd);
|
|
37
|
+
fs.unlinkSync(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const before = fs.statSync(file);
|
|
42
|
+
let owner;
|
|
43
|
+
try { owner = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
44
|
+
// A newly created file may still be receiving its owner record.
|
|
45
|
+
const stale = owner ? owner.host === os.hostname() && !alive(owner.pid)
|
|
46
|
+
: Date.now() - before.mtimeMs > 30000;
|
|
47
|
+
if (stale) {
|
|
48
|
+
const after = fs.statSync(file);
|
|
49
|
+
if (before.ino === after.ino && before.mtimeMs === after.mtimeMs) fs.unlinkSync(file);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT') throw error;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (Date.now() >= deadline) throw new Error('xp ledger is busy; retry');
|
|
57
|
+
sleepSync(20);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { withCareerXpLock };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
// Jobs keep their own checkout for verification. Their durable XP belongs in
|
|
8
|
+
// the main checkout, where removing a landed job's checkout cannot erase it.
|
|
9
|
+
// Preserve a nested workspace's relative path so customer workspaces stay apart.
|
|
10
|
+
function careerXpRoot(workspace) {
|
|
11
|
+
const root = path.resolve(workspace);
|
|
12
|
+
try {
|
|
13
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
14
|
+
cwd: root, encoding: 'utf8', timeout: 5000,
|
|
15
|
+
});
|
|
16
|
+
if (top.status !== 0 || !top.stdout.trim()) return root;
|
|
17
|
+
const listing = spawnSync('git', ['worktree', 'list', '--porcelain', '-z'], {
|
|
18
|
+
cwd: root, encoding: 'utf8', timeout: 5000,
|
|
19
|
+
});
|
|
20
|
+
const first = String(listing.stdout || '').split('\0')[0];
|
|
21
|
+
if (listing.status !== 0 || !first.startsWith('worktree ')) return root;
|
|
22
|
+
const main = first.slice('worktree '.length);
|
|
23
|
+
// Bare repositories and separate git storage may name the storage folder,
|
|
24
|
+
// not a checkout. Never create an XP ledger inside git's internal files.
|
|
25
|
+
if (!fs.existsSync(path.join(main, '.git'))) return root;
|
|
26
|
+
const relative = path.relative(top.stdout.trim(), root);
|
|
27
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return root;
|
|
28
|
+
return path.resolve(main, relative);
|
|
29
|
+
} catch {
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Enumerate only checkouts registered to this repository, at the same nested
|
|
35
|
+
// workspace offset. Never crawl unrelated repositories or customer workspaces.
|
|
36
|
+
function careerXpLinkedRoots(workspace) {
|
|
37
|
+
const root = path.resolve(workspace);
|
|
38
|
+
try {
|
|
39
|
+
// A nested workspace may exist only in a linked checkout so far.
|
|
40
|
+
let existing = root;
|
|
41
|
+
while (!fs.existsSync(existing) && path.dirname(existing) !== existing) existing = path.dirname(existing);
|
|
42
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
43
|
+
cwd: existing, encoding: 'utf8', timeout: 5000,
|
|
44
|
+
});
|
|
45
|
+
if (top.status !== 0 || !top.stdout.trim()) return [];
|
|
46
|
+
const relative = path.relative(top.stdout.trim(), root);
|
|
47
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return [];
|
|
48
|
+
const listing = spawnSync('git', ['worktree', 'list', '--porcelain', '-z'], {
|
|
49
|
+
cwd: existing, encoding: 'utf8', timeout: 5000,
|
|
50
|
+
});
|
|
51
|
+
if (listing.status !== 0) return [];
|
|
52
|
+
return String(listing.stdout || '').split('\0')
|
|
53
|
+
.filter(field => field.startsWith('worktree '))
|
|
54
|
+
.map(field => path.resolve(field.slice('worktree '.length), relative))
|
|
55
|
+
.filter(candidate => candidate !== root);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { careerXpRoot, careerXpLinkedRoots };
|
package/lib/claude-boot-block.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { openingLinesForRoot, renderOpeningLinesBlock } = require('./opening-lines');
|
|
4
|
+
|
|
3
5
|
const ATRIS_START_MARKER = '<!-- ATRIS:START - Auto-generated, do not edit -->';
|
|
4
6
|
const ATRIS_END_MARKER = '<!-- ATRIS:END -->';
|
|
5
7
|
|
|
6
|
-
function renderAtrisClaudeBootBlock() {
|
|
8
|
+
function renderAtrisClaudeBootBlock(openingLines = openingLinesForRoot()) {
|
|
9
|
+
const opening = renderOpeningLinesBlock(openingLines, 'CLAUDE.md');
|
|
7
10
|
return `${ATRIS_START_MARKER}
|
|
8
11
|
## Atris System
|
|
9
12
|
|
|
@@ -14,12 +17,12 @@ You are now running inside Atris, an AI workspace operating system.
|
|
|
14
17
|
After displaying the boot output, respond to the user naturally.
|
|
15
18
|
|
|
16
19
|
The person does not know atris words. Translate their plain English; when unsure run \`atris guide "<their words>"\`.
|
|
17
|
-
${ATRIS_END_MARKER}
|
|
20
|
+
${opening ? `\n${opening}\n` : ''}${ATRIS_END_MARKER}
|
|
18
21
|
`;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function upsertAtrisClaudeBootBlock(source = '') {
|
|
22
|
-
const atrisBlock = renderAtrisClaudeBootBlock();
|
|
24
|
+
function upsertAtrisClaudeBootBlock(source = '', options = {}) {
|
|
25
|
+
const atrisBlock = renderAtrisClaudeBootBlock(options.openingLines);
|
|
23
26
|
const startIdx = source.indexOf('<!-- ATRIS:START');
|
|
24
27
|
const prependBlock = (preserved) => {
|
|
25
28
|
const remainder = preserved.replace(/^\n+/, '');
|