@reunionstudio/airlock-mcp 0.1.2 → 0.1.4

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/docs/workflows.md CHANGED
@@ -87,9 +87,13 @@ The MCP-style flow should be:
87
87
  4. If Codex is creating it, choose where the repo directory should live before
88
88
  files are written.
89
89
  5. Tell Codex: `Use Airlock to help me improve a process with specs.`
90
- 6. Airlock MCP welcomes, asks what process the user wants to improve, explains
91
- the loop around the process, and proposes a small first spec plus a plan for
92
- more.
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.
93
97
 
94
98
  `init-repo` creates `AGENTS.md`, `.agents/skills/airlock-mcp/SKILL.md`, and
95
99
  `workspaces/`. That is what lets Codex understand prompts such as
@@ -130,6 +134,104 @@ current artifacts and record the divergence.
130
134
  Ask for the messy version. Help turn it into a small first Airlock spec and a
131
135
  plan for more.
132
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
+ Use the installed Airlock procedure grammar:
158
+
159
+ - `airlock.observe.*` is read-only and should answer discovery, governance,
160
+ audit, health, activity, billing-event, and access-explanation questions.
161
+ - `airlock.agent.*` is for governed work by the current actor, including
162
+ validation, loading, workflow actions, attachments, references, and
163
+ delegations.
164
+ - `airlock.admin.*` is for administrative mutation and operational changes.
165
+
166
+ For app-first work, start with observe payloads such as `observe.procedures`,
167
+ `observe.specs`, `observe.spec`, `observe.governance_map`,
168
+ `observe.explain_access`, `observe.health`, `observe.activity`,
169
+ `observe.admin_activity`, `observe.spec_admin_activity`, `observe.billing`, and
170
+ `observe.billing_events`. For `alter_spec` activity, use `CHANGED_SECTIONS` and
171
+ `CHANGED_FIELDS` to triage what changed before fetching version snapshots. Do not use retired admin read
172
+ wrappers such as `admin.list_specs`, `admin.describe_role`, `admin.get_spec`,
173
+ or `admin.list_events`; use observe list/detail procedures instead.
174
+
175
+ The app should follow the loop:
176
+
177
+ 1. Observe/read governed records through approved Airlock or Snowflake surfaces.
178
+ 2. Orient with summaries, comparisons, ranked options, recommendations,
179
+ exception queues, dashboards, or proposals.
180
+ 3. Decide by capturing the human or agent choice with rationale and evidence.
181
+ 4. Act/write by submitting the decision, approval, action, comment, or follow-up
182
+ through an Airlock spec contract.
183
+
184
+ Keep the boundary explicit: do not write directly to Airlock-owned tables,
185
+ stages, generated views, or generated tables. Do not bypass spec workflow just
186
+ because the app can connect to Snowflake. If no suitable decision or action spec
187
+ exists, explain the gap and propose a small spec-design step.
188
+
189
+ If the app repo needs local context for coding, seed it with spec snapshots and
190
+ a manifest:
191
+
192
+ ```bash
193
+ airlock-mcp init-app-context . --mode app-first --spec ../home-specs/workspaces/expenses
194
+ ```
195
+
196
+ Use `--mode co-development` when the app and specs will evolve together. Repeat
197
+ `--spec` for each workspace or spec JSON file the app should know about. The
198
+ command creates:
199
+
200
+ ```text
201
+ airlock/
202
+ AGENTS.md
203
+ README.md
204
+ specs.manifest.json
205
+ spec-snapshots/
206
+ sample-records/
207
+ generated/
208
+ types/
209
+ sql/
210
+ ```
211
+
212
+ The app repo can import or generate helper code from these snapshots, but the
213
+ snapshots are not canonical. Canonical specs remain in the specs repo or
214
+ installed Airlock. Mark each manifest spec as read, write, or read_write for
215
+ the app before building data access and submission flows.
216
+
217
+ ## Co-Develop Specs And App
218
+
219
+ Use this when the app surface and the Airlock contracts need to shape each
220
+ other. This is common when a dashboard, queue, approval flow, or agent workflow
221
+ reveals which observations, proposals, decisions, or actions need governed
222
+ records.
223
+
224
+ Keep two tracks visible:
225
+
226
+ - Spec track: row grain, columns, samples, attachments, access, validation,
227
+ workflow, and installed Airlock validation.
228
+ - App track: screens, reads, summaries, ranked options, decision capture,
229
+ governed writes, runtime, and tests.
230
+
231
+ Start with the smallest useful contract. The app may expose a thin prototype
232
+ against samples while the spec is being drafted, but live submissions should
233
+ wait for installed Airlock validation and the approved read/write path.
234
+
133
235
  ## Start From Feedback
