ax-agents 0.0.1-alpha.7 → 0.0.1-alpha.8

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 (2) hide show
  1. package/ax.js +157 -12
  2. package/package.json +1 -1
package/ax.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  readSync,
28
28
  closeSync,
29
29
  } from "node:fs";
30
- import { randomUUID } from "node:crypto";
30
+ import { randomUUID, createHash } from "node:crypto";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import path from "node:path";
33
33
  import os from "node:os";
@@ -305,6 +305,18 @@ const TRUNCATE_THINKING_LEN = 300;
305
305
  const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
306
306
  const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
307
307
  const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
308
+ const ARCHANGEL_PREAMBLE = `## Guidelines
309
+
310
+ - Investigate before speaking. If uncertain, read more code and trace the logic until you're confident.
311
+ - Explain WHY something is an issue, not just that it is.
312
+ - Focus on your area of expertise.
313
+ - Calibrate to the task or plan. Don't suggest refactors during a bug fix.
314
+ - Be clear. Brief is fine, but never sacrifice clarity.
315
+ - For critical issues, request for them to be added to the todo list.
316
+ - Don't repeat observations you've already made unless you have more to say or better clarity.
317
+ - Make judgment calls - don't ask questions.
318
+
319
+ "No issues found." is a valid response when there's nothing significant to report.`;
308
320
 
