@syntax-syllogism/aloop 0.8.2 → 0.8.3

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": "@syntax-syllogism/aloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Syntax & Syllogism agentic loop runner.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/adapters.mjs CHANGED
@@ -413,16 +413,26 @@ const geminiAdapter = {
413
413
  // until the process exits, so a multi-minute phase is indistinguishable from
414
414
  // a hang. stream-json emits an event per step, which `createRenderer` turns
415
415
  // back into readable lines — same reasoning as the claude adapter above.
416
+ //
417
+ // The prompt goes on stdin, not `--prompt`. Gemini's Windows launcher is a
418
+ // `.cmd`/`.ps1` shim, so on Git Bash aloop runs it through `cmd.exe /c`
419
+ // (see command.mjs windowsSpawnSpec). cmd.exe caps the whole command line at
420
+ // ~8191 chars and mangles newlines and metacharacters (`% ! & | < >`), so a
421
+ // real review prompt — work item plus instructions — arrives truncated or
422
+ // garbled and Gemini "ignores" it. stdin is a pipe cmd.exe never parses, so
423
+ // the prompt survives intact. `-p ""` still selects headless mode; Gemini
424
+ // documents `--prompt` as "appended to input on stdin", so an empty flag
425
+ // leaves the stdin prompt as the whole prompt.
416
426
  const canWrite = permissionLevel(permissions) === PERMISSIONS.WRITE_WORKTREE || artifactOnly;
417
427
  const args = [
418
- '--prompt', prompt,
428
+ '--prompt', '',
419
429
  '--approval-mode', canWrite ? 'yolo' : 'plan',
420
430
  '--skip-trust',
421
431
  '--output-format', 'stream-json',
422
432
  ];
423
433
  if (agent.model) args.push('--model', agent.model);
424
434
  for (const dir of addDirs) args.push('--include-directories', dir);
425
- return { command: 'gemini', args };
435
+ return { command: 'gemini', args, input: prompt };
426
436
  },
427
437
  createRenderer: () => createJsonlRenderer(renderGeminiEvent),
428
438
  };
package/src/pipeline.mjs CHANGED
@@ -491,7 +491,7 @@ async function runAgent(phase, ctx, variables) {
491
491
  // writable. Every other artifact-only phase — pr-description included — gets
492
492
  // just its own run directory; it has no business touching the task file repo.
493
493
  const artifactOnlyDirs = phase.role === 'verdict' ? ctx.artifactDirs : [ctx.state.dir];
494
- const { command, args } = adapter.command({
494
+ const { command, args, input } = adapter.command({
495
495
  prompt,
496
496
  cwd: agentCwd,
497
497
  addDirs: worktreeWrite ? ctx.addDirs : [...artifactOnlyDirs, ...(sourceSnapshot ? [sourceSnapshot.path] : [])],
@@ -539,6 +539,10 @@ async function runAgent(phase, ctx, variables) {
539
539
  env: execution.env,
540
540
  timeoutMs: ctx.config.timeoutMs,
541
541
  activeProcessPath: ctx.activeProcessPath,
542
+ // Adapters that carry the prompt on stdin (gemini, to dodge the cmd.exe
543
+ // command-line limit on Windows) return it as `input`; a hermetic wrapper
544
+ // forwards stdin to the sandboxed process unchanged.
545
+ input,
542
546
  onOutput: (text, stream) => emit(stream === 'stderr' ? text : renderer.write(text)),
543
547
  });
544
548
  } catch (error) {
package/src/state.mjs CHANGED
@@ -37,6 +37,15 @@ function isActiveProcessAlive(activeProcess) {
37
37
  return isProcessGroupAlive(activeProcess?.processGroupId) || isProcessAlive(activeProcess?.pid);
38
38
  }
39
39
 
40
+ async function pathExists(path) {
41
+ try {
42
+ await access(path);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
40
49
  async function readActiveProcess(dir) {
41
50
  try {
42
51
  return JSON.parse(await readFile(join(dir, 'active-command.json'), 'utf8'));
@@ -238,7 +247,16 @@ export class RunState {
238
247
  await publishLock(path, lockToken, contents);
239
248
  return lockToken;
240
249
  } catch (error) {
241
- if (!['EEXIST', 'ENOTEMPTY'].includes(error.code)) throw error;
250
+ // POSIX rejects a rename onto the existing (non-empty) lock directory
251
+ // with EEXIST/ENOTEMPTY — the signal that the lock is already held.
252
+ // Windows rejects the same rename with EPERM (sometimes EACCES)
253
+ // instead, so without this a `--resume` that finds any prior lock dir
254
+ // crashes with "EPERM: operation not permitted, rename". Treat those as
255
+ // contention too, but only when the lock path is actually present, so a
256
+ // genuine permission failure still surfaces instead of spinning here.
257
+ const contended = ['EEXIST', 'ENOTEMPTY'].includes(error.code)
258
+ || (['EPERM', 'EACCES'].includes(error.code) && await pathExists(path));
259
+ if (!contended) throw error;
242
260
  }
243
261
 
244
262
  let existing;
package/src/types.d.ts CHANGED
@@ -70,6 +70,12 @@ export interface AdapterCommandOptions {
70
70
  export interface AdapterCommand {
71
71
  command: string;
72
72
  args: string[];
73
+ /**
74
+ * Optional stdin for the command. Adapters use it to carry the prompt off the
75
+ * argument vector (gemini does, to dodge the cmd.exe command-line limit on
76
+ * Windows) rather than passing it as an argv element.
77
+ */
78
+ input?: string;
73
79
  [key: string]: unknown;
74
80
  }
75
81