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.
- package/ax.js +157 -12
- 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
|
|
1729
|
-
if (config.rateLimitPattern && config.rateLimitPattern.test(
|
|
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
|
-
|
|
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)}
|
|
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.
|
|
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
|
-
|
|
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(
|