atris 3.38.0 → 3.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -6
- package/atris/PERSONA.md +8 -4
- package/atris.md +7 -0
- package/ax +2 -1
- package/bin/atris.js +31 -6
- package/commands/agent-spawn.js +13 -11
- package/commands/autoland.js +28 -77
- package/commands/bench.js +10 -12
- package/commands/business.js +345 -0
- package/commands/chat-scan.js +5 -7
- package/commands/codex-goal.js +8 -10
- package/commands/computer.js +20 -0
- package/commands/console.js +19 -3
- package/commands/decide.js +166 -0
- package/commands/deck.js +1 -4
- package/commands/drill.js +14 -24
- package/commands/engine.js +196 -8
- package/commands/gm.js +8 -6
- package/commands/harvest.js +1 -4
- package/commands/init.js +23 -3
- package/commands/land.js +8 -14
- package/commands/launchpad.js +1 -14
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/member.js +558 -574
- package/commands/mission.js +456 -237
- package/commands/pack.js +2746 -164
- package/commands/play.js +6 -4
- package/commands/probe.js +2 -2
- package/commands/pulse.js +15 -16
- package/commands/release.js +10 -9
- package/commands/router.js +5 -4
- package/commands/site-deploy.js +885 -0
- package/commands/site.js +11 -2
- package/commands/slop.js +14 -2
- package/commands/stream.js +4 -18
- package/commands/task.js +880 -558
- package/commands/taste.js +101 -0
- package/commands/team.js +176 -3
- package/commands/vercel.js +4 -2
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -22
- package/commands/wiki.js +1 -4
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +1 -14
- package/commands/xp.js +27 -24
- package/lib/accept-verify-gate.js +5 -1
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +116 -1
- package/lib/autoland.js +66 -0
- package/lib/bench/runner.js +19 -1
- package/lib/context-gatherer.js +7 -1
- package/lib/engine-registry.js +141 -20
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +65 -15
- package/lib/git-spawn.js +15 -0
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +0 -2
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +4 -1
- package/lib/official-cli-integration.js +47 -2
- package/lib/orb-context.js +8 -1
- package/lib/pack-capabilities.js +685 -0
- package/lib/router-brain.js +51 -1
- package/lib/runner-command.js +0 -6
- package/lib/self-drive.js +44 -13
- package/lib/task-db.js +137 -3
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +66 -0
- package/lib/wish-audit.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/zip.js +95 -7
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { addTaste, listTaste } = require('../lib/taste-lessons');
|
|
4
|
+
|
|
5
|
+
const VALID_FLAGS = new Set(['--why', '--scope', '--example']);
|
|
6
|
+
|
|
7
|
+
function usage(stdout = process.stdout) {
|
|
8
|
+
stdout.write([
|
|
9
|
+
'usage: atris taste keep|kill|more "<subject>" --why "<reason>" [--scope writing|design|code|any] [--example path]',
|
|
10
|
+
' atris taste list [--scope writing|design|code|any]',
|
|
11
|
+
'',
|
|
12
|
+
].join('\n'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function flagValue(args, flag) {
|
|
16
|
+
const index = args.indexOf(flag);
|
|
17
|
+
if (index === -1) return undefined;
|
|
18
|
+
const value = args[index + 1];
|
|
19
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rejectUnknownArgs(args, positionalCount) {
|
|
24
|
+
for (let index = positionalCount; index < args.length; index += 1) {
|
|
25
|
+
const value = args[index];
|
|
26
|
+
if (!value.startsWith('--')) continue;
|
|
27
|
+
if (!VALID_FLAGS.has(value)) throw new Error(`unknown option: ${value}`);
|
|
28
|
+
index += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderTaste(entries, stdout = process.stdout, filtered = false) {
|
|
33
|
+
if (!entries.length) {
|
|
34
|
+
stdout.write(`${filtered ? 'no taste lessons match this scope.' : 'no taste lessons have been recorded yet.'}\n`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
stdout.write(`the operator's verdict is ${entry.verdict} for "${entry.subject}".\n`);
|
|
40
|
+
stdout.write(`the reason is: ${entry.why}\n`);
|
|
41
|
+
stdout.write(`this applies to ${entry.scope}.\n`);
|
|
42
|
+
if (entry.example) stdout.write(`the example is ${entry.example}.\n`);
|
|
43
|
+
stdout.write(`this was added on ${entry.added}.\n\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tasteCommand(args = [], options = {}) {
|
|
48
|
+
const stdout = options.stdout || process.stdout;
|
|
49
|
+
const stderr = options.stderr || process.stderr;
|
|
50
|
+
const root = options.root || process.cwd();
|
|
51
|
+
const [subcommand, ...rest] = args;
|
|
52
|
+
|
|
53
|
+
if (!subcommand) {
|
|
54
|
+
renderTaste(listTaste({ root }), stdout);
|
|
55
|
+
stdout.write('\n');
|
|
56
|
+
usage(stdout);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
60
|
+
usage(stdout);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (subcommand === 'list') {
|
|
66
|
+
rejectUnknownArgs(rest, 0);
|
|
67
|
+
const scope = flagValue(rest, '--scope');
|
|
68
|
+
renderTaste(listTaste({ root, scope }), stdout, !!scope);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!['keep', 'kill', 'more'].includes(subcommand)) {
|
|
73
|
+
stderr.write(`unknown taste verdict: ${subcommand}.\n`);
|
|
74
|
+
usage(stderr);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rejectUnknownArgs(rest, 1);
|
|
79
|
+
const subject = rest[0];
|
|
80
|
+
const why = flagValue(rest, '--why');
|
|
81
|
+
const scope = flagValue(rest, '--scope') || 'any';
|
|
82
|
+
const example = flagValue(rest, '--example');
|
|
83
|
+
const entry = addTaste({
|
|
84
|
+
verdict: subcommand,
|
|
85
|
+
subject,
|
|
86
|
+
why,
|
|
87
|
+
scope,
|
|
88
|
+
example,
|
|
89
|
+
added: new Date().toISOString().slice(0, 10),
|
|
90
|
+
root,
|
|
91
|
+
});
|
|
92
|
+
stdout.write(`saved the operator's ${entry.verdict} verdict for "${entry.subject}".\n`);
|
|
93
|
+
return 0;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
stderr.write(`taste could not continue: ${error.message}.\n`);
|
|
96
|
+
usage(stderr);
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { tasteCommand };
|
package/commands/team.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { canonicalEngineName } = require('../lib/engine-registry');
|
|
3
7
|
const taskDb = require('../lib/task-db');
|
|
4
8
|
const { buildTeamPresence, DEFAULT_FRESHNESS_WINDOW_MS, renderTeamPresence } = require('../lib/team-presence');
|
|
5
9
|
const { listMissions, listWorktreeRollupMissions } = require('./mission');
|
|
@@ -45,11 +49,150 @@ function collectTeamPresence(deps = {}) {
|
|
|
45
49
|
});
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
// One team view: member folders under atris/team/ are the who, live missions
|
|
53
|
+
// are the what-they-run-on. The fleet keeps no state file, so an engine
|
|
54
|
+
// "assignment" is read straight from missions still in flight.
|
|
55
|
+
const ROSTER_LIVE_MISSION_STATUSES = new Set(['running', 'planning']);
|
|
56
|
+
|
|
57
|
+
function collectMembers(root, deps = {}) {
|
|
58
|
+
if (Array.isArray(deps.members)) return deps.members;
|
|
59
|
+
const memberModule = deps.memberModule || require('./member');
|
|
60
|
+
return memberModule.findAllMembers(path.join(root, 'atris', 'team'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The MEMBER.md role line, made safe for a human sentence: lowercase, no em
|
|
64
|
+
// dashes, no repeated name prefix ("Linguist - operator language" -> "operator
|
|
65
|
+
// language" when the member is already named on the line).
|
|
66
|
+
function plainRole(member) {
|
|
67
|
+
// findAllMembers stamps '(no role)' on members without a role line; treat
|
|
68
|
+
// that placeholder as empty so the description can fill in.
|
|
69
|
+
const role = String(member?.role || '').trim();
|
|
70
|
+
const raw = (role && role !== '(no role)' ? role : String(member?.description || '')).trim();
|
|
71
|
+
const cleaned = raw
|
|
72
|
+
.replace(/[\u2013\u2014]/g, '-')
|
|
73
|
+
.replace(/\s+/g, ' ')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[.\s]+$/, '');
|
|
76
|
+
const name = String(member?.name || '').trim().toLowerCase();
|
|
77
|
+
const deduped = name && cleaned.startsWith(name)
|
|
78
|
+
? cleaned.slice(name.length).replace(/^[\s:,-]+/, '')
|
|
79
|
+
: cleaned;
|
|
80
|
+
if (!deduped) return 'no role written yet';
|
|
81
|
+
// Keep the sentence readable: descriptions can run long, roles never should.
|
|
82
|
+
if (deduped.length <= 100) return deduped;
|
|
83
|
+
const cut = deduped.slice(0, 100);
|
|
84
|
+
return `${cut.slice(0, cut.lastIndexOf(' '))}`.replace(/[,;:]+$/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function missionEngine(mission) {
|
|
88
|
+
return canonicalEngineName(mission?.runner) || canonicalEngineName(mission?.engine);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectTeamRoster(deps = {}) {
|
|
92
|
+
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
93
|
+
const engineByOwner = new Map();
|
|
94
|
+
for (const mission of collectMissions(root, deps)) {
|
|
95
|
+
if (!ROSTER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) continue;
|
|
96
|
+
const owner = String(mission?.owner || mission?.member || '').trim().toLowerCase();
|
|
97
|
+
const engine = missionEngine(mission);
|
|
98
|
+
// Missions arrive newest-first; the first live one per owner wins.
|
|
99
|
+
if (owner && engine && !engineByOwner.has(owner)) engineByOwner.set(owner, engine);
|
|
100
|
+
}
|
|
101
|
+
return collectMembers(root, deps)
|
|
102
|
+
.map((member) => {
|
|
103
|
+
const name = String(member?.name || '').trim().toLowerCase();
|
|
104
|
+
return { name, role: plainRole(member), engine: engineByOwner.get(name) || '' };
|
|
105
|
+
})
|
|
106
|
+
.filter((entry) => entry.name)
|
|
107
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderTeamRoster(roster) {
|
|
111
|
+
if (!roster.length) {
|
|
112
|
+
return 'no team members yet. create one with: atris member create <name> --role="..."';
|
|
113
|
+
}
|
|
114
|
+
return roster
|
|
115
|
+
.map((entry) => `${entry.name} - ${entry.role}${entry.engine ? `, on ${entry.engine}` : ''}.`)
|
|
116
|
+
.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The pruning pass keeps the team lean like a real company: it flags members
|
|
120
|
+
// with no recent signal, and it never deletes anything. A signal is the newest
|
|
121
|
+
// of MEMBER.md, any logs/*.md, or a mission the member owns that is still
|
|
122
|
+
// active or running.
|
|
123
|
+
const PRUNE_ACTIVE_MISSION_STATUSES = new Set(['active', 'running']);
|
|
124
|
+
const DEFAULT_PRUNE_DAYS = 30;
|
|
125
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
126
|
+
|
|
127
|
+
function newestSignalMs(member) {
|
|
128
|
+
const times = [];
|
|
129
|
+
const stamp = (file) => {
|
|
130
|
+
try { times.push(fs.statSync(file).mtimeMs); } catch { /* missing file is just no signal */ }
|
|
131
|
+
};
|
|
132
|
+
if (member?.path) stamp(member.path);
|
|
133
|
+
if (member?.dir) {
|
|
134
|
+
const logsDir = path.join(member.dir, 'logs');
|
|
135
|
+
let entries = [];
|
|
136
|
+
try { entries = fs.readdirSync(logsDir); } catch { entries = []; }
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.endsWith('.md')) stamp(path.join(logsDir, entry));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return times.length ? Math.max(...times) : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectTeamPrune(deps = {}) {
|
|
145
|
+
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
146
|
+
const days = Number.isFinite(deps.days) && deps.days > 0 ? deps.days : DEFAULT_PRUNE_DAYS;
|
|
147
|
+
const nowMs = typeof deps.now === 'function' ? deps.now() : Date.now();
|
|
148
|
+
const activeOwners = new Set();
|
|
149
|
+
for (const mission of collectMissions(root, deps)) {
|
|
150
|
+
if (!PRUNE_ACTIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) continue;
|
|
151
|
+
const owner = String(mission?.owner || mission?.member || '').trim().toLowerCase();
|
|
152
|
+
if (owner) activeOwners.add(owner);
|
|
153
|
+
}
|
|
154
|
+
const quiet = [];
|
|
155
|
+
let activeCount = 0;
|
|
156
|
+
for (const member of collectMembers(root, deps)) {
|
|
157
|
+
const name = String(member?.name || '').trim().toLowerCase();
|
|
158
|
+
if (!name) continue;
|
|
159
|
+
const signalMs = newestSignalMs(member);
|
|
160
|
+
if (activeOwners.has(name) || (signalMs && nowMs - signalMs < days * DAY_MS)) {
|
|
161
|
+
activeCount += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
quiet.push({
|
|
165
|
+
name,
|
|
166
|
+
days_quiet: signalMs ? Math.floor((nowMs - signalMs) / DAY_MS) : null,
|
|
167
|
+
last_signal: signalMs ? new Date(signalMs).toISOString() : null,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
quiet.sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
return { quiet, active_count: activeCount };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderTeamPrune(report, days = DEFAULT_PRUNE_DAYS) {
|
|
175
|
+
if (!report.quiet.length && !report.active_count) {
|
|
176
|
+
return 'no team members yet. create one with: atris member create <name> --role="..."';
|
|
177
|
+
}
|
|
178
|
+
if (!report.quiet.length) {
|
|
179
|
+
return `everyone on the team has a signal newer than ${days} days. nothing to prune.`;
|
|
180
|
+
}
|
|
181
|
+
const lines = report.quiet.map((entry) => (entry.days_quiet === null
|
|
182
|
+
? `${entry.name} has no recorded activity; keep, hand off, or retire.`
|
|
183
|
+
: `${entry.name} has been quiet for ${entry.days_quiet} days; keep, hand off, or retire.`));
|
|
184
|
+
lines.push(`${report.active_count} member${report.active_count === 1 ? ' is' : 's are'} still active. nothing was deleted; this is a report.`);
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
48
188
|
function helpText() {
|
|
49
189
|
return [
|
|
190
|
+
'atris team - one team view: every member, their role, and any engine running their work',
|
|
50
191
|
'atris team presence - show who is awake and what they are doing',
|
|
192
|
+
'atris team prune - flag members with no recent activity; deletes nothing',
|
|
51
193
|
'',
|
|
52
|
-
'usage: atris team presence [--json]',
|
|
194
|
+
'usage: atris team [roster|presence] [--json]',
|
|
195
|
+
'usage: atris team prune [--days N] [--json]',
|
|
53
196
|
].join('\n');
|
|
54
197
|
}
|
|
55
198
|
|
|
@@ -58,8 +201,38 @@ function teamCommand(args = [], deps = {}) {
|
|
|
58
201
|
(deps.write || process.stdout.write.bind(process.stdout))(`${helpText()}\n`);
|
|
59
202
|
return 0;
|
|
60
203
|
}
|
|
204
|
+
if (args[0] === 'prune') {
|
|
205
|
+
const rest = args.slice(1);
|
|
206
|
+
let days = DEFAULT_PRUNE_DAYS;
|
|
207
|
+
let json = false;
|
|
208
|
+
let bad = false;
|
|
209
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
210
|
+
const arg = rest[i];
|
|
211
|
+
if (arg === '--json') { json = true; continue; }
|
|
212
|
+
if (arg === '--days') { i += 1; days = Number(rest[i]); continue; }
|
|
213
|
+
if (arg.startsWith('--days=')) { days = Number(arg.slice('--days='.length)); continue; }
|
|
214
|
+
bad = true;
|
|
215
|
+
}
|
|
216
|
+
if (bad || !Number.isFinite(days) || days <= 0) {
|
|
217
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team prune [--days N] [--json]\n');
|
|
218
|
+
return 2;
|
|
219
|
+
}
|
|
220
|
+
const report = deps.prune || collectTeamPrune({ ...deps, days });
|
|
221
|
+
const output = json ? JSON.stringify(report, null, 2) : renderTeamPrune(report, days);
|
|
222
|
+
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
const rosterArgs = args.filter((arg) => arg !== 'roster');
|
|
226
|
+
if (args[0] !== 'presence' && rosterArgs.every((arg) => arg === '--json')) {
|
|
227
|
+
const roster = deps.roster || collectTeamRoster(deps);
|
|
228
|
+
const output = rosterArgs.includes('--json')
|
|
229
|
+
? JSON.stringify(roster, null, 2)
|
|
230
|
+
: renderTeamRoster(roster);
|
|
231
|
+
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
61
234
|
if (args[0] !== 'presence' || args.some((arg, index) => index > 0 && arg !== '--json')) {
|
|
62
|
-
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team presence [--json]\n');
|
|
235
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence|prune] [--json]\n');
|
|
63
236
|
return 2;
|
|
64
237
|
}
|
|
65
238
|
const presence = deps.presence || collectTeamPresence(deps);
|
|
@@ -70,4 +243,4 @@ function teamCommand(args = [], deps = {}) {
|
|
|
70
243
|
return 0;
|
|
71
244
|
}
|
|
72
245
|
|
|
73
|
-
module.exports = { collectMissions, collectTasks, collectTeamPresence, teamCommand };
|
|
246
|
+
module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamPrune, collectTeamRoster, renderTeamPrune, renderTeamRoster, teamCommand };
|
package/commands/vercel.js
CHANGED
|
@@ -12,7 +12,8 @@ const vercelCommand = createOfficialCliCommand({
|
|
|
12
12
|
usage: 'deploy',
|
|
13
13
|
match: ['deploy'],
|
|
14
14
|
forward: ['deploy'],
|
|
15
|
-
description: 'deploy the current project',
|
|
15
|
+
description: 'deploy the current project (grouped output)',
|
|
16
|
+
summarize: true,
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
usage: 'ls',
|
|
@@ -24,7 +25,8 @@ const vercelCommand = createOfficialCliCommand({
|
|
|
24
25
|
usage: 'logs',
|
|
25
26
|
match: ['logs'],
|
|
26
27
|
forward: ['logs'],
|
|
27
|
-
description: 'stream or inspect deployment logs',
|
|
28
|
+
description: 'stream or inspect deployment logs (grouped output)',
|
|
29
|
+
summarize: true,
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
32
|
usage: 'inspect',
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const deterministicVoice = require('../scripts/det/voice');
|
|
9
|
+
|
|
10
|
+
const JUDGE_TIMEOUT_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
function extractVoiceSection(repoRoot = process.cwd()) {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path.join(repoRoot, 'atris.md'),
|
|
15
|
+
path.join(repoRoot, 'atris', 'atris.md'),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const file of candidates) {
|
|
19
|
+
if (!fs.existsSync(file)) continue;
|
|
20
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
21
|
+
const start = lines.findIndex((line) => /^## voice\s*$/i.test(line));
|
|
22
|
+
if (start === -1) continue;
|
|
23
|
+
let end = lines.length;
|
|
24
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
25
|
+
if (/^##\s+/.test(lines[i])) {
|
|
26
|
+
end = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines.slice(start, end).join('\n').trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error('the voice section was not found');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findExecutable(name, env = process.env) {
|
|
37
|
+
const pathValue = env.PATH || '';
|
|
38
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
39
|
+
if (!dir) continue;
|
|
40
|
+
const candidate = path.join(dir, name);
|
|
41
|
+
try {
|
|
42
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
43
|
+
return candidate;
|
|
44
|
+
} catch {
|
|
45
|
+
// Try the next directory.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function judgePrompt(rubric) {
|
|
52
|
+
return [
|
|
53
|
+
'Judge only the SHAPE and PLAINNESS of the reply provided on stdin against the rubric below.',
|
|
54
|
+
'Do not judge factual correctness or technical quality.',
|
|
55
|
+
'When unsure, PASS. False alarms are worse than misses.',
|
|
56
|
+
'Return only strict JSON with exactly this shape: {"pass": boolean, "reasons": [strings]}.',
|
|
57
|
+
'',
|
|
58
|
+
rubric,
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseVerdict(output) {
|
|
63
|
+
const verdict = JSON.parse(String(output || '').trim());
|
|
64
|
+
if (!verdict || Array.isArray(verdict) || typeof verdict !== 'object') {
|
|
65
|
+
throw new Error('the verdict is not an object');
|
|
66
|
+
}
|
|
67
|
+
const keys = Object.keys(verdict).sort();
|
|
68
|
+
if (keys.length !== 2 || keys[0] !== 'pass' || keys[1] !== 'reasons') {
|
|
69
|
+
throw new Error('the verdict has the wrong fields');
|
|
70
|
+
}
|
|
71
|
+
if (typeof verdict.pass !== 'boolean'
|
|
72
|
+
|| !Array.isArray(verdict.reasons)
|
|
73
|
+
|| !verdict.reasons.every((reason) => typeof reason === 'string')) {
|
|
74
|
+
throw new Error('the verdict has the wrong shape');
|
|
75
|
+
}
|
|
76
|
+
return verdict;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unavailable(stderr, reason) {
|
|
80
|
+
const cleanReason = String(reason || 'unknown error').replace(/\s+/g, ' ').trim();
|
|
81
|
+
stderr.write(`voice judge unavailable: ${cleanReason}\n`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runScan(reply, args, stdout) {
|
|
86
|
+
const mode = args.includes('--json') ? 'json' : 'scan';
|
|
87
|
+
const result = deterministicVoice.run(mode, reply);
|
|
88
|
+
if (result.error) return { code: 2, error: result.error };
|
|
89
|
+
stdout.write(`${result.text}\n`);
|
|
90
|
+
const passed = result.text.startsWith('PASS') || result.text.startsWith('{"pass":true');
|
|
91
|
+
return { code: passed ? 0 : 1 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runJudge(reply, options = {}) {
|
|
95
|
+
const repoRoot = options.repoRoot || process.cwd();
|
|
96
|
+
const env = options.env || process.env;
|
|
97
|
+
const stdout = options.stdout || process.stdout;
|
|
98
|
+
const stderr = options.stderr || process.stderr;
|
|
99
|
+
|
|
100
|
+
let rubric;
|
|
101
|
+
try {
|
|
102
|
+
rubric = extractVoiceSection(repoRoot);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return unavailable(stderr, error.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const customCommand = String(env.ATRIS_VOICE_JUDGE_CMD || '').trim();
|
|
108
|
+
const claudePath = customCommand ? null : findExecutable('claude', env);
|
|
109
|
+
if (!customCommand && !claudePath) {
|
|
110
|
+
return unavailable(stderr, 'no judge command was found');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let tempDir;
|
|
114
|
+
try {
|
|
115
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-voice-'));
|
|
116
|
+
const rubricPath = path.join(tempDir, 'rubric.md');
|
|
117
|
+
fs.writeFileSync(rubricPath, rubric, 'utf8');
|
|
118
|
+
const childEnv = { ...env, ATRIS_VOICE_RUBRIC: rubricPath };
|
|
119
|
+
const result = customCommand
|
|
120
|
+
? spawnSync('/bin/sh', ['-c', customCommand], {
|
|
121
|
+
input: reply,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
env: childEnv,
|
|
124
|
+
timeout: JUDGE_TIMEOUT_MS,
|
|
125
|
+
maxBuffer: 1024 * 1024,
|
|
126
|
+
})
|
|
127
|
+
: spawnSync(claudePath, ['-p', '--model', 'haiku', judgePrompt(rubric)], {
|
|
128
|
+
input: reply,
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
env: childEnv,
|
|
131
|
+
timeout: JUDGE_TIMEOUT_MS,
|
|
132
|
+
maxBuffer: 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (result.error) return unavailable(stderr, result.error.code === 'ETIMEDOUT' ? 'the judge timed out' : 'the judge command failed');
|
|
136
|
+
if (result.status !== 0) return unavailable(stderr, 'the judge command failed');
|
|
137
|
+
|
|
138
|
+
let verdict;
|
|
139
|
+
try {
|
|
140
|
+
verdict = parseVerdict(result.stdout);
|
|
141
|
+
} catch {
|
|
142
|
+
return unavailable(stderr, 'the judge returned an unreadable verdict');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
stdout.write(`${JSON.stringify(verdict)}\n`);
|
|
146
|
+
return verdict.pass ? 0 : 1;
|
|
147
|
+
} catch {
|
|
148
|
+
return unavailable(stderr, 'the judge command failed');
|
|
149
|
+
} finally {
|
|
150
|
+
if (tempDir) {
|
|
151
|
+
try {
|
|
152
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
153
|
+
} catch {
|
|
154
|
+
// Cleanup cannot block the reply.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showVoiceHelp(stdout = process.stdout) {
|
|
161
|
+
stdout.write([
|
|
162
|
+
'usage: atris voice scan [--json]',
|
|
163
|
+
' atris voice judge',
|
|
164
|
+
'',
|
|
165
|
+
'scan checks binary voice tells. judge checks reply shape and plainness.',
|
|
166
|
+
'',
|
|
167
|
+
].join('\n'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function voiceCommand(args, options = {}) {
|
|
171
|
+
const subcommand = args[0];
|
|
172
|
+
const stdout = options.stdout || process.stdout;
|
|
173
|
+
const stderr = options.stderr || process.stderr;
|
|
174
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
175
|
+
showVoiceHelp(stdout);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
if (!['scan', 'judge'].includes(subcommand)) {
|
|
179
|
+
stderr.write('usage: atris voice scan [--json] | atris voice judge\n');
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const reply = options.input === undefined ? fs.readFileSync(0, 'utf8') : options.input;
|
|
184
|
+
if (subcommand === 'scan') {
|
|
185
|
+
const result = runScan(reply, args.slice(1), stdout);
|
|
186
|
+
if (result.error) stderr.write(`${result.error}\n`);
|
|
187
|
+
return result.code;
|
|
188
|
+
}
|
|
189
|
+
return runJudge(reply, { ...options, stdout, stderr });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
extractVoiceSection,
|
|
194
|
+
voiceCommand,
|
|
195
|
+
};
|
package/commands/watch.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
6
7
|
const { startMission, listMissions } = require('./mission');
|
|
7
8
|
|
|
8
9
|
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
@@ -22,28 +23,6 @@ function slugify(value) {
|
|
|
22
23
|
.slice(0, 48) || 'watch';
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function hasFlag(args, name) {
|
|
26
|
-
return args.includes(name);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function unquote(value) {
|
|
30
|
-
const text = String(value);
|
|
31
|
-
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
32
|
-
return text.slice(1, -1);
|
|
33
|
-
}
|
|
34
|
-
return text;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function readFlag(args, name, fallback = '') {
|
|
38
|
-
const prefix = `${name}=`;
|
|
39
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
40
|
-
const arg = String(args[i]);
|
|
41
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return unquote(args[i + 1]);
|
|
42
|
-
if (arg.startsWith(prefix)) return unquote(arg.slice(prefix.length));
|
|
43
|
-
}
|
|
44
|
-
return fallback;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
26
|
function parseSentenceArgs(args) {
|
|
48
27
|
const sentenceParts = [];
|
|
49
28
|
let index = 0;
|
package/commands/wiki.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
3
4
|
const { loadCredentials } = require('../utils/auth');
|
|
4
5
|
const { apiRequestJson } = require('../utils/api');
|
|
5
6
|
const { loadBusinesses, saveBusinesses } = require('./business');
|
|
@@ -223,10 +224,6 @@ function printWikiHelp(scope = null) {
|
|
|
223
224
|
console.log('');
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
function hasFlag(args, name) {
|
|
227
|
-
return args.includes(name);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
227
|
function optionValue(args, name, fallback = null) {
|
|
231
228
|
const index = args.indexOf(name);
|
|
232
229
|
if (index === -1 || index + 1 >= args.length) return fallback;
|
package/commands/workflow.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { getLogPath } = require('../lib/journal');
|
|
4
|
-
const {
|
|
4
|
+
const { buildToolResultBody } = require('../lib/tool-result-encode');
|
|
5
5
|
|
|
6
6
|
function wrapWorkflowText(text, width = 76) {
|
|
7
7
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
@@ -140,7 +140,7 @@ function postToolResult(callId, result, base = 'http://127.0.0.1:8000') {
|
|
|
140
140
|
const url = new URL('/api/atris2/turn/tool-result', base);
|
|
141
141
|
const transport = url.protocol === 'https:' ? require('https') : require('http');
|
|
142
142
|
return new Promise((resolve, reject) => {
|
|
143
|
-
const postData = JSON.stringify(
|
|
143
|
+
const postData = JSON.stringify(buildToolResultBody(callId, result));
|
|
144
144
|
const req = transport.request({
|
|
145
145
|
hostname: url.hostname,
|
|
146
146
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
package/commands/worktree.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
6
7
|
const { stampLatestOpenBriefForWorktree } = require('../lib/brief-ledger');
|
|
7
8
|
const { isConductorArtifact } = require('../lib/conductor-artifacts');
|
|
8
9
|
|
|
@@ -321,20 +322,6 @@ function restoreRegeneratedAdapterChurn(root, message, { dryRun = false } = {})
|
|
|
321
322
|
return skipped;
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
function readFlag(args, name, fallback = '') {
|
|
325
|
-
const prefix = `${name}=`;
|
|
326
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
327
|
-
const arg = String(args[i]);
|
|
328
|
-
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
|
|
329
|
-
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
330
|
-
}
|
|
331
|
-
return fallback;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function hasFlag(args, name) {
|
|
335
|
-
return args.includes(name);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
325
|
function swarloClaim(root, { channel, taskKey, content }) {
|
|
339
326
|
const script = path.join(root, 'scripts', 'swarlo.py');
|
|
340
327
|
if (!fs.existsSync(script)) return 'skip: scripts/swarlo.py not found';
|