@reunionstudio/airlock-mcp 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/airlock-mcp/SKILL.md +137 -5
- package/.agents/skills/airlock-mcp/agents/openai.yaml +2 -2
- package/README.md +76 -15
- package/docs/architecture.md +21 -10
- package/docs/install-surface.md +46 -9
- package/docs/ooda-loop.md +11 -0
- package/docs/spec-workbench-architecture.md +17 -5
- package/docs/workflows.md +138 -17
- package/package.json +1 -1
- package/setup.py +1 -1
- package/src/airlock_mcp/__init__.py +1 -1
- package/src/airlock_mcp/app_context.py +271 -0
- package/src/airlock_mcp/art.py +1 -1
- package/src/airlock_mcp/bootstrap.py +74 -6
- package/src/airlock_mcp/cli.py +33 -1
- package/src/airlock_mcp/summary.py +215 -23
- package/src/mcp.mjs +5 -5
- package/src/text.mjs +86 -21
- package/src/workbench.mjs +64 -1
package/docs/workflows.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Start A New Specs Repo
|
|
4
4
|
|
|
5
|
-
Use this when a person opens a
|
|
5
|
+
Use this when a person opens a new Codex project and wants to start working
|
|
6
6
|
with real Airlock specs. The project repo is where they work. Public users
|
|
7
7
|
should come through the single Airlock MCP install; this repo provides the
|
|
8
8
|
spec-building capability behind that experience.
|
|
@@ -10,6 +10,11 @@ spec-building capability behind that experience.
|
|
|
10
10
|
Ask for the project or organization name first, then lowercase it, replace
|
|
11
11
|
spaces with hyphens, and append `-specs`. For example, `Home` becomes
|
|
12
12
|
`home-specs`. Reserve `airlock-specs` for the canonical reusable spec library.
|
|
13
|
+
Recommend a real Git repo for the specs project. GitHub is the recommended
|
|
14
|
+
default when the user already has GitHub set up, but any normal repository host
|
|
15
|
+
is acceptable. If Codex is creating the repo, ask where the `<slug>-specs`
|
|
16
|
+
directory should live before making files, then offer to initialize git and
|
|
17
|
+
create or push the GitHub repo.
|
|
13
18
|
Airlock itself does not create this repo; Codex can create it locally while
|
|
14
19
|
helping the user start an Airlock specs project.
|
|
15
20
|
Start in Codex, not Snowflake Cortex. Snow CLI or Cortex only matters later when
|
|
@@ -20,13 +25,22 @@ The preferred Codex prompt is:
|
|
|
20
25
|
```text
|
|
21
26
|
I want to use Airlock MCP to start working with Airlock specs for Home.
|
|
22
27
|
|
|
23
|
-
Set up this project as an Airlock specs repo.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
Set up this project as an Airlock specs repo. If this project is not already a
|
|
29
|
+
Git repo, recommend storing it in version control, preferably GitHub if
|
|
30
|
+
available. If you are creating the repo for me, ask where the `home-specs`
|
|
31
|
+
directory should live before making files.
|
|
32
|
+
|
|
33
|
+
Welcome me by asking what process I want to improve. Explain that Airlock works
|
|
34
|
+
best when we can identify the loop around that process: what information comes
|
|
35
|
+
in, what context helps us understand it, what decision needs to be made, and
|
|
36
|
+
what action happens after the decision. Ask whether I already have artifacts:
|
|
37
|
+
CSV or Excel files, JSON samples, API docs, schemas, forms, screenshots, PDFs,
|
|
38
|
+
exports, message examples, or other content people already use. When useful,
|
|
39
|
+
check the reusable airlock-specs library for starting points, patterns, and
|
|
40
|
+
ideas, but prefer current API docs, real exports, samples, and other artifacts
|
|
41
|
+
when they conflict with a library shape. Ask for the messy version, then help
|
|
42
|
+
turn it into a small first Airlock spec and a plan for more. Do not create the
|
|
43
|
+
first workspace until I choose a path.
|
|
30
44
|
```
|
|
31
45
|
|
|
32
46
|
When dogfooding this implementation repo before the MCP package is published,
|
|
@@ -53,9 +67,10 @@ The connector implementation lives in `reunionstudio/airlock-mcp`; this
|
|
|
53
67
|
workbench keeps the Airlock MCP spec-building capability.
|
|
54
68
|
|
|
55
69
|
That should install or register the Airlock MCP connector for the user's agent
|
|
56
|
-
environment. After that, the user still creates
|
|
57
|
-
and starts chatting there. Airlock MCP covers building
|
|
58
|
-
pull and push governed data, and improving specs from real
|
|
70
|
+
environment. After that, the user still creates or opens a Git-backed
|
|
71
|
+
`<slug>-specs` project and starts chatting there. Airlock MCP covers building
|
|
72
|
+
specs, using specs to pull and push governed data, and improving specs from real
|
|
73
|
+
use cases.
|
|
59
74
|
|
|
60
75
|
For Codex, the install command should register the server with:
|
|
61
76
|
|
|
@@ -67,12 +82,18 @@ The MCP-style flow should be:
|
|
|
67
82
|
|
|
68
83
|
1. Run `npx @reunionstudio/airlock-mcp install` once for the agent environment.
|
|
69
84
|
2. Open Codex.
|
|
70
|
-
3. Create a
|
|
85
|
+
3. Create or open a Git-backed specs repo named for the org or project, such as
|
|
71
86
|
`home-specs`.
|
|
72
|
-
4.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
4. If Codex is creating it, choose where the repo directory should live before
|
|
88
|
+
files are written.
|
|
89
|
+
5. Tell Codex: `Use Airlock to help me improve a process with specs.`
|
|
90
|
+
6. Airlock MCP welcomes and asks which delivery mode the user wants:
|
|
91
|
+
spec-first, app-first from existing specs, or co-development.
|
|
92
|
+
7. For spec-first or co-development, Airlock MCP asks what process the user
|
|
93
|
+
wants to improve, explains the loop around the process, and proposes a small
|
|
94
|
+
first spec plus a plan for more.
|
|
95
|
+
8. For app-first or co-development, Airlock MCP identifies the app goal,
|
|
96
|
+
available specs, read specs, write specs, runtime, and approved access path.
|
|
76
97
|
|
|
77
98
|
`init-repo` creates `AGENTS.md`, `.agents/skills/airlock-mcp/SKILL.md`, and
|
|
78
99
|
`workspaces/`. That is what lets Codex understand prompts such as
|
|
@@ -98,9 +119,101 @@ websites, APIs, data feeds, or physical events. Actions may go back through
|
|
|
98
119
|
those same places. After giving examples, call these places interfaces: where
|
|
99
120
|
the process observes from or acts through.
|
|
100
121
|
|
|
122
|
+
Ask whether the user already has artifacts for the process: CSV or Excel files,
|
|
123
|
+
JSON samples, API docs, schemas, forms, screenshots, PDFs, exports, message
|
|
124
|
+
examples, or other content people already use. Treat these as design artifacts
|
|
125
|
+
for drafting the spec. They are different from Airlock attachments, which are
|
|
126
|
+
evidence files submitted later with governed records.
|
|
127
|
+
|
|
128
|
+
When useful, point at the reusable `airlock-specs` library for starting points,
|
|
129
|
+
patterns, and ideas. Do not treat those library specs as guaranteed to reflect
|
|
130
|
+
the current shape of third-party systems. If current API docs, exports, samples,
|
|
131
|
+
schemas, or user-provided artifacts disagree with the library, prefer the
|
|
132
|
+
current artifacts and record the divergence.
|
|
133
|
+
|
|
101
134
|
Ask for the messy version. Help turn it into a small first Airlock spec and a
|
|
102
135
|
plan for more.
|
|
103
136
|
|
|
137
|
+
## Build An App From Existing Specs
|
|
138
|
+
|
|
139
|
+
Use this when the user already has access to specs and wants to build an app,
|
|
140
|
+
dashboard, approval queue, decision UI, analysis workflow, scheduled agent, or
|
|
141
|
+
other code that reads from and submits through Airlock.
|
|
142
|
+
|
|
143
|
+
Do not bootstrap or edit a specs workspace by default. Work in the app repo
|
|
144
|
+
unless the user asks to change specs. Start by asking for:
|
|
145
|
+
|
|
146
|
+
- the app goal and decision the app should support
|
|
147
|
+
- the specs the user can access
|
|
148
|
+
- read specs, such as budgets, expenses, requests, forecasts, observations,
|
|
149
|
+
payouts, or reference data
|
|
150
|
+
- write specs, such as decisions, approvals, comments, commitments, actions, or
|
|
151
|
+
follow-ups
|
|
152
|
+
- where the app should run, such as Streamlit, web app, CLI, notebook,
|
|
153
|
+
scheduled agent, or an existing framework
|
|
154
|
+
- the available Airlock/Snowflake access path
|
|
155
|
+
- identity, evidence, timestamp, approval, and separation-of-duties needs
|
|
156
|
+
|
|
157
|
+
The app should follow the loop:
|
|
158
|
+
|
|
159
|
+
1. Observe/read governed records through approved Airlock or Snowflake surfaces.
|
|
160
|
+
2. Orient with summaries, comparisons, ranked options, recommendations,
|
|
161
|
+
exception queues, dashboards, or proposals.
|
|
162
|
+
3. Decide by capturing the human or agent choice with rationale and evidence.
|
|
163
|
+
4. Act/write by submitting the decision, approval, action, comment, or follow-up
|
|
164
|
+
through an Airlock spec contract.
|
|
165
|
+
|
|
166
|
+
Keep the boundary explicit: do not write directly to Airlock-owned tables,
|
|
167
|
+
stages, generated views, or generated tables. Do not bypass spec workflow just
|
|
168
|
+
because the app can connect to Snowflake. If no suitable decision or action spec
|
|
169
|
+
exists, explain the gap and propose a small spec-design step.
|
|
170
|
+
|
|
171
|
+
If the app repo needs local context for coding, seed it with spec snapshots and
|
|
172
|
+
a manifest:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
airlock-mcp init-app-context . --mode app-first --spec ../home-specs/workspaces/expenses
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Use `--mode co-development` when the app and specs will evolve together. Repeat
|
|
179
|
+
`--spec` for each workspace or spec JSON file the app should know about. The
|
|
180
|
+
command creates:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
airlock/
|
|
184
|
+
AGENTS.md
|
|
185
|
+
README.md
|
|
186
|
+
specs.manifest.json
|
|
187
|
+
spec-snapshots/
|
|
188
|
+
sample-records/
|
|
189
|
+
generated/
|
|
190
|
+
types/
|
|
191
|
+
sql/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The app repo can import or generate helper code from these snapshots, but the
|
|
195
|
+
snapshots are not canonical. Canonical specs remain in the specs repo or
|
|
196
|
+
installed Airlock. Mark each manifest spec as read, write, or read_write for
|
|
197
|
+
the app before building data access and submission flows.
|
|
198
|
+
|
|
199
|
+
## Co-Develop Specs And App
|
|
200
|
+
|
|
201
|
+
Use this when the app surface and the Airlock contracts need to shape each
|
|
202
|
+
other. This is common when a dashboard, queue, approval flow, or agent workflow
|
|
203
|
+
reveals which observations, proposals, decisions, or actions need governed
|
|
204
|
+
records.
|
|
205
|
+
|
|
206
|
+
Keep two tracks visible:
|
|
207
|
+
|
|
208
|
+
- Spec track: row grain, columns, samples, attachments, access, validation,
|
|
209
|
+
workflow, and installed Airlock validation.
|
|
210
|
+
- App track: screens, reads, summaries, ranked options, decision capture,
|
|
211
|
+
governed writes, runtime, and tests.
|
|
212
|
+
|
|
213
|
+
Start with the smallest useful contract. The app may expose a thin prototype
|
|
214
|
+
against samples while the spec is being drafted, but live submissions should
|
|
215
|
+
wait for installed Airlock validation and the approved read/write path.
|
|
216
|
+
|
|
104
217
|
## Start From Feedback
|
|
105
218
|
|
|
106
219
|
Use this when the user chooses a shared feedback loop for humans and agents.
|
|
@@ -144,6 +257,11 @@ Supported input shapes:
|
|
|
144
257
|
- a raw canonical object with `core_config` or `column_config`
|
|
145
258
|
- a legacy `{"specs": [{"config": ...}]}`
|
|
146
259
|
|
|
260
|
+
Spec-library imports are starting points, not live system contracts. Before
|
|
261
|
+
using one for a third-party system, compare it with current API docs, CSV or
|
|
262
|
+
Excel exports, JSON samples, schemas, forms, and other real artifacts. Override
|
|
263
|
+
outdated or mismatched fields when current evidence says the shape has changed.
|
|
264
|
+
|
|
147
265
|
## Clone A Workspace
|
|
148
266
|
|
|
149
267
|
Use this when a related spec should preserve most design choices but needs a new
|
|
@@ -187,7 +305,10 @@ airlock-mcp next workspaces/team-posts
|
|
|
187
305
|
|
|
188
306
|
The summary shows the current spec identity, required fields, variant fields,
|
|
189
307
|
attachment posture, guest access model, sample record count, note-file status,
|
|
190
|
-
and local check counts.
|
|
308
|
+
and local check counts. It is organized as a structured spec card with sections
|
|
309
|
+
for core, file rules, attachments, guest access, column rules, samples, notes,
|
|
310
|
+
and check status so Codex can present the draft back to the user before asking
|
|
311
|
+
for decisions.
|
|
191
312
|
|
|
192
313
|
`next` prints the same recap plus a conservative next action: fix local errors,
|
|
193
314
|
review warnings, fill open design prompts in `decisions.md`, add sample records,
|
package/package.json
CHANGED
package/setup.py
CHANGED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .jsonio import read_json, write_json, write_text
|
|
9
|
+
from .specs import extract_spec_config, spec_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
APP_CONTEXT_MODES = ("spec-first", "app-first", "co-development")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
APP_README = """# Airlock App Context
|
|
16
|
+
|
|
17
|
+
This folder is app-local context for building software that uses Airlock specs.
|
|
18
|
+
|
|
19
|
+
The canonical specs live in the specs repo or installed Airlock. Files here are
|
|
20
|
+
snapshots, samples, generated helpers, and planning context for app development.
|
|
21
|
+
Refresh them when the canonical spec changes.
|
|
22
|
+
|
|
23
|
+
Use:
|
|
24
|
+
|
|
25
|
+
- `specs.manifest.json` to track which specs the app reads from or writes to.
|
|
26
|
+
- `spec-snapshots/` for app-local copies of spec configs.
|
|
27
|
+
- `sample-records/` for app-local sample records used in UI and test fixtures.
|
|
28
|
+
- `generated/types/` for generated application types.
|
|
29
|
+
- `generated/sql/` for reviewed SQL helpers or query templates.
|
|
30
|
+
|
|
31
|
+
Do not store credentials here. Do not write directly to Airlock-owned tables,
|
|
32
|
+
stages, generated views, or generated tables. Use approved Airlock/Snowflake
|
|
33
|
+
access paths and submit governed decisions or actions through spec contracts.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
APP_AGENTS = """# Airlock App Guidance
|
|
38
|
+
|
|
39
|
+
This repo may contain application code that uses Airlock specs.
|
|
40
|
+
|
|
41
|
+
## Modes
|
|
42
|
+
|
|
43
|
+
- Spec-first: design governed specs before building the app surface.
|
|
44
|
+
- App-first: use existing specs to build an app, dashboard, queue, or workflow.
|
|
45
|
+
- Co-development: evolve the app and specs together while keeping the contract
|
|
46
|
+
explicit.
|
|
47
|
+
|
|
48
|
+
## App Context
|
|
49
|
+
|
|
50
|
+
The `airlock/` folder is app-local. Its spec snapshots are references for
|
|
51
|
+
development, not canonical specs. Canonical specs live in the specs repo or
|
|
52
|
+
installed Airlock.
|
|
53
|
+
|
|
54
|
+
Keep two tracks visible during co-development:
|
|
55
|
+
|
|
56
|
+
- Spec track: row grain, columns, samples, access, validation, and workflow.
|
|
57
|
+
- App track: screens, reads, decisions, writes, user actions, and runtime.
|
|
58
|
+
|
|
59
|
+
Use approved Airlock/Snowflake access paths. Do not bypass spec workflow or
|
|
60
|
+
write directly to Airlock-owned tables, stages, generated views, or generated
|
|
61
|
+
tables. If the app needs to submit a decision or action and no suitable write
|
|
62
|
+
spec exists, propose a small spec-design step.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class AppContextResult:
|
|
68
|
+
root: Path
|
|
69
|
+
mode: str
|
|
70
|
+
created: tuple[Path, ...]
|
|
71
|
+
updated: tuple[Path, ...]
|
|
72
|
+
kept: tuple[Path, ...]
|
|
73
|
+
specs: tuple[str, ...]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _safe_name(value: str) -> str:
|
|
77
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_.-]+", "_", value.strip())
|
|
78
|
+
cleaned = cleaned.strip("._-").lower()
|
|
79
|
+
return cleaned or "spec"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_text_if_needed(path: Path, text: str, *, force: bool) -> str:
|
|
83
|
+
if path.exists() and not force:
|
|
84
|
+
return "kept"
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
write_text(path, text, force=True)
|
|
87
|
+
return "created"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _write_json_if_needed(path: Path, value: Any, *, force: bool) -> str:
|
|
91
|
+
if path.exists() and not force:
|
|
92
|
+
return "kept"
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
write_json(path, value, force=True)
|
|
95
|
+
return "created"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _load_spec_source(source: Path) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
99
|
+
if source.is_dir():
|
|
100
|
+
raw_spec = read_json(source / "spec.config.json")
|
|
101
|
+
if not isinstance(raw_spec, dict):
|
|
102
|
+
raise ValueError(f"invalid spec workspace: {source}")
|
|
103
|
+
raw_sample = read_json(source / "sample.records.json")
|
|
104
|
+
return raw_spec, raw_sample if isinstance(raw_sample, dict) else None
|
|
105
|
+
|
|
106
|
+
raw = read_json(source)
|
|
107
|
+
if raw is None:
|
|
108
|
+
raise FileNotFoundError(source)
|
|
109
|
+
return extract_spec_config(raw), None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _manifest(mode: str, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
"schema_version": 1,
|
|
115
|
+
"mode": mode,
|
|
116
|
+
"canonical_source": "specs repo or installed Airlock",
|
|
117
|
+
"snapshot_policy": "Snapshots are app-local development references, not canonical specs.",
|
|
118
|
+
"specs": entries,
|
|
119
|
+
"tracks": {
|
|
120
|
+
"spec_track": "row grain, columns, samples, access, validation, workflow",
|
|
121
|
+
"app_track": "screens, reads, decisions, writes, user actions, runtime",
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _merge_manifest(existing: Any, new_manifest: dict[str, Any]) -> dict[str, Any]:
|
|
127
|
+
if not isinstance(existing, dict):
|
|
128
|
+
return new_manifest
|
|
129
|
+
|
|
130
|
+
merged = dict(existing)
|
|
131
|
+
merged.update(
|
|
132
|
+
{
|
|
133
|
+
"schema_version": new_manifest["schema_version"],
|
|
134
|
+
"mode": new_manifest["mode"],
|
|
135
|
+
"canonical_source": new_manifest["canonical_source"],
|
|
136
|
+
"snapshot_policy": new_manifest["snapshot_policy"],
|
|
137
|
+
"tracks": new_manifest["tracks"],
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
existing_entries = existing.get("specs")
|
|
142
|
+
by_name: dict[str, dict[str, Any]] = {}
|
|
143
|
+
if isinstance(existing_entries, list):
|
|
144
|
+
for entry in existing_entries:
|
|
145
|
+
if isinstance(entry, dict) and isinstance(entry.get("spec_name"), str):
|
|
146
|
+
by_name[entry["spec_name"]] = dict(entry)
|
|
147
|
+
|
|
148
|
+
for entry in new_manifest["specs"]:
|
|
149
|
+
previous = by_name.get(entry["spec_name"], {})
|
|
150
|
+
merged_entry = {**previous, **entry}
|
|
151
|
+
if previous.get("role") and entry.get("role") == "unknown":
|
|
152
|
+
merged_entry["role"] = previous["role"]
|
|
153
|
+
by_name[entry["spec_name"]] = merged_entry
|
|
154
|
+
|
|
155
|
+
merged["specs"] = list(by_name.values())
|
|
156
|
+
return merged
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def init_app_context(
|
|
160
|
+
target: Path,
|
|
161
|
+
*,
|
|
162
|
+
mode: str = "app-first",
|
|
163
|
+
spec_sources: list[Path] | None = None,
|
|
164
|
+
force: bool = False,
|
|
165
|
+
) -> AppContextResult:
|
|
166
|
+
if mode not in APP_CONTEXT_MODES:
|
|
167
|
+
raise ValueError(f"mode must be one of: {', '.join(APP_CONTEXT_MODES)}")
|
|
168
|
+
|
|
169
|
+
root = target.expanduser().resolve()
|
|
170
|
+
if root.exists() and not root.is_dir():
|
|
171
|
+
raise NotADirectoryError(str(root))
|
|
172
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
airlock_root = root / "airlock"
|
|
175
|
+
created: list[Path] = []
|
|
176
|
+
updated: list[Path] = []
|
|
177
|
+
kept: list[Path] = []
|
|
178
|
+
|
|
179
|
+
for directory in (
|
|
180
|
+
airlock_root,
|
|
181
|
+
airlock_root / "spec-snapshots",
|
|
182
|
+
airlock_root / "sample-records",
|
|
183
|
+
airlock_root / "generated",
|
|
184
|
+
airlock_root / "generated" / "types",
|
|
185
|
+
airlock_root / "generated" / "sql",
|
|
186
|
+
):
|
|
187
|
+
if directory.exists():
|
|
188
|
+
kept.append(directory)
|
|
189
|
+
else:
|
|
190
|
+
directory.mkdir(parents=True)
|
|
191
|
+
created.append(directory)
|
|
192
|
+
|
|
193
|
+
entries: list[dict[str, Any]] = []
|
|
194
|
+
spec_names: list[str] = []
|
|
195
|
+
for source in spec_sources or []:
|
|
196
|
+
spec_config, sample_records = _load_spec_source(source.expanduser())
|
|
197
|
+
name = spec_name(spec_config)
|
|
198
|
+
safe = _safe_name(name)
|
|
199
|
+
snapshot_path = airlock_root / "spec-snapshots" / f"{safe}.spec.config.json"
|
|
200
|
+
bucket = created if _write_json_if_needed(snapshot_path, spec_config, force=force) == "created" else kept
|
|
201
|
+
bucket.append(snapshot_path)
|
|
202
|
+
|
|
203
|
+
sample_relative: str | None = None
|
|
204
|
+
if sample_records is not None:
|
|
205
|
+
sample_path = airlock_root / "sample-records" / f"{safe}.sample.records.json"
|
|
206
|
+
bucket = created if _write_json_if_needed(sample_path, sample_records, force=force) == "created" else kept
|
|
207
|
+
bucket.append(sample_path)
|
|
208
|
+
sample_relative = str(sample_path.relative_to(root))
|
|
209
|
+
|
|
210
|
+
core = spec_config.get("core_config")
|
|
211
|
+
core = core if isinstance(core, dict) else {}
|
|
212
|
+
entries.append(
|
|
213
|
+
{
|
|
214
|
+
"spec_name": name,
|
|
215
|
+
"spec_alias": core.get("spec_alias"),
|
|
216
|
+
"source": str(source),
|
|
217
|
+
"snapshot": str(snapshot_path.relative_to(root)),
|
|
218
|
+
"sample_records": sample_relative,
|
|
219
|
+
"role": "unknown",
|
|
220
|
+
"snapshot_only": True,
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
spec_names.append(name)
|
|
224
|
+
|
|
225
|
+
for path, content in (
|
|
226
|
+
(airlock_root / "README.md", APP_README),
|
|
227
|
+
(airlock_root / "AGENTS.md", APP_AGENTS),
|
|
228
|
+
(airlock_root / "generated" / "types" / ".gitkeep", ""),
|
|
229
|
+
(airlock_root / "generated" / "sql" / ".gitkeep", ""),
|
|
230
|
+
):
|
|
231
|
+
bucket = created if _write_text_if_needed(path, content, force=force) == "created" else kept
|
|
232
|
+
bucket.append(path)
|
|
233
|
+
|
|
234
|
+
manifest_path = airlock_root / "specs.manifest.json"
|
|
235
|
+
manifest = _manifest(mode, entries)
|
|
236
|
+
if manifest_path.exists() and not force:
|
|
237
|
+
write_json(manifest_path, _merge_manifest(read_json(manifest_path), manifest), force=True)
|
|
238
|
+
updated.append(manifest_path)
|
|
239
|
+
else:
|
|
240
|
+
bucket = created if _write_json_if_needed(manifest_path, manifest, force=force) == "created" else kept
|
|
241
|
+
bucket.append(manifest_path)
|
|
242
|
+
|
|
243
|
+
return AppContextResult(
|
|
244
|
+
root=airlock_root,
|
|
245
|
+
mode=mode,
|
|
246
|
+
created=tuple(created),
|
|
247
|
+
updated=tuple(updated),
|
|
248
|
+
kept=tuple(kept),
|
|
249
|
+
specs=tuple(spec_names),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def format_app_context_result(result: AppContextResult) -> str:
|
|
254
|
+
lines = [f"initialized app context {result.root}", f"mode: {result.mode}"]
|
|
255
|
+
for path in result.created:
|
|
256
|
+
lines.append(f"created {path.relative_to(result.root.parent)}")
|
|
257
|
+
for path in result.updated:
|
|
258
|
+
lines.append(f"updated {path.relative_to(result.root.parent)}")
|
|
259
|
+
for path in result.kept:
|
|
260
|
+
lines.append(f"kept {path.relative_to(result.root.parent)}")
|
|
261
|
+
lines.append("seeded_specs: " + (", ".join(result.specs) if result.specs else "none"))
|
|
262
|
+
lines.extend(
|
|
263
|
+
[
|
|
264
|
+
"",
|
|
265
|
+
"next:",
|
|
266
|
+
"1. Treat spec snapshots as app-local references, not canonical specs.",
|
|
267
|
+
"2. Mark each manifest spec as read, write, or read_write for the app.",
|
|
268
|
+
"3. Build app reads and governed submissions through approved Airlock/Snowflake access paths.",
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
return "\n".join(lines)
|
package/src/airlock_mcp/art.py
CHANGED
|
@@ -21,6 +21,6 @@ def about_text() -> str:
|
|
|
21
21
|
return f"""{MCP_MARK.rstrip()}
|
|
22
22
|
|
|
23
23
|
version: {__version__}
|
|
24
|
-
commands: init-repo, init, import-spec, clone, rename, archive, restore, list-workspaces, summary, next, check, export-csv, render-sql, self-update
|
|
24
|
+
commands: init-repo, init-app-context, init, import-spec, clone, rename, archive, restore, list-workspaces, summary, next, check, export-csv, render-sql, self-update
|
|
25
25
|
principle: Codex is the conversation; files are memory; Airlock is authority.
|
|
26
26
|
"""
|
|
@@ -16,8 +16,9 @@ This repo is an Airlock MCP workspace for drafting Airlock specs with Codex.
|
|
|
16
16
|
The public install surface is Airlock MCP, for example
|
|
17
17
|
`npx @reunionstudio/airlock-mcp install`. Airlock MCP is the single installed
|
|
18
18
|
interface for building specs, using specs, pulling and pushing governed data,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
building apps or workflows that use existing specs, and discovering
|
|
20
|
+
improvements from real use cases. This project repo remains the durable memory
|
|
21
|
+
for the specs being drafted.
|
|
21
22
|
|
|
22
23
|
## Repo Naming
|
|
23
24
|
|
|
@@ -29,6 +30,13 @@ Reserve `airlock-specs` for the canonical reusable Airlock spec library. A
|
|
|
29
30
|
team, customer, domain, or project should use its own scoped repo name such as
|
|
30
31
|
`home-specs`, `acme-finance-specs`, or `customer-onboarding-specs`.
|
|
31
32
|
|
|
33
|
+
Specs work should live in a real version-controlled repo. Prefer GitHub when
|
|
34
|
+
the user already uses GitHub, but any normal Git host is acceptable. Before
|
|
35
|
+
creating a new `<slug>-specs` directory, ask where it should live. If Codex can
|
|
36
|
+
create the repo, offer to initialize git and create or push the GitHub repo. If
|
|
37
|
+
not, ask the user to create the repo and open it in Codex before bootstrapping
|
|
38
|
+
Airlock MCP files.
|
|
39
|
+
|
|
32
40
|
Airlock itself does not create this repo. Codex can create it locally while
|
|
33
41
|
helping the user start an Airlock MCP project, then Airlock receives the
|
|
34
42
|
finished spec later. Start in Codex, not Snowflake Cortex. Snow CLI or Cortex
|
|
@@ -38,7 +46,13 @@ installed Airlock app.
|
|
|
38
46
|
## Starting A Spec Project
|
|
39
47
|
|
|
40
48
|
After bootstrap, welcome the user and orient before creating a workspace. Start
|
|
41
|
-
|
|
49
|
+
by asking which delivery mode they want:
|
|
50
|
+
|
|
51
|
+
1. Spec-first: design governed specs before building the app surface.
|
|
52
|
+
2. App-first: build from existing specs.
|
|
53
|
+
3. Co-development: develop the app and specs together.
|
|
54
|
+
|
|
55
|
+
For spec-first or co-development work, ask:
|
|
42
56
|
|
|
43
57
|
What process do you want to improve?
|
|
44
58
|
|
|
@@ -55,15 +69,65 @@ websites, APIs, data feeds, or physical events. Actions may go back through
|
|
|
55
69
|
those same places. After giving examples, call these places interfaces: where
|
|
56
70
|
the process observes from or acts through.
|
|
57
71
|
|
|
72
|
+
Ask whether the user already has artifacts for the process: CSV or Excel files,
|
|
73
|
+
JSON samples, API docs, schemas, forms, screenshots, PDFs, exports, message
|
|
74
|
+
examples, or other defined content people already use. A small real sample is
|
|
75
|
+
often better than a long explanation. Remind the user to redact secrets before
|
|
76
|
+
attaching files or pasting content.
|
|
77
|
+
|
|
78
|
+
Use `airlock-specs` as a reusable library of starting points, patterns, and
|
|
79
|
+
ideas when it is available. Do not assume library specs match the current shape
|
|
80
|
+
of a third-party API, export, or business object. Prefer current API docs, real
|
|
81
|
+
data exports, samples, and user-provided artifacts when they conflict with the
|
|
82
|
+
library, and record the reason for the divergence.
|
|
83
|
+
|
|
58
84
|
Ask for the messy version. Help turn it into a small first Airlock spec and a
|
|
59
85
|
plan for more. Do not create the first workspace until the user chooses a path.
|
|
60
86
|
Create `posts` only when the user wants a shared feedback loop or explicitly
|
|
61
87
|
asks for the posts pattern.
|
|
62
88
|
|
|
89
|
+
## Building Apps With Existing Specs
|
|
90
|
+
|
|
91
|
+
Use this path when the user wants to build an app, dashboard, approval queue,
|
|
92
|
+
decision UI, analysis workflow, scheduled agent, or other code that uses specs
|
|
93
|
+
they already have access to. Treat this as app/workflow implementation, not
|
|
94
|
+
spec editing, unless the user explicitly asks to change specs.
|
|
95
|
+
|
|
96
|
+
Use `airlock-mcp init-app-context` in an app repo when the app needs local
|
|
97
|
+
Airlock context. It creates `airlock/specs.manifest.json`,
|
|
98
|
+
`airlock/spec-snapshots/`, `airlock/sample-records/`, and generated helper
|
|
99
|
+
folders. These files are app-local references, not canonical specs. Canonical
|
|
100
|
+
specs live in the specs repo or installed Airlock.
|
|
101
|
+
|
|
102
|
+
Identify:
|
|
103
|
+
|
|
104
|
+
- the app goal and decision the app should support
|
|
105
|
+
- read specs such as budgets, expenses, requests, forecasts, observations,
|
|
106
|
+
payouts, or reference data
|
|
107
|
+
- write specs such as decisions, approvals, comments, commitments, actions, or
|
|
108
|
+
follow-ups
|
|
109
|
+
- where the app should run, such as Streamlit, web app, CLI, notebook,
|
|
110
|
+
scheduled agent, or existing app framework
|
|
111
|
+
- the Airlock access path available in this environment
|
|
112
|
+
- identity, evidence, timestamp, approval, and separation-of-duties rules
|
|
113
|
+
|
|
114
|
+
The app may orient the user with summaries, comparisons, rankings, exception
|
|
115
|
+
queues, proposals, or dashboards. It should submit governed choices back
|
|
116
|
+
through an Airlock spec contract. Do not write directly to Airlock-owned tables,
|
|
117
|
+
stages, generated views, or generated tables. Do not bypass spec workflow just
|
|
118
|
+
because the app can connect to Snowflake. If no suitable decision or action spec
|
|
119
|
+
exists, explain the gap and propose a small spec-design step.
|
|
120
|
+
|
|
121
|
+
In co-development mode, keep two tracks visible:
|
|
122
|
+
|
|
123
|
+
- Spec track: row grain, columns, samples, access, validation, workflow
|
|
124
|
+
- App track: screens, reads, decisions, writes, user actions, runtime
|
|
125
|
+
|
|
63
126
|
## Working Style
|
|
64
127
|
|
|
65
128
|
- Use the repo-scoped `$airlock-mcp` skill for spec drafting, review, and
|
|
66
|
-
pattern selection
|
|
129
|
+
pattern selection, or for app/workflow implementation that uses existing
|
|
130
|
+
Airlock specs.
|
|
67
131
|
- Keep drafts small and concrete. Prefer one useful governed output over a
|
|
68
132
|
large speculative process map, then keep a plan for later specs.
|
|
69
133
|
- Preserve decisions in workspace files so future Codex sessions can resume
|
|
@@ -91,6 +155,9 @@ Resolve these before final JSON:
|
|
|
91
155
|
- references and expectations
|
|
92
156
|
- delegation and agent identity
|
|
93
157
|
- interfaces observed from or acted through
|
|
158
|
+
- existing artifacts such as CSV, Excel, JSON, API docs, schemas, exports,
|
|
159
|
+
forms, screenshots, PDFs, or message examples
|
|
160
|
+
- whether an `airlock-specs` library pattern was used, changed, or rejected
|
|
94
161
|
- likely gaps in observations, orientation, decisions, or actions
|
|
95
162
|
- observe-orient-decide-act loop
|
|
96
163
|
|
|
@@ -168,8 +235,9 @@ def format_bootstrap_result(result: BootstrapResult) -> str:
|
|
|
168
235
|
"",
|
|
169
236
|
"next:",
|
|
170
237
|
"1. Open this repo in Codex.",
|
|
171
|
-
"2.
|
|
172
|
-
"3.
|
|
238
|
+
"2. Keep this specs project in git; GitHub is recommended when available.",
|
|
239
|
+
"3. Ask: Use Airlock to help me build and use specs.",
|
|
240
|
+
"4. Start with: What process do you want to improve?",
|
|
173
241
|
]
|
|
174
242
|
)
|
|
175
243
|
return "\n".join(lines)
|