ac-framework 2.0.0 → 2.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-framework",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Agentic Coding Framework - Multi-assistant configuration system with OpenSpec workflows",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -43,6 +43,14 @@ function runCommand(command, args, options = {}) {
43
43
  });
44
44
  }
45
45
 
46
+ function sleep(ms) {
47
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
48
+ }
49
+
50
+ function stripAnsi(text) {
51
+ return String(text || '').replace(/\x1B\[[0-9;]*m/g, '');
52
+ }
53
+
46
54
  function workerCommand(sessionId, role, roleLog) {
47
55
  return `bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`;
48
56
  }
@@ -91,26 +99,26 @@ export async function tmuxSessionExists(sessionName) {
91
99
  }
92
100
 
93
101
  async function writeZellijLayout({ layoutPath, sessionId, sessionDir }) {
94
- const panes = COLLAB_ROLES.map((role) => {
102
+ const paneNode = (role) => {
95
103
  const roleLog = roleLogPath(sessionDir, role);
96
104
  const cmd = workerCommand(sessionId, role, roleLog).replace(/"/g, '\\"');
97
- return ` pane name="${role}" command="bash" args { "-lc" "${cmd}" }`;
98
- });
105
+ return [
106
+ ` pane name="${role}" command="bash" {`,
107
+ ` args "-lc" "${cmd}"`,
108
+ ' }',
109
+ ].join('\n');
110
+ };
99
111
 
100
112
  const content = [
101
113
  'layout {',
102
- ' default_tab_template {',
103
- ' tab name="SynapseGrid" {',
104
- ' pane split_direction="vertical" {',
105
- ' pane split_direction="horizontal" {',
106
- panes[0],
107
- panes[1],
108
- ' }',
109
- ' pane split_direction="horizontal" {',
110
- panes[2],
111
- panes[3],
112
- ' }',
113
- ' }',
114
+ ' pane split_direction="vertical" {',
115
+ ' pane split_direction="horizontal" {',
116
+ paneNode(COLLAB_ROLES[0]),
117
+ paneNode(COLLAB_ROLES[1]),
118
+ ' }',
119
+ ' pane split_direction="horizontal" {',
120
+ paneNode(COLLAB_ROLES[2]),
121
+ paneNode(COLLAB_ROLES[3]),
114
122
  ' }',
115
123
  ' }',
116
124
  '}',
@@ -120,19 +128,55 @@ async function writeZellijLayout({ layoutPath, sessionId, sessionDir }) {
120
128
  await writeFile(layoutPath, content, 'utf8');
121
129
  }
122
130
 
123
- export async function spawnZellijSession({ sessionName, sessionDir, sessionId, binaryPath }) {
131
+ export async function spawnZellijSession({
132
+ sessionName,
133
+ sessionDir,
134
+ sessionId,
135
+ binaryPath,
136
+ waitForSessionMs = 10000,
137
+ pollIntervalMs = 250,
138
+ runCommandImpl,
139
+ }) {
124
140
  const layoutPath = resolve(sessionDir, 'synapsegrid-layout.kdl');
125
141
  await writeZellijLayout({ layoutPath, sessionId, sessionDir });
126
142
  const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
127
- await runCommand(command, ['--session', sessionName, '--layout', layoutPath, '--detach']);
128
- return { layoutPath };
143
+ const runner = runCommandImpl || runCommand;
144
+
145
+ const existing = await zellijSessionExists(sessionName, binaryPath, { runCommandImpl: runner });
146
+ if (existing) {
147
+ return { layoutPath };
148
+ }
149
+
150
+ await runner(command, ['--layout', layoutPath, 'attach', '--create-background', sessionName], {
151
+ cwd: sessionDir,
152
+ });
153
+
154
+ const startedAt = Date.now();
155
+ while ((Date.now() - startedAt) < waitForSessionMs) {
156
+ // eslint-disable-next-line no-await-in-loop
157
+ const exists = await zellijSessionExists(sessionName, binaryPath, { runCommandImpl: runner });
158
+ if (exists) {
159
+ return { layoutPath };
160
+ }
161
+ // eslint-disable-next-line no-await-in-loop
162
+ await sleep(pollIntervalMs);
163
+ }
164
+
165
+ throw new Error(
166
+ `Timed out waiting for zellij session '${sessionName}' to start (binary: ${command}). ` +
167
+ 'Try `acfm agents doctor` or fallback with `acfm agents start --mux tmux ...`'
168
+ );
129
169
  }
130
170
 
131
- export async function zellijSessionExists(sessionName, binaryPath) {
171
+ export async function zellijSessionExists(sessionName, binaryPath, options = {}) {
132
172
  try {
173
+ const runner = options.runCommandImpl || runCommand;
133
174
  const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
134
- const result = await runCommand(command, ['list-sessions']);
135
- const lines = result.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
175
+ const result = await runner(command, ['list-sessions']);
176
+ const lines = stripAnsi(result.stdout)
177
+ .split('\n')
178
+ .map((line) => line.trim())
179
+ .filter(Boolean);
136
180
  return lines.some((line) => line === sessionName || line.startsWith(`${sessionName} `));
137
181
  } catch {
138
182
  return false;
@@ -1503,7 +1503,15 @@ Examples:
1503
1503
  try {
1504
1504
  await runZellij(['delete-session', muxSessionName], { binaryPath: zellijPath });
1505
1505
  } catch {
1506
- // ignore if already closed
1506
+ try {
1507
+ await runZellij(['kill-session', muxSessionName], { binaryPath: zellijPath });
1508
+ } catch {
1509
+ try {
1510
+ await runZellij(['delete-session', '--force', muxSessionName], { binaryPath: zellijPath });
1511
+ } catch {
1512
+ // ignore if already closed
1513
+ }
1514
+ }
1507
1515
  }
1508
1516
  }
1509
1517
  if (multiplexer === 'tmux' && muxSessionName && hasCommand('tmux')) {