309
321
  /**
310
322
  * @param {string} session
@@ -544,6 +556,16 @@ function generateSessionName(tool) {
544
556
  return `${tool}-partner-${randomUUID()}`;
545
557
  }
546
558
 
559
+ /**
560
+ * Quick hash for change detection (not cryptographic).
561
+ * @param {string | null | undefined} str
562
+ * @returns {string | null}
563
+ */
564
+ function quickHash(str) {
565
+ if (!str) return null;
566
+ return createHash("md5").update(str).digest("hex").slice(0, 8);
567
+ }
568
+
547
569
  /**
548
570
  * @param {string} cwd
549
571
  * @returns {string}
@@ -671,6 +693,93 @@ function findCodexLogPath(sessionName) {
671
693
  }
672
694
  }
673
695
 
696
+ /**
697
+ * @typedef {Object} SessionMeta
698
+ * @property {string | null} slug - Plan identifier (if plan is active)
699
+ * @property {Array<{content: string, status: string, id?: string}> | null} todos - Current todos
700
+ * @property {string | null} permissionMode - "default", "acceptEdits", "plan"
701
+ * @property {string | null} gitBranch - Current git branch
702
+ * @property {string | null} cwd - Working directory
703
+ */
704
+
705
+ /**
706
+ * Get metadata from a Claude session's JSONL file.
707
+ * Returns null for Codex sessions (different format, no equivalent metadata).
708
+ * @param {string} sessionName - The tmux session name
709
+ * @returns {SessionMeta | null}
710
+ */
711
+ function getSessionMeta(sessionName) {
712
+ const parsed = parseSessionName(sessionName);
713
+ if (!parsed) return null;
714
+
715
+ // Only Claude sessions have this metadata
716
+ if (parsed.tool !== "claude") return null;
717
+ if (!parsed.uuid) return null;
718
+
719
+ const logPath = findClaudeLogPath(parsed.uuid, sessionName);
720
+ if (!logPath || !existsSync(logPath)) return null;
721
+
722
+ try {
723
+ const content = readFileSync(logPath, "utf-8");
724
+ const lines = content.trim().split("\n").filter(Boolean);
725
+
726
+ // Read from end to find most recent entry with metadata
727
+ for (let i = lines.length - 1; i >= 0; i--) {
728
+ try {
729
+ const entry = JSON.parse(lines[i]);
730
+ // User entries typically have the metadata fields
731
+ if (entry.type === "user" || entry.slug || entry.gitBranch) {
732
+ return {
733
+ slug: entry.slug || null,
734
+ todos: entry.todos || null,
735
+ permissionMode: entry.permissionMode || null,
736
+ gitBranch: entry.gitBranch || null,
737
+ cwd: entry.cwd || null,
738
+ };
739
+ }
740
+ } catch {
741
+ // Skip malformed lines
742
+ }
743
+ }
744
+ return null;
745
+ } catch (err) {
746
+ debugError("getSessionMeta", err);
747
+ return null;
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Read a plan file by its slug.
753
+ * @param {string} slug - The plan slug (e.g., "curious-roaming-pascal")
754
+ * @returns {string | null} The plan content or null if not found
755
+ */
756
+ function readPlanFile(slug) {
757
+ const planPath = path.join(CLAUDE_CONFIG_DIR, "plans", `${slug}.md`);
758
+ try {
759
+ if (existsSync(planPath)) {
760
+ return readFileSync(planPath, "utf-8");
761
+ }
762
+ } catch (err) {
763
+ debugError("readPlanFile", err);
764
+ }
765
+ return null;
766
+ }
767
+
768
+ /**
769
+ * Format todos for display in a prompt.
770
+ * @param {Array<{content: string, status: string, id?: string}>} todos
771
+ * @returns {string}
772
+ */
773
+ function formatTodos(todos) {
774
+ if (!todos || todos.length === 0) return "";
775
+ return todos
776
+ .map((t) => {
777
+ const status = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
778
+ return `${status} ${t.content || "(no content)"}`;
779
+ })
780
+ .join("\n");
781
+ }
782
+
674
783
  /**
675
784
  * Extract assistant text responses from a JSONL log file.
676
785
  * This provides clean text without screen-scraped artifacts.
@@ -1725,8 +1834,8 @@ function detectState(screen, config) {
1725
1834
  // Larger range for confirmation detection (catches dialogs that scrolled slightly)
1726
1835
  const recentLines = lines.slice(-15).join("\n");
1727
1836
 
1728
- // Rate limited - check full screen (rate limit messages can appear anywhere)
1729
- if (config.rateLimitPattern && config.rateLimitPattern.test(screen)) {
1837
+ // Rate limited - check recent lines (not full screen to avoid matching historical output)
1838
+ if (config.rateLimitPattern && config.rateLimitPattern.test(recentLines)) {
1730
1839
  return State.RATE_LIMITED;
1731
1840
  }
1732
1841
 
@@ -2508,19 +2617,22 @@ function cmdAgents() {
2508
2617
  const agent = parsed.tool === "claude" ? ClaudeAgent : CodexAgent;
2509
2618
  const screen = tmuxCapture(session);
2510
2619
  const state = agent.getState(screen);
2511
- const logPath = agent.findLogPath(session);
2512
2620
  const type = parsed.archangelName ? "archangel" : "-";
2513
2621
  const isDefault =
2514
2622
  (parsed.tool === "claude" && session === claudeDefault) ||
2515
2623
  (parsed.tool === "codex" && session === codexDefault);
2516
2624
 
2625
+ // Get session metadata (Claude only)
2626
+ const meta = getSessionMeta(session);
2627
+
2517
2628
  return {
2518
2629
  session,
2519
2630
  tool: parsed.tool,
2520
2631
  state: state || "unknown",
2521
2632
  target: isDefault ? "*" : "",
2522
2633
  type,
2523
- log: logPath || "-",
2634
+ plan: meta?.slug || "-",
2635
+ branch: meta?.gitBranch || "-",
2524
2636
  };
2525
2637
  });
2526
2638
 
@@ -2530,13 +2642,14 @@ function cmdAgents() {
2530
2642
  const maxState = Math.max(5, ...agents.map((a) => a.state.length));
2531
2643
  const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
2532
2644
  const maxType = Math.max(4, ...agents.map((a) => a.type.length));
2645
+ const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
2533
2646
 
2534
2647
  console.log(
2535
- `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} LOG`,
2648
+ `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
2536
2649
  );
2537
2650
  for (const a of agents) {
2538
2651
  console.log(
2539
- `${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.log}`,
2652
+ `${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.plan.padEnd(maxPlan)} ${a.branch}`,
2540
2653
  );
2541
2654
  }
2542
2655
 
@@ -2685,6 +2798,13 @@ async function cmdArchangel(agentName) {
2685
2798
  let isProcessing = false;
2686
2799
  const intervalMs = config.interval * 1000;
2687
2800
 
2801
+ // Hash tracking for incremental context updates
2802
+ /** @type {string | null} */
2803
+ let lastPlanHash = null;
2804
+ /** @type {string | null} */
2805
+ let lastTodosHash = null;
2806
+ let isFirstTrigger = true;
2807
+
2688
2808
  async function processChanges() {
2689
2809
  clearTimeout(debounceTimer);
2690
2810
  clearTimeout(maxWaitTimer);
@@ -2702,6 +2822,21 @@ async function cmdArchangel(agentName) {
2702
2822
  const parent = findParentSession();
2703
2823
  const logPath = parent ? findClaudeLogPath(parent.uuid, parent.session) : null;
2704
2824
 
2825
+ // Get orientation context (plan and todos) from parent session
2826
+ const meta = parent?.session ? getSessionMeta(parent.session) : null;
2827
+ const planContent = meta?.slug ? readPlanFile(meta.slug) : null;
2828
+ const todosContent = meta?.todos?.length ? formatTodos(meta.todos) : null;
2829
+
2830
+ // Check if plan/todos have changed since last trigger
2831
+ const planHash = quickHash(planContent);
2832
+ const todosHash = quickHash(todosContent);
2833
+ const includePlan = planHash !== lastPlanHash;
2834
+ const includeTodos = todosHash !== lastTodosHash;
2835
+
2836
+ // Update tracking for next trigger
2837
+ lastPlanHash = planHash;
2838
+ lastTodosHash = todosHash;
2839
+
2705
2840
  // Build file-specific context from JSONL
2706
2841
  const fileContexts = [];
2707
2842
  for (const file of files.slice(0, 5)) {
@@ -2713,7 +2848,18 @@ async function cmdArchangel(agentName) {
2713
2848
  }
2714
2849
 
2715
2850
  // Build the prompt
2716
- let prompt = basePrompt;
2851
+ // First trigger: include intro, guidelines, and focus (archangel has memory)
2852
+ let prompt = isFirstTrigger
2853
+ ? `You are the archangel of ${agentName}.\n\n${ARCHANGEL_PREAMBLE}\n\n## Focus\n\n${basePrompt}\n\n---`
2854
+ : "";
2855
+
2856
+ // Add orientation context (plan and todos) only if changed since last trigger
2857
+ if (includePlan && planContent) {
2858
+ prompt += (prompt ? "\n\n" : "") + "## Current Plan\n\n" + planContent;
2859
+ }
2860
+ if (includeTodos && todosContent) {
2861
+ prompt += (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
2862
+ }
2717
2863
 
2718
2864
  if (fileContexts.length > 0) {
2719
2865
  prompt += "\n\n## Recent Edits (from parent session)\n";
@@ -2747,8 +2893,7 @@ async function cmdArchangel(agentName) {
2747
2893
  prompt += "\n\n## Git Context\n\n" + gitContext;
2748
2894
  }
2749
2895
 
2750
- prompt +=
2751
- '\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
2896
+ prompt += "\n\nReview these changes.";
2752
2897
  } else {
2753
2898
  // Fallback: no JSONL context available, use conversation + git context
2754
2899
  const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
@@ -2768,8 +2913,7 @@ async function cmdArchangel(agentName) {
2768
2913
  prompt += "\n\n## Git Context\n\n" + gitContext;
2769
2914
  }
2770
2915
 
2771
- prompt +=
2772
- '\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
2916
+ prompt += "\n\nReview these changes.";
2773
2917
  }
2774
2918
 
2775
2919
  // Check session still exists
@@ -2798,6 +2942,7 @@ async function cmdArchangel(agentName) {
2798
2942
  await sleep(200); // Allow time for large prompts to be processed
2799
2943
  tmuxSend(sessionName, "Enter");
2800
2944
  await sleep(100); // Ensure Enter is processed
2945
+ isFirstTrigger = false;
2801
2946
 
2802
2947
  // Wait for response
2803
2948
  const { state: endState, screen: afterScreen } = await waitForResponse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ax-agents",
3
- "version": "0.0.1-alpha.7",
3
+ "version": "0.0.1-alpha.8",
4
4
  "description": "A CLI for orchestrating AI coding agents via tmux",
5
5
  "bin": {
6
6
  "ax": "ax.js",