praxis-agent 0.60.1 → 0.62.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.
Files changed (31) hide show
  1. package/README.md +21 -4
  2. package/dist/application/agent-worktree-owner.d.ts +12 -0
  3. package/dist/application/agent-worktree-owner.js +35 -0
  4. package/dist/application/agent-worktree.d.ts +19 -0
  5. package/dist/application/agent-worktree.js +96 -0
  6. package/dist/application/managed-worktree-hooks.d.ts +11 -0
  7. package/dist/application/managed-worktree-hooks.js +23 -0
  8. package/dist/application/managed-worktree.d.ts +88 -0
  9. package/dist/application/managed-worktree.js +1129 -106
  10. package/dist/application/subagent-service.d.ts +3 -1
  11. package/dist/application/subagent-service.js +117 -23
  12. package/dist/application/team-capability.d.ts +4 -0
  13. package/dist/application/team-capability.js +20 -0
  14. package/dist/application/team-manager.d.ts +2 -0
  15. package/dist/application/team-manager.js +207 -52
  16. package/dist/application/team-observability.d.ts +5 -0
  17. package/dist/application/team-observability.js +152 -32
  18. package/dist/application/team-workspace.d.ts +18 -1
  19. package/dist/application/team-workspace.js +217 -71
  20. package/dist/application/workflow-worktree.d.ts +3 -0
  21. package/dist/application/workflow-worktree.js +4 -0
  22. package/dist/cli/tui/doctor-dashboard.js +1 -0
  23. package/dist/cli-runtime.js +5 -0
  24. package/dist/hooks/claude-hooks.js +16 -2
  25. package/dist/maintenance/doctor.d.ts +1 -1
  26. package/dist/maintenance/doctor.js +55 -0
  27. package/dist/persistence/managed-worktree-store.d.ts +16 -1
  28. package/dist/persistence/managed-worktree-store.js +61 -21
  29. package/dist/platform/exclusive-file-lease.d.ts +6 -1
  30. package/dist/platform/exclusive-file-lease.js +48 -7
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -215,9 +215,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
215
215
  tool, memory, first-turn, and resume behavior. Agent execution uses one
216
216
  durable lifecycle vocabulary with bounded cancellation and drain,
217
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
220
- local Teams (`PRAXIS_ENABLE_TEAMS=true`) stay absent from ordinary startup by
218
+ Workflow and Agent turns use isolated repo-local worktrees with trusted
219
+ synchronous lifecycle hooks. Blocked creation rolls back safely; unchanged
220
+ worktrees are cleaned up, while failed, dirty, committed, or otherwise
221
+ unsafe worktrees retain evidence for inspection. Restore accepts only an
222
+ owned current or exact historical Agent path; invalid or unavailable
223
+ evidence safely falls back to the parent cwd. Managed worktrees expose
224
+ bounded five-state lifecycle diagnostics through `praxis doctor`, while
225
+ Team `status`, `logs`, and `attach` surface the same read-only lifecycle
226
+ evidence. Experimental local Teams (`PRAXIS_ENABLE_TEAMS=true`) stay absent
227
+ from ordinary startup by
221
228
  default and add durable task ownership plus one ordered mailbox with stable
222
229
  identities, fixed broadcast recipients, durable cursors, bounded retention,
223
230
  and bounded model-context projection. Teams are explicitly experimental and
@@ -233,7 +240,17 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
233
240
  CLI also exposes `praxis team status`, `logs`, and `attach` in human or JSON
234
241
  form; durable-local attach does not require tmux. Native task, notification,
235
242
  context, and Team resume/inbox seams are implemented with fail-closed
