atris 3.32.0 → 3.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/task.js CHANGED
@@ -16,6 +16,14 @@ const {
16
16
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
17
17
  } = require('../lib/functional-owner');
18
18
  const { operatorReady, hasAgentJargon } = require('./autoland');
19
+ const {
20
+ TASK_INSPECT_FIELDS,
21
+ readFieldsFlag,
22
+ stripInspectArgs,
23
+ validateFields,
24
+ inspectTextLines,
25
+ buildInspectPayload,
26
+ } = require('../lib/inspect-fields');
19
27
 
20
28
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
21
29
  || process.env.USER
@@ -105,7 +113,8 @@ atris task - durable local task state (SQLite, gitignored)
105
113
 
106
114
  atris task Show the task desk
107
115
  atris task new "<title>" Create a task
108
- atris task next [--create-next] Claim/show next open task; optionally create the generated Endgame fallback
116
+ atris task next [--tag <tag>] [--create-next]
117
+ Claim/show next open task; optionally create the generated Endgame fallback
109
118
  atris task continue-work <id> Create/reuse a certified Review follow-up task
110
119
  atris task say <id> "<message>" Add context to a task
111
120
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
@@ -139,6 +148,7 @@ atris task - durable local task state (SQLite, gitignored)
139
148
  atris task day [--json] Show today's owner-grouped task list
140
149
  atris task list [--all] [--status <s>] List tasks (default: this workspace)
141
150
  atris task claim <id> [--as <owner>] Atomic claim
151
+ atris task release <id> [--as <owner>] Release your own mistaken claim back to open
142
152
  atris task capabilities [--json] Read-only task CLI/API capability contract
143
153
  atris task capabilities-check [--json] Verify task capability contract conformance
144
154
  atris task review-lane-drain [--json] Pick next safe Review-lane agent action
@@ -154,6 +164,8 @@ atris task - durable local task state (SQLite, gitignored)
154
164
  review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
155
165
  atris task note <id> "<message>" Append dialogue/context to a task
156
166
  atris task show <id> [--json] Show a task card + dialogue
167
+ atris task inspect <id> --fields review,status,title [--json]
168
+ Field-selectable task state (review metadata, status, title, owner, tag)
157
169
  atris task page <id> [--json] Show the one-task page contract
158
170
  atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
159
171
  atris task done <id> --proof "..." Mark complete with proof
@@ -1105,6 +1117,50 @@ function taskReviewSummary(task) {
1105
1117
  return reviewSummaryWithVerificationChat(task, review);
1106
1118
  }
1107
1119
 
