@eir-labs/coltrane 0.10.0 → 0.11.0

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 (138) hide show
  1. package/agents/bandleader.json +54 -0
  2. package/agents/bill.json +4 -2
  3. package/agents/change-verifier.json +2 -1
  4. package/agents/code-implementer.json +7 -4
  5. package/agents/john.json +5 -3
  6. package/agents/lineage-scout-external.json +19 -6
  7. package/agents/lineage-scribe.json +20 -9
  8. package/agents/lineage-weaver.json +26 -11
  9. package/agents/miles.json +2 -1
  10. package/agents/players/bandleader.md +70 -0
  11. package/agents/pr-publisher.json +4 -2
  12. package/agents/red-spec-drafter.json +12 -6
  13. package/agents/room-prober.json +29 -0
  14. package/agents/source-walker.json +1 -1
  15. package/charts/room-probe-chart-v1.json +14 -0
  16. package/charts/worker-contract-docker-v2.json +135 -0
  17. package/charts/worker-contract-v3.json +206 -0
  18. package/dist/src/boundary_check.d.ts +18 -0
  19. package/dist/src/boundary_check.js +70 -0
  20. package/dist/src/boundary_check.js.map +1 -0
  21. package/dist/src/boundary_scan.d.ts +62 -0
  22. package/dist/src/boundary_scan.js +93 -0
  23. package/dist/src/boundary_scan.js.map +1 -0
  24. package/dist/src/chart.js +34 -1
  25. package/dist/src/chart.js.map +1 -1
  26. package/dist/src/claude_invoker.d.ts +2 -1
  27. package/dist/src/claude_invoker.js +150 -17
  28. package/dist/src/claude_invoker.js.map +1 -1
  29. package/dist/src/cli.d.ts +1 -1
  30. package/dist/src/cli.js +61 -26
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/drain_preflight.d.ts +33 -0
  33. package/dist/src/drain_preflight.js +97 -0
  34. package/dist/src/drain_preflight.js.map +1 -0
  35. package/dist/src/genome_schema.d.ts +390 -9
  36. package/dist/src/genome_schema.js +163 -8
  37. package/dist/src/genome_schema.js.map +1 -1
  38. package/dist/src/genome_store.js +9 -0
  39. package/dist/src/genome_store.js.map +1 -1
  40. package/dist/src/genome_writer.d.ts +1 -1
  41. package/dist/src/genome_writer.js +10 -2
  42. package/dist/src/genome_writer.js.map +1 -1
  43. package/dist/src/gig_conformance.d.ts +66 -0
  44. package/dist/src/gig_conformance.js +153 -0
  45. package/dist/src/gig_conformance.js.map +1 -0
  46. package/dist/src/gig_tracker.js +8 -0
  47. package/dist/src/gig_tracker.js.map +1 -1
  48. package/dist/src/grounding.d.ts +44 -0
  49. package/dist/src/grounding.js +127 -0
  50. package/dist/src/grounding.js.map +1 -0
  51. package/dist/src/index.d.ts +1 -0
  52. package/dist/src/index.js +1 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/institution_loader.d.ts +29 -5
  55. package/dist/src/institution_loader.js +67 -6
  56. package/dist/src/institution_loader.js.map +1 -1
  57. package/dist/src/lineage_adoption.d.ts +82 -0
  58. package/dist/src/lineage_adoption.js +82 -0
  59. package/dist/src/lineage_adoption.js.map +1 -0
  60. package/dist/src/lineage_persist.d.ts +34 -0
  61. package/dist/src/lineage_persist.js +60 -0
  62. package/dist/src/lineage_persist.js.map +1 -0
  63. package/dist/src/loader.d.ts +13 -0
  64. package/dist/src/loader.js +43 -2
  65. package/dist/src/loader.js.map +1 -1
  66. package/dist/src/mcp.js +37 -4
  67. package/dist/src/mcp.js.map +1 -1
  68. package/dist/src/org_hire.d.ts +23 -0
  69. package/dist/src/org_hire.js +42 -0
  70. package/dist/src/org_hire.js.map +1 -0
  71. package/dist/src/outputs.d.ts +17 -0
  72. package/dist/src/outputs.js +86 -1
  73. package/dist/src/outputs.js.map +1 -1
  74. package/dist/src/player_to_claude_code.js +26 -2
  75. package/dist/src/player_to_claude_code.js.map +1 -1
  76. package/dist/src/registry.d.ts +3 -1
  77. package/dist/src/registry.js +43 -3
  78. package/dist/src/registry.js.map +1 -1
  79. package/dist/src/repo_index.d.ts +58 -0
  80. package/dist/src/repo_index.js +232 -0
  81. package/dist/src/repo_index.js.map +1 -0
  82. package/dist/src/reuse.d.ts +44 -4
  83. package/dist/src/reuse.js +0 -0
  84. package/dist/src/reuse.js.map +1 -1
  85. package/dist/src/runtime.d.ts +82 -0
  86. package/dist/src/runtime.js +254 -25
  87. package/dist/src/runtime.js.map +1 -1
  88. package/dist/src/server.d.ts +37 -0
  89. package/dist/src/server.js +422 -11
  90. package/dist/src/server.js.map +1 -1
  91. package/dist/src/server_relay.d.ts +40 -2
  92. package/dist/src/server_relay.js +186 -19
  93. package/dist/src/server_relay.js.map +1 -1
  94. package/dist/src/tool_providers.d.ts +27 -0
  95. package/dist/src/tool_providers.js +48 -0
  96. package/dist/src/tool_providers.js.map +1 -1
  97. package/dist/src/venue_credential.d.ts +41 -0
  98. package/dist/src/venue_credential.js +57 -0
  99. package/dist/src/venue_credential.js.map +1 -0
  100. package/dist/src/venue_realize.d.ts +23 -0
  101. package/dist/src/venue_realize.js +43 -3
  102. package/dist/src/venue_realize.js.map +1 -1
  103. package/dist/src/venue_realizer.d.ts +224 -0
  104. package/dist/src/venue_realizer.js +889 -0
  105. package/dist/src/venue_realizer.js.map +1 -0
  106. package/dist/src/version.d.ts +1 -1
  107. package/dist/src/version.js +1 -1
  108. package/dist/src/worker.d.ts +37 -0
  109. package/dist/src/worker.js +127 -24
  110. package/dist/src/worker.js.map +1 -1
  111. package/dist/src/worker_env.d.ts +79 -0
  112. package/dist/src/worker_env.js +322 -0
  113. package/dist/src/worker_env.js.map +1 -0
  114. package/dist/src/workspace.d.ts +12 -1
  115. package/dist/src/workspace.js +10 -3
  116. package/dist/src/workspace.js.map +1 -1
  117. package/domain_types/change-context.json +59 -13
  118. package/domain_types/lineage-adoption-target.json +22 -0
  119. package/domain_types/lineage-map.json +7 -2
  120. package/domain_types/lineage-record.json +55 -5
  121. package/domain_types/prior-art-hit.json +16 -1
  122. package/domain_types/red-spec.json +5 -3
  123. package/domain_types/woodshed-record.json +149 -0
  124. package/evals/gist-present.json +7 -0
  125. package/institutions/coltrane.json +178 -6
  126. package/package.json +6 -3
  127. package/skills/non-vacuity-probe/fixtures/fixture-001.json +17 -0
  128. package/skills/non-vacuity-probe/meta.json +12 -0
  129. package/skills/non-vacuity-probe/skill.md +27 -0
  130. package/skills/non-vacuity-probe/skill.mjs +81 -0
  131. package/standards/lineage-adopt-v0.json +37 -0
  132. package/standards/room-probe-v1.json +28 -0
  133. package/standards/software-change-pr-v1.json +15 -15
  134. package/standards/software-change-red-first-v0.json +183 -0
  135. package/standards/spec-review-and-sequence-v0.json +112 -0
  136. package/standards/studio-session-v0.json +101 -0
  137. package/tours/coltrane.json +72 -0
  138. package/venues/engine-room-v1.json +32 -0