236
- validation for unsupported payloads.
243
+ validation for unsupported payloads. New write-capable Team generations use
244
+ repo-local durable worktrees under `.praxis/worktrees/team/<team-id>/<generation-hash>`
245
+ with exact ownership records, markers, branches, and execution tokens. Read-only
246
+ Team members continue to use the invocation checkout. Completion, failure,
247
+ cancellation, orphaning, stop, and persistence uncertainty retain writer
248
+ evidence and relinquish the active lease; only a durably persisted explicit
249
+ Lead `accepted` decision may release that exact generation, while rejection
250
+ retains it. The ownership-verified accepted path may explicitly dispose of
251
+ reviewed dirty or committed evidence; hook blocks and ownership uncertainty
252
+ fail closed. Exact historical global Team worktrees remain validation-only
253
+ compatibility and are never implicitly adopted.
237
254
  - **Native resource ecosystem** — shared Praxis instructions with recursive `@`
238
255
  imports, memory, skills, commands, agents, hooks, settings, MCP servers,
239
256
  plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`,
@@ -0,0 +1,12 @@
1
+ export interface AgentWorktreeOwnerIdentity {
2
+ readonly sessionId: string;
3
+ readonly agentId: string;
4
+ readonly executionToken: string;
5
+ }
6
+ export declare function formatAgentWorktreeOwner(identity: AgentWorktreeOwnerIdentity): string;
7
+ export declare function formatAgentWorktreeOwnerPrefix(identity: {
8
+ readonly sessionId: string;
9
+ readonly agentId: string;
10
+ }): string;
11
+ export declare function parseAgentWorktreeOwner(ownerId: string): AgentWorktreeOwnerIdentity | null;
12
+ //# sourceMappingURL=agent-worktree-owner.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { isSessionId } from '../core/session.js';
2
+ const AGENT_ID_PATTERN = /^a(?:[A-Za-z0-9][A-Za-z0-9_-]{0,62}-)?[0-9a-f]{16}$/u;
3
+ const EXECUTION_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
4
+ function isAgentWorktreeOwnerIdentity(value) {
5
+ return (isSessionId(value.sessionId) &&
6
+ AGENT_ID_PATTERN.test(value.agentId) &&
7
+ EXECUTION_TOKEN_PATTERN.test(value.executionToken));
8
+ }
9
+ function isAgentWorktreeIdentity(value) {
10
+ return isSessionId(value.sessionId) && AGENT_ID_PATTERN.test(value.agentId);
11
+ }
12
+ function assertAgentWorktreeOwnerIdentity(value) {
13
+ if (!isAgentWorktreeOwnerIdentity(value))
14
+ throw new Error('Invalid Agent worktree owner identity');
15
+ }
16
+ export function formatAgentWorktreeOwner(identity) {
17
+ assertAgentWorktreeOwnerIdentity(identity);
18
+ return `agent:${identity.sessionId}:${identity.agentId}:${identity.executionToken}`;
19
+ }
20
+ export function formatAgentWorktreeOwnerPrefix(identity) {
21
+ if (!isAgentWorktreeIdentity(identity))
22
+ throw new Error('Invalid Agent worktree owner identity');
23
+ return `agent:${identity.sessionId}:${identity.agentId}:`;
24
+ }
25
+ export function parseAgentWorktreeOwner(ownerId) {
26
+ const parts = ownerId.split(':');
27
+ if (parts.length !== 4 || parts[0] !== 'agent')
28
+ return null;
29
+ const sessionId = parts[1] ?? '';
30
+ const agentId = parts[2] ?? '';
31
+ const executionToken = parts[3] ?? '';
32
+ const identity = { sessionId, agentId, executionToken };
33
+ return isAgentWorktreeOwnerIdentity(identity) ? identity : null;
34
+ }
35
+ //# sourceMappingURL=agent-worktree-owner.js.map
@@ -0,0 +1,19 @@
1
+ import { type ManagedWorktree } from './managed-worktree.js';
2
+ import { type ManagedWorktreeHookContext } from './managed-worktree-hooks.js';
3
+ export declare function createAgentWorktree(options: {
4
+ cwd: string;
5
+ stateRoot: string;
6
+ sessionId: string;
7
+ agentId: string;
8
+ executionToken: string;
9
+ hookContext?: ManagedWorktreeHookContext;
10
+ }): Promise<ManagedWorktree>;
11
+ export declare function restoreAgentWorktree(options: {
12
+ cwd: string;
13
+ stateRoot: string;
14
+ sessionId: string;
15
+ agentId: string;
16
+ path: string;
17
+ hookContext?: ManagedWorktreeHookContext;
18
+ }): Promise<ManagedWorktree>;
19
+ //# sourceMappingURL=agent-worktree.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { createOwnedManagedWorktree, restoreManagedWorktree, restoreOwnedManagedWorktree, } from './managed-worktree.js';
6
+ import { createManagedWorktreeHooks, } from './managed-worktree-hooks.js';
7
+ import { resolveProjectIdentity } from '../platform/project-identity.js';
8
+ import { formatAgentWorktreeOwner, formatAgentWorktreeOwnerPrefix, } from './agent-worktree-owner.js';
9
+ const execFileAsync = promisify(execFile);
10
+ function agentWorktreeDirectoryName(sessionId, agentId) {
11
+ return `${sessionId}-${agentId}`;
12
+ }
13
+ export async function createAgentWorktree(options) {
14
+ return createOwnedManagedWorktree({
15
+ cwd: options.cwd,
16
+ stateRoot: options.stateRoot,
17
+ directoryName: agentWorktreeDirectoryName(options.sessionId, options.agentId),
18
+ ownerId: formatAgentWorktreeOwner({
19
+ sessionId: options.sessionId,
20
+ agentId: options.agentId,
21
+ executionToken: options.executionToken,
22
+ }),
23
+ label: 'Agent',
24
+ kind: 'agent',
25
+ policy: 'ephemeral',
26
+ ...(options.hookContext
27
+ ? { hooks: createManagedWorktreeHooks(options.hookContext) }
28
+ : {}),
29
+ });
30
+ }
31
+ async function git(cwd, args) {
32
+ return (await execFileAsync('git', ['-C', cwd, ...args], { encoding: 'utf8' })).stdout.trim();
33
+ }
34
+ async function assertLegacyPath(options) {
35
+ if (!isAbsolute(options.path) ||
36
+ options.path.includes('\0') ||
37
+ resolve(options.path) !== options.expectedPath) {
38
+ throw new Error('retained Agent worktree path is not the legacy path');
39
+ }
40
+ const entry = await lstat(options.path);
41
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
42
+ throw new Error('retained Agent worktree must be a real directory');
43
+ }
44
+ const canonicalPath = await realpath(options.path);
45
+ const repositoryRoot = await resolveProjectIdentity(options.cwd);
46
+ if (repositoryRoot !== (await resolveProjectIdentity(options.path))) {
47
+ throw new Error('retained Agent worktree repository identity does not match');
48
+ }
49
+ const registrations = await git(options.cwd, [
50
+ 'worktree',
51
+ 'list',
52
+ '--porcelain',
53
+ '-z',
54
+ ]);
55
+ const registered = registrations
56
+ .split('\0')
57
+ .filter((line) => line.startsWith('worktree '))
58
+ .map((line) => resolve(repositoryRoot, line.slice('worktree '.length)));
59
+ if (!registered.includes(options.path) &&
60
+ !registered.includes(canonicalPath)) {
61
+ throw new Error('retained Agent worktree is not registered');
62
+ }
63
+ }
64
+ export async function restoreAgentWorktree(options) {
65
+ if (!isAbsolute(options.path) || options.path.includes('\0')) {
66
+ throw new Error('retained Agent worktree path is invalid');
67
+ }
68
+ const managedPath = join(await resolveProjectIdentity(options.cwd), '.praxis', 'worktrees', 'agent', agentWorktreeDirectoryName(options.sessionId, options.agentId));
69
+ const path = resolve(options.path);
70
+ if (path === managedPath) {
71
+ return restoreOwnedManagedWorktree({
72
+ cwd: options.cwd,
73
+ stateRoot: options.stateRoot,
74
+ path,
75
+ directoryName: agentWorktreeDirectoryName(options.sessionId, options.agentId),
76
+ ownerPrefix: formatAgentWorktreeOwnerPrefix(options),
77
+ label: 'Agent',
78
+ kind: 'agent',
79
+ policy: 'ephemeral',
80
+ ...(options.hookContext
81
+ ? { hooks: createManagedWorktreeHooks(options.hookContext) }
82
+ : {}),
83
+ });
84
+ }
85
+ const legacyPath = join(resolve(options.stateRoot), 'agent-worktrees', agentWorktreeDirectoryName(options.sessionId, options.agentId));
86
+ if (path !== legacyPath) {
87
+ throw new Error('retained Agent worktree path is not an accepted path');
88
+ }
89
+ await assertLegacyPath({
90
+ cwd: options.cwd,
91
+ path,
92
+ expectedPath: legacyPath,
93
+ });
94
+ return restoreManagedWorktree({ cwd: options.cwd, path, label: 'Agent' });
95
+ }
96
+ //# sourceMappingURL=agent-worktree.js.map
@@ -0,0 +1,11 @@
1
+ import type { ClaudeHookRunner } from '../hooks/claude-hooks.js';
2
+ import type { ManagedWorktreeHooks } from './managed-worktree.js';
3
+ export interface ManagedWorktreeHookContext {
4
+ runner: ClaudeHookRunner;
5
+ sessionId: string;
6
+ transcriptPath: string;
7
+ permissionMode: string;
8
+ signal?: AbortSignal;
9
+ }
10
+ export declare function createManagedWorktreeHooks(context: ManagedWorktreeHookContext): ManagedWorktreeHooks;
11
+ //# sourceMappingURL=managed-worktree-hooks.d.ts.map
@@ -0,0 +1,23 @@
1
+ function lifecycleHookInput(input, context, event) {
2
+ const reason = 'reason' in input ? input.reason : undefined;
3
+ return {
4
+ session_id: context.sessionId,
5
+ transcript_path: context.transcriptPath,
6
+ cwd: input.worktreePath,
7
+ permission_mode: context.permissionMode,
8
+ hook_event_name: event,
9
+ worktree_path: input.worktreePath,
10
+ worktree_kind: input.worktreeKind,
11
+ worktree_id: input.worktreeId,
12
+ owner_id: input.ownerId,
13
+ base_commit: input.baseCommit,
14
+ ...(event === 'WorktreeRemove' && reason !== undefined ? { reason } : {}),
15
+ };
16
+ }
17
+ export function createManagedWorktreeHooks(context) {
18
+ return {
19
+ afterCreate: async (input) => context.runner.run(lifecycleHookInput(input, context, 'WorktreeCreate'), input.worktreeKind, context.signal),
20
+ beforeRemove: async (input) => context.runner.run(lifecycleHookInput(input, context, 'WorktreeRemove'), input.worktreeKind, context.signal),
21
+ };
22
+ }
23
+ //# sourceMappingURL=managed-worktree-hooks.js.map
@@ -1,3 +1,4 @@
1
+ import { type ManagedWorktreeRecord } from '../persistence/managed-worktree-store.js';
1
2
  export interface ManagedWorktreeCleanup {
2
3
  retained: boolean;
3
4
  reason?: string;
@@ -5,17 +6,104 @@ export interface ManagedWorktreeCleanup {
5
6
  export interface ManagedWorktree {
6
7
  cwd: string;
7
8
  cleanup(): Promise<ManagedWorktreeCleanup>;
9
+ retain(reason: string): Promise<ManagedWorktreeCleanup>;
10
+ release(): Promise<ManagedWorktreeCleanup>;
11
+ }
12
+ export interface ManagedWorktreeHookInput {
13
+ readonly worktreePath: string;
14
+ readonly worktreeKind: 'workflow' | 'agent' | 'team';
15
+ readonly worktreeId: string;
16
+ readonly ownerId: string;
17
+ readonly baseCommit: string;
18
+ }
19
+ export interface ManagedWorktreeRemoveHookInput extends ManagedWorktreeHookInput {
20
+ readonly reason: 'normal' | 'reconcile';
21
+ }
22
+ export interface ManagedWorktreeHookOutcome {
23
+ blockedReason?: string;
24
+ }
25
+ export interface ManagedWorktreeHooks {
26
+ afterCreate(input: ManagedWorktreeHookInput): Promise<ManagedWorktreeHookOutcome>;
27
+ beforeRemove(input: ManagedWorktreeRemoveHookInput): Promise<ManagedWorktreeHookOutcome>;
8
28
  }
9
29
  export interface OwnedManagedWorktreeOptions {
10
30
  cwd: string;
11
31
  stateRoot: string;
32
+ parentDirectoryName?: string;
12
33
  directoryName: string;
34
+ branch?: string;
13
35
  ownerId: string;
14
36
  label: 'Agent' | 'Workflow' | 'Team';
15
37
  kind: 'workflow' | 'agent' | 'team';
16
38
  policy: 'ephemeral' | 'durable';
39
+ hooks?: ManagedWorktreeHooks;
40
+ }
41
+ export type ManagedWorktreeReconciliationDisposition = 'released' | 'retained' | 'skipped' | 'invalid';
42
+ export interface ManagedWorktreeReconciliationEntry {
43
+ recordPath: string;
44
+ worktreeId?: string;
45
+ disposition: ManagedWorktreeReconciliationDisposition;
46
+ reason: string;
17
47
  }
48
+ export interface ManagedWorktreeReconciliationResult {
49
+ repositoryRoot: string;
50
+ inspected: number;
51
+ truncated: boolean;
52
+ entries: readonly ManagedWorktreeReconciliationEntry[];
53
+ }
54
+ export type ManagedWorktreeHealthStatus = 'active' | 'retained' | 'safely-releasable' | 'released' | 'unsafe';
55
+ export interface ManagedWorktreeHealthEntry {
56
+ recordPath: string;
57
+ worktreeId: string | null;
58
+ kind: ManagedWorktreeRecord['kind'] | null;
59
+ policy: ManagedWorktreeRecord['policy'] | null;
60
+ recordState: ManagedWorktreeRecord['state'] | null;
61
+ worktreePath: string | null;
62
+ branch: string | null;
63
+ present: boolean | null;
64
+ status: ManagedWorktreeHealthStatus;
65
+ reason: string;
66
+ }
67
+ export interface ManagedWorktreeHealthReport {
68
+ repositoryRoot: string;
69
+ inspected: number;
70
+ truncated: boolean;
71
+ counts: {
72
+ active: number;
73
+ retained: number;
74
+ safelyReleasable: number;
75
+ released: number;
76
+ unsafe: number;
77
+ };
78
+ entries: readonly ManagedWorktreeHealthEntry[];
79
+ }
80
+ export declare function inspectManagedWorktreeHealth(options: {
81
+ cwd: string;
82
+ stateRoot: string;
83
+ limit?: number;
84
+ }): Promise<ManagedWorktreeHealthReport>;
85
+ export declare function reconcileManagedWorktrees(options: {
86
+ cwd: string;
87
+ stateRoot: string;
88
+ hooks?: ManagedWorktreeHooks;
89
+ }): Promise<ManagedWorktreeReconciliationResult>;
18
90
  export declare function createOwnedManagedWorktree(options: OwnedManagedWorktreeOptions): Promise<ManagedWorktree>;
91
+ export interface OwnedManagedWorktreeRestoreOptions {
92
+ cwd: string;
93
+ stateRoot: string;
94
+ path: string;
95
+ parentDirectoryName?: string;
96
+ directoryName: string;
97
+ ownerPrefix: string;
98
+ ownerId?: string;
99
+ branch?: string;
100
+ label: 'Agent' | 'Workflow' | 'Team';
101
+ kind: 'workflow' | 'agent' | 'team';
102
+ policy: 'ephemeral' | 'durable';
103
+ hooks?: ManagedWorktreeHooks;
104
+ }
105
+ /** Restore a checkout only when its complete managed ownership proof matches. */
106
+ export declare function restoreOwnedManagedWorktree(options: OwnedManagedWorktreeRestoreOptions): Promise<ManagedWorktree>;
19
107
  export declare function createManagedWorktree(options: {
20
108
  cwd: string;
21
109
  parentDirectory: string;