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.
Files changed (73) hide show
  1. package/README.md +28 -4
  2. package/dist/application/session-service.d.ts +3 -1
  3. package/dist/application/session-service.js +2 -2
  4. package/dist/application/team-agent-runtime.d.ts +3 -0
  5. package/dist/application/team-agent-runtime.js +20 -3
  6. package/dist/application/team-lead-decision-surface.d.ts +22 -0
  7. package/dist/application/team-lead-decision-surface.js +16 -0
  8. package/dist/application/team-observability.d.ts +113 -0
  9. package/dist/application/team-observability.js +312 -0
  10. package/dist/cli/interactive.js +444 -189
  11. package/dist/cli/tui/btw-surface-model.d.ts +22 -0
  12. package/dist/cli/tui/btw-surface-model.js +10 -0
  13. package/dist/cli/tui/claude-style.d.ts +50 -86
  14. package/dist/cli/tui/claude-style.js +93 -74
  15. package/dist/cli/tui/command-palette-model.d.ts +33 -0
  16. package/dist/cli/tui/command-palette-model.js +44 -0
  17. package/dist/cli/tui/config-dashboard.d.ts +3 -9
  18. package/dist/cli/tui/config-dashboard.js +2 -1
  19. package/dist/cli/tui/config-surface-model.d.ts +24 -0
  20. package/dist/cli/tui/config-surface-model.js +16 -0
  21. package/dist/cli/tui/diff-surface-model.d.ts +83 -0
  22. package/dist/cli/tui/diff-surface-model.js +142 -0
  23. package/dist/cli/tui/doctor-dashboard.d.ts +3 -4
  24. package/dist/cli/tui/doctor-dashboard.js +2 -1
  25. package/dist/cli/tui/doctor-surface-model.d.ts +13 -0
  26. package/dist/cli/tui/doctor-surface-model.js +9 -0
  27. package/dist/cli/tui/hooks-surface-model.d.ts +18 -0
  28. package/dist/cli/tui/hooks-surface-model.js +11 -0
  29. package/dist/cli/tui/leaf-surface-model.d.ts +68 -0
  30. package/dist/cli/tui/leaf-surface-model.js +55 -0
  31. package/dist/cli/tui/list-surface-model.d.ts +27 -0
  32. package/dist/cli/tui/list-surface-model.js +22 -0
  33. package/dist/cli/tui/mcp-elicitation-surface-model.d.ts +27 -0
  34. package/dist/cli/tui/mcp-elicitation-surface-model.js +25 -0
  35. package/dist/cli/tui/mcp-elicitation.d.ts +7 -1
  36. package/dist/cli/tui/mcp-elicitation.js +6 -2
  37. package/dist/cli/tui/mcp-panel.d.ts +3 -4
  38. package/dist/cli/tui/mcp-panel.js +2 -1
  39. package/dist/cli/tui/mcp-surface-model.d.ts +11 -0
  40. package/dist/cli/tui/mcp-surface-model.js +4 -0
  41. package/dist/cli/tui/memory-surface-model.d.ts +20 -0
  42. package/dist/cli/tui/memory-surface-model.js +12 -0
  43. package/dist/cli/tui/mention-picker-model.d.ts +41 -0
  44. package/dist/cli/tui/mention-picker-model.js +53 -0
  45. package/dist/cli/tui/model-effort-surface-model.d.ts +29 -0
  46. package/dist/cli/tui/model-effort-surface-model.js +38 -0
  47. package/dist/cli/tui/rewind-surface-model.d.ts +53 -0
  48. package/dist/cli/tui/rewind-surface-model.js +54 -0
  49. package/dist/cli/tui/sandbox-dashboard.d.ts +3 -4
  50. package/dist/cli/tui/sandbox-dashboard.js +2 -1
  51. package/dist/cli/tui/sandbox-surface-model.d.ts +13 -0
  52. package/dist/cli/tui/sandbox-surface-model.js +4 -0
  53. package/dist/cli/tui/session-picker-model.d.ts +41 -0
  54. package/dist/cli/tui/session-picker-model.js +69 -0
  55. package/dist/cli/tui/task-panel.d.ts +3 -3
  56. package/dist/cli/tui/task-panel.js +2 -1
  57. package/dist/cli/tui/task-surface-model.d.ts +11 -0
  58. package/dist/cli/tui/task-surface-model.js +4 -0
  59. package/dist/cli/tui/theme-surface-model.d.ts +85 -0
  60. package/dist/cli/tui/theme-surface-model.js +52 -0
  61. package/dist/cli-runtime.js +165 -16
  62. package/dist/compatibility/claude/team.d.ts +77 -0
  63. package/dist/compatibility/claude/team.js +219 -0
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/hooks/claude-file-change-watcher.d.ts +3 -0
  66. package/dist/hooks/claude-file-change-watcher.js +70 -8
  67. package/dist/hooks/claude-hook-tools.js +6 -1
  68. package/dist/persistence/native-transcript-migration.d.ts +37 -0
  69. package/dist/persistence/native-transcript-migration.js +430 -0
  70. package/dist/tools/local-tools.js +10 -1
  71. package/dist/tools/team-lead-tools.d.ts +16 -2
  72. package/dist/tools/team-lead-tools.js +124 -3
  73. 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, and measured context budgets; print mode,
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 current Praxis-owned core, provider
258
- adapters, and native data-plane slice without the Claude compatibility adapter.
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 TeamLeadToolRegistryFactory = (base: ToolRegistry, operations: TeamLeadOperations, sessionId: string, enabledTools: readonly string[]) => ToolRegistry;
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 approveTool = this.options.approveTool;
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
- ...(approveTool
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
- return approveTool(call, originalCall, wrapped);
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