praxis-agent 0.36.0 → 0.37.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 +16 -2
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +15 -0
- package/dist/application/session-service.js +611 -69
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +391 -143
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/transcript-projection.d.ts +2 -0
- package/dist/application/transcript-projection.js +30 -1
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +387 -136
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +158 -160
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/tool-permission.js +7 -3
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +1 -0
- package/dist/core/runtime.js +5 -1
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parseTeamId, parseTeamSnapshot, } from '../core/team-ownership.js';
|
|
4
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
5
|
+
import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
|
|
6
|
+
import { resolveProjectIdentity } from '../platform/project-identity.js';
|
|
7
|
+
import { sanitizeProjectPath } from '../platform/project-path-key.js';
|
|
8
|
+
function assertTeamId(teamId) {
|
|
9
|
+
parseTeamId(teamId);
|
|
10
|
+
}
|
|
11
|
+
function assertRoot(root) {
|
|
12
|
+
if (typeof root !== 'string' || root.trim() === '')
|
|
13
|
+
throw new Error('Invalid native Team root');
|
|
14
|
+
return resolve(root);
|
|
15
|
+
}
|
|
16
|
+
export class NativeTeamStore {
|
|
17
|
+
nativeRoot;
|
|
18
|
+
projectIdentity;
|
|
19
|
+
projectRoot;
|
|
20
|
+
mutation = Promise.resolve();
|
|
21
|
+
constructor(nativeRoot, projectIdentity) {
|
|
22
|
+
this.nativeRoot = nativeRoot;
|
|
23
|
+
this.projectIdentity = projectIdentity;
|
|
24
|
+
this.projectRoot = resolve(nativeRoot, 'state', 'teams', sanitizeProjectPath(projectIdentity));
|
|
25
|
+
}
|
|
26
|
+
static async open(options) {
|
|
27
|
+
const root = assertRoot(options.nativeRoot);
|
|
28
|
+
const projectIdentity = await resolveProjectIdentity(options.cwd);
|
|
29
|
+
return new NativeTeamStore(root, projectIdentity);
|
|
30
|
+
}
|
|
31
|
+
async createAndClaim(snapshot) {
|
|
32
|
+
const parsed = parseTeamSnapshot(snapshot);
|
|
33
|
+
this.assertSnapshot(parsed, parsed.teamId);
|
|
34
|
+
if (parsed.revision !== 0)
|
|
35
|
+
throw new Error('Native Team creation requires revision 0');
|
|
36
|
+
const claim = await this.acquireClaim(parsed.teamId);
|
|
37
|
+
let mutation;
|
|
38
|
+
try {
|
|
39
|
+
mutation = await this.acquireMutation(parsed.teamId);
|
|
40
|
+
if (await this.exists(this.statePath(parsed.teamId)))
|
|
41
|
+
throw new Error(`Native Team state already exists: ${this.statePath(parsed.teamId)}`);
|
|
42
|
+
await writeFileAtomically(this.statePath(parsed.teamId), `${JSON.stringify(parsed)}\n`, { mode: 0o600 });
|
|
43
|
+
return claim;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await claim.release();
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await mutation?.release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async claim(teamId) {
|
|
54
|
+
assertTeamId(teamId);
|
|
55
|
+
const claim = await this.acquireClaim(teamId);
|
|
56
|
+
try {
|
|
57
|
+
const snapshot = await this.read(teamId);
|
|
58
|
+
if (snapshot === null)
|
|
59
|
+
throw new Error(`Missing native Team state: ${this.statePath(teamId)}`);
|
|
60
|
+
return claim;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
await claim.release();
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async read(teamId) {
|
|
68
|
+
assertTeamId(teamId);
|
|
69
|
+
const path = this.statePath(teamId);
|
|
70
|
+
let source;
|
|
71
|
+
try {
|
|
72
|
+
source = await readFile(path, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === 'ENOENT')
|
|
76
|
+
return null;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value = JSON.parse(source);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw new Error(`Corrupt native Team state: ${path}`, { cause: error });
|
|
85
|
+
}
|
|
86
|
+
let snapshot;
|
|
87
|
+
try {
|
|
88
|
+
snapshot = parseTeamSnapshot(value);
|
|
89
|
+
this.assertSnapshot(snapshot, teamId);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof Error &&
|
|
93
|
+
error.message.startsWith('Invalid native Team state:'))
|
|
94
|
+
throw error;
|
|
95
|
+
throw new Error(`Invalid native Team state: ${path}`, { cause: error });
|
|
96
|
+
}
|
|
97
|
+
return snapshot;
|
|
98
|
+
}
|
|
99
|
+
async list() {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(this.projectRoot, { withFileTypes: true });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === 'ENOENT')
|
|
106
|
+
return [];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const ids = entries
|
|
110
|
+
.filter((entry) => {
|
|
111
|
+
if (!entry.isDirectory())
|
|
112
|
+
return false;
|
|
113
|
+
try {
|
|
114
|
+
parseTeamId(entry.name);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.map((entry) => entry.name)
|
|
122
|
+
.sort();
|
|
123
|
+
const snapshots = [];
|
|
124
|
+
for (const id of ids) {
|
|
125
|
+
const snapshot = await this.read(id);
|
|
126
|
+
if (snapshot === null)
|
|
127
|
+
throw new Error(`Invalid native Team state: ${this.statePath(id)}`);
|
|
128
|
+
snapshots.push(snapshot);
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze(snapshots);
|
|
131
|
+
}
|
|
132
|
+
async acquireClaim(teamId) {
|
|
133
|
+
assertTeamId(teamId);
|
|
134
|
+
const lease = await new ExclusiveFileLease(`${this.statePath(teamId)}.owner.lock`).tryAcquire();
|
|
135
|
+
if (!lease)
|
|
136
|
+
throw new Error(`Team ${teamId} is already owned`);
|
|
137
|
+
return new NativeTeamClaimImpl(teamId, lease, async () => {
|
|
138
|
+
const snapshot = await this.read(teamId);
|
|
139
|
+
if (snapshot === null)
|
|
140
|
+
throw new Error(`Missing native Team state: ${this.statePath(teamId)}`);
|
|
141
|
+
return snapshot;
|
|
142
|
+
}, (expectedRevision, next) => this.save(teamId, expectedRevision, next));
|
|
143
|
+
}
|
|
144
|
+
async acquireMutation(teamId) {
|
|
145
|
+
const leaseFile = `${this.statePath(teamId)}.mutation.lock`;
|
|
146
|
+
const deadline = Date.now() + 5_000;
|
|
147
|
+
for (;;) {
|
|
148
|
+
const lease = await new ExclusiveFileLease(leaseFile).tryAcquire();
|
|
149
|
+
if (lease)
|
|
150
|
+
return lease;
|
|
151
|
+
if (Date.now() >= deadline)
|
|
152
|
+
throw new Error(`Team ${teamId} mutation is busy: ${this.statePath(teamId)}`);
|
|
153
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 10));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
enqueue(operation) {
|
|
157
|
+
const queued = this.mutation.then(operation);
|
|
158
|
+
this.mutation = queued.then(() => undefined, () => undefined);
|
|
159
|
+
return queued;
|
|
160
|
+
}
|
|
161
|
+
async save(teamId, expectedRevision, next) {
|
|
162
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
|
|
163
|
+
throw new Error('Invalid expected Team revision');
|
|
164
|
+
return this.enqueue(async () => {
|
|
165
|
+
const mutation = await this.acquireMutation(teamId);
|
|
166
|
+
try {
|
|
167
|
+
const current = await this.read(teamId);
|
|
168
|
+
if (current === null)
|
|
169
|
+
throw new Error(`Missing native Team state: ${this.statePath(teamId)}`);
|
|
170
|
+
if (current.revision !== expectedRevision)
|
|
171
|
+
throw new Error(`Team revision conflict: expected ${expectedRevision}, found ${current.revision}`);
|
|
172
|
+
const parsed = parseTeamSnapshot(next);
|
|
173
|
+
this.assertSnapshot(parsed, teamId);
|
|
174
|
+
if (parsed.teamId !== current.teamId ||
|
|
175
|
+
parsed.projectIdentity !== current.projectIdentity ||
|
|
176
|
+
parsed.leadSessionId !== current.leadSessionId ||
|
|
177
|
+
parsed.createdAt !== current.createdAt)
|
|
178
|
+
throw new Error('Immutable native Team identity fields changed');
|
|
179
|
+
if (JSON.stringify(parsed.policy) !== JSON.stringify(current.policy) ||
|
|
180
|
+
JSON.stringify(parsed.budgets) !== JSON.stringify(current.budgets))
|
|
181
|
+
throw new Error('Immutable native Team policy or budgets changed');
|
|
182
|
+
if (parsed.revision !== current.revision + 1 ||
|
|
183
|
+
parsed.updatedAt < current.updatedAt)
|
|
184
|
+
throw new Error('Invalid native Team revision or timestamp');
|
|
185
|
+
await writeFileAtomically(this.statePath(teamId), `${JSON.stringify(parsed)}\n`, { mode: 0o600 });
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await mutation.release();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
assertSnapshot(snapshot, teamId) {
|
|
194
|
+
if (snapshot.projectIdentity !== this.projectIdentity ||
|
|
195
|
+
snapshot.teamId !== teamId)
|
|
196
|
+
throw new Error(`Invalid native Team state: ${this.statePath(teamId)}`);
|
|
197
|
+
}
|
|
198
|
+
statePath(teamId) {
|
|
199
|
+
return resolve(this.projectRoot, teamId, 'team.json');
|
|
200
|
+
}
|
|
201
|
+
async exists(path) {
|
|
202
|
+
try {
|
|
203
|
+
await readFile(path);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error.code === 'ENOENT')
|
|
208
|
+
return false;
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
class NativeTeamClaimImpl {
|
|
214
|
+
lease;
|
|
215
|
+
readState;
|
|
216
|
+
saveState;
|
|
217
|
+
active = true;
|
|
218
|
+
operations = Promise.resolve();
|
|
219
|
+
releasePromise;
|
|
220
|
+
teamId;
|
|
221
|
+
token;
|
|
222
|
+
pid;
|
|
223
|
+
acquiredAt;
|
|
224
|
+
constructor(teamId, lease, readState, saveState) {
|
|
225
|
+
this.lease = lease;
|
|
226
|
+
this.readState = readState;
|
|
227
|
+
this.saveState = saveState;
|
|
228
|
+
this.teamId = teamId;
|
|
229
|
+
this.token = lease.token;
|
|
230
|
+
this.pid = lease.pid;
|
|
231
|
+
this.acquiredAt = lease.createdAt;
|
|
232
|
+
}
|
|
233
|
+
assertActive() {
|
|
234
|
+
if (!this.active)
|
|
235
|
+
throw new Error(`Team ${this.teamId} claim is released`);
|
|
236
|
+
}
|
|
237
|
+
read() {
|
|
238
|
+
return this.admit(() => this.readState());
|
|
239
|
+
}
|
|
240
|
+
save(expectedRevision, next) {
|
|
241
|
+
return this.admit(() => this.saveState(expectedRevision, next));
|
|
242
|
+
}
|
|
243
|
+
async release() {
|
|
244
|
+
if (this.releasePromise)
|
|
245
|
+
return this.releasePromise;
|
|
246
|
+
this.active = false;
|
|
247
|
+
this.releasePromise = this.operations.then(() => this.lease.release());
|
|
248
|
+
return this.releasePromise;
|
|
249
|
+
}
|
|
250
|
+
admit(operation) {
|
|
251
|
+
this.assertActive();
|
|
252
|
+
const admitted = this.operations.then(operation, operation);
|
|
253
|
+
this.operations = admitted.then(() => undefined, () => undefined);
|
|
254
|
+
return admitted;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=native-team-store.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TranscriptEvent } from '../core/transcript-event.js';
|
|
2
|
+
import { type DecodedTranscriptRecord, type TranscriptCodecDiagnostic } from '../core/transcript-codec.js';
|
|
3
|
+
export interface NativeTranscriptTail {
|
|
4
|
+
byteLength: number;
|
|
5
|
+
lastLineHash: string | null;
|
|
6
|
+
lastEventId: string | null;
|
|
7
|
+
newlineTerminated: boolean;
|
|
8
|
+
branchParentId?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface NativeTranscriptSnapshot {
|
|
11
|
+
records: readonly DecodedTranscriptRecord[];
|
|
12
|
+
tail: NativeTranscriptTail;
|
|
13
|
+
}
|
|
14
|
+
export interface NativeTranscriptRecovery extends NativeTranscriptSnapshot {
|
|
15
|
+
issue: TranscriptCodecDiagnostic | null;
|
|
16
|
+
}
|
|
17
|
+
export type NativeTranscriptAppendResult = {
|
|
18
|
+
status: 'appended';
|
|
19
|
+
tail: NativeTranscriptTail;
|
|
20
|
+
} | {
|
|
21
|
+
status: 'conflict';
|
|
22
|
+
reason: 'interleaved-write' | 'locked' | 'tail-changed';
|
|
23
|
+
};
|
|
24
|
+
export type NativeTranscriptCreateResult = {
|
|
25
|
+
status: 'created';
|
|
26
|
+
tail: NativeTranscriptTail;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'conflict';
|
|
29
|
+
reason: 'already-exists';
|
|
30
|
+
};
|
|
31
|
+
export type NativeTranscriptReserveResult = {
|
|
32
|
+
status: 'reserved';
|
|
33
|
+
} | {
|
|
34
|
+
status: 'conflict';
|
|
35
|
+
reason: 'already-exists';
|
|
36
|
+
};
|
|
37
|
+
export interface NativeTranscriptLease {
|
|
38
|
+
reserve(): Promise<NativeTranscriptReserveResult>;
|
|
39
|
+
load(): Promise<NativeTranscriptSnapshot>;
|
|
40
|
+
append(expectedTail: NativeTranscriptTail, event: TranscriptEvent): Promise<NativeTranscriptAppendResult>;
|
|
41
|
+
appendMany(expectedTail: NativeTranscriptTail, events: readonly TranscriptEvent[]): Promise<NativeTranscriptAppendResult>;
|
|
42
|
+
}
|
|
43
|
+
export type NativeTranscriptLeaseResult<T> = {
|
|
44
|
+
status: 'completed';
|
|
45
|
+
value: T;
|
|
46
|
+
} | {
|
|
47
|
+
status: 'conflict';
|
|
48
|
+
reason: 'locked';
|
|
49
|
+
};
|
|
50
|
+
export declare class NativeTranscriptStore {
|
|
51
|
+
private readonly options;
|
|
52
|
+
private readonly lease;
|
|
53
|
+
constructor(options: {
|
|
54
|
+
transcriptFile: string;
|
|
55
|
+
lockFile: string;
|
|
56
|
+
});
|
|
57
|
+
private source;
|
|
58
|
+
private decode;
|
|
59
|
+
load(): Promise<NativeTranscriptSnapshot>;
|
|
60
|
+
loadReadOnly(): Promise<NativeTranscriptRecovery>;
|
|
61
|
+
exportReadOnly(): Promise<Buffer<ArrayBuffer>>;
|
|
62
|
+
private prepare;
|
|
63
|
+
create(events: readonly TranscriptEvent[]): Promise<NativeTranscriptCreateResult>;
|
|
64
|
+
private reserveUnderLease;
|
|
65
|
+
withLease<T>(operation: (lease: NativeTranscriptLease) => Promise<T>): Promise<NativeTranscriptLeaseResult<T>>;
|
|
66
|
+
private appendUnderLease;
|
|
67
|
+
append(expectedTail: NativeTranscriptTail, event: TranscriptEvent): Promise<NativeTranscriptAppendResult>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=native-transcript-store.d.ts.map
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
5
|
+
import { createNativeTranscriptCodec } from './native-transcript-codec.js';
|
|
6
|
+
import { classifyTranscriptAppend } from './transcript-file-append.js';
|
|
7
|
+
const hashLine = (line) => createHash('sha256').update(line).digest('hex');
|
|
8
|
+
const emptyTail = () => ({
|
|
9
|
+
byteLength: 0,
|
|
10
|
+
lastLineHash: null,
|
|
11
|
+
lastEventId: null,
|
|
12
|
+
newlineTerminated: true,
|
|
13
|
+
});
|
|
14
|
+
function splitLines(source) {
|
|
15
|
+
const end = source.at(-1) === 0x0a ? source.length - 1 : source.length;
|
|
16
|
+
if (!end)
|
|
17
|
+
return [];
|
|
18
|
+
const result = [];
|
|
19
|
+
let start = 0;
|
|
20
|
+
for (let i = 0; i < end; i++) {
|
|
21
|
+
if (source[i] === 0x0a) {
|
|
22
|
+
result.push(source.subarray(start, i));
|
|
23
|
+
start = i + 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
result.push(source.subarray(start, end));
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function tailFrom(source, records) {
|
|
30
|
+
const lines = splitLines(source);
|
|
31
|
+
const line = lines.at(-1);
|
|
32
|
+
return {
|
|
33
|
+
byteLength: source.length,
|
|
34
|
+
lastLineHash: line ? hashLine(line) : null,
|
|
35
|
+
lastEventId: records.at(-1)?.event.id ?? null,
|
|
36
|
+
newlineTerminated: source.length === 0 || source.at(-1) === 0x0a,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function equalTail(a, b) {
|
|
40
|
+
return (a.byteLength === b.byteLength &&
|
|
41
|
+
a.lastLineHash === b.lastLineHash &&
|
|
42
|
+
a.lastEventId === b.lastEventId &&
|
|
43
|
+
a.newlineTerminated === b.newlineTerminated);
|
|
44
|
+
}
|
|
45
|
+
function logicalTail(tail) {
|
|
46
|
+
return tail.branchParentId === undefined
|
|
47
|
+
? tail.lastEventId
|
|
48
|
+
: (tail.branchParentId ?? null);
|
|
49
|
+
}
|
|
50
|
+
function effectiveParentId(event) {
|
|
51
|
+
return event.kind === 'context-boundary'
|
|
52
|
+
? event.logicalParentId
|
|
53
|
+
: event.parentId;
|
|
54
|
+
}
|
|
55
|
+
function validateHistory(events) {
|
|
56
|
+
const ids = new Set();
|
|
57
|
+
const sessions = new Set();
|
|
58
|
+
const calls = new Set();
|
|
59
|
+
const children = new Map();
|
|
60
|
+
for (const [index, event] of events.entries()) {
|
|
61
|
+
if (ids.has(event.id))
|
|
62
|
+
throw new Error('Invalid native transcript invariant: event IDs must be unique');
|
|
63
|
+
if (sessions.size && !sessions.has(event.sessionId))
|
|
64
|
+
throw new Error('Invalid native transcript invariant: sessionId must match history');
|
|
65
|
+
if (event.kind === 'context-boundary') {
|
|
66
|
+
if (event.parentId !== null || !ids.has(event.logicalParentId))
|
|
67
|
+
throw new Error('Invalid native transcript invariant: context boundary logicalParentId must reference history');
|
|
68
|
+
}
|
|
69
|
+
else if (event.parentId === null) {
|
|
70
|
+
if (index !== 0)
|
|
71
|
+
throw new Error('Invalid native transcript invariant: only the first logical root may have a null parentId');
|
|
72
|
+
}
|
|
73
|
+
else if (!ids.has(event.parentId)) {
|
|
74
|
+
throw new Error('Invalid native transcript invariant: event parentId must reference an earlier event');
|
|
75
|
+
}
|
|
76
|
+
const parentId = effectiveParentId(event);
|
|
77
|
+
if (parentId !== null) {
|
|
78
|
+
const siblings = children.get(parentId) ?? [];
|
|
79
|
+
siblings.push(event);
|
|
80
|
+
children.set(parentId, siblings);
|
|
81
|
+
}
|
|
82
|
+
ids.add(event.id);
|
|
83
|
+
sessions.add(event.sessionId);
|
|
84
|
+
if (event.kind !== 'messages')
|
|
85
|
+
continue;
|
|
86
|
+
for (const message of event.messages) {
|
|
87
|
+
if (message.role !== 'assistant')
|
|
88
|
+
continue;
|
|
89
|
+
for (const call of message.toolCalls ?? []) {
|
|
90
|
+
if (calls.has(call.id))
|
|
91
|
+
throw new Error('Invalid native transcript invariant: assistant tool-call IDs must be unique');
|
|
92
|
+
calls.add(call.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const root = events[0];
|
|
97
|
+
if (!root)
|
|
98
|
+
return;
|
|
99
|
+
const activeCalls = new Set();
|
|
100
|
+
const activeResults = new Set();
|
|
101
|
+
const stack = [{ event: root, leaving: false }];
|
|
102
|
+
while (stack.length > 0) {
|
|
103
|
+
const frame = stack.pop();
|
|
104
|
+
if (!frame)
|
|
105
|
+
break;
|
|
106
|
+
if (frame.leaving) {
|
|
107
|
+
for (const id of frame.addedCalls)
|
|
108
|
+
activeCalls.delete(id);
|
|
109
|
+
for (const id of frame.addedResults)
|
|
110
|
+
activeResults.delete(id);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const addedCalls = [];
|
|
114
|
+
const addedResults = [];
|
|
115
|
+
if (frame.event.kind === 'messages') {
|
|
116
|
+
for (const message of frame.event.messages) {
|
|
117
|
+
if (message.role === 'assistant') {
|
|
118
|
+
for (const call of message.toolCalls ?? []) {
|
|
119
|
+
activeCalls.add(call.id);
|
|
120
|
+
addedCalls.push(call.id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (message.role === 'tool') {
|
|
124
|
+
if (!activeCalls.has(message.toolCallId))
|
|
125
|
+
throw new Error('Invalid native transcript invariant: tool result references unknown tool call on active ancestry');
|
|
126
|
+
if (activeResults.has(message.toolCallId))
|
|
127
|
+
throw new Error('Invalid native transcript invariant: tool call has multiple results on active ancestry');
|
|
128
|
+
activeResults.add(message.toolCallId);
|
|
129
|
+
addedResults.push(message.toolCallId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
stack.push({
|
|
134
|
+
event: frame.event,
|
|
135
|
+
leaving: true,
|
|
136
|
+
addedCalls,
|
|
137
|
+
addedResults,
|
|
138
|
+
});
|
|
139
|
+
const descendants = children.get(frame.event.id) ?? [];
|
|
140
|
+
for (let index = descendants.length - 1; index >= 0; index -= 1) {
|
|
141
|
+
const descendant = descendants[index];
|
|
142
|
+
if (descendant)
|
|
143
|
+
stack.push({ event: descendant, leaving: false });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export class NativeTranscriptStore {
|
|
148
|
+
options;
|
|
149
|
+
lease;
|
|
150
|
+
constructor(options) {
|
|
151
|
+
this.options = options;
|
|
152
|
+
this.lease = new ExclusiveFileLease(options.lockFile);
|
|
153
|
+
}
|
|
154
|
+
async source() {
|
|
155
|
+
try {
|
|
156
|
+
return await readFile(this.options.transcriptFile);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
if (e.code === 'ENOENT')
|
|
160
|
+
return Buffer.alloc(0);
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
decode(source, recover) {
|
|
165
|
+
if (!source.length)
|
|
166
|
+
return { records: [], tail: emptyTail(), issue: null };
|
|
167
|
+
const decoded = createNativeTranscriptCodec().decodeDocument(source);
|
|
168
|
+
if (decoded.issue && !recover)
|
|
169
|
+
throw new Error(`Invalid native transcript: ${decoded.issue.message}`);
|
|
170
|
+
if (!recover)
|
|
171
|
+
validateHistory(decoded.records.map((record) => record.event));
|
|
172
|
+
return {
|
|
173
|
+
records: decoded.records,
|
|
174
|
+
tail: tailFrom(source, decoded.records),
|
|
175
|
+
issue: decoded.issue,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async load() {
|
|
179
|
+
const { records, tail } = this.decode(await this.source(), false);
|
|
180
|
+
return { records, tail };
|
|
181
|
+
}
|
|
182
|
+
async loadReadOnly() {
|
|
183
|
+
return this.decode(await this.source(), true);
|
|
184
|
+
}
|
|
185
|
+
async exportReadOnly() {
|
|
186
|
+
return this.source();
|
|
187
|
+
}
|
|
188
|
+
prepare(history, events, expected) {
|
|
189
|
+
if (!events.length)
|
|
190
|
+
throw new Error('Cannot append an empty native transcript batch');
|
|
191
|
+
const all = history.map((r) => r.event);
|
|
192
|
+
const historicalIds = new Set(all.map((event) => event.id));
|
|
193
|
+
if (expected.branchParentId !== undefined &&
|
|
194
|
+
expected.branchParentId !== null &&
|
|
195
|
+
!historicalIds.has(expected.branchParentId))
|
|
196
|
+
throw new Error('Invalid native transcript invariant: branchParentId must reference an earlier event');
|
|
197
|
+
validateHistory([...history.map((record) => record.event), ...events]);
|
|
198
|
+
const ids = new Set();
|
|
199
|
+
const sessions = new Set();
|
|
200
|
+
for (const e of all) {
|
|
201
|
+
if (ids.has(e.id))
|
|
202
|
+
throw new Error('Invalid native transcript invariant: event IDs must be unique');
|
|
203
|
+
if (sessions.size && !sessions.has(e.sessionId))
|
|
204
|
+
throw new Error('Invalid native transcript invariant: sessionId must match history');
|
|
205
|
+
ids.add(e.id);
|
|
206
|
+
sessions.add(e.sessionId);
|
|
207
|
+
}
|
|
208
|
+
let parent = logicalTail(expected);
|
|
209
|
+
const lines = [];
|
|
210
|
+
for (const event of events) {
|
|
211
|
+
const codec = createNativeTranscriptCodec();
|
|
212
|
+
const encoded = codec.encodeLine(event);
|
|
213
|
+
if (!encoded.ok)
|
|
214
|
+
throw new Error(`Invalid native transcript event: ${encoded.issue.message}`);
|
|
215
|
+
if (ids.has(event.id))
|
|
216
|
+
throw new Error('Invalid native transcript invariant: event IDs must be unique');
|
|
217
|
+
if (sessions.size && !sessions.has(event.sessionId))
|
|
218
|
+
throw new Error('Invalid native transcript invariant: sessionId must match history');
|
|
219
|
+
sessions.add(event.sessionId);
|
|
220
|
+
if (event.kind === 'context-boundary') {
|
|
221
|
+
if (event.parentId !== null || !ids.has(event.logicalParentId))
|
|
222
|
+
throw new Error('Invalid native transcript invariant: context boundary logicalParentId must reference history');
|
|
223
|
+
parent = event.id;
|
|
224
|
+
}
|
|
225
|
+
else if (event.parentId !== parent)
|
|
226
|
+
throw new Error('Invalid native transcript invariant: event parentId does not match logical tail');
|
|
227
|
+
lines.push(Buffer.from(`${encoded.line}\n`));
|
|
228
|
+
ids.add(event.id);
|
|
229
|
+
parent = event.id;
|
|
230
|
+
}
|
|
231
|
+
return Buffer.concat(lines);
|
|
232
|
+
}
|
|
233
|
+
async create(events) {
|
|
234
|
+
const bytes = this.prepare([], events, emptyTail());
|
|
235
|
+
await mkdir(dirname(this.options.transcriptFile), { recursive: true });
|
|
236
|
+
let handle;
|
|
237
|
+
try {
|
|
238
|
+
handle = await open(this.options.transcriptFile, 'wx', 0o600);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
if (e.code === 'EEXIST')
|
|
242
|
+
return { status: 'conflict', reason: 'already-exists' };
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
await handle.writeFile(bytes);
|
|
247
|
+
await handle.sync();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await handle.close();
|
|
251
|
+
}
|
|
252
|
+
return { status: 'created', tail: (await this.load()).tail };
|
|
253
|
+
}
|
|
254
|
+
async reserveUnderLease() {
|
|
255
|
+
await mkdir(dirname(this.options.transcriptFile), { recursive: true });
|
|
256
|
+
let handle;
|
|
257
|
+
try {
|
|
258
|
+
handle = await open(this.options.transcriptFile, 'wx', 0o600);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
if (e.code === 'EEXIST')
|
|
262
|
+
return { status: 'conflict', reason: 'already-exists' };
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await handle.sync();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await handle.close();
|
|
270
|
+
}
|
|
271
|
+
return { status: 'reserved' };
|
|
272
|
+
}
|
|
273
|
+
async withLease(operation) {
|
|
274
|
+
const lock = await this.lease.tryAcquire();
|
|
275
|
+
if (!lock)
|
|
276
|
+
return { status: 'conflict', reason: 'locked' };
|
|
277
|
+
try {
|
|
278
|
+
const value = await operation({
|
|
279
|
+
reserve: () => this.reserveUnderLease(),
|
|
280
|
+
load: () => this.load(),
|
|
281
|
+
append: (tail, event) => this.appendUnderLease(tail, [event]),
|
|
282
|
+
appendMany: (tail, events) => this.appendUnderLease(tail, events),
|
|
283
|
+
});
|
|
284
|
+
return { status: 'completed', value };
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await lock.release();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async appendUnderLease(expected, events) {
|
|
291
|
+
const current = await this.load();
|
|
292
|
+
if (!equalTail(current.tail, expected))
|
|
293
|
+
return { status: 'conflict', reason: 'tail-changed' };
|
|
294
|
+
if (!current.tail.newlineTerminated)
|
|
295
|
+
throw new Error('Invalid native transcript: file is not newline-terminated');
|
|
296
|
+
const bytes = this.prepare(current.records, events, expected);
|
|
297
|
+
await mkdir(dirname(this.options.transcriptFile), { recursive: true });
|
|
298
|
+
const handle = await open(this.options.transcriptFile, 'a');
|
|
299
|
+
try {
|
|
300
|
+
const stat = await handle.stat();
|
|
301
|
+
if (stat.size !== expected.byteLength)
|
|
302
|
+
return { status: 'conflict', reason: 'tail-changed' };
|
|
303
|
+
await handle.writeFile(bytes);
|
|
304
|
+
await handle.sync();
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await handle.close();
|
|
308
|
+
}
|
|
309
|
+
const written = await readFile(this.options.transcriptFile);
|
|
310
|
+
if (classifyTranscriptAppend(written, expected.byteLength, bytes) ===
|
|
311
|
+
'interleaved-write')
|
|
312
|
+
return { status: 'conflict', reason: 'interleaved-write' };
|
|
313
|
+
const tail = (await this.load()).tail;
|
|
314
|
+
return { status: 'appended', tail };
|
|
315
|
+
}
|
|
316
|
+
async append(expectedTail, event) {
|
|
317
|
+
const result = await this.withLease((lease) => lease.append(expectedTail, event));
|
|
318
|
+
return result.status === 'completed' ? result.value : result;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=native-transcript-store.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function classifyTranscriptAppend(written, previousByteLength, encodedLine) {
|
|
2
|
+
const expectedEnd = previousByteLength + encodedLine.length;
|
|
3
|
+
if (written.length === expectedEnd &&
|
|
4
|
+
Buffer.from(written)
|
|
5
|
+
.subarray(previousByteLength, expectedEnd)
|
|
6
|
+
.equals(encodedLine))
|
|
7
|
+
return 'appended';
|
|
8
|
+
return 'interleaved-write';
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=transcript-file-append.js.map
|
|
@@ -25,6 +25,7 @@ export interface ClaudeToolCapabilityInput {
|
|
|
25
25
|
readonly agentTriggers?: boolean;
|
|
26
26
|
readonly backgroundAgents?: boolean;
|
|
27
27
|
readonly subagents?: boolean;
|
|
28
|
+
readonly teams?: boolean;
|
|
28
29
|
/** Explicit allow-list for capability-gated tools; never enables a gate. */
|
|
29
30
|
readonly tools?: readonly string[];
|
|
30
31
|
/** Always wins over the allow-list and the default rules. */
|
|
@@ -35,6 +36,7 @@ export interface ClaudeToolCapabilityInput {
|
|
|
35
36
|
export declare const CLAUDE_CODE_ENABLE_TASKS = "CLAUDE_CODE_ENABLE_TASKS";
|
|
36
37
|
export declare const CLAUDE_CODE_DISABLE_CRON = "CLAUDE_CODE_DISABLE_CRON";
|
|
37
38
|
export declare const PRAXIS_ENABLE_WORKFLOW_SCRIPTS = "PRAXIS_ENABLE_WORKFLOW_SCRIPTS";
|
|
39
|
+
export declare const PRAXIS_ENABLE_TEAMS = "PRAXIS_ENABLE_TEAMS";
|
|
38
40
|
/** task-v2 task-board tools, gated by the `tasks` capability. */
|
|
39
41
|
export declare const CLAUDE_TASK_V2_TOOLS: readonly ["TaskCreate", "TaskGet", "TaskList", "TaskUpdate"];
|
|
40
42
|
/** Workflow tool, gated by the `workflowScripts` capability. */
|
|
@@ -45,6 +47,8 @@ export declare const CLAUDE_AGENT_TRIGGER_TOOLS: readonly ["CronCreate", "CronDe
|
|
|
45
47
|
export declare const CLAUDE_AGENT_TOOLS: readonly ["Agent"];
|
|
46
48
|
/** Background task lifecycle tools, gated by the `backgroundAgents` capability. */
|
|
47
49
|
export declare const CLAUDE_BACKGROUND_TOOLS: readonly ["TaskOutput", "TaskStop"];
|
|
50
|
+
/** Experimental local Team lifecycle names, gated by `PRAXIS_ENABLE_TEAMS`. */
|
|
51
|
+
export declare const PRAXIS_TEAM_TOOLS: readonly ["TeamCreate", "TeamResume", "TeamList", "TeamAccept", "TeamStop", "TeamSend"];
|
|
48
52
|
/** Coordination tools advertised by a coordinator only when explicitly enabled. */
|
|
49
53
|
export declare const CLAUDE_COORDINATION_TOOLS: readonly ["AskUserQuestion", "SendMessage", "SendUserMessage", "Monitor", "PushNotification"];
|
|
50
54
|
/** Recursively suppressed for worker agents regardless of any allow-list. */
|