@@ -0,0 +1,135 @@
1
+ {
2
+ "slug": "worker-contract-docker-v2",
3
+ "movements": [
4
+ {
5
+ "movement_id": "band-renderer",
6
+ "standard_slug": "software-change-pr-v1",
7
+ "runtime_fills": {
8
+ "change-request": {
9
+ "source": "SPEC-worker-contract.md Gap 6, renderer half; laws in tests/spec_venue_realization_substrate.test.ts; sequenced as package P1 (split) by change-plan of gig 73ffa76e",
10
+ "requested_by": "eugene",
11
+ "repository": "eir-labs/coltrane",
12
+ "request_text": "BEFORE WRITING ANY FILE, resolve one open question the sequencing seat flagged: read src/tool_providers.ts around line 153 and confirm the current arity of assertToolGrantsResolvable. Gap 2's drift-guard law calls it with a fourth argument (a realized mcpServerConfigs map). If it takes three today, the change is an additive optional fourth parameter that leaves every existing caller unchanged. Record what you found; a later package depends on it.\n\nTHEN build the seam and the renderer, and make these four families of tests/spec_venue_realization_substrate.test.ts green without weakening one law: 'the substrate is a named, injectable seam with two implementations' (8), 'the renderer emits only from a closed allowlist' (8), 'the renderer refuses a forbidden value smuggled through an allowed field' (2), and 'credentials reach the room only through the resolver' (2). Twenty laws. This ships first because every other gap in the specification fails visibly — a capability is missing and somebody notices — while a permissive realizer fails invisibly: the venue still claims the guarantee and there is a real escape at the end of it.\n\nExtend VenueSchema in src/genome_schema.ts first, following that file's existing grain: it is the one source every restatement derives from, so add the fields there and let the TypeScript type, the generated MCP input_schema and the loader check follow. Do not hand-edit any of the three to match. Re-run the genome regression gate after the schema change (tests/skills_agent_carried.test.ts, tests/genome_schema_drift.test.ts, tests/genome_browse_parity.test.ts, and any genome-hash stability test) before going further — a schema addition must not move a hash or reject an existing venue.\n\nThen create src/venue_realizer.ts, exporting at minimum: the VenueRealizer interface exactly as the test file's own type block declares it; localProcessRealizer and dockerComposeRealizer, each naming its own substrate and its own guarantees drawn from the closed set withholds_capabilities, isolated_filesystem, network_policy_doors, reproducible_tool_surface, per_chair_isolation; selectRealizer, which throws VenueSubstrateUnavailable naming what was required and what is available and NEVER falls back to a weaker realizer; renderComposeConfig; COMPOSE_SUBSTITUTABLE_FIELDS; and VenueRenderRefusal carrying a .forbidden field.\n\nFOUR PROPERTIES THE LAWS PIN. One: localProcessRealizer is the baseline, must work on a host with no container runtime and no daemon installed, and must claim NO guarantee it cannot keep — specifically not withholds_capabilities and not network_policy_doors, because src/workspace.ts:44-56 is already candid that process-level protection is not a security control and must not be described as one. Two: renderComposeConfig accepts only the PARSED schema object and refuses raw input — VenueSchema is already .strict() and that is the enforcement point, so handing the renderer unparsed input walks around it. Three: the renderer EMITS only from the closed allowlist, and each forbidden setting is its own law — container runtime socket, host networking, host PID namespace, privileged mode, added capabilities, and any mount whose source is not derived from the per-realization directory. Implement them individually. Four: the renderer REFUSES a forbidden value smuggled through an ALLOWLISTED field, naming the field and the value — an allowlist over field names says nothing about what those fields contain, and this is the half a naive allowlist misses. Credentials reach the room only through CredentialResolver, bound at realization time from credential_names as a subset of credential_surface: render the credential CLASS, never the material, and never inherit the host environment wholesale, which would hand the room the drain's own venue credential.",
13
+ "target_paths": [
14
+ "src/venue_realizer.ts",
15
+ "src/genome_schema.ts",
16
+ "src/venue_realize.ts",
17
+ "src/workspace.ts",
18
+ "src/tool_providers.ts",
19
+ "tests/spec_venue_realization_substrate.test.ts"
20
+ ],
21
+ "acceptance_criteria": [
22
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts -t 'the substrate is a named, injectable seam with two implementations' reports 8 passed, 0 failed.",
23
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts -t 'the renderer emits only from a closed allowlist' reports 8 passed, 0 failed.",
24
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts -t 'the renderer refuses a forbidden value smuggled through an allowed field' reports 2 passed, 0 failed.",
25
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts -t 'credentials reach the room only through the resolver' reports 2 passed, 0 failed.",
26
+ "THE HOLLOW DEMONSTRATION, which is a check you RUN and not an argument you make. Temporarily replace renderComposeConfig's body with `return {}` and re-run the four families above. Report, law by law, which ones still pass. Then restore the real implementation and confirm the counts above again. A law that passes under the stub proves nothing about the implementation, and counting it as green is exactly the hollow-green this repository forbids. KNOWN BASELINE, established by counting assertions in the committed source: five of the eight laws under 'the renderer emits only from a closed allowlist' have ZERO positive assertions ('never emits a container runtime socket', 'never emits host networking', 'never emits the host PID namespace', 'never emits privileged mode', 'never emits added capabilities'), the mount-source law's only assertion sits inside a loop that does not execute on an empty collection, and 'refuses raw input' asserts toBeTruthy which {} satisfies. Expect roughly eight survivors. If you measure materially fewer, say why; if you measure more, that is a finding. This baseline is given so the demonstration is a verification and not a discovery.",
27
+ "git diff --stat 3237650db7553430e29640e95d62cf651bbe6748 -- tests/spec_venue_realization_substrate.test.ts tests/spec_venue_realization.test.ts tests/spec_worker_environment.test.ts tests/spec_worker_run_modes.test.ts tests/spec_venue_targeting.test.ts tests/spec_venue_credential_mint.test.ts SPEC-worker-contract.md produces NO output. The six law files and the specification are byte-identical to what PR #379 landed: no it.skip, no relaxed matcher, no deleted assertion, no specifier converted back to a static import.",
28
+ "npx tsc --noEmit exits clean.",
29
+ "npx vitest run produces no failure in any file NOT named spec_*. Report the exact failed/passed totals from the real run and compare them to the recorded baseline of 89 failed / 2630 passed on this branch. The remaining Gap 6 laws and Gaps 1-5 stay red; that is expected and is not a regression.",
30
+ "State whether localProcessRealizer was demonstrated to work with no container runtime present, and if that could not be established on this machine, say so plainly rather than asserting it.",
31
+ "Report the arity of assertToolGrantsResolvable as found, since the next packages depend on it."
32
+ ],
33
+ "out_of_scope": [
34
+ "Device classes, device grants, architecture, remote realization hosts, reconciliation and retention, and the environment-identity and concurrency laws — all the next movement's work.",
35
+ "Gap 2's realizeVenue state machine and the tools/list probe — the movement after that.",
36
+ "Gaps 1, 3, 4 and 5 entirely.",
37
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
38
+ "Kernel-level enforcement of doors: the specification names it out of scope, and a realizer may CLAIM network_policy_doors only if it actually enforces it.",
39
+ "Actually starting a container. These laws assert on declared guarantees and on the rendered configuration document; running one is a later step and must not be attempted here."
40
+ ]
41
+ }
42
+ },
43
+ "seatings": []
44
+ },
45
+ {
46
+ "movement_id": "band-realizer",
47
+ "standard_slug": "software-change-pr-v1",
48
+ "runtime_fills": {
49
+ "change-request": {
50
+ "source": "SPEC-worker-contract.md Gap 6, realizer half; the remaining 20 laws of tests/spec_venue_realization_substrate.test.ts",
51
+ "requested_by": "eugene",
52
+ "repository": "eir-labs/coltrane",
53
+ "request_text": "On the seam and renderer the previous movement landed, close the remaining twenty laws of tests/spec_venue_realization_substrate.test.ts: 'a venue asks for a KIND of device, never a path' (3), 'a device grant maps exactly what was declared, and widens nothing' (4), 'architecture is part of the contract' (2), 'realizing somewhere else weakens nothing' (3), 'realization is reconciled, because a killed worker runs no finally' (5), and 'the environment is a function of the contract' (3).\n\nDEVICES ARE A CAPABILITY GRANT WEARING THE COSTUME OF CONFIGURATION, and that is the whole reason they are spec'd this way: a venue that can name an arbitrary device path can name the raw memory device or a whole block device, which is a host compromise submitted as a hardware request. Export DEVICE_CLASSES as an exact closed list — serial, gpio, i2c, spi, video, audio — extend VenueSchema to accept a device class and an architecture list, and refuse a raw device path ON THE VALUE, not on an unknown key. That distinction IS the law: a .strict() schema rejecting an unknown key would pass the test for the wrong reason. The class-to-node mapping belongs in the realizer, never in the contract, so it is audited once rather than in every venue in the genome forever. A grant maps exactly the declared class's nodes and GRANTS THE OWNING GROUP — this is the practical half and where escalation actually happens, because the node is mapped, the process still cannot open it, and the reflexive fix is to escalate the whole room to privileged, undoing every boundary the previous movement built in order to solve a permissions problem. A device grant WIDENS NOTHING ELSE: assert on the realized configuration for the ABSENCE of privileged mode, of added capabilities, and of any broadened device rule, because the failure here is something extra appearing rather than something missing. A class the host cannot provide is a REFUSAL reaching no room, throwing VenueHostUnsuitable. Do not reach for either hot-plug workaround — mapping the whole device tree, or a broad device rule — the specification names both as the escape under another name.\n\nA ROOM NEED NOT BE REALIZED WHERE THE WORKER RUNS. The contract stays byte-identical when the host is remote, every forbidden setting stays forbidden, and the host credential is obtained PER GIG against a live lease — never at boot, never rendered into the configuration — following the discipline src/workspace.ts already sets.\n\nRECONCILIATION, NOT CLEANUP, because a killed worker runs no finally. Every realized artifact carries the gig and the instance that created it; a sweep collects artifacts belonging to no LIVE gig, where the live set is HOST-WIDE so two workers on one box never garbage-collect each other; a realizer declares a retention policy with real ceilings and a real cadence, never 'never'; and every rendered configuration carries a log bound.",
54
+ "target_paths": [
55
+ "src/venue_realizer.ts",
56
+ "src/genome_schema.ts",
57
+ "src/workspace.ts",
58
+ "tests/spec_venue_realization_substrate.test.ts"
59
+ ],
60
+ "acceptance_criteria": [
61
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts reports 40 passed, 0 failed — the whole file, this movement's twenty plus the previous movement's twenty.",
62
+ "The previous movement's twenty laws are re-run and reported individually. A schema change is exactly the kind of edit that silently breaks a renderer allowlist, so their continued green is a claim that must be measured, not assumed.",
63
+ "THE HOLLOW DEMONSTRATION, run again for this movement's families: stub the device-mapping and reconciliation surfaces to no-ops, re-run the six families above, and report which laws still pass. Restore afterwards. Any law surviving the stub is reported as surviving rather than counted as green.",
64
+ "git diff --stat 3237650db7553430e29640e95d62cf651bbe6748 -- tests/ SPEC-worker-contract.md produces NO output: the law files and the specification are byte-identical to PR #379's state.",
65
+ "npx tsc --noEmit exits clean, and npx vitest run shows no failure outside spec_*-named files. Report exact totals from the real run.",
66
+ "The device-class-to-node mapping is shown to live in the realizer and not in any venue in the genome — state where it lives."
67
+ ],
68
+ "out_of_scope": [
69
+ "Gap 2's realizeVenue state machine, mcp_servers declarations and the tools/list probe — the next movement.",
70
+ "Gaps 1, 3, 4 and 5.",
71
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
72
+ "Hot-plug device support, and both of its familiar workarounds.",
73
+ "Starting a real container."
74
+ ]
75
+ }
76
+ },
77
+ "seatings": []
78
+ },
79
+ {
80
+ "movement_id": "band-gap2-realize-venue",
81
+ "standard_slug": "software-change-pr-v1",
82
+ "runtime_fills": {
83
+ "change-request": {
84
+ "source": "SPEC-worker-contract.md Gap 2; laws in tests/spec_venue_realization.test.ts; sequenced as package P2 by change-plan of gig 73ffa76e",
85
+ "requested_by": "eugene",
86
+ "repository": "eir-labs/coltrane",
87
+ "request_text": "Close the ten laws of tests/spec_venue_realization.test.ts, extending the same src/venue_realizer.ts module the previous two movements built — this package is ordered here because it shares that module path and adds to the schema fields already in place.\n\nA VENUE MUST BE ABLE TO DECLARE WHAT PROVIDES ITS TOOLS. Add mcp_servers to VenueSchema: a venue granting an mcp server it does not declare is refused, naming the slug; each transport requires the field it needs (a command for stdio, a url for sse); and a credential_name the room's credential_surface does not declare is refused. Follow the schema's existing grain and let the generated surfaces follow rather than hand-editing them.\n\nTHEN realizeVenue BUILDS THE ENVIRONMENT FROM THE CONTRACT AND NOTHING ELSE. The empty room traverses the machine with no probe and no declared servers — zero child processes, which is the property that must survive the introduction of a substrate seam. The realized map holds the declared servers plus the engine entries, with declared credentials resolved through CredentialResolver, and it holds NOTHING the venue did not declare. The ambient .mcp.json is NEVER merged, whether it would introduce a server or override one; today src/server.ts:1123 states in one line that the config comes from ambient, and that is the line this closes. The realized map is what fails a tool grant closed.\n\nTHE PROBE VERIFIES THE ROOM IN BOTH DIRECTIONS, BEFORE ANYTHING SPAWNS: a granted tool the server does not advertise fails realization naming both grant and server, and a server WIDER than the contract is a violation rather than a bonus. Two distinct named error classes with non-overlapping fields — VenueRealizationError (.state, .missingGrant, .serverSlug) and VenueContractViolation (.state, .extraTool, .serverSlug) — because the two cases demand different remediation and collapsing them into one type makes the law ambiguous. Use the assertToolGrantsResolvable arity the first movement reported; if it takes three parameters today, add an optional fourth additively so every existing caller is unchanged.",
88
+ "target_paths": [
89
+ "src/venue_realizer.ts",
90
+ "src/genome_schema.ts",
91
+ "src/tool_providers.ts",
92
+ "src/server.ts",
93
+ "tests/spec_venue_realization.test.ts"
94
+ ],
95
+ "acceptance_criteria": [
96
+ "npx vitest run tests/spec_venue_realization.test.ts reports 10 passed, 0 failed.",
97
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts still reports 40 passed, 0 failed — measured, not assumed.",
98
+ "THE HOLLOW DEMONSTRATION for this movement: a realizeVenue that always resolves to PLAYING, and one that always throws, are each run against the ten laws and the surviving laws reported. Restore afterwards.",
99
+ "The ambient-exclusion law is shown to be exercised against a real temporary directory containing a real .mcp.json, so that it tests exclusion rather than absence.",
100
+ "git diff --stat 3237650db7553430e29640e95d62cf651bbe6748 -- tests/ SPEC-worker-contract.md produces NO output.",
101
+ "npx tsc --noEmit exits clean, and npx vitest run shows no failure outside spec_*-named files. Report exact totals. After this movement 50 of the 88 laws are green; Gaps 1, 3, 4 and 5 stay red and that is expected.",
102
+ "State whether assertToolGrantsResolvable needed a signature change and, if so, that every existing caller is unchanged."
103
+ ],
104
+ "out_of_scope": [
105
+ "Gaps 1, 3, 4 and 5 — a later chart, in the sequenced order P3 Gap 3, P4 Gap 4, P5 Gap 5, P6 Gap 1.",
106
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
107
+ "Kernel-level enforcement of doors.",
108
+ "Starting a real container."
109
+ ]
110
+ }
111
+ },
112
+ "seatings": []
113
+ }
114
+ ],
115
+ "edges": [],
116
+ "approval_gates": [
117
+ {
118
+ "gate_id": "renderer-landed",
119
+ "after_movement": "band-renderer",
120
+ "before_movement": "band-realizer",
121
+ "chair": "quartet.chair.responsible-officer",
122
+ "prompt": "Read the change-verdict and open the pull request. Do not approve on the pass counts alone — they are the weakest evidence here. Read the HOLLOW DEMONSTRATION first: the verdict must report, law by law, which of the four families still passed with renderComposeConfig stubbed to return {}. Roughly eight survivors is the expected baseline and confirms the demonstration was actually run; a report of zero survivors means it was not run honestly, and that is a reason to refuse. Then confirm the six law files and SPEC-worker-contract.md are byte-identical to PR #379, that localProcessRealizer claims neither withholds_capabilities nor network_policy_doors, and that the renderer refuses a forbidden value smuggled through an allowed field rather than only refusing forbidden field names. The next movement mounts host devices through this renderer — if the allowlist and the value-level refusal are not genuinely closed, a device grant is the escape they exist to prevent. This is the gate where a permissive renderer must be caught."
123
+ },
124
+ {
125
+ "gate_id": "realizer-landed",
126
+ "after_movement": "band-realizer",
127
+ "before_movement": "band-gap2-realize-venue",
128
+ "chair": "quartet.chair.responsible-officer",
129
+ "prompt": "Read the change-verdict and the pull request. Confirm the whole substrate file reports 40 passed and that the previous movement's twenty were re-measured rather than assumed — a schema change is the classic way an allowlist quietly breaks. Confirm a device grant produces no privileged mode, no added capabilities and no broadened device rule, asserted for ABSENCE, since the failure mode here is something extra appearing. Confirm the class-to-node mapping lives in the realizer and not in any venue. The next movement extends the same module into Gap 2's realizeVenue, so it inherits whatever this one got wrong."
130
+ }
131
+ ],
132
+ "budget_envelope": {
133
+ "total_usd": 150
134
+ }
135
+ }
@@ -0,0 +1,206 @@
1
+ {
2
+ "slug": "worker-contract-v3",
3
+ "movements": [
4
+ {
5
+ "movement_id": "gap2-realize-venue",
6
+ "standard_slug": "software-change-pr-v1",
7
+ "runtime_fills": {
8
+ "change-request": {
9
+ "source": "SPEC-worker-contract.md Gap 2; tests/spec_venue_realization.test.ts (9 of 10 laws still red)",
10
+ "requested_by": "eugene",
11
+ "repository": "eir-labs/coltrane",
12
+ "request_text": "Close tests/spec_venue_realization.test.ts, extending src/venue_realizer.ts which already exists and holds Gap 6 green at 40/40. Do not rewrite that module; add to it.\n\nA VENUE MUST DECLARE WHAT PROVIDES ITS TOOLS. VenueSchema already accepts mcp_servers. Add the cross-field rules: a venue granting an mcp server it does not declare is refused naming the slug; each transport requires the field it needs (a command for stdio, a url for sse); a credential_name the room's credential_surface does not declare is refused. Change the Zod schema and let the generated MCP input_schema and the loader check follow — and then make sure the venue_define HANDLER reads whatever you add, because tests/advertised_args_are_read.test.ts enforces that a tool never advertises an argument it never reads, and that law already caught this exact omission once on this branch.\n\nrealizeVenue BUILDS FROM THE CONTRACT AND NOTHING ELSE. The empty room traverses with no probe and no declared servers — zero child processes. The realized map holds the declared servers plus the engine entries with credentials resolved through CredentialResolver, and holds nothing the venue did not declare. The ambient .mcp.json is NEVER merged, whether it would add a server or override one; src/server.ts:1123 states in one line that the config comes from ambient, and that is the line this closes. The realized map is what fails a tool grant closed.\n\nTHE PROBE VERIFIES IN BOTH DIRECTIONS BEFORE ANYTHING SPAWNS: a granted tool the server does not advertise fails realization naming grant and server; a server WIDER than the contract is a violation, not a bonus. Two named error classes with non-overlapping fields — VenueRealizationError (.state, .missingGrant, .serverSlug) and VenueContractViolation (.state, .extraTool, .serverSlug) — because the two demand different remediation and collapsing them makes the law ambiguous.\n\nALREADY ANSWERED, do not re-investigate: assertToolGrantsResolvable at src/tool_providers.ts:153 ALREADY takes four parameters, the fourth being mcpServerConfigs with a default. No signature change is needed.\n\nYOU OWN THE HOLLOW DEMONSTRATION — it is your obligation, not the verifier's, because it requires Edit and a verify seat holds no write grant by design. Before you seal: stub your new surface to its emptiest form (a realizeVenue that always resolves to PLAYING, and separately one that always throws), re-run this file, and record law by law which ones still pass. Restore your real implementation and re-confirm. Report the survivor list IN THE CHANGE-SET. A law that passes against a hollow implementation is not evidence, and on this branch that measurement found 32 of Gap 6's 40 laws passing against a renderer that emitted nothing.",
13
+ "target_paths": [
14
+ "src/venue_realizer.ts",
15
+ "src/genome_schema.ts",
16
+ "src/server.ts",
17
+ "src/tool_providers.ts",
18
+ "tests/spec_venue_realization.test.ts"
19
+ ],
20
+ "acceptance_criteria": [
21
+ "npx vitest run tests/spec_venue_realization.test.ts reports 10 passed, 0 failed.",
22
+ "npx vitest run tests/spec_venue_realization_substrate.test.ts still reports 40 passed, 0 failed — measured in this run, not assumed from the previous band.",
23
+ "npx vitest run tests/advertised_args_are_read.test.ts reports 0 failed. Any schema field added must be read by its handler.",
24
+ "The change-set CONTAINS the hollow demonstration: the stub used, the law-by-law survivor list, and confirmation the real implementation was restored. A change-set without it is incomplete and the verdict should say so rather than infer the demonstration happened.",
25
+ "git diff --stat origin/main -- tests/spec_venue_realization.test.ts shows no change: this law file is not edited.",
26
+ "npx vitest run shows no failure outside spec_*-named files. Report exact totals against this branch's baseline of 47 failed / 2672 passed. After this movement 57 of 88 laws are green."
27
+ ],
28
+ "out_of_scope": [
29
+ "Gaps 1, 3, 4 and 5 — later movements of this same chart.",
30
+ "Editing any spec_* test file, or SPEC-worker-contract.md.",
31
+ "Rewriting the Gap 6 surface in src/venue_realizer.ts — it is green; extend it.",
32
+ "Kernel-level enforcement of doors.",
33
+ "Starting a real container."
34
+ ]
35
+ }
36
+ },
37
+ "seatings": []
38
+ },
39
+ {
40
+ "movement_id": "gap3-worker-environment",
41
+ "standard_slug": "software-change-pr-v1",
42
+ "runtime_fills": {
43
+ "change-request": {
44
+ "source": "SPEC-worker-contract.md Gap 3; tests/spec_worker_environment.test.ts (14 laws)",
45
+ "requested_by": "eugene",
46
+ "repository": "eir-labs/coltrane",
47
+ "request_text": "Create src/worker_env.ts and close tests/spec_worker_environment.test.ts. This is the one gap in the specification that is a BREAK rather than an addition: a worker whose environment names the wrong host boots today and refuses afterwards, which is the point.\n\nONE VARIABLE HAD THREE READERS AND TWO MEANINGS. COLTRANE_DRAIN_URL's own comment said 'ONE VARIABLE, TWO CONTRACTS' and the count was already wrong. Pointed at the database, rows wrote and the artifact 401'd forever, so the only symptom was a missing blob; repointed at the service, boot-time provisioning broke against a 307. Export WORKER_ENV_CONTRACT as one enumerated table where every variable names its host, its role and its meaning; COLTRANE_SERVICE_URL is canonical and COLTRANE_DRAIN_URL survives only as a normalized legacy alias. Two hosts are two variables, with exactly one always-required url per host.\n\nA LEGACY SHAPE NORMALIZES, IT NEVER APPENDS: normalizeWorkerEnv strips a legacy path suffix rather than building on top of it, maps the legacy name onto the canonical one, treats the hosting provider's app name as a legacy alias of the instance, and is IDEMPOTENT — applying it twice changes nothing.\n\nA MISCONFIGURED WORKER REFUSES AT STARTUP IN ITS OWN VOICE: assertWorkerEnv accepts a complete venue environment and a complete player environment, names the required variable that is absent, refuses an environment where the store and the service name the same host, refuses a service url that names the managed database BEFORE the first request, refuses a legacy alias contradicting the canonical name, and refuses a url-shaped variable that is not a url.\n\nTHE COMPLETENESS LAW READS SOURCE TEXT, and that is deliberate: every process.env double-bracket read across the worker path (worker.ts, cli.ts, output_mirror.ts, workspace.ts, run_deps.ts, worker_env.ts) must appear in WORKER_ENV_CONTRACT. It reads code for the same reason tests/advertised_args_are_read.test.ts does — the property is a declaration matching what the code does, and only the code can testify to the second half. Enumerate those reads before you write the table.\n\nYOU OWN THE HOLLOW DEMONSTRATION: WORKER_ENV_CONTRACT={} , normalizeWorkerEnv=identity, assertWorkerEnv=noop. Run each against the file, record which laws still pass, restore, re-confirm, and report the survivors in the change-set.",
48
+ "target_paths": [
49
+ "src/worker_env.ts",
50
+ "src/worker.ts",
51
+ "src/cli.ts",
52
+ "src/output_mirror.ts",
53
+ "src/workspace.ts",
54
+ "src/run_deps.ts",
55
+ "tests/spec_worker_environment.test.ts"
56
+ ],
57
+ "acceptance_criteria": [
58
+ "npx vitest run tests/spec_worker_environment.test.ts reports 14 passed, 0 failed.",
59
+ "The change-set contains the hollow demonstration with its law-by-law survivor list and confirmation of restore.",
60
+ "The completeness law is satisfied against the REAL source: state how many process.env reads were enumerated and from which files.",
61
+ "npx vitest run shows no failure outside spec_*-named files, and tests/spec_venue_realization.test.ts and tests/spec_venue_realization_substrate.test.ts stay green — measured. After this movement 71 of 88 laws are green.",
62
+ "The CHANGELOG records this as a BREAKING change under the repo's inverted semver: a worker whose environment names the wrong host boots today and refuses after."
63
+ ],
64
+ "out_of_scope": [
65
+ "Gaps 1, 4 and 5.",
66
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
67
+ "Moving the claim path behind the service seam — the specification names it as direction and does not settle it.",
68
+ "Deciding how long the legacy alias is tolerated — a release decision the document leaves to the changelog."
69
+ ]
70
+ }
71
+ },
72
+ "seatings": []
73
+ },
74
+ {
75
+ "movement_id": "gap4-run-modes",
76
+ "standard_slug": "software-change-pr-v1",
77
+ "runtime_fills": {
78
+ "change-request": {
79
+ "source": "SPEC-worker-contract.md Gap 4; tests/spec_worker_run_modes.test.ts (9 laws)",
80
+ "requested_by": "eugene",
81
+ "repository": "eir-labs/coltrane",
82
+ "request_text": "Extend src/worker_env.ts with workerCredentialMode and close tests/spec_worker_run_modes.test.ts.\n\nTHE DEFECT IS THE DERIVATION, NOT THE CHOICE. cli.ts:219 computes venueMode, worker.ts:320 re-derives it, and worker.ts:345 carries a defensive branch that exists only because the two might disagree. One function answers which mode a worker is in, carries the fields the answer needs, and owns the refusal text the CLI prints.\n\nFour cases, as a discriminated union: a venue credential with an instance is venue mode with its fields included; venue wins when both credentials are present; a player token alone is player mode; the hosting provider's app name still supplies the instance; a venue key with NO instance refuses even when a player token is present, naming COLTRANE_INSTANCE; no credential at all is a refusal that says what is missing.\n\nTHE CLI ASKS THE FUNCTION RATHER THAN DECIDING AGAIN: runCli prints the mode's own refusal text instead of deriving it inline. CliIO and runCli keep their shape. THE CLAIM ROUTES BY THE SAME ANSWER: a venue answer builds a venue claim, a player answer builds a player claim, and the defensive branch that existed because two derivations might disagree is deleted — two independent computations of one fact is the defect the single-sourcing law exists to catch.\n\nYOU OWN THE HOLLOW DEMONSTRATION: a workerCredentialMode always returning {mode:'none', why:''}, and a runCli that prints nothing. Record survivors, restore, report in the change-set.",
83
+ "target_paths": [
84
+ "src/worker_env.ts",
85
+ "src/cli.ts",
86
+ "src/worker.ts",
87
+ "tests/spec_worker_run_modes.test.ts"
88
+ ],
89
+ "acceptance_criteria": [
90
+ "npx vitest run tests/spec_worker_run_modes.test.ts reports 9 passed, 0 failed.",
91
+ "tests/spec_worker_environment.test.ts still reports 14 passed — measured, since this movement extends the same module.",
92
+ "The defensive branch at worker.ts:345 is GONE, and the change-set says where the single derivation now lives.",
93
+ "The change-set contains the hollow demonstration with survivors named and restore confirmed.",
94
+ "npx vitest run shows no failure outside spec_*-named files. After this movement 80 of 88 laws are green.",
95
+ "The change-set carries the deprecation recommendations the specification asks for."
96
+ ],
97
+ "out_of_scope": [
98
+ "Gaps 1 and 5.",
99
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
100
+ "Settling whether an instance name is per-app or per-machine — the specification explicitly declines to settle it."
101
+ ]
102
+ }
103
+ },
104
+ "seatings": []
105
+ },
106
+ {
107
+ "movement_id": "gap5-venue-targeting",
108
+ "standard_slug": "software-change-pr-v1",
109
+ "runtime_fills": {
110
+ "change-request": {
111
+ "source": "SPEC-worker-contract.md Gap 5; tests/spec_venue_targeting.test.ts (5 laws)",
112
+ "requested_by": "eugene",
113
+ "repository": "eir-labs/coltrane",
114
+ "request_text": "Close tests/spec_venue_targeting.test.ts. ChartSchema.venue exists and runGig honours it, but the claim takes any queued row of the org regardless — so a gig is taken by a worker that cannot realize its room while a capable worker sits idle.\n\nSurface the venue on the queued row: gig_dispatch advertises a venue property in MCP_TOOLS; postgrestQueueGig forwards p_venue in the RPC body and sends explicit p_venue = null when nothing names a room, never an omitted key; rpcQueueGig does the same. Remember tests/advertised_args_are_read.test.ts — an advertised argument must be read.\n\nShare ONE pure predicate: venueMayClaim(gigVenue, realizable). An UNNAMED gig stays claimable by anyone, so targeting never becomes mandatory routing; a named gig is claimable only by a worker that can build that room. claimNextGig applies it and carries venue on the result. The venue check is ADDITIVE to the existing venue/player claim branching — those tests must still pass.\n\nYOU OWN THE HOLLOW DEMONSTRATION: venueMayClaim always true, and a queue seam that omits p_venue. Record survivors, restore, report in the change-set.",
115
+ "target_paths": [
116
+ "src/worker.ts",
117
+ "src/genome_store.ts",
118
+ "src/mcp.ts",
119
+ "src/server.ts",
120
+ "tests/spec_venue_targeting.test.ts"
121
+ ],
122
+ "acceptance_criteria": [
123
+ "npx vitest run tests/spec_venue_targeting.test.ts reports 5 passed, 0 failed.",
124
+ "The existing claimNextGig venue/player tests still pass — measured, since this changes that function.",
125
+ "The change-set contains the hollow demonstration with survivors named and restore confirmed.",
126
+ "npx vitest run shows no failure outside spec_*-named files. After this movement 85 of 88 laws are green.",
127
+ "State plainly that an unnamed gig remains claimable by any worker, and point at the law that holds it."
128
+ ],
129
+ "out_of_scope": [
130
+ "Gap 1.",
131
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
132
+ "Pinning a gig to a venue INSTANCE — the specification names it a different question with a different answer and does not settle it."
133
+ ]
134
+ }
135
+ },
136
+ "seatings": []
137
+ },
138
+ {
139
+ "movement_id": "gap1-credential-mint",
140
+ "standard_slug": "software-change-pr-v1",
141
+ "runtime_fills": {
142
+ "change-request": {
143
+ "source": "SPEC-worker-contract.md Gap 1; tests/spec_venue_credential_mint.test.ts (10 laws)",
144
+ "requested_by": "eugene",
145
+ "repository": "eir-labs/coltrane",
146
+ "request_text": "Create src/venue_credential.ts and close tests/spec_venue_credential_mint.test.ts, the last gap.\n\nTo stand up a worker you need an org-scoped, instance-bound key plus a set of environment variables, and the engine ships no verb producing either — so every deployment invents an out-of-band path, and in practice those end at a human reading a value off a screen. A flow requiring that cannot be driven by an MCP invocation, and the MCP surface is how an assistant operates this engine.\n\nvenue_credential_mint(org_slug, instance) returns the COMPLETE worker environment, with the two hosts distinct, and names the credential classes it provisioned in the room contract's own vocabulary — not a parallel scheme. It refuses an incomplete grant rather than forwarding it. It REFUSES A GIG-SCOPED CREDENTIAL by name and without reaching the backend: a gig credential expires with one gig's lease and a venue credential outlives every gig, so letting the first mint the second turns the narrowest credential in the system into the broadest. When no minting backend is wired it answers with a typed refusal following the shape deps.queueGig / approveGig / cancelGig already use. Export VENUE_CREDENTIAL_REFUSALS naming exactly three refusals, each a reason a mint cannot proceed.\n\nTHERE IS NO READ-BACK VERB. The credential is answered once and is never retrievable. Do not add one.\n\nAuthorization stays entirely store-side. The engine refuses a gig-scoped credential because that is a structural fact about credential scope, not a policy; who may mint is a question the engine must not answer for deployments it cannot see.\n\nYOU OWN THE HOLLOW DEMONSTRATION: a handler always returning {ok:true}, one always returning {ok:false}, and VENUE_CREDENTIAL_REFUSALS=[]. Record survivors, restore, report in the change-set.",
147
+ "target_paths": [
148
+ "src/venue_credential.ts",
149
+ "src/mcp.ts",
150
+ "src/server.ts",
151
+ "tests/spec_venue_credential_mint.test.ts"
152
+ ],
153
+ "acceptance_criteria": [
154
+ "npx vitest run tests/spec_venue_credential_mint.test.ts reports 10 passed, 0 failed.",
155
+ "npx vitest run tests/spec_*.test.ts reports 88 passed, 0 failed — the whole specification, every gap.",
156
+ "npx vitest run reports zero failures across the entire suite, and npx tsc --noEmit is clean.",
157
+ "The change-set contains the hollow demonstration with survivors named and restore confirmed.",
158
+ "Confirm no read-back verb was added, and that the six spec_* law files are unedited apart from the anchor amendment already committed on this branch.",
159
+ "The CHANGELOG records the whole worker contract as a minor bump under inverted semver: Gaps 1, 2, 5 and 6 additive, Gap 3 breaking."
160
+ ],
161
+ "out_of_scope": [
162
+ "Editing any spec_* test file or SPEC-worker-contract.md.",
163
+ "Deciding WHO may mint — deliberately absent from the engine.",
164
+ "Any store schema: no column names, no RPC signatures, no migration. The engine names arguments and their meaning; the store's shape is the deployment's.",
165
+ "A read-back verb, in any form."
166
+ ]
167
+ }
168
+ },
169
+ "seatings": []
170
+ }
171
+ ],
172
+ "edges": [],
173
+ "approval_gates": [
174
+ {
175
+ "gate_id": "gap2-landed",
176
+ "after_movement": "gap2-realize-venue",
177
+ "before_movement": "gap3-worker-environment",
178
+ "chair": "quartet.chair.responsible-officer",
179
+ "prompt": "Read the change-verdict and the PR. Check the HOLLOW DEMONSTRATION is present IN THE CHANGE-SET with a law-by-law survivor list — it is the implementer's obligation now, because the verify seat holds no write grant and cannot run it. A change-set without it is incomplete; a survivor list of zero laws means it was not run honestly. Confirm Gap 6 is still 40/40 measured in this run, and that any new schema field is actually read by venue_define's handler."
180
+ },
181
+ {
182
+ "gate_id": "gap3-landed",
183
+ "after_movement": "gap3-worker-environment",
184
+ "before_movement": "gap4-run-modes",
185
+ "chair": "quartet.chair.responsible-officer",
186
+ "prompt": "Gap 3 is the BREAKING one. Read the verdict and confirm the CHANGELOG says so under inverted semver. Check the completeness law was satisfied against real source — how many process.env reads were enumerated, from which files — rather than against a hand-written list, and that the hollow demonstration is in the change-set."
187
+ },
188
+ {
189
+ "gate_id": "gap4-landed",
190
+ "after_movement": "gap4-run-modes",
191
+ "before_movement": "gap5-venue-targeting",
192
+ "chair": "quartet.chair.responsible-officer",
193
+ "prompt": "Confirm the defensive branch at worker.ts:345 is gone and one derivation answers the mode question — two independent computations of one fact is the defect this gap exists to close, so a change that adds a function without deleting the second derivation has not fixed it. Hollow demonstration in the change-set."
194
+ },
195
+ {
196
+ "gate_id": "gap5-landed",
197
+ "after_movement": "gap5-venue-targeting",
198
+ "before_movement": "gap1-credential-mint",
199
+ "chair": "quartet.chair.responsible-officer",
200
+ "prompt": "Confirm an unnamed gig stays claimable by any worker — targeting must not become mandatory routing. Confirm the existing claim tests still pass. Hollow demonstration in the change-set. The next movement is the last gap, and its acceptance is the whole specification at 88 passed."
201
+ }
202
+ ],
203
+ "budget_envelope": {
204
+ "total_usd": 300
205
+ }
206
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /** THE GATE, WIRED TO WHAT IS ACTUALLY PUBLISHED.
3
+ *
4
+ * `boundary_scan.ts` is pure and knows nothing about this repository. This is the half that decides
5
+ * WHAT is in scope and WHAT an outcome costs, and it makes exactly two decisions:
6
+ *
7
+ * SCOPE IS WHAT GIT TRACKS. Not the working tree — an untracked scratch file is not published, and
8
+ * failing on it trains an operator to ignore the gate. Not the diff either: a term introduced on an
9
+ * earlier commit is still public today, so the whole tracked surface is scanned every run and the
10
+ * gate cannot be walked past by landing the leak in two steps.
11
+ *
12
+ * AN UNAVAILABLE LIST IS FATAL WHERE PUBLISHING HAPPENS. On a contributor's laptop the list is
13
+ * legitimately absent, and a hard failure there teaches people to skip the suite. In CI the list is
14
+ * provisioned from a secret, so its absence means the gate is BROKEN, and a broken gate must never
15
+ * read as a pass. `CI` (set by GitHub Actions) selects between the two, and `--require-list` forces
16
+ * the strict reading anywhere.
17
+ */
18
+ export declare function main(argv: readonly string[], env: NodeJS.ProcessEnv): number;
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /** THE GATE, WIRED TO WHAT IS ACTUALLY PUBLISHED.
3
+ *
4
+ * `boundary_scan.ts` is pure and knows nothing about this repository. This is the half that decides
5
+ * WHAT is in scope and WHAT an outcome costs, and it makes exactly two decisions:
6
+ *
7
+ * SCOPE IS WHAT GIT TRACKS. Not the working tree — an untracked scratch file is not published, and
8
+ * failing on it trains an operator to ignore the gate. Not the diff either: a term introduced on an
9
+ * earlier commit is still public today, so the whole tracked surface is scanned every run and the
10
+ * gate cannot be walked past by landing the leak in two steps.
11
+ *
12
+ * AN UNAVAILABLE LIST IS FATAL WHERE PUBLISHING HAPPENS. On a contributor's laptop the list is
13
+ * legitimately absent, and a hard failure there teaches people to skip the suite. In CI the list is
14
+ * provisioned from a secret, so its absence means the gate is BROKEN, and a broken gate must never
15
+ * read as a pass. `CI` (set by GitHub Actions) selects between the two, and `--require-list` forces
16
+ * the strict reading anywhere.
17
+ */
18
+ import { execFileSync } from "node:child_process";
19
+ import { readFileSync } from "node:fs";
20
+ import { scanBoundary, formatOutcome } from "./boundary_scan.js";
21
+ /** Paths git tracks, minus what cannot carry prose. Binary content would only produce noise. */
22
+ function trackedText() {
23
+ const out = execFileSync("git", ["ls-files", "-z"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
24
+ const skip = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|woff2?|ttf|mp4|wasm)$/i;
25
+ const files = [];
26
+ for (const p of out.split("\0")) {
27
+ if (!p || skip.test(p))
28
+ continue;
29
+ try {
30
+ files.push([p, readFileSync(p, "utf8")]);
31
+ }
32
+ catch {
33
+ /* unreadable or genuinely binary — not publishable prose, so not this gate's business */
34
+ }
35
+ }
36
+ return files;
37
+ }
38
+ export function main(argv, env) {
39
+ const strict = argv.includes("--require-list") || env["CI"] === "true";
40
+ const outcome = scanBoundary(trackedText(), env);
41
+ if (outcome.status === "violation") {
42
+ // The matched terms print for the operator who already holds the list. A public CI log would
43
+ // republish them, so under CI the locations print and the terms do not.
44
+ if (env["CI"] === "true") {
45
+ console.error(`boundary VIOLATION — ${outcome.hits.length} hit(s):`);
46
+ for (const h of outcome.hits)
47
+ console.error(` ${h.path}:${h.line}`);
48
+ }
49
+ else {
50
+ console.error(formatOutcome(outcome));
51
+ }
52
+ return 1;
53
+ }
54
+ if (outcome.status === "unavailable") {
55
+ if (strict) {
56
+ console.error(`boundary GATE BROKEN — ${outcome.reason}`);
57
+ return 2;
58
+ }
59
+ console.warn(`boundary skipped — ${outcome.reason}`);
60
+ return 0;
61
+ }
62
+ console.log(formatOutcome(outcome));
63
+ return 0;
64
+ }
65
+ /* c8 ignore start — entry wiring */
66
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").pop() ?? "")) {
67
+ process.exit(main(process.argv.slice(2), process.env));
68
+ }
69
+ /* c8 ignore stop */
70
+ //# sourceMappingURL=boundary_check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundary_check.js","sourceRoot":"","sources":["../../src/boundary_check.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEjE,gGAAgG;AAChG,SAAS,WAAW;IAClB,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;IACvG,MAAM,IAAI,GAAG,6DAA6D,CAAC;IAC3E,MAAM,KAAK,GAAqC,EAAE,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,SAAS;QACjC,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAU,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,yFAAyF;QAC3F,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,IAAuB,EAAE,GAAsB;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC;IACvE,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,CAAC;IAEjD,IAAI,OAAO,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QACnC,6FAA6F;QAC7F,wEAAwE;QACxE,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC;YACzB,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC;YACrE,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI;gBAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;QACrC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,0BAA0B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,sBAAsB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACpC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,oCAAoC;AACpC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACxF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC;AACD,oBAAoB"}
@@ -0,0 +1,62 @@
1
+ /** THE PUBLISHING BOUNDARY, AS A GATE RATHER THAN A HABIT.
2
+ *
3
+ * This repo is published. Some vocabulary, and some names, must not cross into it. That rule
4
+ * previously lived only in an operator's head and in an assistant's local notes — which is to say
5
+ * it had no failure mode. A rule that cannot fail is not enforced; it is merely remembered, and a
6
+ * remembered rule is one bad session away from a leak that is public the moment it is pushed.
7
+ *
8
+ * THE LIST IS NOT IN THIS FILE, AND MUST NEVER BE. A denylist of forbidden terms, committed to a
9
+ * public repository, publishes exactly the terms it exists to protect — the gate would become the
10
+ * leak. So the mechanism splits: the SCANNER is public (it is only a string search, and reveals
11
+ * nothing), and the TERMS are supplied from outside the tree at scan time.
12
+ *
13
+ * It follows that this module holds no default list, no example terms, and no test fixtures drawn
14
+ * from the real one. A contributor reading this file learns that a boundary exists and learns
15
+ * nothing about where it lies.
16
+ */
17
+ /** Where the terms come from. A path OUTSIDE this repository — a private sibling checkout, or a
18
+ * CI-provisioned file written from a secret. Absent is a distinct state from empty, and the two
19
+ * are reported differently: see `scanBoundary`. */
20
+ export declare const BOUNDARY_LIST_ENV = "COLTRANE_BOUNDARY_TERMS";
21
+ /** One forbidden term and where it was found. The term is echoed back because the finding is
22
+ * reported to the operator who already holds the list — never written to a public artifact. A
23
+ * CI job that surfaces these must mask them or print only `path` and `line`. */
24
+ export interface BoundaryHit {
25
+ readonly path: string;
26
+ readonly line: number;
27
+ readonly term: string;
28
+ }
29
+ export type BoundaryOutcome =
30
+ /** The list was supplied and no term appears in the scanned content. */
31
+ {
32
+ readonly status: "clean";
33
+ readonly terms: number;
34
+ readonly files: number;
35
+ }
36
+ /** The list was supplied and at least one term appears. */
37
+ | {
38
+ readonly status: "violation";
39
+ readonly terms: number;
40
+ readonly files: number;
41
+ readonly hits: readonly BoundaryHit[];
42
+ }
43
+ /** No list was supplied. NOT a pass — the check did not run. The caller decides whether that is
44
+ * fatal (CI, where the list is provisioned and its absence means the gate is broken) or a visible
45
+ * skip (a contributor's laptop, which is not the publishing boundary). */
46
+ | {
47
+ readonly status: "unavailable";
48
+ readonly reason: string;
49
+ };
50
+ /** Scan supplied content against the private term list.
51
+ *
52
+ * PURE IN ITS CONTENT INPUT: the caller decides what is in scope — tracked files, a staged diff, a
53
+ * single PR body — and passes `[path, text]` pairs. This module never walks the filesystem itself,
54
+ * so the same gate serves a pre-push hook, a CI job over the whole tree, and a check on one
55
+ * outbound artifact, with no second implementation to drift.
56
+ *
57
+ * `env` is injected rather than read from `process.env` so a law can exercise every outcome —
58
+ * including `unavailable` — without mutating the ambient environment. */
59
+ export declare function scanBoundary(content: Iterable<readonly [path: string, text: string]>, env: NodeJS.ProcessEnv): BoundaryOutcome;
60
+ /** Render an outcome for an operator who HOLDS the list. Never use this to write a public artifact:
61
+ * it names the matched terms. A public surface gets `path:line` and a count, nothing more. */
62
+ export declare function formatOutcome(o: BoundaryOutcome): string;
@@ -0,0 +1,93 @@
1
+ /** THE PUBLISHING BOUNDARY, AS A GATE RATHER THAN A HABIT.
2
+ *
3
+ * This repo is published. Some vocabulary, and some names, must not cross into it. That rule
4
+ * previously lived only in an operator's head and in an assistant's local notes — which is to say
5
+ * it had no failure mode. A rule that cannot fail is not enforced; it is merely remembered, and a
6
+ * remembered rule is one bad session away from a leak that is public the moment it is pushed.
7
+ *
8
+ * THE LIST IS NOT IN THIS FILE, AND MUST NEVER BE. A denylist of forbidden terms, committed to a
9
+ * public repository, publishes exactly the terms it exists to protect — the gate would become the
10
+ * leak. So the mechanism splits: the SCANNER is public (it is only a string search, and reveals
11
+ * nothing), and the TERMS are supplied from outside the tree at scan time.
12
+ *
13
+ * It follows that this module holds no default list, no example terms, and no test fixtures drawn
14
+ * from the real one. A contributor reading this file learns that a boundary exists and learns
15
+ * nothing about where it lies.
16
+ */
17
+ import { readFileSync } from "node:fs";
18
+ /** Where the terms come from. A path OUTSIDE this repository — a private sibling checkout, or a
19
+ * CI-provisioned file written from a secret. Absent is a distinct state from empty, and the two
20
+ * are reported differently: see `scanBoundary`. */
21
+ export const BOUNDARY_LIST_ENV = "COLTRANE_BOUNDARY_TERMS";
22
+ /** Read the terms from the path in `env[BOUNDARY_LIST_ENV]`.
23
+ *
24
+ * One term per line; blank lines and `#` comments ignored; matching is case-insensitive because a
25
+ * boundary that Title Case walks through is not a boundary. Terms are compared as plain substrings
26
+ * rather than regexes — a regex in a private list is an injection surface nobody reviews, and the
27
+ * rule being enforced is "this word does not appear", which needs nothing more. */
28
+ function loadTerms(env) {
29
+ const path = env[BOUNDARY_LIST_ENV];
30
+ if (!path)
31
+ return { reason: `${BOUNDARY_LIST_ENV} is not set — no term list to scan against` };
32
+ let raw;
33
+ try {
34
+ raw = readFileSync(path, "utf8");
35
+ }
36
+ catch (e) {
37
+ return { reason: `${BOUNDARY_LIST_ENV} points at ${path}, which could not be read: ${String(e)}` };
38
+ }
39
+ const terms = raw
40
+ .split("\n")
41
+ .map((l) => l.trim())
42
+ .filter((l) => l.length > 0 && !l.startsWith("#"))
43
+ .map((l) => l.toLowerCase());
44
+ if (terms.length === 0)
45
+ return { reason: `${BOUNDARY_LIST_ENV} points at ${path}, which holds no terms` };
46
+ return terms;
47
+ }
48
+ /** Scan supplied content against the private term list.
49
+ *
50
+ * PURE IN ITS CONTENT INPUT: the caller decides what is in scope — tracked files, a staged diff, a
51
+ * single PR body — and passes `[path, text]` pairs. This module never walks the filesystem itself,
52
+ * so the same gate serves a pre-push hook, a CI job over the whole tree, and a check on one
53
+ * outbound artifact, with no second implementation to drift.
54
+ *
55
+ * `env` is injected rather than read from `process.env` so a law can exercise every outcome —
56
+ * including `unavailable` — without mutating the ambient environment. */
57
+ export function scanBoundary(content, env) {
58
+ const loaded = loadTerms(env);
59
+ if (!Array.isArray(loaded))
60
+ return { status: "unavailable", reason: loaded.reason };
61
+ const terms = loaded;
62
+ const hits = [];
63
+ let files = 0;
64
+ for (const [path, text] of content) {
65
+ files++;
66
+ const lines = text.split("\n");
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const haystack = lines[i].toLowerCase();
69
+ for (const term of terms) {
70
+ if (haystack.includes(term))
71
+ hits.push({ path, line: i + 1, term });
72
+ }
73
+ }
74
+ }
75
+ return hits.length === 0
76
+ ? { status: "clean", terms: terms.length, files }
77
+ : { status: "violation", terms: terms.length, files, hits };
78
+ }
79
+ /** Render an outcome for an operator who HOLDS the list. Never use this to write a public artifact:
80
+ * it names the matched terms. A public surface gets `path:line` and a count, nothing more. */
81
+ export function formatOutcome(o) {
82
+ switch (o.status) {
83
+ case "clean":
84
+ return `boundary clean — ${o.files} file(s) scanned against ${o.terms} term(s)`;
85
+ case "unavailable":
86
+ return `boundary NOT CHECKED — ${o.reason}`;
87
+ case "violation": {
88
+ const lines = o.hits.map((h) => ` ${h.path}:${h.line} ${h.term}`);
89
+ return `boundary VIOLATION — ${o.hits.length} hit(s) across ${o.files} file(s):\n${lines.join("\n")}`;
90
+ }
91
+ }
92
+ }
93
+ //# sourceMappingURL=boundary_scan.js.map