@bradheitmann/odin-sentinel 0.4.11 → 0.4.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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/README.md +20 -16
  3. package/dist/src/mcp/server.js +32 -0
  4. package/dist/src/mcp/server.js.map +1 -1
  5. package/dist/src/protocol/repository.d.ts +6 -0
  6. package/dist/src/protocol/repository.js +12 -2
  7. package/dist/src/protocol/repository.js.map +1 -1
  8. package/dist/src/protocol/service.js +17 -9
  9. package/dist/src/protocol/service.js.map +1 -1
  10. package/dist/src/protocol/version.d.ts +2 -2
  11. package/dist/src/protocol/version.js +2 -2
  12. package/docs/guides/quick-start.md +7 -7
  13. package/docs/guides/quickstart-prompts.md +4 -4
  14. package/docs/reference/client-compatibility.md +1 -1
  15. package/docs/reference/distribution.md +11 -5
  16. package/docs/reference/public-surface-audit.md +2 -2
  17. package/package.json +5 -3
  18. package/plugins/odin-scp/.claude-plugin/plugin.json +25 -0
  19. package/plugins/odin-scp/README.md +62 -0
  20. package/plugins/odin-scp/skills/odin-scp/CHANGELOG.md +25 -0
  21. package/plugins/odin-scp/skills/odin-scp/SKILL.md +1518 -0
  22. package/plugins/odin-scp/skills/odin-scp/agents/openai.yaml +4 -0
  23. package/plugins/odin-scp/skills/odin-scp/references/boot-receipt-examples.md +439 -0
  24. package/plugins/odin-scp/skills/odin-scp/references/canonical-introduction-prompt.md +118 -0
  25. package/plugins/odin-scp/skills/odin-scp/references/harness-skill-targets.md +56 -0
  26. package/plugins/odin-scp/skills/odin-scp/references/team-bootstrap-runbook.md +298 -0
  27. package/plugins/odin-scp/skills/odin-scp/scripts/sync-installations.sh +233 -0
  28. package/protocol/SCP.md +3 -3
  29. package/protocol/bootstrap-skill.md +22 -13
  30. package/protocol/closeout.yaml +1 -1
  31. package/protocol/delegation.yaml +1 -1
  32. package/protocol/model-profiles.yaml +2 -2
  33. package/protocol/receipts/team-manifest.yaml +1 -1
  34. package/protocol/roles.yaml +1 -1
  35. package/protocol/skill-references/boot-receipt-examples.md +439 -0
  36. package/protocol/skill-references/canonical-introduction-prompt.md +118 -0
  37. package/protocol/skill-references/harness-skill-targets.md +56 -0
  38. package/protocol/skill-references/team-bootstrap-runbook.md +298 -0
  39. package/protocol/topology.yaml +1 -1
  40. package/scripts/audit/public-surface.mjs +32 -3
  41. package/scripts/audit/verify-pack.mjs +175 -12
  42. package/templates/team-manifest-template.yaml +6 -6
