agileflow 4.0.0-alpha.23 → 4.0.0-alpha.24

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": "agileflow",
3
- "version": "4.0.0-alpha.23",
3
+ "version": "4.0.0-alpha.24",
4
4
  "description": "AI-driven agile development toolkit for Claude Code — skills-first architecture with opt-in plugins (v4)",
5
5
  "keywords": [
6
6
  "agile",
@@ -1155,6 +1155,11 @@ async function runInternalSnapshotSession(sessionName, deps = {}) {
1155
1155
  const runner = deps.runner || defaultTmuxRunner();
1156
1156
  const DELIM = "\x1f";
1157
1157
  const fmt = `#{window_index}${DELIM}#{window_name}${DELIM}#{pane_current_path}`;
1158
+ // Capture timestamp BEFORE list-windows so concurrent subprocesses
1159
+ // produce a stable ordering. The registry rejects writes whose
1160
+ // capturedAt is older than what's already stored — without this,
1161
+ // out-of-order lock acquisition can clobber newer state with older.
1162
+ const capturedAt = Date.now();
1158
1163
  const result = runner.runSync(["list-windows", "-t", sessionName, "-F", fmt]);
1159
1164
  if (result.status !== 0) return;
1160
1165
  const lines = (result.stdout || "")
@@ -1177,7 +1182,7 @@ async function runInternalSnapshotSession(sessionName, deps = {}) {
1177
1182
  const {
1178
1183
  updateSession,
1179
1184
  } = require("../../runtime/launch/session-registry.js");
1180
- updateSession(sessionName, { windows });
1185
+ updateSession(sessionName, { windows, windowsCapturedAt: capturedAt });
1181
1186
  } catch {
1182
1187
  // Registry might not exist or session might have been forgotten;
1183
1188
  // either way we can't usefully recover. Stay silent.
@@ -137,6 +137,10 @@ function withRegistryLock(home, fn) {
137
137
  * by tmux hooks; replayed on
138
138
  * restore so reboots bring back
139
139
  * every tab in its original cwd
140
+ * @property {number} [windowsCapturedAt] - epoch ms when `windows` was
141
+ * captured. Newer captures
142
+ * always win when concurrent
143
+ * hook subprocesses race.
140
144
  *
141
145
  * @typedef {Object} RegistryShape
142
146
  * @property {1} version
@@ -233,6 +237,8 @@ function loadRegistry(home) {
233
237
  }
234
238
  : undefined,
235
239
  windows,
240
+ windowsCapturedAt:
241
+ typeof s.windowsCapturedAt === "number" ? s.windowsCapturedAt : 0,
236
242
  });
237
243
  }
238
244
  return { version: 1, sessions: sane };
@@ -369,7 +375,22 @@ function updateSession(name, patch, home) {
369
375
  if (patch.worktree !== undefined) s.worktree = patch.worktree;
370
376
  if (patch.pinned !== undefined) s.pinned = patch.pinned === true;
371
377
  if (patch.windows !== undefined) {
372
- s.windows = Array.isArray(patch.windows) ? patch.windows : undefined;
378
+ // Stale-snapshot guard: hooks fire on every tab change, so
379
+ // when the user spams Alt+t multiple subprocesses can race.
380
+ // Lock acquisition isn't FIFO so an older capture can be
381
+ // committed after a newer one. Reject any update whose
382
+ // capturedAt timestamp is older than what's already stored.
383
+ const incomingTs =
384
+ typeof patch.windowsCapturedAt === "number"
385
+ ? patch.windowsCapturedAt
386
+ : Date.now();
387
+ if (!s.windowsCapturedAt || incomingTs >= s.windowsCapturedAt) {
388
+ s.windows = Array.isArray(patch.windows)
389
+ ? patch.windows
390
+ : undefined;
391
+ s.windowsCapturedAt = incomingTs;
392
+ }
393
+ // else: silently drop the stale write
373
394
  }
374
395
  updated = true;
375
396
  }
@@ -344,9 +344,35 @@ function applyTabFormat(sessionName, runner, opts = {}) {
344
344
  */
345
345
  function installSessionHooks(sessionName, runner, opts = {}) {
346
346
  const agileflowBin = opts.agileflowBin || resolveAgileflowBin();
347
- const snapshotCmd = `run-shell -b '${agileflowBin} launch __snapshot-session ${sessionName}'`;
347
+ // Double-quote the binary path and session name so paths with
348
+ // spaces (e.g., /Users/x with space/agileflow) work. The shell
349
+ // command is itself wrapped in double quotes for run-shell to
350
+ // accept it as a single argument.
351
+ const shellCmd = `"${agileflowBin}" launch __snapshot-session "${sessionName}"`;
352
+ const snapshotCmd = `run-shell -b "${shellCmd.replace(/"/g, '\\"')}"`;
353
+ /** @type {Array<{ event: string, stderr: string }>} */
354
+ const failures = [];
348
355
  for (const event of ["window-linked", "window-unlinked", "window-renamed"]) {
349
- runner.runSync(["set-hook", "-t", sessionName, event, snapshotCmd]);
356
+ const r = runner.runSync([
357
+ "set-hook",
358
+ "-t",
359
+ sessionName,
360
+ event,
361
+ snapshotCmd,
362
+ ]);
363
+ if (r.status !== 0) {
364
+ failures.push({ event, stderr: (r.stderr || "").trim() });
365
+ }
366
+ }
367
+ if (failures.length > 0) {
368
+ // eslint-disable-next-line no-console
369
+ console.error(
370
+ `agileflow launch: tab persistence — ${failures.length} of 3 tmux hooks failed to install:`,
371
+ );
372
+ for (const f of failures) {
373
+ // eslint-disable-next-line no-console
374
+ console.error(` ${f.event}: ${f.stderr || "(no error message)"}`);
375
+ }
350
376
  }
351
377
  }
352
378