atris 3.35.0 → 3.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +565 -265
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3029 -339
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +79 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
package/lib/task-db.js CHANGED
@@ -240,22 +240,43 @@ function newId() {
240
240
  return head + tail;
241
241
  }
242
242
 
243
- // Walk up from `start` to find the canonical workspace root. We check for
244
- // .git, then atris/, then .atris/. If none found, fall back to `start`. This
245
- // makes `atris task add` from a subdirectory write the same workspace_root
246
- // as parseTodo() reads from the project's atris/TODO.md.
243
+ // Resolve the canonical workspace root so `atris task` from any subdirectory
244
+ // keys the SAME store as the repo root. Precedence (nearest ancestor wins per
245
+ // pass):
246
+ // 1. A deliberate workspace spine — .atris/business.json (a bound customer
247
+ // sub-workspace) or atris/atris.md (the framework protocol file). These are
248
+ // intentional nested workspaces INSIDE a repo and must stay isolated.
249
+ // 2. The git toplevel (.git). One git repo == one workspace; this rescues a
250
+ // subdirectory whose only markers are a bare atris/ or .atris/ dir.
251
+ // 3. Legacy fallback for non-git trees: a bare atris/ or .atris/ dir.
252
+ //
253
+ // Before this, a bare atris/ or .atris/ dir short-circuited ahead of the git
254
+ // root, so a subdir like backend/ (which has an atris/ folder plus a stray
255
+ // .atris) hijacked itself as a second workspace root and split task/usage state
256
+ // into backend/.atris (proven footgun 2026-07-16). Checking .git only at the
257
+ // same level meant the walk returned before ever reaching the real repo root.
247
258
  function findWorkspaceRoot(start) {
248
- let cur = path.resolve(start || process.cwd());
249
- // Cap the walk at 32 levels to avoid pathological symlink loops.
250
- for (let i = 0; i < 32; i++) {
251
- if (fs.existsSync(path.join(cur, '.git'))) return cur;
252
- if (fs.existsSync(path.join(cur, 'atris'))) return cur;
253
- if (fs.existsSync(path.join(cur, '.atris'))) return cur;
254
- const parent = path.dirname(cur);
255
- if (parent === cur) break;
256
- cur = parent;
257
- }
258
- return path.resolve(start || process.cwd());
259
+ const origin = path.resolve(start || process.cwd());
260
+ // Cap each walk at 32 levels to avoid pathological symlink loops.
261
+ const walkUp = (test) => {
262
+ let cur = origin;
263
+ for (let i = 0; i < 32; i++) {
264
+ if (test(cur)) return cur;
265
+ const parent = path.dirname(cur);
266
+ if (parent === cur) break;
267
+ cur = parent;
268
+ }
269
+ return null;
270
+ };
271
+ const spine = walkUp((dir) => fs.existsSync(path.join(dir, '.atris', 'business.json'))
272
+ || fs.existsSync(path.join(dir, 'atris', 'atris.md')));
273
+ if (spine) return spine;
274
+ const gitRoot = walkUp((dir) => fs.existsSync(path.join(dir, '.git')));
275
+ if (gitRoot) return gitRoot;
276
+ const legacy = walkUp((dir) => fs.existsSync(path.join(dir, 'atris'))
277
+ || fs.existsSync(path.join(dir, '.atris')));
278
+ if (legacy) return legacy;
279
+ return origin;
259
280
  }
260
281
 
