@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.
Files changed (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -54
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +8 -7
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +9 -9
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4690 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +842 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +15 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,127 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import {
3
+ ACTION_CLASSES,
4
+ RUNTIME_KINDS,
5
+ type ActionClass,
6
+ type RuntimeKind,
7
+ type SeatCapabilityManifest,
8
+ } from '../../../protocol/src/index';
9
+ import { resolveSeatManifestPath } from './paths';
10
+
11
+ export class SeatManifestError extends Error {
12
+ constructor(
13
+ message: string,
14
+ readonly path: string,
15
+ readonly issues: string[],
16
+ ) {
17
+ super(`${message}: ${issues.join('; ')}`);
18
+ this.name = 'SeatManifestError';
19
+ }
20
+ }
21
+
22
+ function isString(v: unknown): v is string {
23
+ return typeof v === 'string';
24
+ }
25
+
26
+ function isBoolean(v: unknown): v is boolean {
27
+ return typeof v === 'boolean';
28
+ }
29
+
30
+ function isStringArray(v: unknown): v is string[] {
31
+ return Array.isArray(v) && v.every(isString);
32
+ }
33
+
34
+ function isActionClass(v: unknown): v is ActionClass {
35
+ return isString(v) && (ACTION_CLASSES as readonly string[]).includes(v);
36
+ }
37
+
38
+ function isRuntimeKind(v: unknown): v is RuntimeKind {
39
+ return isString(v) && (RUNTIME_KINDS as readonly string[]).includes(v);
40
+ }
41
+
42
+ /**
43
+ * Validate a parsed seat manifest object against the schema.
44
+ * Returns the validated manifest or throws `SeatManifestError` with every issue.
45
+ */
46
+ export function validateSeatManifest(raw: unknown, path: string): SeatCapabilityManifest {
47
+ const issues: string[] = [];
48
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
49
+ throw new SeatManifestError('manifest root is not an object', path, [
50
+ `got ${Array.isArray(raw) ? 'array' : typeof raw}`,
51
+ ]);
52
+ }
53
+ const obj = raw as Record<string, unknown>;
54
+
55
+ const required: Array<[string, (v: unknown) => boolean, string]> = [
56
+ ['schemaVersion', (v) => v === 'discipline-supervision-seat-capabilities.v1', 'must equal "discipline-supervision-seat-capabilities.v1"'],
57
+ ['seatId', isString, 'must be a non-empty string'],
58
+ ['displayName', isString, 'must be a string'],
59
+ ['runtimeKind', isRuntimeKind, `must be one of: ${RUNTIME_KINDS.join(', ')}`],
60
+ ['canReadHandshake', isBoolean, 'must be a boolean'],
61
+ ['canWriteHandshake', isBoolean, 'must be a boolean'],
62
+ ['canReadProductRepo', isBoolean, 'must be a boolean'],
63
+ ['canRunBuilds', isBoolean, 'must be a boolean'],
64
+ ['canWriteProductRepo', isBoolean, 'must be a boolean'],
65
+ ['canLaunchRuntimeTargets', isBoolean, 'must be a boolean'],
66
+ ['canAcceptMobileSteering', isBoolean, 'must be a boolean'],
67
+ ['defaultRequiresHumanApprovalForProductMutation', isBoolean, 'must be a boolean'],
68
+ ['updatedAt', isString, 'must be an ISO-8601 string'],
69
+ ];
70
+
71
+ for (const [key, predicate, msg] of required) {
72
+ if (!(key in obj)) {
73
+ issues.push(`${key} missing`);
74
+ continue;
75
+ }
76
+ if (!predicate(obj[key])) {
77
+ issues.push(`${key} invalid (${msg})`);
78
+ }
79
+ }
80
+
81
+ if ('seatId' in obj && isString(obj.seatId) && obj.seatId.trim().length === 0) {
82
+ issues.push('seatId must not be empty');
83
+ }
84
+
85
+ if (!('allowedActionClasses' in obj)) {
86
+ issues.push('allowedActionClasses missing');
87
+ } else if (!Array.isArray(obj.allowedActionClasses)) {
88
+ issues.push('allowedActionClasses must be an array');
89
+ } else {
90
+ const bad = obj.allowedActionClasses.filter((v) => !isActionClass(v));
91
+ if (bad.length > 0) {
92
+ issues.push(`allowedActionClasses contains unknown values: ${bad.map(String).join(', ')}`);
93
+ }
94
+ }
95
+
96
+ if (issues.length > 0) {
97
+ throw new SeatManifestError('seat manifest validation failed', path, issues);
98
+ }
99
+
100
+ return obj as unknown as SeatCapabilityManifest;
101
+ }
102
+
103
+ /**
104
+ * Read and validate a seat manifest from disk. The path defaults to the
105
+ * user-global location (~/.local/share/etiquette/seat-capabilities.json)
106
+ * unless overridden.
107
+ */
108
+ export async function loadSeatManifest(override?: string): Promise<SeatCapabilityManifest> {
109
+ const path = resolveSeatManifestPath(override);
110
+ let raw: string;
111
+ try {
112
+ raw = await readFile(path, 'utf-8');
113
+ } catch (err) {
114
+ throw new SeatManifestError(
115
+ 'seat manifest could not be read',
116
+ path,
117
+ [(err as NodeJS.ErrnoException).code ?? String(err)],
118
+ );
119
+ }
120
+ let parsed: unknown;
121
+ try {
122
+ parsed = JSON.parse(raw);
123
+ } catch (err) {
124
+ throw new SeatManifestError('seat manifest is not valid JSON', path, [String(err)]);
125
+ }
126
+ return validateSeatManifest(parsed, path);
127
+ }
@@ -0,0 +1,49 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ export const SUPERVISION_RUNTIME_SUBDIR = '.etiquette/runtime/supervision';
5
+
6
+ export interface SupervisionRuntimePaths {
7
+ campaignRoot: string;
8
+ runtimeDir: string;
9
+ sqlitePath: string;
10
+ eventsPath: string;
11
+ latestStatePath: string;
12
+ latestProjectionPath: string;
13
+ }
14
+
15
+ /**
16
+ * Resolve campaign-local supervision runtime paths. Per doctrine
17
+ * (llm-substrate/governance/shared-live-supervision.md), hot state lives at
18
+ * <campaign-root>/.etiquette/runtime/supervision/.
19
+ */
20
+ export function supervisionRuntimePaths(campaignRoot: string): SupervisionRuntimePaths {
21
+ const root = resolve(campaignRoot);
22
+ const runtimeDir = join(root, SUPERVISION_RUNTIME_SUBDIR);
23
+ return {
24
+ campaignRoot: root,
25
+ runtimeDir,
26
+ sqlitePath: join(runtimeDir, 'supervision.sqlite'),
27
+ eventsPath: join(runtimeDir, 'events.jsonl'),
28
+ latestStatePath: join(runtimeDir, 'latest-state.json'),
29
+ latestProjectionPath: join(runtimeDir, 'latest-projection.json'),
30
+ };
31
+ }
32
+
33
+ export const DEFAULT_SEAT_MANIFEST_PATH = join(
34
+ homedir(),
35
+ '.local',
36
+ 'share',
37
+ 'etiquette',
38
+ 'seat-capabilities.json',
39
+ );
40
+
41
+ /**
42
+ * Resolve the seat capability manifest path, honoring an optional override.
43
+ */
44
+ export function resolveSeatManifestPath(override?: string): string {
45
+ if (override && override.trim().length > 0) {
46
+ return resolve(override);
47
+ }
48
+ return DEFAULT_SEAT_MANIFEST_PATH;
49
+ }
@@ -0,0 +1,274 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import type {
4
+ SupervisionCommandRecord,
5
+ SupervisionEvent,
6
+ SupervisionProjection,
7
+ SupervisionProjectionAdapter,
8
+ SupervisionTurn,
9
+ } from '../../../protocol/src/index';
10
+ import { readAllSupervisionEvents } from './events';
11
+
12
+ /**
13
+ * File-system projection adapter for a campaign supervision session. This is
14
+ * intentionally in control so the public package does not need the legacy
15
+ * handshake adapter just to render local session projections.
16
+ */
17
+ export interface SupervisionProjectionConfig {
18
+ projectionDir: string;
19
+ eventsPath: string;
20
+ seatsOfInterest?: string[];
21
+ }
22
+
23
+ export function createSupervisionProjectionAdapter(
24
+ config: SupervisionProjectionConfig,
25
+ ): SupervisionProjectionAdapter {
26
+ const projectionDir = resolve(config.projectionDir);
27
+ const eventsPath = resolve(config.eventsPath);
28
+
29
+ const statusBoardPath = join(projectionDir, 'STATUS_BOARD.md');
30
+ const channelPath = (seatId: string): string => {
31
+ const sanitized = seatId.replace(/[^A-Za-z0-9._@-]/g, '_');
32
+ return join(projectionDir, `${sanitized}.channel.md`);
33
+ };
34
+
35
+ return {
36
+ async renderStatusBoard(projection: SupervisionProjection): Promise<void> {
37
+ const md = renderStatusBoardMarkdown(projection);
38
+ await mkdir(projectionDir, { recursive: true });
39
+ await writeFile(statusBoardPath, md, 'utf-8');
40
+ },
41
+
42
+ async renderSeatChannel(projection: SupervisionProjection, seatId: string): Promise<void> {
43
+ const events = await readAllSupervisionEvents(eventsPath);
44
+ const md = renderSeatChannelMarkdown(projection, seatId, events);
45
+ await mkdir(projectionDir, { recursive: true });
46
+ await writeFile(channelPath(seatId), md, 'utf-8');
47
+ },
48
+
49
+ async writeLatestProjection(
50
+ projectionPath: string,
51
+ projection: SupervisionProjection,
52
+ ): Promise<void> {
53
+ const canonical = canonicalizeProjection(projection);
54
+ await mkdir(dirname(projectionPath), { recursive: true });
55
+ await writeFile(projectionPath, `${JSON.stringify(canonical, null, 2)}\n`, 'utf-8');
56
+ },
57
+ };
58
+ }
59
+
60
+ export function defaultSeatsFromProjection(projection: SupervisionProjection): string[] {
61
+ const s = projection.sessionSnapshot;
62
+ return [...new Set([s.leadSeat, s.responderSeat, s.escalationOwner])];
63
+ }
64
+
65
+ function renderStatusBoardMarkdown(projection: SupervisionProjection): string {
66
+ const s = projection.sessionSnapshot;
67
+ const lines: string[] = [];
68
+ lines.push('# Supervision Status Board');
69
+ lines.push('');
70
+ lines.push('> Generated by etiquette supervision sidecar.');
71
+ lines.push('> Source of truth: `events.jsonl` in the campaign runtime.');
72
+ lines.push('> If this file and supervision state disagree, regenerate; do not hand-edit.');
73
+ lines.push('');
74
+ lines.push('## Session');
75
+ lines.push('');
76
+ lines.push(`- **session_id:** \`${s.sessionId}\``);
77
+ lines.push(`- **topic:** ${s.topic}`);
78
+ lines.push(`- **objective:** ${s.objective}`);
79
+ lines.push(`- **status:** \`${s.status}\``);
80
+ lines.push(`- **lead_seat:** \`${s.leadSeat}\``);
81
+ lines.push(`- **responder_seat:** \`${s.responderSeat}\``);
82
+ lines.push(`- **escalation_owner:** \`${s.escalationOwner}\``);
83
+ lines.push(`- **time_cap_minutes:** ${s.timeCapMinutes}`);
84
+ lines.push(`- **turn_cap:** ${s.turnCap}`);
85
+ if (s.nextOwnerHint) {
86
+ lines.push(`- **next_owner_hint:** \`${s.nextOwnerHint}\``);
87
+ }
88
+ if (s.approvedNextTurnJson) {
89
+ lines.push('- **approved_next_turn:** present (see events.jsonl for payload)');
90
+ }
91
+ lines.push(`- **created_at:** ${s.createdAt}`);
92
+ lines.push(`- **updated_at:** ${s.updatedAt}`);
93
+ if (s.sourceRefs.length > 0) {
94
+ lines.push('- **source_refs:**');
95
+ for (const ref of [...s.sourceRefs].sort()) {
96
+ lines.push(` - ${ref}`);
97
+ }
98
+ }
99
+ lines.push('');
100
+ lines.push('## Current turn');
101
+ lines.push('');
102
+ if (projection.currentTurn) {
103
+ renderTurn(lines, projection.currentTurn);
104
+ } else {
105
+ lines.push('_no open turn_');
106
+ }
107
+ lines.push('');
108
+ lines.push('## Pending commands');
109
+ lines.push('');
110
+ if (projection.pendingCommands.length === 0) {
111
+ lines.push('_none_');
112
+ } else {
113
+ for (const c of [...projection.pendingCommands].sort((a, b) => a.issuedAt.localeCompare(b.issuedAt))) {
114
+ renderCommand(lines, c);
115
+ }
116
+ }
117
+ if (s.lastBlockerJson) {
118
+ lines.push('');
119
+ lines.push('## Last blocker');
120
+ lines.push('');
121
+ lines.push('```json');
122
+ lines.push(formatJson(s.lastBlockerJson));
123
+ lines.push('```');
124
+ }
125
+ lines.push('');
126
+ lines.push('---');
127
+ lines.push(`Rendered at ${projection.renderedAt} by the supervision sidecar.`);
128
+ lines.push('Divergence rule: patch this file by regenerating, not by hand-edit.');
129
+ lines.push('');
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function renderTurn(lines: string[], turn: SupervisionTurn): void {
134
+ lines.push(`- **turn_id:** \`${turn.turnId}\``);
135
+ lines.push(`- **sequence_no:** ${turn.sequenceNo}`);
136
+ lines.push(`- **status:** \`${turn.status}\``);
137
+ lines.push(`- **owner:** \`${turn.ownerSeat}\``);
138
+ lines.push(`- **action_class:** \`${turn.allowedActionClass}\``);
139
+ lines.push(`- **max_duration_minutes:** ${turn.maxDurationMinutes}`);
140
+ lines.push(`- **allowed_commands:** [${[...turn.allowedCommands].sort().join(', ')}]`);
141
+ if (turn.repoRoots.length > 0) {
142
+ lines.push(`- **repo_roots:** [${[...turn.repoRoots].sort().join(', ')}]`);
143
+ }
144
+ if (turn.ownedPaths.length > 0) {
145
+ lines.push(`- **owned_paths:** [${[...turn.ownedPaths].sort().join(', ')}]`);
146
+ }
147
+ if (turn.forbiddenPaths.length > 0) {
148
+ lines.push(`- **forbidden_paths:** [${[...turn.forbiddenPaths].sort().join(', ')}]`);
149
+ }
150
+ if (turn.verifyCommands.length > 0) {
151
+ lines.push(`- **verify_commands:** [${[...turn.verifyCommands].sort().join(', ')}]`);
152
+ }
153
+ lines.push(`- **requires_human_approval:** ${turn.requiresHumanApproval}`);
154
+ lines.push(`- **projection_policy:** \`${turn.projectionPolicy}\``);
155
+ lines.push(`- **started_at:** ${turn.startedAt}`);
156
+ }
157
+
158
+ function renderCommand(lines: string[], command: SupervisionCommandRecord): void {
159
+ lines.push(`- \`${command.command}\` by \`${command.issuedBy}\` at ${command.issuedAt}`);
160
+ }
161
+
162
+ function renderSeatChannelMarkdown(
163
+ projection: SupervisionProjection,
164
+ seatId: string,
165
+ events: SupervisionEvent[],
166
+ ): string {
167
+ const s = projection.sessionSnapshot;
168
+ const seatAudience = `seat:${seatId}` as const;
169
+ const roles: string[] = [];
170
+ if (s.leadSeat === seatId) roles.push('lead');
171
+ if (s.responderSeat === seatId) roles.push('responder');
172
+ if (s.escalationOwner === seatId) roles.push('escalation_owner');
173
+
174
+ const metaEventTypes = new Set(['projection.rendered', 'rebuild.completed']);
175
+ const filtered = events.filter((e) => {
176
+ if (metaEventTypes.has(e.eventType)) return false;
177
+ if (e.audience === 'all') return true;
178
+ if (e.audience === seatAudience) return true;
179
+ if (e.audience === 'lead' && roles.includes('lead')) return true;
180
+ if (e.audience === 'responder' && roles.includes('responder')) return true;
181
+ if (e.audience === 'escalation_owner' && roles.includes('escalation_owner')) return true;
182
+ return false;
183
+ });
184
+
185
+ const lines: string[] = [];
186
+ lines.push(`# ${seatId} - Supervision Channel`);
187
+ lines.push('');
188
+ lines.push(`- **session_id:** \`${s.sessionId}\``);
189
+ lines.push(`- **session_topic:** ${s.topic}`);
190
+ lines.push(`- **seat_roles_in_session:** ${roles.length > 0 ? roles.join(', ') : '_none_'}`);
191
+ lines.push(`- **generated_at:** ${projection.renderedAt}`);
192
+ lines.push('');
193
+ lines.push('> Auto-generated projection. Do not hand-edit.');
194
+ lines.push('> Authoritative source: `events.jsonl` filtered by seat audience.');
195
+ lines.push('> Regeneration overwrites this file on every state change.');
196
+ lines.push('');
197
+ lines.push(`## Event log (${filtered.length} entries)`);
198
+ lines.push('');
199
+ if (filtered.length === 0) {
200
+ lines.push('_no events addressed to this seat yet_');
201
+ } else {
202
+ for (const e of filtered) {
203
+ lines.push(`- \`${e.at}\` — \`${e.eventType}\` — ${formatEventSummary(e)}`);
204
+ }
205
+ }
206
+ lines.push('');
207
+ return lines.join('\n');
208
+ }
209
+
210
+ function formatEventSummary(event: SupervisionEvent): string {
211
+ switch (event.eventType) {
212
+ case 'session.opened':
213
+ return `session opened by ${event.actor}`;
214
+ case 'session.status_changed': {
215
+ const p = (event.payload as { from?: string; to?: string }) ?? {};
216
+ return `status ${p.from ?? '?'} -> ${p.to ?? '?'}`;
217
+ }
218
+ case 'session.next_owner_changed': {
219
+ const p = (event.payload as { from?: string | null; to?: string | null }) ?? {};
220
+ return `next_owner_hint ${p.from ?? 'null'} -> ${p.to ?? 'null'}`;
221
+ }
222
+ case 'turn.opened': {
223
+ const p = (event.payload as { ownerSeat?: string; allowedActionClass?: string }) ?? {};
224
+ return `turn opened for ${p.ownerSeat ?? '?'} (${p.allowedActionClass ?? '?'})`;
225
+ }
226
+ case 'turn.closed': {
227
+ const p = (event.payload as { result?: string; summary?: string }) ?? {};
228
+ return `turn closed (${p.result ?? '?'}): ${p.summary ?? ''}`;
229
+ }
230
+ case 'command.queued': {
231
+ const p = (event.payload as { command?: string; issuedBy?: string }) ?? {};
232
+ return `${p.command ?? '?'} queued by ${p.issuedBy ?? '?'}`;
233
+ }
234
+ case 'command.applied': {
235
+ const p = (event.payload as { command?: string }) ?? {};
236
+ return `${p.command ?? '?'} applied`;
237
+ }
238
+ case 'command.rejected': {
239
+ const p = (event.payload as { command?: string; reason?: string }) ?? {};
240
+ return `${p.command ?? '?'} rejected: ${p.reason ?? ''}`;
241
+ }
242
+ case 'projection.rendered':
243
+ return 'projection rendered';
244
+ case 'rebuild.completed': {
245
+ const p = (event.payload as { eventCount?: number }) ?? {};
246
+ return `rebuild completed (${p.eventCount ?? '?'} events replayed)`;
247
+ }
248
+ default:
249
+ return `(event type ${String(event.eventType)})`;
250
+ }
251
+ }
252
+
253
+ function formatJson(raw: string): string {
254
+ try {
255
+ return JSON.stringify(JSON.parse(raw), null, 2);
256
+ } catch {
257
+ return raw;
258
+ }
259
+ }
260
+
261
+ function canonicalizeProjection(projection: SupervisionProjection): SupervisionProjection {
262
+ const sortedSourceRefs = [...projection.sessionSnapshot.sourceRefs].sort();
263
+ const sortedPendingCommands = [...projection.pendingCommands].sort((a, b) =>
264
+ a.issuedAt.localeCompare(b.issuedAt) || a.commandId.localeCompare(b.commandId),
265
+ );
266
+ return {
267
+ ...projection,
268
+ sessionSnapshot: {
269
+ ...projection.sessionSnapshot,
270
+ sourceRefs: sortedSourceRefs,
271
+ },
272
+ pendingCommands: sortedPendingCommands,
273
+ };
274
+ }
@@ -0,0 +1,75 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type {
4
+ SupervisionCommandRecord,
5
+ SupervisionDeps,
6
+ SupervisionProjection,
7
+ SupervisionSession,
8
+ SupervisionTurn,
9
+ } from '../../../protocol/src/index';
10
+ import type { supervisionRuntimePaths } from './paths';
11
+
12
+ /**
13
+ * Projection coordination — internal helpers shared by every runtime writer.
14
+ *
15
+ * These were previously inline in `session.ts`. Extracted per the Phase D.7
16
+ * surgical split so the public-entrypoint modules (session-open, turn-open,
17
+ * turn-close, command-apply, rebuild, session-read) can import a single
18
+ * shared implementation instead of duplicating coordination logic.
19
+ *
20
+ * No public export surface — callers remain the runtime modules within this
21
+ * directory. Not re-exported from `supervision/index.ts`.
22
+ */
23
+
24
+ /**
25
+ * Write `{ session, currentTurn }` to disk as pretty-printed JSON.
26
+ *
27
+ * Used after every state-mutating runtime operation so the latest-state.json
28
+ * artifact stays in sync with SQLite.
29
+ */
30
+ export async function writeLatestState(
31
+ path: string,
32
+ session: SupervisionSession,
33
+ currentTurn: SupervisionTurn | null,
34
+ ): Promise<void> {
35
+ const body = JSON.stringify({ session, currentTurn }, null, 2);
36
+ await mkdir(join(path, '..'), { recursive: true });
37
+ await writeFile(path, `${body}\n`, 'utf-8');
38
+ }
39
+
40
+ /**
41
+ * Build a `SupervisionProjection` snapshot and drive the configured
42
+ * projection adapter through its three render hooks:
43
+ * - `writeLatestProjection` (machine-readable snapshot)
44
+ * - `renderStatusBoard` (single STATUS_BOARD.md for the campaign)
45
+ * - `renderSeatChannel` for each of lead / responder / escalation_owner
46
+ * (de-duplicated; a seat can play multiple roles in the same session)
47
+ *
48
+ * Per PHASE1.md § Projection rules, this runs on every state change and is
49
+ * idempotent — safe to rerun after partial failure.
50
+ */
51
+ export async function renderProjection(
52
+ deps: SupervisionDeps,
53
+ paths: ReturnType<typeof supervisionRuntimePaths>,
54
+ session: SupervisionSession,
55
+ currentTurn: SupervisionTurn | null,
56
+ pendingCommands: SupervisionCommandRecord[],
57
+ ): Promise<SupervisionProjection> {
58
+ const projection: SupervisionProjection = {
59
+ sessionId: session.sessionId,
60
+ renderedAt: deps.now().toISOString(),
61
+ sessionSnapshot: session,
62
+ currentTurn,
63
+ pendingCommands,
64
+ };
65
+
66
+ await deps.projectionAdapter.writeLatestProjection(paths.latestProjectionPath, projection);
67
+ await deps.projectionAdapter.renderStatusBoard(projection);
68
+
69
+ const seats = new Set([session.leadSeat, session.responderSeat, session.escalationOwner]);
70
+ for (const seatId of seats) {
71
+ await deps.projectionAdapter.renderSeatChannel(projection, seatId);
72
+ }
73
+
74
+ return projection;
75
+ }
@@ -0,0 +1,99 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { rm } from 'node:fs/promises';
3
+ import type {
4
+ SupervisionDeps,
5
+ SupervisionProjection,
6
+ } from '../../../protocol/src/index';
7
+ import {
8
+ appendSupervisionEvent,
9
+ buildEvent,
10
+ readAllSupervisionEvents,
11
+ } from './events';
12
+ import { replayEvent } from './event-replay';
13
+ import { supervisionRuntimePaths } from './paths';
14
+ import { renderProjection, writeLatestState } from './projection';
15
+ import { applySupervisionSchema } from './sqlite';
16
+ import { withSupervisionTransaction } from './sqlite-impl';
17
+ import {
18
+ insertEventRow,
19
+ loadCurrentTurn,
20
+ loadPendingCommands,
21
+ loadSession,
22
+ } from './store-rows';
23
+
24
+ export interface SessionRebuildArgs {
25
+ sessionId: string;
26
+ campaignRoot: string;
27
+ }
28
+
29
+ /**
30
+ * Drop the SQLite mirror (including WAL + SHM siblings), then replay
31
+ * `events.jsonl` into a fresh schema. All replays for the session run
32
+ * inside a single transaction so a partial rebuild cannot leave a
33
+ * half-populated mirror visible to concurrent readers.
34
+ *
35
+ * Final step: append a `rebuild.completed` event so history remains
36
+ * self-describing.
37
+ *
38
+ * Per PHASE1.md § Session-rebuild contract: after this call, SQLite state
39
+ * MUST equal the pre-rebuild state for any durable session.
40
+ */
41
+ export async function rebuildSupervisionSession(
42
+ args: SessionRebuildArgs,
43
+ deps: SupervisionDeps,
44
+ ): Promise<SupervisionProjection> {
45
+ const paths = supervisionRuntimePaths(args.campaignRoot);
46
+
47
+ if (existsSync(paths.sqlitePath)) {
48
+ await rm(paths.sqlitePath, { force: true });
49
+ }
50
+ // Remove WAL + SHM siblings too.
51
+ await rm(`${paths.sqlitePath}-wal`, { force: true });
52
+ await rm(`${paths.sqlitePath}-shm`, { force: true });
53
+
54
+ const events = await readAllSupervisionEvents(paths.eventsPath);
55
+ const db = deps.openSqlite(paths.sqlitePath);
56
+ try {
57
+ applySupervisionSchema(db);
58
+
59
+ // Replay is idempotent per event but inserts many rows; group the whole
60
+ // replay as one atomic bundle so a partial rebuild cannot leave a
61
+ // half-populated SQLite mirror visible to concurrent readers.
62
+ const eventCount = withSupervisionTransaction(db, () => {
63
+ let count = 0;
64
+ for (const event of events) {
65
+ if (event.sessionId !== args.sessionId) continue;
66
+ replayEvent(db, event);
67
+ count += 1;
68
+ }
69
+ return count;
70
+ });
71
+
72
+ const session = loadSession(db, args.sessionId);
73
+ const currentTurn = loadCurrentTurn(db, session);
74
+ const pending = loadPendingCommands(db, args.sessionId);
75
+
76
+ await writeLatestState(paths.latestStatePath, session, currentTurn);
77
+ const projection = await renderProjection(deps, paths, session, currentTurn, pending);
78
+
79
+ const completedAt = deps.now().toISOString();
80
+ const completed = buildEvent(deps, {
81
+ eventType: 'rebuild.completed',
82
+ sessionId: args.sessionId,
83
+ turnId: session.currentTurnId,
84
+ commandId: null,
85
+ actor: 'system',
86
+ audience: 'all',
87
+ at: completedAt,
88
+ payload: { completedAt, eventCount },
89
+ });
90
+ await appendSupervisionEvent(paths.eventsPath, completed);
91
+ withSupervisionTransaction(db, () => {
92
+ insertEventRow(db, completed);
93
+ });
94
+
95
+ return projection;
96
+ } finally {
97
+ db.close();
98
+ }
99
+ }