@etiquekit/etq 1.0.13 → 1.0.15

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 (143) hide show
  1. package/AGENTS.md +26 -19
  2. package/LICENSE +3 -2
  3. package/NOTICE +4 -6
  4. package/QuickStart.md +46 -35
  5. package/README.md +57 -55
  6. package/bin/etiquette +5 -1
  7. package/bin/etiquette-core +5 -1
  8. package/docs/ARCHITECTURE.md +17 -20
  9. package/docs/CONCEPTS.md +5 -5
  10. package/docs/CORE_PROFILE.md +15 -7
  11. package/docs/LANE_PROVISIONING.md +2 -7
  12. package/docs/README.md +14 -13
  13. package/docs/RELEASE_SURFACE_AUDIT.md +6 -7
  14. package/docs/SEAT_DISCIPLINE.md +91 -55
  15. package/docs/SEAT_PROVISIONING.md +19 -16
  16. package/docs/TEAM_HANDOFF.md +21 -21
  17. package/docs/WORKTREE_QOL.md +0 -7
  18. package/docs/contracts/ledger-entry/README.md +11 -11
  19. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +2 -2
  20. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  21. package/lib/etiquette-core.js +313 -0
  22. package/lib/etiquette.js +1151 -0
  23. package/package.json +13 -12
  24. package/templates/etiquette-vanilla-v0/README.md +3 -2
  25. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +6 -0
  26. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
  27. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +5 -0
  28. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
  29. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
  30. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +5 -0
  31. package/templates/hosted-receiver/README.md +41 -0
  32. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
  33. package/templates/seat-packs-v0/README.md +2 -3
  34. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -3
  35. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -3
  36. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -3
  37. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  38. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  39. package/docs/CONCEPT_STATUS.md +0 -66
  40. package/packages/control/src/authority/lease.ts +0 -261
  41. package/packages/control/src/authority/node-delegation.ts +0 -257
  42. package/packages/control/src/authority/rig-conductor.ts +0 -632
  43. package/packages/control/src/cli/argv.ts +0 -155
  44. package/packages/control/src/cli/commands/console.ts +0 -200
  45. package/packages/control/src/cli/commands/dispatch-core.ts +0 -89
  46. package/packages/control/src/cli/commands/dispatch.ts +0 -279
  47. package/packages/control/src/cli/commands/harness.ts +0 -89
  48. package/packages/control/src/cli/commands/hook.ts +0 -50
  49. package/packages/control/src/cli/commands/ledger.ts +0 -91
  50. package/packages/control/src/cli/commands/local-workflow.ts +0 -4686
  51. package/packages/control/src/cli/commands/memory.ts +0 -445
  52. package/packages/control/src/cli/commands/release.ts +0 -108
  53. package/packages/control/src/cli/commands/rubric.ts +0 -103
  54. package/packages/control/src/cli/commands/seat.ts +0 -179
  55. package/packages/control/src/cli/commands/session.ts +0 -127
  56. package/packages/control/src/cli/commands/supervision.ts +0 -246
  57. package/packages/control/src/cli/commands/sync.ts +0 -119
  58. package/packages/control/src/cli/commands/workflow.ts +0 -86
  59. package/packages/control/src/cli/commands/workspace.ts +0 -50
  60. package/packages/control/src/cli/core-usage.ts +0 -34
  61. package/packages/control/src/cli/prompt.ts +0 -67
  62. package/packages/control/src/cli/supervision-deps.ts +0 -44
  63. package/packages/control/src/cli/usage.ts +0 -241
  64. package/packages/control/src/cli.ts +0 -207
  65. package/packages/control/src/core-cli.ts +0 -50
  66. package/packages/control/src/dispatch/decision.ts +0 -202
  67. package/packages/control/src/dispatch/projection.ts +0 -293
  68. package/packages/control/src/dispatch/record.ts +0 -153
  69. package/packages/control/src/engagement/project.ts +0 -170
  70. package/packages/control/src/fs.ts +0 -19
  71. package/packages/control/src/harness/pruning.ts +0 -406
  72. package/packages/control/src/hooks/dispatcher.ts +0 -117
  73. package/packages/control/src/hooks/outbox.ts +0 -86
  74. package/packages/control/src/hooks/sanitize.ts +0 -6
  75. package/packages/control/src/hooks/types.ts +0 -34
  76. package/packages/control/src/index.ts +0 -384
  77. package/packages/control/src/ledger/entry.ts +0 -303
  78. package/packages/control/src/ledger/indexer.ts +0 -542
  79. package/packages/control/src/memory/context.ts +0 -149
  80. package/packages/control/src/memory/drain-import.ts +0 -207
  81. package/packages/control/src/memory/indexer.ts +0 -284
  82. package/packages/control/src/memory/query.ts +0 -75
  83. package/packages/control/src/memory/sanitize.ts +0 -50
  84. package/packages/control/src/memory/sharded-drain-import.ts +0 -212
  85. package/packages/control/src/memory/status.ts +0 -211
  86. package/packages/control/src/memory/store-lifecycle.ts +0 -509
  87. package/packages/control/src/memory/store.ts +0 -284
  88. package/packages/control/src/memory/types.ts +0 -146
  89. package/packages/control/src/parity/surfaces.ts +0 -748
  90. package/packages/control/src/project.ts +0 -141
  91. package/packages/control/src/projection/local-ledger-view.ts +0 -373
  92. package/packages/control/src/projection/return-enforcement.ts +0 -48
  93. package/packages/control/src/projection/timeline-preview.ts +0 -539
  94. package/packages/control/src/projection/timeline.ts +0 -708
  95. package/packages/control/src/release/readiness.ts +0 -831
  96. package/packages/control/src/rubric/loader.ts +0 -326
  97. package/packages/control/src/rubric/promotion.ts +0 -54
  98. package/packages/control/src/rubric/runner.ts +0 -159
  99. package/packages/control/src/rubric/types.ts +0 -158
  100. package/packages/control/src/seat/owner-card.ts +0 -388
  101. package/packages/control/src/seat/readiness.ts +0 -834
  102. package/packages/control/src/session/runbook.ts +0 -431
  103. package/packages/control/src/shared/sanitize.ts +0 -49
  104. package/packages/control/src/supervision/action-classes.ts +0 -192
  105. package/packages/control/src/supervision/command-apply.ts +0 -378
  106. package/packages/control/src/supervision/errors.ts +0 -14
  107. package/packages/control/src/supervision/event-replay.ts +0 -155
  108. package/packages/control/src/supervision/events.ts +0 -109
  109. package/packages/control/src/supervision/index.ts +0 -16
  110. package/packages/control/src/supervision/manifest.ts +0 -127
  111. package/packages/control/src/supervision/paths.ts +0 -49
  112. package/packages/control/src/supervision/projection-adapter.ts +0 -274
  113. package/packages/control/src/supervision/projection.ts +0 -75
  114. package/packages/control/src/supervision/rebuild.ts +0 -99
  115. package/packages/control/src/supervision/session-open.ts +0 -131
  116. package/packages/control/src/supervision/session-read.ts +0 -99
  117. package/packages/control/src/supervision/sqlite-impl.ts +0 -71
  118. package/packages/control/src/supervision/sqlite.ts +0 -121
  119. package/packages/control/src/supervision/store-rows.ts +0 -371
  120. package/packages/control/src/supervision/turn-close.ts +0 -154
  121. package/packages/control/src/supervision/turn-open.ts +0 -284
  122. package/packages/control/src/sync/event-log-merge-driver.ts +0 -263
  123. package/packages/control/src/sync/join-plan.ts +0 -375
  124. package/packages/control/src/sync/outbox.ts +0 -492
  125. package/packages/control/src/workflow/evaluator.ts +0 -140
  126. package/packages/control/src/workflow/loader.ts +0 -200
  127. package/packages/control/src/workflow/types.ts +0 -90
  128. package/packages/control/src/workspace/authority.ts +0 -499
  129. package/packages/protocol/src/guards.ts +0 -119
  130. package/packages/protocol/src/huddle-board.ts +0 -198
  131. package/packages/protocol/src/huddle.ts +0 -295
  132. package/packages/protocol/src/incident.ts +0 -251
  133. package/packages/protocol/src/index.ts +0 -8
  134. package/packages/protocol/src/interfaces.ts +0 -107
  135. package/packages/protocol/src/packet-profile.ts +0 -195
  136. package/packages/protocol/src/state.ts +0 -81
  137. package/packages/protocol/src/types.ts +0 -434
  138. package/release/lineage.v0.json +0 -14
  139. package/scripts/release-candidate-verify.sh +0 -175
  140. package/scripts/release-checksum.sh +0 -25
  141. package/scripts/release-pack-canary.sh +0 -97
  142. package/scripts/release-scan.sh +0 -249
  143. package/scripts/release-sign.sh +0 -34
