atris 3.29.0 → 3.30.1

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
@@ -17,6 +17,7 @@ const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
17
17
  || os.userInfo().username
18
18
  || 'unknown';
19
19
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
20
+ const RESULT_SAVED_TEXT_LIMIT = 200;
20
21
  const REVIEW_LANE_LOOP_DEFAULT_MAX_STEPS = 3;
21
22
  const REVIEW_LANE_LOOP_MAX_STEPS = 10;
22
23
  const REVIEW_LANE_RUN_DEFAULT_MAX_RUNS = 3;
@@ -97,8 +98,15 @@ atris task - durable local task state (SQLite, gitignored)
97
98
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
98
99
  atris task ready <id> --proof "..." Agent proof ready; native goal can complete
99
100
  atris task ready <id> --verify "<cmd>" Run <cmd>; only ready if it exits 0 (executed proof)
101
+ atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
102
+ Show the plain Plan before work starts
103
+ atris task ready <id> --proof "..." [--changed "..." --checked "..." --saved "..." --try "..."]
104
+ Agent proof ready; records Result if needed
105
+ atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
106
+ Show the plain Result and store trace on the task
100
107
  atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
101
- atris task accept <id> [--proof "..."] Human accepts proof, marks done
108
+ atris task accept <id> [--proof "..."] [--public]
109
+ Human accepts proof, marks done; --public also publishes AgentXP
102
110
  atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
103
111
  Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
104
112
  atris task revise <id> --note "..." Send reviewed work back to Do
@@ -5021,7 +5029,8 @@ function cmdPlan(args) {
5021
5029
  const pos = positional(args);
5022
5030
  const id = pos[0];
5023
5031
  if (!id) failTask('atris task plan', 'missing_id', 'id required');
5024
- const actor = String(flag(args, '--as') || DEFAULT_OWNER);
5032
+ const actorFlag = flag(args, '--as');
5033
+ const actor = String(actorFlag || DEFAULT_OWNER);
5025
5034
  const goal = textFlag(args, ['--goal', '--objective']);
5026
5035
  const exit = textFlag(args, ['--exit', '--exit-condition']);
5027
5036
  const proofNeeded = textFlag(args, ['--proof-needed', '--proof', '--verify']);
@@ -5033,18 +5042,29 @@ function cmdPlan(args) {
5033
5042
  const taskDb = getTaskDb();
5034
5043
  const db = taskDb.open();
5035
5044
  const taskId = requireTaskId(taskDb, db, id, 'atris task plan');
5045
+ const task = taskDetail(taskDb, db, taskId);
5046
+ const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
5047
+ actor,
5048
+ actorExplicit: typeof actorFlag === 'string' && Boolean(actorFlag.trim()),
5049
+ owner,
5050
+ goal,
5051
+ summary,
5052
+ firstMove,
5053
+ exit,
5054
+ });
5036
5055
  const result = taskDb.stageTask(db, {
5037
5056
  id: taskId,
5038
5057
  actor,
5039
5058
  stage: 'plan',
5040
5059
  goal,
5041
5060
  summary,
5042
- owner,
5061
+ owner: automaticPlan.ownerForStage || owner,
5043
5062
  exit,
5044
5063
  proofNeeded,
5045
5064
  firstMove,
5046
5065
  nextButton,
5047
5066
  confidence,
5067
+ planTrace: automaticPlan.trace,
5048
5068
  });
