atris 3.33.3 → 3.35.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.
Files changed (54) hide show
  1. package/AGENTS.md +0 -2
  2. package/FOR_AGENTS.md +5 -3
  3. package/atris/skills/engines/SKILL.md +16 -7
  4. package/atris/skills/render-cli/SKILL.md +88 -0
  5. package/atris.md +19 -0
  6. package/ax +475 -17
  7. package/bin/atris.js +206 -44
  8. package/commands/aeo.js +52 -0
  9. package/commands/autoland.js +313 -19
  10. package/commands/business.js +91 -8
  11. package/commands/codex-goal.js +26 -2
  12. package/commands/drive.js +187 -0
  13. package/commands/engine.js +73 -2
  14. package/commands/feed.js +202 -0
  15. package/commands/github.js +38 -0
  16. package/commands/gm.js +262 -3
  17. package/commands/init.js +13 -1
  18. package/commands/integrations.js +39 -11
  19. package/commands/interview.js +143 -0
  20. package/commands/land.js +114 -13
  21. package/commands/lesson.js +112 -1
  22. package/commands/linear.js +38 -0
  23. package/commands/loops.js +212 -0
  24. package/commands/member.js +398 -42
  25. package/commands/mission.js +598 -64
  26. package/commands/now.js +25 -1
  27. package/commands/radar.js +259 -14
  28. package/commands/serve.js +54 -0
  29. package/commands/status.js +50 -5
  30. package/commands/stripe.js +38 -0
  31. package/commands/supabase.js +39 -0
  32. package/commands/task.js +935 -71
  33. package/commands/truth.js +29 -3
  34. package/commands/unknowns.js +627 -0
  35. package/commands/update.js +44 -0
  36. package/commands/vercel.js +38 -0
  37. package/commands/worktree.js +68 -10
  38. package/commands/write.js +399 -0
  39. package/lib/auto-accept-certified.js +70 -19
  40. package/lib/autoland.js +39 -3
  41. package/lib/fleet.js +256 -13
  42. package/lib/memory-view.js +14 -5
  43. package/lib/mission-runtime-loop.js +7 -0
  44. package/lib/official-cli-integration.js +174 -0
  45. package/lib/outbound-send-gate.js +165 -0
  46. package/lib/permission-grants.js +293 -0
  47. package/lib/review-integrity.js +147 -0
  48. package/lib/runner-command.js +23 -0
  49. package/lib/task-db.js +220 -7
  50. package/lib/task-proof.js +20 -0
  51. package/lib/task-receipt.js +93 -0
  52. package/package.json +1 -1
  53. package/utils/update-check.js +27 -6
  54. package/atris/learnings.jsonl +0 -1