261
282
  function workspaceRoot(cwd) {
@@ -558,17 +579,18 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof, a
558
579
  // (e.g. the 109 duplicate "Loop tick:" orphans fail-closed before 'archived'
559
580
  // existed — cluster 1 of failed-tasks-analysis-2026-07-03). It permits the
560
581
  // failed→archived transition and records the prior status in
561
- // metadata.archived_from. 'done' rows are never archivable: accepted work
562
- // stays done, with or without the flag.
563
- function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
582
+ // metadata.archived_from. Individual archive commands never archive 'done'
583
+ // rows. The clear-done sweep opts in through fromDone after selecting only the
584
+ // same 'done' rows counted by `atris status`.
585
+ function archiveTask(db, { id, actor, reason, fromFailed = false, fromDone = false } = {}) {
564
586
  if (!id) throw new Error('id required');
565
587
  const reasonText = String(reason || '').trim();
566
588
  if (!reasonText) throw new Error('reason required');
567
589
  const row = getTask(db, id);
568
590
  if (!row) return { archived: false, reason: 'not_found' };
569
- const allowedStatuses = fromFailed
570
- ? ['open', 'claimed', 'review', 'failed']
571
- : ['open', 'claimed', 'review'];
591
+ const allowedStatuses = ['open', 'claimed', 'review'];
592
+ if (fromFailed) allowedStatuses.push('failed');
593
+ if (fromDone) allowedStatuses.push('done');
572
594
  if (!allowedStatuses.includes(row.status)) {
573
595
  return { archived: false, reason: `already_${row.status}` };
574
596
  }
@@ -578,7 +600,8 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
578
600
  metadata.archived_at = new Date(now).toISOString();
579
601
  metadata.archived_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
580
602
  metadata.approval_status = 'archived';
581
- if (row.status === 'failed') metadata.archived_from = 'failed';
603
+ if (row.status === 'failed' || row.status === 'done') metadata.archived_from = row.status;
604
+ const terminalAt = row.status === 'done' && row.done_at ? row.done_at : now;
582
605
  const statusPlaceholders = allowedStatuses.map(() => '?').join(', ');
583
606
  const result = withBusyRetry(() => db.prepare(`
584
607
  UPDATE tasks
@@ -588,7 +611,7 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
588
611
  metadata = ?
589
612
  WHERE id = ?
590
613
  AND status IN (${statusPlaceholders})
591
- `).run(now, now, JSON.stringify(metadata), id, ...allowedStatuses));
614
+ `).run(terminalAt, now, JSON.stringify(metadata), id, ...allowedStatuses));
592
615
  if (result.changes !== 1) return { archived: false, reason: 'stale_task_state' };
593
616
  const updated = getTask(db, id);
594
617
  const event = appendTaskEvent(db, {
@@ -596,7 +619,10 @@ function archiveTask(db, { id, actor, reason, fromFailed = false } = {}) {
596
619
  workspaceRoot: updated.workspace_root,
597
620
  actor: metadata.archived_by,
598
621
  eventType: 'archived',
599
- payload: { reason: reasonText, ...(row.status === 'failed' ? { previous_status: 'failed' } : {}) },
622
+ payload: {
623
+ reason: reasonText,
624
+ ...(['failed', 'done'].includes(row.status) ? { previous_status: row.status } : {}),
625
+ },
600
626
  });
601
627
  const logs = appendTaskCompletionLogs(db, updated, {
602
628
  status: 'archived',
@@ -684,7 +710,7 @@ function recordReviewPassActor(metadata, claimedBy, rawActor) {
684
710
  return { builder: builder || null, independent };
685
711
  }
686
712
 
687
- function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace }) {
713
+ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resultTrace, result: resultSentence, reason }) {
688
714
  if (!id) throw new Error('id required');
689
715
  const text = String(proof || '').trim();
690
716
  if (!text) throw new Error('proof required');
@@ -703,6 +729,10 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
703
729
  metadata.latest_agent_proof = text;
704
730
  metadata.latest_agent_lesson = String(lesson || '').trim() || null;
705
731
  metadata.latest_agent_next_task = String(nextTask || '').trim() || null;
732
+ const resultText = cleanLandingText(resultSentence);
733
+ if (resultText) metadata.result = resultText;
734
+ const reasonText = cleanLandingText(reason);
735
+ if (reasonText) metadata.result_reason = reasonText;
706
736
  const landingInput = landing && typeof landing === 'object' ? landing : {};
707
737
  for (const key of ['happened', 'checked', 'tested', 'decision']) {
708
738
  const cleaned = cleanLandingText(landingInput[key]);
@@ -735,6 +765,7 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
735
765
  review_pass_count: reviewPassCount,
736
766
  agent_certified: metadata.agent_certified === true,
737
767
  agent_certification_policy: metadata.agent_certification_policy || null,
768
+ result: metadata.result || null,
738
769
  };
739
770
  if (resultTrace && typeof resultTrace === 'object') {
740
771
  payload.result_trace = resultTrace;
@@ -756,13 +787,46 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
756
787
  return { ready: true, event, row: updated };
757
788
  }
758
789
 
759
- function reviseTask(db, { id, actor, note }) {
790
+ function setTaskResult(db, { id, actor, result }) {
791
+ if (!id) throw new Error('id required');
792
+ const text = cleanLandingText(result);
793
+ if (!text) throw new Error('result required');
794
+ const row = getTask(db, id);
795
+ if (!row) return { saved: false, reason: 'not_found' };
796
+ const now = Date.now();
797
+ const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
798
+ metadata.result = text;
799
+ metadata.result_at = new Date(now).toISOString();
800
+ metadata.result_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
801
+ const updated = withBusyRetry(() => db.prepare(`
802
+ UPDATE tasks
803
+ SET metadata = ?,
804
+ updated_at = ?
805
+ WHERE id = ?
806
+ `).run(JSON.stringify(metadata), now, id));
807
+ if (updated.changes !== 1) return { saved: false, reason: 'stale_task_state' };
808
+ const latest = getTask(db, id);
809
+ const event = appendTaskEvent(db, {
810
+ taskId: id,
811
+ workspaceRoot: latest.workspace_root,
812
+ actor: metadata.result_by,
813
+ eventType: 'result_set',
814
+ payload: { result: text },
815
+ });
816
+ return { saved: true, event, row: latest };
817
+ }
818
+
819
+ function reviseTask(db, { id, actor, note, allowDone = false }) {
760
820
  if (!id) throw new Error('id required');
761
821
  const text = String(note || '').trim();
762
822
  if (!text) throw new Error('note required');
763
823
  const row = getTask(db, id);
764
824
  if (!row) return { revised: false, reason: 'not_found' };
765
- if (row.status !== 'review') {
825
+ const acceptedDone = allowDone
826
+ && row.status === 'done'
827
+ && row.metadata
828
+ && row.metadata.approval_status === 'accepted';
829
+ if (row.status !== 'review' && !acceptedDone) {
766
830
  return { revised: false, reason: `not_reviewable_${row.status}` };
767
831
  }
768
832
  const now = Date.now();
@@ -787,7 +851,19 @@ function reviseTask(db, { id, actor, note }) {
787
851
  metadata.human_revision_at = new Date(now).toISOString();
788
852
  metadata.human_revision_by = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
789
853
  metadata.human_revision_note = text;
854
+ if (acceptedDone) {
855
+ for (const key of [
856
+ 'accepted_at',
857
+ 'accepted_by',
858
+ 'auto_accepted_at',
859
+ 'auto_accepted_by',
860
+ 'auto_accept_policy',
861
+ ]) {
862
+ delete metadata[key];
863
+ }
864
+ }
790
865
  const revisedStatus = row.claimed_by ? 'claimed' : 'open';
866
+ const expectedStatus = acceptedDone ? 'done' : 'review';
791
867
  const result = withBusyRetry(() => db.prepare(`
792
868
  UPDATE tasks
793
869
  SET status = ?,
@@ -795,8 +871,8 @@ function reviseTask(db, { id, actor, note }) {
795
871
  updated_at = ?,
796
872
  metadata = ?
797
873
  WHERE id = ?
798
- AND status = 'review'
799
- `).run(revisedStatus, now, JSON.stringify(metadata), id));
874
+ AND status = ?
875
+ `).run(revisedStatus, now, JSON.stringify(metadata), id, expectedStatus));
800
876
  if (result.changes !== 1) return { revised: false, reason: 'not_updated' };
801
877
  const updated = getTask(db, id);
802
878
  const event = appendTaskEvent(db, {
@@ -808,6 +884,7 @@ function reviseTask(db, { id, actor, note }) {
808
884
  note: text,
809
885
  approval_status: 'revise',
810
886
  revision_count: revisionCount,
887
+ ...(acceptedDone ? { previous_status: row.status } : {}),
811
888
  },
812
889
  });
813
890
  const episode = taskEpisodeFromRevision(updated, event, event.payload);
@@ -1846,6 +1923,7 @@ function taskProjection(db, {
1846
1923
  id: row.id,
1847
1924
  ...(refById.get(row.id) || {}),
1848
1925
  title: row.title,
1926
+ result: row.metadata && row.metadata.result || null,
1849
1927
  status: row.status,
1850
1928
  tag: row.tag,
1851
1929
  workspace_root: row.workspace_root,
@@ -1975,6 +2053,7 @@ module.exports = {
1975
2053
  relabelArchivedTasks,
1976
2054
  isJune10BacklogResetMarker,
1977
2055
  readyTask,
2056
+ setTaskResult,
1978
2057
  reviseTask,
1979
2058
  stageTask,
1980
2059
  noteTask,
package/lib/task-proof.js CHANGED
@@ -7,6 +7,11 @@ const FILE_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|src\/|scripts\/|a
7
7
  const PATH_ONLY_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|\/private\/|\/var\/|atris\/runs\/|\.atris\/state\/)[^\s'"`,;)]+(?:[.](?:json|jsonl|md|log|txt|png|jpg|jpeg|pdf))?/i;
8
8
  const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=|file=|bytes=|model=|opened=|https?:\/\/)\b/i;
9
9
  const RESULT_PAIR_RE = /\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b.{0,80}\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b|\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b.{0,80}\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b/i;
10
+ const SUITE_GREEN_CLAIM_RE = /\b(?:tests|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:all\s+)?(?:green|pass(?:es|ed)?|ok)\b|\b(?:all|full|whole|entire)\s+(?:tests?|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:green|pass(?:es|ed)?|ok)\b|\ball\s+green\b|\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b\x60?(?:\s+(?:\d+\s*\/\s*\d+|all|suite))?\s+(?:is\s+)?(?:green|pass(?:es|ed)?|ok)\b/i;
11
+ const CI_RUN_CITATION_RE = /https?:\/\/github[.]com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\b|\brun_id\s*=\s*\d+\b|\brun(?:\s+id)?\s*(?:=|:)?\s*\d+\b/i;
12
+ const COMMIT_PINNED_LOCAL_VERIFY_RE = /\bcommit\s+[0-9a-f]{7,40}\b/i;
13
+ const EXIT_ZERO_RE = /\bexit(?:ed)?(?:\s+code)?\s*0\b/i;
14
+ const SUITE_GREEN_CITATION_REASON = 'cite the CI run URL, run id, or commit-pinned verify command with exit 0 before claiming the suite is green';
10
15
  const HUMAN_PROOF_RE = /\b(?:team human approved|human approved|human approval|approved by|accepted by|reviewed by|customer replied|customer approved|customer accepted|replied)\b/i;
11
16
  const FILE_ACTION_RE = /\b(?:changed|updated|edited|created|deleted|saved|wrote|patched|reviewed|verified|validated|opened|read)\b/i;
12
17
  const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/i;
@@ -16,12 +21,29 @@ function compactWhitespace(text) {
16
21
  return String(text || '').replace(/\s+/g, ' ').trim();
17
22
  }
18
23
 
24
+ function hasSuiteGreenCitation(text) {
25
+ return CI_RUN_CITATION_RE.test(text)
26
+ || (COMMIT_PINNED_LOCAL_VERIFY_RE.test(text)
27
+ && COMMAND_PROOF_RE.test(text)
28
+ && EXIT_ZERO_RE.test(text));
29
+ }
30
+
19
31
  function taskProofState(proof) {
20
32
  const text = compactWhitespace(proof);
21
33
  if (!text) return { ok: false, reason: 'proof required' };
22
34
  if (GENERIC_COMPLETION_PROOF_RE.test(text)) {
23
35
  return { ok: false, reason: 'proof must name what was verified, changed, approved, or produced' };
24
36
  }
37
+ if (SUITE_GREEN_CLAIM_RE.test(text)) {
38
+ if (!hasSuiteGreenCitation(text)) {
39
+ return {
40
+ ok: false,
41
+ code: 'suite_green_citation_required',
42
+ reason: SUITE_GREEN_CITATION_REASON,
43
+ };
44
+ }
45
+ return { ok: true, reason: 'proof cites a CI run or commit-pinned local verify' };
46
+ }
25
47
  if (COMMAND_PROOF_RE.test(text)) return { ok: true, reason: 'proof names a command' };
26
48
  if (HUMAN_PROOF_RE.test(text)) return { ok: true, reason: 'proof names human/customer approval' };
27
49
  if (RESULT_PAIR_RE.test(text)) return { ok: true, reason: 'proof names a verifier result' };
@@ -66,7 +88,7 @@ function tailText(text, max = 400) {
66
88
  // happily pattern-matches even if nothing ran) and a VERIFIED proof: the command
67
89
  // actually executed and exited 0. On failure it returns ok:false so the caller
68
90
  // can refuse to mark the task ready. The returned proof string is prefixed
69
- // `[verified]` and names the command + exit, so it also passes taskProofState.
91
+ // It names the command and exit; suite-green claims still need a citation.
70
92
  function buildVerifiedProof(verifyCmd, baseProof = '', runner, options = {}) {
71
93
  const cmd = String(verifyCmd || '').trim();
72
94
  if (!cmd) return { ok: false, reason: 'verify_command_required' };
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_FRESHNESS_WINDOW_MS = 15 * 60 * 1000;
4
+ const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
5
+ const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
6
+
7
+ function timestampMs(value) {
8
+ if (value == null || value === '') return 0;
9
+ if (typeof value === 'number') {
10
+ if (!Number.isFinite(value)) return 0;
11
+ return value > 1000000000000 ? value : value * 1000;
12
+ }
13
+ const numeric = Number(value);
14
+ if (Number.isFinite(numeric) && String(value).trim() !== '') return timestampMs(numeric);
15
+ const parsed = Date.parse(String(value));
16
+ return Number.isFinite(parsed) ? parsed : 0;
17
+ }
18
+
19
+ function latestTimestamp(...values) {
20
+ return values.reduce((latest, value) => Math.max(latest, timestampMs(value)), 0);
21
+ }
22
+
23
+ function isoTimestamp(value) {
24
+ const ms = timestampMs(value);
25
+ return ms ? new Date(ms).toISOString() : null;
26
+ }
27
+
28
+ function memberName(value) {
29
+ let text = String(value || '').trim();
30
+ if (!text) return '';
31
+ if (text.includes('/')) text = text.split('/')[0];
32
+ text = text.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
33
+ if (!text || /^(team|unknown)$/i.test(text)) return '';
34
+ return text.slice(0, 32);
35
+ }
36
+
37
+ function operatorSentence(value, fallback = 'current activity was recorded') {
38
+ let text = String(value || '')
39
+ .split(/\r?\n/)
40
+ .map((line) => line.trim().replace(/^[-*]\s+/, ''))
41
+ .find(Boolean) || fallback;
42
+ text = text
43
+ .replace(/(^|\s)--[a-z0-9][a-z0-9-]*(?:=[^\s]+)?/ig, '$1')
44
+ .replace(/\b[0-9A-HJKMNP-TV-Z]{20,26}\b/g, 'the item')
45
+ .replace(/\b[A-Z]{2,12}-\d+\b/gi, 'the task')
46
+ .replace(/\bmission-[a-z0-9][a-z0-9-]{8,}\b/ig, 'the mission')
47
+ .replace(/\s+/g, ' ')
48
+ .trim();
49
+ if (!text) text = fallback;
50
+ if (text.length > 180) text = `${text.slice(0, 177).trim()}...`;
51
+ if (!/[.!?]$/.test(text)) text += '.';
52
+ return text;
53
+ }
54
+
55
+ function taskOwner(task) {
56
+ return task && (task.claimed_by || task.assigned_to || task.metadata?.assigned_to) || '';
57
+ }
58
+
59
+ function taskActivityMs(task) {
60
+ const events = Array.isArray(task?.events) ? task.events : [];
61
+ return latestTimestamp(
62
+ task?.updated_at,
63
+ task?.created_at,
64
+ ...events.map((event) => event && event.created_at),
65
+ );
66
+ }
67
+
68
+ function missionActivityMs(mission) {
69
+ return latestTimestamp(
70
+ mission?.last_tick_at,
71
+ mission?.last_tick?.finished_at,
72
+ mission?.last_tick?.at,
73
+ mission?.updated_at,
74
+ mission?.created_at,
75
+ );
76
+ }
77
+
78
+ function activityOrder(a, b, activityFn) {
79
+ return activityFn(b) - activityFn(a)
80
+ || String(a.id || a.title || a.objective || '').localeCompare(String(b.id || b.title || b.objective || ''));
81
+ }
82
+
83
+ function taskRows(input) {
84
+ if (Array.isArray(input.tasks)) return input.tasks;
85
+ if (Array.isArray(input.taskStatus?.tasks)) return input.taskStatus.tasks;
86
+ if (Array.isArray(input.taskStatus?.streams)) {
87
+ return input.taskStatus.streams.flatMap((stream) => Array.isArray(stream.tasks) ? stream.tasks : []);
88
+ }
89
+ return [input.taskStatus?.current].filter(Boolean);
90
+ }
91
+
92
+ function missionRows(input) {
93
+ if (Array.isArray(input.missions)) return input.missions;
94
+ if (Array.isArray(input.missionStatus?.missions)) return input.missionStatus.missions;
95
+ return [];
96
+ }
97
+
98
+ function loopMissionName(mission) {
99
+ const raw = mission?.name || mission?.title || mission?.objective || mission?.id || 'mission';
100
+ return operatorSentence(raw, 'mission').replace(/[.!?]+$/, '').slice(0, 90);
101
+ }
102
+
103
+ function loopForMission(mission) {
104
+ if (!mission) return null;
105
+ const lastTick = isoTimestamp(
106
+ mission.last_tick_at
107
+ || mission.last_tick?.finished_at
108
+ || mission.last_tick?.at,
109
+ );
110
+ return {
111
+ mission: loopMissionName(mission),
112
+ cadence: String(mission.cadence || 'manual'),
113
+ runner: String(mission.runner || mission.executed_by || 'manual'),
114
+ last_tick: lastTick || 'never',
115
+ };
116
+ }
117
+
118
+ function streamActiveRows(stream) {
119
+ return (Array.isArray(stream?.active) ? stream.active : []).map((row) => {
120
+ if (Array.isArray(row)) return { name: row[0], summary: row[1], last_seen: row[2] };
121
+ return {
122
+ name: row?.name || row?.agent || row?.member,
123
+ summary: row?.summary || row?.doing || row?.work,
124
+ last_seen: row?.last_seen || row?.ts || row?.at,
125
+ };
126
+ });
127
+ }
128
+
129
+ function buildTeamPresence(input = {}) {
130
+ const nowMs = timestampMs(input.nowMs ?? input.now ?? Date.now()) || Date.now();
131
+ const freshnessWindowMs = Number(input.freshnessWindowMs) > 0
132
+ ? Number(input.freshnessWindowMs)
133
+ : DEFAULT_FRESHNESS_WINDOW_MS;
134
+ const stream = input.stream || input.streamSnapshot || {};
135
+ const events = Array.isArray(input.streamEvents)
136
+ ? input.streamEvents
137
+ : Array.isArray(input.events) ? input.events : [];
138
+ const tasks = taskRows(input).filter(Boolean);
139
+ const missions = missionRows(input).filter(Boolean);
140
+ const candidates = new Map();
141
+ const lastSeen = new Map();
142
+ const summaries = new Map();
143
+ const tasksByMember = new Map();
144
+ const missionsByMember = new Map();
145
+
146
+ const keyFor = (value) => memberName(value).toLowerCase();
147
+ const addCandidate = (value) => {
148
+ const name = memberName(value);
149
+ const key = name.toLowerCase();
150
+ if (!key) return '';
151
+ if (!candidates.has(key)) candidates.set(key, name);
152
+ return key;
153
+ };
154
+ const noteSeen = (value, timestamp) => {
155
+ const key = keyFor(value);
156
+ const ms = timestampMs(timestamp);
157
+ if (!key || !ms) return;
158
+ lastSeen.set(key, Math.max(lastSeen.get(key) || 0, ms));
159
+ };
160
+
161
+ for (const event of events) {
162
+ noteSeen(event?.agent || event?.member || event?.owner, event?.ms || event?.ts || event?.at || event?.created_at);
163
+ }
164
+
165
+ for (const row of streamActiveRows(stream)) {
166
+ const key = addCandidate(row.name);
167
+ if (!key) continue;
168
+ if (row.summary) summaries.set(key, row.summary);
169
+ noteSeen(row.name, row.last_seen);
170
+ }
171
+
172
+ for (const task of tasks) {
173
+ if (!ACTIVE_TASK_STATES.has(String(task.status || '').toLowerCase())) continue;
174
+ const owner = taskOwner(task);
175
+ const key = addCandidate(owner);
176
+ if (!key) continue;
177
+ noteSeen(owner, taskActivityMs(task));
178
+ if (!tasksByMember.has(key)) tasksByMember.set(key, []);
179
+ tasksByMember.get(key).push(task);
180
+ }
181
+
182
+ for (const mission of missions) {
183
+ if (!ACTIVE_MISSION_STATES.has(String(mission.status || '').toLowerCase())) continue;
184
+ const owner = mission.owner || mission.member;
185
+ const key = addCandidate(owner);
186
+ if (!key) continue;
187
+ noteSeen(owner, missionActivityMs(mission));
188
+ if (!missionsByMember.has(key)) missionsByMember.set(key, []);
189
+ missionsByMember.get(key).push(mission);
190
+ }
191
+
192
+ for (const rows of tasksByMember.values()) rows.sort((a, b) => activityOrder(a, b, taskActivityMs));
193
+ for (const rows of missionsByMember.values()) rows.sort((a, b) => activityOrder(a, b, missionActivityMs));
194
+
195
+ const cutoffMs = nowMs - freshnessWindowMs;
196
+ const members = [...candidates.entries()]
197
+ .sort((a, b) => a[1].localeCompare(b[1]))
198
+ .flatMap(([key, name]) => {
199
+ const seenMs = lastSeen.get(key) || 0;
200
+ if (seenMs < cutoffMs) return [];
201
+ const task = tasksByMember.get(key)?.[0] || null;
202
+ const mission = missionsByMember.get(key)?.[0] || null;
203
+ const doing = summaries.get(key)
204
+ || task?.title
205
+ || mission?.next_action
206
+ || mission?.objective;
207
+ return [{
208
+ name,
209
+ awake: true,
210
+ doing: operatorSentence(doing),
211
+ loop: loopForMission(mission),
212
+ last_seen: new Date(seenMs).toISOString(),
213
+ }];
214
+ });
215
+
216
+ return {
217
+ schema: 'atris.team_presence.v1',
218
+ generated_at: new Date(nowMs).toISOString(),
219
+ freshness_window_seconds: Math.round(freshnessWindowMs / 1000),
220
+ totals: {
221
+ awake: members.length,
222
+ waiting_operator: Math.max(0, Number(stream.waiting_operator) || 0),
223
+ landing_wait: Math.max(0, Number(stream.landing_wait) || 0),
224
+ },
225
+ members,
226
+ };
227
+ }
228
+
229
+ function renderTeamPresence(presence) {
230
+ const minutes = presence.freshness_window_seconds / 60;
231
+ const windowText = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? '' : 's'}` : `${presence.freshness_window_seconds} seconds`;
232
+ const lines = [
233
+ `team presence: ${presence.totals.awake} awake`,
234
+ `freshness window: ${windowText}`,
235
+ `waiting on operator: ${presence.totals.waiting_operator}`,
236
+ `landing wait: ${presence.totals.landing_wait}`,
237
+ ];
238
+ if (!presence.members.length) {
239
+ lines.push('awake roster: empty');
240
+ return lines.join('\n');
241
+ }
242
+ lines.push('awake roster:');
243
+ for (const member of presence.members) {
244
+ lines.push(` ${member.name}: ${member.doing}`);
245
+ if (member.loop) {
246
+ lines.push(` loop: ${member.loop.mission} [${member.loop.cadence} | ${member.loop.runner} | last tick ${member.loop.last_tick}]`);
247
+ }
248
+ lines.push(` last seen: ${member.last_seen}`);
249
+ }
250
+ return lines.join('\n');
251
+ }
252
+
253
+ module.exports = {
254
+ ACTIVE_MISSION_STATES,
255
+ ACTIVE_TASK_STATES,
256
+ DEFAULT_FRESHNESS_WINDOW_MS,
257
+ buildTeamPresence,
258
+ operatorSentence,
259
+ renderTeamPresence,
260
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ function encodeToolResult(result) {
4
+ return Buffer.from(JSON.stringify(result), 'utf8').toString('base64');
5
+ }
6
+
7
+ module.exports = { encodeToolResult };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const HISTORY_FILES = [
7
+ path.join('.atris', 'state', 'career_xp_receipts.jsonl'),
8
+ path.join('.atris', 'state', 'scorecards.jsonl'),
9
+ ];
10
+
11
+ const PASS_OUTCOMES = new Set(['accepted', 'approved', 'done', 'pass', 'passed', 'success', 'succeeded']);
12
+ const FAIL_OUTCOMES = new Set(['bounced', 'fail', 'failed', 'rejected', 'revised', 'rework_requested']);
13
+
14
+ function normalizedActor(value) {
15
+ return String(value || '').trim().toLowerCase();
16
+ }
17
+
18
+ function rowActor(row) {
19
+ return normalizedActor(row?.claimed_by || row?.metadata?.executed_by);
20
+ }
21
+
22
+ function rowPassed(row) {
23
+ for (const value of [row?.passed, row?.verify_passed, row?.metadata?.verify_passed]) {
24
+ if (typeof value === 'boolean') return value;
25
+ }
26
+ const outcome = String(row?.outcome || row?.status || '').trim().toLowerCase();
27
+ if (PASS_OUTCOMES.has(outcome)) return true;
28
+ if (FAIL_OUTCOMES.has(outcome)) return false;
29
+ return null;
30
+ }
31
+
32
+ function rowTime(row) {
33
+ for (const value of [row?.accepted_at, row?.ts, row?.recorded_at, row?.created_at, row?.updated_at]) {
34
+ const parsed = Date.parse(value || '');
35
+ if (Number.isFinite(parsed)) return parsed;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function readHistory(root) {
41
+ const rows = [];
42
+ let sequence = 0;
43
+ for (const relative of HISTORY_FILES) {
44
+ let text;
45
+ try {
46
+ text = fs.readFileSync(path.join(root, relative), 'utf8');
47
+ } catch {
48
+ return null;
49
+ }
50
+ for (const line of text.split(/\r?\n/)) {
51
+ if (!line.trim()) continue;
52
+ let row;
53
+ try {
54
+ row = JSON.parse(line);
55
+ } catch {
56
+ return null;
57
+ }
58
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return null;
59
+ rows.push({ row, sequence: sequence++, time: rowTime(row) });
60
+ }
61
+ }
62
+ return rows;
63
+ }
64
+
65
+ function computeTrustTier(actor, root = process.cwd()) {
66
+ const target = normalizedActor(actor);
67
+ if (!target) return 'probation';
68
+ const history = readHistory(root);
69
+ if (!history) return 'probation';
70
+
71
+ const outcomes = history
72
+ .filter(({ row }) => rowActor(row) === target)
73
+ .map((entry) => ({ ...entry, passed: rowPassed(entry.row) }))
74
+ .filter(({ passed }) => passed !== null)
75
+ .sort((a, b) => {
76
+ if (a.time !== null && b.time !== null && a.time !== b.time) return a.time - b.time;
77
+ if (a.time !== null && b.time === null) return 1;
78
+ if (a.time === null && b.time !== null) return -1;
79
+ return a.sequence - b.sequence;
80
+ })
81
+ .slice(-20);
82
+
83
+ const passed = outcomes.filter((outcome) => outcome.passed).length;
84
+ const passRate = outcomes.length ? passed / outcomes.length : 0;
85
+ if (outcomes.length >= 10 && passRate >= 0.9) return 'trusted';
86
+ if (outcomes.length >= 5 && passRate >= 0.7) return 'standard';
87
+ return 'probation';
88
+ }
89
+
90
+ module.exports = { computeTrustTier };