atris 3.32.0 → 3.33.3
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/README.md +43 -1
- package/ax +364 -11
- package/bin/atris.js +53 -4
- package/commands/autoland.js +8 -2
- package/commands/engine.js +299 -0
- package/commands/integrations.js +147 -0
- package/commands/member.js +63 -10
- package/commands/mission.js +119 -18
- package/commands/task.js +173 -14
- package/commands/truth.js +70 -10
- package/lib/fleet.js +354 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/runner-command.js +24 -0
- package/lib/task-db.js +38 -0
- package/package.json +2 -1
- package/scripts/agent_worktree.py +72 -0
package/README.md
CHANGED
|
@@ -12,6 +12,14 @@ Computer = workspace + files + tools + secrets + memory + agents + validation lo
|
|
|
12
12
|
|
|
13
13
|
A computer can be personal or shared by a business. It can run a job like code, research, CRM, reporting, event ops, support, or business ops.
|
|
14
14
|
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g atris
|
|
19
|
+
cd your-project
|
|
20
|
+
atris init && atris
|
|
21
|
+
```
|
|
22
|
+
|
|
15
23
|
## For Coding Agents
|
|
16
24
|
|
|
17
25
|
**Read [`FOR_AGENTS.md`](FOR_AGENTS.md) first** — why agents adopt Atris, 60-second start, spread-it instructions.
|
|
@@ -47,6 +55,7 @@ Then read the workspace's `atris/atris.md` and follow it exactly. `atris.md` is
|
|
|
47
55
|
| `atris/features/` | Feature packs with `idea.md`, `build.md`, `validate.md` |
|
|
48
56
|
| `atris/skills/` | Reusable skills for agents |
|
|
49
57
|
| `atris/team/` | Team member specs and local context |
|
|
58
|
+
| `atris/wiki/` | Repo memory, briefs, concepts, and wiki health |
|
|
50
59
|
|
|
51
60
|
```text
|
|
52
61
|
atris/
|
|
@@ -56,7 +65,8 @@ atris/
|
|
|
56
65
|
├── logs/
|
|
57
66
|
├── features/
|
|
58
67
|
├── skills/
|
|
59
|
-
|
|
68
|
+
├── team/
|
|
69
|
+
└── wiki/
|
|
60
70
|
```
|
|
61
71
|
|
|
62
72
|
## Install
|
|
@@ -232,6 +242,38 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
232
242
|
- `atris pull` and `atris push` sync cloud workspaces and journals
|
|
233
243
|
- `atris live` keeps a business brain fresh by checking/fixing the workspace, pushing local state, pulling cloud state, and pushing again after local changes go quiet
|
|
234
244
|
|
|
245
|
+
## Engines
|
|
246
|
+
|
|
247
|
+
An engine is the intelligence that builds a mission. Every installed headless coding CLI is a swappable worker behind one contract: a bounded prompt goes in, verified proof comes out, and engines never self-certify their own work. The house default is `atris-fast` (`ax`); the roster also covers `claude`, `codex`, `cursor`, and `devin` when their CLIs are installed.
|
|
248
|
+
|
|
249
|
+
Show the roster and the current default:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
atris engine
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Flip the default for this workspace (persists to `.atris/engine.json`):
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
atris engine cursor
|
|
259
|
+
atris engine reset # back to the house default (atris-fast)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Ride a different engine for one run only, without changing the default. `--engine <name>` works on `mission run`, `autopilot`, and `run`. Per-run flags and `ATRIS_RUNNER_PROFILE` always beat the saved file.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
atris mission run "fix the flaky login test" --engine codex
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Fleet Flight
|
|
269
|
+
|
|
270
|
+
`atris mission run --fleet` staffs every idle installed engine on the board's claimable safe-lane tasks — one mission per task, one worktree per engine. Builds run in parallel; arrivals land serially with rebase-before-ship, so a rebase conflict pauses that landing (never auto-resolved) and keeps its worktree instead of orphaning work. Each flight writes a receipt to `atris/runs/fleet-<stamp>.json`.
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
atris mission run --fleet --slots 3 # staff up to 3 engines
|
|
274
|
+
atris mission run --fleet --dry-run # preview the staffing only
|
|
275
|
+
```
|
|
276
|
+
|
|
235
277
|
## Verifiable Feedback Loop
|
|
236
278
|
|
|
237
279
|
Under the hood, Atris can keep score on real repo work.
|
package/ax
CHANGED
|
@@ -862,6 +862,19 @@ function formatAtrisGoal(goal) {
|
|
|
862
862
|
].filter(Boolean).join('\n');
|
|
863
863
|
}
|
|
864
864
|
|
|
865
|
+
function formatGoalStatusLine(goal, options = {}) {
|
|
866
|
+
if (!goal || !goal.objective) return '';
|
|
867
|
+
const status = String(goal.mission_status || '').toLowerCase();
|
|
868
|
+
if (!['active', 'running', 'planning', 'in_progress', 'ready'].includes(status)) return '';
|
|
869
|
+
const elapsed = goalElapsedSeconds(goal);
|
|
870
|
+
const objective = String(goal.objective).length > 64
|
|
871
|
+
? `${String(goal.objective).slice(0, 61)}...`
|
|
872
|
+
: String(goal.objective);
|
|
873
|
+
const parts = [`goal: ${objective}`, status];
|
|
874
|
+
if (elapsed !== null) parts.push(`${formatSeconds(elapsed)} in`);
|
|
875
|
+
return paint(`· ${parts.join(' · ')}`, [ANSI.muted], options);
|
|
876
|
+
}
|
|
877
|
+
|
|
865
878
|
function writeAtrisGoalBanner(output, cwd = process.cwd()) {
|
|
866
879
|
const text = formatAtrisGoal(currentAtrisGoal(cwd));
|
|
867
880
|
if (!text) return false;
|
|
@@ -937,28 +950,176 @@ function resetMarkdownState(state) {
|
|
|
937
950
|
state.markdownMode = 'normal';
|
|
938
951
|
state.markdownBuffer = '';
|
|
939
952
|
state.markdownCarry = '';
|
|
953
|
+
state.markdownFence = false;
|
|
954
|
+
state.markdownTable = [];
|
|
955
|
+
state.markdownLine = '';
|
|
956
|
+
state.markdownLineMode = 'start';
|
|
940
957
|
}
|
|
941
958
|
|
|
942
959
|
function ensureMarkdownState(state) {
|
|
943
960
|
if (!state.markdownMode) state.markdownMode = 'normal';
|
|
944
961
|
if (typeof state.markdownBuffer !== 'string') state.markdownBuffer = '';
|
|
945
962
|
if (typeof state.markdownCarry !== 'string') state.markdownCarry = '';
|
|
963
|
+
if (typeof state.markdownFence !== 'boolean') state.markdownFence = false;
|
|
964
|
+
if (!Array.isArray(state.markdownTable)) state.markdownTable = [];
|
|
965
|
+
if (typeof state.markdownLine !== 'string') state.markdownLine = '';
|
|
966
|
+
if (!state.markdownLineMode) state.markdownLineMode = 'start';
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function renderInlineMarkdown(text, options = {}) {
|
|
970
|
+
let rendered = String(text || '');
|
|
971
|
+
const codeSpans = [];
|
|
972
|
+
rendered = rendered.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
973
|
+
const token = `\u0000CODE${codeSpans.length}\u0000`;
|
|
974
|
+
codeSpans.push(code);
|
|
975
|
+
return token;
|
|
976
|
+
});
|
|
977
|
+
rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
978
|
+
rendered = rendered.replace(/__([^_\n]+)__/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
979
|
+
rendered = rendered.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => paint(codeSpans[Number(index)] || '', [ANSI.accent], options));
|
|
980
|
+
return rendered;
|
|
946
981
|
}
|
|
947
982
|
|
|
983
|
+
function renderMarkdownTable(lines, options = {}) {
|
|
984
|
+
const rows = [];
|
|
985
|
+
for (const line of lines) {
|
|
986
|
+
const trimmed = String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
987
|
+
const cells = trimmed.split('|').map(cell => cell.trim());
|
|
988
|
+
if (cells.length && cells.every(cell => /^:?-{2,}:?$/.test(cell))) continue;
|
|
989
|
+
rows.push(cells);
|
|
990
|
+
}
|
|
991
|
+
if (!rows.length) return '';
|
|
992
|
+
const width = Math.max(...rows.map(row => row.length));
|
|
993
|
+
const plain = value => String(value || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
994
|
+
const rendered = rows.map(row => row.map(cell => renderInlineMarkdown(cell, options)));
|
|
995
|
+
const colWidths = Array.from({ length: width }, (_, col) => (
|
|
996
|
+
Math.max(...rendered.map(row => plain(row[col] || '').length))
|
|
997
|
+
));
|
|
998
|
+
const sep = paint('│', [ANSI.muted], options);
|
|
999
|
+
return rendered.map((row, rowIndex) => {
|
|
1000
|
+
const cells = Array.from({ length: width }, (_, col) => {
|
|
1001
|
+
const cell = row[col] || '';
|
|
1002
|
+
const pad = ' '.repeat(Math.max(0, colWidths[col] - plain(cell).length));
|
|
1003
|
+
const value = `${cell}${pad}`;
|
|
1004
|
+
return rowIndex === 0 ? paint(plain(value), [ANSI.bold], options) : value;
|
|
1005
|
+
});
|
|
1006
|
+
return ` ${cells.join(` ${sep} `)}`;
|
|
1007
|
+
}).join('\n') + '\n';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function flushMarkdownTable(state, options = {}) {
|
|
1011
|
+
if (!state.markdownTable.length) return '';
|
|
1012
|
+
const table = renderMarkdownTable(state.markdownTable, options);
|
|
1013
|
+
state.markdownTable = [];
|
|
1014
|
+
return table;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function renderMarkdownLine(state, line, options = {}) {
|
|
1018
|
+
const fenceMatch = /^\s*```/.test(line);
|
|
1019
|
+
if (fenceMatch) {
|
|
1020
|
+
state.markdownFence = !state.markdownFence;
|
|
1021
|
+
const label = line.trim().slice(3).trim();
|
|
1022
|
+
if (state.markdownFence && label) return `${paint(label, [ANSI.muted], options)}\n`;
|
|
1023
|
+
return '';
|
|
1024
|
+
}
|
|
1025
|
+
if (state.markdownFence) {
|
|
1026
|
+
return ` ${paint(line, [ANSI.accent], options)}\n`;
|
|
1027
|
+
}
|
|
1028
|
+
if (/^\s*\|/.test(line)) {
|
|
1029
|
+
state.markdownTable.push(line);
|
|
1030
|
+
return '';
|
|
1031
|
+
}
|
|
1032
|
+
let out = flushMarkdownTable(state, options);
|
|
1033
|
+
const header = line.match(/^(#{1,6})\s+(.*)$/);
|
|
1034
|
+
if (header) {
|
|
1035
|
+
const codes = header[1].length <= 2 ? [ANSI.bold, ANSI.accent] : [ANSI.bold];
|
|
1036
|
+
return `${out}${paint(renderInlineMarkdown(header[2], options).replace(/\x1b\[[0-9;]*m/g, ''), codes, options)}\n`;
|
|
1037
|
+
}
|
|
1038
|
+
if (/^\s*(?:-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
|
|
1039
|
+
return `${out}${paint('─'.repeat(36), [ANSI.muted], options)}\n`;
|
|
1040
|
+
}
|
|
1041
|
+
const bullet = line.match(/^(\s*)[-*]\s+(.*)$/);
|
|
1042
|
+
if (bullet) {
|
|
1043
|
+
return `${out}${bullet[1]}${paint('•', [ANSI.bold], options)} ${renderInlineMarkdown(bullet[2], options)}\n`;
|
|
1044
|
+
}
|
|
1045
|
+
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
1046
|
+
if (quote) {
|
|
1047
|
+
return `${out}${paint('│', [ANSI.muted], options)} ${paint(renderInlineMarkdown(quote[1], options).replace(/\x1b\[[0-9;]*m/g, ''), [ANSI.muted], options)}\n`;
|
|
1048
|
+
}
|
|
1049
|
+
return `${out}${renderInlineMarkdown(line, options)}\n`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Hybrid streaming renderer. Prose streams character-live through the inline
|
|
1053
|
+
// state machine (bold/code style as delimiters close), so chat feels
|
|
1054
|
+
// immediate. Structure-shaped lines (tables, fences, headers, bullets,
|
|
1055
|
+
// quotes) buffer to the line boundary and render as blocks, because they
|
|
1056
|
+
// cannot be styled until the whole line exists. The first character of each
|
|
1057
|
+
// line decides which lane the line takes.
|
|
1058
|
+
const MARKDOWN_BLOCK_LINE_START = new Set(['|', '#', '`', '>', '-', '*']);
|
|
1059
|
+
|
|
948
1060
|
function renderStreamingMarkdown(state, text, options = {}) {
|
|
949
1061
|
ensureMarkdownState(state);
|
|
950
|
-
const input =
|
|
951
|
-
state.markdownCarry = '';
|
|
1062
|
+
const input = String(text || '');
|
|
952
1063
|
let out = '';
|
|
953
1064
|
|
|
954
1065
|
for (let i = 0; i < input.length;) {
|
|
955
1066
|
const char = input[i];
|
|
956
|
-
const next = input[i + 1];
|
|
957
1067
|
|
|
1068
|
+
// Structure lane: buffer the whole line, render at the newline.
|
|
1069
|
+
if (state.markdownFence || state.markdownLineMode === 'block') {
|
|
1070
|
+
if (char === '\n') {
|
|
1071
|
+
out += renderMarkdownLine(state, state.markdownLine, options);
|
|
1072
|
+
state.markdownLine = '';
|
|
1073
|
+
state.markdownLineMode = 'start';
|
|
1074
|
+
} else {
|
|
1075
|
+
state.markdownLine += char;
|
|
1076
|
+
}
|
|
1077
|
+
i += 1;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Line start: decide which lane this line takes.
|
|
1082
|
+
if (state.markdownLineMode === 'start') {
|
|
1083
|
+
if (char === '\n') {
|
|
1084
|
+
out += flushMarkdownTable(state, options);
|
|
1085
|
+
out += '\n';
|
|
1086
|
+
i += 1;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (MARKDOWN_BLOCK_LINE_START.has(char)) {
|
|
1090
|
+
state.markdownLineMode = 'block';
|
|
1091
|
+
state.markdownLine = char;
|
|
1092
|
+
i += 1;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
out += flushMarkdownTable(state, options);
|
|
1096
|
+
state.markdownLineMode = 'prose';
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Prose lane: the inline state machine, character-live.
|
|
1101
|
+
const next = input[i + 1];
|
|
958
1102
|
if (state.markdownMode === 'normal') {
|
|
1103
|
+
if (state.markdownCarry === '*') {
|
|
1104
|
+
state.markdownCarry = '';
|
|
1105
|
+
if (char === '*') {
|
|
1106
|
+
state.markdownMode = 'bold';
|
|
1107
|
+
state.markdownBuffer = '';
|
|
1108
|
+
i += 1;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
out += '*';
|
|
1112
|
+
}
|
|
1113
|
+
if (char === '\n') {
|
|
1114
|
+
out += '\n';
|
|
1115
|
+
state.markdownLineMode = 'start';
|
|
1116
|
+
i += 1;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
959
1119
|
if (char === '*' && next === undefined) {
|
|
960
1120
|
state.markdownCarry = '*';
|
|
961
|
-
|
|
1121
|
+
i += 1;
|
|
1122
|
+
continue;
|
|
962
1123
|
}
|
|
963
1124
|
if (char === '*' && next === '*') {
|
|
964
1125
|
state.markdownMode = 'bold';
|
|
@@ -978,23 +1139,52 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
978
1139
|
}
|
|
979
1140
|
|
|
980
1141
|
if (state.markdownMode === 'bold') {
|
|
1142
|
+
if (char === '\n') {
|
|
1143
|
+
out += `**${state.markdownBuffer}\n`;
|
|
1144
|
+
state.markdownMode = 'normal';
|
|
1145
|
+
state.markdownBuffer = '';
|
|
1146
|
+
state.markdownLineMode = 'start';
|
|
1147
|
+
i += 1;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
981
1150
|
if (char === '*' && next === undefined) {
|
|
982
1151
|
state.markdownCarry = '*';
|
|
983
|
-
|
|
1152
|
+
i += 1;
|
|
1153
|
+
continue;
|
|
984
1154
|
}
|
|
985
|
-
if (char === '*' && next === '*') {
|
|
1155
|
+
if (char === '*' && (next === '*' || state.markdownCarry === '*')) {
|
|
986
1156
|
out += paint(state.markdownBuffer, [ANSI.bold], options);
|
|
987
1157
|
state.markdownMode = 'normal';
|
|
988
1158
|
state.markdownBuffer = '';
|
|
989
|
-
|
|
1159
|
+
state.markdownCarry = '';
|
|
1160
|
+
i += next === '*' ? 2 : 1;
|
|
990
1161
|
continue;
|
|
991
1162
|
}
|
|
1163
|
+
if (state.markdownCarry === '*') {
|
|
1164
|
+
state.markdownCarry = '';
|
|
1165
|
+
if (char === '*') {
|
|
1166
|
+
out += paint(state.markdownBuffer, [ANSI.bold], options);
|
|
1167
|
+
state.markdownMode = 'normal';
|
|
1168
|
+
state.markdownBuffer = '';
|
|
1169
|
+
i += 1;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
state.markdownBuffer += '*';
|
|
1173
|
+
}
|
|
992
1174
|
state.markdownBuffer += char;
|
|
993
1175
|
i += 1;
|
|
994
1176
|
continue;
|
|
995
1177
|
}
|
|
996
1178
|
|
|
997
1179
|
if (state.markdownMode === 'code') {
|
|
1180
|
+
if (char === '\n') {
|
|
1181
|
+
out += `\`${state.markdownBuffer}\n`;
|
|
1182
|
+
state.markdownMode = 'normal';
|
|
1183
|
+
state.markdownBuffer = '';
|
|
1184
|
+
state.markdownLineMode = 'start';
|
|
1185
|
+
i += 1;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
998
1188
|
if (char === '`') {
|
|
999
1189
|
out += paint(state.markdownBuffer, [ANSI.accent], options);
|
|
1000
1190
|
state.markdownMode = 'normal';
|
|
@@ -1013,12 +1203,27 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
1013
1203
|
|
|
1014
1204
|
function flushStreamingMarkdown(state, output) {
|
|
1015
1205
|
ensureMarkdownState(state);
|
|
1016
|
-
let out =
|
|
1206
|
+
let out = '';
|
|
1207
|
+
if (state.markdownLine) {
|
|
1208
|
+
if (/^\s*```/.test(state.markdownLine)) {
|
|
1209
|
+
// A fence marker with no trailing newline is a block boundary, not code.
|
|
1210
|
+
state.markdownFence = !state.markdownFence;
|
|
1211
|
+
} else if (state.markdownFence) {
|
|
1212
|
+
out += ` ${paint(state.markdownLine, [ANSI.accent], output)}`;
|
|
1213
|
+
} else {
|
|
1214
|
+
out += renderMarkdownLine(state, state.markdownLine, output).replace(/\n$/, '');
|
|
1215
|
+
}
|
|
1216
|
+
state.markdownLine = '';
|
|
1217
|
+
}
|
|
1218
|
+
if (state.markdownCarry) {
|
|
1219
|
+
out += state.markdownCarry;
|
|
1220
|
+
}
|
|
1017
1221
|
if (state.markdownMode === 'bold' && state.markdownBuffer) {
|
|
1018
1222
|
out += paint(state.markdownBuffer, [ANSI.bold], output);
|
|
1019
1223
|
} else if (state.markdownMode === 'code' && state.markdownBuffer) {
|
|
1020
1224
|
out += paint(state.markdownBuffer, [ANSI.accent], output);
|
|
1021
1225
|
}
|
|
1226
|
+
out += flushMarkdownTable(state, output);
|
|
1022
1227
|
resetMarkdownState(state);
|
|
1023
1228
|
if (out) output.write(out);
|
|
1024
1229
|
return out;
|
|
@@ -1476,6 +1681,99 @@ function removeStoredApproval(ref, options = {}) {
|
|
|
1476
1681
|
return record;
|
|
1477
1682
|
}
|
|
1478
1683
|
|
|
1684
|
+
// Local turns stage side effects as signed artifacts in the workspace's
|
|
1685
|
+
// atris/approvals/ (atris.local_approval.v1), a different store from
|
|
1686
|
+
// ~/.atris/ax-approvals.json. ax --approve redeems them through the local
|
|
1687
|
+
// backend's approvals/execute endpoint, which re-signs with explicit user
|
|
1688
|
+
// approval and runs the gated executor.
|
|
1689
|
+
function workspaceApprovalsDir(cwd) {
|
|
1690
|
+
return path.join(cwd || process.cwd(), 'atris', 'approvals');
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function listWorkspaceApprovals(cwd) {
|
|
1694
|
+
const dir = workspaceApprovalsDir(cwd);
|
|
1695
|
+
let names = [];
|
|
1696
|
+
try {
|
|
1697
|
+
names = fs.readdirSync(dir).filter(name => name.endsWith('.json'));
|
|
1698
|
+
} catch {
|
|
1699
|
+
return [];
|
|
1700
|
+
}
|
|
1701
|
+
const records = [];
|
|
1702
|
+
for (const name of names) {
|
|
1703
|
+
const filePath = path.join(dir, name);
|
|
1704
|
+
try {
|
|
1705
|
+
const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1706
|
+
if (record && record.schema === 'atris.local_approval.v1') {
|
|
1707
|
+
records.push({ path: filePath, record, mtimeMs: fs.statSync(filePath).mtimeMs });
|
|
1708
|
+
}
|
|
1709
|
+
} catch {
|
|
1710
|
+
// Unreadable artifact: skip, never block the turn.
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return records;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function findWorkspaceApproval(ref, cwd) {
|
|
1717
|
+
const target = String(ref || '').trim();
|
|
1718
|
+
if (!target) return null;
|
|
1719
|
+
const matches = listWorkspaceApprovals(cwd)
|
|
1720
|
+
.filter(item => String(item.record.approval_id || '').startsWith(target));
|
|
1721
|
+
if (matches.length !== 1) return null;
|
|
1722
|
+
return matches[0];
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function pendingWorkspaceApprovalsSince(cwd, sinceMs) {
|
|
1726
|
+
return listWorkspaceApprovals(cwd).filter(item => (
|
|
1727
|
+
item.mtimeMs >= sinceMs
|
|
1728
|
+
&& String(item.record.status || '') === 'pending'
|
|
1729
|
+
&& !(item.record.decision && item.record.decision.approved)
|
|
1730
|
+
));
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
async function approveWorkspaceApproval(ref, options = {}) {
|
|
1734
|
+
const cwd = options.cwd || process.cwd();
|
|
1735
|
+
const found = findWorkspaceApproval(ref, cwd);
|
|
1736
|
+
if (!found) throw new Error(`Pending approval not found: ${ref}`);
|
|
1737
|
+
const output = options.output || process.stdout;
|
|
1738
|
+
const { record } = found;
|
|
1739
|
+
const startedAt = Date.now();
|
|
1740
|
+
const payload = record.payload && typeof record.payload === 'object' ? record.payload : {};
|
|
1741
|
+
output.write(`${formatAuxRow('id', record.approval_id, output)}\n`);
|
|
1742
|
+
if (record.summary) output.write(`${formatAuxRow('action', record.summary, output)}\n`);
|
|
1743
|
+
if (payload.command) output.write(`${formatAuxRow('command', payload.command, output)}\n`);
|
|
1744
|
+
output.write('Accepted.\n');
|
|
1745
|
+
const response = await postJson(APPROVAL_EXECUTE_PATH, {
|
|
1746
|
+
model: modelForMode(options.mode || 'fast'),
|
|
1747
|
+
approval_request: {
|
|
1748
|
+
action_type: record.action_type,
|
|
1749
|
+
approval_id: record.approval_id,
|
|
1750
|
+
summary: record.summary,
|
|
1751
|
+
payload: { ...payload, workspace: payload.workspace || cwd },
|
|
1752
|
+
},
|
|
1753
|
+
}, { route: 'local' });
|
|
1754
|
+
const execution = response && response.data && typeof response.data === 'object' ? response.data : {};
|
|
1755
|
+
const executed = Boolean(execution.execution && execution.execution.executed) || execution.status === 'executed';
|
|
1756
|
+
const statusText = executed
|
|
1757
|
+
? 'Done.'
|
|
1758
|
+
: `Not executed: ${String(execution.status || response.error || `HTTP ${response.status || 0}`)}`;
|
|
1759
|
+
output.write(`${formatAuxRow(executed ? 'done' : 'wait', statusText, output)}\n`);
|
|
1760
|
+
try {
|
|
1761
|
+
const spent = { ...record, status: executed ? 'executed' : record.status, executed_at: executed ? new Date().toISOString() : undefined };
|
|
1762
|
+
fs.writeFileSync(found.path, `${JSON.stringify(spent, null, 2)}\n`);
|
|
1763
|
+
} catch {
|
|
1764
|
+
// Bookkeeping only; the backend holds replay protection.
|
|
1765
|
+
}
|
|
1766
|
+
output.write(`${formatDoneLine(Date.now() - startedAt)}\n`);
|
|
1767
|
+
if (!response.ok && !executed) process.exitCode = 1;
|
|
1768
|
+
return { response, executed };
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function writeWorkspaceApprovalHints(cwd, sinceMs, output) {
|
|
1772
|
+
for (const item of pendingWorkspaceApprovalsSince(cwd, sinceMs)) {
|
|
1773
|
+
output.write(`${formatAuxRow('approve', formatApprovalCommand(item.record.approval_id), output)}\n`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1479
1777
|
function formatStoredApprovals(store, options = {}) {
|
|
1480
1778
|
const approvals = Array.isArray(store && store.approvals) ? store.approvals : [];
|
|
1481
1779
|
if (!approvals.length) return 'No pending approvals.';
|
|
@@ -1762,8 +2060,30 @@ function writeAuxLine(state, output, line) {
|
|
|
1762
2060
|
state.needsBullet = true;
|
|
1763
2061
|
}
|
|
1764
2062
|
|
|
2063
|
+
// Turns beyond the recent window used to fall off a cliff: turn 9 of a chat
|
|
2064
|
+
// silently forgot turn 1 and never said so. Evicted turns now compress into
|
|
2065
|
+
// a digest that rides as the first previous message, so long chats keep the
|
|
2066
|
+
// thread of the whole conversation. AX_NO_COMPACT=1 restores the bare window.
|
|
2067
|
+
const HISTORY_DIGEST_MAX_CHARS = 2400;
|
|
2068
|
+
|
|
2069
|
+
function compactedHistoryDigest(evicted) {
|
|
2070
|
+
const lines = [];
|
|
2071
|
+
for (const turn of evicted) {
|
|
2072
|
+
const who = turn && turn.role === 'assistant' ? 'atris' : 'user';
|
|
2073
|
+
const text = String((turn && turn.content) || '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
2074
|
+
if (text) lines.push(`${who}: ${text}`);
|
|
2075
|
+
}
|
|
2076
|
+
let digest = lines.join('\n');
|
|
2077
|
+
if (digest.length > HISTORY_DIGEST_MAX_CHARS) {
|
|
2078
|
+
digest = digest.slice(digest.length - HISTORY_DIGEST_MAX_CHARS);
|
|
2079
|
+
digest = digest.slice(digest.indexOf('\n') + 1);
|
|
2080
|
+
}
|
|
2081
|
+
return digest;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1765
2084
|
function structuredHistory(history, limit = 8) {
|
|
1766
|
-
|
|
2085
|
+
const rows = Array.isArray(history) ? history : [];
|
|
2086
|
+
const recent = rows
|
|
1767
2087
|
.slice(-limit)
|
|
1768
2088
|
.map(turn => {
|
|
1769
2089
|
const message = {
|
|
@@ -1776,6 +2096,16 @@ function structuredHistory(history, limit = 8) {
|
|
|
1776
2096
|
return message;
|
|
1777
2097
|
})
|
|
1778
2098
|
.filter(turn => turn.content.trim());
|
|
2099
|
+
if (rows.length <= limit || process.env.AX_NO_COMPACT === '1') return recent;
|
|
2100
|
+
const digest = compactedHistoryDigest(rows.slice(0, rows.length - limit));
|
|
2101
|
+
if (!digest) return recent;
|
|
2102
|
+
return [
|
|
2103
|
+
{
|
|
2104
|
+
role: 'user',
|
|
2105
|
+
content: `[conversation so far, compacted from ${rows.length - limit} earlier turns]\n${digest}`,
|
|
2106
|
+
},
|
|
2107
|
+
...recent,
|
|
2108
|
+
];
|
|
1779
2109
|
}
|
|
1780
2110
|
|
|
1781
2111
|
function latestPendingTaskPreview(history = []) {
|
|
@@ -2300,6 +2630,7 @@ async function chat(options = {}) {
|
|
|
2300
2630
|
const logger = createRunLogger({ cwd, mode, kind: 'play', output: baseOutput });
|
|
2301
2631
|
const output = logger ? logger.output : baseOutput;
|
|
2302
2632
|
const history = [];
|
|
2633
|
+
let lastCompactionNotice = 0;
|
|
2303
2634
|
const conversationId = options.conversationId || `ax-${process.pid}-${Date.now().toString(36)}`;
|
|
2304
2635
|
|
|
2305
2636
|
output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
|
|
@@ -2444,6 +2775,12 @@ async function chat(options = {}) {
|
|
|
2444
2775
|
if (!(await ensureRuntimeReady(turnRoute))) return false;
|
|
2445
2776
|
const turnFunction = options.turnFunction || turnFunctionForMode(mode);
|
|
2446
2777
|
const turnId = options.turnId || createTurnId();
|
|
2778
|
+
const turnStartedAt = Date.now();
|
|
2779
|
+
const compactedTurns = process.env.AX_NO_COMPACT === '1' ? 0 : Math.max(0, history.length - 8);
|
|
2780
|
+
if (compactedTurns > 0 && compactedTurns !== lastCompactionNotice) {
|
|
2781
|
+
lastCompactionNotice = compactedTurns;
|
|
2782
|
+
output.write(`${paint(`· context: ${compactedTurns} earlier turn${compactedTurns === 1 ? '' : 's'} compacted into a digest`, [ANSI.muted], output)}\n`);
|
|
2783
|
+
}
|
|
2447
2784
|
const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId, turnId });
|
|
2448
2785
|
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2449
2786
|
const approvalExecution = normalizeMode(mode) === 'code-fast'
|
|
@@ -2453,7 +2790,11 @@ async function chat(options = {}) {
|
|
|
2453
2790
|
output,
|
|
2454
2791
|
...(options.postApproval ? { postApproval: options.postApproval } : {}),
|
|
2455
2792
|
});
|
|
2456
|
-
|
|
2793
|
+
if (!options.turnFunction) writeWorkspaceApprovalHints(cwd, turnStartedAt, output);
|
|
2794
|
+
output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n`);
|
|
2795
|
+
const goalLine = options.turnFunction ? '' : formatGoalStatusLine(currentAtrisGoal(cwd), output);
|
|
2796
|
+
if (goalLine) output.write(`${goalLine}\n`);
|
|
2797
|
+
output.write('\n');
|
|
2457
2798
|
const receipt = receiptFromState(result);
|
|
2458
2799
|
const approvalRequest = approvalExecution
|
|
2459
2800
|
? (
|
|
@@ -3365,7 +3706,11 @@ async function main() {
|
|
|
3365
3706
|
}
|
|
3366
3707
|
|
|
3367
3708
|
if (approveId) {
|
|
3368
|
-
|
|
3709
|
+
if (findStoredApproval(approveId)) {
|
|
3710
|
+
await approveStoredApproval(approveId, { mode, output: process.stdout });
|
|
3711
|
+
} else {
|
|
3712
|
+
await approveWorkspaceApproval(approveId, { cwd: process.cwd(), mode, output: process.stdout });
|
|
3713
|
+
}
|
|
3369
3714
|
return;
|
|
3370
3715
|
}
|
|
3371
3716
|
|
|
@@ -3383,6 +3728,7 @@ async function main() {
|
|
|
3383
3728
|
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
|
|
3384
3729
|
console.log('');
|
|
3385
3730
|
maybeWriteAtrisGoalBanner(process.stdout, process.cwd());
|
|
3731
|
+
const turnStartedAt = Date.now();
|
|
3386
3732
|
const result = await turnFunctionForMode(mode)(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
|
|
3387
3733
|
const receipt = receiptFromState(result);
|
|
3388
3734
|
const approvalRequest = normalizeMode(mode) === 'code-fast' ? null : approvalRequestFromReceipt(receipt);
|
|
@@ -3390,6 +3736,9 @@ async function main() {
|
|
|
3390
3736
|
? persistPendingApproval(receipt, approvalRequest, { cwd: process.cwd(), mode })
|
|
3391
3737
|
: null;
|
|
3392
3738
|
if (approvalRecord) console.log(formatAuxRow('approve', formatApprovalCommand(approvalRecord.id), process.stdout));
|
|
3739
|
+
// A one-shot has no next turn, so "reply yes to approve" is a dead end;
|
|
3740
|
+
// surface the exact redeem command for anything the turn staged on disk.
|
|
3741
|
+
writeWorkspaceApprovalHints(process.cwd(), turnStartedAt, process.stdout);
|
|
3393
3742
|
console.log('');
|
|
3394
3743
|
console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
|
|
3395
3744
|
} catch (error) {
|
|
@@ -3417,6 +3766,10 @@ module.exports = {
|
|
|
3417
3766
|
approvalRequestFromReceipt,
|
|
3418
3767
|
approvalStorePath,
|
|
3419
3768
|
approveStoredApproval,
|
|
3769
|
+
approveWorkspaceApproval,
|
|
3770
|
+
findWorkspaceApproval,
|
|
3771
|
+
listWorkspaceApprovals,
|
|
3772
|
+
pendingWorkspaceApprovalsSince,
|
|
3420
3773
|
backendBaseUrl,
|
|
3421
3774
|
backendStartCommand,
|
|
3422
3775
|
backendUrl,
|