@@ -1,431 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
3
- import { appendFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
4
- import { dirname, join, relative, resolve } from 'node:path';
5
- import { assertSessionProject, ensureProjectBinding } from '../engagement/project';
6
- import { assertSharedMemoryScopeId, assertSharedMemoryText } from '../shared/sanitize';
7
-
8
- export const SESSION_NOTE_SECTIONS = ['plan', 'tool', 'retry', 'evidence', 'note'] as const;
9
-
10
- export type SessionNoteSection = (typeof SESSION_NOTE_SECTIONS)[number];
11
-
12
- export interface SessionRunbookManifest {
13
- schema: 'session-runbook.v0';
14
- session_id: string;
15
- created_at: string;
16
- updated_at: string;
17
- creation_mode: 'explicit' | 'dynamic';
18
- project_id: string;
19
- project_ref: '.';
20
- task_ref: string;
21
- seat: string;
22
- operator_principal: string | null;
23
- owner_principal: string | null;
24
- target_ref: string | null;
25
- runbook_ref: string;
26
- manifest_ref: string;
27
- note_count: number;
28
- ledger_boundary: {
29
- global_governance_ledger: 'receipt_ref_only';
30
- repo_local_session_ledger: 'runbook_and_manifest';
31
- ephemeral_workspace: 'summarized_only';
32
- };
33
- authority_boundary: {
34
- can_authorize: false;
35
- can_merge: false;
36
- can_close: false;
37
- can_write_global_ledger: false;
38
- session_record_is_authority: false;
39
- };
40
- }
41
-
42
- export interface CreateSessionRunbookOptions {
43
- projectRoot: string;
44
- seat: string;
45
- taskRef: string;
46
- sessionId?: string;
47
- operatorPrincipal?: string;
48
- ownerPrincipal?: string;
49
- targetRef?: string;
50
- creationMode?: 'explicit' | 'dynamic';
51
- at?: string;
52
- }
53
-
54
- export interface AppendSessionRunbookNoteOptions {
55
- projectRoot: string;
56
- sessionId: string;
57
- section: SessionNoteSection;
58
- message: string;
59
- at?: string;
60
- }
61
-
62
- export interface SessionRunbookResult {
63
- manifest: SessionRunbookManifest;
64
- manifestPath: string;
65
- runbookPath: string;
66
- }
67
-
68
- export interface SessionRunbookNoteResult extends SessionRunbookResult {
69
- section: SessionNoteSection;
70
- appended_at: string;
71
- }
72
-
73
- export interface MigrateSessionProjectBindingsOptions {
74
- projectRoot: string;
75
- dryRun?: boolean;
76
- at?: string;
77
- }
78
-
79
- export interface MigrateSessionProjectBindingsResult {
80
- schema: 'session-project-binding-migration.v0';
81
- project_id: string;
82
- project_ref: '.etiquette/project.yaml';
83
- dry_run: boolean;
84
- migrated: string[];
85
- unchanged: string[];
86
- authority_boundary: {
87
- can_authorize: false;
88
- can_merge: false;
89
- can_close: false;
90
- can_write_global_ledger: false;
91
- };
92
- }
93
-
94
- const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/;
95
-
96
- function assertIsoTimestamp(value: string): void {
97
- if (!Number.isFinite(Date.parse(value))) {
98
- throw new Error('--at must be an ISO-8601 timestamp');
99
- }
100
- }
101
-
102
- function isoNow(at?: string): string {
103
- const value = at ?? new Date().toISOString();
104
- assertIsoTimestamp(value);
105
- return new Date(value).toISOString();
106
- }
107
-
108
- function compactTimestamp(value: string): string {
109
- return value.replace(/[-:.]/g, '').replace(/\.\d+Z$/, 'Z');
110
- }
111
-
112
- function generateSessionId(createdAt: string): string {
113
- return `session-${compactTimestamp(createdAt)}-${randomUUID().slice(0, 8)}`;
114
- }
115
-
116
- function assertSessionId(value: string): void {
117
- if (!SESSION_ID_RE.test(value)) {
118
- throw new Error('session id must be an opaque id using letters, numbers, _, ., or -');
119
- }
120
- }
121
-
122
- function assertOptionalScopeId(value: string | undefined, fieldName: string): string | null {
123
- if (value === undefined) return null;
124
- assertSharedMemoryScopeId(value, fieldName);
125
- return value;
126
- }
127
-
128
- function projectRelative(projectRoot: string, path: string): string {
129
- const resolvedRoot = resolve(projectRoot);
130
- const resolvedPath = resolve(path);
131
- const rel = relative(resolvedRoot, resolvedPath);
132
- if (rel.length === 0 || rel.startsWith('..') || rel.includes('\\')) {
133
- throw new Error(`path must stay inside project root: ${path}`);
134
- }
135
- return rel;
136
- }
137
-
138
- function sessionDir(projectRoot: string, sessionId: string): string {
139
- return join(projectRoot, 'docs', 'work', 'sessions', sessionId);
140
- }
141
-
142
- function manifestPath(projectRoot: string, sessionId: string): string {
143
- return join(sessionDir(projectRoot, sessionId), 'session.json');
144
- }
145
-
146
- function runbookPath(projectRoot: string, sessionId: string): string {
147
- return join(sessionDir(projectRoot, sessionId), 'RUNBOOK.md');
148
- }
149
-
150
- async function listSessionManifestFiles(projectRoot: string): Promise<string[]> {
151
- const root = join(projectRoot, 'docs', 'work', 'sessions');
152
- if (!existsSync(root)) return [];
153
- const entries = await readdir(root, { withFileTypes: true });
154
- return entries
155
- .filter((entry) => entry.isDirectory())
156
- .map((entry) => join(root, entry.name, 'session.json'))
157
- .filter((path) => existsSync(path));
158
- }
159
-
160
- function renderNullable(value: string | null): string {
161
- return value ?? 'none';
162
- }
163
-
164
- function renderRunbook(manifest: SessionRunbookManifest): string {
165
- return [
166
- `# Session Runbook: ${manifest.session_id}`,
167
- '',
168
- `- Schema: \`${manifest.schema}\``,
169
- `- Project ID: \`${manifest.project_id}\``,
170
- `- Seat: \`${manifest.seat}\``,
171
- `- Operator Principal: \`${renderNullable(manifest.operator_principal)}\``,
172
- `- Owner Principal: \`${renderNullable(manifest.owner_principal)}\``,
173
- `- Task: \`${manifest.task_ref}\``,
174
- `- Target Ref: \`${renderNullable(manifest.target_ref)}\``,
175
- `- Created At: \`${manifest.created_at}\``,
176
- `- Creation Mode: \`${manifest.creation_mode}\``,
177
- `- Manifest Ref: \`${manifest.manifest_ref}\``,
178
- `- Runbook Ref: \`${manifest.runbook_ref}\``,
179
- '',
180
- '## Ledger Boundary',
181
- '',
182
- '- Global governance ledger: receipt/ref only.',
183
- '- Repo-local session ledger: this runbook and `session.json` manifest.',
184
- '- Ephemeral workspace logs: summarize outcomes; do not paste raw prompts, secrets, or full tool output.',
185
- '- Authority: this session record is evidence only; it cannot authorize, merge, close, promote, or write global ledger truth.',
186
- '',
187
- '## Assumptions & Plan',
188
- '',
189
- '- _No entries yet._',
190
- '',
191
- '## Tool Summary',
192
- '',
193
- '- _No entries yet._',
194
- '',
195
- '## Retries & Corrections',
196
- '',
197
- '- _No entries yet._',
198
- '',
199
- '## Exit Evidence',
200
- '',
201
- '- _No entries yet._',
202
- '',
203
- '## Notes',
204
- '',
205
- '- _No entries yet._',
206
- '',
207
- ].join('\n');
208
- }
209
-
210
- function sectionHeading(section: SessionNoteSection): string {
211
- switch (section) {
212
- case 'plan':
213
- return 'Assumptions & Plan';
214
- case 'tool':
215
- return 'Tool Summary';
216
- case 'retry':
217
- return 'Retries & Corrections';
218
- case 'evidence':
219
- return 'Exit Evidence';
220
- case 'note':
221
- return 'Notes';
222
- }
223
- }
224
-
225
- function appendToSection(content: string, section: SessionNoteSection, line: string): string {
226
- const marker = `## ${sectionHeading(section)}\n\n`;
227
- const start = content.indexOf(marker);
228
- if (start < 0) throw new Error(`runbook missing section: ${section}`);
229
- const bodyStart = start + marker.length;
230
- const nextSection = content.indexOf('\n## ', bodyStart);
231
- const end = nextSection < 0 ? content.length : nextSection;
232
- let body = content.slice(bodyStart, end);
233
- body = body.replace('- _No entries yet._\n', '');
234
- if (body.length > 0 && !body.endsWith('\n')) body = `${body}\n`;
235
- body = `${body}${line}\n`;
236
- return `${content.slice(0, bodyStart)}${body}${content.slice(end)}`;
237
- }
238
-
239
- async function writeManifest(path: string, manifest: SessionRunbookManifest): Promise<void> {
240
- await mkdir(dirname(path), { recursive: true });
241
- await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
242
- }
243
-
244
- async function readManifest(path: string): Promise<SessionRunbookManifest> {
245
- const parsed = JSON.parse(await readFile(path, 'utf-8')) as SessionRunbookManifest;
246
- if (parsed.schema !== 'session-runbook.v0') {
247
- throw new Error('session manifest schema must be session-runbook.v0');
248
- }
249
- return parsed;
250
- }
251
-
252
- function withRunbookProjectLine(content: string, projectId: string): string {
253
- if (content.includes('- Project ID:')) return content;
254
- const schemaLine = '- Schema: `session-runbook.v0`';
255
- const index = content.indexOf(schemaLine);
256
- if (index < 0) return content;
257
- const lineEnd = content.indexOf('\n', index);
258
- if (lineEnd < 0) return content;
259
- return `${content.slice(0, lineEnd + 1)}- Project ID: \`${projectId}\`\n${content.slice(lineEnd + 1)}`;
260
- }
261
-
262
- export async function migrateSessionProjectBindings(
263
- options: MigrateSessionProjectBindingsOptions,
264
- ): Promise<MigrateSessionProjectBindingsResult> {
265
- const projectRoot = resolve(options.projectRoot);
266
- const project = await ensureProjectBinding(projectRoot, options.at);
267
- const manifestFiles = await listSessionManifestFiles(projectRoot);
268
- const migrated: string[] = [];
269
- const unchanged: string[] = [];
270
- const mismatched: string[] = [];
271
-
272
- for (const manifestFile of manifestFiles) {
273
- const raw = JSON.parse(await readFile(manifestFile, 'utf-8')) as Record<string, unknown>;
274
- if (raw.schema !== 'session-runbook.v0') {
275
- throw new Error(`session manifest schema must be session-runbook.v0: ${projectRelative(projectRoot, manifestFile)}`);
276
- }
277
- const sessionId = typeof raw.session_id === 'string' ? raw.session_id : projectRelative(projectRoot, manifestFile);
278
- if (typeof raw.project_id === 'string') {
279
- if (raw.project_id === project.project_id) unchanged.push(sessionId);
280
- else mismatched.push(`${sessionId}=${raw.project_id}`);
281
- continue;
282
- }
283
- migrated.push(sessionId);
284
- }
285
-
286
- if (mismatched.length > 0) {
287
- throw new Error(
288
- `session project_id mismatch; refusing migration: ${mismatched.join(', ')}; inspect or recreate those sessions`,
289
- );
290
- }
291
-
292
- if (!options.dryRun) {
293
- for (const manifestFile of manifestFiles) {
294
- const raw = JSON.parse(await readFile(manifestFile, 'utf-8')) as Record<string, unknown>;
295
- if (typeof raw.project_id === 'string') continue;
296
- const updated = {
297
- ...raw,
298
- project_id: project.project_id,
299
- project_ref: '.',
300
- };
301
- await writeFile(manifestFile, `${JSON.stringify(updated, null, 2)}\n`);
302
- const runbookFile = join(dirname(manifestFile), 'RUNBOOK.md');
303
- if (existsSync(runbookFile)) {
304
- const runbook = await readFile(runbookFile, 'utf-8');
305
- await writeFile(runbookFile, withRunbookProjectLine(runbook, project.project_id));
306
- }
307
- }
308
- }
309
-
310
- return {
311
- schema: 'session-project-binding-migration.v0',
312
- project_id: project.project_id,
313
- project_ref: '.etiquette/project.yaml',
314
- dry_run: Boolean(options.dryRun),
315
- migrated,
316
- unchanged,
317
- authority_boundary: {
318
- can_authorize: false,
319
- can_merge: false,
320
- can_close: false,
321
- can_write_global_ledger: false,
322
- },
323
- };
324
- }
325
-
326
- export async function createSessionRunbook(options: CreateSessionRunbookOptions): Promise<SessionRunbookResult> {
327
- const projectRoot = resolve(options.projectRoot);
328
- const project = await ensureProjectBinding(projectRoot, options.at);
329
- const createdAt = isoNow(options.at);
330
- const sessionId = options.sessionId ?? generateSessionId(createdAt);
331
- assertSessionId(sessionId);
332
- assertSharedMemoryScopeId(options.seat, '--seat');
333
- assertSharedMemoryScopeId(options.taskRef, '--task');
334
-
335
- const operatorPrincipal = assertOptionalScopeId(options.operatorPrincipal, '--operator');
336
- const ownerPrincipal = assertOptionalScopeId(options.ownerPrincipal, '--owner');
337
- const targetRef = assertOptionalScopeId(options.targetRef, '--target-ref');
338
- const creationMode = options.creationMode ?? 'explicit';
339
-
340
- if (creationMode !== 'explicit' && creationMode !== 'dynamic') {
341
- throw new Error('--creation-mode must be explicit or dynamic');
342
- }
343
-
344
- const manifestFile = manifestPath(projectRoot, sessionId);
345
- const runbookFile = runbookPath(projectRoot, sessionId);
346
- if (existsSync(manifestFile) || existsSync(runbookFile)) {
347
- throw new Error(`session already exists: ${sessionId}`);
348
- }
349
-
350
- const manifest: SessionRunbookManifest = {
351
- schema: 'session-runbook.v0',
352
- session_id: sessionId,
353
- created_at: createdAt,
354
- updated_at: createdAt,
355
- creation_mode: creationMode,
356
- project_id: project.project_id,
357
- project_ref: '.',
358
- task_ref: options.taskRef,
359
- seat: options.seat,
360
- operator_principal: operatorPrincipal,
361
- owner_principal: ownerPrincipal,
362
- target_ref: targetRef,
363
- runbook_ref: projectRelative(projectRoot, runbookFile),
364
- manifest_ref: projectRelative(projectRoot, manifestFile),
365
- note_count: 0,
366
- ledger_boundary: {
367
- global_governance_ledger: 'receipt_ref_only',
368
- repo_local_session_ledger: 'runbook_and_manifest',
369
- ephemeral_workspace: 'summarized_only',
370
- },
371
- authority_boundary: {
372
- can_authorize: false,
373
- can_merge: false,
374
- can_close: false,
375
- can_write_global_ledger: false,
376
- session_record_is_authority: false,
377
- },
378
- };
379
-
380
- await mkdir(dirname(runbookFile), { recursive: true });
381
- await writeFile(runbookFile, renderRunbook(manifest));
382
- await writeManifest(manifestFile, manifest);
383
-
384
- return { manifest, manifestPath: manifestFile, runbookPath: runbookFile };
385
- }
386
-
387
- export async function appendSessionRunbookNote(options: AppendSessionRunbookNoteOptions): Promise<SessionRunbookNoteResult> {
388
- const projectRoot = resolve(options.projectRoot);
389
- const project = await ensureProjectBinding(projectRoot, options.at);
390
- assertSessionId(options.sessionId);
391
- if (!(SESSION_NOTE_SECTIONS as readonly string[]).includes(options.section)) {
392
- throw new Error(`--section must be one of: ${SESSION_NOTE_SECTIONS.join(', ')}`);
393
- }
394
- assertSharedMemoryText(options.message, '--message');
395
-
396
- const appendedAt = isoNow(options.at);
397
- const manifestFile = manifestPath(projectRoot, options.sessionId);
398
- const runbookFile = runbookPath(projectRoot, options.sessionId);
399
- const manifest = await readManifest(manifestFile);
400
- assertSessionProject(manifest.project_id, project.project_id, `session ${options.sessionId}`);
401
- const runbook = await readFile(runbookFile, 'utf-8');
402
- const line = `- ${appendedAt} - ${options.message}`;
403
- await writeFile(runbookFile, appendToSection(runbook, options.section, line));
404
-
405
- const updated: SessionRunbookManifest = {
406
- ...manifest,
407
- updated_at: appendedAt,
408
- note_count: manifest.note_count + 1,
409
- };
410
- await writeManifest(manifestFile, updated);
411
- await appendFile(join(sessionDir(projectRoot, options.sessionId), 'events.ndjson'), `${JSON.stringify({
412
- schema: 'session-note-event.v0',
413
- session_id: options.sessionId,
414
- project_id: project.project_id,
415
- at: appendedAt,
416
- section: options.section,
417
- message: options.message,
418
- authority_boundary: {
419
- can_authorize: false,
420
- can_write_global_ledger: false,
421
- },
422
- })}\n`);
423
-
424
- return {
425
- manifest: updated,
426
- manifestPath: manifestFile,
427
- runbookPath: runbookFile,
428
- section: options.section,
429
- appended_at: appendedAt,
430
- };
431
- }
@@ -1,49 +0,0 @@
1
- export const SHARED_RISK_FLAGS = [
2
- 'no_durable_evidence',
3
- 'stale',
4
- 'contains_private_context',
5
- 'contradicts_current_ledger',
6
- 'contains_raw_prompt',
7
- 'contains_raw_tool_output',
8
- 'contains_source_snippet',
9
- 'contains_local_path',
10
- 'contains_hostname',
11
- 'contains_credential',
12
- 'confidence_low_for_shared_export',
13
- ] as const;
14
-
15
- export type SharedRiskFlag = (typeof SHARED_RISK_FLAGS)[number];
16
-
17
- const LOCAL_PATH_RE = /(?:\/Users\/|\/home\/|\/private\/|\/tmp\/|\$HOME|~\/|[A-Za-z]:\\)/;
18
- const HOSTNAME_RE = /\b[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*\.(?:local|lan|internal|intranet|localdomain)\b/i;
19
- const IPV4_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/;
20
- const IPV6_RE = /\b(?:[0-9a-f]{1,4}:){2,}[0-9a-f]{0,4}\b/i;
21
- const CREDENTIAL_RE = /\b(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|GEMINI_API_KEY|api[_-]?key|token|secret)\b\s*[:=]|sk-[A-Za-z0-9_-]{12,}/i;
22
- const RAW_PROMPT_RE = /\b(raw prompt|user prompt|prompt transcript|UserPromptSubmit)\b/i;
23
- const RAW_TOOL_RE = /\b(raw tool output|tool transcript|PostToolUse output|stdout:|stderr:)\b/i;
24
- const CODE_FENCE_RE = /```/;
25
-
26
- export function detectMemoryRiskFlags(value: string): SharedRiskFlag[] {
27
- const flags = new Set<SharedRiskFlag>();
28
- if (LOCAL_PATH_RE.test(value)) flags.add('contains_local_path');
29
- if (HOSTNAME_RE.test(value) || IPV4_RE.test(value) || IPV6_RE.test(value)) flags.add('contains_hostname');
30
- if (CREDENTIAL_RE.test(value)) flags.add('contains_credential');
31
- if (RAW_PROMPT_RE.test(value)) flags.add('contains_raw_prompt');
32
- if (RAW_TOOL_RE.test(value)) flags.add('contains_raw_tool_output');
33
- if (CODE_FENCE_RE.test(value)) flags.add('contains_source_snippet');
34
- return [...flags];
35
- }
36
-
37
- export function assertSharedMemoryText(value: string, fieldName: string): void {
38
- const flags = detectMemoryRiskFlags(value);
39
- if (flags.length > 0) {
40
- throw new Error(`${fieldName} contains restricted shared-memory content: ${flags.join(', ')}`);
41
- }
42
- }
43
-
44
- export function assertSharedMemoryScopeId(value: string, fieldName: string): void {
45
- assertSharedMemoryText(value, fieldName);
46
- if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value)) {
47
- throw new Error(`${fieldName} must be an opaque id using only letters, numbers, _, ., :, or -`);
48
- }
49
- }
@@ -1,192 +0,0 @@
1
- import type {
2
- ActionClass,
3
- AllowedCommandId,
4
- SeatCapabilityManifest,
5
- SupervisionTurn,
6
- } from '../../../protocol/src/index';
7
-
8
- export class ActionClassViolation extends Error {
9
- constructor(
10
- readonly actionClass: ActionClass,
11
- readonly issues: string[],
12
- ) {
13
- super(`action class ${actionClass} violated: ${issues.join('; ')}`);
14
- this.name = 'ActionClassViolation';
15
- }
16
- }
17
-
18
- export interface TurnBoundsProposal {
19
- allowedActionClass: ActionClass;
20
- maxDurationMinutes: number;
21
- allowedCommands: AllowedCommandId[];
22
- repoRoots: string[];
23
- ownedPaths: string[];
24
- forbiddenPaths: string[];
25
- verifyCommands: string[];
26
- requiresHumanApproval: boolean;
27
- }
28
-
29
- function isSubset<T>(subset: readonly T[], universe: readonly T[]): boolean {
30
- const set = new Set(universe);
31
- return subset.every((v) => set.has(v));
32
- }
33
-
34
- /**
35
- * Validate a turn bounds proposal against the charter's class rules.
36
- * Does NOT validate seat capabilities — use `authorizeTurnForSeat` for that.
37
- * Throws `ActionClassViolation` with every issue; returns silently on pass.
38
- */
39
- export function validateTurnBounds(bounds: TurnBoundsProposal): void {
40
- const issues: string[] = [];
41
-
42
- if (!Number.isInteger(bounds.maxDurationMinutes) || bounds.maxDurationMinutes <= 0) {
43
- issues.push('maxDurationMinutes must be a positive integer');
44
- }
45
-
46
- switch (bounds.allowedActionClass) {
47
- case 'coordination_only': {
48
- if (!isSubset(bounds.allowedCommands, ['write_projection', 'append_evidence'])) {
49
- issues.push('allowedCommands must be a subset of {write_projection, append_evidence}');
50
- }
51
- if (bounds.repoRoots.length > 0) issues.push('repoRoots must be empty');
52
- if (bounds.ownedPaths.length > 0) issues.push('ownedPaths must be empty');
53
- if (bounds.forbiddenPaths.length > 0) issues.push('forbiddenPaths must be empty');
54
- if (bounds.verifyCommands.length > 0) issues.push('verifyCommands must be empty');
55
- break;
56
- }
57
- case 'read_only_local': {
58
- if (!isSubset(bounds.allowedCommands, ['inspect_repo', 'write_projection', 'append_evidence'])) {
59
- issues.push('allowedCommands must be a subset of {inspect_repo, write_projection, append_evidence}');
60
- }
61
- if (bounds.repoRoots.length === 0) issues.push('repoRoots must be non-empty');
62
- if (bounds.ownedPaths.length > 0) issues.push('ownedPaths must be empty');
63
- if (bounds.forbiddenPaths.length > 0) issues.push('forbiddenPaths must be empty');
64
- if (bounds.verifyCommands.length > 0) issues.push('verifyCommands must be empty');
65
- break;
66
- }
67
- case 'bounded_verification': {
68
- if (!bounds.allowedCommands.includes('run_verify')) {
69
- issues.push('allowedCommands must include run_verify');
70
- }
71
- if (
72
- !isSubset(bounds.allowedCommands, [
73
- 'inspect_repo',
74
- 'run_verify',
75
- 'write_projection',
76
- 'append_evidence',
77
- ])
78
- ) {
79
- issues.push('allowedCommands may only include {inspect_repo, run_verify, write_projection, append_evidence}');
80
- }
81
- if (bounds.repoRoots.length === 0) issues.push('repoRoots must be non-empty');
82
- if (bounds.verifyCommands.length === 0) issues.push('verifyCommands must be non-empty');
83
- if (bounds.ownedPaths.length > 0) issues.push('ownedPaths must be empty');
84
- break;
85
- }
86
- case 'bounded_product_mutation': {
87
- if (!bounds.allowedCommands.includes('write_product_files')) {
88
- issues.push('allowedCommands must include write_product_files');
89
- }
90
- if (bounds.repoRoots.length === 0) issues.push('repoRoots must be non-empty');
91
- if (bounds.ownedPaths.length === 0) issues.push('ownedPaths must be non-empty');
92
- if (bounds.verifyCommands.length === 0) issues.push('verifyCommands must be non-empty');
93
- if (!bounds.requiresHumanApproval) {
94
- issues.push('requiresHumanApproval must be true for bounded_product_mutation');
95
- }
96
- break;
97
- }
98
- case 'human_only': {
99
- issues.push('human_only turns may not be opened by automation');
100
- break;
101
- }
102
- default: {
103
- issues.push(`unknown action class: ${bounds.allowedActionClass satisfies never}`);
104
- }
105
- }
106
-
107
- if (issues.length > 0) {
108
- throw new ActionClassViolation(bounds.allowedActionClass, issues);
109
- }
110
- }
111
-
112
- /**
113
- * Check that a seat manifest permits opening a turn of the proposed class.
114
- * Per doctrine, `bounded_product_mutation` additionally requires an approval
115
- * blob on the session; that check is caller's responsibility here.
116
- */
117
- export function authorizeTurnForSeat(
118
- bounds: TurnBoundsProposal,
119
- seat: SeatCapabilityManifest,
120
- ): void {
121
- const issues: string[] = [];
122
-
123
- if (!seat.allowedActionClasses.includes(bounds.allowedActionClass)) {
124
- issues.push(
125
- `seat ${seat.seatId} does not allow action class ${bounds.allowedActionClass}`,
126
- );
127
- }
128
-
129
- if (bounds.allowedCommands.includes('run_verify') && !seat.canRunBuilds) {
130
- issues.push(`seat ${seat.seatId} lacks canRunBuilds for run_verify`);
131
- }
132
-
133
- if (bounds.allowedCommands.includes('write_product_files') && !seat.canWriteProductRepo) {
134
- issues.push(`seat ${seat.seatId} lacks canWriteProductRepo for write_product_files`);
135
- }
136
-
137
- if (bounds.allowedCommands.includes('launch_runtime_target') && !seat.canLaunchRuntimeTargets) {
138
- issues.push(`seat ${seat.seatId} lacks canLaunchRuntimeTargets for launch_runtime_target`);
139
- }
140
-
141
- if (bounds.allowedActionClass === 'bounded_product_mutation' && !bounds.requiresHumanApproval) {
142
- issues.push('bounded_product_mutation requires requiresHumanApproval=true');
143
- }
144
-
145
- if (issues.length > 0) {
146
- throw new ActionClassViolation(bounds.allowedActionClass, issues);
147
- }
148
- }
149
-
150
- /**
151
- * Exact-match comparison between a requested turn and a previously stored
152
- * approval blob. Used for `approve_next_turn` enforcement: opening a turn
153
- * while an approval is present requires all bounds to match exactly.
154
- */
155
- export function turnMatchesApproval(
156
- requested: TurnBoundsProposal & { ownerSeat: string; projectionPolicy: SupervisionTurn['projectionPolicy'] },
157
- approval: {
158
- ownerSeat: string;
159
- allowedActionClass: ActionClass;
160
- maxDurationMinutes: number;
161
- allowedCommands: AllowedCommandId[];
162
- repoRoots: string[];
163
- ownedPaths: string[];
164
- forbiddenPaths: string[];
165
- verifyCommands: string[];
166
- requiresHumanApproval: boolean;
167
- projectionPolicy: SupervisionTurn['projectionPolicy'];
168
- },
169
- ): { matches: boolean; diffs: string[] } {
170
- const diffs: string[] = [];
171
- const eq = <T>(a: T, b: T, label: string): void => {
172
- if (a !== b) diffs.push(`${label}: requested=${String(a)} vs approved=${String(b)}`);
173
- };
174
- const eqList = (a: readonly string[], b: readonly string[], label: string): void => {
175
- const sa = [...a].sort();
176
- const sb = [...b].sort();
177
- if (sa.length !== sb.length || sa.some((v, i) => v !== sb[i])) {
178
- diffs.push(`${label}: requested=[${sa.join(',')}] vs approved=[${sb.join(',')}]`);
179
- }
180
- };
181
- eq(requested.ownerSeat, approval.ownerSeat, 'ownerSeat');
182
- eq(requested.allowedActionClass, approval.allowedActionClass, 'allowedActionClass');
183
- eq(requested.maxDurationMinutes, approval.maxDurationMinutes, 'maxDurationMinutes');
184
- eq(requested.requiresHumanApproval, approval.requiresHumanApproval, 'requiresHumanApproval');
185
- eq(requested.projectionPolicy, approval.projectionPolicy, 'projectionPolicy');
186
- eqList(requested.allowedCommands, approval.allowedCommands, 'allowedCommands');
187
- eqList(requested.repoRoots, approval.repoRoots, 'repoRoots');
188
- eqList(requested.ownedPaths, approval.ownedPaths, 'ownedPaths');
189
- eqList(requested.forbiddenPaths, approval.forbiddenPaths, 'forbiddenPaths');
190
- eqList(requested.verifyCommands, approval.verifyCommands, 'verifyCommands');
191
- return { matches: diffs.length === 0, diffs };
192
- }