praxis-agent 0.59.1 → 0.60.1

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/README.md CHANGED
@@ -102,6 +102,7 @@ Common non-interactive operations:
102
102
 
103
103
  ```sh
104
104
  praxis -p "Inspect this project"
105
+ printf 'Inspect this project\n' | praxis -p
105
106
  praxis -p --output-format json "Summarize the test failures"
106
107
  praxis --resume
107
108
  praxis sessions --json
@@ -126,8 +127,11 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
126
127
  composer grammar, compact stable tool rows, responsive density,
127
128
  terminal-native background, and a minimal composer/status row. Successful
128
129
  background Bash completion bursts collapse in normal reading, while
129
- failed/stopped notifications remain detailed. Interactive
130
- surfaces share the same presentation across terminals, with English
130
+ failed/stopped notifications remain detailed.
131
+ Prompt-like background Bash output that remains unchanged for 50 seconds
132
+ raises one warning and model follow-up without stopping or reclassifying the
133
+ running task; silent and ordinary output remain quiet.
134
+ Interactive surfaces share the same presentation across terminals, with English
131
135
  permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
132
136
  interaction grammar. While a regular turn is active, the composer remains
133
137
  editable: Enter steers at the next safe continuation boundary, Tab or
@@ -210,7 +214,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
210
214
  Claude-compatible main-thread agent definitions with native prompt, model,
211
215
  tool, memory, first-turn, and resume behavior. Agent execution uses one
212
216
  durable lifecycle vocabulary with bounded cancellation and drain,
213
- continuation, notifications, and single-owner orphan recovery. Experimental
217
+ continuation, notifications, and single-owner orphan recovery. Isolated
218
+ Workflow turns use ownership-recorded repo-local temporary worktrees and
219
+ retain dirty or committed results for inspection. Experimental
214
220
  local Teams (`PRAXIS_ENABLE_TEAMS=true`) stay absent from ordinary startup by
215
221
  default and add durable task ownership plus one ordered mailbox with stable
216
222
  identities, fixed broadcast recipients, durable cursors, bounded retention,
@@ -18,6 +18,7 @@ export interface BackgroundBashManagerOptions {
18
18
  stateRoot: string;
19
19
  maxOutputBytes?: number;
20
20
  eventSink?: RuntimeEventSink;
21
+ stallWatchdogMs?: number;
21
22
  }
22
23
  export interface BackgroundBashLaunchInput {
23
24
  command: string;
@@ -40,6 +41,7 @@ export declare class BackgroundBashManager {
40
41
  private readonly runner;
41
42
  private readonly outputRoot;
42
43
  private readonly sessionStateRoot;
44
+ private readonly stallWatchdogMs;
43
45
  constructor(options: BackgroundBashManagerOptions);
44
46
  has(taskId: string): boolean;
45
47
  snapshots(): Promise<readonly BackgroundBashSnapshot[]>;
@@ -56,6 +58,8 @@ export declare class BackgroundBashManager {
56
58
  stop(taskId: string): Promise<BackgroundBashToolResult>;
57
59
  notifications(waitForRunning: boolean): Promise<string[]>;
58
60
  private run;
61
+ private clearWatchdog;
62
+ private rearmWatchdog;
59
63
  private emitNotification;
60
64
  private complete;
61
65
  private outputResult;
@@ -53,14 +53,48 @@ function escapeXml(value) {
53
53
  .replaceAll('"', '&quot;')
54
54
  .replaceAll("'", '&apos;');
55
55
  }
56
+ function isPromptTail(output) {
57
+ const tail = output
58
+ .slice(-1024)
59
+ .replace(new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'gu'), '');
60
+ const lines = tail.split(/\r?\n/u);
61
+ const line = [...lines].reverse().find((item) => item.trim() !== '');
62
+ if (!line)
63
+ return false;
64
+ const value = line.trim().replace(/[\t ]+$/u, '');
65
+ if (/\?\s*(?:\[[^\]]{1,20}\]|\([^)]{1,20}\))$/u.test(value))
66
+ return true;
67
+ if (/^(?:password|passphrase|username)\s*:\s*$/iu.test(value) ||
68
+ /^(?:enter|input|select|selection|choice|confirm)\b[^\n:]{0,100}:\s*$/iu.test(value))
69
+ return true;
70
+ return /^(?:press|hit)\s+(?:enter|return|any key)(?:\s+to\b[^\n]*)?[.!?:…]?$/iu.test(value);
71
+ }
72
+ function watchdogNotification(task, summary) {
73
+ return `<task-notification>\n<task-id>${escapeXml(task.taskId)}</task-id>\n<tool-use-id>${escapeXml(task.toolUseId)}</tool-use-id>\n<output-file>${escapeXml(task.outputFile)}</output-file>\n<summary>${escapeXml(summary)}</summary>\n</task-notification>\nLast output:\n${task.output}\n\nThe command is likely blocked on an interactive prompt. Stop this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`;
74
+ }
75
+ function createWatchdogWake() {
76
+ let resolveWake;
77
+ const promise = new Promise((resolve) => {
78
+ resolveWake = resolve;
79
+ });
80
+ return { promise, resolve: resolveWake };
81
+ }
56
82
  export class BackgroundBashManager {
57
83
  options;
58
84
  tasks = new Map();
59
85
  runner;
60
86
  outputRoot;
61
87
  sessionStateRoot;
88
+ stallWatchdogMs;
62
89
  constructor(options) {
63
90
  this.options = options;
91
+ if (options.stallWatchdogMs !== undefined &&
92
+ (!Number.isFinite(options.stallWatchdogMs) ||
93
+ !Number.isInteger(options.stallWatchdogMs) ||
94
+ options.stallWatchdogMs <= 0)) {
95
+ throw new RangeError('stallWatchdogMs must be a positive integer');
96
+ }
97
+ this.stallWatchdogMs = options.stallWatchdogMs ?? 50_000;
64
98
  this.runner = new BoundedProcessRunner({
65
99
  cwd: options.cwd,
66
100
  maxOutputBytes: options.maxOutputBytes ?? 128 * 1024,
@@ -88,6 +122,7 @@ export class BackgroundBashManager {
88
122
  const outputFile = resolve(this.outputRoot, `${id}.output`);
89
123
  await writeFile(outputFile, '', { mode: 0o600 });
90
124
  const controller = new AbortController();
125
+ const watchdogWake = createWatchdogWake();
91
126
  const task = {
92
127
  taskId: id,
93
128
  command: input.command,
@@ -108,6 +143,11 @@ export class BackgroundBashManager {
108
143
  parentAbort: () => controller.abort(),
109
144
  }
110
145
  : {}),
146
+ watchdogTimer: null,
147
+ watchdogFired: false,
148
+ pendingWatchdogMessage: null,
149
+ watchdogWake: watchdogWake.promise,
150
+ watchdogWakeResolve: watchdogWake.resolve,
111
151
  };
112
152
  if (task.parentSignal && task.parentAbort) {
113
153
  if (task.parentSignal.aborted)
@@ -192,12 +232,25 @@ export class BackgroundBashManager {
192
232
  async notifications(waitForRunning) {
193
233
  await this.hydratePersistedTasks();
194
234
  if (waitForRunning) {
195
- await Promise.all([...this.tasks.values()]
196
- .filter(({ status }) => status === 'running')
197
- .map(({ completion }) => completion));
235
+ const hasPendingWatchdog = [...this.tasks.values()].some(({ pendingWatchdogMessage }) => pendingWatchdogMessage !== null);
236
+ const running = [...this.tasks.values()].filter(({ status }) => status === 'running');
237
+ const completions = Promise.all(running.map(({ completion }) => completion));
238
+ const wakes = running
239
+ .filter((task) => !task.watchdogFired)
240
+ .map(({ watchdogWake }) => watchdogWake);
241
+ if (!hasPendingWatchdog && wakes.length > 0)
242
+ await Promise.race([completions, ...wakes]);
243
+ else if (!hasPendingWatchdog)
244
+ await completions;
198
245
  }
199
246
  const messages = [];
200
247
  for (const task of this.tasks.values()) {
248
+ if (task.pendingWatchdogMessage) {
249
+ const pendingWatchdogMessage = task.pendingWatchdogMessage;
250
+ task.pendingWatchdogMessage = null;
251
+ if (!task.notified && task.status !== 'stopped')
252
+ messages.push(pendingWatchdogMessage);
253
+ }
201
254
  if (task.status === 'running' ||
202
255
  task.status === 'stopped' ||
203
256
  task.notified) {
@@ -219,6 +272,7 @@ export class BackgroundBashManager {
219
272
  signal: task.controller.signal,
220
273
  onOutput: async (output) => {
221
274
  task.output = output;
275
+ this.rearmWatchdog(task);
222
276
  await writeFile(task.outputFile, output, { mode: 0o600 });
223
277
  },
224
278
  });
@@ -244,12 +298,45 @@ export class BackgroundBashManager {
244
298
  });
245
299
  }
246
300
  finally {
301
+ this.clearWatchdog(task);
247
302
  this.emitNotification(task);
248
303
  if (task.parentSignal && task.parentAbort) {
249
304
  task.parentSignal.removeEventListener('abort', task.parentAbort);
250
305
  }
251
306
  }
252
307
  }
308
+ clearWatchdog(task) {
309
+ if (task.watchdogTimer !== null) {
310
+ clearTimeout(task.watchdogTimer);
311
+ task.watchdogTimer = null;
312
+ }
313
+ }
314
+ rearmWatchdog(task) {
315
+ this.clearWatchdog(task);
316
+ if (task.status !== 'running' ||
317
+ task.watchdogFired ||
318
+ !isPromptTail(task.output))
319
+ return;
320
+ task.watchdogTimer = setTimeout(() => {
321
+ task.watchdogTimer = null;
322
+ if (task.status !== 'running' ||
323
+ task.watchdogFired ||
324
+ !isPromptTail(task.output))
325
+ return;
326
+ task.watchdogFired = true;
327
+ const summary = `Background command "${task.description}" appears to be waiting for interactive input`;
328
+ task.pendingWatchdogMessage = watchdogNotification(task, summary);
329
+ this.options.eventSink?.({
330
+ type: 'task-input-waiting',
331
+ taskId: task.taskId,
332
+ toolUseId: task.toolUseId,
333
+ outputFile: task.outputFile,
334
+ summary,
335
+ });
336
+ task.watchdogWakeResolve?.();
337
+ task.watchdogWakeResolve = null;
338
+ }, this.stallWatchdogMs);
339
+ }
253
340
  emitNotification(task) {
254
341
  if (task.status === 'running')
255
342
  return;
@@ -330,6 +417,11 @@ export class BackgroundBashManager {
330
417
  completion: Promise.resolve(),
331
418
  startedAt: state.startedAt ?? Math.floor(metadata.mtimeMs),
332
419
  durationMs: state.durationMs ?? null,
420
+ watchdogTimer: null,
421
+ watchdogFired: false,
422
+ pendingWatchdogMessage: null,
423
+ watchdogWake: createWatchdogWake().promise,
424
+ watchdogWakeResolve: null,
333
425
  };
334
426
  this.tasks.set(taskId, task);
335
427
  return task;
@@ -6,6 +6,16 @@ export interface ManagedWorktree {
6
6
  cwd: string;
7
7
  cleanup(): Promise<ManagedWorktreeCleanup>;
8
8
  }
9
+ export interface OwnedManagedWorktreeOptions {
10
+ cwd: string;
11
+ stateRoot: string;
12
+ directoryName: string;
13
+ ownerId: string;
14
+ label: 'Agent' | 'Workflow' | 'Team';
15
+ kind: 'workflow' | 'agent' | 'team';
16
+ policy: 'ephemeral' | 'durable';
17
+ }
18
+ export declare function createOwnedManagedWorktree(options: OwnedManagedWorktreeOptions): Promise<ManagedWorktree>;
9
19
  export declare function createManagedWorktree(options: {
10
20
  cwd: string;
11
21
  parentDirectory: string;