134
236
 
135
237
  Use this when the user chooses a shared feedback loop for humans and agents.
@@ -144,6 +246,28 @@ airlock-mcp check workspaces/feedback-loop
144
246
  Then ask Codex to help decide what the posts should observe, who responds, and
145
247
  what next spec should grow from the feedback.
146
248
 
249
+ ## Start From Governed Knowledge
250
+
251
+ Use this when the user wants accepted Markdown business context for people or
252
+ agents: policies, runbooks, metric definitions, investigations, decisions, or
253
+ operating notes.
254
+
255
+ ```bash
256
+ airlock-mcp init sales-context --pattern okf-knowledge-bundle
257
+ airlock-mcp check workspaces/sales-context
258
+ ```
259
+
260
+ This pattern sets `core_config.payload_adapter` to `okf_knowledge_bundle`.
261
+ Installed Airlock loads locally validated bundles with
262
+ `airlock.admin.load_okf_bundle(...)`, can refresh parsed metadata with
263
+ `airlock.admin.sync_okf_bundle_metadata(...)`, and exposes authoritative
264
+ accepted concept metadata through
265
+ `AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA`.
266
+
267
+ Do not treat draft or rejected Markdown bundles as authoritative agent context.
268
+ The MCP local checker validates the spec workspace; bundle validation belongs
269
+ to Airlock's OKF validator and installed procedures.
270
+
147
271
  ## Start From A Known Process
148
272
 
149
273
  Use this when the process is known but the data shape is not.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reunionstudio/airlock-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Single-install MCP interface for Airlock agents.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,6 +11,12 @@
11
11
  "title": "Posts feedback loop",
12
12
  "summary": "Small governed feedback stream for discovering what to build next.",
13
13
  "directory": "starter-posts"
14
+ },
15
+ {
16
+ "name": "okf-knowledge-bundle",
17
+ "title": "OKF knowledge bundle",
18
+ "summary": "Governed Markdown knowledge bundles for accepted agent context.",
19
+ "directory": "okf-knowledge-bundle"
14
20
  }
15
21
  ]
16
22
  }
