praxis-agent 0.61.0 → 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.
- package/README.md +21 -6
- package/dist/application/agent-worktree-owner.d.ts +12 -0
- package/dist/application/agent-worktree-owner.js +35 -0
- package/dist/application/agent-worktree.d.ts +19 -0
- package/dist/application/agent-worktree.js +96 -0
- package/dist/application/managed-worktree-hooks.d.ts +11 -0
- package/dist/application/managed-worktree-hooks.js +23 -0
- package/dist/application/managed-worktree.d.ts +70 -0
- package/dist/application/managed-worktree.js +1081 -108
- package/dist/application/subagent-service.d.ts +3 -1
- package/dist/application/subagent-service.js +106 -23
- package/dist/application/team-capability.d.ts +4 -0
- package/dist/application/team-capability.js +20 -0
- package/dist/application/team-manager.d.ts +2 -0
- package/dist/application/team-manager.js +207 -52
- package/dist/application/team-observability.d.ts +5 -0
- package/dist/application/team-observability.js +152 -32
- package/dist/application/team-workspace.d.ts +18 -1
- package/dist/application/team-workspace.js +217 -71
- package/dist/application/workflow-worktree.d.ts +2 -8
- package/dist/application/workflow-worktree.js +2 -23
- package/dist/cli/tui/doctor-dashboard.js +1 -0
- package/dist/cli-runtime.js +5 -0
- package/dist/maintenance/doctor.d.ts +1 -1
- package/dist/maintenance/doctor.js +55 -0
- package/dist/persistence/managed-worktree-store.d.ts +16 -1
- package/dist/persistence/managed-worktree-store.js +61 -21
- package/dist/platform/exclusive-file-lease.d.ts +6 -1
- package/dist/platform/exclusive-file-lease.js +48 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,11 +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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for inspection.
|
|
222
|
-
|
|
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
|
|
223
228
|
default and add durable task ownership plus one ordered mailbox with stable
|
|
224
229
|
identities, fixed broadcast recipients, durable cursors, bounded retention,
|
|
225
230
|
and bounded model-context projection. Teams are explicitly experimental and
|
|
@@ -235,7 +240,17 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
235
240
|
CLI also exposes `praxis team status`, `logs`, and `attach` in human or JSON
|
|
236
241
|
form; durable-local attach does not require tmux. Native task, notification,
|
|
237
242
|
context, and Team resume/inbox seams are implemented with fail-closed
|
|
238
|
-
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.
|
|
239
254
|
- **Native resource ecosystem** — shared Praxis instructions with recursive `@`
|
|
240
255
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
241
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,6 +6,8 @@ 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>;
|
|
8
11
|
}
|
|
9
12
|
export interface ManagedWorktreeHookInput {
|
|
10
13
|
readonly worktreePath: string;
|
|
@@ -26,14 +29,81 @@ export interface ManagedWorktreeHooks {
|
|
|
26
29
|
export interface OwnedManagedWorktreeOptions {
|
|
27
30
|
cwd: string;
|
|
28
31
|
stateRoot: string;
|
|
32
|
+
parentDirectoryName?: string;
|
|
29
33
|
directoryName: string;
|
|
34
|
+
branch?: string;
|
|
30
35
|
ownerId: string;
|
|
31
36
|
label: 'Agent' | 'Workflow' | 'Team';
|
|
32
37
|
kind: 'workflow' | 'agent' | 'team';
|
|
33
38
|
policy: 'ephemeral' | 'durable';
|
|
34
39
|
hooks?: ManagedWorktreeHooks;
|
|
35
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;
|
|
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>;
|
|
36
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>;
|
|
37
107
|
export declare function createManagedWorktree(options: {
|
|
38
108
|
cwd: string;
|
|
39
109
|
parentDirectory: string;
|