atris 3.57.4 → 3.58.6

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 (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +66 -21
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +59 -31
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +216 -50
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +101 -48
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +93 -22
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +1019 -123
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytnotes +82 -5
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +47 -9
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -19,6 +19,7 @@ const { startFirstTalk } = require('../lib/context-gatherer');
19
19
  const { isNonInteractive } = require('../lib/noninteractive');
20
20
  const { loadContext } = require('../lib/state-detection');
21
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
22
+ const { commitReviewLearning } = require('./learn');
22
23
 
23
24
  function wrapWorkflowText(text, width = 76) {
24
25
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
@@ -612,7 +613,7 @@ async function planAtris(userInput = null) {
612
613
 
613
614
  if (showFactory) {
614
615
  console.log('┌─────────────────────────────────────────────────────────────┐');
615
- console.log('│ Atris Plan Navigator Agent Activated │');
616
+ console.log('│ Atris Plan - Navigator Agent Activated │');
616
617
  console.log('└─────────────────────────────────────────────────────────────┘');
617
618
  console.log('');
618
619
 
@@ -641,7 +642,7 @@ async function planAtris(userInput = null) {
641
642
  console.log(`- Navigator spec: ${navigatorPath || 'atris/team/navigator/MEMBER.md (missing)'}`);
642
643
  console.log(`- Persona: ${personaFileRef || 'atris/PERSONA.md (missing)'}`);
643
644
  const mapDisplay = mapFileRef
644
- ? `${mapFileRef}${mapIsPlaceholder ? ' (placeholder generate first)' : ''}`
645
+ ? `${mapFileRef}${mapIsPlaceholder ? ' (placeholder, generate first)' : ''}`
645
646
  : 'atris/MAP.md (missing)';
646
647
  console.log(`- MAP: ${mapDisplay}`);
647
648
  console.log(`- TODO: ${taskSourcePath || 'atris/TODO.md (missing)'}`);
@@ -710,11 +711,12 @@ async function planAtris(userInput = null) {
710
711
  console.log('');
711
712
  }
712
713
  console.log('Workflow:');
713
- console.log('1) ASCII visualize + wait for approval');
714
+ console.log('1) ASCII visualize; use existing approval for this scope, otherwise wait for approval');
714
715
  console.log('2) Run the Confidence Gate before writing tasks');
715
716
  printConfidenceGate(' ');
716
- console.log('3) Write tasks to atris/TODO.md under ## Backlog');
717
- console.log(' Format: - **T#:** Description [explore|execute]');
717
+ console.log('3) Create each task in the live task plane: `atris task add "<title>" --tag <tag>`, then `atris task plan <id> --goal ... --exit ... --proof-needed ...`');
718
+ console.log(' Keep a delegated owner: `atris task delegate "<title>" --to <member>`; plan keeps that owner unless you pass --owner');
719
+ console.log(' atris/TODO.md is a generated view: never hand-edit it, regenerate with `atris task render --out atris/TODO.md`');
718
720
  console.log('4) Log to atris/team/navigator/logs/YYYY-MM-DD.md');
719
721
  console.log(' (Task, Delivered, User reaction, Pattern)');
720
722
  if (atris2Mode) {
@@ -783,7 +785,7 @@ async function planAtris(userInput = null) {
783
785
  userPrompt += `Your job (execute these steps):\n\n`;
784
786
  userPrompt += `STEP 1: Generate ASCII visualizations for user approval\n`;
785
787
  userPrompt += ` Create diagrams showing architecture, flows, schemas, UI/UX.\n`;
786
- userPrompt += ` SHOW these diagrams and wait for approval before proceeding.\n\n`;
788
+ userPrompt += ` SHOW these diagrams; use existing approval for this scope, otherwise wait for approval before proceeding.\n\n`;
787
789
  userPrompt += `STEP 2: Run the Confidence Gate before writing tasks\n`;
788
790
  userPrompt += confidenceGatePrompt('plan') + `\n\n`;
789
791
  userPrompt += `STEP 3: Break approved ideas into concrete tasks\n`;
@@ -799,7 +801,7 @@ async function planAtris(userInput = null) {
799
801
  userPrompt += `STEP 5: Log to your journal\n`;
800
802
  userPrompt += ` - Write to atris/team/navigator/logs/YYYY-MM-DD.md\n`;
801
803
  userPrompt += ` - Include: Task, Delivered, User reaction, Pattern\n`;
802
- userPrompt += ` - Your journal is how you learn record what worked\n\n`;
804
+ userPrompt += ` - Your journal is how you learn, so record what worked\n\n`;
803
805
  userPrompt += `Start planning now. Read MAP.md for file references.`;
804
806
 
805
807
  console.log('');
@@ -866,6 +868,73 @@ async function planAtris(userInput = null) {
866
868
  // Prompt mode continues with existing output (already logged above)
867
869
  }
868
870
 
871
+ // Both executor surfaces require the exact live record before any work.
872
+ // The rendered list omits raw instructions and cannot select the mission.
873
+ function executorDispatchForTask(task) {
874
+ const metadata = task && task.metadata || {};
875
+ return {
876
+ taskId: task && task.id || '',
877
+ owner: metadata.assigned_to || task && task.claimed_by || metadata.stage_owner || '',
878
+ goalId: metadata.goal_id || '',
879
+ missionId: metadata.mission_id || '',
880
+ objective: metadata.task_goal || metadata.goal_objective || metadata.stage_goal || '',
881
+ workspaceRoot: task && task.workspace_root || '',
882
+ };
883
+ }
884
+
885
+ function validateExecutorDispatch(expected = {}, task) {
886
+ if (!expected.taskId || !expected.owner || (!expected.goalId && !expected.missionId && !expected.objective)) {
887
+ return { ok: false, reason: 'missing task, owner, or mission in dispatch' };
888
+ }
889
+ const actual = executorDispatchForTask(task);
890
+ if (!task || ['taskId', 'owner', 'goalId', 'missionId', 'objective', 'workspaceRoot']
891
+ .some(key => (expected[key] || '') !== (actual[key] || ''))) {
892
+ return { ok: false, reason: 'task, owner, or mission changed since dispatch' };
893
+ }
894
+ if (!['open', 'claimed'].includes(task.status)
895
+ || (task.status === 'claimed' && task.claimed_by !== expected.owner)) {
896
+ return { ok: false, reason: 'task is inactive or claimed by another owner' };
897
+ }
898
+ return { ok: true };
899
+ }
900
+
901
+ function executorTaskHandoff({ taskId = '', owner = '', goalId = '', missionId = '', objective = '' } = {}) {
902
+ const ref = taskId || '<task-id>';
903
+ const expected = JSON.stringify({ taskId: taskId || null, owner: owner || null, goalId: goalId || null, missionId: missionId || null, objective: objective || null });
904
+ return `Expected dispatch: ${expected}. Before claiming or editing, run \`atris task show ${ref} --json\` for the exact current dispatched task. If the expected task ID, owner, or mission is missing, stop; no edits are allowed. Do not select another displayed row. Check the ID, current mission, active status (open or claimed by the same owner), and functional owner against these expected values. Refuse a stale or mismatched task. Read the raw metadata, requirements, events, and verify command for exact paths, flags, and engine/model instructions; the explanation and rendered TODO are summaries only. Then claim that same task with \`atris task claim ${ref} --as <task-owner>\`, using its recorded functional owner and keeping the engine separate.`;
905
+ }
906
+
907
+ // The executor prompt sent to a backend agent in --execute mode. Kept as a
908
+ // pure function so the emitted instructions can be tested without a network:
909
+ // the agent claims and finishes work through the live task plane, and treats
910
+ // atris/TODO.md as a generated view it never hand-edits.
911
+ function executorAgentPrompt({ filteredTasks = '', taskSource = 'atris/TODO.md', context = 'UNKNOWN', taskId = '', owner = '', goalId = '', missionId = '', objective = '' } = {}) {
912
+ let userPrompt = `⚠️ CRITICAL: Execute tasks NOW. Use file tools to edit code, terminal to run commands.\n\n`;
913
+ userPrompt += `You are the Executor. Get it done, precisely, following instructions perfectly.\n\n`;
914
+
915
+ if (filteredTasks) {
916
+ userPrompt += `## REFERENCE TASK SUMMARIES (generated view from ${taskSource}; live truth is \`atris task list\`):\n${filteredTasks}\n\n`;
917
+ } else {
918
+ userPrompt += `## TASKS TO EXECUTE:\n(No tasks found - run \`atris task list\` for the live task plane)\n\n`;
919
+ }
920
+
921
+ userPrompt += `Your process (EXECUTE these steps):\n`;
922
+ userPrompt += `1. ${executorTaskHandoff({ taskId, owner, goalId, missionId, objective })}\n`;
923
+ userPrompt += `2. For this task: Show ASCII visualization first (especially complex changes)\n`;
924
+ userPrompt += `3. Run the Confidence Gate before editing\n`;
925
+ userPrompt += confidenceGatePrompt('do') + `\n`;
926
+ userPrompt += `4. Execute task: Use file edit tools, terminal commands, etc.\n`;
927
+ userPrompt += `5. Before completion, rerun the gate against proof and residual risk\n`;
928
+ userPrompt += `6. Send it to review: \`atris task ready <id> --proof "<commands run>"\`; a human accepts. Never hand-edit TODO.md; \`atris task render --out atris/TODO.md\` regenerates it\n`;
929
+ userPrompt += `7. Log to atris/team/executor/logs/YYYY-MM-DD.md\n`;
930
+ userPrompt += ` (Task, Delivered, Errors hit, Learned)\n`;
931
+ userPrompt += `8. Use MAP.md to navigate codebase\n\n`;
932
+ userPrompt += `DO NOT just describe what you would do - actually edit files and execute commands!\n`;
933
+ userPrompt += `Context: ${context}\n`;
934
+ userPrompt += `Start only the dispatched task after the raw-record check.`;
935
+ return userPrompt;
936
+ }
937
+
869
938
  async function doAtris() {
870
939
  const { loadConfig } = require('../utils/config');
871
940
  const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
@@ -1044,7 +1113,7 @@ async function doAtris() {
1044
1113
  console.log(`- Executor spec: ${executorPath || 'atris/team/executor/MEMBER.md (missing)'}`);
1045
1114
  console.log(`- Persona: ${personaFileRef || 'atris/PERSONA.md (missing)'}`);
1046
1115
  const mapDisplay = mapPath
1047
- ? `${mapPath}${mapIsPlaceholder ? ' (placeholder generate first)' : ''}`
1116
+ ? `${mapPath}${mapIsPlaceholder ? ' (placeholder, generate first)' : ''}`
1048
1117
  : 'atris/MAP.md (missing)';
1049
1118
  console.log(`- MAP: ${mapDisplay}`);
1050
1119
  console.log(`- TODO: ${taskSourcePath || 'atris/TODO.md (missing)'}`);
@@ -1097,17 +1166,17 @@ async function doAtris() {
1097
1166
  console.log('');
1098
1167
  }
1099
1168
  console.log('Workflow:');
1100
- console.log('1) Read atris/TODO.md — claim next unclaimed Backlog task');
1101
- console.log(' Move to ## In Progress: add "Claimed by: executor at YYYY-MM-DD HH:MM"');
1169
+ console.log(`1) ${executorTaskHandoff(executorDispatchForTask(firstMinute && firstMinute.task))}`);
1170
+ console.log(' The live task plane is truth; never hand-edit TODO.md to claim or move work');
1102
1171
  console.log('2) Run the Confidence Gate against the task before editing');
1103
1172
  printConfidenceGate(' ');
1104
1173
  console.log('3) Execute step-by-step. Run tests as you go.');
1105
1174
  console.log('4) Before completion, rerun the gate against proof and residual risk');
1106
- console.log('5) When done, move task to ## Completed');
1175
+ console.log('5) When done, send it to review: `atris task ready <id> --proof "<commands run>"`; a human accepts, then `atris task render --out atris/TODO.md` refreshes the view');
1107
1176
  console.log('6) Log to atris/team/executor/logs/YYYY-MM-DD.md');
1108
1177
  console.log(' (Task, Delivered, Errors hit, Learned)');
1109
1178
  console.log('');
1110
- console.log('⛔ Do NOT plan just execute what\'s written.');
1179
+ console.log('⛔ Do NOT plan, just execute what\'s written.');
1111
1180
  console.log('');
1112
1181
 
1113
1182
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -1146,6 +1215,10 @@ async function doAtris() {
1146
1215
 
1147
1216
  // Check execution mode
1148
1217
  if (executionMode === 'agent') {
1218
+ // Bind edit permission to the selected live task, not its rendered summary.
1219
+ const dispatch = executorDispatchForTask(firstMinute && firstMinute.task);
1220
+ const dispatchCheck = validateExecutorDispatch(dispatch, firstMinute && firstMinute.task);
1221
+ if (!dispatchCheck.ok) throw new Error(`executor dispatch refused: ${dispatchCheck.reason}`);
1149
1222
  // Agent mode: execute via backend API
1150
1223
  if (!config.agent_id) {
1151
1224
  throw new Error('No agent selected. Run "atris agent" first.');
@@ -1175,29 +1248,11 @@ async function doAtris() {
1175
1248
  }
1176
1249
 
1177
1250
  // Build user prompt with context
1178
- let userPrompt = `⚠️ CRITICAL: Execute tasks NOW. Use file tools to edit code, terminal to run commands.\n\n`;
1179
- userPrompt += `You are the Executor. Get it done, precisely, following instructions perfectly.\n\n`;
1180
-
1181
- if (filteredTasks) {
1182
- userPrompt += `## TASKS TO EXECUTE (from ${taskSource}):\n${filteredTasks}\n\n`;
1183
- } else {
1184
- userPrompt += `## TASKS TO EXECUTE:\n(No tasks found - check TODO.md)\n\n`;
1185
- }
1186
-
1187
- userPrompt += `Your process (EXECUTE these steps):\n`;
1188
- userPrompt += `1. Read tasks from TODO.md (shown above)\n`;
1189
- userPrompt += `2. For each task: Show ASCII visualization first (especially complex changes)\n`;
1190
- userPrompt += `3. Run the Confidence Gate before editing\n`;
1191
- userPrompt += confidenceGatePrompt('do') + `\n`;
1192
- userPrompt += `4. Execute task: Use file edit tools, terminal commands, etc.\n`;
1193
- userPrompt += `5. Before completion, rerun the gate against proof and residual risk\n`;
1194
- userPrompt += `6. Move task to ## Completed in TODO.md\n`;
1195
- userPrompt += `7. Log to atris/team/executor/logs/YYYY-MM-DD.md\n`;
1196
- userPrompt += ` (Task, Delivered, Errors hit, Learned)\n`;
1197
- userPrompt += `8. Use MAP.md to navigate codebase\n\n`;
1198
- userPrompt += `DO NOT just describe what you would do - actually edit files and execute commands!\n`;
1199
- userPrompt += `Context: ${context}\n`;
1200
- userPrompt += `Start executing tasks now.`;
1251
+ const taskDb = require('../lib/task-db');
1252
+ const liveTask = taskDb.getTask(taskDb.open(), dispatch.taskId);
1253
+ const currentDispatchCheck = validateExecutorDispatch(dispatch, liveTask);
1254
+ if (!currentDispatchCheck.ok) throw new Error(`executor dispatch refused: ${currentDispatchCheck.reason}`);
1255
+ const userPrompt = executorAgentPrompt({ filteredTasks, taskSource, context, ...dispatch });
1201
1256
 
1202
1257
  console.log('');
1203
1258
  console.log('🤖 AGENT MODE: Executing via backend API...');
@@ -1417,13 +1472,13 @@ async function reviewAtris() {
1417
1472
  }
1418
1473
 
1419
1474
  const mapDisplay = mapPath
1420
- ? `${mapPath}${mapIsPlaceholder ? ' (placeholder generate first)' : ''}`
1475
+ ? `${mapPath}${mapIsPlaceholder ? ' (placeholder, generate first)' : ''}`
1421
1476
  : 'atris/MAP.md (missing)';
1422
1477
 
1423
1478
  if (showFull) {
1424
1479
  console.log('');
1425
1480
  console.log('┌─────────────────────────────────────────────────────────────┐');
1426
- console.log('│ Atris Review Validator Agent Activated │');
1481
+ console.log('│ Atris Review - Validator Agent Activated │');
1427
1482
  console.log('└─────────────────────────────────────────────────────────────┘');
1428
1483
  console.log('');
1429
1484
 
@@ -1594,7 +1649,7 @@ async function reviewAtris() {
1594
1649
  userPrompt += ` • Verify everything works\n`;
1595
1650
  userPrompt += ` • Find all plausible loopholes; patch them or name residual risk\n`;
1596
1651
  userPrompt += ` • Test thoroughly (unless user says no)\n`;
1597
- userPrompt += ` • Confirm active task state is clean no unresolved Backlog/In Progress/Blocked rows for reviewed work.\n`;
1652
+ userPrompt += ` • Confirm active task state is clean, with no unresolved Backlog/In Progress/Blocked rows for reviewed work.\n`;
1598
1653
  userPrompt += ` If durable task state changed, regenerate the readable view with \`atris task render --out atris/TODO.md\`.\n`;
1599
1654
  userPrompt += ` Do not hand-delete rendered completed history; if a task fails, move or mark it blocked with a note.\n`;
1600
1655
  userPrompt += ` • Log to atris/team/validator/logs/YYYY-MM-DD.md\n`;
@@ -1718,7 +1773,7 @@ async function reviewAtris() {
1718
1773
  if (fs.existsSync(logFile)) {
1719
1774
  let journalContent = fs.readFileSync(logFile, 'utf8');
1720
1775
  const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
1721
- const learning = `- ${timestamp} ${answer.trim()}`;
1776
+ const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
1722
1777
 
1723
1778
  // Find or create ## Notes section
1724
1779
  if (journalContent.includes('## Notes')) {
@@ -1732,17 +1787,12 @@ async function reviewAtris() {
1732
1787
  console.log(`✓ Logged to journal: ${learning}`);
1733
1788
  }
1734
1789
 
1735
- // Also log to structured learnings (if learnings module exists)
1736
1790
  try {
1737
- const { addLearning } = require('../lib/learnings');
1738
- const insight = answer.trim();
1739
- // Auto-classify: starts with "don't" or "never" or "avoid" → pitfall, else pattern
1740
- const type = /^(don't|never|avoid|watch out|careful)/i.test(insight) ? 'pitfall' : 'pattern';
1741
- const key = insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
1742
- addLearning({ type, key, insight, confidence: 7, source: 'review', files: [] });
1743
- console.log(`✓ Saved to learnings: [7/10] ${type}/${key}`);
1791
+ commitReviewLearning(answer.trim(), {
1792
+ cwd: process.cwd(),
1793
+ });
1744
1794
  } catch {
1745
- // learnings module not available skip silently
1795
+ // learnings module not available, so skip silently
1746
1796
  }
1747
1797
  }
1748
1798
 
@@ -1844,6 +1894,9 @@ module.exports = {
1844
1894
  doAtris,
1845
1895
  reviewAtris,
1846
1896
  renderReviewMinute,
1897
+ executorAgentPrompt,
1898
+ executorDispatchForTask,
1899
+ validateExecutorDispatch,
1847
1900
  executeAgentSDKFast,
1848
1901
  makeCloudExecutor,
1849
1902
  postToolResult
@@ -939,7 +939,7 @@ function guard(args) {
939
939
  return 2;
940
940
  }
941
941
  // Conductor plumbing (worktree metadata, the fleet prompt written for this very
942
- // engine, runtime scratch, the brief ledger) must not count as dirt otherwise the
942
+ // engine, runtime scratch, the brief ledger) must not count as dirt, otherwise the
943
943
  // harness blocks the engine with the harness's own files.
944
944
  const counts = statusCounts(root, { ignoreUntracked: isConductorArtifact });
945
945
  if (counts && (counts.staged || counts.unstaged || counts.untracked) && !hasFlag(args, '--allow-dirty')) {
package/commands/write.js CHANGED
@@ -1,14 +1,14 @@
1
- // atris write guided writing sessions (plan-do-review for prose).
1
+ // atris write: guided writing sessions (plan-do-review for prose).
2
2
  //
3
3
  // The contract: the human types every word of the draft. Atris structures the
4
4
  // session (outline beats with states), tracks progress against the plan, and
5
5
  // reviews against the taste gate (writing policy + slop detector). It NEVER
6
- // writes or rewrites prose review output is suggestions, not edits.
6
+ // writes or rewrites prose. Review output is suggestions, not edits.
7
7
  //
8
8
  // Session = a folder of plain markdown, so the same files open in the web and
9
9
  // desktop editors with no extra format:
10
10
  // atris/writing/<slug>/plan.md outline beats: [ ] empty, [~] drafted, [x] passed
11
- // atris/writing/<slug>/draft.md the piece human-only territory
11
+ // atris/writing/<slug>/draft.md the piece, human-only territory
12
12
  //
13
13
  // Usage:
14
14
  // atris write start "<topic>" [--dump "raw ideas"] [--beats "a | b | c"]
@@ -17,7 +17,7 @@
17
17
  // atris write pass <n> [slug] mark beat n passed (human calls it, not the AI)
18
18
  // atris write list list sessions
19
19
  //
20
- // Zero external deps (Node built-ins only) repo contract.
20
+ // Zero external deps (Node built-ins only). Repo contract.
21
21
 
22
22
  const fs = require('fs');
23
23
  const path = require('path');
@@ -212,7 +212,7 @@ function review(argv, root = process.cwd()) {
212
212
  console.log(` ⚠ ${empty.length} beat${empty.length === 1 ? '' : 's'} still empty: ${empty.map((b) => b.title).join(', ')}`);
213
213
  }
214
214
  if (findings.length) {
215
- console.log(`\n slop tells (fix them yourself atris never edits your draft):`);
215
+ console.log(`\n slop tells (fix them yourself, atris never edits your draft):`);
216
216
  for (const f of findings) console.log(` ✗ draft.md:${f.line} ${f.rule.padEnd(16)} ${f.why}`);
217
217
  } else {
218
218
  console.log(` ✓ slop scan clean`);
@@ -235,7 +235,7 @@ function pass(argv, root = process.cwd()) {
235
235
  const beat = plan.beats.find((b) => b.n === n);
236
236
  if (!beat) { console.error(` no beat ${n} in ${slug}`); return 2; }
237
237
  const counts = draftWordCounts(slug, plan.beats, root);
238
- if ((counts.get(n) || 0) === 0) { console.error(` beat ${n} "${beat.title}" has no words yet write it first`); return 2; }
238
+ if ((counts.get(n) || 0) === 0) { console.error(` beat ${n} "${beat.title}" has no words yet, write it first`); return 2; }
239
239
  beat.state = 'x';
240
240
  writePlanStates(slug, plan.beats, root);
241
241
  console.log(` ● passed: ${n}. ${beat.title}`);
@@ -370,17 +370,17 @@ function coach(argv, root = process.cwd()) {
370
370
 
371
371
  function help() {
372
372
  console.log(`
373
- atris write guided writing sessions (you write every word; atris structures + reviews)
373
+ atris write: guided writing sessions (you write every word; atris structures + reviews)
374
374
 
375
375
  atris write the coach: knows where you are, gets you going
376
376
  atris write start "<topic>" [--dump "..."] [--beats "a | b | c"]
377
377
  atris write coach [slug] the coach: cheers your best line, asks the next question (--offline for no-LLM)
378
378
  atris write status [slug] progress against the outline (beats landed)
379
379
  atris write review [slug] taste gate: slop scan + writing-policy passes (read-only)
380
- atris write pass <n> [slug] mark beat n passed the human calls it
380
+ atris write pass <n> [slug] mark beat n passed, the human calls it
381
381
  atris write list all sessions
382
382
 
383
- sessions are plain markdown in atris/writing/<slug>/ the same files open
383
+ sessions are plain markdown in atris/writing/<slug>/, the same files open
384
384
  in the web and desktop editors. atris never writes or edits your draft.
385
385
  `);
386
386
  return 0;
@@ -11,6 +11,8 @@ const {
11
11
  extractTeachMechanisms,
12
12
  isThinTeachLesson,
13
13
  TEACH_THIN_REFUSE,
14
+ printLearnerCheckGate,
15
+ proveSavedLearnerBaseline,
14
16
  } = require('./youtube');
15
17
 
16
18
  const DEFAULT_TIMEOUT_MS = 120000;
@@ -27,10 +29,10 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
27
29
  output('');
28
30
  output(`Search X/Twitter via Atris (${COST_HINT}).`);
29
31
  output('Requires login. Same auth path as atris youtube process.');
30
- output('Prints to stdout. Rich ephemeral prints one apply next-step (no files).');
32
+ output('Prints to stdout. Rich ephemeral prints one apply next-step, then hands off to atris youtube search (no files).');
31
33
  output('--save files a brief only when the result is rich.');
32
34
  output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
33
- output('Empty or failed search refunds the credits.');
35
+ output('Empty or failed search prints credits refunded only when the server marks a refund.');
34
36
  output('');
35
37
  output('Options:');
36
38
  output(' --limit <n> Max results hint (search only)');
@@ -221,7 +223,9 @@ function parseUnsaveArgs(argv = []) {
221
223
  const options = {
222
224
  mode: 'unsave',
223
225
  help: false,
226
+ json: false,
224
227
  source: null,
228
+ sources: [],
225
229
  };
226
230
 
227
231
  if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
@@ -236,19 +240,20 @@ function parseUnsaveArgs(argv = []) {
236
240
  const arg = args[i];
237
241
  if (arg === '--help' || arg === '-h' || arg === 'help') {
238
242
  options.help = true;
243
+ } else if (arg === '--json') {
244
+ options.json = true;
239
245
  } else if (arg === '--unsave') {
240
246
  continue;
241
247
  } else if (arg.startsWith('-')) {
242
248
  throw new Error(`Unknown option: ${arg}`);
243
- } else if (!options.source) {
244
- options.source = arg;
245
249
  } else {
246
- throw new Error(`Unexpected argument: ${arg}`);
250
+ options.sources.push(arg);
251
+ if (!options.source) options.source = arg;
247
252
  }
248
253
  }
249
254
 
250
255
  if (options.help) return options;
251
- if (!options.source) throw new Error('usage: atris x-search unsave <query-or-source>');
256
+ if (!options.sources.length) throw new Error('usage: atris x-search unsave <query-or-source>');
252
257
  return options;
253
258
  }
254
259
 
@@ -322,9 +327,8 @@ function xSearchCredits(data) {
322
327
  return { used, remaining, refunded };
323
328
  }
324
329
 
325
- function creditsWereRefunded(credits) {
330
+ function creditsRefundedExplicitly(credits) {
326
331
  if (!credits) return false;
327
- if (credits.used === 0) return true;
328
332
  if (credits.refunded === true) return true;
329
333
  return typeof credits.refunded === 'number' && credits.refunded > 0;
330
334
  }
@@ -334,7 +338,7 @@ function formatCreditsLines(credits) {
334
338
  if (credits.used !== undefined || credits.remaining !== undefined) {
335
339
  lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
336
340
  }
337
- if (creditsWereRefunded(credits)) {
341
+ if (creditsRefundedExplicitly(credits)) {
338
342
  lines.push('credits refunded');
339
343
  }
340
344
  return lines;
@@ -349,7 +353,7 @@ function xSearchFailureError(result) {
349
353
  ? ' xAI is unavailable; retry in a few seconds.'
350
354
  : '';
351
355
  const credits = xSearchCredits(result.data);
352
- const refundHint = result.status === 502 && creditsWereRefunded(credits)
356
+ const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
353
357
  ? ' credits refunded.'
354
358
  : '';
355
359
  const lines = [`X search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
@@ -387,6 +391,12 @@ async function runXSearch(options, deps = {}) {
387
391
  forceMint: true,
388
392
  });
389
393
  if (remint?.ok && remint.token) {
394
+ if (!options.json) {
395
+ const print = typeof deps.output === 'function' ? deps.output : () => {};
396
+ for (const line of formatCreditsLines(xSearchCredits(result.data))) {
397
+ print(line);
398
+ }
399
+ }
390
400
  auth = remint;
391
401
  result = await call(auth.token);
392
402
  }
@@ -403,6 +413,21 @@ function xSearchApplySource(options) {
403
413
  return options?.query;
404
414
  }
405
415
 
416
+ function quoteYoutubeSearchQuery(source) {
417
+ return `"${String(source || '').replace(/"/g, '')}"`;
418
+ }
419
+
420
+ function printYoutubeSearchNext(source, output) {
421
+ if (!source) return;
422
+ output(`next: atris youtube search ${quoteYoutubeSearchQuery(source)}`);
423
+ }
424
+
425
+ const EMPTY_YOUTUBE_SEARCH_NEXT = 'next: atris youtube search " "';
426
+
427
+ function printEmptyYoutubeSearchNext(output) {
428
+ output(EMPTY_YOUTUBE_SEARCH_NEXT);
429
+ }
430
+
406
431
  function xSearchApplyRel(source) {
407
432
  return applyGate.applySidecarRel('x-search', applyGate.applySlug(source));
408
433
  }
@@ -509,10 +534,34 @@ function unsaveXSearch(target, deps = {}) {
509
534
  return 0;
510
535
  }
511
536
 
537
+ function unsaveTargets(options) {
538
+ if (Array.isArray(options?.sources) && options.sources.length) return options.sources;
539
+ const single = options?.source || options?.query;
540
+ return single ? [single] : [];
541
+ }
542
+
543
+ function runXSearchUnsave(options, deps = {}) {
544
+ const output = deps.output || ((line = '') => console.log(line));
545
+ const targets = unsaveTargets(options);
546
+ if (!targets.length) {
547
+ output('usage: atris x-search unsave <query-or-source>');
548
+ return 2;
549
+ }
550
+ let code = 0;
551
+ for (const target of targets) {
552
+ const status = unsaveXSearch(target, deps);
553
+ if (status !== 0) code = status;
554
+ }
555
+ if (code === 0 && options.json !== true && deps.json !== true) {
556
+ printEmptyYoutubeSearchNext(output);
557
+ }
558
+ return code;
559
+ }
560
+
512
561
  function saveRichXSearch({ cwd, source, text, now } = {}) {
513
562
  const lesson = xSearchLessonFromText(text);
514
563
  if (isThinTeachLesson(lesson)) {
515
- return { thin: true, brief: null, packRel: null };
564
+ return { thin: true, brief: null, packRel: null, lesson };
516
565
  }
517
566
  const brief = fileXSearchBrief({ cwd, source, text, now });
518
567
  const packRel = fileTeachExperiment({
@@ -521,19 +570,20 @@ function saveRichXSearch({ cwd, source, text, now } = {}) {
521
570
  slug: source ? xSearchExperimentSlug(source) : null,
522
571
  applyRel: source ? xSearchApplyRel(source) : null,
523
572
  });
524
- return { thin: false, brief, packRel };
573
+ return { thin: false, brief, packRel, lesson };
525
574
  }
526
575
 
527
576
  function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
528
577
  const pack = packRel || (source ? xSearchExperimentRel(source) : null);
578
+ const slug = pack ? path.basename(pack) : null;
529
579
  return applyGate.ensureApply({
530
580
  cwd,
531
581
  source,
532
582
  rel: source ? xSearchApplyRel(source) : null,
533
583
  now,
534
584
  output,
535
- incompleteMessage: pack
536
- ? `next: apply ${pack}. keep only if measure.py moves 0→1`
585
+ incompleteMessage: slug
586
+ ? `next: atris experiments keep ${slug}`
537
587
  : APPLY_NEXT_MESSAGE,
538
588
  required: false,
539
589
  change: pack ? `apply ${pack}` : undefined,
@@ -598,7 +648,7 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
598
648
  }
599
649
 
600
650
  if (options.mode === 'unsave' || options.unsave) {
601
- return unsaveXSearch(options.source || options.query, deps);
651
+ return runXSearchUnsave(options, deps);
602
652
  }
603
653
 
604
654
  let status = 0;
@@ -611,11 +661,19 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
611
661
  output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
612
662
  }
613
663
  if (!hasResults) {
664
+ if (
665
+ !options.json
666
+ && !options.save
667
+ && !creditsRefundedExplicitly(xSearchCredits(data))
668
+ ) {
669
+ printEmptyYoutubeSearchNext(output);
670
+ }
614
671
  status = 2;
615
672
  } else if (options.save) {
616
673
  const source = xSearchApplySource(options);
674
+ const cwd = deps.cwd || process.cwd();
617
675
  const saved = saveRichXSearch({
618
- cwd: deps.cwd || process.cwd(),
676
+ cwd,
619
677
  source,
620
678
  text: xSearchContent(data),
621
679
  now: deps.applyNow || deps.now,
@@ -625,21 +683,34 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
625
683
  status = 2;
626
684
  } else {
627
685
  const ensureApply = deps.ensureApply || ensureXSearchApply;
628
- status = ensureApply({
629
- cwd: deps.cwd || process.cwd(),
686
+ const applyCode = ensureApply({
687
+ cwd,
630
688
  source,
631
689
  packRel: saved.packRel,
632
690
  now: deps.applyNow || deps.now,
633
691
  output,
634
692
  });
693
+ if (deps.ensureApply) {
694
+ status = applyCode;
695
+ } else {
696
+ const baseline = proveSavedLearnerBaseline({
697
+ cwd,
698
+ applyRel: source ? xSearchApplyRel(source) : null,
699
+ lesson: saved.lesson,
700
+ output,
701
+ json: options.json === true,
702
+ });
703
+ status = baseline !== 0 ? baseline : applyCode;
704
+ }
635
705
  }
636
706
  } else {
637
- if (
638
- !options.json
639
- && !isThinTeachLesson(xSearchLessonFromText(xSearchContent(data)))
640
- ) {
707
+ const lesson = xSearchLessonFromText(xSearchContent(data));
708
+ const json = options.json === true;
709
+ if (!json && !isThinTeachLesson(lesson)) {
641
710
  applyGate.hintEphemeralApply(output, 'x-search');
642
711
  }
712
+ printLearnerCheckGate(output, lesson, { includeCheck: true, json });
713
+ if (!json) printYoutubeSearchNext(xSearchApplySource(options), output);
643
714
  status = 0;
644
715
  }
645
716
  } catch (err) {
package/commands/xp.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const crypto = require('crypto');
7
7
  const { spawnSync } = require('child_process');
8
8
  const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
9
+ const { treeHashFor } = require('../lib/tree-hash');
9
10
 
10
11
  const DEFAULT_GRAPH_DAYS = 365;
11
12
  const MAX_SYNC_GRAPH_DAYS = 370;
@@ -22,7 +23,7 @@ const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
22
23
 
23
24
  // Default single-workspace XP root. Local XP reads task_episodes.jsonl (written
24
25
  // by task-db at the resolved workspace root) and writes its projection beside
25
- // it, so the `--workspace` default must resolve the same shared root otherwise
26
+ // it, so the `--workspace` default must resolve the same shared root, otherwise
26
27
  // `xp --local` from a subdir (e.g. backend/) read the wrong/empty episodes and
27
28
  // split the projection into a nested .atris. Explicit --workspace still wins;
28
29
  // the multi-root roster path (discoverCareerXpWorkspaces) is untouched. Falls
@@ -908,7 +909,7 @@ function readTaskEpisodeTail(episodePath, cursorPath, options = {}) {
908
909
  }
909
910
  }
910
911
 
911
- function receiptFromTaskEpisode(episode) {
912
+ function receiptFromTaskEpisode(episode, treeHash) {
912
913
  const episodeId = episode?.episode_id;
913
914
  const label = episode?.rl?.label;
914
915
  const rejected = label === 'rework_requested' || label === 'rejected';
@@ -924,6 +925,7 @@ function receiptFromTaskEpisode(episode) {
924
925
 
925
926
  return {
926
927
  schema: 'atris.career_xp_receipt.v1',
928
+ tree_hash: treeHash,
927
929
  receipt_id: `task_review:${episodeId}${rejected ? ':rejected' : ''}`,
928
930
  source: 'atris-cli',
929
931
  source_type: 'task_review',
@@ -1071,10 +1073,11 @@ function collectLocalXpProjectionState(args = [], { write = true } = {}) {
1071
1073
  && fileSize(episodePath) > 0
1072
1074
  && fileSize(receiptsPath) === 0;
1073
1075
  const tail = readTaskEpisodeTail(episodePath, cursorPath, { forceReset: replayFromStart });
1076
+ const treeHash = write && tail.episodes.length ? treeHashFor(workspace) : null;
1074
1077
  const seen = new Set(existingChain.receipts.map(receipt => receipt.receipt_id).filter(Boolean));
1075
1078
  let previousHash = existingChain.integrity.head_hash || null;
1076
1079
  const newReceipts = tail.episodes
1077
- .map(receiptFromTaskEpisode)
1080
+ .map((episode) => receiptFromTaskEpisode(episode, treeHash))
1078
1081
  .filter(Boolean)
1079
1082
  .filter((receipt) => {
1080
1083
  if (seen.has(receipt.receipt_id)) return false;