5049
5069
  if (!result.staged) {
5050
5070
  failTask('atris task plan', result.reason || 'stage_failed', stageErrorDetail('atris task plan', result.reason, result), 1);
@@ -5056,6 +5076,11 @@ function cmdPlan(args) {
5056
5076
  action: 'planned',
5057
5077
  task_id: taskId,
5058
5078
  version: result.event.version,
5079
+ plan_trace: automaticPlan.trace ? {
5080
+ plan: automaticPlan.plan,
5081
+ owner_choice: automaticPlan.ownerChoice,
5082
+ trace: automaticPlan.trace,
5083
+ } : null,
5059
5084
  stage_packet: result.stage_packet,
5060
5085
  projection_path: outPath,
5061
5086
  task: compactTaskFromProjection(projection, taskId),
@@ -5320,6 +5345,611 @@ function taskCommandQuote(value) {
5320
5345
  return `"${text || '...'}"`;
5321
5346
  }
5322
5347
 
5348
+ function cleanPublicText(value, max = 500) {
5349
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
5350
+ if (!text) return '';
5351
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
5352
+ }
5353
+
5354
+ function publicWords(value) {
5355
+ return (String(value || '').toLowerCase().match(/[a-z0-9]{3,}/g) || [])
5356
+ .map(word => word.endsWith('s') && word.length > 4 ? word.slice(0, -1) : word)
5357
+ .filter(word => !new Set([
5358
+ 'and',
5359
+ 'for',
5360
+ 'from',
5361
+ 'into',
5362
+ 'the',
5363
+ 'this',
5364
+ 'that',
5365
+ 'task',
5366
+ 'work',
5367
+ 'with',
5368
+ ]).has(word));
5369
+ }
5370
+
5371
+ function parseMemberFrontmatter(text) {
5372
+ const source = String(text || '');
5373
+ if (!source.startsWith('---')) return {};
5374
+ const end = source.indexOf('\n---', 3);
5375
+ if (end === -1) return {};
5376
+ const block = source.slice(3, end).split(/\r?\n/);
5377
+ const data = {};
5378
+ for (const raw of block) {
5379
+ const match = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
5380
+ if (!match) continue;
5381
+ data[match[1]] = match[2].replace(/^["']|["']$/g, '').trim();
5382
+ }
5383
+ return data;
5384
+ }
5385
+
5386
+ function readTeamMembers(root = process.cwd()) {
5387
+ const teamDir = path.join(root, 'atris', 'team');
5388
+ if (!fs.existsSync(teamDir)) return [];
5389
+ return fs.readdirSync(teamDir, { withFileTypes: true })
5390
+ .filter(entry => entry.isDirectory())
5391
+ .map(entry => {
5392
+ const slug = entry.name;
5393
+ const memberPath = path.join(teamDir, slug, 'MEMBER.md');
5394
+ if (!fs.existsSync(memberPath)) return null;
5395
+ const text = fs.readFileSync(memberPath, 'utf8');
5396
+ const frontmatter = parseMemberFrontmatter(text);
5397
+ return {
5398
+ slug,
5399
+ role: cleanPublicText(frontmatter.role || slug.replace(/[-_]/g, ' '), 120),
5400
+ description: cleanPublicText(frontmatter.description || '', 240),
5401
+ path: memberPath,
5402
+ };
5403
+ })
5404
+ .filter(Boolean);
5405
+ }
5406
+
5407
+ const GENERIC_MEMBER_SLUGS = new Set([
5408
+ '_template',
5409
+ 'coordinator',
5410
+ 'executor',
5411
+ 'generalist',
5412
+ 'navigator',
5413
+ 'supervisor',
5414
+ ]);
5415
+
5416
+ function scoreTeamMember(member, words, tag) {
5417
+ const slug = String(member.slug || '').toLowerCase();
5418
+ const role = String(member.role || '').toLowerCase();
5419
+ const description = String(member.description || '').toLowerCase();
5420
+ const haystack = `${slug} ${role} ${description}`.replace(/[-_]/g, ' ');
5421
+ let score = 0;
5422
+ const cleanTag = String(tag || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
5423
+ if (cleanTag) {
5424
+ if (slug === cleanTag || slug.replace(/[-_]/g, ' ') === cleanTag) score += 12;
5425
+ else if (haystack.includes(cleanTag)) score += 5;
5426
+ }
5427
+ for (const word of words) {
5428
+ if (slug.includes(word)) score += 5;
5429
+ else if (role.includes(word)) score += 3;
5430
+ else if (description.includes(word)) score += 2;
5431
+ }
5432
+ if (GENERIC_MEMBER_SLUGS.has(slug)) score -= 3;
5433
+ return score;
5434
+ }
5435
+
5436
+ function plainMemberDescription(member) {
5437
+ const description = cleanPublicText(member && member.description, 220);
5438
+ if (!description) return '';
5439
+ const lowered = `${description.charAt(0).toLowerCase()}${description.slice(1)}`;
5440
+ return /[.!?]$/.test(lowered) ? lowered : `${lowered}.`;
5441
+ }
5442
+
5443
+ function chooseTaskOwner({ purpose, tag, requestedOwner, root = process.cwd() } = {}) {
5444
+ const members = readTeamMembers(root);
5445
+ const requested = cleanPublicText(requestedOwner, 80);
5446
+ if (requested) {
5447
+ const match = members.find(member => member.slug === requested);
5448
+ if (match) {
5449
+ const description = plainMemberDescription(match);
5450
+ return {
5451
+ owner: match.slug,
5452
+ member: match,
5453
+ source: 'requested',
5454
+ reason: `${match.slug} matches this work${description ? ` because ${description}` : '.'}`,
5455
+ };
5456
+ }
5457
+ return {
5458
+ owner: requested,
5459
+ member: null,
5460
+ source: 'requested',
5461
+ reason: `${requested} was requested, but no matching atris/team member was found.`,
5462
+ };
5463
+ }
5464
+ const words = publicWords(`${purpose || ''} ${tag || ''}`);
5465
+ let best = null;
5466
+ for (const member of members) {
5467
+ const score = scoreTeamMember(member, words, tag);
5468
+ if (!best || score > best.score) best = { member, score };
5469
+ }
5470
+ if (best && best.score > 0) {
5471
+ const member = best.member;
5472
+ const description = plainMemberDescription(member);
5473
+ return {
5474
+ owner: member.slug,
5475
+ member,
5476
+ source: 'team',
5477
+ score: best.score,
5478
+ reason: `${member.slug} fits this work${description ? ` because ${description}` : '.'}`,
5479
+ };
5480
+ }
5481
+ return {
5482
+ owner: DEFAULT_OWNER,
5483
+ member: null,
5484
+ source: 'fallback',
5485
+ reason: `${DEFAULT_OWNER} is handling it because no specific atris/team owner matched this work.`,
5486
+ };
5487
+ }
5488
+
5489
+ function isGenericPlanActor(value) {
5490
+ const actor = cleanPublicText(value, 80).toLowerCase();
5491
+ if (!actor) return true;
5492
+ if (actor === String(DEFAULT_OWNER || '').toLowerCase()) return true;
5493
+ return GENERIC_MEMBER_SLUGS.has(actor) || new Set([
5494
+ 'codex',
5495
+ 'codex-executor',
5496
+ 'claude',
5497
+ 'claude-code',
5498
+ 'cursor',
5499
+ 'devin',
5500
+ ]).has(actor);
5501
+ }
5502
+
5503
+ function taskTextMentionsActor(actor, text) {
5504
+ const actorWords = publicWords(actor);
5505
+ if (!actorWords.length) return false;
5506
+ const words = new Set(publicWords(text));
5507
+ return actorWords.some(word => words.has(word));
5508
+ }
5509
+
5510
+ function buildPublicPlan({ purpose, owner, ownerReason, plan, expected }) {
5511
+ const cleanPurpose = cleanPublicText(purpose, 240);
5512
+ const cleanOwner = cleanPublicText(owner, 80);
5513
+ const cleanReason = cleanPublicText(ownerReason, 240);
5514
+ const cleanPlan = cleanPublicText(plan, 320) || `${cleanOwner || 'The owner'} will make the smallest needed change, then check the result.`;
5515
+ const cleanExpected = cleanPublicText(expected, 240) || 'the check passes and the result is ready to review.';
5516
+ return {
5517
+ purpose: cleanPurpose,
5518
+ owner: cleanOwner,
5519
+ owner_reason: cleanReason,
5520
+ plan: cleanPlan,
5521
+ expected_result: cleanExpected,
5522
+ };
5523
+ }
5524
+
5525
+ function renderPublicPlan(plan) {
5526
+ const lines = [];
5527
+ if (plan.purpose) lines.push(`Purpose: ${plan.purpose}`);
5528
+ if (plan.owner) lines.push(`Owner: ${plan.owner} is handling it.`);
5529
+ if (plan.owner_reason) lines.push(`Why: ${plan.owner_reason}`);
5530
+ if (plan.plan) lines.push(`Plan: ${plan.plan}`);
5531
+ if (plan.expected_result) lines.push(`Expected result: ${plan.expected_result}`);
5532
+ return lines.join('\n');
5533
+ }
5534
+
5535
+ function planTraceData(plan, ownerChoice) {
5536
+ return {
5537
+ schema: 'atris.task_plan_trace.v1',
5538
+ purpose: plan.purpose,
5539
+ owner: plan.owner,
5540
+ owner_reason: plan.owner_reason,
5541
+ plan: plan.plan,
5542
+ expected_result: plan.expected_result,
5543
+ owner_source: ownerChoice && ownerChoice.source || null,
5544
+ owner_score: ownerChoice && ownerChoice.score || null,
5545
+ recorded_at: new Date().toISOString(),
5546
+ };
5547
+ }
5548
+
5549
+ function planTraceNote(plan, ownerChoice) {
5550
+ return `TASK_PLAN_TRACE ${JSON.stringify(planTraceData(plan, ownerChoice))}`;
5551
+ }
5552
+
5553
+ function traceLineFromContent(content, prefix) {
5554
+ const lines = String(content || '').split(/\r?\n/);
5555
+ return lines.find(line => line.startsWith(prefix)) || '';
5556
+ }
5557
+
5558
+ function latestTraceValue(task, prefix, key) {
5559
+ const messages = Array.isArray(task && task.messages) ? task.messages : [];
5560
+ for (let i = messages.length - 1; i >= 0; i--) {
5561
+ const content = String(messages[i] && messages[i].content || '');
5562
+ const line = traceLineFromContent(content, prefix);
5563
+ if (!line) continue;
5564
+ try {
5565
+ const parsed = JSON.parse(line.slice(prefix.length).trim());
5566
+ const value = parsed && parsed[key];
5567
+ if (value !== undefined && value !== null && String(value).trim()) return String(value);
5568
+ } catch (_) {
5569
+ continue;
5570
+ }
5571
+ }
5572
+ return '';
5573
+ }
5574
+
5575
+ function taskHasTrace(task, prefix) {
5576
+ const messages = Array.isArray(task && task.messages) ? task.messages : [];
5577
+ if (messages.some(message => traceLineFromContent(message && message.content, prefix))) return true;
5578
+ const events = Array.isArray(task && task.events) ? task.events : [];
5579
+ return events.some(event => {
5580
+ const payload = event && event.payload && typeof event.payload === 'object' ? event.payload : {};
5581
+ if (traceLineFromContent(payload.stage_packet, prefix)) return true;
5582
+ if (traceLineFromContent(payload.result_packet, prefix)) return true;
5583
+ if (prefix === 'TASK_RESULT_TRACE ' && payload.result_trace && typeof payload.result_trace === 'object') return true;
5584
+ if (prefix === 'TASK_PLAN_TRACE ' && payload.plan_trace && typeof payload.plan_trace === 'object') return true;
5585
+ return false;
5586
+ });
5587
+ }
5588
+
5589
+ function taskPurpose(task) {
5590
+ const metadata = task && task.metadata || {};
5591
+ return cleanPublicText(
5592
+ metadata.task_goal
5593
+ || metadata.goal_objective
5594
+ || metadata.objective
5595
+ || metadata.stage_goal
5596
+ || latestTraceValue(task, 'TASK_PLAN_TRACE ', 'purpose')
5597
+ || task && task.title
5598
+ || '',
5599
+ 240,
5600
+ );
5601
+ }
5602
+
5603
+ function buildPublicResult(task, fields) {
5604
+ const owner = cleanPublicText(
5605
+ fields.owner
5606
+ || latestTraceValue(task, 'TASK_PLAN_TRACE ', 'owner')
5607
+ || taskAssignee(task)
5608
+ || task && task.claimed_by
5609
+ || fields.actor,
5610
+ 80,
5611
+ );
5612
+ const result = {
5613
+ purpose: cleanPublicText(fields.purpose || taskPurpose(task), 240),
5614
+ owner,
5615
+ changed: cleanPublicText(fields.changed, 320),
5616
+ checked: cleanPublicText(fields.checked, 320),
5617
+ passed: cleanPublicText(fields.passed, 240),
5618
+ failed: cleanPublicText(fields.failed, 240),
5619
+ cost: cleanPublicText(fields.cost, 80),
5620
+ saved: cleanPublicText(fields.saved, RESULT_SAVED_TEXT_LIMIT),
5621
+ try_next: cleanPublicText(fields.tryNext, 240),
5622
+ status: cleanPublicText(fields.status, 160) || 'ready for review',
5623
+ };
5624
+ return result;
5625
+ }
5626
+
5627
+ function renderPublicResult(result) {
5628
+ return [
5629
+ `Changed: ${cleanPublicText(result && result.changed, 320) || 'changed the requested work'}`,
5630
+ `Checked: ${cleanPublicText(result && result.checked, 320) || 'checked the result'}`,
5631
+ `Try: ${cleanPublicText(result && result.try_next, 240) || 'try the changed work'}`,
5632
+ ].join('\n');
5633
+ }
5634
+
5635
+ function latestResultTrace(task) {
5636
+ const messages = Array.isArray(task && task.messages) ? task.messages : [];
5637
+ for (let i = messages.length - 1; i >= 0; i--) {
5638
+ const content = String(messages[i] && messages[i].content || '');
5639
+ const line = traceLineFromContent(content, 'TASK_RESULT_TRACE ');
5640
+ if (!line) continue;
5641
+ try {
5642
+ const parsed = JSON.parse(line.slice('TASK_RESULT_TRACE '.length).trim());
5643
+ if (parsed && typeof parsed === 'object') return parsed;
5644
+ } catch (_) {}
5645
+ }
5646
+
5647
+ const events = Array.isArray(task && task.events) ? task.events : [];
5648
+ for (let i = events.length - 1; i >= 0; i--) {
5649
+ const payload = events[i] && events[i].payload && typeof events[i].payload === 'object'
5650
+ ? events[i].payload
5651
+ : {};
5652
+ if (payload.result_trace && typeof payload.result_trace === 'object') return payload.result_trace;
5653
+ const packetLine = traceLineFromContent(payload.result_packet, 'TASK_RESULT_TRACE ');
5654
+ if (!packetLine) continue;
5655
+ try {
5656
+ const parsed = JSON.parse(packetLine.slice('TASK_RESULT_TRACE '.length).trim());
5657
+ if (parsed && typeof parsed === 'object') return parsed;
5658
+ } catch (_) {}
5659
+ }
5660
+ return null;
5661
+ }
5662
+
5663
+ function buildAcceptHumanResult({ task, proof, nextTask, publicSync }) {
5664
+ const trace = latestResultTrace(task) || {};
5665
+ const traceChanged = cleanPublicText(trace.changed, 320);
5666
+ const changed = traceChanged === 'prepared the work for review'
5667
+ ? 'accepted the completed work'
5668
+ : traceChanged
5669
+ || cleanPublicText(task && task.title, 260)
5670
+ || 'accepted the completed work';
5671
+ let checked = cleanPublicText(trace.checked, 320)
5672
+ || cleanPublicText(proof, 320)
5673
+ || 'checked the proof';
5674
+ if (publicSync && publicSync.enabled === true && !publicSync.ok) {
5675
+ const error = cleanPublicText(publicSync.error || 'publish failed', 140);
5676
+ checked = `${checked}; AgentXP publish failed${error ? ` (${error})` : ''}`;
5677
+ }
5678
+ const tryNext = cleanPublicText(nextTask, 240)
5679
+ || cleanPublicText(trace.try_next, 240)
5680
+ || 'try the changed work';
5681
+ return { changed, checked, try_next: tryNext };
5682
+ }
5683
+
5684
+ function renderAcceptLanding({ task, proof, nextTask, publicSync }) {
5685
+ return renderPublicResult(buildAcceptHumanResult({
5686
+ task,
5687
+ proof,
5688
+ nextTask,
5689
+ publicSync,
5690
+ }));
5691
+ }
5692
+
5693
+ async function publishAcceptAgentXp(args, actor) {
5694
+ const token = flag(args, '--token');
5695
+ const syncArgs = ['--all', '--root', process.cwd(), '--public', '--as', actor];
5696
+ if (typeof token === 'string' && token.trim()) syncArgs.push('--token', token.trim());
5697
+ try {
5698
+ const { syncAgentXp } = require('../commands/xp');
5699
+ const result = await syncAgentXp(syncArgs);
5700
+ const server = result && result.server ? result.server : {};
5701
+ const publicCount = Number(server.public_accepted_count);
5702
+ const acceptedCount = Number(server.accepted_count);
5703
+ const published = (
5704
+ (Number.isFinite(publicCount) && publicCount > 0)
5705
+ || (Number.isFinite(acceptedCount) && acceptedCount > 0 && server.private_agentxp !== true)
5706
+ );
5707
+ return {
5708
+ enabled: true,
5709
+ ok: published,
5710
+ result,
5711
+ error: published ? null : 'server accepted no public AgentXP rows',
5712
+ };
5713
+ } catch (error) {
5714
+ return {
5715
+ enabled: true,
5716
+ ok: false,
5717
+ error: error && error.message ? error.message : String(error),
5718
+ };
5719
+ }
5720
+ }
5721
+
5722
+ function todayResultLogName() {
5723
+ const now = new Date();
5724
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
5725
+ }
5726
+
5727
+ function appendResultOwnerLog(root, task, result) {
5728
+ const owner = cleanPublicText(result && result.owner, 80);
5729
+ if (!owner || !/^[A-Za-z0-9._-]+$/.test(owner)) return null;
5730
+ const memberFile = path.join(root, 'atris', 'team', owner, 'MEMBER.md');
5731
+ if (!fs.existsSync(memberFile)) return null;
5732
+ const logName = todayResultLogName();
5733
+ const logDir = path.join(root, 'atris', 'team', owner, 'logs');
5734
+ fs.mkdirSync(logDir, { recursive: true });
5735
+ const logPath = path.join(logDir, logName);
5736
+ const stamp = new Date().toTimeString().slice(0, 5);
5737
+ const lines = [
5738
+ `## ${stamp} - Result`,
5739
+ `- task: ${taskRef(task)}`,
5740
+ `- purpose: ${result.purpose || ''}`,
5741
+ `- result: ${result.changed || ''}`,
5742
+ `- checked: ${result.checked || ''}`,
5743
+ `- saved: ${result.saved || ''}`,
5744
+ `- try: ${result.try_next || ''}`,
5745
+ `- status: ${result.status || ''}`,
5746
+ '',
5747
+ ];
5748
+ fs.appendFileSync(logPath, lines.join('\n'), 'utf8');
5749
+ return {
5750
+ member_log_path: path.relative(root, logPath),
5751
+ };
5752
+ }
5753
+
5754
+ function resultTraceData(result, fields) {
5755
+ return {
5756
+ schema: 'atris.task_result_trace.v1',
5757
+ purpose: result.purpose,
5758
+ owner: result.owner || null,
5759
+ changed: result.changed,
5760
+ checked: result.checked,
5761
+ passed: result.passed || null,
5762
+ failed: result.failed || null,
5763
+ cost: result.cost || null,
5764
+ saved: result.saved || null,
5765
+ try_next: result.try_next || null,
5766
+ status: result.status,
5767
+ files: cleanPublicText(fields.files, 500) || null,
5768
+ commands: cleanPublicText(fields.commands, 500) || null,
5769
+ member_log_path: fields.savedPaths && fields.savedPaths.member_log_path || null,
5770
+ recorded_at: new Date().toISOString(),
5771
+ };
5772
+ }
5773
+
5774
+ function resultTraceNote(result, fields) {
5775
+ return `TASK_RESULT_TRACE ${JSON.stringify(resultTraceData(result, fields))}`;
5776
+ }
5777
+
5778
+ function cmdPlanPreview(args) {
5779
+ const pos = positional(args);
5780
+ const purpose = cleanPublicText(textFlag(args, ['--purpose', '--goal', '--objective']) || pos.join(' '), 240);
5781
+ if (!purpose) failTask('atris task plan-preview', 'missing_purpose', 'purpose required');
5782
+ const tag = textFlag(args, ['--tag']);
5783
+ const requestedOwner = textFlag(args, ['--owner', '--as', '--member']);
5784
+ const planText = textFlag(args, ['--plan', '--action', '--first-move']);
5785
+ const expected = textFlag(args, ['--expected', '--expected-result', '--exit']);
5786
+ const recordRef = textFlag(args, ['--task', '--record']);
5787
+ const taskDb = getTaskDb();
5788
+ const db = taskDb.open();
5789
+ const ownerChoice = chooseTaskOwner({
5790
+ purpose,
5791
+ tag,
5792
+ requestedOwner,
5793
+ root: taskDb.workspaceRoot(),
5794
+ });
5795
+ const publicPlan = buildPublicPlan({
5796
+ purpose,
5797
+ owner: ownerChoice.owner,
5798
+ ownerReason: ownerChoice.reason,
5799
+ plan: planText,
5800
+ expected,
5801
+ });
5802
+ let recorded = null;
5803
+ if (recordRef) {
5804
+ const taskId = requireTaskId(taskDb, db, recordRef, 'atris task plan-preview');
5805
+ const note = taskDb.noteTask(db, {
5806
+ id: taskId,
5807
+ actor: publicPlan.owner || DEFAULT_OWNER,
5808
+ content: planTraceNote(publicPlan, ownerChoice),
5809
+ });
5810
+ if (!note.noted) failTask('atris task plan-preview', note.reason || 'note_failed', `plan-preview failed: ${note.reason || 'note_failed'}`, 1);
5811
+ const { outPath } = writeDefaultProjection(taskDb, db);
5812
+ recorded = {
5813
+ task_id: taskId,
5814
+ version: note.event.version,
5815
+ projection_path: outPath,
5816
+ };
5817
+ }
5818
+ if (wantsJson(args)) {
5819
+ printJson({
5820
+ ok: true,
5821
+ action: 'plan_preview',
5822
+ plan: publicPlan,
5823
+ owner_choice: {
5824
+ owner: ownerChoice.owner,
5825
+ source: ownerChoice.source,
5826
+ score: ownerChoice.score || null,
5827
+ member: ownerChoice.member ? {
5828
+ slug: ownerChoice.member.slug,
5829
+ role: ownerChoice.member.role,
5830
+ description: ownerChoice.member.description,
5831
+ } : null,
5832
+ },
5833
+ recorded,
5834
+ text: renderPublicPlan(publicPlan),
5835
+ });
5836
+ return;
5837
+ }
5838
+ console.log(renderPublicPlan(publicPlan));
5839
+ }
5840
+
5841
+ function buildAutomaticPlanTrace(taskDb, task, { actor, actorExplicit = false, owner, goal, summary, firstMove, exit } = {}) {
5842
+ if (!task) return { trace: null, plan: null, ownerChoice: null, ownerForStage: owner || null };
5843
+ const metadata = task.metadata || {};
5844
+ const purpose = cleanPublicText(goal, 240) || taskPurpose(task);
5845
+ const claimedOwner = cleanPublicText(task.claimed_by, 80);
5846
+ const actorNamed = taskTextMentionsActor(actor, `${purpose} ${task.title || ''} ${task.tag || ''}`);
5847
+ const requestedActor = actorExplicit && (!isGenericPlanActor(actor) || actorNamed) ? actor : null;
5848
+ const requestedOwner = owner || claimedOwner || requestedActor || null;
5849
+ const ownerChoice = chooseTaskOwner({
5850
+ purpose,
5851
+ tag: task.tag,
5852
+ requestedOwner,
5853
+ root: taskDb.workspaceRoot(),
5854
+ });
5855
+ const publicPlan = buildPublicPlan({
5856
+ purpose,
5857
+ owner: ownerChoice.owner,
5858
+ ownerReason: ownerChoice.reason,
5859
+ plan: firstMove || summary || metadata.first_move || metadata.stage_summary || '',
5860
+ expected: exit || metadata.exit_condition || '',
5861
+ });
5862
+ return {
5863
+ trace: planTraceData(publicPlan, ownerChoice),
5864
+ plan: publicPlan,
5865
+ ownerChoice: {
5866
+ owner: ownerChoice.owner,
5867
+ source: ownerChoice.source,
5868
+ score: ownerChoice.score || null,
5869
+ },
5870
+ ownerForStage: publicPlan.owner || actor || DEFAULT_OWNER,
5871
+ };
5872
+ }
5873
+
5874
+ function buildAutomaticResultTrace(taskDb, db, taskId, { actor, proof, changed, checked, passed, failed, cost, saved, tryNext, status, files, commands } = {}) {
5875
+ const task = taskDetail(taskDb, db, taskId);
5876
+ if (!task || taskHasTrace(task, 'TASK_RESULT_TRACE ')) return null;
5877
+ const fields = {
5878
+ actor,
5879
+ changed: cleanPublicText(changed, 320) || 'prepared the work for review',
5880
+ checked: cleanPublicText(checked, 320) || cleanPublicText(proof, 320),
5881
+ passed: cleanPublicText(passed, 240),
5882
+ failed: cleanPublicText(failed, 240),
5883
+ cost: cleanPublicText(cost, 80),
5884
+ saved: cleanPublicText(saved, RESULT_SAVED_TEXT_LIMIT) || 'task trace was updated',
5885
+ tryNext: cleanPublicText(tryNext, 240) || 'review the proof and try the changed work',
5886
+ status: cleanPublicText(status, 160) || 'ready for review',
5887
+ files,
5888
+ commands,
5889
+ };
5890
+ const result = buildPublicResult(task, fields);
5891
+ const savedPaths = appendResultOwnerLog(taskDb.workspaceRoot(), task, result);
5892
+ const trace = resultTraceData(result, { ...fields, savedPaths });
5893
+ return {
5894
+ result,
5895
+ trace,
5896
+ saved_paths: savedPaths,
5897
+ };
5898
+ }
5899
+
5900
+ function cmdResult(args) {
5901
+ const pos = positional(args);
5902
+ const id = pos[0];
5903
+ if (!id) failTask('atris task result', 'missing_id', 'id required');
5904
+ const fields = {
5905
+ purpose: textFlag(args, ['--purpose', '--goal', '--objective']),
5906
+ changed: textFlag(args, ['--changed', '--result', '--done']),
5907
+ checked: textFlag(args, ['--checked', '--check', '--verified']),
5908
+ passed: textFlag(args, ['--passed', '--pass']),
5909
+ failed: textFlag(args, ['--failed', '--fail']),
5910
+ cost: textFlag(args, ['--cost']),
5911
+ saved: textFlag(args, ['--saved', '--savings']),
5912
+ tryNext: textFlag(args, ['--try', '--try-next', '--handoff']),
5913
+ status: textFlag(args, ['--status']),
5914
+ files: textFlag(args, ['--files']),
5915
+ commands: textFlag(args, ['--commands', '--command']),
5916
+ };
5917
+ if (!fields.changed) failTask('atris task result', 'changed_required', '--changed required');
5918
+ if (!fields.checked) failTask('atris task result', 'checked_required', '--checked required');
5919
+ if (!fields.tryNext) failTask('atris task result', 'try_required', '--try required');
5920
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
5921
+ const taskDb = getTaskDb();
5922
+ const db = taskDb.open();
5923
+ const taskId = requireTaskId(taskDb, db, id, 'atris task result');
5924
+ const task = taskDetail(taskDb, db, taskId);
5925
+ if (!task) failTask('atris task result', 'not_found', `task not found: ${id}`, 1);
5926
+ const result = buildPublicResult(task, { ...fields, actor });
5927
+ const savedPaths = appendResultOwnerLog(taskDb.workspaceRoot(), task, result);
5928
+ const note = taskDb.noteTask(db, {
5929
+ id: taskId,
5930
+ actor,
5931
+ content: resultTraceNote(result, { ...fields, savedPaths }),
5932
+ });
5933
+ if (!note.noted) failTask('atris task result', note.reason || 'note_failed', `result failed: ${note.reason || 'note_failed'}`, 1);
5934
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
5935
+ const text = renderPublicResult(result);
5936
+ if (wantsJson(args)) {
5937
+ printJson({
5938
+ ok: true,
5939
+ action: 'result',
5940
+ task_id: taskId,
5941
+ version: note.event.version,
5942
+ projection_path: outPath,
5943
+ result,
5944
+ saved_paths: savedPaths,
5945
+ text,
5946
+ task: compactTaskFromProjection(projection, taskId),
5947
+ });
5948
+ return;
5949
+ }
5950
+ console.log(text);
5951
+ }
5952
+
5323
5953
  function taskPageGoal(task) {
5324
5954
  const metadata = task && task.metadata || {};
5325
5955
  const candidates = [
@@ -5764,16 +6394,25 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
5764
6394
  let episode = null;
5765
6395
  let xpProjection = null;
5766
6396
  if (current === 'backlog') {
6397
+ const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
6398
+ actor,
6399
+ owner: actor,
6400
+ goal,
6401
+ summary,
6402
+ firstMove: String(options.firstMove || ''),
6403
+ exit: String(options.exit || ''),
6404
+ });
5767
6405
  const planned = taskDb.stageTask(db, {
5768
6406
  id: taskId,
5769
6407
  actor,
5770
6408
  stage: 'plan',
5771
6409
  goal,
5772
6410
  summary,
5773
- owner: actor,
6411
+ owner: automaticPlan.ownerForStage || actor,
5774
6412
  exit: String(options.exit || ''),
5775
6413
  proofNeeded: String(options.proofNeeded || ''),
5776
6414
  firstMove: String(options.firstMove || ''),
6415
+ planTrace: automaticPlan.trace,
5777
6416
  });
5778
6417
  if (!planned.staged) taskStepFailure('atris task step', planned, actionPage);
5779
6418
  stepAction = 'planned';
@@ -5807,7 +6446,15 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
5807
6446
  }
5808
6447
  const lesson = String(options.lesson || '');
5809
6448
  const nextTask = String(options.nextTask || '');
5810
- const ready = taskDb.readyTask(db, { id: taskId, actor, proof, lesson, nextTask });
6449
+ const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, { actor, proof });
6450
+ const ready = taskDb.readyTask(db, {
6451
+ id: taskId,
6452
+ actor,
6453
+ proof,
6454
+ lesson,
6455
+ nextTask,
6456
+ resultTrace: resultTrace && resultTrace.trace,
6457
+ });
5811
6458
  if (!ready.ready) taskStepFailure('atris task step', ready, actionPage);
5812
6459
  task = taskDetail(taskDb, db, taskId) || task;
5813
6460
  stepAction = 'ready';
@@ -6343,15 +6990,33 @@ function cmdReady(args) {
6343
6990
  const lesson = flag(args, '--lesson') || '';
6344
6991
  const nextTaskInput = normalizeReviewNextTaskInput(typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '');
6345
6992
  const actor = String(flag(args, '--as') || DEFAULT_OWNER);
6993
+ const resultFields = {
6994
+ changed: textFlag(args, ['--changed', '--result', '--done']),
6995
+ checked: textFlag(args, ['--checked', '--check', '--verified']),
6996
+ passed: textFlag(args, ['--passed', '--pass']),
6997
+ failed: textFlag(args, ['--failed', '--fail']),
6998
+ cost: textFlag(args, ['--cost']),
6999
+ saved: textFlag(args, ['--saved', '--savings']),
7000
+ tryNext: textFlag(args, ['--try', '--try-next', '--handoff']),
7001
+ status: textFlag(args, ['--status']),
7002
+ files: textFlag(args, ['--files']),
7003
+ commands: textFlag(args, ['--commands', '--command']),
7004
+ };
6346
7005
  const taskDb = getTaskDb();
6347
7006
  const db = taskDb.open();
6348
7007
  const taskId = requireTaskId(taskDb, db, id, 'atris task ready');
7008
+ const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
7009
+ actor,
7010
+ proof: String(proof),
7011
+ ...resultFields,
7012
+ });
6349
7013
  const result = taskDb.readyTask(db, {
6350
7014
  id: taskId,
6351
7015
  actor,
6352
7016
  proof: String(proof),
6353
7017
  lesson: typeof lesson === 'string' ? lesson : '',
6354
7018
  nextTask: nextTaskInput.nextTask,
7019
+ resultTrace: resultTrace && resultTrace.trace,
6355
7020
  });
6356
7021
  if (!result.ready) {
6357
7022
  console.error(`ready failed: ${result.reason}`);
@@ -6397,6 +7062,7 @@ function cmdReady(args) {
6397
7062
  review_pass_count: result.event.payload.review_pass_count,
6398
7063
  agent_certified: agentCertified,
6399
7064
  handoff,
7065
+ result_trace: resultTrace,
6400
7066
  ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
6401
7067
  projection_path: outPath,
6402
7068
  task: compactTaskFromProjection(projection, taskId),
@@ -6404,13 +7070,14 @@ function cmdReady(args) {
6404
7070
  return;
6405
7071
  }
6406
7072
  console.log(`ready ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version} pending approval`);
7073
+ if (resultTrace) console.log('Result trace recorded.');
6407
7074
  console.log(handoff.rule);
6408
7075
  for (const hint of policyHints) {
6409
7076
  console.log(`policy (${hint.id}): ${hint.hint}`);
6410
7077
  }
6411
7078
  }
6412
7079
 
6413
- function cmdAccept(args) {
7080
+ async function cmdAccept(args) {
6414
7081
  const pos = positional(args);
6415
7082
  const id = pos[0];
6416
7083
  if (!id) {
@@ -6490,6 +7157,7 @@ function cmdAccept(args) {
6490
7157
  // Inform the gate, never block it: show what the receipts named in the proof
6491
7158
  // actually say so the accepting human isn't trusting prose.
6492
7159
  const evidence = extractReceiptEvidence(proof, projection.workspace_root || process.cwd());
7160
+ const publicSync = hasFlag(args, '--public') ? await publishAcceptAgentXp(args, actor) : null;
6493
7161
  if (wantsJson(args)) {
6494
7162
  printJson({
6495
7163
  ok: true,
@@ -6499,21 +7167,21 @@ function cmdAccept(args) {
6499
7167
  reward: reviewed.episode.reward.value,
6500
7168
  episode: reviewed.episode,
6501
7169
  evidence,
7170
+ public_sync: publicSync,
6502
7171
  xp_projection: xpProjection,
6503
7172
  projection_path: outPath,
6504
7173
  task: compactTaskFromProjection(projection, taskId),
6505
7174
  });
7175
+ if (publicSync && !publicSync.ok) process.exitCode = 1;
6506
7176
  return;
6507
7177
  }
6508
- console.log(`accepted ${taskRef(compactTaskFromProjection(projection, taskId))} reward=${reviewed.episode.reward.value}`);
6509
- if (evidence) {
6510
- evidence.receipts.forEach((receipt) => {
6511
- const verdict = receipt.verifier_passed === true ? ' verifier:passed'
6512
- : receipt.verifier_passed === false ? ' verifier:FAILED' : '';
6513
- console.log(` receipt: ${receipt.path}${verdict}`);
6514
- });
6515
- evidence.missing.forEach((missingPath) => console.log(` receipt: ${missingPath} MISSING`));
6516
- }
7178
+ console.log(renderAcceptLanding({
7179
+ task: taskDetail(taskDb, db, taskId) || compactTaskFromProjection(projection, taskId),
7180
+ proof,
7181
+ nextTask,
7182
+ publicSync,
7183
+ }));
7184
+ if (publicSync && !publicSync.ok) process.exitCode = 1;
6517
7185
  }
6518
7186
 
6519
7187
  function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
@@ -7975,22 +8643,39 @@ async function handleTaskApi(req, res, taskDb, db) {
7975
8643
  });
7976
8644
  }
7977
8645
  if (op === 'plan') {
8646
+ const actor = String(body.actor || DEFAULT_OWNER);
8647
+ const goal = String(body.goal || body.objective || '');
8648
+ const summary = String(body.summary || body.plan || '');
8649
+ const owner = String(body.owner || body.assignee || '');
8650
+ const exit = String(body.exit || body.exit_condition || '');
8651
+ const firstMove = String(body.first_move || body.firstMove || body.first || '');
8652
+ const task = taskDetail(taskDb, db, taskId);
8653
+ const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
8654
+ actor,
8655
+ actorExplicit: Boolean(body.actor),
8656
+ owner,
8657
+ goal,
8658
+ summary,
8659
+ firstMove,
8660
+ exit,
8661
+ });
7978
8662
  const result = taskDb.stageTask(db, {
7979
8663
  id: taskId,
7980
- actor: String(body.actor || DEFAULT_OWNER),
8664
+ actor,
7981
8665
  stage: 'plan',
7982
- goal: String(body.goal || body.objective || ''),
7983
- summary: String(body.summary || body.plan || ''),
7984
- owner: String(body.owner || body.assignee || ''),
7985
- exit: String(body.exit || body.exit_condition || ''),
8666
+ goal,
8667
+ summary,
8668
+ owner: automaticPlan.ownerForStage || owner,
8669
+ exit,
7986
8670
  proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
7987
- firstMove: String(body.first_move || body.firstMove || body.first || ''),
8671
+ firstMove,
7988
8672
  nextButton: String(body.next_button || body.nextButton || ''),
7989
8673
  confidence: body.confidence,
8674
+ planTrace: automaticPlan.trace,
7990
8675
  });
7991
8676
  if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
7992
8677
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
7993
- return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
8678
+ return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, plan_trace: automaticPlan.trace, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
7994
8679
  }
7995
8680
  if (op === 'do') {
7996
8681
  const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
@@ -8097,12 +8782,28 @@ async function handleTaskApi(req, res, taskDb, db) {
8097
8782
  const proofIssue = meaningfulTaskProofIssue(proof);
8098
8783
  if (proofIssue) return sendProofIssue(res, proof, proofIssue);
8099
8784
  const nextTaskInput = normalizeReviewNextTaskInput(body.next);
8785
+ const actor = String(body.actor || DEFAULT_OWNER);
8786
+ const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
8787
+ actor,
8788
+ proof,
8789
+ changed: body.changed || body.result || body.done,
8790
+ checked: body.checked || body.check || body.verified,
8791
+ passed: body.passed || body.pass,
8792
+ failed: body.failed || body.fail,
8793
+ cost: body.cost,
8794
+ saved: body.saved || body.savings,
8795
+ tryNext: body.try_next || body.tryNext || body.try || body.handoff,
8796
+ status: body.status,
8797
+ files: body.files,
8798
+ commands: body.commands || body.command,
8799
+ });
8100
8800
  const result = taskDb.readyTask(db, {
8101
8801
  id: taskId,
8102
- actor: String(body.actor || DEFAULT_OWNER),
8802
+ actor,
8103
8803
  proof,
8104
8804
  lesson: String(body.lesson || ''),
8105
8805
  nextTask: nextTaskInput.nextTask,
8806
+ resultTrace: resultTrace && resultTrace.trace,
8106
8807
  });
8107
8808
  if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
8108
8809
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
@@ -8110,6 +8811,7 @@ async function handleTaskApi(req, res, taskDb, db) {
8110
8811
  ok: true,
8111
8812
  action: 'ready',
8112
8813
  task_id: taskId,
8814
+ result_trace: resultTrace,
8113
8815
  ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
8114
8816
  projection_path: outPath,
8115
8817
  task: taskFromProjection(projection, taskId),
@@ -8300,6 +9002,10 @@ async function run(args) {
8300
9002
  case 'continue':
8301
9003
  return cmdContinueWork(rest);
8302
9004
  case 'chat': return cmdChat(rest);
9005
+ case 'plan-preview':
9006
+ case 'preview-plan':
9007
+ case 'plan-card':
9008
+ return cmdPlanPreview(rest);
8303
9009
  case 'note': return cmdNote(rest);
8304
9010
  case 'say': return cmdNote(rest);
8305
9011
  case 'show': return cmdShow(rest);
@@ -8309,6 +9015,7 @@ async function run(args) {
8309
9015
  case 'chat-review':
8310
9016
  return cmdReviewChat(rest);
8311
9017
  case 'ready': return cmdReady(rest);
9018
+ case 'result': return cmdResult(rest);
8312
9019
  case 'accept': return cmdAccept(rest);
8313
9020
  case 'auto-accept-certified':
8314
9021
  case 'auto-accept':