atris 3.30.8 → 3.31.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 +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +4 -2
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +7 -0
- package/ax +367 -93
- package/bin/atris.js +317 -155
- package/commands/autoland.js +319 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +1 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +519 -19
- package/commands/mission.js +3659 -282
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run.js +3 -3
- package/commands/strings.js +301 -0
- package/commands/task.js +575 -102
- package/commands/truth.js +170 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +283 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/mission-runtime-loop.js +320 -0
- package/lib/next-moves.js +212 -6
- package/lib/pulse.js +74 -1
- package/lib/runner-command.js +20 -8
- package/lib/runs-prune.js +242 -0
- package/lib/task-proof.js +1 -1
- package/package.json +3 -3
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
package/commands/codex-goal.js
CHANGED
|
@@ -32,7 +32,50 @@ function sqlString(value) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function resolveStatePath(args = []) {
|
|
35
|
-
|
|
35
|
+
const explicit = readFlag(args, '--state', process.env.CODEX_STATE_DB || '');
|
|
36
|
+
if (explicit) return path.resolve(expandHome(explicit));
|
|
37
|
+
// Codex moved native goals into ~/.codex/goals_1.sqlite; older builds kept them in state_5.sqlite.
|
|
38
|
+
// Prefer the live goals DB so the bridge sees real goal activity, fall back to the legacy state DB.
|
|
39
|
+
const goalsDb = path.join(os.homedir(), '.codex', 'goals_1.sqlite');
|
|
40
|
+
const legacyDb = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
41
|
+
return path.resolve(fs.existsSync(goalsDb) ? goalsDb : legacyDb);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Thread metadata (cwd/title) stayed in state_5.sqlite even after goals moved to goals_1.sqlite.
|
|
45
|
+
function resolveThreadsPath(args = []) {
|
|
46
|
+
const explicit = readFlag(args, '--threads-db', process.env.CODEX_THREADS_DB || '');
|
|
47
|
+
if (explicit) return path.resolve(expandHome(explicit));
|
|
48
|
+
const legacyDb = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
49
|
+
return fs.existsSync(legacyDb) ? path.resolve(legacyDb) : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tableExists(dbPath, table) {
|
|
53
|
+
if (!dbPath || !fs.existsSync(dbPath)) return false;
|
|
54
|
+
try {
|
|
55
|
+
const rows = runSqliteJson(dbPath, `SELECT name FROM sqlite_master WHERE type='table' AND name=${sqlString(table)} LIMIT 1;`);
|
|
56
|
+
return rows.length > 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Goals live in goals_1.sqlite (no `threads` table); thread metadata lives in state_5.sqlite.
|
|
63
|
+
// Legacy single-file layouts (and test fixtures) keep both in one DB. Resolve which join to use.
|
|
64
|
+
function goalQueryContext(args = []) {
|
|
65
|
+
const goalsDb = resolveStatePath(args);
|
|
66
|
+
if (tableExists(goalsDb, 'threads')) {
|
|
67
|
+
return { goalsDb, threadsTable: 'threads', prefix: '' };
|
|
68
|
+
}
|
|
69
|
+
const threadsDb = resolveThreadsPath(args);
|
|
70
|
+
if (threadsDb && threadsDb !== goalsDb && tableExists(threadsDb, 'threads')) {
|
|
71
|
+
return { goalsDb, threadsTable: 'tdb.threads', prefix: `ATTACH ${sqlString(threadsDb)} AS tdb;\n` };
|
|
72
|
+
}
|
|
73
|
+
return { goalsDb, threadsTable: null, prefix: '' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runGoalQuery(args, buildSql) {
|
|
77
|
+
const ctx = goalQueryContext(args);
|
|
78
|
+
return runSqliteJson(ctx.goalsDb, ctx.prefix + buildSql(ctx.threadsTable));
|
|
36
79
|
}
|
|
37
80
|
|
|
38
81
|
function defaultRunsDir(args = []) {
|
|
@@ -81,7 +124,11 @@ function ensureStateDb(dbPath) {
|
|
|
81
124
|
}
|
|
82
125
|
}
|
|
83
126
|
|
|
84
|
-
function selectGoalSql(whereClause, limit = 10) {
|
|
127
|
+
function selectGoalSql(whereClause, limit = 10, threadsTable = 'threads') {
|
|
128
|
+
const join = threadsTable ? `LEFT JOIN ${threadsTable} t ON t.id = tg.thread_id` : '';
|
|
129
|
+
const cwd = threadsTable ? 't.cwd' : 'NULL';
|
|
130
|
+
const title = threadsTable ? 't.title' : 'NULL';
|
|
131
|
+
const threadUpdated = threadsTable ? 't.updated_at_ms' : 'NULL';
|
|
85
132
|
return `
|
|
86
133
|
SELECT
|
|
87
134
|
tg.thread_id,
|
|
@@ -93,41 +140,43 @@ SELECT
|
|
|
93
140
|
tg.time_used_seconds,
|
|
94
141
|
tg.created_at_ms,
|
|
95
142
|
tg.updated_at_ms,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
${cwd} AS thread_cwd,
|
|
144
|
+
${title} AS thread_title,
|
|
145
|
+
${threadUpdated} AS thread_updated_at_ms
|
|
99
146
|
FROM thread_goals tg
|
|
100
|
-
|
|
147
|
+
${join}
|
|
101
148
|
${whereClause}
|
|
102
|
-
ORDER BY COALESCE(
|
|
149
|
+
ORDER BY COALESCE(${threadUpdated}, tg.updated_at_ms) DESC
|
|
103
150
|
LIMIT ${Number(limit) || 10}
|
|
104
151
|
`;
|
|
105
152
|
}
|
|
106
153
|
|
|
107
|
-
function readGoalByThread(
|
|
108
|
-
const rows =
|
|
154
|
+
function readGoalByThread(args, threadId) {
|
|
155
|
+
const rows = runGoalQuery(args, (tt) => selectGoalSql(`WHERE tg.thread_id = ${sqlString(threadId)}`, 1, tt));
|
|
109
156
|
return rows[0] || null;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
|
-
function readLatestGoalForCwd(
|
|
159
|
+
function readLatestGoalForCwd(args, cwd) {
|
|
160
|
+
const ctx = goalQueryContext(args);
|
|
161
|
+
if (!ctx.threadsTable) return null; // cannot cwd-match without thread metadata
|
|
113
162
|
const realCwd = fs.realpathSync.native ? fs.realpathSync.native(cwd) : fs.realpathSync(cwd);
|
|
114
163
|
const pwd = process.env.PWD || '';
|
|
115
164
|
const pwdReal = pwd && fs.existsSync(pwd) ? (fs.realpathSync.native ? fs.realpathSync.native(pwd) : fs.realpathSync(pwd)) : '';
|
|
116
165
|
const candidates = [...new Set([cwd, realCwd, pwdReal === realCwd ? pwd : ''].filter(Boolean))];
|
|
117
|
-
const rows = runSqliteJson(
|
|
166
|
+
const rows = runSqliteJson(ctx.goalsDb, ctx.prefix + selectGoalSql(`WHERE t.cwd IN (${candidates.map(sqlString).join(', ')})`, 1, ctx.threadsTable));
|
|
118
167
|
return rows[0] || null;
|
|
119
168
|
}
|
|
120
169
|
|
|
121
|
-
function readRecentGoals(
|
|
122
|
-
return
|
|
170
|
+
function readRecentGoals(args, limit = 10) {
|
|
171
|
+
return runGoalQuery(args, (tt) => selectGoalSql('', limit, tt));
|
|
123
172
|
}
|
|
124
173
|
|
|
125
|
-
function resolveThreadGoal(
|
|
174
|
+
function resolveThreadGoal(args) {
|
|
126
175
|
const explicitThread = readFlag(args, '--thread', '');
|
|
127
|
-
if (explicitThread) return readGoalByThread(
|
|
128
|
-
if (hasFlag(args, '--latest')) return readLatestGoalForCwd(
|
|
176
|
+
if (explicitThread) return readGoalByThread(args, explicitThread);
|
|
177
|
+
if (hasFlag(args, '--latest')) return readLatestGoalForCwd(args, process.cwd());
|
|
129
178
|
const envThread = process.env.CODEX_THREAD_ID || '';
|
|
130
|
-
if (envThread) return readGoalByThread(
|
|
179
|
+
if (envThread) return readGoalByThread(args, envThread);
|
|
131
180
|
return null;
|
|
132
181
|
}
|
|
133
182
|
|
|
@@ -163,7 +212,7 @@ function statusCommand(args) {
|
|
|
163
212
|
const dbPath = resolveStatePath(args);
|
|
164
213
|
ensureStateDb(dbPath);
|
|
165
214
|
|
|
166
|
-
const goal = resolveThreadGoal(
|
|
215
|
+
const goal = resolveThreadGoal(args);
|
|
167
216
|
if (goal) {
|
|
168
217
|
const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goal };
|
|
169
218
|
printJsonOrText(payload, [
|
|
@@ -176,7 +225,7 @@ function statusCommand(args) {
|
|
|
176
225
|
}
|
|
177
226
|
|
|
178
227
|
const limit = Math.max(1, Math.min(50, Number(readFlag(args, '--limit', '10')) || 10));
|
|
179
|
-
const goals = readRecentGoals(
|
|
228
|
+
const goals = readRecentGoals(args, limit);
|
|
180
229
|
const payload = { ok: true, schema: SCHEMA, action: 'status', state_path: dbPath, goals };
|
|
181
230
|
printJsonOrText(payload, [
|
|
182
231
|
`Codex goals: ${goals.length} recent`,
|
|
@@ -192,7 +241,7 @@ function resetCommand(args) {
|
|
|
192
241
|
ensureStateDb(dbPath);
|
|
193
242
|
|
|
194
243
|
const startedAt = new Date().toISOString();
|
|
195
|
-
const goal = resolveThreadGoal(
|
|
244
|
+
const goal = resolveThreadGoal(args);
|
|
196
245
|
if (!goal) {
|
|
197
246
|
throw new Error('No Codex goal found. Pass --thread <thread-id> or --latest.');
|
|
198
247
|
}
|
|
@@ -238,7 +287,7 @@ SELECT changes() AS deleted;
|
|
|
238
287
|
COMMIT;
|
|
239
288
|
`, { readonly: false });
|
|
240
289
|
const deleted = Number(rows[0]?.deleted || 0);
|
|
241
|
-
const remaining = readGoalByThread(
|
|
290
|
+
const remaining = readGoalByThread(args, goal.thread_id);
|
|
242
291
|
const ok = deleted === 1 && !remaining;
|
|
243
292
|
const payload = writeReceipt(outDir, {
|
|
244
293
|
ok,
|
|
@@ -277,7 +326,8 @@ function usage() {
|
|
|
277
326
|
` atris codex-goal reset --thread <id> ${CONFIRM_RESET_FLAG}`,
|
|
278
327
|
'',
|
|
279
328
|
'Flags:',
|
|
280
|
-
' --state <path> Codex
|
|
329
|
+
' --state <path> Codex goals DB (default ~/.codex/goals_1.sqlite, falls back to state_5.sqlite)',
|
|
330
|
+
' --threads-db <path> Codex thread metadata DB for cwd/title (default ~/.codex/state_5.sqlite)',
|
|
281
331
|
' --latest Use the latest Codex goal whose thread cwd matches the current directory',
|
|
282
332
|
' --out-dir <path> Receipt/backup directory (default .atris/runs)',
|
|
283
333
|
'',
|
package/commands/computer.js
CHANGED
|
@@ -1635,6 +1635,41 @@ async function resolveBusinessContextBySlug(token, slug, options = {}) {
|
|
|
1635
1635
|
return null;
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
|
+
function shortcutBusinessLabel(slug, fallbackName) {
|
|
1639
|
+
if (normalizeBusinessSlug(slug) === RECRUITING_BUSINESS_SLUG) return 'Atris Labs';
|
|
1640
|
+
return fallbackName || slug;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async function resolveSingleBusinessShortcutFallback(token, slug) {
|
|
1644
|
+
const wantedSlug = normalizeBusinessSlug(slug);
|
|
1645
|
+
if (wantedSlug !== RECRUITING_BUSINESS_SLUG) return null;
|
|
1646
|
+
|
|
1647
|
+
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
1648
|
+
const rows = list.ok && Array.isArray(list.data)
|
|
1649
|
+
? list.data.filter((business) => business && business.id)
|
|
1650
|
+
: [];
|
|
1651
|
+
if (rows.length !== 1) return null;
|
|
1652
|
+
|
|
1653
|
+
const match = rows[0];
|
|
1654
|
+
const businesses = loadBusinesses();
|
|
1655
|
+
const businessName = shortcutBusinessLabel(wantedSlug, match.name || match.slug);
|
|
1656
|
+
businesses[wantedSlug] = {
|
|
1657
|
+
business_id: match.id,
|
|
1658
|
+
workspace_id: match.workspace_id,
|
|
1659
|
+
name: businessName,
|
|
1660
|
+
slug: wantedSlug,
|
|
1661
|
+
cloud_slug: match.slug || null,
|
|
1662
|
+
added_at: new Date().toISOString(),
|
|
1663
|
+
};
|
|
1664
|
+
saveBusinesses(businesses);
|
|
1665
|
+
return {
|
|
1666
|
+
slug: wantedSlug,
|
|
1667
|
+
businessId: match.id,
|
|
1668
|
+
workspaceId: match.workspace_id,
|
|
1669
|
+
businessName,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1638
1673
|
async function resolveComputerCommandContext(token, options = {}) {
|
|
1639
1674
|
if (options.businessSlug || options.workspaceId) {
|
|
1640
1675
|
const ctx = options.businessSlug
|
|
@@ -1660,7 +1695,10 @@ async function resolveTypedBusinessComputerContext(token, options = {}, defaults
|
|
|
1660
1695
|
return resolveComputerCommandContext(token, { ...options, businessSlug });
|
|
1661
1696
|
}
|
|
1662
1697
|
|
|
1663
|
-
|
|
1698
|
+
let ctx = await resolveBusinessContextBySlug(token, businessSlug, { preferCache: true });
|
|
1699
|
+
if (!ctx?.businessId && (options.allowCanonicalShortcutFallback || defaults.allowCanonicalShortcutFallback)) {
|
|
1700
|
+
ctx = await resolveSingleBusinessShortcutFallback(token, businessSlug);
|
|
1701
|
+
}
|
|
1664
1702
|
if (!ctx?.businessId) return null;
|
|
1665
1703
|
const workspaces = await listBusinessWorkspaces(token, ctx);
|
|
1666
1704
|
const workspace = resolveWorkspaceByComputerType(workspaces, computerType);
|
|
@@ -1705,7 +1743,7 @@ async function resolveWorkspaceSelector(token, ctx, input) {
|
|
|
1705
1743
|
return workspace?.id || selector;
|
|
1706
1744
|
}
|
|
1707
1745
|
|
|
1708
|
-
async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
|
|
1746
|
+
async function resolveBusinessOwnerForCreate(token, businessSlug = null, options = {}) {
|
|
1709
1747
|
const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
|
|
1710
1748
|
if (wantedSlug) {
|
|
1711
1749
|
const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
|
|
@@ -1720,6 +1758,9 @@ async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
|
|
|
1720
1758
|
businessName: cached.name || cached.slug || wantedSlug,
|
|
1721
1759
|
};
|
|
1722
1760
|
}
|
|
1761
|
+
if (options.allowCanonicalShortcutFallback) {
|
|
1762
|
+
return resolveSingleBusinessShortcutFallback(token, wantedSlug);
|
|
1763
|
+
}
|
|
1723
1764
|
return null;
|
|
1724
1765
|
}
|
|
1725
1766
|
|
|
@@ -2248,7 +2289,9 @@ async function computerCreate(token, args = [], defaults = {}) {
|
|
|
2248
2289
|
return;
|
|
2249
2290
|
}
|
|
2250
2291
|
|
|
2251
|
-
const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug
|
|
2292
|
+
const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug, {
|
|
2293
|
+
allowCanonicalShortcutFallback: Boolean(defaults.allowCanonicalShortcutFallback),
|
|
2294
|
+
});
|
|
2252
2295
|
if (!ctx?.businessId) {
|
|
2253
2296
|
console.error('No business found.');
|
|
2254
2297
|
console.error('Run inside a bound business workspace or pass: --business <slug>');
|
|
@@ -3874,6 +3917,7 @@ async function runRecruitingComputerShortcut(token, args, cloudOptions = {}) {
|
|
|
3874
3917
|
return computerCreate(token, finalArgs, {
|
|
3875
3918
|
businessSlug: cloudOptions.businessSlug || RECRUITING_BUSINESS_SLUG,
|
|
3876
3919
|
computerType: 'recruiting',
|
|
3920
|
+
allowCanonicalShortcutFallback: !cloudOptions.businessSlug,
|
|
3877
3921
|
});
|
|
3878
3922
|
}
|
|
3879
3923
|
|
|
@@ -3888,6 +3932,7 @@ async function runRecruitingComputerShortcut(token, args, cloudOptions = {}) {
|
|
|
3888
3932
|
const ctx = await resolveTypedBusinessComputerContext(token, recruitingOptions, {
|
|
3889
3933
|
businessSlug: RECRUITING_BUSINESS_SLUG,
|
|
3890
3934
|
computerType: 'recruiting',
|
|
3935
|
+
allowCanonicalShortcutFallback: !recruitingOptions.businessSlug,
|
|
3891
3936
|
});
|
|
3892
3937
|
if (!ctx?.businessId) {
|
|
3893
3938
|
console.error('Atris Recruiting is not available for this account.');
|
package/commands/gm.js
CHANGED
|
@@ -331,7 +331,7 @@ function gmState(args = []) {
|
|
|
331
331
|
missions,
|
|
332
332
|
review_queue: reviewQueue,
|
|
333
333
|
next_commands: commands,
|
|
334
|
-
xp_rule: 'GM can route missions and review proof, but AgentXP
|
|
334
|
+
xp_rule: 'GM can route missions and review proof, but AgentXP is awarded only after human approval.',
|
|
335
335
|
global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
|
|
336
336
|
leaderboard_url: AGENTXP_LEADERBOARD_URL,
|
|
337
337
|
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {
|
|
4
|
+
isGenericInboxPlaceholder,
|
|
5
|
+
seedInboxFromMove,
|
|
6
|
+
} = require('../lib/next-moves');
|
|
7
|
+
|
|
8
|
+
function hasFlag(args, name) {
|
|
9
|
+
return args.includes(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeRead(file) {
|
|
13
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function walkFiles(dir, predicate, limit = 200) {
|
|
17
|
+
const out = [];
|
|
18
|
+
const visit = (current) => {
|
|
19
|
+
if (out.length >= limit) return;
|
|
20
|
+
let entries = [];
|
|
21
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (out.length >= limit) break;
|
|
24
|
+
const full = path.join(current, entry.name);
|
|
25
|
+
if (entry.isDirectory()) visit(full);
|
|
26
|
+
else if (!predicate || predicate(full)) out.push(full);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
visit(dir);
|
|
30
|
+
return out.sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clip(value, max = 140) {
|
|
34
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
if (text.length <= max) return text;
|
|
36
|
+
return `${text.slice(0, max - 3).trim()}...`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addAction(actions, title, source, reason, evidence = null) {
|
|
40
|
+
const clean = clip(title, 180);
|
|
41
|
+
if (!clean || isGenericInboxPlaceholder(clean)) return;
|
|
42
|
+
const key = clean.toLowerCase();
|
|
43
|
+
if (actions.some((action) => action.key === key)) return;
|
|
44
|
+
actions.push({
|
|
45
|
+
key,
|
|
46
|
+
title: clean,
|
|
47
|
+
source,
|
|
48
|
+
reason: clip(reason, 220),
|
|
49
|
+
evidence,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function receiptActions(root, actions) {
|
|
54
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
55
|
+
for (const file of walkFiles(runsDir, (full) => /\.json$/i.test(full), 300).slice(-80)) {
|
|
56
|
+
let receipt = null;
|
|
57
|
+
try { receipt = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { receipt = null; }
|
|
58
|
+
if (!receipt || receipt.schema !== 'atris.mission_receipt.v1') continue;
|
|
59
|
+
const rel = path.relative(root, file);
|
|
60
|
+
const landing = receipt.result?.landing || {};
|
|
61
|
+
const text = [
|
|
62
|
+
receipt.objective,
|
|
63
|
+
landing.changed,
|
|
64
|
+
landing.reason,
|
|
65
|
+
landing.checked,
|
|
66
|
+
landing.tested,
|
|
67
|
+
landing.next,
|
|
68
|
+
receipt.result?.reason,
|
|
69
|
+
].filter(Boolean).join(' ');
|
|
70
|
+
if (/no concrete follow-up|stop|placeholder|zero value score|near[- ]?miss/i.test(text)) {
|
|
71
|
+
addAction(actions, 'Improve the next-mission chooser so it explains stopped work and better candidates', 'receipt', text, rel);
|
|
72
|
+
}
|
|
73
|
+
if (receipt.result?.verifier_result?.passed === false || /\b(verifier|test|check).{0,40}\b(fail|failed|error)\b/i.test(text)) {
|
|
74
|
+
addAction(actions, `Fix failing verifier for ${receipt.objective || 'latest mission'}`, 'receipt', text, rel);
|
|
75
|
+
}
|
|
76
|
+
if (landing.reason && landing.proof && /receipt saved/i.test(landing.proof)) {
|
|
77
|
+
addAction(actions, 'Surface buried result landing reasoning in the normal mission view', 'receipt', landing.reason, rel);
|
|
78
|
+
}
|
|
79
|
+
if (landing.proof == null || /proof:\s*null/i.test(text)) {
|
|
80
|
+
addAction(actions, 'Repair proof mismatch where proof exists in metadata but renders as null', 'receipt', text, rel);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runLogActions(root, actions) {
|
|
86
|
+
const logsDir = path.join(root, 'atris', 'logs', 'runs');
|
|
87
|
+
for (const file of walkFiles(logsDir, (full) => /\.(log|txt|md|json)$/i.test(full), 200).slice(-60)) {
|
|
88
|
+
const rel = path.relative(root, file);
|
|
89
|
+
const text = safeRead(file);
|
|
90
|
+
if (!text) continue;
|
|
91
|
+
if (/no concrete follow-up|placeholder|test idea|zero value score/i.test(text)) {
|
|
92
|
+
addAction(actions, 'Harvest weak chooser traces into a concrete next-mission fix', 'runlog', text, rel);
|
|
93
|
+
}
|
|
94
|
+
if (/\b(error|failed|exception|traceback|unhandled)\b/i.test(text)) {
|
|
95
|
+
addAction(actions, 'Turn recent run-log failures into a bounded fix task with proof', 'runlog', text, rel);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function thinkingActions(root, actions) {
|
|
101
|
+
const rel = 'atris/thinking.md';
|
|
102
|
+
const text = safeRead(path.join(root, rel));
|
|
103
|
+
if (!text) return;
|
|
104
|
+
if (/plain English|jargon|feynman|understand/i.test(text)) {
|
|
105
|
+
addAction(actions, 'Make proof and task previews plain enough to understand without opening logs', 'thinking', text, rel);
|
|
106
|
+
}
|
|
107
|
+
if (/proof over motion|receipt|verified|checked/i.test(text)) {
|
|
108
|
+
addAction(actions, 'Audit proof surfaces for null proof, hidden receipts, and vague verifier claims', 'thinking', text, rel);
|
|
109
|
+
}
|
|
110
|
+
if (/stop.*no concrete|no concrete.*stop|fake|busywork/i.test(text)) {
|
|
111
|
+
addAction(actions, 'Make autonomous loops stop or ask instead of inventing fake work', 'thinking', text, rel);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function harvestActions(root = process.cwd()) {
|
|
116
|
+
const actions = [];
|
|
117
|
+
receiptActions(root, actions);
|
|
118
|
+
runLogActions(root, actions);
|
|
119
|
+
thinkingActions(root, actions);
|
|
120
|
+
return actions.slice(0, 12).map(({ key, ...action }, index) => ({
|
|
121
|
+
id: `I${index + 1}`,
|
|
122
|
+
...action,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function harvestCommand(args = []) {
|
|
127
|
+
const root = process.cwd();
|
|
128
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
|
|
129
|
+
console.log('Usage: atris harvest [--write] [--json]');
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log('Find plain next actions from mission receipts, run logs, and thinking.md.');
|
|
132
|
+
console.log('Read-only by default. Use --write to append actions to today\'s Inbox.');
|
|
133
|
+
return { ok: true, action: 'help' };
|
|
134
|
+
}
|
|
135
|
+
const asJson = hasFlag(args, '--json');
|
|
136
|
+
const write = hasFlag(args, '--write');
|
|
137
|
+
const actions = harvestActions(root);
|
|
138
|
+
const writes = [];
|
|
139
|
+
if (write) {
|
|
140
|
+
for (const action of actions) {
|
|
141
|
+
const written = seedInboxFromMove(root, { title: action.title, source: action.source });
|
|
142
|
+
writes.push({
|
|
143
|
+
title: action.title,
|
|
144
|
+
file: written.file ? path.relative(root, written.file) : null,
|
|
145
|
+
line: written.line,
|
|
146
|
+
already_present: Boolean(written.alreadyPresent),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const payload = {
|
|
151
|
+
ok: true,
|
|
152
|
+
action: 'harvest',
|
|
153
|
+
write,
|
|
154
|
+
count: actions.length,
|
|
155
|
+
actions,
|
|
156
|
+
writes,
|
|
157
|
+
};
|
|
158
|
+
if (asJson) {
|
|
159
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
if (!actions.length) {
|
|
163
|
+
console.log('No harvest actions found.');
|
|
164
|
+
return payload;
|
|
165
|
+
}
|
|
166
|
+
for (const action of actions) {
|
|
167
|
+
console.log(`- **${action.id}:** ${action.title}`);
|
|
168
|
+
console.log(` reason: ${action.reason}`);
|
|
169
|
+
if (action.evidence) console.log(` evidence: ${action.evidence}`);
|
|
170
|
+
}
|
|
171
|
+
if (write) console.log(`Wrote ${writes.filter((item) => item.line).length} inbox action(s).`);
|
|
172
|
+
else console.log('Read-only. Add --write to append these to today\'s Inbox.');
|
|
173
|
+
return payload;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
harvestActions,
|
|
178
|
+
harvestCommand,
|
|
179
|
+
};
|
package/commands/init.js
CHANGED
|
@@ -512,7 +512,7 @@ function initAtris() {
|
|
|
512
512
|
if (!fs.existsSync(featuresDir)) {
|
|
513
513
|
fs.mkdirSync(featuresDir, { recursive: true });
|
|
514
514
|
const featuresReadme = path.join(featuresDir, 'README.md');
|
|
515
|
-
fs.writeFileSync(featuresReadme, '# Features\n\nThis directory tracks all features built using the
|
|
515
|
+
fs.writeFileSync(featuresReadme, '# Features\n\nThis directory tracks all features built using the Atris protocol.\n\nEach feature has:\n- `[feature-name]/idea.md` - Problem, solution, diagrams, success criteria\n- `[feature-name]/build.md` - Implementation plan, files changed, testing\n- `[feature-name]/validate.md` - End-to-end simulation script\n\n---\n\n## Features Built\n\n*Features will appear here as you build them.*\n');
|
|
516
516
|
console.log('✓ Created features/ directory with README');
|
|
517
517
|
}
|
|
518
518
|
|