@@ -0,0 +1,64 @@
1
+ # Starter Pattern: OKF Knowledge Bundle
2
+
3
+ Use `okf-knowledge-bundle` when the team wants governed Markdown knowledge for
4
+ people and agents to use as accepted business context.
5
+
6
+ This pattern is for Airlock's `okf_knowledge_bundle` payload adapter. It is
7
+ inspired by Google's Open Knowledge Format, but Airlock applies a narrower
8
+ profile so accepted bundles can become governed context instead of loose wiki
9
+ content.
10
+
11
+ ## Use When
12
+
13
+ - Policies, runbooks, metrics, definitions, decisions, investigations, or
14
+ operating context need a governed place to land.
15
+ - Agents need business context that can be accepted, audited, and queried from
16
+ Snowflake.
17
+ - The team has useful Markdown knowledge, but needs Airlock workflow, PDP,
18
+ Expectations, evidence, and acceptance boundaries around it.
19
+
20
+ ## Bundle Shape
21
+
22
+ - Bundle source is a directory or `.zip` archive of UTF-8 Markdown files.
23
+ - Root `index.md` may contain bundle frontmatter such as `okf_version`,
24
+ `bundle_name`, `bundle_version`, `domain`, `owner`, and `purpose`.
25
+ - Every non-reserved Markdown file is a concept document with YAML frontmatter.
26
+ - Concept documents require `type`.
27
+ - Recommended concept fields are `title`, `description`, `resource`, `tags`,
28
+ `timestamp`, and `source_links`.
29
+ - `index.md` and `log.md` are reserved at every hierarchy level.
30
+
31
+ ## Airlock Contract
32
+
33
+ The spec must declare:
34
+
35
+ ```json
36
+ {
37
+ "core_config": {
38
+ "payload_adapter": "okf_knowledge_bundle"
39
+ }
40
+ }
41
+ ```
42
+
43
+ Installed Airlock provides two admin procedures for this adapter:
44
+
45
+ - `airlock.admin.load_okf_bundle(...)`: load a locally validated bundle into
46
+ Airlock stage storage and `FILE_MANIFEST`, then project concept metadata.
47
+ - `airlock.admin.sync_okf_bundle_metadata(...)`: project parsed concept
48
+ metadata for an existing active manifest row.
49
+
50
+ Use `validate_only => TRUE` before writing. Treat accepted context as
51
+ authoritative only from:
52
+
53
+ ```sql
54
+ AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA
55
+ ```
56
+
57
+ Draft and rejected bundles are not authoritative agent context.
58
+
59
+ ## Local Workbench Notes
60
+
61
+ `airlock-mcp check` validates the Airlock spec draft and sample metadata. It
62
+ does not validate a Markdown bundle. Bundle validation currently lives in the
63
+ Airlock source repo's `airlock.okf_knowledge_bundle.validate_okf_bundle`
64
+ helper and the installed Airlock OKF procedures.
@@ -0,0 +1,16 @@
1
+ {
2
+ "spec_name": "okf_knowledge",
3
+ "filename": "okf_knowledge_sample.csv",
4
+ "records": [
5
+ {
6
+ "concept_path": "runbooks/monthly-close",
7
+ "concept_type": "Runbook",
8
+ "bundle_version": "2026.06"
9
+ },
10
+ {
11
+ "concept_path": "metrics/net-revenue",
12
+ "concept_type": "Metric",
13
+ "bundle_version": "2026.06"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "core_config": {
3
+ "spec_name": "okf_knowledge",
4
+ "spec_alias": "OKF Knowledge",
5
+ "description": "Governed OKF-style Markdown knowledge bundles for accepted agent context.",
6
+ "owner_role": "app_admin",
7
+ "current_mode": "append",
8
+ "is_published": false,
9
+ "is_locked": false,
10
+ "is_archived": false,
11
+ "is_multi_role": false,
12
+ "file_workflow": false,
13
+ "workflow_steps": [],
14
+ "materialize_as_table": false,
15
+ "payload_adapter": "okf_knowledge_bundle"
16
+ },
17
+ "column_config": [
18
+ {
19
+ "name": "concept_path",
20
+ "type": "string",
21
+ "description": "Stable normalized Markdown concept path without the .md suffix.",
22
+ "tests": ["not_null", "unique"]
23
+ },
24
+ {
25
+ "name": "concept_type",
26
+ "type": "string",
27
+ "description": "Concept type from OKF frontmatter.",
28
+ "tests": ["not_null"]
29
+ },
30
+ {
31
+ "name": "bundle_version",
32
+ "type": "string",
33
+ "description": "Bundle version containing the concept.",
34
+ "tests": ["not_null"]
35
+ }
36
+ ],
37
+ "rules": [],
38
+ "file_rules": {
39
+ "file_format": {
40
+ "file_type": "csv",
41
+ "record_delimiter": "\n",
42
+ "field_delimiter": ",",
43
+ "field_optionally_enclosed_by": "\"",
44
+ "escape_unenclosed_field": "\\",
45
+ "encoding": "UTF8",
46
+ "parse_header": true,
47
+ "save_header": true
48
+ },
49
+ "filename_rules": {
50
+ "enforce_filename_pattern": false,
51
+ "filename_pattern": null,
52
+ "sample_filenames": null
53
+ }
54
+ },
55
+ "sample_data": {
56
+ "sample_data_acknowledged": true,
57
+ "sample_rows": [],
58
+ "edited_sample_data": "concept_path,concept_type,bundle_version\nrunbooks/monthly-close,Runbook,2026.06\nmetrics/net-revenue,Metric,2026.06\n"
59
+ },
60
+ "attachment_policy": {
61
+ "attachments_enabled": true,
62
+ "attachment_required": false
63
+ }
64
+ }
package/setup.py CHANGED
@@ -27,7 +27,7 @@ def data_files():
27
27
 
28
28
  setup(
29
29
  name="airlock-mcp",
30
- version="0.1.2",
30
+ version="0.1.4",
31
31
  description="Codex-first workbench and CLI for drafting Airlock specs.",
32
32
  packages=find_packages("src"),
33
33
  package_dir={"": "src"},
@@ -1,3 +1,3 @@
1
1
  """Airlock MCP CLI package."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -0,0 +1,303 @@
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
+ Installed Airlock separates procedure intent:
32
+
33
+ - `observe.*`: read-only governance observation for discovery, health, access
34
+ explanation, activity, admin activity, billing events, governance maps, and
35
+ context packets.
36
+ - `agent.*`: governed agent work in the actor's scope.
37
+ - `admin.*`: administrative mutation and operational changes.
38
+
39
+ Prefer `observe.*` for app read-side setup and monitoring questions. Use
40
+ `observe.admin_activity` for broad admin mutation and maintenance audit
41
+ questions, and `observe.spec_admin_activity` for one spec's definition-change
42
+ timeline. For `alter_spec` activity, use `CHANGED_SECTIONS` and
43
+ `CHANGED_FIELDS` to triage what changed before fetching version snapshots.
44
+ Do not use retired admin read wrappers such as `admin.list_specs`,
45
+ `admin.describe_role`, or `admin.list_events`; use the matching observe procedure.
46
+
47
+ Do not store credentials here. Do not write directly to Airlock-owned tables,
48
+ stages, generated views, or generated tables. Use approved Airlock/Snowflake
49
+ access paths and submit governed decisions or actions through spec contracts.
50
+ """
51
+
52
+
53
+ APP_AGENTS = """# Airlock App Guidance
54
+
55
+ This repo may contain application code that uses Airlock specs.
56
+
57
+ ## Modes
58
+
59
+ - Spec-first: design governed specs before building the app surface.
60
+ - App-first: use existing specs to build an app, dashboard, queue, or workflow.
61
+ - Co-development: evolve the app and specs together while keeping the contract
62
+ explicit.
63
+
64
+ ## App Context
65
+
66
+ The `airlock/` folder is app-local. Its spec snapshots are references for
67
+ development, not canonical specs. Canonical specs live in the specs repo or
68
+ installed Airlock.
69
+
70
+ Keep two tracks visible during co-development:
71
+
72
+ - Spec track: row grain, columns, samples, access, validation, and workflow.
73
+ - App track: screens, reads, decisions, writes, user actions, and runtime.
74
+
75
+ Use approved Airlock/Snowflake access paths. Do not bypass spec workflow or
76
+ write directly to Airlock-owned tables, stages, generated views, or generated
77
+ tables. If the app needs to submit a decision or action and no suitable write
78
+ spec exists, propose a small spec-design step.
79
+
80
+ Installed Airlock procedure grammar:
81
+
82
+ - `observe.*` is read-only governance observation. Use it for discovery,
83
+ health, access explanation, governance maps, activity, billing events, and
84
+ context packets.
85
+ - `agent.*` is governed agent work in the actor's scope.
86
+ - `admin.*` is administrative mutation. Do not use retired admin read wrappers;
87
+ use observe list/detail procedures instead.
88
+ """
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class AppContextResult:
93
+ root: Path
94
+ mode: str
95
+ created: tuple[Path, ...]
96
+ updated: tuple[Path, ...]
97
+ kept: tuple[Path, ...]
98
+ specs: tuple[str, ...]
99
+
100
+
101
+ def _safe_name(value: str) -> str:
102
+ cleaned = re.sub(r"[^a-zA-Z0-9_.-]+", "_", value.strip())
103
+ cleaned = cleaned.strip("._-").lower()
104
+ return cleaned or "spec"
105
+
106
+
107
+ def _write_text_if_needed(path: Path, text: str, *, force: bool) -> str:
108
+ if path.exists() and not force:
109
+ return "kept"
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ write_text(path, text, force=True)
112
+ return "created"
113
+
114
+
115
+ def _write_json_if_needed(path: Path, value: Any, *, force: bool) -> str:
116
+ if path.exists() and not force:
117
+ return "kept"
118
+ path.parent.mkdir(parents=True, exist_ok=True)
119
+ write_json(path, value, force=True)
120
+ return "created"
121
+
122
+
123
+ def _load_spec_source(source: Path) -> tuple[dict[str, Any], dict[str, Any] | None]:
124
+ if source.is_dir():
125
+ raw_spec = read_json(source / "spec.config.json")
126
+ if not isinstance(raw_spec, dict):
127
+ raise ValueError(f"invalid spec workspace: {source}")
128
+ raw_sample = read_json(source / "sample.records.json")
129
+ return raw_spec, raw_sample if isinstance(raw_sample, dict) else None
130
+
131
+ raw = read_json(source)
132
+ if raw is None:
133
+ raise FileNotFoundError(source)
134
+ return extract_spec_config(raw), None
135
+
136
+
137
+ def _manifest(mode: str, entries: list[dict[str, Any]]) -> dict[str, Any]:
138
+ return {
139
+ "schema_version": 1,
140
+ "mode": mode,
141
+ "canonical_source": "specs repo or installed Airlock",
142
+ "snapshot_policy": "Snapshots are app-local development references, not canonical specs.",
143
+ "installed_airlock_contract": {
144
+ "observe": "read-only governance observation, context, activity, health, billing events, and access explanation",
145
+ "agent": "governed agent work in the actor scope",
146
+ "admin": "administrative mutation and operational changes",
147
+ },
148
+ "specs": entries,
149
+ "tracks": {
150
+ "spec_track": "row grain, columns, samples, access, validation, workflow",
151
+ "app_track": "screens, reads, decisions, writes, user actions, runtime",
152
+ },
153
+ }
154
+
155
+
156
+ def _merge_manifest(existing: Any, new_manifest: dict[str, Any]) -> dict[str, Any]:
157
+ if not isinstance(existing, dict):
158
+ return new_manifest
159
+
160
+ merged = dict(existing)
161
+ merged.update(
162
+ {
163
+ "schema_version": new_manifest["schema_version"],
164
+ "mode": new_manifest["mode"],
165
+ "canonical_source": new_manifest["canonical_source"],
166
+ "snapshot_policy": new_manifest["snapshot_policy"],
167
+ "installed_airlock_contract": new_manifest["installed_airlock_contract"],
168
+ "tracks": new_manifest["tracks"],
169
+ }
170
+ )
171
+
172
+ existing_entries = existing.get("specs")
173
+ by_name: dict[str, dict[str, Any]] = {}
174
+ if isinstance(existing_entries, list):
175
+ for entry in existing_entries:
176
+ if isinstance(entry, dict) and isinstance(entry.get("spec_name"), str):
177
+ by_name[entry["spec_name"]] = dict(entry)
178
+
179
+ for entry in new_manifest["specs"]:
180
+ previous = by_name.get(entry["spec_name"], {})
181
+ merged_entry = {**previous, **entry}
182
+ if previous.get("role") and entry.get("role") == "unknown":
183
+ merged_entry["role"] = previous["role"]
184
+ by_name[entry["spec_name"]] = merged_entry
185
+
186
+ merged["specs"] = list(by_name.values())
187
+ return merged
188
+
189
+
190
+ def init_app_context(
191
+ target: Path,
192
+ *,
193
+ mode: str = "app-first",
194
+ spec_sources: list[Path] | None = None,
195
+ force: bool = False,
196
+ ) -> AppContextResult:
197
+ if mode not in APP_CONTEXT_MODES:
198
+ raise ValueError(f"mode must be one of: {', '.join(APP_CONTEXT_MODES)}")
199
+
200
+ root = target.expanduser().resolve()
201
+ if root.exists() and not root.is_dir():
202
+ raise NotADirectoryError(str(root))
203
+ root.mkdir(parents=True, exist_ok=True)
204
+
205
+ airlock_root = root / "airlock"
206
+ created: list[Path] = []
207
+ updated: list[Path] = []
208
+ kept: list[Path] = []
209
+
210
+ for directory in (
211
+ airlock_root,
212
+ airlock_root / "spec-snapshots",
213
+ airlock_root / "sample-records",
214
+ airlock_root / "generated",
215
+ airlock_root / "generated" / "types",
216
+ airlock_root / "generated" / "sql",
217
+ ):
218
+ if directory.exists():
219
+ kept.append(directory)
220
+ else:
221
+ directory.mkdir(parents=True)
222
+ created.append(directory)
223
+
224
+ entries: list[dict[str, Any]] = []
225
+ spec_names: list[str] = []
226
+ for source in spec_sources or []:
227
+ spec_config, sample_records = _load_spec_source(source.expanduser())
228
+ name = spec_name(spec_config)
229
+ safe = _safe_name(name)
230
+ snapshot_path = airlock_root / "spec-snapshots" / f"{safe}.spec.config.json"
231
+ bucket = created if _write_json_if_needed(snapshot_path, spec_config, force=force) == "created" else kept
232
+ bucket.append(snapshot_path)
233
+
234
+ sample_relative: str | None = None
235
+ if sample_records is not None:
236
+ sample_path = airlock_root / "sample-records" / f"{safe}.sample.records.json"
237
+ bucket = created if _write_json_if_needed(sample_path, sample_records, force=force) == "created" else kept
238
+ bucket.append(sample_path)
239
+ sample_relative = str(sample_path.relative_to(root))
240
+
241
+ core = spec_config.get("core_config")
242
+ core = core if isinstance(core, dict) else {}
243
+ entries.append(
244
+ {
245
+ "spec_name": name,
246
+ "spec_alias": core.get("spec_alias"),
247
+ "source": str(source),
248
+ "snapshot": str(snapshot_path.relative_to(root)),
249
+ "sample_records": sample_relative,
250
+ "role": "unknown",
251
+ "snapshot_only": True,
252
+ }
253
+ )
254
+ spec_names.append(name)
255
+
256
+ for path, content in (
257
+ (airlock_root / "README.md", APP_README),
258
+ (airlock_root / "AGENTS.md", APP_AGENTS),
259
+ (airlock_root / "generated" / "types" / ".gitkeep", ""),
260
+ (airlock_root / "generated" / "sql" / ".gitkeep", ""),
261
+ ):
262
+ bucket = created if _write_text_if_needed(path, content, force=force) == "created" else kept
263
+ bucket.append(path)
264
+
265
+ manifest_path = airlock_root / "specs.manifest.json"
266
+ manifest = _manifest(mode, entries)
267
+ if manifest_path.exists() and not force:
268
+ write_json(manifest_path, _merge_manifest(read_json(manifest_path), manifest), force=True)
269
+ updated.append(manifest_path)
270
+ else:
271
+ bucket = created if _write_json_if_needed(manifest_path, manifest, force=force) == "created" else kept
272
+ bucket.append(manifest_path)
273
+
274
+ return AppContextResult(
275
+ root=airlock_root,
276
+ mode=mode,
277
+ created=tuple(created),
278
+ updated=tuple(updated),
279
+ kept=tuple(kept),
280
+ specs=tuple(spec_names),
281
+ )
282
+
283
+
284
+ def format_app_context_result(result: AppContextResult) -> str:
285
+ lines = [f"initialized app context {result.root}", f"mode: {result.mode}"]
286
+ for path in result.created:
287
+ lines.append(f"created {path.relative_to(result.root.parent)}")
288
+ for path in result.updated:
289
+ lines.append(f"updated {path.relative_to(result.root.parent)}")
290
+ for path in result.kept:
291
+ lines.append(f"kept {path.relative_to(result.root.parent)}")
292
+ lines.append("seeded_specs: " + (", ".join(result.specs) if result.specs else "none"))
293
+ lines.extend(
294
+ [
295
+ "",
296
+ "next:",
297
+ "1. Treat spec snapshots as app-local references, not canonical specs.",
298
+ "2. Mark each manifest spec as read, write, or read_write for the app.",
299
+ "3. Use observe.* for read-only governance discovery and agent.* for governed actor work.",
300
+ "4. Build governed submissions through approved Airlock/Snowflake access paths.",
301
+ ]
302
+ )
303
+ return "\n".join(lines)
@@ -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
  """