@@ -0,0 +1,298 @@
1
+ # SCP Team Bootstrap Runbook
2
+
3
+ Use this when a single `EXEC PM` pane must create, operate, and tear down SCP teams in CMUX or another terminal surface manager.
4
+
5
+ ## Required Inputs
6
+
7
+ - Objective and phase boundary.
8
+ - Target repo/worktree/branch/SHA.
9
+ - Pod count, default 3 federated pods unless human operator specifies otherwise.
10
+ - Team exceptions, especially UX/design teams.
11
+ - Allowed harness/model pool and any budget/cost priority.
12
+
13
+ If any input is missing, choose the conservative default and record it in `SCP-TEAM-MANIFEST`. Ask human operator only when missing information changes authority, scope, destructive cleanup, or cost materially.
14
+
15
+ ## Skill Composition
16
+
17
+ - Use `team-composition-patterns` to choose minimum viable pod shape.
18
+ - Use `dispatching-parallel-agents` only after workstreams are independent and write scopes do not conflict.
19
+ - Use `delegate` for harness/model selection, preflight probes, fallback ladder, and instruction bundles.
20
+ - Use `handoff` for parked retained panes.
21
+ - Use `qa-swarm-review` for cleanup or closure review.
22
+ - Use `atlas-synthesis` for post-run SCP improvement packets.
23
+
24
+ ## Default Three-Pod Layout
25
+
26
+ ```yaml
27
+ executive_office:
28
+ - pane: A/EXEC-PM
29
+ role: EXEC PM
30
+ - pane: A/EXEC-ASST
31
+ role: EXEC ASST
32
+ - pane: A/EXEC-RSCH
33
+ role: EXEC RSCH
34
+ - pane: A/EXEC-QA
35
+ role: EXEC QA
36
+ pods:
37
+ - team: B
38
+ panes: [B/TEAM-PM, B/TEAM-SENTINEL, B/DEV-1, B/QA-1, B/SHADOW-1]
39
+ - team: C
40
+ panes: [C/TEAM-PM, C/TEAM-SENTINEL, C/DEV-1, C/QA-1, C/SHADOW-1]
41
+ - team: D
42
+ panes: [D/TEAM-PM, D/TEAM-SENTINEL, D/DEV-1, D/QA-1, D/SHADOW-1]
43
+ floaters:
44
+ - A/INTEGRATION-STEWARD
45
+ - A/QUEUE-TRIAGE
46
+ ```
47
+
48
+ Start smaller when the objective is narrow. Add pods horizontally only when the queue has independent work, non-overlapping write scopes, and enough QA capacity.
49
+
50
+ `TEAM PM` and `TEAM SENTINEL` are separate control roles. `TEAM PM` routes pod assignments and activates workers. `TEAM SENTINEL` watches delivery, scope, role discipline, branch proof, and intervention health. Neither implements or QA-closes by default.
51
+
52
+ ## Terminal Setup Pattern
53
+
54
+ Use live CMUX commands only after identifying the current workspace. If operating in tmux, WezTerm, iTerm2, Ghostty, Warp, Cursor, Zed, VS Code, libghostyy, or another surface manager, capture the equivalent `terminal_locator` fields and mark unsupported fields `unavailable`.
55
+
56
+ If the surface manager exposes libghostty-vt or a congruent terminal-state API, also capture `vt_state_snapshot`. Keep this separate from `terminal_locator`: libghostty-vt models terminal emulator state such as terminal instances, rows/cols, active screen, cursor, scrollback, render dirty state, formatter output, semantic prompt state, and input encoding; it does not by itself define SCP team identity or CMUX-style workspace routing.
57
+
58
+ ```bash
59
+ /Applications/cmux.app/Contents/Resources/bin/cmux --json --id-format both current-workspace
60
+ /Applications/cmux.app/Contents/Resources/bin/cmux --json --id-format both identify --workspace <workspace> --surface <surface>
61
+ /Applications/cmux.app/Contents/Resources/bin/cmux --json --id-format both list-pane-surfaces --workspace <workspace>
62
+ /Applications/cmux.app/Contents/Resources/bin/cmux new-surface --type terminal --workspace <workspace>
63
+ /Applications/cmux.app/Contents/Resources/bin/cmux rename-tab --workspace <workspace> --surface <surface> 'C/TEAM-SENTINEL'
64
+ /Applications/cmux.app/Contents/Resources/bin/cmux send --workspace <workspace> --surface <surface> '<launch command or boot prompt>'
65
+ /Applications/cmux.app/Contents/Resources/bin/cmux send-key --workspace <workspace> --surface <surface> Enter
66
+ /Applications/cmux.app/Contents/Resources/bin/cmux read-screen --workspace <workspace> --surface <surface> --lines 80 --scrollback
67
+ ```
68
+
69
+ Do not claim delivery until send, enter, read-screen, and ack state are recorded in `[SCP-CMUX-DELIVERY]` or `[SCP-TERMINAL-DELIVERY]`.
70
+
71
+ Use these delivery verdicts:
72
+
73
+ - `DELIVERED_ACKED`: send/enter/read-screen completed and ack observed.
74
+ - `DELIVERED_NO_ACK`: message landed but no ack yet; poll again.
75
+ - `INPUT_BAR_ONLY`: text was not submitted; not delivered.
76
+ - `PANE_BLOCKED_ON_PERMISSION`: plan mode, auth, quota, modal, or permission blocker.
77
+ - `PANE_STILL_THINKING`: instruction landed and pane is still processing.
78
+
79
+ ## Fast Bootstrap Mode
80
+
81
+ For initial team creation, do not require every idle pane to emit the full `SCP_BOOT_RECEIPT`. Use `SCP_MIN_BOOT_RECEIPT` to park panes quickly, then require full `SCP_BOOT_RECEIPT` only before activation, dispatch, mutation, QA, commit, push, or closure language.
82
+
83
+ ```yaml
84
+ SCP_MIN_BOOT_RECEIPT:
85
+ agent_id: c-dev-1
86
+ team: C
87
+ role: DEV WORKER
88
+ reports_to: C/TEAM-PM
89
+ cwd: <pwd or EXEC PM supplied>
90
+ branch: <branch or EXEC PM supplied>
91
+ head_sha: <sha or EXEC PM supplied>
92
+ may_implement: false
93
+ may_qa_accept: false
94
+ permission_mode: DEGRADED_READ_ONLY | READ_ONLY | WRITE_WHEN_ACTIVATED
95
+ current_state: BOOTSTRAPPED_IDLE
96
+ ```
97
+
98
+ Minimal boot receipt states:
99
+
100
+ - `BOOTSTRAPPED_IDLE`: pane launched, role acknowledged, no work active.
101
+ - `BOOT_RECEIPT_PARTIAL`: minimal receipt present, full receipt deferred until activation.
102
+ - `BOOT_RECEIPT_BLOCKED`: pane paused on permission, auth, quota, context, or plan-mode issue.
103
+
104
+ When EXEC PM has exact CMUX/tmux/terminal refs, provide them in the boot prompt. Pane self-report is secondary and should not override adapter-captured locator ids.
105
+
106
+ ## Plan-Mode Bootstrap
107
+
108
+ Claude Code and similar harnesses may pause for approval on reads or shell proof. During bootstrap-only runs:
109
+
110
+ - Safe to approve or pre-supply: SCP/AGENTS/handoff reads, `pwd`, `git status --short --branch --untracked-files=all`, `git rev-parse HEAD @{u}`, `cmux identify`, `cmux read-screen`, and receipt acknowledgment.
111
+ - Keep blocked: writes, lifecycle moves, evidence/verdict creation, implementation, QA acceptance, commits, pushes, destructive cleanup, and secret output.
112
+ - If the pane is stuck in plan mode, EXEC PM may supply cwd/branch/SHA/locator proof and request `SCP_MIN_BOOT_RECEIPT` only.
113
+ - Do not dispatch real work to a plan-mode pane until full receipt and activation proof are present.
114
+
115
+ ## SCP-TEAM-MANIFEST
116
+
117
+ Record the manifest before dispatching work.
118
+
119
+ During no-product-work bootstrap, the manifest may remain runtime-only in the EXEC PM transcript, screen report, or status ledger. Before dispatch, mutation, QA activation, commit, push, finish, or clean/ready claims, promote the manifest to a durable handoff, ledger, or branch-visible artifact appropriate to the run.
120
+
121
+ ```yaml
122
+ SCP-TEAM-MANIFEST:
123
+ created_by: A/EXEC-PM
124
+ workspace: <semantic workspace label>
125
+ workspace_ref: <workspace:1 | session name | unavailable>
126
+ workspace_id: <uuid-or-stable-id | unavailable>
127
+ objective: <bounded objective>
128
+ branch_authority: <branch and sha>
129
+ executive_office:
130
+ - pane: A/EXEC-PM
131
+ agent_id: a-exec-pm
132
+ terminal_locator:
133
+ terminal_app: cmux
134
+ terminal_adapter: cmux
135
+ workspace_ref: <workspace:1>
136
+ workspace_id: <uuid>
137
+ window_ref: <window:1>
138
+ window_id: <uuid>
139
+ pane_ref: <pane>
140
+ pane_id: <uuid>
141
+ surface_ref: <surface>
142
+ surface_id: <uuid>
143
+ tab_ref: <tab>
144
+ tab_id: <uuid>
145
+ surface_type: terminal
146
+ title: A/EXEC-PM
147
+ route_command: cmux send --workspace <workspace> --surface <surface>
148
+ locator_source: cmux --json --id-format both identify
149
+ locator_captured_at: <ISO-8601 timestamp>
150
+ vt_state_snapshot:
151
+ vt_provider: unavailable
152
+ vt_api_stability: unknown
153
+ terminal_instance_ref: unavailable
154
+ terminal_instance_id: unavailable
155
+ pty_ref: unavailable
156
+ capture_source: cmux read-screen
157
+ formatter_format: plain
158
+ rows: unavailable
159
+ cols: unavailable
160
+ total_rows: unavailable
161
+ scrollback_rows: unavailable
162
+ width_px: unavailable
163
+ height_px: unavailable
164
+ active_screen: unavailable
165
+ cursor_x: unavailable
166
+ cursor_y: unavailable
167
+ cursor_visible: unavailable
168
+ cursor_pending_wrap: unavailable
169
+ title: A/EXEC-PM
170
+ pwd: unavailable
171
+ render_dirty: unavailable
172
+ semantic_prompt_observed: unavailable
173
+ semantic_input_observed: unavailable
174
+ semantic_output_observed: unavailable
175
+ paste_safety_checked: unavailable
176
+ paste_safe: unavailable
177
+ key_encoding_provider: unavailable
178
+ mouse_encoding_provider: unavailable
179
+ focus_encoding_provider: unavailable
180
+ snapshot_captured_at: <ISO-8601 timestamp>
181
+ role: EXEC PM
182
+ harness_model: <model/harness>
183
+ disposition: retain
184
+ pods:
185
+ - team: C
186
+ purpose: <workstream>
187
+ surfaces:
188
+ - pane: C/TEAM-PM
189
+ terminal_locator: <same schema>
190
+ role: TEAM PM
191
+ harness_model: <model/harness>
192
+ disposition: retain_or_close_on_finish
193
+ - pane: C/TEAM-SENTINEL
194
+ terminal_locator:
195
+ terminal_app: <cmux|tmux|wezterm|iterm2|ghostty|warp|cursor|zed|vscode|unknown>
196
+ terminal_adapter: <cmux|tmux|libghostyy|apple_script|cli|ide_terminal|unavailable>
197
+ workspace_ref: <workspace/session/ref-or-unavailable>
198
+ workspace_id: <uuid-or-stable-id-or-unavailable>
199
+ window_ref: <window-ref-or-unavailable>
200
+ window_id: <uuid-or-stable-id-or-unavailable>
201
+ pane_ref: <pane-ref-or-unavailable>
202
+ pane_id: <uuid-or-stable-id-or-unavailable>
203
+ surface_ref: <surface-ref-or-unavailable>
204
+ surface_id: <uuid-or-stable-id-or-unavailable>
205
+ tab_ref: <tab-ref-or-unavailable>
206
+ tab_id: <uuid-or-stable-id-or-unavailable>
207
+ surface_type: terminal
208
+ title: C/TEAM-SENTINEL
209
+ route_command: <non-secret route command or unavailable>
210
+ locator_source: <command/tool/observation used>
211
+ locator_captured_at: <ISO-8601 timestamp or unavailable>
212
+ vt_state_snapshot:
213
+ vt_provider: <libghostty-vt|terminal-capture|unavailable>
214
+ vt_api_stability: <work_in_progress_unstable|stable|unknown>
215
+ terminal_instance_ref: <GhosttyTerminal handle/ref or unavailable>
216
+ terminal_instance_id: <product-generated id or unavailable>
217
+ pty_ref: <pty/process route or unavailable>
218
+ capture_source: <formatter|render_state|grid_ref|read_screen|unavailable>
219
+ formatter_format: <plain|vt|html|unavailable>
220
+ rows: <rows or unavailable>
221
+ cols: <cols or unavailable>
222
+ total_rows: <total_rows or unavailable>
223
+ scrollback_rows: <scrollback_rows or unavailable>
224
+ width_px: <width_px or unavailable>
225
+ height_px: <height_px or unavailable>
226
+ active_screen: <primary|alternate|unavailable>
227
+ cursor_x: <cursor_x or unavailable>
228
+ cursor_y: <cursor_y or unavailable>
229
+ cursor_visible: <true|false|unavailable>
230
+ cursor_pending_wrap: <true|false|unavailable>
231
+ title: C/TEAM-SENTINEL
232
+ pwd: <pwd or unavailable>
233
+ render_dirty: <false|partial|full|unavailable>
234
+ semantic_prompt_observed: <true|false|unavailable>
235
+ semantic_input_observed: <true|false|unavailable>
236
+ semantic_output_observed: <true|false|unavailable>
237
+ paste_safety_checked: <true|false|unavailable>
238
+ paste_safe: <true|false|unavailable>
239
+ key_encoding_provider: <libghostty-vt|terminal|unavailable>
240
+ mouse_encoding_provider: <libghostty-vt|terminal|unavailable>
241
+ focus_encoding_provider: <libghostty-vt|terminal|unavailable>
242
+ snapshot_captured_at: <ISO-8601 timestamp or unavailable>
243
+ role: TEAM SENTINEL
244
+ harness_model: <model/harness>
245
+ disposition: retain_or_close_on_finish
246
+ - pane: C/DEV-1
247
+ terminal_locator: <same schema>
248
+ role: DEV WORKER
249
+ harness_model: <model/harness>
250
+ disposition: close_on_finish
251
+ exclusions:
252
+ - UX/design teams unless separately profiled
253
+ teardown_policy:
254
+ close_temp_panes_after_finish: true
255
+ retain_dirty_or_blocked_panes: true
256
+ require_approval_for_worktree_delete: true
257
+ ```
258
+
259
+ ## Harness Launch Rules
260
+
261
+ Before launching a harness:
262
+
263
+ 1. Run the `delegate` preflight for that harness/model.
264
+ 2. Prefer cached successful model/harness choices for the same task class.
265
+ 3. Use high-reasoning/costly models for executive, sentinel, architecture, integration, and high-risk QA.
266
+ 4. Use cheaper/bounded models for DEV, routine QA, scans, and shadow review.
267
+ 5. If a harness fails, substitute by role contract, not by pane identity.
268
+
269
+ Each launched pane must receive:
270
+
271
+ - role-specific boot prompt,
272
+ - `SCP_MIN_BOOT_RECEIPT` requirements for initial parking,
273
+ - full `SCP_BOOT_RECEIPT` requirements for activation or mutation,
274
+ - write/read/prohibited scopes,
275
+ - reports-to chain,
276
+ - `may_implement` and `may_qa_accept`,
277
+ - exact current objective,
278
+ - first expected receipt.
279
+
280
+ ## Dispatch Rules
281
+
282
+ `EXEC PM` dispatches to `TEAM PM`; `TEAM PM` activates and routes pod workers; `TEAM SENTINEL` monitors, polls, intervenes, and may relay corrective prompts. Workers do not self-select work.
283
+
284
+ Every downstream assignment requires `[SCP-DELEGATE]` and `[SCP-CMUX-DELIVERY]` or `[SCP-TERMINAL-DELIVERY]` with a delivery verdict. During active work, each sentinel emits `[SCP-POLL]` on the assigned cadence.
285
+
286
+ ## Teardown Rules
287
+
288
+ On `$odin-scp --finish`:
289
+
290
+ 1. `EXEC PM` broadcasts finish to all manifest panes.
291
+ 2. Every pane emits `[SCP-FINISH]` or is recorded as non-responsive.
292
+ 3. `EXEC ASST` captures final read-screen snapshots and pane list.
293
+ 4. `EXEC QA` or a QA swarm reviews cleanup evidence.
294
+ 5. Close only panes marked `close_on_finish` and only after their state is captured.
295
+ 6. Retain UX/design, dirty, blocked, or explicitly parked panes.
296
+ 7. Record a final manifest with closed/retained/deferred disposition.
297
+
298
+ Never delete worktrees or local files during automated teardown unless exact entries were approved and proof was captured.
@@ -1,4 +1,4 @@
1
- version: 0.4.11
1
+ version: 0.4.13
2
2
  default_topology:
