@etiquekit/etq 1.0.11 → 1.0.13
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 -56
- 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 +7 -6
- 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 +11 -11
- 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 +4686 -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 +831 -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 +14 -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,431 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
}
|