@@ -0,0 +1,187 @@
1
+ // atris drive — self-driving tick over the mission plane.
2
+ // One tick: run mission doctor, auto-execute the fixes it prescribes that are
3
+ // safe to run (stale ready receipts -> verify tick), and log everything that
4
+ // still needs a human as an explicit disengagement. The metric that matters:
5
+ // disengagements per tick, driven to zero.
6
+ //
7
+ // State: .atris/state/drive.jsonl (one line per tick, append-only).
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { spawnSync } = require('child_process');
12
+
13
+ const BIN = path.join(__dirname, '..', 'bin', 'atris.js');
14
+
15
+ function runAtris(args, cwd) {
16
+ const res = spawnSync(process.execPath, [BIN, ...args], {
17
+ cwd,
18
+ encoding: 'utf8',
19
+ timeout: 120000,
20
+ maxBuffer: 16 * 1024 * 1024,
21
+ });
22
+ return { code: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
23
+ }
24
+
25
+ function parseJsonLoose(text) {
26
+ if (!text) return null;
27
+ const start = text.indexOf('{');
28
+ if (start === -1) return null;
29
+ try { return JSON.parse(text.slice(start)); } catch (err) { return null; }
30
+ }
31
+
32
+ function statePath(cwd) {
33
+ return path.join(cwd, '.atris', 'state', 'drive.jsonl');
34
+ }
35
+
36
+ function loadHistory(cwd, limit = 20) {
37
+ try {
38
+ const lines = fs.readFileSync(statePath(cwd), 'utf8').trim().split('\n');
39
+ return lines.slice(-limit).map((l) => { try { return JSON.parse(l); } catch (e) { return null; } }).filter(Boolean);
40
+ } catch (err) { return []; }
41
+ }
42
+
43
+ function appendState(cwd, record) {
44
+ const file = statePath(cwd);
45
+ fs.mkdirSync(path.dirname(file), { recursive: true });
46
+ fs.appendFileSync(file, JSON.stringify(record) + '\n');
47
+ }
48
+
49
+ async function driveCommand(argv) {
50
+ const cwd = process.cwd();
51
+ const json = argv.includes('--json');
52
+ const dryRun = argv.includes('--dry-run');
53
+
54
+ if (argv.includes('--help') || argv.includes('-h')) {
55
+ console.log('atris drive - one self-driving tick over the mission plane');
56
+ console.log('');
57
+ console.log(' atris drive [--dry-run] [--json] doctor -> auto-fix safe findings -> count disengagements');
58
+ console.log(' atris drive status [--json] disengagement trend from past ticks');
59
+ console.log('');
60
+ console.log(' Auto-handled: stale_ready_receipt (runs the verify tick, completes on pass).');
61
+ console.log(' Everything else = a disengagement: the system needed a human. Logged, counted, escalated.');
62
+ return 0;
63
+ }
64
+
65
+ if (argv[0] === 'status') {
66
+ const history = loadHistory(cwd);
67
+ if (!history.length) { console.log('No drive ticks yet. Run: atris drive'); return 0; }
68
+ if (json) { console.log(JSON.stringify({ ok: true, ticks: history }, null, 2)); return 0; }
69
+ console.log('drive — disengagements per tick (goal: 0)\n');
70
+ for (const t of history) {
71
+ console.log(` ${t.at} fixed ${t.fixed}/${t.findings} disengagements ${t.disengagements}`);
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ const doctor = runAtris(['mission', 'doctor', '--json'], cwd);
77
+ const report = parseJsonLoose(doctor.stdout);
78
+ if (!report || !Array.isArray(report.findings)) {
79
+ console.error('drive: mission doctor returned no parseable findings.');
80
+ if (doctor.stderr) console.error(doctor.stderr.slice(0, 500));
81
+ return 1;
82
+ }
83
+
84
+ const fixed = [];
85
+ const disengagements = [];
86
+
87
+ // Missions the doctor says have no verifier. A verify-tick physically cannot
88
+ // pass without one, so promising to verify-tick such a mission is a phantom
89
+ // fix that never sticks — count it as a wheel-grab instead.
90
+ const noVerifier = new Set(
91
+ report.findings.filter((f) => f.code === 'missing_verifier' && f.mission_id).map((f) => f.mission_id),
92
+ );
93
+
94
+ for (const f of report.findings) {
95
+ if (f.code === 'stale_ready_receipt' && f.mission_id && noVerifier.has(f.mission_id)) {
96
+ disengagements.push({ ...pick(f), reason: 'ready_without_verifier' });
97
+ continue;
98
+ }
99
+ if (f.code === 'stale_ready_receipt' && f.mission_id && !dryRun) {
100
+ const tick = runAtris(['mission', 'tick', f.mission_id, '--verify', '--complete-on-pass', '--json'], cwd);
101
+ const result = parseJsonLoose(tick.stdout);
102
+ const passed = tick.code === 0 && result && result.ok !== false;
103
+ if (passed) {
104
+ fixed.push({ mission_id: f.mission_id, action: 'verify_tick', objective: f.objective });
105
+ continue;
106
+ }
107
+ disengagements.push({ ...pick(f), reason: 'verify_tick_failed' });
108
+ continue;
109
+ }
110
+ if (f.code === 'stale_ready_receipt' && dryRun) {
111
+ fixed.push({ mission_id: f.mission_id, action: 'would_verify_tick', objective: f.objective });
112
+ continue;
113
+ }
114
+ // No verifier and stale (planning >24h old, or already paused): park it.
115
+ // A paused mission is reversible; a zombie in planning is a permanent disengagement.
116
+ if (f.code === 'missing_verifier' && f.mission_id && isParkable(f)) {
117
+ if (dryRun) {
118
+ fixed.push({ mission_id: f.mission_id, action: 'would_park', objective: f.objective });
119
+ continue;
120
+ }
121
+ const stop = runAtris(['mission', 'stop', f.mission_id, '--pause', '--reason', 'drive: no verifier + stale, auto-parked (restart with a --verify to resume)'], cwd);
122
+ if (stop.code === 0) {
123
+ fixed.push({ mission_id: f.mission_id, action: 'parked', objective: f.objective });
124
+ continue;
125
+ }
126
+ disengagements.push({ ...pick(f), reason: 'park_failed' });
127
+ continue;
128
+ }
129
+ disengagements.push({ ...pick(f), reason: f.code });
130
+ }
131
+
132
+ // One stuck mission is one wheel-grab, even if it trips several findings.
133
+ // Count disengagements per mission so the metric tracks "how many missions
134
+ // still need a human", not "how many symptoms did we print".
135
+ const dedupedDisengagements = [];
136
+ const seenDisengaged = new Set();
137
+ for (const d of disengagements) {
138
+ const key = d.mission_id || `${d.reason}:${d.objective || ''}`;
139
+ if (seenDisengaged.has(key)) continue;
140
+ seenDisengaged.add(key);
141
+ dedupedDisengagements.push(d);
142
+ }
143
+ disengagements.length = 0;
144
+ disengagements.push(...dedupedDisengagements);
145
+
146
+ function isParkable(f) {
147
+ if (f.status === 'paused') return true;
148
+ if (f.status !== 'planning') return false;
149
+ const m = /^mission-(\d{4}-\d{2}-\d{2})-/.exec(f.mission_id || '');
150
+ if (!m) return false;
151
+ return (Date.now() - new Date(m[1] + 'T00:00:00Z').getTime()) > 24 * 3600 * 1000;
152
+ }
153
+
154
+ function pick(f) {
155
+ return { mission_id: f.mission_id, code: f.code, owner: f.owner, objective: (f.objective || '').slice(0, 120), next: f.next };
156
+ }
157
+
158
+ const record = {
159
+ at: new Date().toISOString(),
160
+ findings: report.findings.length,
161
+ checked: report.checked_count,
162
+ fixed: fixed.length,
163
+ disengagements: disengagements.length,
164
+ dry_run: dryRun,
165
+ fixed_detail: fixed,
166
+ disengagement_detail: disengagements,
167
+ };
168
+ if (!dryRun) appendState(cwd, record);
169
+
170
+ if (json) { console.log(JSON.stringify({ ok: true, ...record }, null, 2)); return 0; }
171
+
172
+ console.log(`drive tick — ${report.checked_count} missions checked, ${report.findings.length} findings`);
173
+ console.log(` auto-fixed: ${fixed.length}`);
174
+ for (const x of fixed) console.log(` ✓ ${x.action} ${x.mission_id}`);
175
+ console.log(` disengagements: ${disengagements.length} (a human has to grab the wheel)`);
176
+ const byCode = {};
177
+ for (const d of disengagements) byCode[d.reason] = (byCode[d.reason] || 0) + 1;
178
+ for (const [code, n] of Object.entries(byCode)) console.log(` ✋ ${code} ×${n}`);
179
+ const history = loadHistory(cwd, 5);
180
+ if (history.length > 1) {
181
+ console.log(` trend: ${history.map((t) => t.disengagements).join(' → ')}`);
182
+ }
183
+ console.log(` next: atris drive status · fix the ✋ list · re-run atris drive`);
184
+ return disengagements.length > 0 ? 0 : 0;
185
+ }
186
+
187
+ module.exports = { driveCommand };
@@ -23,6 +23,7 @@ const {
23
23
  RUNNER_PROFILE_NAMES,
24
24
  buildRunnerCommand,
25
25
  } = require('../lib/runner-command');
26
+ const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
26
27
 
27
28
  const HOUSE_ENGINE = 'atris-fast';
28
29
 
@@ -240,11 +241,79 @@ function runEngineTest(targets, { json, root } = {}) {
240
241
  return failures.length ? 1 : 0;
241
242
  }
242
243
 
244
+ // One-command dispatch: claim -> worktree start -> bounded prompt -> engine
245
+ // -> re-run Check: -> ship -> task ready, in one call instead of the 6
246
+ // hand-rolled Bash calls per task the manual version took. Task ids are
247
+ // positional; --engine/--prompt-file are the only flags, so parse by hand
248
+ // instead of the generic "anything starting with --" split used above (that
249
+ // split would swallow an --engine value like "cursor" as a task id).
250
+ function parseDispatchArgs(args) {
251
+ const taskIds = [];
252
+ let engine = '';
253
+ let promptFile = '';
254
+ let base = '';
255
+ let json = false;
256
+ for (let i = 0; i < args.length; i += 1) {
257
+ const a = args[i];
258
+ if (a === '--engine') { engine = args[i + 1] || ''; i += 1; continue; }
259
+ if (a.startsWith('--engine=')) { engine = a.slice('--engine='.length); continue; }
260
+ if (a === '--prompt-file') { promptFile = args[i + 1] || ''; i += 1; continue; }
261
+ if (a.startsWith('--prompt-file=')) { promptFile = a.slice('--prompt-file='.length); continue; }
262
+ if (a === '--base') { base = args[i + 1] || ''; i += 1; continue; }
263
+ if (a.startsWith('--base=')) { base = a.slice('--base='.length); continue; }
264
+ if (a === '--json') { json = true; continue; }
265
+ if (a.startsWith('--')) continue;
266
+ taskIds.push(a);
267
+ }
268
+ return { taskIds, engine, promptFile, base, json };
269
+ }
270
+
271
+ function runDispatchCommand(args, root) {
272
+ const { taskIds, engine, promptFile, base, json } = parseDispatchArgs(args);
273
+ if (!taskIds.length || !engine) {
274
+ console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]');
275
+ return 2;
276
+ }
277
+ const canonical = canonicalEngineName(engine);
278
+ if (!canonical || !FLEET_CAPABLE.includes(canonical)) {
279
+ console.error(`engine dispatch: --engine must be one of ${FLEET_CAPABLE.join(', ')}`);
280
+ return 2;
281
+ }
282
+ // Argument-shape errors surface before environment errors: --prompt-file
283
+ // with multiple ids is wrong on any machine, installed CLI or not.
284
+ let promptOverride = '';
285
+ if (promptFile) {
286
+ if (taskIds.length > 1) {
287
+ console.error('engine dispatch: --prompt-file only supports a single task id');
288
+ return 2;
289
+ }
290
+ try {
291
+ promptOverride = fs.readFileSync(promptFile, 'utf8');
292
+ } catch (err) {
293
+ console.error(`engine dispatch: could not read --prompt-file ${promptFile}: ${err.message}`);
294
+ return 2;
295
+ }
296
+ }
297
+ const def = RUNNER_PROFILE_DEFS[canonical];
298
+ if (!binInstalled(def.bin)) {
299
+ console.error(`engine dispatch: ${canonical} CLI (${def.bin}) is not installed here`);
300
+ return 2;
301
+ }
302
+ return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
303
+ if (json) console.log(JSON.stringify(flight, null, 2));
304
+ return flight.paused.length ? 1 : 0;
305
+ });
306
+ }
307
+
243
308
  function engineCommand(args = []) {
309
+ const root = process.cwd();
310
+ if ((args[0] || '').trim() === 'dispatch') {
311
+ return runDispatchCommand(args.slice(1), root);
312
+ }
313
+
244
314
  const json = args.includes('--json');
245
315
  const positional = args.filter((a) => !a.startsWith('--'));
246
316
  const sub = (positional[0] || '').trim();
247
- const root = process.cwd();
248
317
 
249
318
  if (sub === 'test') {
250
319
  return runEngineTest(positional.slice(1), { json, root });
@@ -269,7 +338,7 @@ function engineCommand(args = []) {
269
338
  }
270
339
 
271
340
  if (sub === 'help') {
272
- console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
341
+ console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]\n one-command claim, worktree, build, verify, ship, ready\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
273
342
  return 0;
274
343
  }
275
344
 
@@ -295,5 +364,7 @@ module.exports = {
295
364
  roster,
296
365
  probeEngine,
297
366
  runEngineTest,
367
+ parseDispatchArgs,
368
+ runDispatchCommand,
298
369
  HOUSE_ENGINE,
299
370
  };
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ // Feed: the business group feed from the terminal — read receipts, post receipts.
4
+ // Auth: existing atris login. Business: nearest .atris/business.json above cwd.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { ensureValidCredentials } = require('../utils/auth');
9
+ const { apiRequestJson } = require('../utils/api');
10
+
11
+ function showHelp() {
12
+ console.log('');
13
+ console.log('Usage: atris feed [list] [n] [--author <name>] [--since <7d|24h>] [--full] [--json]');
14
+ console.log(' atris feed post "<content>"');
15
+ console.log('');
16
+ console.log('Description:');
17
+ console.log(' Read and write the business group feed (the same feed as the web GM dashboard).');
18
+ console.log(' Posts should be receipts and state changes only: what shipped, what blocked, what changed.');
19
+ console.log('');
20
+ console.log('Options:');
21
+ console.log(' --author <name> Only posts whose author matches (name, email prefix, or id prefix).');
22
+ console.log(' --since <window> Only posts newer than e.g. 7d, 48h.');
23
+ console.log(' --full Print full post bodies instead of one line each.');
24
+ console.log(' --json Machine-readable output.');
25
+ console.log(' --help, -h Show this help.');
26
+ console.log('');
27
+ }
28
+
29
+ function findBusiness(startDir) {
30
+ let dir = startDir;
31
+ while (dir !== path.dirname(dir)) {
32
+ const file = path.join(dir, '.atris', 'business.json');
33
+ if (fs.existsSync(file)) {
34
+ try {
35
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
36
+ if (parsed.business_id) return { businessId: parsed.business_id, name: parsed.name || parsed.slug, root: dir };
37
+ } catch { /* fall through to parent dirs */ }
38
+ }
39
+ dir = path.dirname(dir);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function loadAuthorAliases(workspaceRoot) {
45
+ const file = path.join(workspaceRoot, '.atris', 'feed_authors.json');
46
+ try {
47
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ function parseSince(raw) {
54
+ const match = /^(\d+)([dh])$/.exec(String(raw || '').trim());
55
+ if (!match) return null;
56
+ const amount = parseInt(match[1], 10);
57
+ const ms = match[2] === 'd' ? amount * 24 * 3600 * 1000 : amount * 3600 * 1000;
58
+ return Date.now() - ms;
59
+ }
60
+
61
+ function authorLabel(post, aliases, selfId, selfEmail) {
62
+ if (post.post_type === 'agent_post') return post.agent_name || post.author_display_name || 'agent';
63
+ if (post.author_display_name) return post.author_display_name;
64
+ const id = post.author_id || '';
65
+ if (aliases[id]) return aliases[id];
66
+ if (selfId && id === selfId) return (selfEmail || 'me').split('@')[0];
67
+ return id ? id.slice(0, 8) : '?';
68
+ }
69
+
70
+ async function fetchPosts(businessId, token, want) {
71
+ const pageSize = 100;
72
+ let all = [];
73
+ for (let offset = 0; all.length < want; offset += pageSize) {
74
+ const res = await apiRequestJson(
75
+ `/business/${businessId}/feed?limit=${pageSize}&offset=${offset}`,
76
+ { token, timeoutMs: 20000 }
77
+ );
78
+ const statusCode = res.statusCode || res.status;
79
+ if (statusCode && statusCode >= 400) {
80
+ throw new Error(`Feed fetch failed (${statusCode}): ${JSON.stringify(res.data)}`);
81
+ }
82
+ const page = Array.isArray(res.data?.posts) ? res.data.posts : Array.isArray(res.data) ? res.data : [];
83
+ all = all.concat(page);
84
+ if (page.length < pageSize) break;
85
+ }
86
+ return all;
87
+ }
88
+
89
+ async function feedCommand(args = []) {
90
+ if (args.includes('--help') || args.includes('-h')) {
91
+ showHelp();
92
+ return 0;
93
+ }
94
+
95
+ const business = findBusiness(process.cwd());
96
+ if (!business) {
97
+ console.error('✗ No .atris/business.json found above this directory. Run from a bound workspace.');
98
+ return 1;
99
+ }
100
+
101
+ const auth = await ensureValidCredentials(apiRequestJson);
102
+ if (auth.error || !auth.credentials?.token) {
103
+ console.error(`✗ Not logged in (${auth.error || 'no token'}). Run: atris login`);
104
+ return 1;
105
+ }
106
+ const token = auth.credentials.token;
107
+ const selfId = auth.credentials.user_id || auth.user?.id || null;
108
+ const selfEmail = auth.credentials.email || auth.user?.email || null;
109
+
110
+ const positional = args.filter((a) => !a.startsWith('--'));
111
+ const sub = positional[0] || 'list';
112
+
113
+ if (sub === 'post') {
114
+ const content = positional.slice(1).join(' ').trim();
115
+ if (!content) {
116
+ console.error('✗ Nothing to post. Usage: atris feed post "<content>"');
117
+ return 1;
118
+ }
119
+ const res = await apiRequestJson(`/business/${business.businessId}/feed`, {
120
+ method: 'POST',
121
+ token,
122
+ body: { content: content.slice(0, 10000) },
123
+ timeoutMs: 20000,
124
+ });
125
+ if (!res.data?.id) {
126
+ console.error(`✗ Post failed: ${JSON.stringify(res.data)}`);
127
+ return 1;
128
+ }
129
+ console.log(`✓ Posted to ${business.name || 'business'} feed (${res.data.id})`);
130
+ return 0;
131
+ }
132
+
133
+ if (sub !== 'list') {
134
+ console.error(`✗ Unknown subcommand: ${sub}`);
135
+ showHelp();
136
+ return 1;
137
+ }
138
+
139
+ const countArg = parseInt(positional[1], 10);
140
+ const count = Number.isFinite(countArg) ? Math.max(1, Math.min(300, countArg)) : 15;
141
+ const authorFlag = args.includes('--author') ? String(args[args.indexOf('--author') + 1] || '').toLowerCase() : null;
142
+ const sinceFlag = args.includes('--since') ? parseSince(args[args.indexOf('--since') + 1]) : null;
143
+ if (args.includes('--since') && sinceFlag === null) {
144
+ console.error('✗ Bad --since window. Use forms like 7d or 48h.');
145
+ return 1;
146
+ }
147
+ const full = args.includes('--full');
148
+ const json = args.includes('--json');
149
+
150
+ const aliases = loadAuthorAliases(business.root);
151
+ // Over-fetch when filtering so the filtered result can still reach `count`.
152
+ const fetchWant = authorFlag || sinceFlag ? 300 : count;
153
+ let posts = await fetchPosts(business.businessId, token, fetchWant);
154
+
155
+ if (sinceFlag) posts = posts.filter((p) => Date.parse(p.created_at) >= sinceFlag);
156
+ if (authorFlag) {
157
+ posts = posts.filter((p) => {
158
+ const label = authorLabel(p, aliases, selfId, selfEmail).toLowerCase();
159
+ return label.includes(authorFlag) || String(p.author_id || '').startsWith(authorFlag);
160
+ });
161
+ }
162
+ posts = posts.slice(0, count);
163
+
164
+ if (json) {
165
+ console.log(JSON.stringify(posts.map((p) => ({
166
+ id: p.id,
167
+ author: authorLabel(p, aliases, selfId, selfEmail),
168
+ type: p.post_type,
169
+ created_at: p.created_at,
170
+ content: p.content,
171
+ })), null, 2));
172
+ return 0;
173
+ }
174
+
175
+ if (!posts.length) {
176
+ console.log('No posts matched.');
177
+ return 0;
178
+ }
179
+
180
+ for (const p of posts) {
181
+ const when = String(p.created_at || '').slice(0, 16).replace('T', ' ');
182
+ const who = authorLabel(p, aliases, selfId, selfEmail);
183
+ if (full) {
184
+ console.log(`─── ${who} — ${when}`);
185
+ console.log(String(p.content || '').trim());
186
+ console.log('');
187
+ } else {
188
+ const oneLine = String(p.content || '').replace(/\s+/g, ' ').slice(0, 120);
189
+ console.log(`[${when}] ${who}: ${oneLine}`);
190
+ }
191
+ }
192
+ return 0;
193
+ }
194
+
195
+ module.exports = {
196
+ feedCommand,
197
+ findBusiness,
198
+ loadAuthorAliases,
199
+ parseSince,
200
+ authorLabel,
201
+ fetchPosts,
202
+ };
@@ -0,0 +1,38 @@
1
+ const { createOfficialCliCommand } = require('../lib/official-cli-integration');
2
+
3
+ const githubCommand = createOfficialCliCommand({
4
+ name: 'github',
5
+ binary: 'gh',
6
+ versionArgs: ['--version'],
7
+ authArgs: ['auth', 'status'],
8
+ installHint: 'https://cli.github.com/',
9
+ loginHint: 'gh auth login',
10
+ commands: [
11
+ {
12
+ usage: 'pr list',
13
+ match: ['pr', 'list'],
14
+ forward: ['pr', 'list'],
15
+ description: 'list pull requests',
16
+ },
17
+ {
18
+ usage: 'pr create',
19
+ match: ['pr', 'create'],
20
+ forward: ['pr', 'create'],
21
+ description: 'create a pull request',
22
+ },
23
+ {
24
+ usage: 'pr checks',
25
+ match: ['pr', 'checks'],
26
+ forward: ['pr', 'checks'],
27
+ description: 'show pull request checks',
28
+ },
29
+ {
30
+ usage: 'pr view',
31
+ match: ['pr', 'view'],
32
+ forward: ['pr', 'view'],
33
+ description: 'show pull request details',
34
+ },
35
+ ],
36
+ });
37
+
38
+ module.exports = { githubCommand };