atris 3.16.1 → 3.17.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 (58) hide show
  1. package/README.md +32 -7
  2. package/atris/skills/atris/SKILL.md +15 -2
  3. package/atris/skills/atris-feedback/SKILL.md +7 -0
  4. package/atris/skills/design/SKILL.md +29 -2
  5. package/atris/skills/engines/SKILL.md +44 -0
  6. package/atris/skills/flow/SKILL.md +1 -1
  7. package/atris/skills/wake/SKILL.md +37 -0
  8. package/atris/skills/youtube/SKILL.md +13 -39
  9. package/atris/team/validator/MEMBER.md +1 -0
  10. package/atris/wiki/concepts/agent-activation-contract.md +3 -3
  11. package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
  12. package/atris/wiki/index.md +1 -0
  13. package/atris.md +43 -19
  14. package/bin/atris.js +400 -30
  15. package/commands/agent-spawn.js +480 -0
  16. package/commands/analytics.js +6 -3
  17. package/commands/apps.js +11 -0
  18. package/commands/autopilot.js +42 -18
  19. package/commands/brain.js +74 -7
  20. package/commands/brainstorm.js +9 -58
  21. package/commands/clean.js +1 -4
  22. package/commands/compile.js +9 -4
  23. package/commands/console.js +8 -3
  24. package/commands/deck.js +135 -0
  25. package/commands/init.js +22 -11
  26. package/commands/lesson.js +76 -0
  27. package/commands/member.js +252 -48
  28. package/commands/mission.js +405 -13
  29. package/commands/now.js +4 -2
  30. package/commands/probe.js +105 -27
  31. package/commands/pulse.js +504 -0
  32. package/commands/radar.js +1 -0
  33. package/commands/recap.js +55 -25
  34. package/commands/run.js +615 -22
  35. package/commands/slop.js +173 -0
  36. package/commands/spaceship.js +39 -0
  37. package/commands/sync.js +0 -2
  38. package/commands/task.js +429 -37
  39. package/commands/verify.js +7 -3
  40. package/lib/activity-stream.js +166 -0
  41. package/lib/auto-accept-certified.js +23 -1
  42. package/lib/context-gatherer.js +170 -0
  43. package/lib/escape-regexp.js +13 -0
  44. package/lib/file-ops.js +6 -3
  45. package/lib/journal.js +1 -1
  46. package/lib/lesson-contradiction.js +113 -0
  47. package/lib/policy-lessons.js +3 -2
  48. package/lib/pulse.js +401 -0
  49. package/lib/runner-command.js +156 -0
  50. package/lib/slides-deck.js +236 -0
  51. package/lib/state-detection.js +1 -4
  52. package/lib/task-db.js +101 -4
  53. package/lib/task-proof.js +1 -1
  54. package/lib/todo-fallback.js +2 -1
  55. package/lib/todo-sections.js +33 -0
  56. package/package.json +1 -2
  57. package/utils/api.js +14 -2
  58. package/atris/atrisDev.md +0 -717
@@ -1994,6 +1994,28 @@ function repoRelative(root, filePath) {
1994
1994
  return path.relative(root, filePath).replace(/\\/g, '/');
1995
1995
  }
1996
1996
 