1120
+ function taskReviewInspectMetadata(task) {
1121
+ const review = task.review || taskReviewSummary(task);
1122
+ if (!review) return null;
1123
+ return {
1124
+ approval_status: review.approval_status || null,
1125
+ agent_certified: review.agent_certified === true,
1126
+ agent_review_pass_count: review.agent_review_pass_count || null,
1127
+ agent_certification_policy: review.agent_certification_policy || null,
1128
+ summary: review.summary || null,
1129
+ proof: review.proof || null,
1130
+ lesson: review.lesson || null,
1131
+ next_task: review.next_task || null,
1132
+ human_revision_count: review.human_revision_count || null,
1133
+ human_revision_note: review.human_revision_note || null,
1134
+ reward: review.reward ?? null,
1135
+ };
1136
+ }
1137
+
1138
+ function taskInspectFieldValues(task, fields) {
1139
+ const values = {};
1140
+ for (const field of fields) {
1141
+ switch (field) {
1142
+ case 'status':
1143
+ values.status = task.status || null;
1144
+ break;
1145
+ case 'title':
1146
+ values.title = task.title || null;
1147
+ break;
1148
+ case 'claimed_by':
1149
+ values.claimed_by = task.claimed_by || null;
1150
+ break;
1151
+ case 'tag':
1152
+ values.tag = task.tag || null;
1153
+ break;
1154
+ case 'review':
1155
+ values.review = taskReviewInspectMetadata(task);
1156
+ break;
1157
+ default:
1158
+ break;
1159
+ }
1160
+ }
1161
+ return values;
1162
+ }
1163
+
1108
1164
  function reviewSummaryWithVerificationChat(task, review) {
1109
1165
  if (!review || task.status !== 'review' || review.approval_status !== 'pending') return review;
1110
1166
  const verifierTask = taskWithReviewEvidence(task, {
@@ -3204,6 +3260,11 @@ function formatTaskQueueScope(scope = {}) {
3204
3260
  .join(' ');
3205
3261
  }
3206
3262
 
3263
+ function noOpenTasksMessage(scope = {}) {
3264
+ const scopeText = formatTaskQueueScope(scope);
3265
+ return scopeText ? `No open tasks for ${scopeText}.` : 'No open tasks.';
3266
+ }
3267
+
3207
3268
  function taskQueueContract(projection, { reviewer = 'codex-review', limit = 8, scope = {}, hasExistingReviewFollowUp = null, hasPendingReviewChat = null, excludeTaskIds = null } = {}) {
3208
3269
  const normalizedScope = normalizeTaskQueueScope(scope);
3209
3270
  const tasks = filterTasksByScope(sortTasksNewestFirst(projection.tasks || []), normalizedScope, { hasExistingReviewFollowUp });
@@ -5175,20 +5236,24 @@ function cmdHome(args) {
5175
5236
  function cmdList(args) {
5176
5237
  const all = hasFlag(args, '--all');
5177
5238
  const status = flag(args, '--status');
5239
+ const scope = taskQueueScopeFromArgs(args);
5240
+ const scoped = !taskQueueScopeIsEmpty(scope);
5178
5241
  const taskDb = getTaskDb();
5179
5242
  const db = taskDb.open();
5180
- const rows = taskDb.listTasks(db, {
5243
+ const rawRows = taskDb.listTasks(db, {
5181
5244
  workspaceRoot: all ? null : taskDb.workspaceRoot(),
5182
5245
  status: typeof status === 'string' ? status : null,
5183
- limit: 200,
5246
+ limit: scoped ? null : 200,
5184
5247
  });
5248
+ const rows = filterTasksByScope(rawRows, scope);
5185
5249
  const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, all));
5186
5250
  if (wantsJson(args)) {
5187
- printJson({ ok: true, action: 'list', tasks: displayRows });
5251
+ printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
5188
5252
  return;
5189
5253
  }
5190
5254
  if (rows.length === 0) {
5191
- console.log('(no tasks)');
5255
+ const scopeText = formatTaskQueueScope(scope);
5256
+ console.log(scopeText ? `(no tasks for ${scopeText})` : '(no tasks)');
5192
5257
  return;
5193
5258
  }
5194
5259
  for (const r of displayRows) {
@@ -5224,19 +5289,68 @@ function cmdClaim(args) {
5224
5289
  }
5225
5290
  console.log(`claimed ${taskRef(compactTaskFromProjection(projection, taskId))} as ${owner}`);
5226
5291
  } else {
5292
+ const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
5293
+ ? `atris task release ${id} --as ${result.claimed_by}`
5294
+ : null;
5227
5295
  if (wantsJson(args)) {
5228
5296
  printJson({
5229
5297
  ok: false,
5230
5298
  command: 'atris task claim',
5231
5299
  reason: result.reason,
5232
5300
  claimed_by: result.claimed_by || null,
5301
+ recovery_command: recoveryCommand,
5233
5302
  detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
5234
5303
  });
5235
5304
  process.exit(1);
5236
5305
  }
5237
5306
  console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
5307
+ if (recoveryCommand) console.error(`Recovery: ${recoveryCommand}`);
5308
+ process.exit(1);
5309
+ }
5310
+ }
5311
+
5312
+ function cmdRelease(args) {
5313
+ const pos = positional(args);
5314
+ const id = pos[0];
5315
+ if (!id) {
5316
+ failTask('atris task release', 'missing_id', 'id required');
5317
+ }
5318
+ const owner = flag(args, '--as') || DEFAULT_OWNER;
5319
+ const taskDb = getTaskDb();
5320
+ const db = taskDb.open();
5321
+ const taskId = requireTaskId(taskDb, db, id, 'atris task release');
5322
+ const result = taskDb.releaseTask(db, { id: taskId, actor: String(owner) });
5323
+ if (result.released) {
5324
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
5325
+ const task = compactTaskFromProjection(projection, taskId);
5326
+ if (wantsJson(args)) {
5327
+ printJson({
5328
+ ok: true,
5329
+ action: 'released',
5330
+ task_id: taskId,
5331
+ owner: String(owner),
5332
+ projection_path: outPath,
5333
+ task,
5334
+ event_version: result.event && result.event.version || null,
5335
+ });
5336
+ return;
5337
+ }
5338
+ console.log(`released ${taskRef(task)} from ${owner}`);
5339
+ console.log(`Next: atris task next --as ${owner}`);
5340
+ return;
5341
+ }
5342
+ if (wantsJson(args)) {
5343
+ printJson({
5344
+ ok: false,
5345
+ command: 'atris task release',
5346
+ reason: result.reason,
5347
+ claimed_by: result.claimed_by || null,
5348
+ detail: `release failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
5349
+ });
5238
5350
  process.exit(1);
5239
5351
  }
5352
+ console.error(`release failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
5353
+ process.exit(1);
5240
5354
  }
5241
5355
 
5242
5356
  function liveTaskWithTitle(tasks, title) {
@@ -5342,14 +5456,16 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
5342
5456
 
5343
5457
  function cmdNext(args) {
5344
5458
  const owner = flag(args, '--as') || DEFAULT_OWNER;
5459
+ const scope = taskQueueScopeFromArgs(args);
5460
+ const scoped = !taskQueueScopeIsEmpty(scope);
5345
5461
  const taskDb = getTaskDb();
5346
5462
  const db = taskDb.open();
5347
- const claimed = taskDb.listTasks(db, {
5463
+ const claimed = filterTasksByScope(taskDb.listTasks(db, {
5348
5464
  workspaceRoot: taskDb.workspaceRoot(),
5349
5465
  status: 'claimed',
5350
5466
  claimedBy: String(owner),
5351
- limit: 1,
5352
- });
5467
+ limit: scoped ? null : 1,
5468
+ }), scope);
5353
5469
  if (claimed.length) {
5354
5470
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
5355
5471
  if (wantsJson(args)) {
@@ -5358,6 +5474,7 @@ function cmdNext(args) {
5358
5474
  action: 'current',
5359
5475
  task_id: claimed[0].id,
5360
5476
  owner: String(owner),
5477
+ scope: normalizeTaskQueueScope(scope),
5361
5478
  projection_path: outPath,
5362
5479
  task: compactTaskFromProjection(projection, claimed[0].id),
5363
5480
  });
@@ -5368,14 +5485,14 @@ function cmdNext(args) {
5368
5485
  return;
5369
5486
  }
5370
5487
  const reviewProjection = writeDefaultProjection(taskDb, db);
5371
- const reviewTasks = (reviewProjection.projection.tasks || [])
5488
+ const reviewTasks = filterTasksByScope(reviewProjection.projection.tasks || [], scope)
5372
5489
  .map(compactTaskForStatus)
5373
5490
  .filter(task => task && task.review && task.review.handoff);
5374
- const open = taskDb.listTasks(db, {
5491
+ const open = filterTasksByScope(taskDb.listTasks(db, {
5375
5492
  workspaceRoot: taskDb.workspaceRoot(),
5376
5493
  status: 'open',
5377
- limit: 1,
5378
- });
5494
+ limit: scoped ? null : 1,
5495
+ }), scope);
5379
5496
  if (!open.length) {
5380
5497
  const { projection, outPath } = reviewProjection;
5381
5498
  const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
@@ -5405,6 +5522,7 @@ function cmdNext(args) {
5405
5522
  action: 'created_next',
5406
5523
  task_id: created.task_id,
5407
5524
  owner: String(owner),
5525
+ scope: normalizeTaskQueueScope(scope),
5408
5526
  projection_path: createdOutPath,
5409
5527
  handoff,
5410
5528
  next_agent_action: nextAgentAction,
@@ -5427,6 +5545,7 @@ function cmdNext(args) {
5427
5545
  action: handoff.next_action,
5428
5546
  task_id: handoff.next_action === 'continue_work' ? null : reviewTask.id,
5429
5547
  owner: String(owner),
5548
+ scope: normalizeTaskQueueScope(scope),
5430
5549
  projection_path: outPath,
5431
5550
  handoff,
5432
5551
  next_agent_action: nextAgentAction,
@@ -5436,7 +5555,7 @@ function cmdNext(args) {
5436
5555
  });
5437
5556
  return;
5438
5557
  }
5439
- console.log('No open tasks.');
5558
+ console.log(noOpenTasksMessage(scope));
5440
5559
  console.log(handoff.next_action === 'agent_review_again'
5441
5560
  ? `${taskRef(reviewTask)} needs one more agent check before approval.`
5442
5561
  : `${taskRef(reviewTask)} is ready for approval.`);
@@ -5461,11 +5580,12 @@ function cmdNext(args) {
5461
5580
  action: 'none',
5462
5581
  task_id: null,
5463
5582
  owner: String(owner),
5583
+ scope: normalizeTaskQueueScope(scope),
5464
5584
  projection_path: outPath,
5465
5585
  });
5466
5586
  return;
5467
5587
  }
5468
- console.log('No open tasks.');
5588
+ console.log(noOpenTasksMessage(scope));
5469
5589
  console.log('Start with: atris task new "..."');
5470
5590
  return;
5471
5591
  }
@@ -5481,6 +5601,7 @@ function cmdNext(args) {
5481
5601
  action: 'next',
5482
5602
  task_id: open[0].id,
5483
5603
  owner: String(owner),
5604
+ scope: normalizeTaskQueueScope(scope),
5484
5605
  projection_path: outPath,
5485
5606
  task: compactTaskFromProjection(projection, open[0].id),
5486
5607
  });
@@ -5837,6 +5958,40 @@ function cmdClearPlan(args) {
5837
5958
  console.log(`clear-plan moved ${result.cleared.length} task${result.cleared.length === 1 ? '' : 's'} to Backlog`);
5838
5959
  }
5839
5960
 
5961
+ function cmdInspect(args) {
5962
+ const parsed = readFieldsFlag(args, '--fields');
5963
+ if (!parsed || parsed.error) {
5964
+ failTask('atris task inspect', 'missing_fields', parsed?.error || 'Usage: atris task inspect <id> --fields review,status,title [--json]');
5965
+ }
5966
+ const fieldError = validateFields(parsed.fields, TASK_INSPECT_FIELDS, 'task');
5967
+ if (fieldError) failTask('atris task inspect', 'unknown_fields', fieldError);
5968
+ const ref = stripInspectArgs(args)[0] || '';
5969
+ if (!ref) {
5970
+ failTask('atris task inspect', 'missing_id', 'Usage: atris task inspect <id> --fields review,status,title [--json]');
5971
+ }
5972
+ const taskDb = getTaskDb();
5973
+ const db = taskDb.open();
5974
+ const taskId = requireTaskId(taskDb, db, ref, 'atris task inspect');
5975
+ const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
5976
+ const task = projection.tasks[0];
5977
+ if (!task) {
5978
+ failTask('atris task inspect', 'not_found', `task not found: ${ref}`, 1);
5979
+ }
5980
+ const values = taskInspectFieldValues(task, parsed.fields);
5981
+ const payload = buildInspectPayload({
5982
+ action: 'task_inspect',
5983
+ idKey: 'task_id',
5984
+ idValue: task.id,
5985
+ fields: parsed.fields,
5986
+ values,
5987
+ });
5988
+ if (wantsJson(args)) {
5989
+ printJson(payload);
5990
+ return;
5991
+ }
5992
+ for (const line of inspectTextLines(parsed.fields, values)) console.log(line);
5993
+ }
5994
+
5840
5995
  function cmdShow(args) {
5841
5996
  const pos = positional(args);
5842
5997
  const id = pos[0];
@@ -9912,6 +10067,9 @@ async function run(args) {
9912
10067
  return cmdClearPlan(rest);
9913
10068
  case 'claim': return cmdClaim(rest);
9914
10069
  case 'start': return cmdClaim(rest);
10070
+ case 'release':
10071
+ case 'unclaim':
10072
+ return cmdRelease(rest);
9915
10073
  case 'current':
9916
10074
  case 'select':
9917
10075
  return cmdCurrent(rest);
@@ -9957,6 +10115,7 @@ async function run(args) {
9957
10115
  case 'note': return cmdNote(rest);
9958
10116
  case 'say': return cmdNote(rest);
9959
10117
  case 'show': return cmdShow(rest);
10118
+ case 'inspect': return cmdInspect(rest);
9960
10119
  case 'page': return cmdPage(rest);
9961
10120
  case 'step': return cmdStep(rest);
9962
10121
  case 'review-chat':
package/commands/truth.js CHANGED
@@ -1,13 +1,15 @@
1
1
  // atris truth — one table of what is actually proven, blocked, or stale.
2
2
  // Rolls up sources that already exist; writes nothing. SQL/state is truth, this is the render.
3
3
  // 1. .atris/state/missions.jsonl — mission states (dedupe by id, latest wins)
4
- // 2. ~/.atris/tasks.db — task counts by status
4
+ // 2. ~/.atris/tasks.db — task counts by status, scoped to this workspace unless --all is passed
5
5
  // 3. atris/features/*/ — validate.md frontmatter + newest proof/ receipt age
6
6
  // 4. ~/.atris/heartbeat/{registry,state}.json — declared loops vs last_run / fails
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
+ const { execFileSync } = require('child_process');
12
+ const taskDb = require('../lib/task-db');
11
13
 
12
14
  const STALE_DAYS = 7;
13
15
 
@@ -42,16 +44,67 @@ function loadMissions(cwd) {
42
44
  return [...seen.values()].filter((m) => m.status && !['complete', 'archived', 'stopped'].includes(m.status));
43
45
  }
44
46
 
45
- function loadTaskCounts() {
47
+ function gitCommonWorkspaceRoot(cwd) {
48
+ try {
49
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
50
+ cwd: cwd || process.cwd(),
51
+ encoding: 'utf8',
52
+ stdio: ['ignore', 'pipe', 'ignore'],
53
+ }).trim();
54
+ if (!common) return null;
55
+ const gitDir = path.resolve(cwd || process.cwd(), common);
56
+ return path.basename(gitDir) === '.git' ? path.dirname(gitDir) : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function workspaceRootCandidates(cwd) {
63
+ const primary = taskDb.workspaceRoot(cwd || process.cwd());
64
+ const candidates = [primary];
65
+ const commonRoot = gitCommonWorkspaceRoot(primary);
66
+ if (commonRoot && commonRoot !== primary) candidates.push(commonRoot);
67
+ return candidates;
68
+ }
69
+
70
+ function countsFromRows(rows) {
71
+ const counts = {};
72
+ for (const r of rows) counts[r.status] = Number(r.n || 0);
73
+ return counts;
74
+ }
75
+
76
+ function taskCountRows(db, workspaceRoot) {
77
+ return workspaceRoot
78
+ ? db.prepare('SELECT status, COUNT(*) n FROM tasks WHERE workspace_root = ? GROUP BY status').all(workspaceRoot)
79
+ : db.prepare('SELECT status, COUNT(*) n FROM tasks GROUP BY status').all();
80
+ }
81
+
82
+ function loadTaskCounts({ cwd, all = false } = {}) {
83
+ const candidates = all ? [null] : workspaceRootCandidates(cwd || process.cwd());
46
84
  try {
47
85
  const { DatabaseSync } = require('node:sqlite');
48
- const db = new DatabaseSync(path.join(os.homedir(), '.atris', 'tasks.db'), { readOnly: true });
49
- const rows = db.prepare('SELECT status, COUNT(*) n FROM tasks GROUP BY status').all();
86
+ const dbPath = taskDb.getDbPath();
87
+ if (!fs.existsSync(dbPath)) return { counts: null, workspaceRoot: candidates[0] || null };
88
+ const db = new DatabaseSync(dbPath, { readOnly: true });
89
+ for (const workspaceRoot of candidates) {
90
+ const rows = taskCountRows(db, workspaceRoot);
91
+ if (all || rows.length > 0) {
92
+ db.close();
93
+ return { counts: countsFromRows(rows), workspaceRoot };
94
+ }
95
+ }
50
96
  db.close();
51
- const counts = {};
52
- for (const r of rows) counts[r.status] = r.n;
53
- return counts;
54
- } catch { return null; }
97
+ return { counts: {}, workspaceRoot: candidates[0] || null };
98
+ } catch { return { counts: null, workspaceRoot: candidates[0] || null }; }
99
+ }
100
+
101
+ function taskScopeLabel(scope) {
102
+ return scope.kind === 'global' ? 'global' : 'workspace';
103
+ }
104
+
105
+ function taskScopeLine(scope) {
106
+ if (scope.kind === 'global') return 'global';
107
+ return `${taskScopeLabel(scope)} (${scope.workspace_root})`;
55
108
  }
56
109
 
57
110
  function loadFeatures(cwd) {
@@ -111,9 +164,14 @@ function truthCommand(args = []) {
111
164
  const cwd = process.cwd();
112
165
  const json = args.includes('--json');
113
166
  const summary = args.includes('--summary');
167
+ const all = args.includes('--all');
114
168
 
115
169
  const missions = loadMissions(cwd);
116
- const tasks = loadTaskCounts();
170
+ const taskCounts = loadTaskCounts({ cwd, all });
171
+ const scope = all
172
+ ? { kind: 'global' }
173
+ : { kind: 'workspace', workspace_root: taskCounts.workspaceRoot };
174
+ const tasks = taskCounts.counts;
117
175
  const features = loadFeatures(cwd);
118
176
  const heartbeats = loadHeartbeats();
119
177
 
@@ -125,6 +183,7 @@ function truthCommand(args = []) {
125
183
  const result = {
126
184
  ok: true,
127
185
  generated_at: new Date().toISOString(),
186
+ scope,
128
187
  missions_active: missions.map((m) => ({ id: m.id || m.mission_id, status: m.status, owner: m.owner })),
129
188
  tasks,
130
189
  features: featureTally,
@@ -140,11 +199,12 @@ function truthCommand(args = []) {
140
199
 
141
200
  const line = (s) => console.log(s);
142
201
  if (summary) {
143
- line(`truth: features ${featureTally.proven || 0} proven / ${featureTally.stale || 0} stale / ${featureTally.blocked || 0} blocked / ${featureTally.unproven || 0} unproven · loops ${loopTally.proven || 0} live / ${(loopTally.stale || 0) + (loopTally['never-ran'] || 0)} stale / ${loopTally.blocked || 0} failing · missions ${missions.length} active`);
202
+ line(`truth [${taskScopeLabel(scope)}]: features ${featureTally.proven || 0} proven / ${featureTally.stale || 0} stale / ${featureTally.blocked || 0} blocked / ${featureTally.unproven || 0} unproven · loops ${loopTally.proven || 0} live / ${(loopTally.stale || 0) + (loopTally['never-ran'] || 0)} stale / ${loopTally.blocked || 0} failing · missions ${missions.length} active`);
144
203
  return 0;
145
204
  }
146
205
 
147
206
  line('ATRIS TRUTH — live state, not belief\n');
207
+ line(`Scope: ${taskScopeLine(scope)}`);
148
208
 
149
209
  line(`Missions active: ${missions.length}`);
150
210
  for (const m of missions) line(` ${m.status.padEnd(8)} ${m.owner || '?'} ${m.id || m.mission_id}`);