@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.
- package/.claude-plugin/marketplace.json +23 -0
- package/README.md +20 -16
- package/dist/src/mcp/server.js +32 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/protocol/repository.d.ts +6 -0
- package/dist/src/protocol/repository.js +12 -2
- package/dist/src/protocol/repository.js.map +1 -1
- package/dist/src/protocol/service.js +17 -9
- package/dist/src/protocol/service.js.map +1 -1
- package/dist/src/protocol/version.d.ts +2 -2
- package/dist/src/protocol/version.js +2 -2
- package/docs/guides/quick-start.md +7 -7
- package/docs/guides/quickstart-prompts.md +4 -4
- package/docs/reference/client-compatibility.md +1 -1
- package/docs/reference/distribution.md +11 -5
- package/docs/reference/public-surface-audit.md +2 -2
- package/package.json +5 -3
- package/plugins/odin-scp/.claude-plugin/plugin.json +25 -0
- package/plugins/odin-scp/README.md +62 -0
- package/plugins/odin-scp/skills/odin-scp/CHANGELOG.md +25 -0
- package/plugins/odin-scp/skills/odin-scp/SKILL.md +1518 -0
- package/plugins/odin-scp/skills/odin-scp/agents/openai.yaml +4 -0
- package/plugins/odin-scp/skills/odin-scp/references/boot-receipt-examples.md +439 -0
- package/plugins/odin-scp/skills/odin-scp/references/canonical-introduction-prompt.md +118 -0
- package/plugins/odin-scp/skills/odin-scp/references/harness-skill-targets.md +56 -0
- package/plugins/odin-scp/skills/odin-scp/references/team-bootstrap-runbook.md +298 -0
- package/plugins/odin-scp/skills/odin-scp/scripts/sync-installations.sh +233 -0
- package/protocol/SCP.md +3 -3
- package/protocol/bootstrap-skill.md +22 -13
- package/protocol/closeout.yaml +1 -1
- package/protocol/delegation.yaml +1 -1
- package/protocol/model-profiles.yaml +2 -2
- package/protocol/receipts/team-manifest.yaml +1 -1
- package/protocol/roles.yaml +1 -1
- package/protocol/skill-references/boot-receipt-examples.md +439 -0
- package/protocol/skill-references/canonical-introduction-prompt.md +118 -0
- package/protocol/skill-references/harness-skill-targets.md +56 -0
- package/protocol/skill-references/team-bootstrap-runbook.md +298 -0
- package/protocol/topology.yaml +1 -1
- package/scripts/audit/public-surface.mjs +32 -3
- package/scripts/audit/verify-pack.mjs +175 -12
- 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.
|
package/protocol/topology.yaml
CHANGED
|
@@ -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
|
-
"
|
|
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) {
|