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/README.md +43 -1
- package/ax +364 -11
- package/bin/atris.js +53 -4
- package/commands/autoland.js +8 -2
- package/commands/engine.js +299 -0
- package/commands/integrations.js +147 -0
- package/commands/member.js +63 -10
- package/commands/mission.js +119 -18
- package/commands/task.js +173 -14
- package/commands/truth.js +70 -10
- package/lib/fleet.js +354 -0
- package/lib/inspect-fields.js +174 -0
- package/lib/runner-command.js +24 -0
- package/lib/task-db.js +38 -0
- package/package.json +2 -1
- package/scripts/agent_worktree.py +72 -0
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]
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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}`);
|