@etiquekit/etq 1.0.14 → 1.0.16
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/AGENTS.md +26 -19
- package/LICENSE +3 -2
- package/NOTICE +4 -6
- package/QuickStart.md +50 -39
- package/README.md +56 -56
- package/bin/etiquette +5 -1
- package/bin/etiquette-core +5 -1
- package/docs/ARCHITECTURE.md +17 -20
- package/docs/CONCEPTS.md +5 -5
- package/docs/CORE_PROFILE.md +15 -7
- package/docs/LANE_PROVISIONING.md +2 -7
- package/docs/README.md +14 -13
- package/docs/RELEASE_SURFACE_AUDIT.md +7 -8
- package/docs/SEAT_DISCIPLINE.md +91 -55
- package/docs/SEAT_PROVISIONING.md +19 -16
- package/docs/TEAM_HANDOFF.md +21 -21
- package/docs/WORKTREE_QOL.md +0 -7
- package/docs/contracts/ledger-entry/README.md +11 -11
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +2 -2
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
- package/lib/etiquette-core.js +313 -0
- package/lib/etiquette.js +1154 -0
- package/package.json +11 -10
- package/templates/etiquette-vanilla-v0/README.md +3 -2
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +6 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +5 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +5 -0
- package/templates/hosted-receiver/README.md +41 -0
- package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
- package/templates/seat-packs-v0/README.md +2 -3
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/codex-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
- package/docs/CONCEPT_STATUS.md +0 -66
- package/packages/control/src/authority/lease.ts +0 -261
- package/packages/control/src/authority/node-delegation.ts +0 -257
- package/packages/control/src/authority/rig-conductor.ts +0 -632
- package/packages/control/src/cli/argv.ts +0 -155
- package/packages/control/src/cli/commands/console.ts +0 -200
- package/packages/control/src/cli/commands/dispatch-core.ts +0 -89
- package/packages/control/src/cli/commands/dispatch.ts +0 -279
- package/packages/control/src/cli/commands/harness.ts +0 -89
- package/packages/control/src/cli/commands/hook.ts +0 -50
- package/packages/control/src/cli/commands/ledger.ts +0 -91
- package/packages/control/src/cli/commands/local-workflow.ts +0 -4690
- package/packages/control/src/cli/commands/memory.ts +0 -445
- package/packages/control/src/cli/commands/release.ts +0 -108
- package/packages/control/src/cli/commands/rubric.ts +0 -103
- package/packages/control/src/cli/commands/seat.ts +0 -179
- package/packages/control/src/cli/commands/session.ts +0 -127
- package/packages/control/src/cli/commands/supervision.ts +0 -246
- package/packages/control/src/cli/commands/sync.ts +0 -119
- package/packages/control/src/cli/commands/workflow.ts +0 -86
- package/packages/control/src/cli/commands/workspace.ts +0 -50
- package/packages/control/src/cli/core-usage.ts +0 -34
- package/packages/control/src/cli/prompt.ts +0 -67
- package/packages/control/src/cli/supervision-deps.ts +0 -44
- package/packages/control/src/cli/usage.ts +0 -241
- package/packages/control/src/cli.ts +0 -207
- package/packages/control/src/core-cli.ts +0 -50
- package/packages/control/src/dispatch/decision.ts +0 -202
- package/packages/control/src/dispatch/projection.ts +0 -293
- package/packages/control/src/dispatch/record.ts +0 -153
- package/packages/control/src/engagement/project.ts +0 -170
- package/packages/control/src/fs.ts +0 -19
- package/packages/control/src/harness/pruning.ts +0 -406
- package/packages/control/src/hooks/dispatcher.ts +0 -117
- package/packages/control/src/hooks/outbox.ts +0 -86
- package/packages/control/src/hooks/sanitize.ts +0 -6
- package/packages/control/src/hooks/types.ts +0 -34
- package/packages/control/src/index.ts +0 -384
- package/packages/control/src/ledger/entry.ts +0 -303
- package/packages/control/src/ledger/indexer.ts +0 -542
- package/packages/control/src/memory/context.ts +0 -149
- package/packages/control/src/memory/drain-import.ts +0 -207
- package/packages/control/src/memory/indexer.ts +0 -284
- package/packages/control/src/memory/query.ts +0 -75
- package/packages/control/src/memory/sanitize.ts +0 -50
- package/packages/control/src/memory/sharded-drain-import.ts +0 -212
- package/packages/control/src/memory/status.ts +0 -211
- package/packages/control/src/memory/store-lifecycle.ts +0 -509
- package/packages/control/src/memory/store.ts +0 -284
- package/packages/control/src/memory/types.ts +0 -146
- package/packages/control/src/parity/surfaces.ts +0 -748
- package/packages/control/src/project.ts +0 -141
- package/packages/control/src/projection/local-ledger-view.ts +0 -373
- package/packages/control/src/projection/return-enforcement.ts +0 -48
- package/packages/control/src/projection/timeline-preview.ts +0 -539
- package/packages/control/src/projection/timeline.ts +0 -708
- package/packages/control/src/release/readiness.ts +0 -842
- package/packages/control/src/rubric/loader.ts +0 -326
- package/packages/control/src/rubric/promotion.ts +0 -54
- package/packages/control/src/rubric/runner.ts +0 -159
- package/packages/control/src/rubric/types.ts +0 -158
- package/packages/control/src/seat/owner-card.ts +0 -388
- package/packages/control/src/seat/readiness.ts +0 -834
- package/packages/control/src/session/runbook.ts +0 -431
- package/packages/control/src/shared/sanitize.ts +0 -49
- package/packages/control/src/supervision/action-classes.ts +0 -192
- package/packages/control/src/supervision/command-apply.ts +0 -378
- package/packages/control/src/supervision/errors.ts +0 -14
- package/packages/control/src/supervision/event-replay.ts +0 -155
- package/packages/control/src/supervision/events.ts +0 -109
- package/packages/control/src/supervision/index.ts +0 -16
- package/packages/control/src/supervision/manifest.ts +0 -127
- package/packages/control/src/supervision/paths.ts +0 -49
- package/packages/control/src/supervision/projection-adapter.ts +0 -274
- package/packages/control/src/supervision/projection.ts +0 -75
- package/packages/control/src/supervision/rebuild.ts +0 -99
- package/packages/control/src/supervision/session-open.ts +0 -131
- package/packages/control/src/supervision/session-read.ts +0 -99
- package/packages/control/src/supervision/sqlite-impl.ts +0 -71
- package/packages/control/src/supervision/sqlite.ts +0 -121
- package/packages/control/src/supervision/store-rows.ts +0 -371
- package/packages/control/src/supervision/turn-close.ts +0 -154
- package/packages/control/src/supervision/turn-open.ts +0 -284
- package/packages/control/src/sync/event-log-merge-driver.ts +0 -263
- package/packages/control/src/sync/join-plan.ts +0 -375
- package/packages/control/src/sync/outbox.ts +0 -492
- package/packages/control/src/workflow/evaluator.ts +0 -140
- package/packages/control/src/workflow/loader.ts +0 -200
- package/packages/control/src/workflow/types.ts +0 -90
- package/packages/control/src/workspace/authority.ts +0 -499
- package/packages/protocol/src/guards.ts +0 -119
- package/packages/protocol/src/huddle-board.ts +0 -198
- package/packages/protocol/src/huddle.ts +0 -295
- package/packages/protocol/src/incident.ts +0 -251
- package/packages/protocol/src/index.ts +0 -8
- package/packages/protocol/src/interfaces.ts +0 -107
- package/packages/protocol/src/packet-profile.ts +0 -195
- package/packages/protocol/src/state.ts +0 -81
- package/packages/protocol/src/types.ts +0 -434
- package/release/lineage.v0.json +0 -15
- package/scripts/release-candidate-verify.sh +0 -175
- package/scripts/release-checksum.sh +0 -25
- package/scripts/release-pack-canary.sh +0 -97
- package/scripts/release-scan.sh +0 -249
- package/scripts/release-sign.sh +0 -34
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApprovedNextTurnPayload,
|
|
3
|
-
ProjectionPolicy,
|
|
4
|
-
SupervisionCommandRecord,
|
|
5
|
-
SupervisionDeps,
|
|
6
|
-
SupervisionSession,
|
|
7
|
-
SupervisionTurn,
|
|
8
|
-
} from '../../../protocol/src/index';
|
|
9
|
-
import {
|
|
10
|
-
authorizeTurnForSeat,
|
|
11
|
-
turnMatchesApproval,
|
|
12
|
-
validateTurnBounds,
|
|
13
|
-
type TurnBoundsProposal,
|
|
14
|
-
} from './action-classes';
|
|
15
|
-
import { SupervisionRuntimeError } from './errors';
|
|
16
|
-
import { appendSupervisionEvent, buildEvent } from './events';
|
|
17
|
-
import { loadSeatManifest } from './manifest';
|
|
18
|
-
import { supervisionRuntimePaths } from './paths';
|
|
19
|
-
import { renderProjection, writeLatestState } from './projection';
|
|
20
|
-
import { applySupervisionSchema } from './sqlite';
|
|
21
|
-
import { withSupervisionTransaction } from './sqlite-impl';
|
|
22
|
-
import {
|
|
23
|
-
clearSessionApproval,
|
|
24
|
-
insertCommandRow,
|
|
25
|
-
insertEventRow,
|
|
26
|
-
insertTurnRow,
|
|
27
|
-
loadPendingCommands,
|
|
28
|
-
loadSession,
|
|
29
|
-
updateSessionCurrentTurn,
|
|
30
|
-
updateSessionStatus,
|
|
31
|
-
} from './store-rows';
|
|
32
|
-
|
|
33
|
-
export interface TurnOpenArgs extends TurnBoundsProposal {
|
|
34
|
-
sessionId: string;
|
|
35
|
-
campaignRoot: string;
|
|
36
|
-
ownerSeat: string;
|
|
37
|
-
projectionPolicy: ProjectionPolicy;
|
|
38
|
-
seatManifestOverride?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Open a new turn for a supervision session.
|
|
43
|
-
*
|
|
44
|
-
* Enforces (in order): seat-manifest identity match, `human_only` short-
|
|
45
|
-
* circuit, bounds validation, seat-capability authorization, session-running
|
|
46
|
-
* gate, single-open-turn rule, approve-next-turn exact match,
|
|
47
|
-
* bounded_product_mutation-requires-approval, turn-cap.
|
|
48
|
-
*
|
|
49
|
-
* Canonical ordering: events.jsonl first, then SQLite txn, then artifacts.
|
|
50
|
-
*/
|
|
51
|
-
export async function openSupervisionTurn(
|
|
52
|
-
args: TurnOpenArgs,
|
|
53
|
-
deps: SupervisionDeps,
|
|
54
|
-
): Promise<SupervisionTurn> {
|
|
55
|
-
const paths = supervisionRuntimePaths(args.campaignRoot);
|
|
56
|
-
const seat = await loadSeatManifest(args.seatManifestOverride);
|
|
57
|
-
if (seat.seatId !== args.ownerSeat) {
|
|
58
|
-
throw new SupervisionRuntimeError(
|
|
59
|
-
`seat manifest seatId (${seat.seatId}) does not match turn owner (${args.ownerSeat}); ` +
|
|
60
|
-
`each seat must load its own manifest`,
|
|
61
|
-
'SEAT_MISMATCH',
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// human_only short-circuit: set session awaiting_human, record rejection.
|
|
66
|
-
if (args.allowedActionClass === 'human_only') {
|
|
67
|
-
await rejectHumanOnlyTurn(paths, deps, args.sessionId, args.ownerSeat);
|
|
68
|
-
throw new SupervisionRuntimeError(
|
|
69
|
-
`human_only turns may not be opened by automation`,
|
|
70
|
-
'HUMAN_ONLY',
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
validateTurnBounds(args);
|
|
75
|
-
authorizeTurnForSeat(args, seat);
|
|
76
|
-
|
|
77
|
-
const db = deps.openSqlite(paths.sqlitePath);
|
|
78
|
-
try {
|
|
79
|
-
applySupervisionSchema(db);
|
|
80
|
-
const session = loadSession(db, args.sessionId);
|
|
81
|
-
|
|
82
|
-
if (session.status !== 'running') {
|
|
83
|
-
throw new SupervisionRuntimeError(
|
|
84
|
-
`cannot open turn while session status is ${session.status}`,
|
|
85
|
-
'SESSION_NOT_RUNNING',
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const openTurn = db.get<{ turn_id: string }>(
|
|
90
|
-
'SELECT turn_id FROM turns WHERE session_id = ? AND status = ?',
|
|
91
|
-
[args.sessionId, 'open'],
|
|
92
|
-
);
|
|
93
|
-
if (openTurn) {
|
|
94
|
-
throw new SupervisionRuntimeError(
|
|
95
|
-
`session ${args.sessionId} already has an open turn (${openTurn.turn_id})`,
|
|
96
|
-
'TURN_ALREADY_OPEN',
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// If an approval blob exists, the requested turn MUST match exactly.
|
|
101
|
-
let consumedApproval = false;
|
|
102
|
-
if (session.approvedNextTurnJson) {
|
|
103
|
-
let approval: ApprovedNextTurnPayload;
|
|
104
|
-
try {
|
|
105
|
-
approval = JSON.parse(session.approvedNextTurnJson) as ApprovedNextTurnPayload;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
throw new SupervisionRuntimeError(
|
|
108
|
-
`stored approvedNextTurnJson is not valid JSON: ${String(err)}`,
|
|
109
|
-
'APPROVAL_CORRUPT',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
const cmp = turnMatchesApproval(
|
|
113
|
-
{ ...args, projectionPolicy: args.projectionPolicy },
|
|
114
|
-
approval,
|
|
115
|
-
);
|
|
116
|
-
if (!cmp.matches) {
|
|
117
|
-
throw new SupervisionRuntimeError(
|
|
118
|
-
`requested turn does not match approved_next_turn: ${cmp.diffs.join('; ')}`,
|
|
119
|
-
'APPROVAL_MISMATCH',
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
consumedApproval = true;
|
|
123
|
-
} else if (args.allowedActionClass === 'bounded_product_mutation') {
|
|
124
|
-
throw new SupervisionRuntimeError(
|
|
125
|
-
`bounded_product_mutation requires a prior approve_next_turn; none is present`,
|
|
126
|
-
'APPROVAL_REQUIRED',
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const nextSeq = db.get<{ max_seq: number | null }>(
|
|
131
|
-
'SELECT MAX(sequence_no) AS max_seq FROM turns WHERE session_id = ?',
|
|
132
|
-
[args.sessionId],
|
|
133
|
-
)?.max_seq;
|
|
134
|
-
const sequenceNo = (nextSeq ?? 0) + 1;
|
|
135
|
-
|
|
136
|
-
if (sequenceNo > session.turnCap) {
|
|
137
|
-
throw new SupervisionRuntimeError(
|
|
138
|
-
`turn cap exceeded: session allows ${session.turnCap}, this would be turn ${sequenceNo}`,
|
|
139
|
-
'TURN_CAP',
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const now = deps.now().toISOString();
|
|
144
|
-
const turn: SupervisionTurn = {
|
|
145
|
-
turnId: deps.randomId(),
|
|
146
|
-
sessionId: args.sessionId,
|
|
147
|
-
sequenceNo,
|
|
148
|
-
status: 'open',
|
|
149
|
-
ownerSeat: args.ownerSeat,
|
|
150
|
-
allowedActionClass: args.allowedActionClass,
|
|
151
|
-
maxDurationMinutes: args.maxDurationMinutes,
|
|
152
|
-
allowedCommands: [...args.allowedCommands],
|
|
153
|
-
repoRoots: [...args.repoRoots],
|
|
154
|
-
ownedPaths: [...args.ownedPaths],
|
|
155
|
-
forbiddenPaths: [...args.forbiddenPaths],
|
|
156
|
-
verifyCommands: [...args.verifyCommands],
|
|
157
|
-
requiresHumanApproval: args.requiresHumanApproval,
|
|
158
|
-
projectionPolicy: args.projectionPolicy,
|
|
159
|
-
startedAt: now,
|
|
160
|
-
endedAt: null,
|
|
161
|
-
result: null,
|
|
162
|
-
summary: null,
|
|
163
|
-
evidenceRefs: [],
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const event = buildEvent(deps, {
|
|
167
|
-
eventType: 'turn.opened',
|
|
168
|
-
sessionId: session.sessionId,
|
|
169
|
-
turnId: turn.turnId,
|
|
170
|
-
commandId: null,
|
|
171
|
-
actor: args.ownerSeat,
|
|
172
|
-
audience: 'all',
|
|
173
|
-
at: now,
|
|
174
|
-
payload: turn,
|
|
175
|
-
});
|
|
176
|
-
await appendSupervisionEvent(paths.eventsPath, event);
|
|
177
|
-
|
|
178
|
-
withSupervisionTransaction(db, () => {
|
|
179
|
-
insertTurnRow(db, turn);
|
|
180
|
-
updateSessionCurrentTurn(db, session.sessionId, turn.turnId, now);
|
|
181
|
-
if (consumedApproval) {
|
|
182
|
-
clearSessionApproval(db, session.sessionId, now);
|
|
183
|
-
}
|
|
184
|
-
insertEventRow(db, event);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const updated: SupervisionSession = {
|
|
188
|
-
...session,
|
|
189
|
-
currentTurnId: turn.turnId,
|
|
190
|
-
approvedNextTurnJson: consumedApproval ? null : session.approvedNextTurnJson,
|
|
191
|
-
updatedAt: now,
|
|
192
|
-
};
|
|
193
|
-
await writeLatestState(paths.latestStatePath, updated, turn);
|
|
194
|
-
await renderProjection(deps, paths, updated, turn, loadPendingCommands(db, session.sessionId));
|
|
195
|
-
|
|
196
|
-
return turn;
|
|
197
|
-
} finally {
|
|
198
|
-
db.close();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Side-effect path invoked when `openSupervisionTurn` receives an
|
|
204
|
-
* `allowedActionClass: 'human_only'` request. Flips the session to
|
|
205
|
-
* `awaiting_human` (if not already), records a rejected
|
|
206
|
-
* `escalate_to_human` command, renders the updated projection, and returns.
|
|
207
|
-
* The caller throws `SupervisionRuntimeError('HUMAN_ONLY')` after this
|
|
208
|
-
* returns.
|
|
209
|
-
*/
|
|
210
|
-
async function rejectHumanOnlyTurn(
|
|
211
|
-
paths: ReturnType<typeof supervisionRuntimePaths>,
|
|
212
|
-
deps: SupervisionDeps,
|
|
213
|
-
sessionId: string,
|
|
214
|
-
actor: string,
|
|
215
|
-
): Promise<void> {
|
|
216
|
-
const db = deps.openSqlite(paths.sqlitePath);
|
|
217
|
-
try {
|
|
218
|
-
applySupervisionSchema(db);
|
|
219
|
-
const session = loadSession(db, sessionId);
|
|
220
|
-
const now = deps.now().toISOString();
|
|
221
|
-
|
|
222
|
-
const needsStatusFlip = session.status !== 'awaiting_human';
|
|
223
|
-
const statusEvent = needsStatusFlip
|
|
224
|
-
? buildEvent(deps, {
|
|
225
|
-
eventType: 'session.status_changed',
|
|
226
|
-
sessionId,
|
|
227
|
-
turnId: null,
|
|
228
|
-
commandId: null,
|
|
229
|
-
actor: 'system',
|
|
230
|
-
audience: 'all',
|
|
231
|
-
at: now,
|
|
232
|
-
payload: { from: session.status, to: 'awaiting_human', reason: 'human_only_turn_attempted' },
|
|
233
|
-
})
|
|
234
|
-
: null;
|
|
235
|
-
|
|
236
|
-
const commandRecord: SupervisionCommandRecord = {
|
|
237
|
-
commandId: deps.randomId(),
|
|
238
|
-
sessionId,
|
|
239
|
-
turnId: null,
|
|
240
|
-
issuedBy: actor,
|
|
241
|
-
command: 'escalate_to_human',
|
|
242
|
-
payloadJson: JSON.stringify({ reason: 'human_only_turn_attempted' }),
|
|
243
|
-
status: 'rejected',
|
|
244
|
-
issuedAt: now,
|
|
245
|
-
appliedAt: now,
|
|
246
|
-
rejectionReason: 'human_only turns may not be opened by automation',
|
|
247
|
-
};
|
|
248
|
-
const rejectedEvent = buildEvent(deps, {
|
|
249
|
-
eventType: 'command.rejected',
|
|
250
|
-
sessionId,
|
|
251
|
-
turnId: null,
|
|
252
|
-
commandId: commandRecord.commandId,
|
|
253
|
-
actor: 'system',
|
|
254
|
-
audience: 'all',
|
|
255
|
-
at: now,
|
|
256
|
-
payload: {
|
|
257
|
-
command: 'turn-open:human_only',
|
|
258
|
-
rejectedAt: now,
|
|
259
|
-
reason: commandRecord.rejectionReason,
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Events first (canonical), then SQLite mirror in a single transaction.
|
|
264
|
-
if (statusEvent) {
|
|
265
|
-
await appendSupervisionEvent(paths.eventsPath, statusEvent);
|
|
266
|
-
}
|
|
267
|
-
await appendSupervisionEvent(paths.eventsPath, rejectedEvent);
|
|
268
|
-
|
|
269
|
-
withSupervisionTransaction(db, () => {
|
|
270
|
-
if (statusEvent) {
|
|
271
|
-
updateSessionStatus(db, sessionId, 'awaiting_human', now);
|
|
272
|
-
insertEventRow(db, statusEvent);
|
|
273
|
-
}
|
|
274
|
-
insertCommandRow(db, commandRecord);
|
|
275
|
-
insertEventRow(db, rejectedEvent);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const updated: SupervisionSession = { ...session, status: 'awaiting_human', updatedAt: now };
|
|
279
|
-
await writeLatestState(paths.latestStatePath, updated, null);
|
|
280
|
-
await renderProjection(deps, paths, updated, null, loadPendingCommands(db, sessionId));
|
|
281
|
-
} finally {
|
|
282
|
-
db.close();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
export const EVENT_LOG_MERGE_DRIVER_SCHEMA = 'event-log-merge-driver-install.v0' as const;
|
|
8
|
-
export const EVENT_LOG_MERGE_DRIVER_STATUS_SCHEMA = 'event-log-merge-driver-status.v0' as const;
|
|
9
|
-
export const DEFAULT_EVENT_LOG_MERGE_DRIVER = 'etqseq';
|
|
10
|
-
export const EVENT_LOG_ATTRIBUTE_PATTERN = '.etiquette/events/*.ndjson';
|
|
11
|
-
|
|
12
|
-
export interface EventLogMergeDriverInstallResult {
|
|
13
|
-
schema: typeof EVENT_LOG_MERGE_DRIVER_SCHEMA;
|
|
14
|
-
driver_name: string;
|
|
15
|
-
attributes_ref: '.gitattributes';
|
|
16
|
-
attributes_rule: string;
|
|
17
|
-
gitattributes_updated: boolean;
|
|
18
|
-
git_config_updated: boolean;
|
|
19
|
-
git_config_present: boolean;
|
|
20
|
-
warnings: string[];
|
|
21
|
-
authority_boundary: EventLogMergeDriverAuthorityBoundary;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface EventLogMergeDriverStatus {
|
|
25
|
-
schema: typeof EVENT_LOG_MERGE_DRIVER_STATUS_SCHEMA;
|
|
26
|
-
driver_name: string;
|
|
27
|
-
attributes_ref: '.gitattributes';
|
|
28
|
-
attributes_configured: boolean;
|
|
29
|
-
git_config_present: boolean;
|
|
30
|
-
git_config_configured: boolean;
|
|
31
|
-
ready: boolean;
|
|
32
|
-
warnings: string[];
|
|
33
|
-
authority_boundary: EventLogMergeDriverAuthorityBoundary;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface EventLogMergeDriverAuthorityBoundary {
|
|
37
|
-
can_authorize_work: false;
|
|
38
|
-
can_execute_work: false;
|
|
39
|
-
can_grant: false;
|
|
40
|
-
can_close_work: false;
|
|
41
|
-
can_merge_product_code: false;
|
|
42
|
-
configures_git_merge_driver_only: true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface InstallEventLogMergeDriverOptions {
|
|
46
|
-
projectRoot: string;
|
|
47
|
-
driverName?: string;
|
|
48
|
-
configureGit?: boolean;
|
|
49
|
-
driverCommand?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface EventLogMergeDriverStatusOptions {
|
|
53
|
-
projectRoot: string;
|
|
54
|
-
driverName?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface MergeAppendOnlyEventLogsOptions {
|
|
58
|
-
ancestorPath: string;
|
|
59
|
-
currentPath: string;
|
|
60
|
-
otherPath: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function authorityBoundary(): EventLogMergeDriverAuthorityBoundary {
|
|
64
|
-
return {
|
|
65
|
-
can_authorize_work: false,
|
|
66
|
-
can_execute_work: false,
|
|
67
|
-
can_grant: false,
|
|
68
|
-
can_close_work: false,
|
|
69
|
-
can_merge_product_code: false,
|
|
70
|
-
configures_git_merge_driver_only: true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function assertDriverName(value: string): string {
|
|
75
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(value)) {
|
|
76
|
-
throw new Error('--driver-name must be an opaque Git merge driver name');
|
|
77
|
-
}
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function attributesRule(driverName: string): string {
|
|
82
|
-
return `${EVENT_LOG_ATTRIBUTE_PATTERN} merge=${driverName}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function readTextIfExists(path: string): Promise<string> {
|
|
86
|
-
try {
|
|
87
|
-
return await readFile(path, 'utf8');
|
|
88
|
-
} catch (err) {
|
|
89
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
90
|
-
if (code === 'ENOENT') return '';
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function shellQuote(value: string): string {
|
|
96
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function defaultDriverCommand(): string {
|
|
100
|
-
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'cli.ts');
|
|
101
|
-
return `bun ${shellQuote(cliPath)} sync event-log-merge-driver %O %A %B`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function gitConfigGet(projectRoot: string, key: string): string | null {
|
|
105
|
-
const result = spawnSync('git', ['-C', projectRoot, 'config', '--get', key], { encoding: 'utf8' });
|
|
106
|
-
if (result.status !== 0) return null;
|
|
107
|
-
return result.stdout.trim() || null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function gitConfigSet(projectRoot: string, key: string, value: string): boolean {
|
|
111
|
-
const result = spawnSync('git', ['-C', projectRoot, 'config', key, value], { encoding: 'utf8' });
|
|
112
|
-
if (result.status !== 0) {
|
|
113
|
-
throw new Error(`git config failed for ${key}: ${result.stderr.trim() || result.status}`);
|
|
114
|
-
}
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function isGitWorktree(projectRoot: string): boolean {
|
|
119
|
-
return spawnSync('git', ['-C', projectRoot, 'rev-parse', '--is-inside-work-tree'], {
|
|
120
|
-
encoding: 'utf8',
|
|
121
|
-
}).status === 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function attributesConfigured(text: string, driverName: string): boolean {
|
|
125
|
-
return text
|
|
126
|
-
.split(/\r?\n/)
|
|
127
|
-
.map((line) => line.trim())
|
|
128
|
-
.some((line) => line === attributesRule(driverName));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function gitConfigConfigured(projectRoot: string, driverName: string): boolean {
|
|
132
|
-
const command = gitConfigGet(projectRoot, `merge.${driverName}.driver`);
|
|
133
|
-
return Boolean(command?.includes('sync event-log-merge-driver'));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function installEventLogMergeDriver(
|
|
137
|
-
options: InstallEventLogMergeDriverOptions,
|
|
138
|
-
): Promise<EventLogMergeDriverInstallResult> {
|
|
139
|
-
const projectRoot = resolve(options.projectRoot);
|
|
140
|
-
const driverName = assertDriverName(options.driverName ?? DEFAULT_EVENT_LOG_MERGE_DRIVER);
|
|
141
|
-
const rule = attributesRule(driverName);
|
|
142
|
-
const attributesPath = join(projectRoot, '.gitattributes');
|
|
143
|
-
const previous = await readTextIfExists(attributesPath);
|
|
144
|
-
const hadRule = attributesConfigured(previous, driverName);
|
|
145
|
-
let gitattributesUpdated = false;
|
|
146
|
-
|
|
147
|
-
if (!hadRule) {
|
|
148
|
-
const next = previous.trim().length === 0
|
|
149
|
-
? `${rule}\n`
|
|
150
|
-
: `${previous.replace(/\n*$/, '\n')}${rule}\n`;
|
|
151
|
-
await mkdir(dirname(attributesPath), { recursive: true });
|
|
152
|
-
await writeFile(attributesPath, next);
|
|
153
|
-
gitattributesUpdated = true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const warnings: string[] = [];
|
|
157
|
-
let gitConfigUpdated = false;
|
|
158
|
-
const gitConfigPresent = isGitWorktree(projectRoot);
|
|
159
|
-
if (!gitConfigPresent) {
|
|
160
|
-
warnings.push('project is not a Git worktree; local merge driver config was not written');
|
|
161
|
-
} else if (options.configureGit !== false) {
|
|
162
|
-
gitConfigSet(projectRoot, `merge.${driverName}.name`, 'Etiquette event log seq resequencer');
|
|
163
|
-
gitConfigSet(projectRoot, `merge.${driverName}.driver`, options.driverCommand ?? defaultDriverCommand());
|
|
164
|
-
gitConfigUpdated = true;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
schema: EVENT_LOG_MERGE_DRIVER_SCHEMA,
|
|
169
|
-
driver_name: driverName,
|
|
170
|
-
attributes_ref: '.gitattributes',
|
|
171
|
-
attributes_rule: rule,
|
|
172
|
-
gitattributes_updated: gitattributesUpdated,
|
|
173
|
-
git_config_updated: gitConfigUpdated,
|
|
174
|
-
git_config_present: gitConfigPresent,
|
|
175
|
-
warnings,
|
|
176
|
-
authority_boundary: authorityBoundary(),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function inspectEventLogMergeDriver(
|
|
181
|
-
options: EventLogMergeDriverStatusOptions,
|
|
182
|
-
): Promise<EventLogMergeDriverStatus> {
|
|
183
|
-
const projectRoot = resolve(options.projectRoot);
|
|
184
|
-
const driverName = assertDriverName(options.driverName ?? DEFAULT_EVENT_LOG_MERGE_DRIVER);
|
|
185
|
-
const attributes = await readTextIfExists(join(projectRoot, '.gitattributes'));
|
|
186
|
-
const attrReady = attributesConfigured(attributes, driverName);
|
|
187
|
-
const gitReady = isGitWorktree(projectRoot);
|
|
188
|
-
const gitConfigured = gitReady ? gitConfigConfigured(projectRoot, driverName) : false;
|
|
189
|
-
const warnings: string[] = [];
|
|
190
|
-
if (!attrReady) warnings.push('missing .gitattributes event-log merge rule');
|
|
191
|
-
if (!gitReady) warnings.push('project is not a Git worktree');
|
|
192
|
-
else if (!gitConfigured) warnings.push('missing local git merge driver config');
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
schema: EVENT_LOG_MERGE_DRIVER_STATUS_SCHEMA,
|
|
196
|
-
driver_name: driverName,
|
|
197
|
-
attributes_ref: '.gitattributes',
|
|
198
|
-
attributes_configured: attrReady,
|
|
199
|
-
git_config_present: gitReady,
|
|
200
|
-
git_config_configured: gitConfigured,
|
|
201
|
-
ready: attrReady && gitConfigured,
|
|
202
|
-
warnings,
|
|
203
|
-
authority_boundary: authorityBoundary(),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function readLines(path: string): string[] {
|
|
208
|
-
const text = readFileSync(path, 'utf8');
|
|
209
|
-
if (text === '') return [];
|
|
210
|
-
return text.replace(/\n+$/, '').split('\n');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function beginsWith(full: string[], prefix: string[]): boolean {
|
|
214
|
-
return prefix.length <= full.length && prefix.every((line, index) => full[index] === line);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function parseLine(line: string, label: string, index: number): Record<string, unknown> {
|
|
218
|
-
try {
|
|
219
|
-
const event = JSON.parse(line) as Record<string, unknown>;
|
|
220
|
-
if (!Number.isInteger(event.seq)) {
|
|
221
|
-
throw new Error('seq is not an integer');
|
|
222
|
-
}
|
|
223
|
-
return event;
|
|
224
|
-
} catch (err) {
|
|
225
|
-
throw new Error(`${label} appended line ${index + 1} is not a valid event: ${(err as Error).message}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function seqOfExisting(line: string): number | null {
|
|
230
|
-
try {
|
|
231
|
-
const event = JSON.parse(line) as { seq?: unknown };
|
|
232
|
-
return typeof event.seq === 'number' && Number.isInteger(event.seq) ? event.seq : null;
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function mergeAppendOnlyEventLogs(options: MergeAppendOnlyEventLogsOptions): void {
|
|
239
|
-
const ancestor = readLines(options.ancestorPath);
|
|
240
|
-
const current = readLines(options.currentPath);
|
|
241
|
-
const other = readLines(options.otherPath);
|
|
242
|
-
|
|
243
|
-
if (!beginsWith(current, ancestor) || !beginsWith(other, ancestor)) {
|
|
244
|
-
throw new Error('non-append edit detected; refusing automatic event-log merge');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const currentAppend = current.slice(ancestor.length);
|
|
248
|
-
const otherAppend = other.slice(ancestor.length);
|
|
249
|
-
currentAppend.forEach((line, index) => parseLine(line, 'current', index));
|
|
250
|
-
|
|
251
|
-
const kept = [...ancestor, ...currentAppend];
|
|
252
|
-
const keptSeqs = kept.map(seqOfExisting).filter((seq): seq is number => seq !== null);
|
|
253
|
-
let nextSeq = (keptSeqs.length ? Math.max(...keptSeqs) : 0) + 1;
|
|
254
|
-
|
|
255
|
-
const resequenced = otherAppend.map((line, index) => {
|
|
256
|
-
const event = parseLine(line, 'other', index);
|
|
257
|
-
event.seq = nextSeq;
|
|
258
|
-
nextSeq += 1;
|
|
259
|
-
return JSON.stringify(event);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
writeFileSync(options.currentPath, `${[...kept, ...resequenced].join('\n')}\n`);
|
|
263
|
-
}
|