praxis-agent 0.37.0 → 0.38.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 +28 -4
- package/dist/application/session-service.d.ts +3 -1
- package/dist/application/session-service.js +2 -2
- package/dist/application/team-agent-runtime.d.ts +3 -0
- package/dist/application/team-agent-runtime.js +20 -3
- package/dist/application/team-lead-decision-surface.d.ts +22 -0
- package/dist/application/team-lead-decision-surface.js +16 -0
- package/dist/application/team-observability.d.ts +113 -0
- package/dist/application/team-observability.js +312 -0
- package/dist/cli/interactive.js +444 -189
- package/dist/cli/tui/btw-surface-model.d.ts +22 -0
- package/dist/cli/tui/btw-surface-model.js +10 -0
- package/dist/cli/tui/claude-style.d.ts +50 -86
- package/dist/cli/tui/claude-style.js +93 -74
- package/dist/cli/tui/command-palette-model.d.ts +33 -0
- package/dist/cli/tui/command-palette-model.js +44 -0
- package/dist/cli/tui/config-dashboard.d.ts +3 -9
- package/dist/cli/tui/config-dashboard.js +2 -1
- package/dist/cli/tui/config-surface-model.d.ts +24 -0
- package/dist/cli/tui/config-surface-model.js +16 -0
- package/dist/cli/tui/diff-surface-model.d.ts +83 -0
- package/dist/cli/tui/diff-surface-model.js +142 -0
- package/dist/cli/tui/doctor-dashboard.d.ts +3 -4
- package/dist/cli/tui/doctor-dashboard.js +2 -1
- package/dist/cli/tui/doctor-surface-model.d.ts +13 -0
- package/dist/cli/tui/doctor-surface-model.js +9 -0
- package/dist/cli/tui/hooks-surface-model.d.ts +18 -0
- package/dist/cli/tui/hooks-surface-model.js +11 -0
- package/dist/cli/tui/leaf-surface-model.d.ts +68 -0
- package/dist/cli/tui/leaf-surface-model.js +55 -0
- package/dist/cli/tui/list-surface-model.d.ts +27 -0
- package/dist/cli/tui/list-surface-model.js +22 -0
- package/dist/cli/tui/mcp-elicitation-surface-model.d.ts +27 -0
- package/dist/cli/tui/mcp-elicitation-surface-model.js +25 -0
- package/dist/cli/tui/mcp-elicitation.d.ts +7 -1
- package/dist/cli/tui/mcp-elicitation.js +6 -2
- package/dist/cli/tui/mcp-panel.d.ts +3 -4
- package/dist/cli/tui/mcp-panel.js +2 -1
- package/dist/cli/tui/mcp-surface-model.d.ts +11 -0
- package/dist/cli/tui/mcp-surface-model.js +4 -0
- package/dist/cli/tui/memory-surface-model.d.ts +20 -0
- package/dist/cli/tui/memory-surface-model.js +12 -0
- package/dist/cli/tui/mention-picker-model.d.ts +41 -0
- package/dist/cli/tui/mention-picker-model.js +53 -0
- package/dist/cli/tui/model-effort-surface-model.d.ts +29 -0
- package/dist/cli/tui/model-effort-surface-model.js +38 -0
- package/dist/cli/tui/rewind-surface-model.d.ts +53 -0
- package/dist/cli/tui/rewind-surface-model.js +54 -0
- package/dist/cli/tui/sandbox-dashboard.d.ts +3 -4
- package/dist/cli/tui/sandbox-dashboard.js +2 -1
- package/dist/cli/tui/sandbox-surface-model.d.ts +13 -0
- package/dist/cli/tui/sandbox-surface-model.js +4 -0
- package/dist/cli/tui/session-picker-model.d.ts +41 -0
- package/dist/cli/tui/session-picker-model.js +69 -0
- package/dist/cli/tui/task-panel.d.ts +3 -3
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli/tui/task-surface-model.d.ts +11 -0
- package/dist/cli/tui/task-surface-model.js +4 -0
- package/dist/cli/tui/theme-surface-model.d.ts +85 -0
- package/dist/cli/tui/theme-surface-model.js +52 -0
- package/dist/cli-runtime.js +165 -16
- package/dist/compatibility/claude/team.d.ts +77 -0
- package/dist/compatibility/claude/team.js +219 -0
- package/dist/core/runtime.d.ts +2 -0
- package/dist/hooks/claude-file-change-watcher.d.ts +3 -0
- package/dist/hooks/claude-file-change-watcher.js +70 -8
- package/dist/hooks/claude-hook-tools.js +6 -1
- package/dist/persistence/native-transcript-migration.d.ts +37 -0
- package/dist/persistence/native-transcript-migration.js +430 -0
- package/dist/tools/local-tools.js +10 -1
- package/dist/tools/team-lead-tools.d.ts +16 -2
- package/dist/tools/team-lead-tools.js +124 -3
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ Praxis does not use Claude subscription authentication. Claude Code
|
|
|
38
38
|
interoperability covers local sessions, configuration, permissions, memory,
|
|
39
39
|
skills, hooks, agents, plugins, and MCP data.
|
|
40
40
|
|
|
41
|
+
Claude Team compatibility is explicit and fail-closed: supported delete/send
|
|
42
|
+
wire shapes route through native Team operations, while lossy or unsupported
|
|
43
|
+
Team surfaces are rejected rather than silently simplified.
|
|
44
|
+
|
|
41
45
|
## Install
|
|
42
46
|
|
|
43
47
|
```sh
|
|
@@ -123,7 +127,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
123
127
|
`Ctrl+V` text/image clipboard paste, `Ctrl+Z` shell suspension and `fg`
|
|
124
128
|
recovery, permission-gated `!` shell turns, navigable current/per-turn Git
|
|
125
129
|
diff views, semantic plan/question decision panels with complete
|
|
126
|
-
screen-reader actions,
|
|
130
|
+
screen-reader actions, semantic screen projection across selectable surfaces,
|
|
131
|
+
deterministic resize-aware URL/form elicitation rendering, and measured
|
|
132
|
+
context budgets; print mode,
|
|
127
133
|
structured JSON/JSONL, context compaction, tool loops, and bounded execution.
|
|
128
134
|
- **Built-in tools** — read, write, edit, glob, search, shell, notebook, PDF,
|
|
129
135
|
image, web, scheduled prompts, workflows, and worktrees.
|
|
@@ -166,7 +172,15 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
166
172
|
and shutdown-drain budgets.
|
|
167
173
|
Child permissions can only tighten the parent; concurrent asks form one FIFO
|
|
168
174
|
Lead Decision queue with provenance. Coordinator leads are restricted to
|
|
169
|
-
orchestration, and custom Team agents receive no MCP capability.
|
|
175
|
+
orchestration, and custom Team agents receive no MCP capability. The native
|
|
176
|
+
CLI also exposes `praxis team status`, `logs`, and `attach` in human or JSON
|
|
177
|
+
form; durable-local attach does not require tmux. The Claude Team adapter is
|
|
178
|
+
removable and currently limited to fixture-verified delete/send; Claude create
|
|
179
|
+
is decoded but rejected when the payload cannot represent native roster/task
|
|
180
|
+
claims,
|
|
181
|
+
shutdown, and plan-response shapes; native task, notification, context, and
|
|
182
|
+
Team resume/inbox seams are implemented, while their Claude-facing zero-skip
|
|
183
|
+
qualification remains explicitly separate.
|
|
170
184
|
- **Claude-compatible ecosystem** — shared instructions with recursive `@`
|
|
171
185
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
172
186
|
plugins, and transcript data.
|
|
@@ -254,9 +268,19 @@ npm ci
|
|
|
254
268
|
npm run check
|
|
255
269
|
```
|
|
256
270
|
|
|
257
|
-
`npm run build:native` compiles the
|
|
258
|
-
|
|
271
|
+
`npm run build:native` compiles the implemented Praxis-owned core, provider,
|
|
272
|
+
native transcript, and session profile without the Claude compatibility adapter.
|
|
273
|
+
`npm run test:native:deletion` adds an emitted-output deletion gate. These are
|
|
274
|
+
implemented profile checks, not qualification of the full native package.
|
|
275
|
+
Native transcript migration remains explicit and recoverable. The built-CLI
|
|
276
|
+
smoke gate `npm run verify:native-migration-cli` exercises all-session dry-run,
|
|
277
|
+
publication, text/JSON idempotence, rollback, and Claude-data-plane exclusion.
|
|
278
|
+
The isolated active-stream regression proof is available through
|
|
279
|
+
`npm run test:performance:active-stream`; the product p95 budget remains 50 ms.
|
|
259
280
|
`npm run check` also enforces the corresponding source dependency direction.
|
|
281
|
+
`npm run test:core-completion` runs the 56-story #402 audit and reports
|
|
282
|
+
implemented, qualified, blocked, deferred, and out-of-scope states separately;
|
|
283
|
+
it never treats missing live prerequisites as a pass.
|
|
260
284
|
|
|
261
285
|
Contributions use Conventional Commit pull-request titles and the protected
|
|
262
286
|
squash-merge workflow. Read
|
|
@@ -28,7 +28,8 @@ import type { ClaudeInteractiveToolManager } from '../tools/claude-interactive-t
|
|
|
28
28
|
import type { ClaudePermissionMode } from '../permissions/claude-permission-resolver.js';
|
|
29
29
|
import type { ClaudeMcpRuntime, ClaudeMcpServerStatus, ClaudeMcpToolInspection } from '../mcp/claude-mcp-tools.js';
|
|
30
30
|
import type { TeamLeadOperations } from './team-lead-operations.js';
|
|
31
|
-
type
|
|
31
|
+
import type { ClaudeTeamCompatibilityPort } from '../tools/team-lead-tools.js';
|
|
32
|
+
type TeamLeadToolRegistryFactory = (base: ToolRegistry, operations: TeamLeadOperations, sessionId: string, enabledTools: readonly string[], compatibilityPort?: ClaudeTeamCompatibilityPort) => ToolRegistry;
|
|
32
33
|
export interface ClaudeSessionServiceOptions {
|
|
33
34
|
configRoot: string;
|
|
34
35
|
/** `claude` preserves the legacy shared layout; the CLI selects `native` by default. */
|
|
@@ -113,6 +114,7 @@ export interface ClaudeSessionServiceOptions {
|
|
|
113
114
|
teamLeadOperations?: TeamLeadOperations;
|
|
114
115
|
/** Lazily supplied Team tool wrapper, so disabled runs do not load Team code. */
|
|
115
116
|
teamLeadToolRegistryFactory?: TeamLeadToolRegistryFactory;
|
|
117
|
+
teamLeadCompatibilityPort?: ClaudeTeamCompatibilityPort;
|
|
116
118
|
}
|
|
117
119
|
export interface SessionRunResult {
|
|
118
120
|
sessionId: string;
|
|
@@ -5545,7 +5545,7 @@ export class ClaudeSessionService {
|
|
|
5545
5545
|
return;
|
|
5546
5546
|
if (snapshot.entries.length === 0)
|
|
5547
5547
|
return;
|
|
5548
|
-
const entries = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], snapshot.tailEntries, sessionId);
|
|
5548
|
+
const entries = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], snapshot.tailEntries, sessionId).filter((entry) => entry.type !== 'worktree-state');
|
|
5549
5549
|
if (entries.length === 0)
|
|
5550
5550
|
return;
|
|
5551
5551
|
const appended = await lease.appendMetadataSnapshot(snapshot.tail, entries);
|
|
@@ -5919,7 +5919,7 @@ export class ClaudeSessionService {
|
|
|
5919
5919
|
if (!factory) {
|
|
5920
5920
|
throw new Error('Team lead operations require a Team lead tool registry factory');
|
|
5921
5921
|
}
|
|
5922
|
-
return factory(base, operations, sessionId, enabledTools);
|
|
5922
|
+
return factory(base, operations, sessionId, enabledTools, this.options.teamLeadCompatibilityPort);
|
|
5923
5923
|
}
|
|
5924
5924
|
async append(lease, tail, entry) {
|
|
5925
5925
|
const result = await lease.append(tail, entry);
|
|
@@ -4,6 +4,7 @@ import type { ClaudeExtensionCatalog } from '../extensions/claude-extensions.js'
|
|
|
4
4
|
import type { ClaudeHookRunner } from '../hooks/claude-hooks.js';
|
|
5
5
|
import { type AgentPermissionMode } from './subagent-service.js';
|
|
6
6
|
import type { TeamAgentRuntime } from './team-manager.js';
|
|
7
|
+
import { type TeamLeadDecisionSurface } from './team-lead-decision-surface.js';
|
|
7
8
|
export interface ClaudeTeamAgentRuntimeOptions {
|
|
8
9
|
readonly nativeRoot: string;
|
|
9
10
|
readonly configRoot: string;
|
|
@@ -16,9 +17,11 @@ export interface ClaudeTeamAgentRuntimeOptions {
|
|
|
16
17
|
readonly permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
|
|
17
18
|
readonly eventSink?: RuntimeEventSink;
|
|
18
19
|
readonly approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
20
|
+
readonly decisionSurface?: TeamLeadDecisionSurface;
|
|
19
21
|
}
|
|
20
22
|
export declare class ClaudeTeamAgentRuntime implements TeamAgentRuntime {
|
|
21
23
|
private readonly options;
|
|
24
|
+
private readonly decisionSurface;
|
|
22
25
|
constructor(options: ClaudeTeamAgentRuntimeOptions);
|
|
23
26
|
run(input: Parameters<TeamAgentRuntime['run']>[0]): Promise<{
|
|
24
27
|
status: 'completed' | 'failed' | 'orphaned';
|
|
@@ -5,6 +5,7 @@ import { resolveProjectIdentity } from '../platform/project-identity.js';
|
|
|
5
5
|
import { sanitizeProjectPath } from '../platform/project-path-key.js';
|
|
6
6
|
import { ClaudeSubagentExecutor, } from './subagent-service.js';
|
|
7
7
|
import { teamMailboxMessageId } from './team-mailbox.js';
|
|
8
|
+
import { SerializedTeamLeadDecisionSurface, } from './team-lead-decision-surface.js';
|
|
8
9
|
function digest(value) {
|
|
9
10
|
return createHash('sha256').update(value).digest('hex');
|
|
10
11
|
}
|
|
@@ -41,8 +42,14 @@ function taskPrompt(input) {
|
|
|
41
42
|
}
|
|
42
43
|
export class ClaudeTeamAgentRuntime {
|
|
43
44
|
options;
|
|
45
|
+
decisionSurface;
|
|
44
46
|
constructor(options) {
|
|
45
47
|
this.options = options;
|
|
48
|
+
this.decisionSurface =
|
|
49
|
+
options.decisionSurface ??
|
|
50
|
+
(options.approveTool
|
|
51
|
+
? new SerializedTeamLeadDecisionSurface(options.approveTool)
|
|
52
|
+
: undefined);
|
|
46
53
|
}
|
|
47
54
|
async run(input) {
|
|
48
55
|
if (this.options.provider.capabilities.usage !== true)
|
|
@@ -51,7 +58,7 @@ export class ClaudeTeamAgentRuntime {
|
|
|
51
58
|
const identity = executionIdentity(projectIdentity, input.teamId, input.task.id, input.generation);
|
|
52
59
|
const transcriptDirectory = join(this.options.nativeRoot, 'state', 'team-executions', sanitizeProjectPath(projectIdentity), `team-${digest(input.teamId).slice(0, 16)}`, identity.taskDirectory);
|
|
53
60
|
await mkdir(transcriptDirectory, { recursive: true });
|
|
54
|
-
const
|
|
61
|
+
const decisionSurface = this.decisionSurface;
|
|
55
62
|
const executor = new ClaudeSubagentExecutor({
|
|
56
63
|
configRoot: this.options.configRoot,
|
|
57
64
|
dataPlane: 'native',
|
|
@@ -86,7 +93,7 @@ export class ClaudeTeamAgentRuntime {
|
|
|
86
93
|
});
|
|
87
94
|
this.options.eventSink?.(event);
|
|
88
95
|
},
|
|
89
|
-
...(
|
|
96
|
+
...(decisionSurface
|
|
90
97
|
? {
|
|
91
98
|
approveTool: (call, originalCall, decision) => {
|
|
92
99
|
const wrapped = decision?.behavior === 'ask'
|
|
@@ -102,7 +109,17 @@ export class ClaudeTeamAgentRuntime {
|
|
|
102
109
|
},
|
|
103
110
|
}
|
|
104
111
|
: decision;
|
|
105
|
-
|
|
112
|
+
if (wrapped?.behavior !== 'ask')
|
|
113
|
+
return { behavior: 'allow' };
|
|
114
|
+
return decisionSurface.request({
|
|
115
|
+
call,
|
|
116
|
+
...(originalCall ? { originalCall } : {}),
|
|
117
|
+
decision: wrapped,
|
|
118
|
+
teamId: input.teamId,
|
|
119
|
+
member: input.member.name,
|
|
120
|
+
taskId: input.task.id,
|
|
121
|
+
generation: input.generation,
|
|
122
|
+
});
|
|
106
123
|
},
|
|
107
124
|
}
|
|
108
125
|
: {}),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModelToolCall, PermissionApproval, PermissionDecision } from '../core/runtime.js';
|
|
2
|
+
export interface TeamLeadDecisionRequest {
|
|
3
|
+
readonly call: ModelToolCall;
|
|
4
|
+
readonly originalCall?: ModelToolCall;
|
|
5
|
+
readonly decision: PermissionDecision;
|
|
6
|
+
readonly teamId: string;
|
|
7
|
+
readonly member: string;
|
|
8
|
+
readonly taskId: string;
|
|
9
|
+
readonly generation: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TeamLeadDecisionSurface {
|
|
12
|
+
request(input: TeamLeadDecisionRequest): PermissionApproval | Promise<PermissionApproval>;
|
|
13
|
+
}
|
|
14
|
+
export type TeamLeadApproveTool = (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
15
|
+
/** Serializes Team child permission prompts while allowing later prompts after failure. */
|
|
16
|
+
export declare class SerializedTeamLeadDecisionSurface implements TeamLeadDecisionSurface {
|
|
17
|
+
private readonly approve;
|
|
18
|
+
private tail;
|
|
19
|
+
constructor(approve: TeamLeadApproveTool);
|
|
20
|
+
request(input: TeamLeadDecisionRequest): Promise<PermissionApproval>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=team-lead-decision-surface.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Serializes Team child permission prompts while allowing later prompts after failure. */
|
|
2
|
+
export class SerializedTeamLeadDecisionSurface {
|
|
3
|
+
approve;
|
|
4
|
+
tail = Promise.resolve();
|
|
5
|
+
constructor(approve) {
|
|
6
|
+
this.approve = approve;
|
|
7
|
+
}
|
|
8
|
+
request(input) {
|
|
9
|
+
const run = this.tail
|
|
10
|
+
.catch(() => undefined)
|
|
11
|
+
.then(() => this.approve(input.call, input.originalCall, input.decision));
|
|
12
|
+
this.tail = run.then(() => undefined, () => undefined);
|
|
13
|
+
return run;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=team-lead-decision-surface.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type TeamMailboxMessage } from '../core/team-mailbox.js';
|
|
2
|
+
import type { TeamSnapshot, TeamTask } from '../core/team-ownership.js';
|
|
3
|
+
export interface TeamDashboard {
|
|
4
|
+
readonly team: {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
projectIdentity: string;
|
|
8
|
+
leadPolicy: string;
|
|
9
|
+
executionPolicy: string;
|
|
10
|
+
commitPolicy: string;
|
|
11
|
+
lifecycle: string;
|
|
12
|
+
revision: number;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
};
|
|
16
|
+
readonly agents: readonly {
|
|
17
|
+
name: string;
|
|
18
|
+
agentType: string;
|
|
19
|
+
access: string;
|
|
20
|
+
lifecycle: string;
|
|
21
|
+
taskId: string | null;
|
|
22
|
+
generation: number;
|
|
23
|
+
usage: {
|
|
24
|
+
totalTokens: number;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
};
|
|
27
|
+
}[];
|
|
28
|
+
readonly tasks: readonly {
|
|
29
|
+
id: string;
|
|
30
|
+
description: string;
|
|
31
|
+
assignee: string;
|
|
32
|
+
blockedBy: readonly string[];
|
|
33
|
+
state: string;
|
|
34
|
+
claims: TeamTask['claims'];
|
|
35
|
+
generation: number;
|
|
36
|
+
usage: {
|
|
37
|
+
totalTokens: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
};
|
|
40
|
+
acceptance: 'pending' | 'accepted' | 'rejected' | 'not-applicable';
|
|
41
|
+
}[];
|
|
42
|
+
readonly worktrees: readonly {
|
|
43
|
+
taskId: string;
|
|
44
|
+
path: string | null;
|
|
45
|
+
branch: string | null;
|
|
46
|
+
access: string;
|
|
47
|
+
present: boolean;
|
|
48
|
+
}[];
|
|
49
|
+
readonly mailbox: {
|
|
50
|
+
totalRecords: number;
|
|
51
|
+
retainedRecords: number;
|
|
52
|
+
pendingByRecipient: Readonly<Record<string, number>>;
|
|
53
|
+
highestSequence: number;
|
|
54
|
+
prunedThrough: number;
|
|
55
|
+
};
|
|
56
|
+
readonly budgets: {
|
|
57
|
+
configured: {
|
|
58
|
+
maxTokens: number;
|
|
59
|
+
maxDurationMs: number;
|
|
60
|
+
};
|
|
61
|
+
used: {
|
|
62
|
+
totalTokens: number;
|
|
63
|
+
durationMs: number;
|
|
64
|
+
};
|
|
65
|
+
exhaustedReason: string | null;
|
|
66
|
+
};
|
|
67
|
+
readonly health: {
|
|
68
|
+
status: 'healthy' | 'idle' | 'blocked' | 'degraded' | 'stopping';
|
|
69
|
+
blockers: readonly string[];
|
|
70
|
+
activeCount: number;
|
|
71
|
+
};
|
|
72
|
+
readonly events: readonly {
|
|
73
|
+
kind: string;
|
|
74
|
+
at: string;
|
|
75
|
+
summary: string;
|
|
76
|
+
detail?: string;
|
|
77
|
+
}[];
|
|
78
|
+
}
|
|
79
|
+
export interface TeamObservabilityOptions {
|
|
80
|
+
readonly mailbox?: TeamMailboxAudit;
|
|
81
|
+
readonly worktrees?: readonly TeamWorktreeEvidence[];
|
|
82
|
+
readonly maxEvents?: number;
|
|
83
|
+
}
|
|
84
|
+
export interface TeamMailboxAudit {
|
|
85
|
+
readonly totalRecords: number;
|
|
86
|
+
readonly retainedRecords: number;
|
|
87
|
+
readonly pendingByRecipient: Readonly<Record<string, number>>;
|
|
88
|
+
readonly highestSequence: number;
|
|
89
|
+
readonly prunedThrough: number;
|
|
90
|
+
readonly records: readonly TeamMailboxMessage[];
|
|
91
|
+
}
|
|
92
|
+
export interface TeamWorktreeEvidence {
|
|
93
|
+
readonly taskId: string;
|
|
94
|
+
readonly path: string;
|
|
95
|
+
readonly branch: string | null;
|
|
96
|
+
readonly access: string;
|
|
97
|
+
readonly present: boolean;
|
|
98
|
+
}
|
|
99
|
+
export declare function projectTeamDashboard(snapshot: TeamSnapshot, options?: TeamObservabilityOptions): TeamDashboard;
|
|
100
|
+
export declare function renderTeamSummary(dashboard: TeamDashboard): string;
|
|
101
|
+
export declare function renderTeamAudit(dashboard: TeamDashboard): string;
|
|
102
|
+
export declare function readTeamMailboxAudit(input: {
|
|
103
|
+
nativeRoot: string;
|
|
104
|
+
snapshot: TeamSnapshot;
|
|
105
|
+
maxRecords?: number;
|
|
106
|
+
maxBytes?: number;
|
|
107
|
+
}): Promise<TeamMailboxAudit>;
|
|
108
|
+
export declare function readTeamWorktreeEvidence(input: {
|
|
109
|
+
nativeRoot: string;
|
|
110
|
+
snapshot: TeamSnapshot;
|
|
111
|
+
maxEntries?: number;
|
|
112
|
+
}): Promise<readonly TeamWorktreeEvidence[]>;
|
|
113
|
+
//# sourceMappingURL=team-observability.d.ts.map
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { readFile, lstat } from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { parseTeamMailboxMessage, } from '../core/team-mailbox.js';
|
|
7
|
+
import { sanitizeProjectPath } from '../platform/project-path-key.js';
|
|
8
|
+
function freeze(value) {
|
|
9
|
+
return Object.freeze(value);
|
|
10
|
+
}
|
|
11
|
+
function taskState(task) {
|
|
12
|
+
return (task.execution?.state ?? (task.blockedBy.length ? 'blocked' : 'pending'));
|
|
13
|
+
}
|
|
14
|
+
export function projectTeamDashboard(snapshot, options = {}) {
|
|
15
|
+
const tasks = snapshot.tasks.map((task) => ({
|
|
16
|
+
id: task.id,
|
|
17
|
+
description: task.description,
|
|
18
|
+
assignee: task.assignee,
|
|
19
|
+
blockedBy: freeze([...task.blockedBy]),
|
|
20
|
+
claims: task.claims,
|
|
21
|
+
state: taskState(task),
|
|
22
|
+
generation: task.usage.generation,
|
|
23
|
+
usage: {
|
|
24
|
+
totalTokens: task.usage.totalTokens,
|
|
25
|
+
durationMs: task.usage.durationMs,
|
|
26
|
+
},
|
|
27
|
+
acceptance: task.execution?.acceptance ?? 'not-applicable',
|
|
28
|
+
}));
|
|
29
|
+
const taskByAgent = new Map(tasks.map((task) => [task.assignee, task]));
|
|
30
|
+
const agents = snapshot.roster.map((member) => {
|
|
31
|
+
const task = taskByAgent.get(member.name);
|
|
32
|
+
return {
|
|
33
|
+
name: member.name,
|
|
34
|
+
agentType: member.agentType,
|
|
35
|
+
access: member.access,
|
|
36
|
+
lifecycle: task?.state ?? 'idle',
|
|
37
|
+
taskId: task?.id ?? null,
|
|
38
|
+
generation: task?.generation ?? 0,
|
|
39
|
+
usage: task?.usage ?? { totalTokens: 0, durationMs: 0 },
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
const active = tasks.filter((task) => ['queued', 'running', 'waiting', 'cancelling'].includes(task.state)).length;
|
|
43
|
+
const blockers = tasks
|
|
44
|
+
.filter((task) => task.state === 'blocked' || task.state === 'failed')
|
|
45
|
+
.map((task) => `${task.id}: ${task.state}`);
|
|
46
|
+
if (snapshot.usage.exhausted)
|
|
47
|
+
blockers.push(`budget exhausted: ${snapshot.usage.exhausted.reason}`);
|
|
48
|
+
const lifecycle = snapshot.usage.exhausted
|
|
49
|
+
? 'stopping'
|
|
50
|
+
: active
|
|
51
|
+
? 'active'
|
|
52
|
+
: 'idle';
|
|
53
|
+
const status = snapshot.usage.exhausted
|
|
54
|
+
? 'stopping'
|
|
55
|
+
: blockers.length
|
|
56
|
+
? 'blocked'
|
|
57
|
+
: active
|
|
58
|
+
? 'healthy'
|
|
59
|
+
: 'idle';
|
|
60
|
+
const mailboxEvents = (options.mailbox?.records ?? []).map((record) => ({
|
|
61
|
+
kind: 'mailbox',
|
|
62
|
+
at: record.createdAt,
|
|
63
|
+
summary: `${record.sender} -> ${record.recipients.join(',')} ${record.payload.kind}${'phase' in record.payload ? `/${record.payload.phase}` : ''}`,
|
|
64
|
+
}));
|
|
65
|
+
const events = (options.maxEvents === undefined ? 32 : options.maxEvents) <= 0
|
|
66
|
+
? []
|
|
67
|
+
: [
|
|
68
|
+
{
|
|
69
|
+
kind: 'team',
|
|
70
|
+
at: snapshot.updatedAt,
|
|
71
|
+
summary: `${snapshot.name} revision ${snapshot.revision}`,
|
|
72
|
+
},
|
|
73
|
+
...blockers.map((detail) => ({
|
|
74
|
+
kind: 'blocker',
|
|
75
|
+
at: snapshot.updatedAt,
|
|
76
|
+
summary: detail,
|
|
77
|
+
})),
|
|
78
|
+
...mailboxEvents,
|
|
79
|
+
].slice(0, options.maxEvents ?? 32);
|
|
80
|
+
return freeze({
|
|
81
|
+
team: {
|
|
82
|
+
id: snapshot.teamId,
|
|
83
|
+
name: snapshot.name,
|
|
84
|
+
projectIdentity: snapshot.projectIdentity,
|
|
85
|
+
leadPolicy: snapshot.policy.lead,
|
|
86
|
+
executionPolicy: snapshot.policy.execution,
|
|
87
|
+
commitPolicy: snapshot.policy.commit,
|
|
88
|
+
lifecycle,
|
|
89
|
+
revision: snapshot.revision,
|
|
90
|
+
createdAt: snapshot.createdAt,
|
|
91
|
+
updatedAt: snapshot.updatedAt,
|
|
92
|
+
},
|
|
93
|
+
agents: freeze(agents),
|
|
94
|
+
tasks: freeze(tasks),
|
|
95
|
+
worktrees: freeze((options.worktrees ?? []).map((entry) => ({ ...entry }))),
|
|
96
|
+
mailbox: options.mailbox
|
|
97
|
+
? {
|
|
98
|
+
totalRecords: options.mailbox.totalRecords,
|
|
99
|
+
retainedRecords: options.mailbox.retainedRecords,
|
|
100
|
+
pendingByRecipient: options.mailbox.pendingByRecipient,
|
|
101
|
+
highestSequence: options.mailbox.highestSequence,
|
|
102
|
+
prunedThrough: options.mailbox.prunedThrough,
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
totalRecords: 0,
|
|
106
|
+
retainedRecords: 0,
|
|
107
|
+
pendingByRecipient: {},
|
|
108
|
+
highestSequence: 0,
|
|
109
|
+
prunedThrough: 0,
|
|
110
|
+
},
|
|
111
|
+
budgets: {
|
|
112
|
+
configured: {
|
|
113
|
+
maxTokens: snapshot.budgets.maxTokens,
|
|
114
|
+
maxDurationMs: snapshot.budgets.maxDurationMs,
|
|
115
|
+
},
|
|
116
|
+
used: {
|
|
117
|
+
totalTokens: snapshot.usage.totalTokens,
|
|
118
|
+
durationMs: snapshot.usage.durationMs,
|
|
119
|
+
},
|
|
120
|
+
exhaustedReason: snapshot.usage.exhausted?.reason ?? null,
|
|
121
|
+
},
|
|
122
|
+
health: { status, blockers: freeze(blockers), activeCount: active },
|
|
123
|
+
events: freeze(events),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export function renderTeamSummary(dashboard) {
|
|
127
|
+
const h = dashboard.health;
|
|
128
|
+
const base = `${dashboard.team.name} (${dashboard.team.id}): ${h.status}, ${h.activeCount} active, ${dashboard.tasks.length} tasks`;
|
|
129
|
+
return h.blockers.length
|
|
130
|
+
? `${base} — ${h.blockers.slice(0, 3).join('; ')}`
|
|
131
|
+
: base;
|
|
132
|
+
}
|
|
133
|
+
export function renderTeamAudit(dashboard) {
|
|
134
|
+
return dashboard.events
|
|
135
|
+
.map((event) => `${event.at} [${event.kind}] ${event.summary}${event.detail ? ` — ${event.detail}` : ''}`)
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
|
138
|
+
export async function readTeamMailboxAudit(input) {
|
|
139
|
+
const maxRecords = input.maxRecords ?? 64;
|
|
140
|
+
const maxBytes = input.maxBytes ?? 256 * 1024;
|
|
141
|
+
if (!Number.isSafeInteger(maxRecords) || maxRecords <= 0)
|
|
142
|
+
throw new Error('Invalid Team mailbox maxRecords');
|
|
143
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
144
|
+
throw new Error('Invalid Team mailbox maxBytes');
|
|
145
|
+
const mailboxDir = join(resolve(input.nativeRoot), 'state', 'teams', sanitizeProjectPath(input.snapshot.projectIdentity), input.snapshot.teamId, 'mailbox');
|
|
146
|
+
const messagesPath = join(mailboxDir, 'messages.jsonl');
|
|
147
|
+
const statePath = join(mailboxDir, 'state.json');
|
|
148
|
+
try {
|
|
149
|
+
await lstat(messagesPath);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code === 'ENOENT')
|
|
153
|
+
return {
|
|
154
|
+
totalRecords: 0,
|
|
155
|
+
retainedRecords: 0,
|
|
156
|
+
pendingByRecipient: Object.fromEntries(['lead', ...input.snapshot.roster.map((member) => member.name)].map((member) => [member, 0])),
|
|
157
|
+
highestSequence: 0,
|
|
158
|
+
prunedThrough: 0,
|
|
159
|
+
records: [],
|
|
160
|
+
};
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
let prunedThrough = 0;
|
|
164
|
+
let cursors = {};
|
|
165
|
+
try {
|
|
166
|
+
const state = JSON.parse(await readFile(statePath, 'utf8'));
|
|
167
|
+
if (state.teamId !== input.snapshot.teamId ||
|
|
168
|
+
state.projectIdentity !== input.snapshot.projectIdentity ||
|
|
169
|
+
!Number.isSafeInteger(state.prunedThrough) ||
|
|
170
|
+
state.prunedThrough < 0 ||
|
|
171
|
+
typeof state.cursors !== 'object' ||
|
|
172
|
+
state.cursors === null)
|
|
173
|
+
throw new Error('Invalid Team mailbox state');
|
|
174
|
+
prunedThrough = state.prunedThrough;
|
|
175
|
+
cursors = state.cursors;
|
|
176
|
+
for (const cursor of Object.values(cursors))
|
|
177
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
178
|
+
throw new Error('Invalid Team mailbox cursor');
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code !== 'ENOENT')
|
|
182
|
+
throw new Error(`Corrupt Team mailbox record: ${statePath}`, {
|
|
183
|
+
cause: error,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const retainedRows = [];
|
|
187
|
+
const retainedSizes = [];
|
|
188
|
+
let retainedBytes = 0;
|
|
189
|
+
let totalRecords = 0;
|
|
190
|
+
let highestSequence = 0;
|
|
191
|
+
let previous = prunedThrough;
|
|
192
|
+
const pendingByRecipient = {};
|
|
193
|
+
for (const member of ['lead', ...input.snapshot.roster.map((m) => m.name)])
|
|
194
|
+
pendingByRecipient[member] = 0;
|
|
195
|
+
const reader = createInterface({
|
|
196
|
+
input: createReadStream(messagesPath, { encoding: 'utf8' }),
|
|
197
|
+
crlfDelay: Infinity,
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
for await (const line of reader) {
|
|
201
|
+
if (!line)
|
|
202
|
+
continue;
|
|
203
|
+
const bytes = Buffer.byteLength(line, 'utf8') + 1;
|
|
204
|
+
let record;
|
|
205
|
+
try {
|
|
206
|
+
record = parseTeamMailboxMessage(JSON.parse(line));
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw new Error(`Corrupt Team mailbox record: ${messagesPath}`, {
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (record.teamId !== input.snapshot.teamId ||
|
|
214
|
+
record.sequence <= previous)
|
|
215
|
+
throw new Error(`Corrupt Team mailbox record: ${messagesPath}`);
|
|
216
|
+
previous = record.sequence;
|
|
217
|
+
totalRecords += 1;
|
|
218
|
+
highestSequence = record.sequence;
|
|
219
|
+
for (const recipient of record.recipients)
|
|
220
|
+
if (recipient in pendingByRecipient &&
|
|
221
|
+
record.sequence > (cursors[recipient] ?? prunedThrough))
|
|
222
|
+
pendingByRecipient[recipient] =
|
|
223
|
+
(pendingByRecipient[recipient] ?? 0) + 1;
|
|
224
|
+
retainedRows.push(record);
|
|
225
|
+
retainedSizes.push(bytes);
|
|
226
|
+
retainedBytes += bytes;
|
|
227
|
+
while (retainedRows.length > maxRecords || retainedBytes > maxBytes) {
|
|
228
|
+
retainedBytes -= retainedSizes.shift() ?? 0;
|
|
229
|
+
retainedRows.shift();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
reader.close();
|
|
235
|
+
}
|
|
236
|
+
/*
|
|
237
|
+
* The stream above deliberately keeps only the bounded tail while still
|
|
238
|
+
* validating ordering and counting pending messages across the file.
|
|
239
|
+
*/
|
|
240
|
+
return {
|
|
241
|
+
totalRecords,
|
|
242
|
+
retainedRecords: retainedRows.length,
|
|
243
|
+
pendingByRecipient,
|
|
244
|
+
highestSequence,
|
|
245
|
+
prunedThrough,
|
|
246
|
+
records: freeze(retainedRows),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export async function readTeamWorktreeEvidence(input) {
|
|
250
|
+
const root = join(resolve(input.nativeRoot), 'state', 'team-worktrees', sanitizeProjectPath(input.snapshot.projectIdentity), input.snapshot.teamId);
|
|
251
|
+
try {
|
|
252
|
+
const rootStat = await lstat(root);
|
|
253
|
+
if (rootStat.isSymbolicLink())
|
|
254
|
+
throw new Error(`Unsafe Team worktree symlink: ${root}`);
|
|
255
|
+
if (!rootStat.isDirectory())
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error.code === 'ENOENT')
|
|
260
|
+
return [];
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
const result = [];
|
|
264
|
+
const limit = input.maxEntries ?? 64;
|
|
265
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
266
|
+
throw new Error('Invalid Team worktree maxEntries');
|
|
267
|
+
const members = new Map(input.snapshot.roster.map((member) => [member.name, member]));
|
|
268
|
+
for (const task of input.snapshot.tasks) {
|
|
269
|
+
if (result.length >= limit)
|
|
270
|
+
break;
|
|
271
|
+
if (task.usage.generation <= 0)
|
|
272
|
+
continue;
|
|
273
|
+
const member = members.get(task.assignee);
|
|
274
|
+
if (!member || member.access !== 'write')
|
|
275
|
+
continue;
|
|
276
|
+
const hash = createHash('sha256')
|
|
277
|
+
.update(`${input.snapshot.projectIdentity}\0${input.snapshot.teamId}\0${task.id}\0${task.usage.generation}`)
|
|
278
|
+
.digest('hex')
|
|
279
|
+
.slice(0, 24);
|
|
280
|
+
const path = join(root, hash);
|
|
281
|
+
let stat;
|
|
282
|
+
try {
|
|
283
|
+
stat = await lstat(path);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error.code === 'ENOENT')
|
|
287
|
+
continue;
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (stat.isSymbolicLink())
|
|
291
|
+
throw new Error(`Unsafe Team worktree symlink: ${path}`);
|
|
292
|
+
if (!stat.isDirectory())
|
|
293
|
+
continue;
|
|
294
|
+
let branch = null;
|
|
295
|
+
try {
|
|
296
|
+
branch =
|
|
297
|
+
(await readFile(join(path, '.praxis-branch'), 'utf8')).trim() || null;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* optional metadata */
|
|
301
|
+
}
|
|
302
|
+
result.push({
|
|
303
|
+
taskId: task.id,
|
|
304
|
+
path,
|
|
305
|
+
branch: branch ?? `praxis/team/${input.snapshot.teamId}/${hash}`,
|
|
306
|
+
access: 'write',
|
|
307
|
+
present: true,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return freeze(result);
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=team-observability.js.map
|