atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// atris write — guided writing sessions (plan-do-review for prose).
|
|
2
|
+
//
|
|
3
|
+
// The contract: the human types every word of the draft. Atris structures the
|
|
4
|
+
// session (outline beats with states), tracks progress against the plan, and
|
|
5
|
+
// reviews against the taste gate (writing policy + slop detector). It NEVER
|
|
6
|
+
// writes or rewrites prose — review output is suggestions, not edits.
|
|
7
|
+
//
|
|
8
|
+
// Session = a folder of plain markdown, so the same files open in the web and
|
|
9
|
+
// desktop editors with no extra format:
|
|
10
|
+
// atris/writing/<slug>/plan.md outline beats: [ ] empty, [~] drafted, [x] passed
|
|
11
|
+
// atris/writing/<slug>/draft.md the piece — human-only territory
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// atris write start "<topic>" [--dump "raw ideas"] [--beats "a | b | c"]
|
|
15
|
+
// atris write status [slug] progress: which beats are landed
|
|
16
|
+
// atris write review [slug] taste gate: policy checklist + slop scan (read-only)
|
|
17
|
+
// atris write pass <n> [slug] mark beat n passed (human calls it, not the AI)
|
|
18
|
+
// atris write list list sessions
|
|
19
|
+
//
|
|
20
|
+
// Zero external deps (Node built-ins only) — repo contract.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawnSync } = require('child_process');
|
|
25
|
+
const { scanFile, RULES, loadProjectRules } = require('./slop');
|
|
26
|
+
|
|
27
|
+
const WRITING_DIR = path.join('atris', 'writing');
|
|
28
|
+
// Beats are question-shaped (Pollan: "almost everything I write is a quest to
|
|
29
|
+
// answer a question"). The piece has suspense built in structurally: each beat
|
|
30
|
+
// exists to answer its question, and the reader wonders if you will.
|
|
31
|
+
const DEFAULT_BEATS = [
|
|
32
|
+
{ title: 'Hook', q: 'What is the moment this became a question for you? Start naive: wonder on page one, never lecture.' },
|
|
33
|
+
{ title: 'Thesis', q: 'What is the question this piece exists to answer? Pose it so the reader has to know.' },
|
|
34
|
+
{ title: 'Evidence', q: 'What did you actually see happen? The specific thing, not the category. Spread facts thin so the story never sags.' },
|
|
35
|
+
{ title: 'Counterpunch', q: 'What would a smart skeptic say back? Steelman it, then answer in your own words.' },
|
|
36
|
+
{ title: 'Landing', q: 'Did you answer the question you posed? Say what the reader should do or believe now, and stop.' },
|
|
37
|
+
];
|
|
38
|
+
const STATE_ICON = { ' ': '·', '~': '◐', x: '●' };
|
|
39
|
+
const STATE_WORD = { ' ': 'empty', '~': 'drafted', x: 'passed' };
|
|
40
|
+
|
|
41
|
+
function slugify(topic) {
|
|
42
|
+
return topic.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'untitled';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sessionDir(slug, root = process.cwd()) {
|
|
46
|
+
return path.join(root, WRITING_DIR, slug);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listSessions(root = process.cwd()) {
|
|
50
|
+
const dir = path.join(root, WRITING_DIR);
|
|
51
|
+
let names;
|
|
52
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
53
|
+
return names
|
|
54
|
+
.filter((n) => fs.existsSync(path.join(dir, n, 'plan.md')))
|
|
55
|
+
.map((n) => ({ slug: n, mtime: fs.statSync(path.join(dir, n, 'plan.md')).mtimeMs }))
|
|
56
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveSlug(maybeSlug, root = process.cwd()) {
|
|
60
|
+
if (maybeSlug && fs.existsSync(path.join(sessionDir(maybeSlug, root), 'plan.md'))) return maybeSlug;
|
|
61
|
+
if (maybeSlug) return null;
|
|
62
|
+
const sessions = listSessions(root);
|
|
63
|
+
return sessions.length ? sessions[0].slug : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse plan.md → { topic, beats: [{ n, state, title, q }] }
|
|
67
|
+
// A beat line may be followed by an indented `? question` line: the question that beat answers.
|
|
68
|
+
function readPlan(slug, root = process.cwd()) {
|
|
69
|
+
const text = fs.readFileSync(path.join(sessionDir(slug, root), 'plan.md'), 'utf8');
|
|
70
|
+
const topic = (text.match(/^# plan: (.+)$/m) || [, slug])[1];
|
|
71
|
+
const beats = [];
|
|
72
|
+
const lines = text.split('\n');
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
const m = lines[i].match(/^- \[([ ~x])\] (\d+)\. (.+)$/);
|
|
75
|
+
if (!m) continue;
|
|
76
|
+
const qm = (lines[i + 1] || '').match(/^\s+\? (.+)$/);
|
|
77
|
+
beats.push({ state: m[1], n: +m[2], title: m[3].trim(), q: qm ? qm[1].trim() : null });
|
|
78
|
+
}
|
|
79
|
+
return { topic, beats, text };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writePlanStates(slug, beats, root = process.cwd()) {
|
|
83
|
+
const file = path.join(sessionDir(slug, root), 'plan.md');
|
|
84
|
+
let text = fs.readFileSync(file, 'utf8');
|
|
85
|
+
for (const b of beats) {
|
|
86
|
+
text = text.replace(new RegExp(`^- \\[[ ~x]\\] ${b.n}\\. `, 'm'), `- [${b.state}] ${b.n}. `);
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(file, text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Words the human has typed under each `## beat` heading in draft.md.
|
|
92
|
+
function draftWordCounts(slug, beats, root = process.cwd()) {
|
|
93
|
+
let text = '';
|
|
94
|
+
try { text = fs.readFileSync(path.join(sessionDir(slug, root), 'draft.md'), 'utf8'); } catch {}
|
|
95
|
+
const lines = text.split('\n');
|
|
96
|
+
const counts = new Map(beats.map((b) => [b.n, 0]));
|
|
97
|
+
let current = null;
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const h = line.match(/^## (.+)$/);
|
|
100
|
+
if (h) {
|
|
101
|
+
const beat = beats.find((b) => h[1].trim().toLowerCase() === b.title.toLowerCase());
|
|
102
|
+
current = beat ? beat.n : null;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (current !== null) {
|
|
106
|
+
const words = line.trim().split(/\s+/).filter(Boolean).length;
|
|
107
|
+
counts.set(current, counts.get(current) + words);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return counts;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sync beat states from the draft: empty→drafted when words land. Never downgrades passed.
|
|
114
|
+
function syncStates(slug, root = process.cwd()) {
|
|
115
|
+
const plan = readPlan(slug, root);
|
|
116
|
+
const counts = draftWordCounts(slug, plan.beats, root);
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const b of plan.beats) {
|
|
119
|
+
const words = counts.get(b.n) || 0;
|
|
120
|
+
if (b.state === ' ' && words > 0) { b.state = '~'; changed = true; }
|
|
121
|
+
if (b.state === '~' && words === 0) { b.state = ' '; changed = true; }
|
|
122
|
+
}
|
|
123
|
+
if (changed) writePlanStates(slug, plan.beats, root);
|
|
124
|
+
return { ...plan, counts };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function start(argv, root = process.cwd()) {
|
|
128
|
+
const topic = argv.find((a) => !a.startsWith('-'));
|
|
129
|
+
if (!topic) { console.error(' usage: atris write start "<topic>" [--dump "..."] [--beats "a | b | c"]'); return 2; }
|
|
130
|
+
const flag = (name) => { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; };
|
|
131
|
+
const dump = flag('--dump');
|
|
132
|
+
const beatsArg = flag('--beats');
|
|
133
|
+
const beats = beatsArg
|
|
134
|
+
? beatsArg.split('|').map((s) => s.trim()).filter(Boolean).map((t) => ({ title: t, q: GENERIC_QUESTION(t) }))
|
|
135
|
+
: DEFAULT_BEATS;
|
|
136
|
+
|
|
137
|
+
const slug = slugify(topic);
|
|
138
|
+
const dir = sessionDir(slug, root);
|
|
139
|
+
if (fs.existsSync(path.join(dir, 'plan.md'))) { console.error(` session already exists: ${slug} (atris write status ${slug})`); return 2; }
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const dumpLines = dump ? dump.split(/\n|(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean).map((s) => `- ${s}`) : ['- (dump raw ideas here)'];
|
|
143
|
+
const plan = [
|
|
144
|
+
`# plan: ${topic}`, '',
|
|
145
|
+
'## Dump', ...dumpLines, '',
|
|
146
|
+
'## Outline',
|
|
147
|
+
...beats.flatMap((b, i) => [`- [ ] ${i + 1}. ${b.title}`, ` ? ${b.q}`]), '',
|
|
148
|
+
'## Gate',
|
|
149
|
+
'- review: atris write review runs the taste checklist + slop scan',
|
|
150
|
+
'- pass: you call `atris write pass <n>` when a beat holds up. The AI never calls it.',
|
|
151
|
+
'- inflow: once drafting starts, no new research. Work with what you have.', '',
|
|
152
|
+
].join('\n');
|
|
153
|
+
const draft = [`# ${topic}`, '', ...beats.flatMap((b) => [`## ${b.title}`, '']), ''].join('\n');
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(path.join(dir, 'plan.md'), plan);
|
|
156
|
+
fs.writeFileSync(path.join(dir, 'draft.md'), draft);
|
|
157
|
+
|
|
158
|
+
console.log(`\n ✓ session started: ${slug}`);
|
|
159
|
+
console.log(` plan: ${path.relative(root, path.join(dir, 'plan.md'))}`);
|
|
160
|
+
console.log(` draft: ${path.relative(root, path.join(dir, 'draft.md'))} ← you write here, every word`);
|
|
161
|
+
console.log(`\n beats: ${beats.map((b) => b.title).join(' → ')}`);
|
|
162
|
+
console.log(` first question: ${beats[0].q}`);
|
|
163
|
+
console.log(` next: open the draft, answer under "## ${beats[0].title}", then \`atris write\`\n`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function status(argv, root = process.cwd()) {
|
|
168
|
+
const slug = resolveSlug(argv.find((a) => !a.startsWith('-')), root);
|
|
169
|
+
if (!slug) { console.error(' no writing session found. start one: atris write start "<topic>"'); return 2; }
|
|
170
|
+
const { topic, beats, counts } = syncStates(slug, root);
|
|
171
|
+
const done = beats.filter((b) => b.state === 'x').length;
|
|
172
|
+
const drafted = beats.filter((b) => b.state === '~').length;
|
|
173
|
+
const totalWords = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
174
|
+
|
|
175
|
+
console.log(`\n ${topic} (${slug})`);
|
|
176
|
+
console.log(` ${beats.map((b) => STATE_ICON[b.state]).join(' ')} ${done} passed · ${drafted} drafted · ${totalWords} words\n`);
|
|
177
|
+
for (const b of beats) {
|
|
178
|
+
const words = counts.get(b.n) || 0;
|
|
179
|
+
console.log(` ${STATE_ICON[b.state]} ${String(b.n).padStart(2)}. ${b.title.padEnd(24)} ${STATE_WORD[b.state].padEnd(8)} ${words ? `${words} words` : ''}`);
|
|
180
|
+
}
|
|
181
|
+
const next = beats.find((b) => b.state === ' ') || beats.find((b) => b.state === '~');
|
|
182
|
+
if (next) console.log(`\n next: ${next.state === ' ' ? `answer "${beatQuestion(next)}"` : `review "${next.title}", then \`atris write pass ${next.n}\` if it holds`}\n`);
|
|
183
|
+
else console.log(`\n all beats passed. run \`atris write review\` for the final gate.\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function review(argv, root = process.cwd()) {
|
|
188
|
+
const json = argv.includes('--json');
|
|
189
|
+
const slug = resolveSlug(argv.find((a) => !a.startsWith('-')), root);
|
|
190
|
+
if (!slug) { console.error(' no writing session found. start one: atris write start "<topic>"'); return 2; }
|
|
191
|
+
const { topic, beats, counts } = syncStates(slug, root);
|
|
192
|
+
const draftPath = path.join(sessionDir(slug, root), 'draft.md');
|
|
193
|
+
const findings = scanFile(draftPath, RULES.concat(loadProjectRules(root)));
|
|
194
|
+
const empty = beats.filter((b) => (counts.get(b.n) || 0) === 0);
|
|
195
|
+
|
|
196
|
+
if (json) {
|
|
197
|
+
console.log(JSON.stringify({
|
|
198
|
+
slug, topic, ok: findings.length === 0 && empty.length === 0,
|
|
199
|
+
beats: beats.map((b) => ({ n: b.n, title: b.title, state: STATE_WORD[b.state], words: counts.get(b.n) || 0 })),
|
|
200
|
+
slop: findings.map((f) => ({ line: f.line, rule: f.rule, why: f.why, snippet: f.snippet })),
|
|
201
|
+
}, null, 2));
|
|
202
|
+
return findings.length || empty.length ? 1 : 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(`\n review: ${topic}\n`);
|
|
206
|
+
if (empty.length) {
|
|
207
|
+
console.log(` ⚠ ${empty.length} beat${empty.length === 1 ? '' : 's'} still empty: ${empty.map((b) => b.title).join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
if (findings.length) {
|
|
210
|
+
console.log(`\n slop tells (fix them yourself — atris never edits your draft):`);
|
|
211
|
+
for (const f of findings) console.log(` ✗ draft.md:${f.line} ${f.rule.padEnd(16)} ${f.why}`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(` ✓ slop scan clean`);
|
|
214
|
+
}
|
|
215
|
+
console.log(`\n human passes (atris/policies/writing.md):`);
|
|
216
|
+
console.log(` - read it out loud: mark stumbles, boredom, and the parts you speed up`);
|
|
217
|
+
console.log(` - voice check: would someone recognize this as yours, blind?`);
|
|
218
|
+
console.log(` - one-sentence test: can you state the core insight in one line?`);
|
|
219
|
+
console.log(` - would you send it to someone you respect?`);
|
|
220
|
+
console.log(`\n when a beat holds up: atris write pass <n>\n`);
|
|
221
|
+
return findings.length || empty.length ? 1 : 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pass(argv, root = process.cwd()) {
|
|
225
|
+
const n = parseInt(argv[0], 10);
|
|
226
|
+
if (!n) { console.error(' usage: atris write pass <beat-number> [slug]'); return 2; }
|
|
227
|
+
const slug = resolveSlug(argv[1], root);
|
|
228
|
+
if (!slug) { console.error(' no writing session found.'); return 2; }
|
|
229
|
+
const plan = readPlan(slug, root);
|
|
230
|
+
const beat = plan.beats.find((b) => b.n === n);
|
|
231
|
+
if (!beat) { console.error(` no beat ${n} in ${slug}`); return 2; }
|
|
232
|
+
const counts = draftWordCounts(slug, plan.beats, root);
|
|
233
|
+
if ((counts.get(n) || 0) === 0) { console.error(` beat ${n} "${beat.title}" has no words yet — write it first`); return 2; }
|
|
234
|
+
beat.state = 'x';
|
|
235
|
+
writePlanStates(slug, plan.beats, root);
|
|
236
|
+
console.log(` ● passed: ${n}. ${beat.title}`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function list(root = process.cwd()) {
|
|
241
|
+
const sessions = listSessions(root);
|
|
242
|
+
if (!sessions.length) { console.log('\n no writing sessions yet. start one: atris write start "<topic>"\n'); return 0; }
|
|
243
|
+
console.log('');
|
|
244
|
+
for (const s of sessions) {
|
|
245
|
+
const { topic, beats } = readPlan(s.slug, root);
|
|
246
|
+
console.log(` ${beats.map((b) => STATE_ICON[b.state]).join('')} ${s.slug.padEnd(32)} ${topic}`);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- coach: intelligent, proactive, gets the writer going. Never writes prose. ----
|
|
253
|
+
|
|
254
|
+
const GENERIC_QUESTION = (title) => `What question does "${title}" answer for the reader? Answer it in your own words.`;
|
|
255
|
+
|
|
256
|
+
function beatQuestion(beat) {
|
|
257
|
+
return beat.q || GENERIC_QUESTION(beat.title);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Pollan: re-edit the whole piece before writing anything new. It primes the pump
|
|
261
|
+
// and loads the draft back into short-term memory. Fires when you return to a
|
|
262
|
+
// session on a later day.
|
|
263
|
+
function isReturningSession(slug, root) {
|
|
264
|
+
try {
|
|
265
|
+
const m = fs.statSync(path.join(sessionDir(slug, root), 'draft.md')).mtime;
|
|
266
|
+
return m.toDateString() !== new Date().toDateString();
|
|
267
|
+
} catch { return false; }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function dumpSeeds(planText) {
|
|
271
|
+
const m = planText.match(/## Dump\n([\s\S]*?)(\n## |$)/);
|
|
272
|
+
if (!m) return [];
|
|
273
|
+
return m[1].split('\n').map((l) => l.replace(/^- /, '').trim()).filter((l) => l && !l.startsWith('(dump'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function coachLogPath(slug, root) { return path.join(sessionDir(slug, root), 'coach.md'); }
|
|
277
|
+
|
|
278
|
+
function appendCoachNote(slug, note, root = process.cwd()) {
|
|
279
|
+
const file = coachLogPath(slug, root);
|
|
280
|
+
const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
281
|
+
const header = fs.existsSync(file) ? '' : `# coach log\n\nStyle lessons and session notes. This is the training data for your voice.\n`;
|
|
282
|
+
fs.appendFileSync(file, `${header}\n## ${stamp}\n${note.trim()}\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function claudeAvailable() {
|
|
286
|
+
try { return spawnSync('claude', ['--version'], { timeout: 8000 }).status === 0; } catch { return false; }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function coachPrompt({ topic, beats, counts, findings, planText, draftText, currentBeat }) {
|
|
290
|
+
return [
|
|
291
|
+
'You are a writing coach inside atris write. HARD RULES:',
|
|
292
|
+
'- You NEVER write, rewrite, or suggest replacement prose for the draft. Not one sentence.',
|
|
293
|
+
'- You coach: point at what the writer did well IN THEIR OWN WORDS, ask one question, teach one style lesson.',
|
|
294
|
+
'- Be warm and direct. No em dashes. No hype words. Sentence case. Under 140 words total.',
|
|
295
|
+
'',
|
|
296
|
+
'Coaching doctrine (Pollan):',
|
|
297
|
+
'- Every beat answers a question. Suspense comes from whether the writer answers it.',
|
|
298
|
+
'- Idiot on page one: the hook should wonder, never lecture. Voice starts naive, sophisticates as it goes.',
|
|
299
|
+
'- Narrative depends on conflict; spread exposition thin or the story sags.',
|
|
300
|
+
'- Once drafting starts, no new information: work with what the writer has and what they remember.',
|
|
301
|
+
'',
|
|
302
|
+
'Output EXACTLY these three sections:',
|
|
303
|
+
'CHEER: quote the writer\'s single best sentence from the draft verbatim and say specifically why it works. If the draft is empty, cheer the strongest dump line instead.',
|
|
304
|
+
`QUESTION: one pointed question to get the next words out for the beat "${currentBeat}". A question they answer in their own voice.`,
|
|
305
|
+
'LESSON: one concrete, reusable style observation about THIS writer (pattern you see in their words, good or fixable). One sentence.',
|
|
306
|
+
'',
|
|
307
|
+
`Topic: ${topic}`,
|
|
308
|
+
`Beats: ${beats.map((b) => `${b.title}(${STATE_WORD[b.state]},${counts.get(b.n) || 0}w)`).join(' ')}`,
|
|
309
|
+
findings.length ? `Slop findings: ${findings.map((f) => `${f.rule}@${f.line}`).join(', ')}` : 'Slop scan: clean',
|
|
310
|
+
'',
|
|
311
|
+
'--- plan.md ---', planText,
|
|
312
|
+
'--- draft.md ---', draftText || '(empty)',
|
|
313
|
+
].join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function coach(argv, root = process.cwd()) {
|
|
317
|
+
const offline = argv.includes('--offline');
|
|
318
|
+
const slug = resolveSlug(argv.find((a) => !a.startsWith('-')), root);
|
|
319
|
+
if (!slug) { console.error(' no writing session found. start one: atris write start "<topic>"'); return 2; }
|
|
320
|
+
const { topic, beats, counts, text: planText } = syncStates(slug, root);
|
|
321
|
+
const draftFile = path.join(sessionDir(slug, root), 'draft.md');
|
|
322
|
+
let draftText = ''; try { draftText = fs.readFileSync(draftFile, 'utf8'); } catch {}
|
|
323
|
+
const findings = scanFile(draftFile, RULES.concat(loadProjectRules(root)));
|
|
324
|
+
const current = beats.find((b) => b.state === ' ') || beats.find((b) => b.state === '~') || beats[beats.length - 1];
|
|
325
|
+
const done = beats.filter((b) => b.state === 'x').length;
|
|
326
|
+
const totalWords = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
327
|
+
|
|
328
|
+
console.log(`\n coach · ${topic}`);
|
|
329
|
+
console.log(` ${beats.map((b) => STATE_ICON[b.state]).join(' ')} ${totalWords} words in, ${done}/${beats.length} beats passed\n`);
|
|
330
|
+
|
|
331
|
+
// Day-two ritual: prime the pump before anything new lands.
|
|
332
|
+
if (totalWords > 0 && isReturningSession(slug, root)) {
|
|
333
|
+
console.log(' welcome back. before anything new: read the whole draft, top to bottom, and touch it up.');
|
|
334
|
+
console.log(' it loads the piece back into your head, and it quietly makes your opening the best part.\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!offline && claudeAvailable()) {
|
|
338
|
+
const prompt = coachPrompt({ topic, beats, counts, findings, planText, draftText, currentBeat: current.title });
|
|
339
|
+
const res = spawnSync('claude', ['-p', prompt], { encoding: 'utf8', timeout: 120000, maxBuffer: 1024 * 1024 });
|
|
340
|
+
const out = (res.stdout || '').trim();
|
|
341
|
+
if (res.status === 0 && /QUESTION:/.test(out)) {
|
|
342
|
+
console.log(out.split('\n').map((l) => ` ${l}`).join('\n'));
|
|
343
|
+
const lesson = (out.match(/LESSON:\s*([\s\S]*?)$/m) || [])[1];
|
|
344
|
+
if (lesson) appendCoachNote(slug, `- lesson: ${lesson.trim()}`, root);
|
|
345
|
+
console.log(`\n your move: answer under "## ${current.title}" in draft.md, then \`atris write status\`\n`);
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
console.log(' (claude coach unavailable, falling back to the offline coach)\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Offline coach: still proactive, still gets you going. Seeds + one question.
|
|
352
|
+
const seeds = dumpSeeds(planText);
|
|
353
|
+
if (totalWords > 0) {
|
|
354
|
+
console.log(` you have ${totalWords} words down. inflow is closed: work with what you have and what you remember.`);
|
|
355
|
+
} else if (seeds.length) {
|
|
356
|
+
console.log(' your raw material (pick the line with the most heat and start there):');
|
|
357
|
+
for (const s of seeds.slice(0, 4)) console.log(` · ${s}`);
|
|
358
|
+
}
|
|
359
|
+
console.log(`\n next beat: ${current.title}`);
|
|
360
|
+
console.log(` question: ${beatQuestion(current)}`);
|
|
361
|
+
if (findings.length) console.log(`\n while you are in there: ${findings.length} slop tell${findings.length === 1 ? '' : 's'} to fix in your own words (atris write review for the list)`);
|
|
362
|
+
console.log(`\n your move: answer under "## ${current.title}" in draft.md, then \`atris write status\`\n`);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function help() {
|
|
367
|
+
console.log(`
|
|
368
|
+
atris write — guided writing sessions (you write every word; atris structures + reviews)
|
|
369
|
+
|
|
370
|
+
atris write the coach: knows where you are, gets you going
|
|
371
|
+
atris write start "<topic>" [--dump "..."] [--beats "a | b | c"]
|
|
372
|
+
atris write coach [slug] the coach: cheers your best line, asks the next question (--offline for no-LLM)
|
|
373
|
+
atris write status [slug] progress against the outline (beats landed)
|
|
374
|
+
atris write review [slug] taste gate: slop scan + writing-policy passes (read-only)
|
|
375
|
+
atris write pass <n> [slug] mark beat n passed — the human calls it
|
|
376
|
+
atris write list all sessions
|
|
377
|
+
|
|
378
|
+
sessions are plain markdown in atris/writing/<slug>/ — the same files open
|
|
379
|
+
in the web and desktop editors. atris never writes or edits your draft.
|
|
380
|
+
`);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function writeCommand(argv) {
|
|
385
|
+
const sub = argv[0];
|
|
386
|
+
if (sub === 'start') return start(argv.slice(1));
|
|
387
|
+
if (sub === 'coach' || sub === 'kick') return coach(argv.slice(1));
|
|
388
|
+
if (sub === 'status') return status(argv.slice(1));
|
|
389
|
+
if (sub === 'review') return review(argv.slice(1));
|
|
390
|
+
if (sub === 'pass') return pass(argv.slice(1));
|
|
391
|
+
if (sub === 'list') return list();
|
|
392
|
+
// bare `atris write` (flags allowed) IS the coach: one door, it knows where you are and gets you going
|
|
393
|
+
if (!sub || sub.startsWith('-')) return listSessions().length ? coach(argv) : help();
|
|
394
|
+
if (sub === 'help') return help();
|
|
395
|
+
// `atris write "some topic"` sugar → start
|
|
396
|
+
return start(argv);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = { writeCommand, start, status, review, pass, coach, dumpSeeds, listSessions, readPlan, draftWordCounts, syncStates, slugify };
|
package/commands/xp.js
CHANGED
|
@@ -18,6 +18,21 @@ const CAREER_XP_CURSOR_FILE = path.join('.atris', 'state', 'career_xp.cursor.jso
|
|
|
18
18
|
const CAREER_XP_SESSIONS_DIR = path.join('.atris', 'state', 'career_xp_sessions');
|
|
19
19
|
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
20
20
|
const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
21
|
+
|
|
22
|
+
// Default single-workspace XP root. Local XP reads task_episodes.jsonl (written
|
|
23
|
+
// by task-db at the resolved workspace root) and writes its projection beside
|
|
24
|
+
// it, so the `--workspace` default must resolve the same shared root — otherwise
|
|
25
|
+
// `xp --local` from a subdir (e.g. backend/) read the wrong/empty episodes and
|
|
26
|
+
// split the projection into a nested .atris. Explicit --workspace still wins;
|
|
27
|
+
// the multi-root roster path (discoverCareerXpWorkspaces) is untouched. Falls
|
|
28
|
+
// back to raw cwd if the resolver can't load.
|
|
29
|
+
function defaultXpWorkspace() {
|
|
30
|
+
try {
|
|
31
|
+
return require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
|
|
32
|
+
} catch {
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
// Codex moved native goals to goals_1.sqlite; thread metadata (cwd/title) stayed in state_5.sqlite.
|
|
22
37
|
const CODEX_GOALS_FILE = path.join(os.homedir(), '.codex', 'goals_1.sqlite');
|
|
23
38
|
const AGENT_XP_LABEL = 'AgentXP';
|
|
@@ -890,30 +905,33 @@ function readTaskEpisodeTail(episodePath, cursorPath, options = {}) {
|
|
|
890
905
|
}
|
|
891
906
|
|
|
892
907
|
function receiptFromTaskEpisode(episode) {
|
|
908
|
+
const episodeId = episode?.episode_id;
|
|
909
|
+
const label = episode?.rl?.label;
|
|
910
|
+
const rejected = label === 'rework_requested' || label === 'rejected';
|
|
893
911
|
const doneForXp = String(episode?.state?.status || '').toLowerCase() === 'done';
|
|
894
912
|
const hasExplicitCareerXp = episode?.career_xp && typeof episode.career_xp === 'object';
|
|
895
913
|
const eligible = doneForXp && (
|
|
896
914
|
episode?.career_xp?.eligible === true
|
|
897
|
-
|| (!hasExplicitCareerXp &&
|
|
915
|
+
|| (!hasExplicitCareerXp && label === 'accepted')
|
|
898
916
|
);
|
|
899
917
|
const proof = String(episode?.proof || '').trim();
|
|
900
918
|
const reward = asNumber(episode?.career_xp?.reward ?? episode?.reward?.value, 0);
|
|
901
|
-
if (!eligible || !proof || reward <= 0
|
|
919
|
+
if (!episodeId || (!rejected && (!eligible || !proof || reward <= 0))) return null;
|
|
902
920
|
|
|
903
921
|
return {
|
|
904
922
|
schema: 'atris.career_xp_receipt.v1',
|
|
905
|
-
receipt_id: `task_review:${
|
|
923
|
+
receipt_id: `task_review:${episodeId}${rejected ? ':rejected' : ''}`,
|
|
906
924
|
source: 'atris-cli',
|
|
907
925
|
source_type: 'task_review',
|
|
908
926
|
source_task_id: episode.task_id || null,
|
|
909
|
-
source_episode_id:
|
|
927
|
+
source_episode_id: episodeId,
|
|
910
928
|
workspace_root: episode.workspace_root || null,
|
|
911
929
|
actor: episode.action?.actor || null,
|
|
912
|
-
outcome: 'accepted',
|
|
913
|
-
xp: reward,
|
|
914
|
-
reward,
|
|
915
|
-
proof,
|
|
916
|
-
proof_ref: proof,
|
|
930
|
+
outcome: rejected ? 'rejected' : 'accepted',
|
|
931
|
+
xp: rejected ? 0 : reward,
|
|
932
|
+
reward: rejected ? 0 : reward,
|
|
933
|
+
proof: proof || null,
|
|
934
|
+
proof_ref: proof || null,
|
|
917
935
|
source_episode_hash: hashPayload(episode),
|
|
918
936
|
title: episode.state?.title || null,
|
|
919
937
|
goal: episode.goal || null,
|
|
@@ -1022,7 +1040,7 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
|
|
|
1022
1040
|
}
|
|
1023
1041
|
|
|
1024
1042
|
function collectLocalXpProjectionState(args = [], { write = true } = {}) {
|
|
1025
|
-
const workspace = path.resolve(readFlag(args, '--workspace',
|
|
1043
|
+
const workspace = path.resolve(readFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1026
1044
|
const episodePath = path.join(workspace, TASK_EPISODES_FILE);
|
|
1027
1045
|
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
1028
1046
|
const projectionPath = path.join(workspace, CAREER_XP_PROJECTION_FILE);
|
|
@@ -1614,7 +1632,7 @@ LIMIT 25
|
|
|
1614
1632
|
}
|
|
1615
1633
|
|
|
1616
1634
|
function buildCareerXpSessionCapsule(args = []) {
|
|
1617
|
-
const workspace = path.resolve(readFlag(args, '--workspace',
|
|
1635
|
+
const workspace = path.resolve(readFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1618
1636
|
const sinceInput = readFlag(args, '--since', 'today');
|
|
1619
1637
|
const untilInput = readFlag(args, '--until', null);
|
|
1620
1638
|
const since = parseSessionBoundary(sinceInput, startOfToday());
|