1997
+ function dateFromLogPath(relativePath) {
1998
+ const match = String(relativePath || '').match(/\b(\d{4}-\d{2}-\d{2})\b/);
1999
+ return match ? match[1] : null;
2000
+ }
2001
+
2002
+ function compareLogEvidenceRecentFirst(a, b) {
2003
+ const dateA = a?.date || '';
2004
+ const dateB = b?.date || '';
2005
+ if (dateA !== dateB) return dateB.localeCompare(dateA);
2006
+ const pathCompare = String(a?.path || '').localeCompare(String(b?.path || ''));
2007
+ if (pathCompare !== 0) return pathCompare;
2008
+ return Number(a?.line || 0) - Number(b?.line || 0);
2009
+ }
2010
+
2011
+ function compareRepeatedFailureSignals(a, b) {
2012
+ const countCompare = Number(b?.count || 0) - Number(a?.count || 0);
2013
+ if (countCompare !== 0) return countCompare;
2014
+ const evidenceCompare = compareLogEvidenceRecentFirst(a?.evidence?.[0], b?.evidence?.[0]);
2015
+ if (evidenceCompare !== 0) return evidenceCompare;
2016
+ return String(a?.pattern || '').localeCompare(String(b?.pattern || ''));
2017
+ }
2018
+
1997
2019
  function listFilesBounded(rootDir, { maxFiles = 220, extensions = ['.md', '.txt', '.json', '.jsonl'] } = {}) {
1998
2020
  const files = [];
1999
2021
  const stack = [rootDir];
@@ -2034,12 +2056,13 @@ function stripAutoImproverTitleNoise(text) {
2034
2056
  return compactSentence(clean, 180);
2035
2057
  }
2036
2058
 
2037
- function isAutoImproverGeneratedLogLine(line) {
2059
+ function isAutoImproverGeneratedLogLine(line, relativePath = '') {
2038
2060
  const text = String(line || '').trim();
2039
2061
  if (!text) return false;
2062
+ if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
2040
2063
  if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
2041
2064
  if (/\bauto_improver\b/i.test(text)) return true;
2042
- if (/^-\s*candidate:\s*recurring log pattern:/i.test(text)) return true;
2065
+ if (/^-\s*candidate:\s*/i.test(text)) return true;
2043
2066
  if (/recurring log pattern:\s*(candidate:|recurring log pattern:)/i.test(text)) return true;
2044
2067
  return false;
2045
2068
  }
@@ -2062,12 +2085,15 @@ function collectAutoImproverLogSignals(root) {
2062
2085
  const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
2063
2086
  const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
2064
2087
  const counts = new Map();
2088
+ const unclearActions = [];
2065
2089
  let filesScanned = 0;
2066
2090
  let linesScanned = 0;
2067
2091
  let unclearNextActions = 0;
2068
2092
  for (const scanRoot of roots) {
2069
2093
  for (const filePath of listFilesBounded(scanRoot)) {
2070
2094
  const relative = repoRelative(root, filePath);
2095
+ const logDate = dateFromLogPath(relative);
2096
+ if (relative.startsWith('atris/logs/archive/')) continue;
2071
2097
  const isRuntimeLog = relative.startsWith('atris/logs/')
2072
2098
  || /^atris\/team\/[^/]+\/logs\//.test(relative)
2073
2099
  || /^atris\/team\/[^/]+\/goals\.(md|json)$/.test(relative)
@@ -2076,37 +2102,52 @@ function collectAutoImproverLogSignals(root) {
2076
2102
  const text = safeReadText(filePath);
2077
2103
  if (!text) continue;
2078
2104
  filesScanned += 1;
2079
- const lines = text.split(/\r?\n/).slice(-500);
2105
+ const allLines = text.split(/\r?\n/);
2106
+ const lineOffset = Math.max(0, allLines.length - 500);
2107
+ const lines = allLines.slice(-500);
2080
2108
  linesScanned += lines.length;
2081
2109
  for (let index = 0; index < lines.length; index += 1) {
2110
+ const sourceLine = lineOffset + index + 1;
2082
2111
  const line = lines[index];
2083
- if (isAutoImproverGeneratedLogLine(line)) continue;
2112
+ if (isAutoImproverGeneratedLogLine(line, relative)) continue;
2084
2113
  // Declared verification receipts ("check: <command> ...") describe what
2085
2114
  // was verified, not what broke. Wiki upkeep sweeps write one per page,
2086
2115
  // so counting them as failures spawns bogus recurring-pattern tasks
2087
2116
  // (CLI-199 came from 13 such lines in atris/wiki/log.md).
2088
2117
  if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
2089
2118
  if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
2090
- if (unclearRegex.test(line)) unclearNextActions += 1;
2119
+ if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
2120
+ if (unclearRegex.test(line)) {
2121
+ unclearNextActions += 1;
2122
+ unclearActions.push({
2123
+ path: relative,
2124
+ date: logDate,
2125
+ line: sourceLine,
2126
+ text: compactSentence(line, 180),
2127
+ });
2128
+ }
2091
2129
  if (!failureRegex.test(line)) continue;
2092
2130
  const pattern = normalizeFailurePattern(line);
2093
2131
  if (!pattern) continue;
2094
2132
  const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
2095
2133
  existing.count += 1;
2096
- if (existing.evidence.length < 5) {
2097
- existing.evidence.push({
2098
- path: repoRelative(root, filePath),
2099
- line: index + 1,
2100
- text: compactSentence(line, 180),
2101
- });
2102
- }
2134
+ existing.evidence.push({
2135
+ path: relative,
2136
+ date: logDate,
2137
+ line: sourceLine,
2138
+ text: compactSentence(line, 180),
2139
+ });
2103
2140
  counts.set(pattern, existing);
2104
2141
  }
2105
2142
  }
2106
2143
  }
2107
2144
  const repeated = [...counts.values()]
2108
2145
  .filter((item) => item.count >= 2)
2109
- .sort((a, b) => b.count - a.count)
2146
+ .map((item) => ({
2147
+ ...item,
2148
+ evidence: item.evidence.sort(compareLogEvidenceRecentFirst).slice(0, 5),
2149
+ }))
2150
+ .sort(compareRepeatedFailureSignals)
2110
2151
  .slice(0, 8);
2111
2152
  return {
2112
2153
  files_scanned: filesScanned,
@@ -2114,9 +2155,19 @@ function collectAutoImproverLogSignals(root) {
2114
2155
  repeated_failures: repeated,
2115
2156
  repeated_failure_count: repeated.length,
2116
2157
  unclear_next_action_count: unclearNextActions,
2158
+ unclear_next_actions: unclearActions.sort(compareLogEvidenceRecentFirst).slice(0, 5),
2117
2159
  };
2118
2160
  }
2119
2161
 
2162
+ function autoImproverTaskWaitingForHuman(task, status) {
2163
+ if (status !== 'review') return false;
2164
+ const metadata = task?.metadata || {};
2165
+ const review = task?.review || {};
2166
+ const certified = metadata.agent_certified === true || review.agent_certified === true;
2167
+ const approval = lowerCompact(review.approval_status || metadata.approval_status || task?.approval_status || '');
2168
+ return certified && (!approval || approval === 'pending' || approval === 'agent_certified');
2169
+ }
2170
+
2120
2171
  function collectAutoImproverTaskSignals(root) {
2121
2172
  const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
2122
2173
  const projection = safeReadJson(projectionPath);
@@ -2135,10 +2186,11 @@ function collectAutoImproverTaskSignals(root) {
2135
2186
  status: status || null,
2136
2187
  owner: task.claimed_by || task.assigned_to || task.owner || task.metadata?.assigned_to || null,
2137
2188
  };
2189
+ const waitingForHuman = autoImproverTaskWaitingForHuman(task, status);
2138
2190
  if (status === 'blocked') blockedTasks.push(sample);
2139
2191
  if (status === 'review') reviewTasks.push(sample);
2140
- if (openStatuses.has(status)) staleTasks.push(sample);
2141
- if (/\b(tbd|unclear|unknown|needs proof|needs owner|blocked|stale|no next)\b/i.test(`${title} ${task.notes || ''}`)) {
2192
+ if (openStatuses.has(status) && !waitingForHuman) staleTasks.push(sample);
2193
+ if (openStatuses.has(status) && !waitingForHuman && /\b(tbd|unclear|unknown|needs proof|needs owner|blocked|stale|no next)\b/i.test(`${title} ${task.notes || ''}`)) {
2142
2194
  unclearTasks.push(sample);
2143
2195
  }
2144
2196
  }
@@ -2366,7 +2418,9 @@ function collectAutoImproverScan(root = process.cwd()) {
2366
2418
  title: 'Unclear next-action language is accumulating',
2367
2419
  problem: 'Several logs or tasks mention blocked/unclear/proof-needed states without a crisp next move.',
2368
2420
  recommendation: 'Convert the highest-value unclear item into one task with owner, proof, and stop rule.',
2369
- evidence: taskSignals.unclear_tasks.length ? taskSignals.unclear_tasks : logSignals.repeated_failures.slice(0, 3),
2421
+ evidence: taskSignals.unclear_tasks.length
2422
+ ? taskSignals.unclear_tasks
2423
+ : (logSignals.unclear_next_actions.length ? logSignals.unclear_next_actions : logSignals.repeated_failures.slice(0, 3)),
2370
2424
  score: 18,
2371
2425
  });
2372
2426
  }
@@ -2416,21 +2470,43 @@ function collectAutoImproverScan(root = process.cwd()) {
2416
2470
  };
2417
2471
  }
2418
2472
 
2473
+ // Lifecycle filter for wake dedupe: a task that already crossed the review boundary
2474
+ // (done/failed/archived, or human-accepted) must never be re-selected as the wake target —
2475
+ // that loop produced an endless "existing_task_found OBL-1433" no-op spiral (OBL-1469).
2476
+ const AUTO_IMPROVER_INACTIVE_STATUSES = new Set(['done', 'failed', 'archived']);
2477
+
2478
+ function autoImproverTaskIsActionable(task) {
2479
+ const status = String(task?.status || '').toLowerCase();
2480
+ if (AUTO_IMPROVER_INACTIVE_STATUSES.has(status)) return false;
2481
+ const approval = String(task?.metadata?.approval_status || task?.approval_status || '').toLowerCase();
2482
+ if (approval === 'accepted') return false;
2483
+ return true;
2484
+ }
2485
+
2419
2486
  function findExistingAutoImproverTask(title) {
2420
2487
  const projection = safeReadJson(path.join(process.cwd(), '.atris', 'state', 'tasks.projection.json'));
2421
2488
  const tasks = Array.isArray(projection?.tasks) ? projection.tasks : [];
2422
- const key = lowerCompact(title);
2489
+ const key = lowerCompact(stripAutoImproverTitleNoise(title));
2423
2490
  if (!key) return null;
2424
2491
  return tasks.find((task) => {
2425
- const taskTitle = lowerCompact(task.title || '');
2492
+ if (!autoImproverTaskIsActionable(task)) return false;
2493
+ const taskTitle = lowerCompact(stripAutoImproverTitleNoise(task.title || ''));
2426
2494
  if (!taskTitle) return false;
2427
2495
  return taskTitle.includes(key) || key.includes(taskTitle);
2428
2496
  }) || null;
2429
2497
  }
2430
2498
 
2431
- function createAutoImproverTask(candidate, receiptPath) {
2499
+ function autoImproverTaskTitle(candidate) {
2432
2500
  const core = stripAutoImproverTitleNoise(candidate?.title || 'Prevent top dogfood failure');
2433
- const title = `Auto-improver: ${compactSentence(core, 92)}`;
2501
+ return `Auto-improver: ${compactSentence(core, 92)}`;
2502
+ }
2503
+
2504
+ function existingAutoImproverTaskForCandidate(candidate) {
2505
+ return findExistingAutoImproverTask(autoImproverTaskTitle(candidate));
2506
+ }
2507
+
2508
+ function createAutoImproverTask(candidate, receiptPath) {
2509
+ const title = autoImproverTaskTitle(candidate);
2434
2510
  const existing = findExistingAutoImproverTask(title);
2435
2511
  if (existing) {
2436
2512
  return {
@@ -2496,11 +2572,18 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
2496
2572
  const scan = collectAutoImproverScan(process.cwd());
2497
2573
  const mode = execute ? 'execute' : 'dry_run';
2498
2574
  const candidate = scan.prevented_fire_candidate;
2499
- let createdTask = null;
2500
- let decision = candidate ? 'scan_found_problem' : 'scan_clean';
2501
- let reason = candidate ? `top_candidate:${candidate.source}` : 'no_prevented_fire_candidate';
2575
+ const existingTask = candidate ? existingAutoImproverTaskForCandidate(candidate) : null;
2576
+ let createdTask = existingTask ? {
2577
+ ok: true,
2578
+ existing: true,
2579
+ task_ref: taskRef(existingTask),
2580
+ task: existingTask,
2581
+ command: `atris task show ${taskRef(existingTask)} --json`,
2582
+ } : null;
2583
+ let decision = candidate ? (existingTask ? 'existing_task_found' : 'scan_found_problem') : 'scan_clean';
2584
+ let reason = candidate ? (existingTask ? 'auto_improver_task_already_exists' : `top_candidate:${candidate.source}`) : 'no_prevented_fire_candidate';
2502
2585
  let nextCommand = candidate
2503
- ? `atris member wake ${name} --execute --confirm-autonomy-policy --json`
2586
+ ? (existingTask ? createdTask.command : `atris member wake ${name} --execute --confirm-autonomy-policy --json`)
2504
2587
  : `atris member wake ${name} --json`;
2505
2588
 
2506
2589
  let payload = {
@@ -2571,27 +2654,42 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
2571
2654
  : 'No bounded prevented-fire task was created yet.',
2572
2655
  },
2573
2656
  };
2657
+ const previousLatest = safeReadJson(path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json'));
2574
2658
  const finalWrite = writeAutoImproverReceipt(name, payload, plannedReceiptPath);
2575
- const logPath = appendProjectLog('Auto-improver dogfood scan', {
2576
- member: name,
2577
- mode,
2578
- found: payload.proof.found_problems,
2579
- prevented: payload.proof.prevented_suffering,
2580
- pain_before: payload.pain.before,
2581
- pain_after: payload.pain.after,
2582
- candidate: candidate ? stripAutoImproverTitleNoise(candidate.title) : '',
2583
- task: payload.created_task?.task_ref || '',
2584
- receipt: repoRelative(process.cwd(), finalWrite.receiptPath),
2585
- });
2586
- const memberLogPath = appendMemberGoalLog(paths.memberDir, name, 'Auto-improver dogfood scan', {
2587
- mode,
2588
- found: payload.proof.found_problems,
2589
- prevented: payload.proof.prevented_suffering,
2590
- pain_before: payload.pain.before,
2591
- pain_after: payload.pain.after,
2592
- receipt: repoRelative(process.cwd(), finalWrite.receiptPath),
2593
- next: nextCommand,
2594
- });
2659
+ // A no-op scan identical to the previous tick earns a receipt but not a journal entry —
2660
+ // the cadence loop was appending the same "found: N, prevented: 0" row every ~4 minutes
2661
+ // and flooding the daily log (2026-06-10 had 100+ identical entries).
2662
+ const createdNewTask = Boolean(payload.created_task) && payload.created_task.existing === false;
2663
+ const duplicateNoop = Boolean(previousLatest)
2664
+ && !createdNewTask
2665
+ && previousLatest.decision === decision
2666
+ && previousLatest.proof?.found_problems === payload.proof.found_problems
2667
+ && previousLatest.pain?.before === payload.pain.before
2668
+ && (previousLatest.created_task?.task_ref || null) === (payload.created_task?.task_ref || null);
2669
+ let logPath = null;
2670
+ let memberLogPath = null;
2671
+ if (!duplicateNoop) {
2672
+ logPath = appendProjectLog('Auto-improver dogfood scan', {
2673
+ member: name,
2674
+ mode,
2675
+ found: payload.proof.found_problems,
2676
+ prevented: payload.proof.prevented_suffering,
2677
+ pain_before: payload.pain.before,
2678
+ pain_after: payload.pain.after,
2679
+ candidate: candidate ? stripAutoImproverTitleNoise(candidate.title) : '',
2680
+ task: payload.created_task?.task_ref || '',
2681
+ receipt: repoRelative(process.cwd(), finalWrite.receiptPath),
2682
+ });
2683
+ memberLogPath = appendMemberGoalLog(paths.memberDir, name, 'Auto-improver dogfood scan', {
2684
+ mode,
2685
+ found: payload.proof.found_problems,
2686
+ prevented: payload.proof.prevented_suffering,
2687
+ pain_before: payload.pain.before,
2688
+ pain_after: payload.pain.after,
2689
+ receipt: repoRelative(process.cwd(), finalWrite.receiptPath),
2690
+ next: nextCommand,
2691
+ });
2692
+ }
2595
2693
 
2596
2694
  return {
2597
2695
  ok: true,
@@ -2610,6 +2708,7 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
2610
2708
  latest_path: finalWrite.latestPath,
2611
2709
  log_path: logPath,
2612
2710
  member_log_path: memberLogPath,
2711
+ journal_skipped: duplicateNoop ? 'duplicate_noop' : null,
2613
2712
  created_task: payload.created_task,
2614
2713
  };
2615
2714
  }
@@ -2752,14 +2851,17 @@ function appendProjectLog(title, fields = {}) {
2752
2851
  // --- YAML Frontmatter Parser (shared with skill.js) ---
2753
2852
 
2754
2853
  function parseFrontmatter(content) {
2755
- const match = content.match(/^---\n([\s\S]*?)\n---/);
2854
+ // \r?\n so a CRLF-line-ending MEMBER.md (Windows-edited) still parses; otherwise
2855
+ // the delimiter never matched (and trailing \r broke the per-line key matchers),
2856
+ // silently dropping the member's entire frontmatter — role, skills, permissions, tools.
2857
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2756
2858
  if (!match) return null;
2757
2859
 
2758
2860
  const yaml = match[1];
2759
2861
  const result = {};
2760
2862
  let currentKey = null;
2761
2863
 
2762
- for (const line of yaml.split('\n')) {
2864
+ for (const line of yaml.split(/\r?\n/)) {
2763
2865
  const listMatch = line.match(/^\s+-\s+(.+)$/);
2764
2866
  if (listMatch && currentKey) {
2765
2867
  if (!Array.isArray(result[currentKey])) result[currentKey] = [];
@@ -3592,12 +3694,23 @@ function memberGoalFromMission(name, ...args) {
3592
3694
  goal.mission_id = runtime.id || goal.mission_id || null;
3593
3695
  goal.mission_north_star = purpose.northStar;
3594
3696
  goal.history = Array.isArray(goal.history) ? goal.history : [];
3595
- goal.history.push({
3697
+ const historyEntry = {
3596
3698
  at: stampIso(),
3597
3699
  event: existing ? 'goal_from_mission_reused' : 'goal_from_mission_created',
3598
3700
  mission_id: runtime.id || null,
3599
3701
  mission_status: runtime.status || null,
3600
- });
3702
+ };
3703
+ // Collapse repeated no-op reuse ticks (same mission state) instead of recording an identical entry
3704
+ // every cadence — that grew one member's goals.json to 2,200+ duplicate entries / 13K lines. Real
3705
+ // transitions (created, changed mission id/status) are still kept; only consecutive dupes are dropped.
3706
+ const lastEntry = goal.history[goal.history.length - 1];
3707
+ const isDuplicateReuse = existing && lastEntry
3708
+ && lastEntry.event === historyEntry.event
3709
+ && lastEntry.mission_id === historyEntry.mission_id
3710
+ && lastEntry.mission_status === historyEntry.mission_status;
3711
+ if (!isDuplicateReuse) goal.history.push(historyEntry);
3712
+ // Backstop cap so no event type can grow the file without bound (bloated files self-heal on write).
3713
+ if (goal.history.length > 50) goal.history = goal.history.slice(-50);
3601
3714
  if (!existing) state.goals.push(goal);
3602
3715
  state.goals = [goal, ...state.goals.filter((item) => item !== goal)];
3603
3716
  writeMemberGoals(paths, state);
@@ -7205,6 +7318,94 @@ function memberStatus(name, ...args) {
7205
7318
  );
7206
7319
  }
7207
7320
 
7321
+ function memberHistory(name, ...args) {
7322
+ const { spawnSync } = require('child_process');
7323
+
7324
+ const paths = requireMemberDir(name);
7325
+ const asJson = hasFlag(args, '--json');
7326
+ const limitArg = readNumberFlag(args, '--limit', null);
7327
+ const limit = limitArg !== null ? Math.max(1, limitArg) : null;
7328
+
7329
+ // resolve files to track: MEMBER.md + SOUL.md if present
7330
+ const filesToTrack = [];
7331
+ if (fs.existsSync(paths.memberFile)) {
7332
+ filesToTrack.push(paths.memberFile);
7333
+ }
7334
+ const soulPath = path.join(paths.memberDir, 'SOUL.md');
7335
+ if (fs.existsSync(soulPath)) {
7336
+ filesToTrack.push(soulPath);
7337
+ }
7338
+
7339
+ if (filesToTrack.length === 0) {
7340
+ // no files exist yet (unborn member); empty history ok
7341
+ const payload = {
7342
+ ok: true,
7343
+ action: 'history',
7344
+ member: name,
7345
+ files: [],
7346
+ };
7347
+ printJsonOrText(payload, [`identity history: ${name}`, '(no files found)'], asJson);
7348
+ return;
7349
+ }
7350
+
7351
+ // run git log for each file
7352
+ const cwd = process.cwd();
7353
+ const fileHistories = [];
7354
+
7355
+ for (const filePath of filesToTrack) {
7356
+ const relativePath = path.relative(cwd, filePath);
7357
+ const result = spawnSync('git', ['log', '--follow', '--pretty=format:%h|%ai|%s', '--', relativePath], {
7358
+ cwd,
7359
+ encoding: 'utf8',
7360
+ });
7361
+
7362
+ let commits = [];
7363
+ if (result.status === 0 && result.stdout) {
7364
+ const lines = result.stdout.trim().split('\n').filter(Boolean);
7365
+ commits = lines.map((line) => {
7366
+ const [hash, date, subject] = line.split('|');
7367
+ return { hash: hash || '', date: date || '', subject: subject || '' };
7368
+ });
7369
+
7370
+ // apply limit if specified
7371
+ if (limit !== null) {
7372
+ commits = commits.slice(0, limit);
7373
+ }
7374
+ }
7375
+
7376
+ fileHistories.push({
7377
+ path: relativePath,
7378
+ commits,
7379
+ });
7380
+ }
7381
+
7382
+ // render output
7383
+ if (asJson) {
7384
+ const payload = {
7385
+ ok: true,
7386
+ action: 'history',
7387
+ member: name,
7388
+ files: fileHistories,
7389
+ };
7390
+ console.log(JSON.stringify(payload, null, 2));
7391
+ } else {
7392
+ console.log('');
7393
+ console.log(`identity history: ${name}`);
7394
+ console.log('');
7395
+ for (const fileHist of fileHistories) {
7396
+ console.log(`${fileHist.path}:`);
7397
+ if (fileHist.commits.length === 0) {
7398
+ console.log(' (no git history)');
7399
+ } else {
7400
+ for (const commit of fileHist.commits) {
7401
+ console.log(` ${commit.hash} ${commit.date} ${commit.subject}`);
7402
+ }
7403
+ }
7404
+ console.log('');
7405
+ }
7406
+ }
7407
+ }
7408
+
7208
7409
  // --- Command Dispatcher ---
7209
7410
 
7210
7411
  async function memberCommand(subcommand, ...args) {
@@ -7254,6 +7455,8 @@ async function memberCommand(subcommand, ...args) {
7254
7455
  return memberBlock(args[0], args[1], ...args.slice(2));
7255
7456
  case 'status':
7256
7457
  return memberStatus(args[0], ...args.slice(1));
7458
+ case 'history':
7459
+ return memberHistory(args[0], ...args.slice(1));
7257
7460
  case 'supervisor':
7258
7461
  return memberSupervisorCommand(args[0], ...args.slice(1));
7259
7462
  case 'objective-generator':
@@ -7285,6 +7488,7 @@ async function memberCommand(subcommand, ...args) {
7285
7488
  console.log(' review <name> <id> Accept/discard an experiment with proof');
7286
7489
  console.log(' block <name> <id> Mark an experiment blocked with a human/orchestrator ask');
7287
7490
  console.log(' status <name> Show goal, open experiment, value, ask, and recent log');
7491
+ console.log(' history <name> Show git history of member identity files (MEMBER.md, SOUL.md)');
7288
7492
  console.log(' supervisor recommendations Show advisory supervisor recommendations');
7289
7493
  console.log(' objective-generator proposals Show autonomous objective proposal');
7290
7494
  console.log(' generalist proof Show latest cross-domain generalist proof');