atris 3.52.0 → 3.55.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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +8 -7
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +89 -40
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/ax +592 -91
- package/bin/atris +21 -0
- package/bin/atris.js +580 -378
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/computer.js +2 -2
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +399 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +339 -74
- package/commands/now.js +25 -6
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +96 -40
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +418 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -33
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +10 -2
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-ledger-compact.js +127 -0
- package/lib/mission-root.js +4 -2
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/noninteractive.js +88 -0
- package/lib/permission-grants.js +10 -1
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/runner-command.js +5 -4
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +22 -2
- package/lib/task-proof.js +75 -9
- package/lib/task-receipt.js +5 -1
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +4 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/scripts/outbound-artifact-gate.js +227 -0
- package/utils/update-check.js +15 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const MEMBER_DIRS = Object.freeze(['skills', 'tools', 'context', 'logs']);
|
|
5
|
+
|
|
6
|
+
function dateStamp(now = new Date()) {
|
|
7
|
+
return [
|
|
8
|
+
now.getFullYear(),
|
|
9
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
10
|
+
String(now.getDate()).padStart(2, '0'),
|
|
11
|
+
].join('-');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeIfMissing(filePath, content) {
|
|
15
|
+
if (fs.existsSync(filePath)) return false;
|
|
16
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8', flag: 'wx' });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function memberMarkdown({ name, role, description }) {
|
|
21
|
+
const title = role || 'member';
|
|
22
|
+
const mission = description || 'Define why this member exists and how it chooses goals.';
|
|
23
|
+
return `---
|
|
24
|
+
name: ${name}
|
|
25
|
+
role: ${title}
|
|
26
|
+
description: ${mission}
|
|
27
|
+
version: 1.0.0
|
|
28
|
+
|
|
29
|
+
skills: []
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
can-read: true
|
|
33
|
+
can-execute: true
|
|
34
|
+
can-approve: false
|
|
35
|
+
can-accept-task: false
|
|
36
|
+
approval-required: []
|
|
37
|
+
|
|
38
|
+
tools: []
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# ${title}
|
|
42
|
+
|
|
43
|
+
## Mission
|
|
44
|
+
|
|
45
|
+
${mission}
|
|
46
|
+
|
|
47
|
+
## Persona
|
|
48
|
+
|
|
49
|
+
(Define how this member communicates, their tone, and decision-making style)
|
|
50
|
+
|
|
51
|
+
## Workflow
|
|
52
|
+
|
|
53
|
+
1. Understand the request.
|
|
54
|
+
2. Do one bounded useful step.
|
|
55
|
+
3. Leave proof another teammate can trust.
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
|
|
59
|
+
1. Stay inside this member's role and proof standard.
|
|
60
|
+
2. Ask when vision, taste, risk, or uncertainty matters.
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function memberSoulMarkdown({ name, role, born }) {
|
|
65
|
+
return `---
|
|
66
|
+
name: ${name}
|
|
67
|
+
version: 1.0.0
|
|
68
|
+
born: ${born}
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# Soul
|
|
72
|
+
|
|
73
|
+
This is who ${name} is when the rules run out.
|
|
74
|
+
|
|
75
|
+
## Beliefs
|
|
76
|
+
|
|
77
|
+
- Useful work leaves evidence another teammate can trust.
|
|
78
|
+
- Clear limits produce better judgment than vague autonomy.
|
|
79
|
+
- The mission matters more than preserving a first idea.
|
|
80
|
+
|
|
81
|
+
## Values
|
|
82
|
+
|
|
83
|
+
1. Truth before confidence.
|
|
84
|
+
2. Useful progress before activity.
|
|
85
|
+
3. Continuity before cleverness.
|
|
86
|
+
|
|
87
|
+
## Lessons
|
|
88
|
+
|
|
89
|
+
- Empty until this member earns lessons through experience.
|
|
90
|
+
|
|
91
|
+
## Edges
|
|
92
|
+
|
|
93
|
+
- **Strong:** Focused judgment within the ${role} role.
|
|
94
|
+
- **Weak:** Must ask when work crosses its stated authority or proof standard.
|
|
95
|
+
|
|
96
|
+
## Voice
|
|
97
|
+
|
|
98
|
+
- Understand the real need, make one grounded move, and leave the trail clearer.
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function memberMissionMarkdown({ name, role, description }) {
|
|
103
|
+
const purpose = description || `Handle ${role.toLowerCase()} work for the team.`;
|
|
104
|
+
return `# Mission
|
|
105
|
+
|
|
106
|
+
<!-- Human-authored purpose file. Keep this durable; runtime state belongs in .atris/state/*.jsonl and now.md. -->
|
|
107
|
+
|
|
108
|
+
## North Star
|
|
109
|
+
|
|
110
|
+
${purpose}
|
|
111
|
+
|
|
112
|
+
## How To Choose Goals
|
|
113
|
+
|
|
114
|
+
- Use MEMBER.md to stay inside ${role}'s identity, authority, and tools.
|
|
115
|
+
- Choose one useful bounded goal toward this mission.
|
|
116
|
+
- Verify the work, write the receipt, and update the log.
|
|
117
|
+
- Ask the human when vision, taste, risk, or uncertainty matters.
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function emptyMemberGoals(name, now = new Date()) {
|
|
122
|
+
return {
|
|
123
|
+
schema: 'atris.member_goals.v1',
|
|
124
|
+
member: name,
|
|
125
|
+
updated_at: now.toISOString(),
|
|
126
|
+
goals: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function initialMemberLog({ name, role, description, source, now }) {
|
|
131
|
+
const stamp = now.toTimeString().slice(0, 5);
|
|
132
|
+
return [
|
|
133
|
+
`## ${stamp} · Member initialized`,
|
|
134
|
+
`- team: ${name}`,
|
|
135
|
+
`- role: ${role}`,
|
|
136
|
+
description ? `- mission: ${description}` : '',
|
|
137
|
+
`- source: ${source}`,
|
|
138
|
+
'- status: ready_for_room',
|
|
139
|
+
'',
|
|
140
|
+
].filter(Boolean).join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureMemberBundle(memberDir, {
|
|
144
|
+
name = path.basename(memberDir),
|
|
145
|
+
role = 'member',
|
|
146
|
+
description = 'Define why this member exists and how it chooses goals.',
|
|
147
|
+
source = 'cli',
|
|
148
|
+
memberContent,
|
|
149
|
+
now = new Date(),
|
|
150
|
+
} = {}) {
|
|
151
|
+
fs.mkdirSync(memberDir, { recursive: true });
|
|
152
|
+
for (const dirName of MEMBER_DIRS) {
|
|
153
|
+
fs.mkdirSync(path.join(memberDir, dirName), { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const created = [];
|
|
157
|
+
const ensure = (relativePath, content) => {
|
|
158
|
+
const filePath = path.join(memberDir, relativePath);
|
|
159
|
+
if (writeIfMissing(filePath, content)) created.push(relativePath);
|
|
160
|
+
return filePath;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const memberFile = typeof memberContent === 'string'
|
|
164
|
+
? memberContent
|
|
165
|
+
: memberMarkdown({ name, role, description });
|
|
166
|
+
ensure('MEMBER.md', memberFile);
|
|
167
|
+
ensure('SOUL.md', memberSoulMarkdown({ name, role, born: dateStamp(now) }));
|
|
168
|
+
ensure('MISSION.md', memberMissionMarkdown({ name, role, description }));
|
|
169
|
+
ensure('goals.json', `${JSON.stringify(emptyMemberGoals(name, now), null, 2)}\n`);
|
|
170
|
+
ensure('goals.md', '# Goals\n\n<!-- Generated from goals.json. Edit with `atris member goal/tick/review` when possible. -->\n\nNo goals yet.\n');
|
|
171
|
+
const logName = `logs/${dateStamp(now)}.md`;
|
|
172
|
+
const logPath = ensure(logName, initialMemberLog({ name, role, description, source, now }));
|
|
173
|
+
|
|
174
|
+
return { created, logPath };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function memberBundlePresent(memberDir) {
|
|
178
|
+
if (!memberDir) return false;
|
|
179
|
+
const files = ['MEMBER.md', 'SOUL.md', 'MISSION.md', 'goals.json', 'goals.md'];
|
|
180
|
+
for (const fileName of files) {
|
|
181
|
+
if (!fs.existsSync(path.join(memberDir, fileName))) return false;
|
|
182
|
+
}
|
|
183
|
+
for (const dirName of MEMBER_DIRS) {
|
|
184
|
+
try {
|
|
185
|
+
if (!fs.statSync(path.join(memberDir, dirName)).isDirectory()) return false;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
ensureMemberBundle,
|
|
195
|
+
memberBundlePresent,
|
|
196
|
+
memberMarkdown,
|
|
197
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Mission ledger compaction. Every mission save appends a full snapshot to
|
|
4
|
+
// .atris/state/missions.jsonl, so a mission saved N times costs N rows and
|
|
5
|
+
// every reader pays for the whole history (2,417 rows for 302 missions when
|
|
6
|
+
// this shipped). Worse, old snapshots keep referencing old run receipts, so
|
|
7
|
+
// the daily runs-prune can never reclaim them. Compacting to one row per
|
|
8
|
+
// mission keeps exactly what every reader reconstructs anyway.
|
|
9
|
+
//
|
|
10
|
+
// Readers' contract (loadMissionMap in commands/mission.js): the surviving
|
|
11
|
+
// row per id must carry the LATEST state but the FIRST display number ever
|
|
12
|
+
// assigned, and first-appearance order must hold so numbering stays stable.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { displayNumber } = require('./short-name');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_MIN_ROWS = 64;
|
|
19
|
+
const DEFAULT_RATIO = 4;
|
|
20
|
+
|
|
21
|
+
function parseLines(text) {
|
|
22
|
+
return String(text || '')
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.map((line) => line.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((line) => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(line);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Latest snapshot per id, first-seen order, first assigned display number.
|
|
36
|
+
// Rows without an id are not mission snapshots (cloud mission receipts land
|
|
37
|
+
// here as {cloud: true, ...}); they pass through untouched, in order, after
|
|
38
|
+
// the compacted snapshots so latest-row readers still find them.
|
|
39
|
+
function compactRows(rows) {
|
|
40
|
+
const order = [];
|
|
41
|
+
const latest = new Map();
|
|
42
|
+
const firstNumber = new Map();
|
|
43
|
+
const passthrough = [];
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
if (!row) continue;
|
|
46
|
+
if (!row.id) {
|
|
47
|
+
passthrough.push(row);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!latest.has(row.id)) order.push(row.id);
|
|
51
|
+
if (!firstNumber.has(row.id) && displayNumber(row.n)) firstNumber.set(row.id, displayNumber(row.n));
|
|
52
|
+
latest.set(row.id, row);
|
|
53
|
+
}
|
|
54
|
+
const compacted = order.map((id) => {
|
|
55
|
+
const row = { ...latest.get(id) };
|
|
56
|
+
if (firstNumber.has(id)) row.n = firstNumber.get(id);
|
|
57
|
+
else delete row.n;
|
|
58
|
+
return row;
|
|
59
|
+
});
|
|
60
|
+
return compacted.concat(passthrough);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compact when history outweighs live records by `ratio`, and never bother
|
|
64
|
+
// below `minRows`. Returns a receipt either way; writes atomically so a
|
|
65
|
+
// concurrent reader sees the old file or the new one, never a torn one.
|
|
66
|
+
function compactMissionLedger(file, options = {}) {
|
|
67
|
+
const minRows = Number.isFinite(options.minRows) ? options.minRows : DEFAULT_MIN_ROWS;
|
|
68
|
+
const ratio = Number.isFinite(options.ratio) ? options.ratio : DEFAULT_RATIO;
|
|
69
|
+
// Cheap stat gate so the every-save hook does not re-read a small healthy
|
|
70
|
+
// ledger: below this size compaction cannot be worth a full read.
|
|
71
|
+
const minBytes = Number.isFinite(options.minBytes) ? options.minBytes : 262144;
|
|
72
|
+
if (options.force !== true) {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.statSync(file).size < minBytes) {
|
|
75
|
+
return { compacted: false, reason: 'below_threshold', rows_before: 0, rows_after: 0 };
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
return { compacted: false, reason: 'missing', rows_before: 0, rows_after: 0 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let text = '';
|
|
82
|
+
try {
|
|
83
|
+
text = fs.readFileSync(file, 'utf8');
|
|
84
|
+
} catch {
|
|
85
|
+
return { compacted: false, reason: 'missing', rows_before: 0, rows_after: 0 };
|
|
86
|
+
}
|
|
87
|
+
const rows = parseLines(text);
|
|
88
|
+
const valid = rows.filter((row) => row && row.id);
|
|
89
|
+
const uniqueIds = new Set(valid.map((row) => row.id)).size;
|
|
90
|
+
const receipt = {
|
|
91
|
+
rows_before: rows.length,
|
|
92
|
+
unique_missions: uniqueIds,
|
|
93
|
+
bytes_before: Buffer.byteLength(text),
|
|
94
|
+
};
|
|
95
|
+
const force = options.force === true;
|
|
96
|
+
if (!force && (rows.length < minRows || rows.length < uniqueIds * ratio)) {
|
|
97
|
+
return { ...receipt, compacted: false, reason: 'below_threshold', rows_after: rows.length };
|
|
98
|
+
}
|
|
99
|
+
const compacted = compactRows(rows.filter(Boolean));
|
|
100
|
+
const nextText = compacted.length
|
|
101
|
+
? `${compacted.map((row) => JSON.stringify(row)).join('\n')}\n`
|
|
102
|
+
: '';
|
|
103
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
fs.writeFileSync(tmp, nextText, 'utf8');
|
|
106
|
+
// The rename replaces the whole file, so an append that landed after our
|
|
107
|
+
// read would be silently lost. Re-check right before the swap and stand
|
|
108
|
+
// down if anyone wrote in between; the next save retries compaction.
|
|
109
|
+
try {
|
|
110
|
+
if (fs.statSync(file).size !== receipt.bytes_before) {
|
|
111
|
+
fs.unlinkSync(tmp);
|
|
112
|
+
return { ...receipt, compacted: false, reason: 'concurrent_write', rows_after: rows.length };
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
fs.unlinkSync(tmp);
|
|
116
|
+
return { ...receipt, compacted: false, reason: 'missing', rows_after: 0 };
|
|
117
|
+
}
|
|
118
|
+
fs.renameSync(tmp, file);
|
|
119
|
+
return {
|
|
120
|
+
...receipt,
|
|
121
|
+
compacted: true,
|
|
122
|
+
rows_after: compacted.length,
|
|
123
|
+
bytes_after: Buffer.byteLength(nextText),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { compactMissionLedger, compactRows };
|
package/lib/mission-root.js
CHANGED
|
@@ -17,8 +17,10 @@ const { spawnSync } = require('child_process');
|
|
|
17
17
|
// same root — previously mission used only the git toplevel, so a mission
|
|
18
18
|
// started inside a bound sub-workspace split away from that workspace's task
|
|
19
19
|
// state. A git worktree reports its own toplevel (its .git marker), which is
|
|
20
|
-
// the correct per-worktree store, so --worktree missions are unaffected.
|
|
21
|
-
//
|
|
20
|
+
// the correct per-worktree store, so --worktree missions are unaffected. Walk-up
|
|
21
|
+
// stops before a generic scratch root (/tmp, /private/tmp, /var/tmp), so an
|
|
22
|
+
// empty folder under /tmp does not inherit /tmp's mission book. If the shared
|
|
23
|
+
// resolver can't load, fall back to the git toplevel, then to cwd.
|
|
22
24
|
function resolveWorkspaceRoot(cwd = process.cwd()) {
|
|
23
25
|
try {
|
|
24
26
|
const { workspaceRoot } = require('./task-db');
|
|
@@ -121,7 +121,34 @@ function runCliJsonStreaming(cliPath, args, options = {}) {
|
|
|
121
121
|
...(options.env || {}),
|
|
122
122
|
},
|
|
123
123
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
124
|
+
// Own process group so a timeout can kill the whole tree: the tick
|
|
125
|
+
// spawns engine workers through a shell, and killing only the direct
|
|
126
|
+
// child leaves them running (and billing) for hours after the timeout.
|
|
127
|
+
detached: process.platform !== 'win32',
|
|
124
128
|
});
|
|
129
|
+
const killTree = (signal) => {
|
|
130
|
+
try {
|
|
131
|
+
if (process.platform !== 'win32' && proc.pid) process.kill(-proc.pid, signal);
|
|
132
|
+
else proc.kill(signal);
|
|
133
|
+
} catch {
|
|
134
|
+
try { proc.kill(signal); } catch {}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// Detached children survive the operator's Ctrl-C (they left our process
|
|
138
|
+
// group), so relay the signal to the whole tree ourselves, then let the
|
|
139
|
+
// default exit happen by re-raising after removing our handler.
|
|
140
|
+
const relaySignal = (signal) => {
|
|
141
|
+
killTree('SIGTERM');
|
|
142
|
+
setTimeout(() => killTree('SIGKILL'), 2000).unref();
|
|
143
|
+
process.removeListener(signal, relaySignal);
|
|
144
|
+
process.kill(process.pid, signal);
|
|
145
|
+
};
|
|
146
|
+
process.once('SIGINT', relaySignal);
|
|
147
|
+
process.once('SIGTERM', relaySignal);
|
|
148
|
+
const detachSignalRelay = () => {
|
|
149
|
+
process.removeListener('SIGINT', relaySignal);
|
|
150
|
+
process.removeListener('SIGTERM', relaySignal);
|
|
151
|
+
};
|
|
125
152
|
let stdout = '';
|
|
126
153
|
let stderr = '';
|
|
127
154
|
let done = false;
|
|
@@ -130,6 +157,7 @@ function runCliJsonStreaming(cliPath, args, options = {}) {
|
|
|
130
157
|
const finish = (status) => {
|
|
131
158
|
if (done) return;
|
|
132
159
|
done = true;
|
|
160
|
+
detachSignalRelay();
|
|
133
161
|
clearTimeout(timer);
|
|
134
162
|
if (progressTimer) clearInterval(progressTimer);
|
|
135
163
|
let payload = null;
|
|
@@ -146,8 +174,8 @@ function runCliJsonStreaming(cliPath, args, options = {}) {
|
|
|
146
174
|
};
|
|
147
175
|
const timer = setTimeout(() => {
|
|
148
176
|
killedByTimeout = true;
|
|
149
|
-
|
|
150
|
-
setTimeout(() =>
|
|
177
|
+
killTree('SIGTERM');
|
|
178
|
+
setTimeout(() => killTree('SIGKILL'), 3000).unref();
|
|
151
179
|
}, options.timeoutMs || 660000);
|
|
152
180
|
progressTimer = options.onProgress
|
|
153
181
|
? setInterval(() => {
|
package/lib/next-moves.js
CHANGED
|
@@ -120,12 +120,17 @@ function readRoadmapOpenItems(root) {
|
|
|
120
120
|
}));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// One parse of tasks.projection.json, shared by every reader that needs the
|
|
124
|
+
// task list, so a single nextMoves() call does not re-read the same big file.
|
|
125
|
+
function readTasksProjection(root) {
|
|
124
126
|
const text = safeRead(path.join(root, '.atris', 'state', 'tasks.projection.json'));
|
|
125
127
|
if (!text) return [];
|
|
126
128
|
let proj;
|
|
127
129
|
try { proj = JSON.parse(text); } catch { return []; }
|
|
128
|
-
|
|
130
|
+
return Array.isArray(proj && proj.tasks) ? proj.tasks : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readActiveTasks(root, tasks = readTasksProjection(root)) {
|
|
129
134
|
return tasks
|
|
130
135
|
.filter((t) => t && t.title && ['open', 'claimed'].includes(String(t.status || '').toLowerCase()))
|
|
131
136
|
.filter((t) => !isInternalNextMoveTask(t))
|
|
@@ -147,12 +152,7 @@ function isInternalNextMoveTask(task) {
|
|
|
147
152
|
return tags.includes('agent-xp') || /^mission xp\s*:/i.test(title);
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
function readHandledTaskTitles(root) {
|
|
151
|
-
const text = safeRead(path.join(root, '.atris', 'state', 'tasks.projection.json'));
|
|
152
|
-
if (!text) return [];
|
|
153
|
-
let proj;
|
|
154
|
-
try { proj = JSON.parse(text); } catch { return []; }
|
|
155
|
-
const tasks = Array.isArray(proj && proj.tasks) ? proj.tasks : [];
|
|
155
|
+
function readHandledTaskTitles(root, tasks = readTasksProjection(root)) {
|
|
156
156
|
return tasks
|
|
157
157
|
.filter((t) => t && t.title && ['review', 'done'].includes(String(t.status || '').toLowerCase()))
|
|
158
158
|
.map((t) => String(t.title).trim())
|
|
@@ -297,8 +297,8 @@ function missionShortLabel(mission) {
|
|
|
297
297
|
return shortRecordLabel(mission, missionVisibleTitle(mission), { wordLimit: 3 });
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
function readActionableMissionCards(root) {
|
|
301
|
-
return Array.from(
|
|
300
|
+
function readActionableMissionCards(root, missions = latestMissionRecords(root)) {
|
|
301
|
+
return Array.from(missions.values())
|
|
302
302
|
.filter((mission) => {
|
|
303
303
|
const status = String(mission?.status || '').toLowerCase();
|
|
304
304
|
if (['complete', 'completed', 'stopped', 'paused'].includes(status)) return false;
|
|
@@ -324,8 +324,8 @@ function readActionableMissionCards(root) {
|
|
|
324
324
|
});
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
function readActiveMissions(root) {
|
|
328
|
-
return Array.from(
|
|
327
|
+
function readActiveMissions(root, missions = latestMissionRecords(root)) {
|
|
328
|
+
return Array.from(missions.values())
|
|
329
329
|
.filter((m) => m && missionVisibleTitle(m) && !['complete', 'stopped', 'paused'].includes(String(m.status || '').toLowerCase()))
|
|
330
330
|
.map((m) => ({
|
|
331
331
|
title: missionVisibleTitle(m),
|
|
@@ -397,9 +397,8 @@ function wishHasReview(wish) {
|
|
|
397
397
|
return Boolean(wish && (wish.reviewed || (Array.isArray(wish.reviews) && wish.reviews.length)));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
function readUnreviewedShippedWishCards(root) {
|
|
400
|
+
function readUnreviewedShippedWishCards(root, missions = latestMissionRecords(root)) {
|
|
401
401
|
const latest = latestWishEventMap(root);
|
|
402
|
-
const missions = latestMissionRecords(root);
|
|
403
402
|
return readWishes(root)
|
|
404
403
|
.filter((wish) => {
|
|
405
404
|
if (!wish || !wish.id || wishHasReview(wish)) return false;
|
|
@@ -430,12 +429,7 @@ function readUnreviewedShippedWishCards(root) {
|
|
|
430
429
|
});
|
|
431
430
|
}
|
|
432
431
|
|
|
433
|
-
function activeEndgameTaskCount(root) {
|
|
434
|
-
const text = safeRead(path.join(root, '.atris', 'state', 'tasks.projection.json'));
|
|
435
|
-
if (!text) return 0;
|
|
436
|
-
let proj;
|
|
437
|
-
try { proj = JSON.parse(text); } catch { return 0; }
|
|
438
|
-
const tasks = Array.isArray(proj && proj.tasks) ? proj.tasks : [];
|
|
432
|
+
function activeEndgameTaskCount(root, tasks = readTasksProjection(root)) {
|
|
439
433
|
return tasks.filter((t) => {
|
|
440
434
|
if (!t) return false;
|
|
441
435
|
const status = String(t.status || '').toLowerCase();
|
|
@@ -444,10 +438,10 @@ function activeEndgameTaskCount(root) {
|
|
|
444
438
|
}).length;
|
|
445
439
|
}
|
|
446
440
|
|
|
447
|
-
function todoKeepsEndgameActive(root) {
|
|
441
|
+
function todoKeepsEndgameActive(root, tasks = readTasksProjection(root)) {
|
|
448
442
|
const text = safeRead(path.join(root, 'atris', 'TODO.md'));
|
|
449
443
|
if (!text) return true;
|
|
450
|
-
if (activeEndgameTaskCount(root) > 0) return true;
|
|
444
|
+
if (activeEndgameTaskCount(root, tasks) > 0) return true;
|
|
451
445
|
|
|
452
446
|
let sawEndgameHeader = false;
|
|
453
447
|
let section = '';
|
|
@@ -464,8 +458,8 @@ function todoKeepsEndgameActive(root) {
|
|
|
464
458
|
return !sawEndgameHeader;
|
|
465
459
|
}
|
|
466
460
|
|
|
467
|
-
function readEndgameMove(root) {
|
|
468
|
-
if (!todoKeepsEndgameActive(root)) return [];
|
|
461
|
+
function readEndgameMove(root, tasks = readTasksProjection(root)) {
|
|
462
|
+
if (!todoKeepsEndgameActive(root, tasks)) return [];
|
|
469
463
|
let state = null;
|
|
470
464
|
try { state = JSON.parse(safeRead(path.join(root, 'atris', 'brain', 'state.json'))); } catch { state = null; }
|
|
471
465
|
const horizon = String(state?.endgame?.horizon || '').trim();
|
|
@@ -508,17 +502,21 @@ function todayInboxItems(root) {
|
|
|
508
502
|
return inboxItemsFrom(safeRead(file));
|
|
509
503
|
}
|
|
510
504
|
|
|
511
|
-
|
|
505
|
+
// Parse each shared big file (tasks projection, missions log) exactly once per
|
|
506
|
+
// call and thread the result to every reader that needs it.
|
|
507
|
+
function gatherCandidates(root = process.cwd(), shared = {}) {
|
|
508
|
+
const tasks = shared.tasks || readTasksProjection(root);
|
|
509
|
+
const missions = shared.missions || latestMissionRecords(root);
|
|
512
510
|
return [
|
|
513
511
|
...readWaitingWishCards(root),
|
|
514
|
-
...readActionableMissionCards(root),
|
|
512
|
+
...readActionableMissionCards(root, missions),
|
|
515
513
|
...readDreamCards(root),
|
|
516
514
|
...readRoadmapOpenItems(root),
|
|
517
|
-
...readActiveMissions(root),
|
|
518
|
-
...readEndgameMove(root),
|
|
519
|
-
...readActiveTasks(root),
|
|
515
|
+
...readActiveMissions(root, missions),
|
|
516
|
+
...readEndgameMove(root, tasks),
|
|
517
|
+
...readActiveTasks(root, tasks),
|
|
520
518
|
...latestInboxItems(root),
|
|
521
|
-
...readUnreviewedShippedWishCards(root),
|
|
519
|
+
...readUnreviewedShippedWishCards(root, missions),
|
|
522
520
|
];
|
|
523
521
|
}
|
|
524
522
|
|
|
@@ -595,10 +593,11 @@ function parkNextCard(root, card, stamp) {
|
|
|
595
593
|
|
|
596
594
|
function nextCards(root = process.cwd(), limit = 1, options = {}) {
|
|
597
595
|
const { killedIds, killedTitles, approvedIds, approvedTitles } = readDecisions(root);
|
|
598
|
-
const
|
|
596
|
+
const tasks = readTasksProjection(root);
|
|
597
|
+
const handledTaskTitles = readHandledTaskTitles(root, tasks);
|
|
599
598
|
const parkedIds = options.includeParked ? [] : parkedNextIds(root);
|
|
600
599
|
const skipIds = Array.isArray(options.skipIds) ? options.skipIds : [];
|
|
601
|
-
return pickNextMoves(gatherCandidates(root), {
|
|
600
|
+
return pickNextMoves(gatherCandidates(root, { tasks }), {
|
|
602
601
|
limit,
|
|
603
602
|
killedIds: [...killedIds, ...parkedIds, ...skipIds],
|
|
604
603
|
killedTitles,
|
|
@@ -836,8 +835,9 @@ function pickRoadmapSeed(root = process.cwd()) {
|
|
|
836
835
|
// so the ranking inputs live in one place.
|
|
837
836
|
function nextMoves(root = process.cwd(), limit = 3) {
|
|
838
837
|
const { killedIds, killedTitles, approvedIds, approvedTitles } = readDecisions(root);
|
|
839
|
-
const
|
|
840
|
-
const
|
|
838
|
+
const tasks = readTasksProjection(root);
|
|
839
|
+
const handledTaskTitles = readHandledTaskTitles(root, tasks);
|
|
840
|
+
const picked = pickNextMoves(gatherCandidates(root, { tasks }), { limit, killedIds, killedTitles, approvedIds, approvedTitles, handledTaskTitles });
|
|
841
841
|
return addMissionOnlyFallback(picked, limit, root);
|
|
842
842
|
}
|
|
843
843
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared headless/agent guard. When true, commands must not open a prompt.
|
|
5
|
+
* Covers: ATRIS_NONINTERACTIVE / ATRIS_NO_INTERACTIVE, --yes/-y, --json.
|
|
6
|
+
* Non-TTY stdin alone is usually non-interactive, but callers that accept
|
|
7
|
+
* piped batch input (like `atris log`) should use isForcedNonInteractive
|
|
8
|
+
* and handle empty vs piped stdin themselves.
|
|
9
|
+
*/
|
|
10
|
+
function isForcedNonInteractive(args = process.argv.slice(2)) {
|
|
11
|
+
const list = Array.isArray(args) ? args : [];
|
|
12
|
+
if (process.env.ATRIS_NONINTERACTIVE === '1') return true;
|
|
13
|
+
if (process.env.ATRIS_NO_INTERACTIVE === '1') return true;
|
|
14
|
+
if (list.includes('--yes') || list.includes('-y')) return true;
|
|
15
|
+
if (list.includes('--json')) return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isNonInteractive(args = process.argv.slice(2)) {
|
|
20
|
+
if (isForcedNonInteractive(args)) return true;
|
|
21
|
+
if (!process.stdin.isTTY) return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function wantsJson(args = process.argv.slice(2)) {
|
|
26
|
+
return Array.isArray(args) && args.includes('--json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isHelpToken(arg) {
|
|
30
|
+
return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function argsWantHelp(args = []) {
|
|
34
|
+
const list = Array.isArray(args) ? args : [];
|
|
35
|
+
return list.some(isHelpToken);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasYesFlag(args = []) {
|
|
39
|
+
const list = Array.isArray(args) ? args : [];
|
|
40
|
+
return list.includes('--yes') || list.includes('-y');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Headless / agent sessions must not hang on interactive runners.
|
|
45
|
+
* Non-TTY, ATRIS_NONINTERACTIVE=1, and ATRIS_NO_INTERACTIVE=1 all refuse
|
|
46
|
+
* unless the caller passed an explicit proceed flag (--yes, or --once for serve).
|
|
47
|
+
*/
|
|
48
|
+
function refuseHeadlessUnless(args, { allowYes = true, allowOnce = false, usage = '' } = {}) {
|
|
49
|
+
const list = Array.isArray(args) ? args : [];
|
|
50
|
+
const headless = Boolean(
|
|
51
|
+
process.env.ATRIS_NONINTERACTIVE === '1'
|
|
52
|
+
|| process.env.ATRIS_NO_INTERACTIVE === '1'
|
|
53
|
+
|| !process.stdin.isTTY
|
|
54
|
+
|| !process.stdout.isTTY
|
|
55
|
+
);
|
|
56
|
+
if (!headless) return false;
|
|
57
|
+
if (allowYes && hasYesFlag(list)) return false;
|
|
58
|
+
if (allowOnce && list.includes('--once')) return false;
|
|
59
|
+
const lines = [];
|
|
60
|
+
if (usage) lines.push(usage);
|
|
61
|
+
else lines.push('This command needs a terminal, or an explicit proceed flag.');
|
|
62
|
+
lines.push('Pass --yes to run headless' + (allowOnce ? ', or --once for a single serve op.' : '.'));
|
|
63
|
+
console.error(lines.join('\n'));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rejectUnsupportedJson(commandName, args = process.argv.slice(2)) {
|
|
68
|
+
if (!wantsJson(args)) return false;
|
|
69
|
+
console.log(JSON.stringify({
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `${commandName} does not support --json`,
|
|
72
|
+
command: commandName,
|
|
73
|
+
usage: `atris ${commandName}`,
|
|
74
|
+
}, null, 2));
|
|
75
|
+
process.exit(2);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
isForcedNonInteractive,
|
|
81
|
+
isNonInteractive,
|
|
82
|
+
wantsJson,
|
|
83
|
+
isHelpToken,
|
|
84
|
+
argsWantHelp,
|
|
85
|
+
hasYesFlag,
|
|
86
|
+
refuseHeadlessUnless,
|
|
87
|
+
rejectUnsupportedJson,
|
|
88
|
+
};
|
package/lib/permission-grants.js
CHANGED
|
@@ -119,7 +119,15 @@ function addGrant({ command, workspaceRoot, createdVia = 'cli', expiresInDays =
|
|
|
119
119
|
|
|
120
120
|
function revokeGrant(grantId, file) {
|
|
121
121
|
const store = loadGrants(file);
|
|
122
|
-
const
|
|
122
|
+
const ref = String(grantId || '');
|
|
123
|
+
let grant = store.grants.find(g => g.grant_id === ref);
|
|
124
|
+
if (!grant && ref) {
|
|
125
|
+
// Prefix refs are a convenience, but an ambiguous prefix must never
|
|
126
|
+
// revoke an arbitrary grant — require exactly one match.
|
|
127
|
+
const matches = store.grants.filter(g => String(g.grant_id).startsWith(ref));
|
|
128
|
+
if (matches.length > 1) return { ok: false, reason: `"${ref}" matches ${matches.length} grants; use more characters` };
|
|
129
|
+
grant = matches[0];
|
|
130
|
+
}
|
|
123
131
|
if (!grant) return { ok: false, reason: 'grant not found' };
|
|
124
132
|
grant.status = 'revoked';
|
|
125
133
|
grant.sync = { ...(grant.sync || {}), revoked_at: new Date().toISOString() };
|
|
@@ -283,5 +291,6 @@ module.exports = {
|
|
|
283
291
|
revokeGrant,
|
|
284
292
|
matchGrant,
|
|
285
293
|
recordUse,
|
|
294
|
+
commandIsGrantable,
|
|
286
295
|
syncGrants,
|
|
287
296
|
};
|
package/lib/policy-lessons.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// differently than the last. No LLM calls; re-mining refreshes the numbers.
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { fileAutoLessons } = require('./auto-lessons');
|
|
12
13
|
const { RECEIPT_PATH_PATTERN, extractReceiptEvidence } = require('./receipt-evidence');
|
|
13
14
|
const escapeRegExp = require('./escape-regexp');
|
|
14
15
|
|
|
@@ -41,11 +42,13 @@ function readJsonlFile(filePath) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function loadHistory(root) {
|
|
44
|
-
|
|
45
|
+
const history = {
|
|
45
46
|
receipts: readJsonlFile(path.join(root, CAREER_XP_RECEIPTS_FILE)),
|
|
46
47
|
episodes: readJsonlFile(path.join(root, TASK_EPISODES_FILE)),
|
|
47
48
|
scorecards: readJsonlFile(path.join(root, SCORECARDS_FILE)),
|
|
48
49
|
};
|
|
50
|
+
fileAutoLessons(root, { episodes: history.episodes });
|
|
51
|
+
return history;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function isAgentActor(actor) {
|