@etiquekit/etq 1.0.12 → 1.0.14
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 +19 -26
- package/LICENSE +2 -3
- package/NOTICE +6 -4
- package/QuickStart.md +35 -47
- package/README.md +54 -54
- package/bin/etiquette +1 -5
- package/bin/etiquette-core +1 -5
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
- package/docs/CONCEPTS.md +7 -23
- package/docs/CONCEPT_STATUS.md +66 -0
- package/docs/CORE_PROFILE.md +3 -5
- package/docs/LANE_PROVISIONING.md +7 -2
- package/docs/README.md +4 -4
- package/docs/RELEASE_SURFACE_AUDIT.md +8 -7
- package/docs/SEAT_DISCIPLINE.md +55 -91
- package/docs/SEAT_PROVISIONING.md +19 -22
- package/docs/TEAM_HANDOFF.md +21 -21
- package/docs/WORKTREE_QOL.md +7 -6
- package/docs/contracts/ledger-entry/README.md +11 -11
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
- package/package.json +9 -9
- package/packages/control/src/authority/lease.ts +261 -0
- package/packages/control/src/authority/node-delegation.ts +257 -0
- package/packages/control/src/authority/rig-conductor.ts +632 -0
- package/packages/control/src/cli/argv.ts +155 -0
- package/packages/control/src/cli/commands/console.ts +200 -0
- package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
- package/packages/control/src/cli/commands/dispatch.ts +279 -0
- package/packages/control/src/cli/commands/harness.ts +89 -0
- package/packages/control/src/cli/commands/hook.ts +50 -0
- package/packages/control/src/cli/commands/ledger.ts +91 -0
- package/packages/control/src/cli/commands/local-workflow.ts +4690 -0
- package/packages/control/src/cli/commands/memory.ts +445 -0
- package/packages/control/src/cli/commands/release.ts +108 -0
- package/packages/control/src/cli/commands/rubric.ts +103 -0
- package/packages/control/src/cli/commands/seat.ts +179 -0
- package/packages/control/src/cli/commands/session.ts +127 -0
- package/packages/control/src/cli/commands/supervision.ts +246 -0
- package/packages/control/src/cli/commands/sync.ts +119 -0
- package/packages/control/src/cli/commands/workflow.ts +86 -0
- package/packages/control/src/cli/commands/workspace.ts +50 -0
- package/packages/control/src/cli/core-usage.ts +34 -0
- package/packages/control/src/cli/prompt.ts +67 -0
- package/packages/control/src/cli/supervision-deps.ts +44 -0
- package/packages/control/src/cli/usage.ts +241 -0
- package/packages/control/src/cli.ts +207 -0
- package/packages/control/src/core-cli.ts +50 -0
- package/packages/control/src/dispatch/decision.ts +202 -0
- package/packages/control/src/dispatch/projection.ts +293 -0
- package/packages/control/src/dispatch/record.ts +153 -0
- package/packages/control/src/engagement/project.ts +170 -0
- package/packages/control/src/fs.ts +19 -0
- package/packages/control/src/harness/pruning.ts +406 -0
- package/packages/control/src/hooks/dispatcher.ts +117 -0
- package/packages/control/src/hooks/outbox.ts +86 -0
- package/packages/control/src/hooks/sanitize.ts +6 -0
- package/packages/control/src/hooks/types.ts +34 -0
- package/packages/control/src/index.ts +384 -0
- package/packages/control/src/ledger/entry.ts +303 -0
- package/packages/control/src/ledger/indexer.ts +542 -0
- package/packages/control/src/memory/context.ts +149 -0
- package/packages/control/src/memory/drain-import.ts +207 -0
- package/packages/control/src/memory/indexer.ts +284 -0
- package/packages/control/src/memory/query.ts +75 -0
- package/packages/control/src/memory/sanitize.ts +50 -0
- package/packages/control/src/memory/sharded-drain-import.ts +212 -0
- package/packages/control/src/memory/status.ts +211 -0
- package/packages/control/src/memory/store-lifecycle.ts +509 -0
- package/packages/control/src/memory/store.ts +284 -0
- package/packages/control/src/memory/types.ts +146 -0
- package/packages/control/src/parity/surfaces.ts +748 -0
- package/packages/control/src/project.ts +141 -0
- package/packages/control/src/projection/local-ledger-view.ts +373 -0
- package/packages/control/src/projection/return-enforcement.ts +48 -0
- package/packages/control/src/projection/timeline-preview.ts +539 -0
- package/packages/control/src/projection/timeline.ts +708 -0
- package/packages/control/src/release/readiness.ts +842 -0
- package/packages/control/src/rubric/loader.ts +326 -0
- package/packages/control/src/rubric/promotion.ts +54 -0
- package/packages/control/src/rubric/runner.ts +159 -0
- package/packages/control/src/rubric/types.ts +158 -0
- package/packages/control/src/seat/owner-card.ts +388 -0
- package/packages/control/src/seat/readiness.ts +834 -0
- package/packages/control/src/session/runbook.ts +431 -0
- package/packages/control/src/shared/sanitize.ts +49 -0
- package/packages/control/src/supervision/action-classes.ts +192 -0
- package/packages/control/src/supervision/command-apply.ts +378 -0
- package/packages/control/src/supervision/errors.ts +14 -0
- package/packages/control/src/supervision/event-replay.ts +155 -0
- package/packages/control/src/supervision/events.ts +109 -0
- package/packages/control/src/supervision/index.ts +16 -0
- package/packages/control/src/supervision/manifest.ts +127 -0
- package/packages/control/src/supervision/paths.ts +49 -0
- package/packages/control/src/supervision/projection-adapter.ts +274 -0
- package/packages/control/src/supervision/projection.ts +75 -0
- package/packages/control/src/supervision/rebuild.ts +99 -0
- package/packages/control/src/supervision/session-open.ts +131 -0
- package/packages/control/src/supervision/session-read.ts +99 -0
- package/packages/control/src/supervision/sqlite-impl.ts +71 -0
- package/packages/control/src/supervision/sqlite.ts +121 -0
- package/packages/control/src/supervision/store-rows.ts +371 -0
- package/packages/control/src/supervision/turn-close.ts +154 -0
- package/packages/control/src/supervision/turn-open.ts +284 -0
- package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
- package/packages/control/src/sync/join-plan.ts +375 -0
- package/packages/control/src/sync/outbox.ts +492 -0
- package/packages/control/src/workflow/evaluator.ts +140 -0
- package/packages/control/src/workflow/loader.ts +200 -0
- package/packages/control/src/workflow/types.ts +90 -0
- package/packages/control/src/workspace/authority.ts +499 -0
- package/packages/protocol/src/guards.ts +119 -0
- package/packages/protocol/src/huddle-board.ts +198 -0
- package/packages/protocol/src/huddle.ts +295 -0
- package/packages/protocol/src/incident.ts +251 -0
- package/packages/protocol/src/index.ts +8 -0
- package/packages/protocol/src/interfaces.ts +107 -0
- package/packages/protocol/src/packet-profile.ts +195 -0
- package/packages/protocol/src/state.ts +81 -0
- package/packages/protocol/src/types.ts +434 -0
- package/release/lineage.v0.json +15 -0
- package/scripts/release-candidate-verify.sh +175 -0
- package/scripts/release-checksum.sh +25 -0
- package/scripts/release-pack-canary.sh +97 -0
- package/scripts/release-scan.sh +249 -0
- package/scripts/release-sign.sh +34 -0
- package/templates/etiquette-vanilla-v0/README.md +2 -3
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
- package/templates/seat-packs-v0/README.md +3 -2
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
- package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
- package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
- 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/AGENT_SPAWNING.md +0 -78
- package/docs/INTEGRATION_UI.md +0 -215
- package/docs/OPERATOR_PLAYBOOK.md +0 -77
- package/docs/TAG_ROUTE.md +0 -95
- package/lib/etiquette-core.js +0 -315
- package/lib/etiquette.js +0 -1160
- package/templates/DISPATCH_PREMISES.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
- package/templates/hosted-receiver/README.md +0 -41
- package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApprovedNextTurnPayload,
|
|
3
|
+
BlockerPayload,
|
|
4
|
+
SessionStatus,
|
|
5
|
+
SupervisionCommand,
|
|
6
|
+
SupervisionCommandRecord,
|
|
7
|
+
SupervisionDeps,
|
|
8
|
+
SupervisionSession,
|
|
9
|
+
} from '../../../protocol/src/index';
|
|
10
|
+
import { appendSupervisionEvent, buildEvent } from './events';
|
|
11
|
+
import { loadSeatManifest } from './manifest';
|
|
12
|
+
import { supervisionRuntimePaths } from './paths';
|
|
13
|
+
import { renderProjection, writeLatestState } from './projection';
|
|
14
|
+
import { applySupervisionSchema } from './sqlite';
|
|
15
|
+
import { withSupervisionTransaction } from './sqlite-impl';
|
|
16
|
+
import {
|
|
17
|
+
insertCommandRow,
|
|
18
|
+
insertEventRow,
|
|
19
|
+
loadCurrentTurn,
|
|
20
|
+
loadPendingCommands,
|
|
21
|
+
loadSession,
|
|
22
|
+
updateCommandApplied,
|
|
23
|
+
updateCommandRejected,
|
|
24
|
+
updateSessionApproval,
|
|
25
|
+
updateSessionBlocker,
|
|
26
|
+
updateSessionNextOwner,
|
|
27
|
+
updateSessionStatus,
|
|
28
|
+
} from './store-rows';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Supervision command — public entrypoint
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface SessionCommandArgs {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
campaignRoot: string;
|
|
37
|
+
command: SupervisionCommand;
|
|
38
|
+
actor: string;
|
|
39
|
+
payloadJson: string;
|
|
40
|
+
seatManifestOverride?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function issueSupervisionCommand(
|
|
44
|
+
args: SessionCommandArgs,
|
|
45
|
+
deps: SupervisionDeps,
|
|
46
|
+
): Promise<SupervisionCommandRecord> {
|
|
47
|
+
// Seat manifest load is a sanity check; per-command authorization beyond
|
|
48
|
+
// existence is currently a Phase 2 concern (no seat-gated commands yet).
|
|
49
|
+
await loadSeatManifest(args.seatManifestOverride);
|
|
50
|
+
|
|
51
|
+
const paths = supervisionRuntimePaths(args.campaignRoot);
|
|
52
|
+
const db = deps.openSqlite(paths.sqlitePath);
|
|
53
|
+
try {
|
|
54
|
+
applySupervisionSchema(db);
|
|
55
|
+
const session = loadSession(db, args.sessionId);
|
|
56
|
+
const now = deps.now().toISOString();
|
|
57
|
+
|
|
58
|
+
const commandId = deps.randomId();
|
|
59
|
+
const commandRecord: SupervisionCommandRecord = {
|
|
60
|
+
commandId,
|
|
61
|
+
sessionId: args.sessionId,
|
|
62
|
+
turnId: session.currentTurnId,
|
|
63
|
+
issuedBy: args.actor,
|
|
64
|
+
command: args.command,
|
|
65
|
+
payloadJson: args.payloadJson,
|
|
66
|
+
status: 'queued',
|
|
67
|
+
issuedAt: now,
|
|
68
|
+
appliedAt: null,
|
|
69
|
+
rejectionReason: null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const queuedEvent = buildEvent(deps, {
|
|
73
|
+
eventType: 'command.queued',
|
|
74
|
+
sessionId: args.sessionId,
|
|
75
|
+
turnId: session.currentTurnId,
|
|
76
|
+
commandId,
|
|
77
|
+
actor: args.actor,
|
|
78
|
+
audience: 'all',
|
|
79
|
+
at: now,
|
|
80
|
+
payload: {
|
|
81
|
+
command: args.command,
|
|
82
|
+
issuedBy: args.actor,
|
|
83
|
+
turnId: session.currentTurnId,
|
|
84
|
+
payloadJson: args.payloadJson,
|
|
85
|
+
issuedAt: now,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Apply is a pure function of the pre-command session state.
|
|
90
|
+
const outcome = applyCommand(session, args.command, args.payloadJson, now);
|
|
91
|
+
|
|
92
|
+
if (outcome.status === 'rejected') {
|
|
93
|
+
const reason = outcome.reason ?? 'unknown';
|
|
94
|
+
const rejectedEvent = buildEvent(deps, {
|
|
95
|
+
eventType: 'command.rejected',
|
|
96
|
+
sessionId: args.sessionId,
|
|
97
|
+
turnId: session.currentTurnId,
|
|
98
|
+
commandId,
|
|
99
|
+
actor: 'system',
|
|
100
|
+
audience: 'all',
|
|
101
|
+
at: now,
|
|
102
|
+
payload: { command: args.command, rejectedAt: now, reason },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Events first (canonical), then SQLite mirror in a single transaction.
|
|
106
|
+
await appendSupervisionEvent(paths.eventsPath, queuedEvent);
|
|
107
|
+
await appendSupervisionEvent(paths.eventsPath, rejectedEvent);
|
|
108
|
+
|
|
109
|
+
withSupervisionTransaction(db, () => {
|
|
110
|
+
insertCommandRow(db, commandRecord);
|
|
111
|
+
insertEventRow(db, queuedEvent);
|
|
112
|
+
updateCommandRejected(db, commandId, now, reason);
|
|
113
|
+
insertEventRow(db, rejectedEvent);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Per the Phase D addendum rule #4 (projection only on state change):
|
|
117
|
+
// rejection leaves session state unchanged — no projection re-render.
|
|
118
|
+
return {
|
|
119
|
+
...commandRecord,
|
|
120
|
+
status: 'rejected',
|
|
121
|
+
appliedAt: now,
|
|
122
|
+
rejectionReason: reason,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Applied path — pre-compute all events, append in order, then one txn.
|
|
127
|
+
const appliedEvent = buildEvent(deps, {
|
|
128
|
+
eventType: 'command.applied',
|
|
129
|
+
sessionId: args.sessionId,
|
|
130
|
+
turnId: session.currentTurnId,
|
|
131
|
+
commandId,
|
|
132
|
+
actor: 'system',
|
|
133
|
+
audience: 'all',
|
|
134
|
+
at: now,
|
|
135
|
+
payload: {
|
|
136
|
+
command: args.command,
|
|
137
|
+
appliedAt: now,
|
|
138
|
+
resultingState: outcome.resultingState ?? {},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const statusChanged = !!(outcome.statusTo && outcome.statusTo !== session.status);
|
|
143
|
+
const nextOwnerChanged =
|
|
144
|
+
outcome.nextOwnerTo !== undefined && outcome.nextOwnerTo !== session.nextOwnerHint;
|
|
145
|
+
|
|
146
|
+
const statusEvent = statusChanged
|
|
147
|
+
? buildEvent(deps, {
|
|
148
|
+
eventType: 'session.status_changed',
|
|
149
|
+
sessionId: args.sessionId,
|
|
150
|
+
turnId: null,
|
|
151
|
+
commandId,
|
|
152
|
+
actor: 'system',
|
|
153
|
+
audience: 'all',
|
|
154
|
+
at: now,
|
|
155
|
+
payload: {
|
|
156
|
+
from: session.status,
|
|
157
|
+
to: outcome.statusTo,
|
|
158
|
+
reason: `command:${args.command}`,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
: null;
|
|
162
|
+
const nextOwnerEvent = nextOwnerChanged
|
|
163
|
+
? buildEvent(deps, {
|
|
164
|
+
eventType: 'session.next_owner_changed',
|
|
165
|
+
sessionId: args.sessionId,
|
|
166
|
+
turnId: null,
|
|
167
|
+
commandId,
|
|
168
|
+
actor: 'system',
|
|
169
|
+
audience: 'all',
|
|
170
|
+
at: now,
|
|
171
|
+
payload: { from: session.nextOwnerHint, to: outcome.nextOwnerTo },
|
|
172
|
+
})
|
|
173
|
+
: null;
|
|
174
|
+
|
|
175
|
+
// Events first (canonical) in declared order, then one SQLite txn.
|
|
176
|
+
await appendSupervisionEvent(paths.eventsPath, queuedEvent);
|
|
177
|
+
await appendSupervisionEvent(paths.eventsPath, appliedEvent);
|
|
178
|
+
if (statusEvent) await appendSupervisionEvent(paths.eventsPath, statusEvent);
|
|
179
|
+
if (nextOwnerEvent) await appendSupervisionEvent(paths.eventsPath, nextOwnerEvent);
|
|
180
|
+
|
|
181
|
+
withSupervisionTransaction(db, () => {
|
|
182
|
+
insertCommandRow(db, commandRecord);
|
|
183
|
+
insertEventRow(db, queuedEvent);
|
|
184
|
+
updateCommandApplied(db, commandId, now);
|
|
185
|
+
insertEventRow(db, appliedEvent);
|
|
186
|
+
if (statusEvent && outcome.statusTo) {
|
|
187
|
+
updateSessionStatus(db, args.sessionId, outcome.statusTo, now);
|
|
188
|
+
insertEventRow(db, statusEvent);
|
|
189
|
+
}
|
|
190
|
+
if (nextOwnerEvent) {
|
|
191
|
+
updateSessionNextOwner(db, args.sessionId, outcome.nextOwnerTo ?? null, now);
|
|
192
|
+
insertEventRow(db, nextOwnerEvent);
|
|
193
|
+
}
|
|
194
|
+
if (outcome.resultingState?.approvedNextTurnJson !== undefined) {
|
|
195
|
+
updateSessionApproval(db, args.sessionId, outcome.resultingState.approvedNextTurnJson, now);
|
|
196
|
+
}
|
|
197
|
+
if (outcome.resultingState?.lastBlockerJson !== undefined) {
|
|
198
|
+
updateSessionBlocker(db, args.sessionId, outcome.resultingState.lastBlockerJson, now);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const sessionNow = loadSession(db, args.sessionId);
|
|
203
|
+
const applied: SupervisionCommandRecord = {
|
|
204
|
+
...commandRecord,
|
|
205
|
+
status: 'applied',
|
|
206
|
+
appliedAt: now,
|
|
207
|
+
};
|
|
208
|
+
await writeLatestState(paths.latestStatePath, sessionNow, loadCurrentTurn(db, sessionNow));
|
|
209
|
+
await renderProjection(
|
|
210
|
+
deps,
|
|
211
|
+
paths,
|
|
212
|
+
sessionNow,
|
|
213
|
+
loadCurrentTurn(db, sessionNow),
|
|
214
|
+
loadPendingCommands(db, args.sessionId),
|
|
215
|
+
);
|
|
216
|
+
return applied;
|
|
217
|
+
} finally {
|
|
218
|
+
db.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Command application (pure) — switch on `command`, validate payload,
|
|
224
|
+
// produce an outcome. No I/O, no state mutation beyond the returned plan.
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
interface CommandApplyOutcome {
|
|
228
|
+
status: 'applied' | 'rejected';
|
|
229
|
+
reason?: string;
|
|
230
|
+
statusTo?: SessionStatus;
|
|
231
|
+
nextOwnerTo?: string | null;
|
|
232
|
+
resultingState?: {
|
|
233
|
+
approvedNextTurnJson?: string | null;
|
|
234
|
+
lastBlockerJson?: string | null;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function applyCommand(
|
|
239
|
+
session: SupervisionSession,
|
|
240
|
+
command: SupervisionCommand,
|
|
241
|
+
payloadJson: string,
|
|
242
|
+
now: string,
|
|
243
|
+
): CommandApplyOutcome {
|
|
244
|
+
switch (command) {
|
|
245
|
+
case 'status':
|
|
246
|
+
return { status: 'applied' };
|
|
247
|
+
|
|
248
|
+
case 'pause':
|
|
249
|
+
if (session.status !== 'running') {
|
|
250
|
+
return {
|
|
251
|
+
status: 'rejected',
|
|
252
|
+
reason: `pause requires status=running (current=${session.status})`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return { status: 'applied', statusTo: 'paused' };
|
|
256
|
+
|
|
257
|
+
case 'resume':
|
|
258
|
+
if (session.status === 'paused') {
|
|
259
|
+
return { status: 'applied', statusTo: 'running' };
|
|
260
|
+
}
|
|
261
|
+
if (session.status === 'awaiting_human' && session.approvedNextTurnJson) {
|
|
262
|
+
return { status: 'applied', statusTo: 'running' };
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
status: 'rejected',
|
|
266
|
+
reason:
|
|
267
|
+
session.status === 'awaiting_human'
|
|
268
|
+
? 'resume from awaiting_human requires prior approve_next_turn'
|
|
269
|
+
: `resume requires status=paused or awaiting_human+approval (current=${session.status})`,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
case 'stop':
|
|
273
|
+
if (session.status === 'closed') {
|
|
274
|
+
return { status: 'rejected', reason: 'cannot stop a closed session' };
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
status: 'applied',
|
|
278
|
+
statusTo: 'awaiting_human',
|
|
279
|
+
resultingState: { approvedNextTurnJson: null },
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
case 'mark_blocked': {
|
|
283
|
+
const parsed = tryParse(payloadJson);
|
|
284
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
285
|
+
return { status: 'rejected', reason: 'mark_blocked requires a JSON object payload' };
|
|
286
|
+
}
|
|
287
|
+
const obj = parsed as Partial<BlockerPayload>;
|
|
288
|
+
if (!obj.reason || typeof obj.reason !== 'string') {
|
|
289
|
+
return { status: 'rejected', reason: 'mark_blocked payload requires "reason" string' };
|
|
290
|
+
}
|
|
291
|
+
const blocker: BlockerPayload = {
|
|
292
|
+
reason: obj.reason,
|
|
293
|
+
raisedBy: typeof obj.raisedBy === 'string' ? obj.raisedBy : 'unknown',
|
|
294
|
+
raisedAt: typeof obj.raisedAt === 'string' ? obj.raisedAt : now,
|
|
295
|
+
escalationOwner:
|
|
296
|
+
typeof obj.escalationOwner === 'string' ? obj.escalationOwner : session.escalationOwner,
|
|
297
|
+
evidenceRefs: Array.isArray(obj.evidenceRefs)
|
|
298
|
+
? obj.evidenceRefs.filter((v): v is string => typeof v === 'string')
|
|
299
|
+
: [],
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
status: 'applied',
|
|
303
|
+
statusTo: 'blocked',
|
|
304
|
+
resultingState: { lastBlockerJson: JSON.stringify(blocker) },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case 'request_summary':
|
|
309
|
+
return { status: 'applied' };
|
|
310
|
+
|
|
311
|
+
case 'redirect_owner': {
|
|
312
|
+
const parsed = tryParse(payloadJson);
|
|
313
|
+
const next = parsed && typeof parsed === 'object'
|
|
314
|
+
? (parsed as { nextOwnerHint?: unknown }).nextOwnerHint
|
|
315
|
+
: undefined;
|
|
316
|
+
if (next !== null && typeof next !== 'string') {
|
|
317
|
+
return {
|
|
318
|
+
status: 'rejected',
|
|
319
|
+
reason: 'redirect_owner requires { nextOwnerHint: string | null }',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { status: 'applied', nextOwnerTo: next as string | null };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'approve_next_turn': {
|
|
326
|
+
const parsed = tryParse(payloadJson);
|
|
327
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
328
|
+
return { status: 'rejected', reason: 'approve_next_turn requires a JSON object payload' };
|
|
329
|
+
}
|
|
330
|
+
const p = parsed as Partial<ApprovedNextTurnPayload>;
|
|
331
|
+
const requiredStrings: Array<keyof ApprovedNextTurnPayload> = [
|
|
332
|
+
'ownerSeat',
|
|
333
|
+
'allowedActionClass',
|
|
334
|
+
'projectionPolicy',
|
|
335
|
+
'issuedBy',
|
|
336
|
+
'issuedAt',
|
|
337
|
+
];
|
|
338
|
+
for (const key of requiredStrings) {
|
|
339
|
+
if (typeof p[key] !== 'string') {
|
|
340
|
+
return { status: 'rejected', reason: `approve_next_turn payload missing "${key}"` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (typeof p.maxDurationMinutes !== 'number') {
|
|
344
|
+
return { status: 'rejected', reason: 'approve_next_turn payload "maxDurationMinutes" must be a number' };
|
|
345
|
+
}
|
|
346
|
+
if (typeof p.requiresHumanApproval !== 'boolean') {
|
|
347
|
+
return { status: 'rejected', reason: 'approve_next_turn payload "requiresHumanApproval" must be a boolean' };
|
|
348
|
+
}
|
|
349
|
+
for (const listKey of ['allowedCommands', 'repoRoots', 'ownedPaths', 'forbiddenPaths', 'verifyCommands'] as const) {
|
|
350
|
+
if (!Array.isArray(p[listKey])) {
|
|
351
|
+
return { status: 'rejected', reason: `approve_next_turn payload "${listKey}" must be an array` };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const nextStatus: SessionStatus | undefined =
|
|
356
|
+
session.status === 'awaiting_human' ? 'running' : undefined;
|
|
357
|
+
return {
|
|
358
|
+
status: 'applied',
|
|
359
|
+
statusTo: nextStatus,
|
|
360
|
+
resultingState: { approvedNextTurnJson: payloadJson },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'escalate_to_human':
|
|
365
|
+
return { status: 'applied', statusTo: 'awaiting_human' };
|
|
366
|
+
|
|
367
|
+
default:
|
|
368
|
+
return { status: 'rejected', reason: `unknown command: ${String(command)}` };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function tryParse(raw: string): unknown {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(raw);
|
|
375
|
+
} catch {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervision runtime errors. Every thrown error from the runtime path uses
|
|
3
|
+
* this class plus a stable string code; callers (CLI, conformance tests)
|
|
4
|
+
* dispatch on the code.
|
|
5
|
+
*/
|
|
6
|
+
export class SupervisionRuntimeError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
readonly code: string,
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SupervisionRuntimeError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionStatus,
|
|
3
|
+
SupervisionCommand,
|
|
4
|
+
SupervisionDeps,
|
|
5
|
+
SupervisionEvent,
|
|
6
|
+
SupervisionSession,
|
|
7
|
+
SupervisionTurn,
|
|
8
|
+
TurnResult,
|
|
9
|
+
} from '../../../protocol/src/index';
|
|
10
|
+
import {
|
|
11
|
+
clearSessionCurrentTurn,
|
|
12
|
+
insertCommandRow,
|
|
13
|
+
insertEventRow,
|
|
14
|
+
insertSessionRow,
|
|
15
|
+
insertTurnRow,
|
|
16
|
+
updateCommandApplied,
|
|
17
|
+
updateCommandRejected,
|
|
18
|
+
updateSessionApproval,
|
|
19
|
+
updateSessionBlocker,
|
|
20
|
+
updateSessionCurrentTurn,
|
|
21
|
+
updateSessionNextOwner,
|
|
22
|
+
updateSessionStatus,
|
|
23
|
+
updateTurnOnClose,
|
|
24
|
+
} from './store-rows';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Replay a single supervision event against the SQLite mirror.
|
|
28
|
+
*
|
|
29
|
+
* Used by `rebuild.ts` when reconstructing SQLite from `events.jsonl` after
|
|
30
|
+
* a drop/rebuild cycle. Every event type recognized by the runtime must have
|
|
31
|
+
* a case here; unrecognized types are silently skipped (permissive read).
|
|
32
|
+
*
|
|
33
|
+
* After the switch, the event itself is inserted into the `events` table so
|
|
34
|
+
* the replayed database carries full event provenance.
|
|
35
|
+
*/
|
|
36
|
+
export function replayEvent(
|
|
37
|
+
db: ReturnType<SupervisionDeps['openSqlite']>,
|
|
38
|
+
event: SupervisionEvent,
|
|
39
|
+
): void {
|
|
40
|
+
switch (event.eventType) {
|
|
41
|
+
case 'session.opened': {
|
|
42
|
+
const s = event.payload as SupervisionSession;
|
|
43
|
+
insertSessionRow(db, s);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'session.status_changed': {
|
|
47
|
+
const p = event.payload as { from: SessionStatus; to: SessionStatus };
|
|
48
|
+
updateSessionStatus(db, event.sessionId, p.to, event.at);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'session.next_owner_changed': {
|
|
52
|
+
const p = event.payload as { from: string | null; to: string | null };
|
|
53
|
+
updateSessionNextOwner(db, event.sessionId, p.to, event.at);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'turn.opened': {
|
|
57
|
+
const t = event.payload as SupervisionTurn;
|
|
58
|
+
insertTurnRow(db, t);
|
|
59
|
+
updateSessionCurrentTurn(db, event.sessionId, t.turnId, event.at);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'turn.closed': {
|
|
63
|
+
const p = event.payload as {
|
|
64
|
+
endedAt: string;
|
|
65
|
+
result: TurnResult;
|
|
66
|
+
summary: string;
|
|
67
|
+
evidenceRefs: string[];
|
|
68
|
+
};
|
|
69
|
+
updateTurnOnClose(db, {
|
|
70
|
+
turnId: event.turnId ?? '',
|
|
71
|
+
sessionId: event.sessionId,
|
|
72
|
+
sequenceNo: 0, // unused on close update
|
|
73
|
+
status: 'closed',
|
|
74
|
+
ownerSeat: '', // unused on close update
|
|
75
|
+
allowedActionClass: 'coordination_only', // unused on close update
|
|
76
|
+
maxDurationMinutes: 0,
|
|
77
|
+
allowedCommands: [],
|
|
78
|
+
repoRoots: [],
|
|
79
|
+
ownedPaths: [],
|
|
80
|
+
forbiddenPaths: [],
|
|
81
|
+
verifyCommands: [],
|
|
82
|
+
requiresHumanApproval: false,
|
|
83
|
+
projectionPolicy: 'on_state_change',
|
|
84
|
+
startedAt: event.at,
|
|
85
|
+
endedAt: p.endedAt,
|
|
86
|
+
result: p.result,
|
|
87
|
+
summary: p.summary,
|
|
88
|
+
evidenceRefs: [...p.evidenceRefs],
|
|
89
|
+
});
|
|
90
|
+
clearSessionCurrentTurn(db, event.sessionId, event.at);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'command.queued': {
|
|
94
|
+
const p = event.payload as {
|
|
95
|
+
command: SupervisionCommand;
|
|
96
|
+
issuedBy: string;
|
|
97
|
+
turnId: string | null;
|
|
98
|
+
payloadJson: string;
|
|
99
|
+
issuedAt: string;
|
|
100
|
+
};
|
|
101
|
+
insertCommandRow(db, {
|
|
102
|
+
commandId: event.commandId ?? '',
|
|
103
|
+
sessionId: event.sessionId,
|
|
104
|
+
turnId: p.turnId,
|
|
105
|
+
issuedBy: p.issuedBy,
|
|
106
|
+
command: p.command,
|
|
107
|
+
payloadJson: p.payloadJson,
|
|
108
|
+
status: 'queued',
|
|
109
|
+
issuedAt: p.issuedAt,
|
|
110
|
+
appliedAt: null,
|
|
111
|
+
rejectionReason: null,
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'command.applied': {
|
|
116
|
+
const p = event.payload as {
|
|
117
|
+
command: SupervisionCommand;
|
|
118
|
+
appliedAt: string;
|
|
119
|
+
resultingState?: {
|
|
120
|
+
approvedNextTurnJson?: string | null;
|
|
121
|
+
lastBlockerJson?: string | null;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
updateCommandApplied(db, event.commandId ?? '', p.appliedAt);
|
|
125
|
+
if (p.resultingState?.approvedNextTurnJson !== undefined) {
|
|
126
|
+
updateSessionApproval(
|
|
127
|
+
db,
|
|
128
|
+
event.sessionId,
|
|
129
|
+
p.resultingState.approvedNextTurnJson,
|
|
130
|
+
p.appliedAt,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (p.resultingState?.lastBlockerJson !== undefined) {
|
|
134
|
+
updateSessionBlocker(
|
|
135
|
+
db,
|
|
136
|
+
event.sessionId,
|
|
137
|
+
p.resultingState.lastBlockerJson,
|
|
138
|
+
p.appliedAt,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'command.rejected': {
|
|
144
|
+
const p = event.payload as { rejectedAt: string; reason: string };
|
|
145
|
+
updateCommandRejected(db, event.commandId ?? '', p.rejectedAt, p.reason);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'projection.rendered':
|
|
149
|
+
case 'rebuild.completed':
|
|
150
|
+
// No DB state change.
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
insertEventRow(db, event);
|
|
155
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
SupervisionDeps,
|
|
5
|
+
SupervisionEvent,
|
|
6
|
+
SupervisionEventType,
|
|
7
|
+
} from '../../../protocol/src/index';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Canonical JSON serialization for event lines. Keys are emitted in a fixed
|
|
11
|
+
* order so two runs over the same event produce byte-identical output (this
|
|
12
|
+
* matters for rebuild determinism).
|
|
13
|
+
*/
|
|
14
|
+
export function serializeEventLine(event: SupervisionEvent): string {
|
|
15
|
+
const canonical = {
|
|
16
|
+
event_id: event.eventId,
|
|
17
|
+
session_id: event.sessionId,
|
|
18
|
+
turn_id: event.turnId,
|
|
19
|
+
command_id: event.commandId,
|
|
20
|
+
event_type: event.eventType,
|
|
21
|
+
actor: event.actor,
|
|
22
|
+
audience: event.audience,
|
|
23
|
+
at: event.at,
|
|
24
|
+
payload: event.payload ?? null,
|
|
25
|
+
};
|
|
26
|
+
return `${JSON.stringify(canonical)}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Append an event to the JSONL log. Creates the parent directory if missing.
|
|
31
|
+
* This is the authoritative write path for all supervision state changes
|
|
32
|
+
* per PHASE1.md; SQLite state is derived from these events and must be
|
|
33
|
+
* rebuildable from them alone.
|
|
34
|
+
*/
|
|
35
|
+
export async function appendSupervisionEvent(
|
|
36
|
+
eventsPath: string,
|
|
37
|
+
event: SupervisionEvent,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
await mkdir(dirname(eventsPath), { recursive: true });
|
|
40
|
+
await appendFile(eventsPath, serializeEventLine(event), 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read all events from the JSONL log, in file order. Used by session-rebuild
|
|
45
|
+
* and for test-side correctness checks.
|
|
46
|
+
*/
|
|
47
|
+
export async function readAllSupervisionEvents(eventsPath: string): Promise<SupervisionEvent[]> {
|
|
48
|
+
let raw: string;
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(eventsPath, 'utf-8');
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
56
|
+
return lines.map((line, idx) => {
|
|
57
|
+
let obj: Record<string, unknown>;
|
|
58
|
+
try {
|
|
59
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new Error(`events.jsonl line ${idx + 1} is not valid JSON: ${String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
eventId: String(obj.event_id),
|
|
65
|
+
sessionId: String(obj.session_id),
|
|
66
|
+
turnId: obj.turn_id === null || obj.turn_id === undefined ? null : String(obj.turn_id),
|
|
67
|
+
commandId: obj.command_id === null || obj.command_id === undefined ? null : String(obj.command_id),
|
|
68
|
+
eventType: obj.event_type as SupervisionEvent['eventType'],
|
|
69
|
+
actor: String(obj.actor),
|
|
70
|
+
audience: obj.audience as SupervisionEvent['audience'],
|
|
71
|
+
at: String(obj.at),
|
|
72
|
+
payload: obj.payload,
|
|
73
|
+
} satisfies SupervisionEvent;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a `SupervisionEvent` envelope from the caller-supplied fields plus a
|
|
79
|
+
* fresh `eventId` from `deps.randomId()`. Pure factory — no I/O.
|
|
80
|
+
*
|
|
81
|
+
* Used by every runtime writer (session-open, turn-open/close, command-apply,
|
|
82
|
+
* rebuild, session-read's `projection.rendered` emission) to construct events
|
|
83
|
+
* with a consistent shape before appending to `events.jsonl`.
|
|
84
|
+
*/
|
|
85
|
+
export function buildEvent(
|
|
86
|
+
deps: SupervisionDeps,
|
|
87
|
+
fields: {
|
|
88
|
+
eventType: SupervisionEventType;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
turnId: string | null;
|
|
91
|
+
commandId: string | null;
|
|
92
|
+
actor: string;
|
|
93
|
+
audience: SupervisionEvent['audience'];
|
|
94
|
+
at: string;
|
|
95
|
+
payload: unknown;
|
|
96
|
+
},
|
|
97
|
+
): SupervisionEvent {
|
|
98
|
+
return {
|
|
99
|
+
eventId: deps.randomId(),
|
|
100
|
+
sessionId: fields.sessionId,
|
|
101
|
+
turnId: fields.turnId,
|
|
102
|
+
commandId: fields.commandId,
|
|
103
|
+
eventType: fields.eventType,
|
|
104
|
+
actor: fields.actor,
|
|
105
|
+
audience: fields.audience,
|
|
106
|
+
at: fields.at,
|
|
107
|
+
payload: fields.payload,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './paths';
|
|
2
|
+
export * from './manifest';
|
|
3
|
+
export * from './action-classes';
|
|
4
|
+
export * from './events';
|
|
5
|
+
export * from './sqlite';
|
|
6
|
+
export * from './sqlite-impl';
|
|
7
|
+
// Split of the former monolithic `session.ts` per the Phase D.7 surgical
|
|
8
|
+
// refactor. Each of these exports the same public surface it previously
|
|
9
|
+
// shared via `./session`; barrel re-exports preserve caller imports.
|
|
10
|
+
export * from './errors';
|
|
11
|
+
export * from './session-open';
|
|
12
|
+
export * from './session-read';
|
|
13
|
+
export * from './turn-open';
|
|
14
|
+
export * from './turn-close';
|
|
15
|
+
export * from './command-apply';
|
|
16
|
+
export * from './rebuild';
|