3
3
  executive_office:
4
4
  team: A
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
 
@@ -9,6 +9,7 @@ const PUBLIC_ROOTS = [
9
9
  "CLAUDE.md",
10
10
  "LICENSE",
11
11
  "package.json",
12
+ ".claude-plugin/",
12
13
  "docs/",
13
14
  "protocol/",
14
15
  "templates/",
@@ -17,6 +18,7 @@ const PUBLIC_ROOTS = [
17
18
  ];
18
19
 
19
20
  const EXCLUDED_PREFIXES = [".git/", "dist/", "node_modules/", "project/" + "planning" + "/", "." + "edge-" + "agentic" + "/local/", "tests/"];
21
+ export const FORBIDDEN_PUBLIC_PREFIXES = ["docs/handoffs/"];
20
22
 
21
23
  function walk(dir) {
22
24
  return readdirSync(dir).flatMap((entry) => {
@@ -46,6 +48,7 @@ function filesToAudit() {
46
48
  return `${tracked}\n${untracked}`
47
49
  .split("\n")
48
50
  .filter(Boolean)
51
+ .filter((file) => existsSync(file))
49
52
  .filter((file) => file !== "pnpm-lock.yaml")
50
53
  .filter(isPublicAuditFile);
51
54
  } catch {
@@ -53,11 +56,21 @@ function filesToAudit() {
53
56
  }
54
57
  }
55
58
 
59
+ function forbiddenPublicPathFindings() {
60
+ return FORBIDDEN_PUBLIC_PREFIXES.flatMap((prefix) => {
61
+ if (!existsSync(prefix)) return [];
62
+ return walk(prefix).map((file) => `${file}: internal handoff files must not exist under public docs`);
63
+ });
64
+ }
65
+
56
66
  const BUNDLED_DOC = new Set([
57
67
  "README.md",
58
68
  "docs/guides/quickstart-prompts.md",
59
69
  "protocol/bootstrap-" + "sk" + "ill.md",
60
- "plugins/sentinel-coordination-protocol/" + "sk" + "ills/sentinel-coordination-protocol/SK" + "ILL.md"
70
+ "protocol/skill-references/harness-skill-targets.md",
71
+ "plugins/odin-scp/" + "sk" + "ills/odin-scp/SK" + "ILL.md",
72
+ "plugins/odin-scp/" + "sk" + "ills/odin-scp/references/harness-skill-targets.md",
73
+ "plugins/odin-scp/" + "sk" + "ills/odin-scp/scripts/sync-installations.sh"
61
74
  ]);
62
75
 
63
76
  const forbidden = [
@@ -74,13 +87,20 @@ const forbidden = [
74
87
  "i"
75
88
  )
76
89
  },
77
- { name: "secret-looking assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i }
90
+ { name: "secret-looking quoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i },
91
+ { name: "secret-looking unquoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*[A-Za-z0-9._~+/=-]{16,}/i },
92
+ { name: "bearer token literal", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
93
+ { name: "URI credential literal", pattern: /[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s:@]+@/i }
78
94
  ];
79
95
 
80
96
  export function auditPublicSurface(fileTextByPath) {
81
97
  const findings = [];
82
98
  for (const [file, text] of Object.entries(fileTextByPath)) {
83
99
  if (!isPublicAuditFile(file)) continue;
100
+ if (FORBIDDEN_PUBLIC_PREFIXES.some((prefix) => file.startsWith(prefix))) {
101
+ findings.push(`${file}: internal handoff files must not exist under public docs`);
102
+ continue;
103
+ }
84
104
  for (const rule of forbidden) {
85
105
  if (rule.exemptFiles?.has(file)) continue;
86
106
  if (rule.pattern.test(text)) findings.push(`${file}: ${rule.name}`);
@@ -91,6 +111,15 @@ export function auditPublicSurface(fileTextByPath) {
91
111
 
92
112
  export function main() {
93
113
  const publicFiles = filesToAudit();
114
+ const forbiddenPublicFiles = publicFiles.filter((file) => FORBIDDEN_PUBLIC_PREFIXES.some((prefix) => file.startsWith(prefix)));
115
+ const forbiddenPathFindings = forbiddenPublicPathFindings();
116
+ if (forbiddenPublicFiles.length > 0) {
117
+ throw new Error(`Public surface audit failed: internal handoff files are public:\n${forbiddenPublicFiles.join("\n")}`);
118
+ }
119
+ if (forbiddenPathFindings.length > 0) {
120
+ throw new Error(`Public surface audit failed:\n${forbiddenPathFindings.join("\n")}`);
121
+ }
122
+
94
123
  const findings = auditPublicSurface(Object.fromEntries(publicFiles.map((file) => [file, readFileSync(file, "utf8")])));
95
124
 
96
125
  if (findings.length > 0) {