atris 3.16.0 → 3.17.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/README.md +33 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +446 -43
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +466 -20
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +574 -0
- package/commands/console.js +8 -3
- package/commands/deck.js +135 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +444 -0
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +233 -0
- package/commands/run.js +615 -22
- package/commands/skill.js +6 -2
- package/commands/slop.js +173 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +458 -43
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/slides-deck.js +236 -0
- package/lib/state-detection.js +40 -3
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/commands/verify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* atris verify [task] - Validate work is actually done
|
|
@@ -354,7 +355,8 @@ function checkDocsVsChanges(cwd, atrisDir) {
|
|
|
354
355
|
*/
|
|
355
356
|
function findTaskInContent(content, taskId) {
|
|
356
357
|
// Try exact ID match (T1, T2, etc.)
|
|
357
|
-
const
|
|
358
|
+
const safeId = escapeRegExp(taskId);
|
|
359
|
+
const idPattern = new RegExp(`### (T${safeId}|Task ${safeId})[:\\s]([\\s\\S]*?)(?=\\n###|\\n##|$)`, 'i');
|
|
358
360
|
let match = content.match(idPattern);
|
|
359
361
|
|
|
360
362
|
if (match) {
|
|
@@ -506,7 +508,7 @@ function verifyRubric(slug, section, opts = {}) {
|
|
|
506
508
|
const content = fs.readFileSync(validateFile, 'utf8');
|
|
507
509
|
// Match "## <section>" (case-insensitive, anchored), skipping optional
|
|
508
510
|
// prose until the first ```bash or ```sh fence. Extract until the closing ```.
|
|
509
|
-
const escaped = section
|
|
511
|
+
const escaped = escapeRegExp(section);
|
|
510
512
|
const pattern = new RegExp(
|
|
511
513
|
`^##\\s+${escaped}\\s*$[\\s\\S]*?\\n\`\`\`(?:bash|sh)?\\s*\\n([\\s\\S]*?)\\n\`\`\``,
|
|
512
514
|
'mi'
|
|
@@ -531,5 +533,7 @@ function verifyRubric(slug, section, opts = {}) {
|
|
|
531
533
|
|
|
532
534
|
module.exports = {
|
|
533
535
|
verifyAtris,
|
|
534
|
-
verifyRubric
|
|
536
|
+
verifyRubric,
|
|
537
|
+
findTaskInContent,
|
|
538
|
+
escapeRegExp
|
|
535
539
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Activity stream: normalize the workspace's receipt channels into ONE
|
|
4
|
+
// time-ordered feed of what the agent actually did, plus a heartbeat summary.
|
|
5
|
+
// The task board shows a static inventory of tasks; this turns the same data
|
|
6
|
+
// into "what happened, in order" — the stream you watch for hours.
|
|
7
|
+
|
|
8
|
+
const TS_KEYS = ['ts', 'at', 'created_at', 'accepted_at', 'timestamp', 'updated_at'];
|
|
9
|
+
|
|
10
|
+
function pickTs(row) {
|
|
11
|
+
for (const k of TS_KEYS) {
|
|
12
|
+
if (row && row[k]) {
|
|
13
|
+
const ms = Date.parse(row[k]);
|
|
14
|
+
if (Number.isFinite(ms)) return { iso: String(row[k]), ms };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { iso: null, ms: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clip(value, n = 100) {
|
|
21
|
+
const t = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
|
22
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function num(v) {
|
|
26
|
+
return v == null || v === '' || Number.isNaN(Number(v)) ? null : Number(v);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- per-channel normalizers (each returns a uniform event or null) ---
|
|
30
|
+
|
|
31
|
+
function normalizePulse(row) {
|
|
32
|
+
if (!row || row.phase !== 'finished') return null; // only completed ticks are events
|
|
33
|
+
const { iso, ms } = pickTs(row);
|
|
34
|
+
const reward = num(row.reward);
|
|
35
|
+
return {
|
|
36
|
+
ts: iso, ms,
|
|
37
|
+
source: 'pulse',
|
|
38
|
+
kind: row.actor === 'autopilot' ? 'autopilot' : 'heartbeat',
|
|
39
|
+
title: clip(row.what || `pulse tick #${row.tick_index}`),
|
|
40
|
+
detail: row.verify_passed == null ? '' : (row.verify_passed ? 'verify pass' : 'verify FAIL'),
|
|
41
|
+
status: row.verify_passed === false ? 'bad' : (reward > 0 ? 'good' : 'neutral'),
|
|
42
|
+
reward,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeScorecard(row) {
|
|
47
|
+
if (!row) return null;
|
|
48
|
+
const { iso, ms } = pickTs(row);
|
|
49
|
+
const reward = num(row.reward);
|
|
50
|
+
return {
|
|
51
|
+
ts: iso, ms,
|
|
52
|
+
source: 'reward',
|
|
53
|
+
kind: row.source || row.member || 'tick',
|
|
54
|
+
title: clip(row.what_shipped || 'tick scored'),
|
|
55
|
+
detail: reward == null ? '' : `reward ${reward}`,
|
|
56
|
+
status: reward != null && reward > 0 ? 'good' : (reward != null && reward < 0 ? 'bad' : 'neutral'),
|
|
57
|
+
reward,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeTaskEpisode(row) {
|
|
62
|
+
if (!row) return null;
|
|
63
|
+
const { iso, ms } = pickTs(row);
|
|
64
|
+
const state = row.state || {};
|
|
65
|
+
const action = row.action || state.status || 'updated';
|
|
66
|
+
const reward = num(row.reward);
|
|
67
|
+
return {
|
|
68
|
+
ts: iso, ms,
|
|
69
|
+
source: 'task',
|
|
70
|
+
kind: state.status || action,
|
|
71
|
+
title: clip(state.title || row.task_id),
|
|
72
|
+
detail: row.lesson ? clip(`lesson: ${row.lesson}`, 70) : clip(action, 24),
|
|
73
|
+
status: reward != null && reward < 0 ? 'bad' : (state.status === 'review' ? 'review' : 'neutral'),
|
|
74
|
+
reward,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeXp(row) {
|
|
79
|
+
if (!row) return null;
|
|
80
|
+
const { iso, ms } = pickTs(row);
|
|
81
|
+
return {
|
|
82
|
+
ts: iso, ms,
|
|
83
|
+
source: 'xp',
|
|
84
|
+
kind: row.outcome || 'accepted',
|
|
85
|
+
title: clip(row.title || 'work accepted'),
|
|
86
|
+
detail: `+${row.xp || 0} XP${row.actor ? ` · ${row.actor}` : ''}`,
|
|
87
|
+
status: 'good',
|
|
88
|
+
reward: num(row.reward),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeMissionEvent(row) {
|
|
93
|
+
if (!row) return null;
|
|
94
|
+
const { iso, ms } = pickTs(row);
|
|
95
|
+
const type = row.type || 'event';
|
|
96
|
+
const p = row.payload || {};
|
|
97
|
+
return {
|
|
98
|
+
ts: iso, ms,
|
|
99
|
+
source: 'mission',
|
|
100
|
+
kind: type,
|
|
101
|
+
title: clip(p.summary || p.objective || p.reason || p.next_action || type),
|
|
102
|
+
detail: clip(row.actor || '', 24),
|
|
103
|
+
status: /error|fail|halt|stop|paused/i.test(type) ? 'bad' : 'neutral',
|
|
104
|
+
reward: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- the feed ---
|
|
109
|
+
|
|
110
|
+
function buildActivityStream(sources = {}, opts = {}) {
|
|
111
|
+
const limit = opts.limit || 60;
|
|
112
|
+
const events = [];
|
|
113
|
+
const push = (rows, fn) => {
|
|
114
|
+
for (const r of rows || []) {
|
|
115
|
+
const e = fn(r);
|
|
116
|
+
if (e && e.ms) events.push(e);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
push(sources.pulseReceipts, normalizePulse);
|
|
120
|
+
push(sources.scorecards, normalizeScorecard);
|
|
121
|
+
push(sources.taskEpisodes, normalizeTaskEpisode);
|
|
122
|
+
push(sources.xpReceipts, normalizeXp);
|
|
123
|
+
push(sources.missionEvents, normalizeMissionEvent);
|
|
124
|
+
events.sort((a, b) => (b.ms - a.ms) || a.source.localeCompare(b.source)); // newest first, stable
|
|
125
|
+
return events.slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- heartbeat: is the agent alive, and what did it just do ---
|
|
129
|
+
|
|
130
|
+
function buildHeartbeat(pulseReceipts, now = Date.now()) {
|
|
131
|
+
const { summarizePulse } = require('./pulse');
|
|
132
|
+
const s = summarizePulse(pulseReceipts || [], now);
|
|
133
|
+
const lastMs = s.last_tick_ts ? Date.parse(s.last_tick_ts) : null;
|
|
134
|
+
const ageMin = Number.isFinite(lastMs) ? Math.max(0, Math.round((now - lastMs) / 60000)) : null;
|
|
135
|
+
const finished = (pulseReceipts || []).filter((r) => r && r.phase === 'finished');
|
|
136
|
+
const last = finished.length ? finished[finished.length - 1] : null;
|
|
137
|
+
let state = 'idle';
|
|
138
|
+
if (s.stale.stale) state = 'stale';
|
|
139
|
+
else if (s.total_ticks > 0) state = 'alive';
|
|
140
|
+
return {
|
|
141
|
+
state, // 'alive' | 'stale' | 'idle'
|
|
142
|
+
alive: state === 'alive',
|
|
143
|
+
stale_reason: s.stale.stale ? s.stale.reason : null,
|
|
144
|
+
last_tick_ts: s.last_tick_ts,
|
|
145
|
+
last_tick_age_min: ageMin,
|
|
146
|
+
last_what: last ? last.what : null,
|
|
147
|
+
last_reward: last ? num(last.reward) : null,
|
|
148
|
+
total_ticks: s.total_ticks,
|
|
149
|
+
reward_sum: s.reward_sum,
|
|
150
|
+
verify_pass: s.verify_pass,
|
|
151
|
+
verify_fail: s.verify_fail,
|
|
152
|
+
orphan_ticks: s.orphan_ticks,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
pickTs,
|
|
158
|
+
clip,
|
|
159
|
+
normalizePulse,
|
|
160
|
+
normalizeScorecard,
|
|
161
|
+
normalizeTaskEpisode,
|
|
162
|
+
normalizeXp,
|
|
163
|
+
normalizeMissionEvent,
|
|
164
|
+
buildActivityStream,
|
|
165
|
+
buildHeartbeat,
|
|
166
|
+
};
|
|
@@ -54,6 +54,28 @@ function safeNodePathArgs(args) {
|
|
|
54
54
|
return args.every(token => safeRelativePathToken(token));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function safeNodeTestArgs(args) {
|
|
58
|
+
let expectPattern = false;
|
|
59
|
+
for (const token of args) {
|
|
60
|
+
if (expectPattern) {
|
|
61
|
+
if (!safeVerifyToken(token) || hasUnsafePathSegment(token)) return false;
|
|
62
|
+
expectPattern = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (token === '--test-name-pattern') {
|
|
66
|
+
expectPattern = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (String(token || '').startsWith('--test-name-pattern=')) {
|
|
70
|
+
const pattern = String(token).slice('--test-name-pattern='.length);
|
|
71
|
+
if (!pattern || !safeVerifyToken(pattern) || hasUnsafePathSegment(pattern)) return false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!safeRelativePathToken(token)) return false;
|
|
75
|
+
}
|
|
76
|
+
return !expectPattern;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
function safeGitDiffCheckArgs(args) {
|
|
58
80
|
return args.length === 0 || (args.length <= 2 && args.every(safeGitRevToken));
|
|
59
81
|
}
|
|
@@ -182,7 +204,7 @@ function parseVerifyCommand(verify) {
|
|
|
182
204
|
|| (first === 'run' && Boolean(second) && !second.startsWith('-') && argv.length === 3)
|
|
183
205
|
))
|
|
184
206
|
|| (bin === 'node' && (
|
|
185
|
-
(first === '--test' &&
|
|
207
|
+
(first === '--test' && safeNodeTestArgs(argv.slice(2)))
|
|
186
208
|
|| (first === '--check' && argv.length === 3 && safeRelativePathToken(second))
|
|
187
209
|
|| (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
|
|
188
210
|
))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PROFILE_REL_PATH = path.join('.atris', 'state', 'context_profile.json');
|
|
7
|
+
|
|
8
|
+
function profilePath(root = process.cwd()) {
|
|
9
|
+
return path.join(root, PROFILE_REL_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadContextProfile(root = process.cwd()) {
|
|
13
|
+
const target = profilePath(root);
|
|
14
|
+
if (!fs.existsSync(target)) return null;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
17
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasContextProfile(root = process.cwd()) {
|
|
24
|
+
const profile = loadContextProfile(root);
|
|
25
|
+
return Boolean(profile && String(profile.first_answer || '').trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactText(value, max = 160) {
|
|
29
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
|
+
if (!text) return '';
|
|
31
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inferDomain(answer) {
|
|
35
|
+
const text = String(answer || '').toLowerCase();
|
|
36
|
+
if (/college|application|essay|common app|school/.test(text)) return 'school';
|
|
37
|
+
if (/code|coding|program|website|app|project/.test(text)) return 'building';
|
|
38
|
+
if (/week|schedule|calendar|plan|homework/.test(text)) return 'planning';
|
|
39
|
+
return 'general';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeQuestionText(value) {
|
|
43
|
+
return String(value || '')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^\w\s']/g, ' ')
|
|
46
|
+
.replace(/\s+/g, ' ')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAtrisMetaQuestion(value) {
|
|
51
|
+
const text = normalizeQuestionText(value);
|
|
52
|
+
if (!text) return false;
|
|
53
|
+
|
|
54
|
+
const taskVerb = /\b(add|audit|build|change|create|debug|deploy|edit|fix|implement|make|patch|refactor|remove|review|run|ship|test|update|write)\b/;
|
|
55
|
+
if (taskVerb.test(text)) return false;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
/^(what'?s|what is|what are|who is|who are)\s+(atris|you|this)\b/,
|
|
59
|
+
/^what\s+atris\s+is\b/,
|
|
60
|
+
/^(what|how)\s+(does|do|can)\s+(atris|you|this)\b/,
|
|
61
|
+
/^(explain|describe|define)\s+(atris|this)\b/,
|
|
62
|
+
/^tell me\s+(about|what)\s+(atris|this)\b/,
|
|
63
|
+
/^why\s+atris\b/,
|
|
64
|
+
].some((pattern) => pattern.test(text));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function starterTaskTitle(answer) {
|
|
68
|
+
const summary = compactText(answer, 80) || 'first useful path';
|
|
69
|
+
return `First useful step: ${summary}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveContextProfile(root, answer, { source = 'first_contact' } = {}) {
|
|
73
|
+
const text = compactText(answer, 500);
|
|
74
|
+
if (!text) return null;
|
|
75
|
+
const target = profilePath(root);
|
|
76
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
77
|
+
const existing = loadContextProfile(root) || {};
|
|
78
|
+
const profile = {
|
|
79
|
+
schema: 'atris.context_profile.v1',
|
|
80
|
+
created_at: existing.created_at || new Date().toISOString(),
|
|
81
|
+
updated_at: new Date().toISOString(),
|
|
82
|
+
source,
|
|
83
|
+
first_answer: text,
|
|
84
|
+
inferred_domain: inferDomain(text),
|
|
85
|
+
};
|
|
86
|
+
fs.writeFileSync(target, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
87
|
+
return profile;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createStarterTask(root, answer) {
|
|
91
|
+
const atrisDir = path.join(root, 'atris');
|
|
92
|
+
if (!fs.existsSync(atrisDir)) return null;
|
|
93
|
+
try {
|
|
94
|
+
const taskDb = require('./task-db');
|
|
95
|
+
const db = taskDb.open();
|
|
96
|
+
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
97
|
+
const title = starterTaskTitle(answer);
|
|
98
|
+
const sourceKey = taskDb.sourceKey('context-gatherer:first-task', title);
|
|
99
|
+
const added = taskDb.addTask(db, {
|
|
100
|
+
title,
|
|
101
|
+
tag: 'onboarding',
|
|
102
|
+
workspaceRoot,
|
|
103
|
+
sourceKey,
|
|
104
|
+
metadata: {
|
|
105
|
+
source: 'context_gatherer',
|
|
106
|
+
first_answer: compactText(answer, 500),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const rows = taskDb.listTasks(db, { workspaceRoot });
|
|
110
|
+
const displayRows = taskDb.withTaskDisplayRefs(rows);
|
|
111
|
+
const task = displayRows.find(row => row.id === added.id) || null;
|
|
112
|
+
try {
|
|
113
|
+
const todoPath = path.join(root, 'atris', 'TODO.md');
|
|
114
|
+
fs.writeFileSync(todoPath, taskDb.renderTodoMarkdown(rows, { title: 'TODO.md' }), 'utf8');
|
|
115
|
+
} catch {}
|
|
116
|
+
return {
|
|
117
|
+
id: added.id,
|
|
118
|
+
inserted: added.inserted,
|
|
119
|
+
display_id: task && task.display_id || null,
|
|
120
|
+
title,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
error: error && error.message ? error.message : String(error),
|
|
125
|
+
title: starterTaskTitle(answer),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function shouldGatherContext({
|
|
131
|
+
root = process.cwd(),
|
|
132
|
+
userInput = '',
|
|
133
|
+
mapStatus = 'ready',
|
|
134
|
+
liveMissionsCount = 0,
|
|
135
|
+
wipCount = 0,
|
|
136
|
+
backlogCount = 0,
|
|
137
|
+
inboxCount = 0,
|
|
138
|
+
} = {}) {
|
|
139
|
+
if (hasContextProfile(root)) return false;
|
|
140
|
+
if (String(userInput || '').trim()) return true;
|
|
141
|
+
if (mapStatus !== 'ready') return true;
|
|
142
|
+
if (liveMissionsCount > 0 || wipCount > 0 || backlogCount > 0 || inboxCount > 0) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderPrompt({ projectName = 'this workspace' } = {}) {
|
|
147
|
+
return [
|
|
148
|
+
'',
|
|
149
|
+
'Context gatherer',
|
|
150
|
+
'----------------',
|
|
151
|
+
`Hi. I am Atris, and I want to understand ${projectName} before I suggest a path.`,
|
|
152
|
+
'',
|
|
153
|
+
'What are you trying to make easier right now: school, college apps, coding, a personal project, or something else?',
|
|
154
|
+
'Answer in one sentence. I will turn it into the first useful step.',
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
PROFILE_REL_PATH,
|
|
160
|
+
profilePath,
|
|
161
|
+
loadContextProfile,
|
|
162
|
+
hasContextProfile,
|
|
163
|
+
isAtrisMetaQuestion,
|
|
164
|
+
saveContextProfile,
|
|
165
|
+
createStarterTask,
|
|
166
|
+
shouldGatherContext,
|
|
167
|
+
renderPrompt,
|
|
168
|
+
starterTaskTitle,
|
|
169
|
+
inferDomain,
|
|
170
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Canonical regex-metacharacter escaper. Embedding unescaped data (user input,
|
|
4
|
+
// task ids, member names, section headings) into `new RegExp(`...${x}...`)` is a
|
|
5
|
+
// recurring crash/mismatch bug in this codebase (CLI-257, CLI-258): a value like
|
|
6
|
+
// "(" throws an uncaught SyntaxError, and "a.b" silently wildcard-matches "aXb".
|
|
7
|
+
// Always wrap data-derived interpolations with this before building a RegExp.
|
|
8
|
+
// (Interpolating a hardcoded regex *fragment* is fine and does not need escaping.)
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = escapeRegExp;
|
package/lib/file-ops.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const escapeRegExp = require('./escape-regexp');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Get the path components for a journal log file.
|
|
@@ -61,7 +62,6 @@ function createLogFile(logFile, dateFormatted) {
|
|
|
61
62
|
if (fs.existsSync(prevLogFile)) {
|
|
62
63
|
const prevContent = fs.readFileSync(prevLogFile, 'utf8');
|
|
63
64
|
|
|
64
|
-
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
65
|
const sectionBody = (headingLine) => {
|
|
66
66
|
const regex = new RegExp(
|
|
67
67
|
`## ${escapeRegExp(headingLine)}\\n([\\s\\S]*?)(?=\\n---|\\n## |$)`
|
|
@@ -89,7 +89,10 @@ function createLogFile(logFile, dateFormatted) {
|
|
|
89
89
|
|
|
90
90
|
// Inbox operations
|
|
91
91
|
function parseInboxItems(content) {
|
|
92
|
-
|
|
92
|
+
// \r?\n so a CRLF-line-ending journal (Windows-edited / round-tripped) still
|
|
93
|
+
// parses; without it the section never matched, IDs reset to 1, and a second
|
|
94
|
+
// "## Inbox" section was appended on the next add (duplicate-section corruption).
|
|
95
|
+
const match = content.match(/## Inbox\r?\n([\s\S]*?)(?=\r?\n##|\r?\n---|$)/);
|
|
93
96
|
if (!match) {
|
|
94
97
|
return [];
|
|
95
98
|
}
|
|
@@ -111,7 +114,7 @@ function parseInboxItems(content) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
function replaceInboxSection(content, items) {
|
|
114
|
-
const regex = /(## Inbox\n)([\s\S]*?)(\n---|\n##|$)/;
|
|
117
|
+
const regex = /(## Inbox\r?\n)([\s\S]*?)(\r?\n---|\r?\n##|$)/;
|
|
115
118
|
if (!regex.test(content)) {
|
|
116
119
|
const lines = items.length ? items.map((item) => item.line).join('\n') : '(Empty - inbox zero achieved)';
|
|
117
120
|
return `${content}\n\n## Inbox\n\n${lines}\n`;
|
package/lib/journal.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const escapeRegExp = require('./escape-regexp');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Check if two timestamps are effectively the same (within 5ms).
|
|
@@ -235,7 +236,6 @@ function createLogFile(logFile, dateFormatted) {
|
|
|
235
236
|
if (fs.existsSync(prevLogFile)) {
|
|
236
237
|
const prevContent = fs.readFileSync(prevLogFile, 'utf8');
|
|
237
238
|
|
|
238
|
-
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
239
239
|
const sectionBody = (headingLine) => {
|
|
240
240
|
const regex = new RegExp(
|
|
241
241
|
`## ${escapeRegExp(headingLine)}\\n([\\s\\S]*?)(?=\\n---|\\n## |$)`
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect contradictions in lessons.md and lessons.json.
|
|
6
|
+
*
|
|
7
|
+
* Returns array of contradictions:
|
|
8
|
+
* - P1: same slug, opposite outcome at later date
|
|
9
|
+
* - P2: lesson applies_to file no longer exists, skipping [resolved]/observed/attempted>=3
|
|
10
|
+
*
|
|
11
|
+
* @param {string} cwd - workspace root
|
|
12
|
+
* @returns {Array} array of {type, slug, evidence, remediation}
|
|
13
|
+
*/
|
|
14
|
+
function detectLessonContradictions(cwd) {
|
|
15
|
+
const lessonsMdPath = path.join(cwd, 'atris', 'lessons.md');
|
|
16
|
+
const lessonsJsonPath = path.join(cwd, 'atris', 'lessons.json');
|
|
17
|
+
|
|
18
|
+
const contradictions = [];
|
|
19
|
+
|
|
20
|
+
// Parse lessons.md
|
|
21
|
+
const lessonsMdContent = fs.existsSync(lessonsMdPath)
|
|
22
|
+
? fs.readFileSync(lessonsMdPath, 'utf8')
|
|
23
|
+
: '';
|
|
24
|
+
|
|
25
|
+
// Format: - **[YYYY-MM-DD] slug** — pass|fail — prose
|
|
26
|
+
const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*—\s*(pass|fail)\s*—\s*(\[resolved\])?\s*(.+)$/m;
|
|
27
|
+
const lines = lessonsMdContent.split('\n');
|
|
28
|
+
|
|
29
|
+
const lessonsBySlug = new Map(); // slug -> [{ date, outcome, lineNum, resolved }]
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const match = line.match(lessonRegex);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
|
|
36
|
+
const [, date, slug, outcome, resolved, prose] = match;
|
|
37
|
+
if (!lessonsBySlug.has(slug)) {
|
|
38
|
+
lessonsBySlug.set(slug, []);
|
|
39
|
+
}
|
|
40
|
+
lessonsBySlug.get(slug).push({
|
|
41
|
+
date,
|
|
42
|
+
outcome,
|
|
43
|
+
lineNum: i + 1,
|
|
44
|
+
resolved: !!resolved,
|
|
45
|
+
prose: prose.trim(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// P1: same slug with opposite outcomes at later dates
|
|
50
|
+
for (const [slug, entries] of lessonsBySlug) {
|
|
51
|
+
// Sort by date
|
|
52
|
+
const sorted = [...entries].sort((a, b) => a.date.localeCompare(b.date));
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
55
|
+
const curr = sorted[i];
|
|
56
|
+
const next = sorted[i + 1];
|
|
57
|
+
|
|
58
|
+
// Skip if either is [resolved]
|
|
59
|
+
if (curr.resolved || next.resolved) continue;
|
|
60
|
+
|
|
61
|
+
// Opposite outcomes?
|
|
62
|
+
if (curr.outcome !== next.outcome) {
|
|
63
|
+
contradictions.push({
|
|
64
|
+
type: 'P1_opposite_outcomes',
|
|
65
|
+
slug,
|
|
66
|
+
evidence: `${curr.outcome} on line ${curr.lineNum} (${curr.date}), then ${next.outcome} on later entry (${next.date})`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// P2: applies_to files that no longer exist
|
|
73
|
+
if (fs.existsSync(lessonsJsonPath)) {
|
|
74
|
+
let lessonsJson = {};
|
|
75
|
+
try {
|
|
76
|
+
lessonsJson = JSON.parse(fs.readFileSync(lessonsJsonPath, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
// invalid JSON, skip
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [slug, entry] of Object.entries(lessonsJson)) {
|
|
82
|
+
if (!entry.applies_to || !Array.isArray(entry.applies_to)) continue;
|
|
83
|
+
|
|
84
|
+
// Skip if [resolved], observed, or attempted >= 3
|
|
85
|
+
const status = entry.status || '';
|
|
86
|
+
if (status === 'resolved' || status === 'observed') continue;
|
|
87
|
+
|
|
88
|
+
const attempts = entry.attempts || 0;
|
|
89
|
+
if (attempts >= 3) continue;
|
|
90
|
+
|
|
91
|
+
// Check if files exist
|
|
92
|
+
for (const filePath of entry.applies_to) {
|
|
93
|
+
const fullPath = path.isAbsolute(filePath)
|
|
94
|
+
? filePath
|
|
95
|
+
: path.join(cwd, filePath);
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(fullPath)) {
|
|
98
|
+
contradictions.push({
|
|
99
|
+
type: 'P2_missing_file',
|
|
100
|
+
slug,
|
|
101
|
+
evidence: `applies_to file no longer exists: ${filePath}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return contradictions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
detectLessonContradictions,
|
|
113
|
+
};
|
package/lib/policy-lessons.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { RECEIPT_PATH_PATTERN, extractReceiptEvidence } = require('./receipt-evidence');
|
|
13
|
+
const escapeRegExp = require('./escape-regexp');
|
|
13
14
|
|
|
14
15
|
const POLICY_LESSONS_FILE = path.join('.atris', 'state', 'policy_lessons.json');
|
|
15
16
|
const CAREER_XP_RECEIPTS_FILE = path.join('.atris', 'state', 'career_xp_receipts.jsonl');
|
|
@@ -20,7 +21,7 @@ const SCORECARDS_FILE = path.join('.atris', 'state', 'scorecards.jsonl');
|
|
|
20
21
|
// two: agent self-review churn and human accept/bounce are different signals.
|
|
21
22
|
const AGENT_ACTOR_PATTERN = /review|validator|certifier|auto|codex|claude|devin|droid|improver|second-pass|agent|bot/i;
|
|
22
23
|
// "Names a runnable verify command" — the check a reviewer could replay.
|
|
23
|
-
const VERIFY_COMMAND_PATTERN = /\b(npm (run )?test|node --test|node --check|node bin\/|pytest|cargo test|go test|make test|atris verify|grep
|
|
24
|
+
const VERIFY_COMMAND_PATTERN = /\b(npm (run )?test|node --test|node --check|node bin\/|pytest|cargo test|go test|make test|atris verify|grep\s+-[A-Za-z]*q[A-Za-z]*|rg\s+(?:-\S+\s+)*(?:"[^"]+"|'[^']+'|\S+)\s+(?:\.{0,2}\/|~\/|\/|[\w.-]+\/|[\w.-]+\.[A-Za-z0-9]|\b(?:atris|bin|commands|lib|scripts|src|test)\b)|git diff --(?:check|exit-code|quiet)|diff (?:-u|--brief)|cmp -s|test -[fs])\b|--verify\b|\bverify:\s/;
|
|
24
25
|
const COMMIT_REF_PATTERN = /\bcommit\s+[0-9a-f]{7,40}\b/i;
|
|
25
26
|
|
|
26
27
|
function readJsonlFile(filePath) {
|
|
@@ -254,7 +255,7 @@ function syncLessonsMd(root, mined) {
|
|
|
254
255
|
}
|
|
255
256
|
const written = [];
|
|
256
257
|
for (const { id, line } of lines) {
|
|
257
|
-
const marker = new RegExp(`^- \\*\\*\\[\\d{4}-\\d{2}-\\d{2}\\] policy-${id
|
|
258
|
+
const marker = new RegExp(`^- \\*\\*\\[\\d{4}-\\d{2}-\\d{2}\\] policy-${escapeRegExp(id)}\\*\\*.*$`, 'm');
|
|
258
259
|
if (marker.test(content)) {
|
|
259
260
|
content = content.replace(marker, line);
|
|
260
261
|
} else {
|