@reunionstudio/airlock-mcp 0.1.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.
- package/.agents/skills/airlock-mcp/SKILL.md +122 -0
- package/.agents/skills/airlock-mcp/agents/openai.yaml +4 -0
- package/LICENSE +187 -0
- package/README.md +126 -0
- package/SECURITY.md +31 -0
- package/bin/airlock-mcp.mjs +5 -0
- package/docs/architecture.md +82 -0
- package/docs/install-surface.md +112 -0
- package/docs/ooda-loop.md +40 -0
- package/docs/spec-workbench-architecture.md +161 -0
- package/docs/spec-workspace.md +33 -0
- package/docs/workflows.md +229 -0
- package/package.json +46 -0
- package/patterns/blank/README.md +14 -0
- package/patterns/blank/sample.records.json +19 -0
- package/patterns/blank/spec.config.json +72 -0
- package/patterns/guest-access/individual-isolation.md +27 -0
- package/patterns/guest-access/role-isolation.md +26 -0
- package/patterns/guest-access/shared-contribution.md +25 -0
- package/patterns/manifest.json +16 -0
- package/patterns/spec-types/commitment.md +24 -0
- package/patterns/spec-types/observation.md +22 -0
- package/patterns/spec-types/reconciliation.md +19 -0
- package/patterns/spec-types/reference-master-data.md +21 -0
- package/patterns/starter-posts/README.md +32 -0
- package/patterns/starter-posts/sample.records.json +27 -0
- package/patterns/starter-posts/spec.config.json +135 -0
- package/schemas/airlock-mcp-workspace.schema.json +14 -0
- package/setup.py +41 -0
- package/src/airlock_mcp/__init__.py +3 -0
- package/src/airlock_mcp/__main__.py +5 -0
- package/src/airlock_mcp/art.py +26 -0
- package/src/airlock_mcp/bootstrap.py +161 -0
- package/src/airlock_mcp/cli.py +450 -0
- package/src/airlock_mcp/jsonio.py +56 -0
- package/src/airlock_mcp/manage.py +247 -0
- package/src/airlock_mcp/models.py +43 -0
- package/src/airlock_mcp/patterns.py +49 -0
- package/src/airlock_mcp/project.py +39 -0
- package/src/airlock_mcp/records.py +43 -0
- package/src/airlock_mcp/specs.py +110 -0
- package/src/airlock_mcp/sql.py +15 -0
- package/src/airlock_mcp/summary.py +115 -0
- package/src/airlock_mcp/updater.py +76 -0
- package/src/airlock_mcp/validation.py +334 -0
- package/src/airlock_mcp/workspace.py +223 -0
- package/src/cli.mjs +89 -0
- package/src/install.mjs +100 -0
- package/src/mcp.mjs +184 -0
- package/src/text.mjs +108 -0
- package/src/workbench.mjs +368 -0
- package/workspaces/_template/brief.md +18 -0
- package/workspaces/_template/decisions.md +40 -0
- package/workspaces/_template/questions.md +9 -0
- package/workspaces/_template/review.md +21 -0
- package/workspaces/_template/sample.records.json +19 -0
- package/workspaces/_template/spec.config.json +72 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Guest Access: Shared Contribution
|
|
2
|
+
|
|
3
|
+
Use this when many people or agents contribute to the same governed dataset.
|
|
4
|
+
|
|
5
|
+
Good fit:
|
|
6
|
+
|
|
7
|
+
- posts and feedback
|
|
8
|
+
- general ops issues
|
|
9
|
+
- shared observations
|
|
10
|
+
- public append-only signal streams
|
|
11
|
+
|
|
12
|
+
Design notes:
|
|
13
|
+
|
|
14
|
+
- Enable a public append path for normal contribution.
|
|
15
|
+
- Consider a public read path only when contributors may safely see the shared
|
|
16
|
+
data.
|
|
17
|
+
- Use workflow for moderation, triage, and pushback.
|
|
18
|
+
- Use typed routing fields or tags for simple triage; create specialized specs
|
|
19
|
+
only when lifecycle, evidence, validation, or ownership diverges.
|
|
20
|
+
|
|
21
|
+
Questions to answer:
|
|
22
|
+
|
|
23
|
+
- Can contributors read all records, or only append?
|
|
24
|
+
- Does moderation happen before records become visible?
|
|
25
|
+
- What downstream agent or human watches the shared dataset?
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patterns": [
|
|
3
|
+
{
|
|
4
|
+
"name": "blank",
|
|
5
|
+
"title": "Blank draft",
|
|
6
|
+
"summary": "Minimal observation-style draft for a known process.",
|
|
7
|
+
"directory": "blank"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "posts",
|
|
11
|
+
"title": "Posts feedback loop",
|
|
12
|
+
"summary": "Small governed feedback stream for discovering what to build next.",
|
|
13
|
+
"directory": "starter-posts"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Spec Type: Commitment
|
|
2
|
+
|
|
3
|
+
A commitment spec records a decision or promise the business intends to honor or
|
|
4
|
+
send downstream.
|
|
5
|
+
|
|
6
|
+
Good fit:
|
|
7
|
+
|
|
8
|
+
- approved budget requests
|
|
9
|
+
- payment commitments
|
|
10
|
+
- outbound product updates
|
|
11
|
+
- accepted offers or invitations
|
|
12
|
+
|
|
13
|
+
Design notes:
|
|
14
|
+
|
|
15
|
+
- Distinguish the observed inputs from the committed output.
|
|
16
|
+
- Use workflow for approval and pushback.
|
|
17
|
+
- Use references to link back to source observations, requests, projects, or
|
|
18
|
+
counterparties.
|
|
19
|
+
- Use expectations when commitments must happen in order or by a due date.
|
|
20
|
+
|
|
21
|
+
Avoid:
|
|
22
|
+
|
|
23
|
+
- copying every upstream observation field into the commitment
|
|
24
|
+
- hiding approval state in payload columns instead of Airlock workflow
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Spec Type: Observation
|
|
2
|
+
|
|
3
|
+
An observation spec records something a person, agent, export, browser capture,
|
|
4
|
+
or API observed.
|
|
5
|
+
|
|
6
|
+
Use typed columns for:
|
|
7
|
+
|
|
8
|
+
- observed object id
|
|
9
|
+
- source system or URL
|
|
10
|
+
- observed or captured timestamp
|
|
11
|
+
- status, amount, category, or score used in decisions
|
|
12
|
+
- capture method
|
|
13
|
+
|
|
14
|
+
Use attachments for screenshots, PDFs, exports, receipts, or other evidence.
|
|
15
|
+
Use a validated `variant` for optional source payload context.
|
|
16
|
+
|
|
17
|
+
Avoid:
|
|
18
|
+
|
|
19
|
+
- using Airlock load time as the event timestamp
|
|
20
|
+
- placing raw attachment bytes in payload fields
|
|
21
|
+
- mixing observations and commitments in one row unless `record_type` is an
|
|
22
|
+
intentional design choice
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Spec Type: Reconciliation
|
|
2
|
+
|
|
3
|
+
A reconciliation spec compares two or more systems, datasets, or expectations
|
|
4
|
+
and records the business difference that needs attention.
|
|
5
|
+
|
|
6
|
+
Good fit:
|
|
7
|
+
|
|
8
|
+
- commerce listing gaps
|
|
9
|
+
- payment versus invoice matching
|
|
10
|
+
- ad spend versus campaign records
|
|
11
|
+
- inventory or order discrepancies
|
|
12
|
+
|
|
13
|
+
Design notes:
|
|
14
|
+
|
|
15
|
+
- Make the row grain the discrepancy, match group, or reconciliation finding.
|
|
16
|
+
- Preserve stable keys from every compared system.
|
|
17
|
+
- Use typed fields for match status, amounts, dates, and source identifiers.
|
|
18
|
+
- Use a validated `variant` for optional comparison detail.
|
|
19
|
+
- Use workflow when a human must resolve or accept the finding.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Spec Type: Reference Or Master Data
|
|
2
|
+
|
|
3
|
+
A reference/master-data spec provides controlled data that other specs validate
|
|
4
|
+
against or read from.
|
|
5
|
+
|
|
6
|
+
Good fit:
|
|
7
|
+
|
|
8
|
+
- projects
|
|
9
|
+
- departments
|
|
10
|
+
- vendors
|
|
11
|
+
- employees
|
|
12
|
+
- locations
|
|
13
|
+
- chart-of-accounts segments
|
|
14
|
+
|
|
15
|
+
Design notes:
|
|
16
|
+
|
|
17
|
+
- Use stable keys and readable aliases.
|
|
18
|
+
- Keep lifecycle simple unless changes require review.
|
|
19
|
+
- Use references from transactional specs into these keys.
|
|
20
|
+
- Consider a read-only reference spec over a materialized table for high-read
|
|
21
|
+
agent workflows.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Starter Pattern: Posts
|
|
2
|
+
|
|
3
|
+
Use `posts` when the team does not yet know which larger Airlock spec to build.
|
|
4
|
+
It creates a small governed feedback loop:
|
|
5
|
+
|
|
6
|
+
- a person or agent submits a post, request, observation, or response
|
|
7
|
+
- optional structured details are captured in a validated `variant`
|
|
8
|
+
- other people or agents can review the feed and decide what spec to build next
|
|
9
|
+
|
|
10
|
+
This pattern is intentionally humble. It gives the organization a safe place to
|
|
11
|
+
observe demand before designing a bigger workflow.
|
|
12
|
+
|
|
13
|
+
## When To Use
|
|
14
|
+
|
|
15
|
+
- You are starting from fuzzy process needs.
|
|
16
|
+
- You want feedback from users inside Snowflake.
|
|
17
|
+
- You want agents to ask for, respond to, or triage requests.
|
|
18
|
+
- You need a governed signal stream before committing to specialized specs.
|
|
19
|
+
|
|
20
|
+
## Adapt Carefully
|
|
21
|
+
|
|
22
|
+
Keep the core small:
|
|
23
|
+
|
|
24
|
+
- one row is one post
|
|
25
|
+
- `post_id` is stable and retry-safe
|
|
26
|
+
- `body` is the human-readable signal
|
|
27
|
+
- `tags` supports light routing
|
|
28
|
+
- `details` holds optional structured context
|
|
29
|
+
|
|
30
|
+
Create separate specs later when posts become reimbursements, budget requests,
|
|
31
|
+
project issues, support tickets, or outbound commitments with different
|
|
32
|
+
validation, evidence, workflow, or ownership.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec_name": "posts",
|
|
3
|
+
"filename": "post_001",
|
|
4
|
+
"records": [
|
|
5
|
+
{
|
|
6
|
+
"post_id": "POST-001",
|
|
7
|
+
"reply_to_post_id": "",
|
|
8
|
+
"submitted_by": "agent.deb",
|
|
9
|
+
"posted_at": "2026-06-13 09:00:00",
|
|
10
|
+
"body": "Please make reimbursements easier to prepare and submit.",
|
|
11
|
+
"tags": "#request #finance #reimbursements",
|
|
12
|
+
"related_area": "finance",
|
|
13
|
+
"related_process": "employee reimbursements",
|
|
14
|
+
"details": {
|
|
15
|
+
"source": {
|
|
16
|
+
"system": "codex"
|
|
17
|
+
},
|
|
18
|
+
"request": {
|
|
19
|
+
"desired_outcome": "An approved agent can prepare a draft reimbursement with receipt evidence."
|
|
20
|
+
},
|
|
21
|
+
"agent": {
|
|
22
|
+
"name": "Deb"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"core_config": {
|
|
3
|
+
"spec_name": "posts",
|
|
4
|
+
"spec_alias": "Posts",
|
|
5
|
+
"description": "Governed feedback, requests, observations, and responses from people or agents.",
|
|
6
|
+
"owner_role": "app_admin",
|
|
7
|
+
"is_published": false,
|
|
8
|
+
"is_archived": false
|
|
9
|
+
},
|
|
10
|
+
"column_config": [
|
|
11
|
+
{
|
|
12
|
+
"name": "post_id",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Stable post identifier supplied by the submitting person or agent.",
|
|
15
|
+
"tests": ["not_null", "unique"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "reply_to_post_id",
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Optional parent post id when this post is a response.",
|
|
21
|
+
"tests": []
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "submitted_by",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Business-facing submitter label or upstream user id.",
|
|
27
|
+
"tests": ["not_null"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "posted_at",
|
|
31
|
+
"type": "datetime",
|
|
32
|
+
"description": "Business timestamp when the post was authored or captured.",
|
|
33
|
+
"format": "%Y-%m-%d %H:%M:%S",
|
|
34
|
+
"tests": ["not_null"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "body",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Plain-language feedback, request, observation, or response.",
|
|
40
|
+
"tests": ["not_null"]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "tags",
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Optional routing tags such as #request, #bug, #finance, or #spec-gap.",
|
|
46
|
+
"tests": []
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "related_area",
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Optional business area such as finance, operations, support, product, or commerce.",
|
|
52
|
+
"tests": []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "related_process",
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Optional process this post may lead to, such as reimbursements or budget requests.",
|
|
58
|
+
"tests": []
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "details",
|
|
62
|
+
"type": "variant",
|
|
63
|
+
"description": "Optional structured context for source, request, evidence metadata, or agent notes.",
|
|
64
|
+
"tests": []
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"rules": [
|
|
68
|
+
{
|
|
69
|
+
"type": "variant_shape",
|
|
70
|
+
"field": "details",
|
|
71
|
+
"allowed_root_keys": ["source", "request", "evidence", "agent"],
|
|
72
|
+
"paths": [
|
|
73
|
+
{
|
|
74
|
+
"json_path": "$.source.system",
|
|
75
|
+
"type": "string",
|
|
76
|
+
"required": false
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"json_path": "$.request.desired_outcome",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"required": false
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"json_path": "$.evidence.url",
|
|
85
|
+
"type": "string",
|
|
86
|
+
"required": false
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"json_path": "$.agent.name",
|
|
90
|
+
"type": "string",
|
|
91
|
+
"required": false
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"file_rules": {
|
|
97
|
+
"file_format": {
|
|
98
|
+
"file_type": "csv",
|
|
99
|
+
"record_delimiter": "\n",
|
|
100
|
+
"field_delimiter": ",",
|
|
101
|
+
"field_optionally_enclosed_by": "\"",
|
|
102
|
+
"escape_unenclosed_field": "\\",
|
|
103
|
+
"encoding": "UTF8",
|
|
104
|
+
"parse_header": true,
|
|
105
|
+
"save_header": true
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"attachment_policy": {
|
|
109
|
+
"attachments_enabled": true,
|
|
110
|
+
"attachment_required": false
|
|
111
|
+
},
|
|
112
|
+
"guest_access": {
|
|
113
|
+
"isolated_directories_enabled": false,
|
|
114
|
+
"public_folder": {
|
|
115
|
+
"enabled": true,
|
|
116
|
+
"subfolders": {
|
|
117
|
+
"append_access": {
|
|
118
|
+
"enabled": true
|
|
119
|
+
},
|
|
120
|
+
"read_access": {
|
|
121
|
+
"enabled": true
|
|
122
|
+
},
|
|
123
|
+
"full_access": {
|
|
124
|
+
"enabled": false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"guest_roles": [
|
|
129
|
+
{
|
|
130
|
+
"role_name": "agent",
|
|
131
|
+
"access_level": "append_access"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Airlock MCP Workspace",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["brief", "decisions", "spec_config", "sample_records"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"brief": { "type": "string" },
|
|
8
|
+
"decisions": { "type": "string" },
|
|
9
|
+
"questions": { "type": "string" },
|
|
10
|
+
"review": { "type": "string" },
|
|
11
|
+
"spec_config": { "type": "object" },
|
|
12
|
+
"sample_records": { "type": "object" }
|
|
13
|
+
}
|
|
14
|
+
}
|
package/setup.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from glob import glob
|
|
2
|
+
from os.path import isfile
|
|
3
|
+
|
|
4
|
+
from setuptools import find_packages, setup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def data_files():
|
|
8
|
+
files = [
|
|
9
|
+
(
|
|
10
|
+
"airlock_mcp/.agents/skills/airlock-mcp",
|
|
11
|
+
[path for path in glob(".agents/skills/airlock-mcp/*") if isfile(path)],
|
|
12
|
+
),
|
|
13
|
+
(
|
|
14
|
+
"airlock_mcp/.agents/skills/airlock-mcp/agents",
|
|
15
|
+
[path for path in glob(".agents/skills/airlock-mcp/agents/*") if isfile(path)],
|
|
16
|
+
),
|
|
17
|
+
("airlock_mcp/patterns", ["patterns/manifest.json"]),
|
|
18
|
+
("airlock_mcp/workspaces/_template", glob("workspaces/_template/*")),
|
|
19
|
+
]
|
|
20
|
+
for pattern_dir in glob("patterns/*"):
|
|
21
|
+
if pattern_dir.endswith("manifest.json"):
|
|
22
|
+
continue
|
|
23
|
+
if glob(f"{pattern_dir}/*"):
|
|
24
|
+
files.append((f"airlock_mcp/{pattern_dir}", glob(f"{pattern_dir}/*")))
|
|
25
|
+
return files
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
setup(
|
|
29
|
+
name="airlock-mcp",
|
|
30
|
+
version="0.1.0",
|
|
31
|
+
description="Codex-first workbench and CLI for drafting Airlock specs.",
|
|
32
|
+
packages=find_packages("src"),
|
|
33
|
+
package_dir={"": "src"},
|
|
34
|
+
python_requires=">=3.9",
|
|
35
|
+
data_files=data_files(),
|
|
36
|
+
entry_points={
|
|
37
|
+
"console_scripts": [
|
|
38
|
+
"airlock-mcp=airlock_mcp.cli:main",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from . import __version__
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MCP_MARK = r"""
|
|
7
|
+
airlock-mcp
|
|
8
|
+
|
|
9
|
+
__
|
|
10
|
+
____/ /__ draft specs
|
|
11
|
+
/ __ / _ \ check the shape
|
|
12
|
+
/_/ /_/\___/ forge the handoff
|
|
13
|
+
|
|
14
|
+
_________
|
|
15
|
+
____/_______/ \____
|
|
16
|
+
\_______\_/
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def about_text() -> str:
|
|
21
|
+
return f"""{MCP_MARK.rstrip()}
|
|
22
|
+
|
|
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
|
|
25
|
+
principle: Codex is the conversation; files are memory; Airlock is authority.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .project import repo_root
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
AGENTS_MD = """# Airlock Spec Workspace Guidance
|
|
11
|
+
|
|
12
|
+
This repo is an Airlock MCP workspace for drafting Airlock specs with Codex.
|
|
13
|
+
|
|
14
|
+
## Product Shape
|
|
15
|
+
|
|
16
|
+
The public install surface is Airlock MCP, for example
|
|
17
|
+
`npx @reunionstudio/airlock-mcp install`. Airlock MCP is the single installed
|
|
18
|
+
interface for building specs, using specs, pulling and pushing governed data,
|
|
19
|
+
and discovering improvements from real use cases. This project repo remains
|
|
20
|
+
the durable memory for the specs being drafted.
|
|
21
|
+
|
|
22
|
+
## Repo Naming
|
|
23
|
+
|
|
24
|
+
When helping create a new specs repo, ask for the project or organization name
|
|
25
|
+
and suggest `<slug>-specs`: lowercase the name, replace spaces with hyphens,
|
|
26
|
+
and append `-specs`. For example, `Home` becomes `home-specs`.
|
|
27
|
+
|
|
28
|
+
Reserve `airlock-specs` for the canonical reusable Airlock spec library. A
|
|
29
|
+
team, customer, domain, or project should use its own scoped repo name such as
|
|
30
|
+
`home-specs`, `acme-finance-specs`, or `customer-onboarding-specs`.
|
|
31
|
+
|
|
32
|
+
Airlock itself does not create this repo. Codex can create it locally while
|
|
33
|
+
helping the user start an Airlock MCP project, then Airlock receives the
|
|
34
|
+
finished spec later. Start in Codex, not Snowflake Cortex. Snow CLI or Cortex
|
|
35
|
+
only matters later when validating, creating, or operating specs against an
|
|
36
|
+
installed Airlock app.
|
|
37
|
+
|
|
38
|
+
## Starting A Spec Project
|
|
39
|
+
|
|
40
|
+
After bootstrap, welcome the user and orient before creating a workspace. Ask
|
|
41
|
+
whether they want to:
|
|
42
|
+
|
|
43
|
+
- brainstorm possible specs with the OODA loop
|
|
44
|
+
- start from a known process they already have in mind
|
|
45
|
+
- create a shared `posts` feedback loop for humans and agents to submit
|
|
46
|
+
requests, observations, and responses
|
|
47
|
+
|
|
48
|
+
Create `posts` only when the user chooses a feedback loop or explicitly asks
|
|
49
|
+
for the posts pattern.
|
|
50
|
+
|
|
51
|
+
## Working Style
|
|
52
|
+
|
|
53
|
+
- Use the repo-scoped `$airlock-mcp` skill for spec drafting, review, and
|
|
54
|
+
pattern selection.
|
|
55
|
+
- Keep drafts small and concrete. Prefer one useful governed output over a
|
|
56
|
+
large speculative process map.
|
|
57
|
+
- Preserve decisions in workspace files so future Codex sessions can resume
|
|
58
|
+
without relying on chat memory.
|
|
59
|
+
- Use `airlock-mcp list-workspaces` before guessing which draft to resume.
|
|
60
|
+
- Use `airlock-mcp summary <workspace>` and `airlock-mcp next <workspace>`
|
|
61
|
+
before editing an existing draft.
|
|
62
|
+
- Keep `sample.records.json` as the editable authoring shape, then use
|
|
63
|
+
`airlock-mcp export-csv <workspace>` when Airlock-ready CSV examples are
|
|
64
|
+
needed.
|
|
65
|
+
- Treat Airlock as the execution authority. Local checks are guardrails, not a
|
|
66
|
+
replacement for installed Airlock validation.
|
|
67
|
+
|
|
68
|
+
## Spec Design Priorities
|
|
69
|
+
|
|
70
|
+
Resolve these before final JSON:
|
|
71
|
+
|
|
72
|
+
- row grain
|
|
73
|
+
- durable identifiers
|
|
74
|
+
- business event timestamps
|
|
75
|
+
- typed columns versus validated variants
|
|
76
|
+
- attachment evidence
|
|
77
|
+
- guest access and path isolation
|
|
78
|
+
- workflow and pushback
|
|
79
|
+
- references and expectations
|
|
80
|
+
- delegation and agent identity
|
|
81
|
+
- observe-orient-decide-act loop
|
|
82
|
+
|
|
83
|
+
Do not encode Airlock lifecycle state, reviewer notes, approval status, or
|
|
84
|
+
workflow transitions as submitted payload fields unless they are true upstream
|
|
85
|
+
business facts.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class BootstrapResult:
|
|
91
|
+
root: Path
|
|
92
|
+
created: tuple[Path, ...]
|
|
93
|
+
kept: tuple[Path, ...]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _write_text_if_needed(path: Path, text: str, *, force: bool) -> str:
|
|
97
|
+
if path.exists() and not force:
|
|
98
|
+
return "kept"
|
|
99
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
path.write_text(text, encoding="utf-8")
|
|
101
|
+
return "created"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _copy_file_if_needed(source: Path, target: Path, *, force: bool) -> str:
|
|
105
|
+
if target.exists() and not force:
|
|
106
|
+
return "kept"
|
|
107
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
shutil.copy2(source, target)
|
|
109
|
+
return "created"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def bootstrap_repo(target: Path, *, force: bool = False) -> BootstrapResult:
|
|
113
|
+
root = target.expanduser().resolve()
|
|
114
|
+
if root.exists() and not root.is_dir():
|
|
115
|
+
raise NotADirectoryError(str(root))
|
|
116
|
+
|
|
117
|
+
created: list[Path] = []
|
|
118
|
+
kept: list[Path] = []
|
|
119
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
workspace_root = root / "workspaces"
|
|
122
|
+
if workspace_root.exists():
|
|
123
|
+
kept.append(workspace_root)
|
|
124
|
+
else:
|
|
125
|
+
workspace_root.mkdir()
|
|
126
|
+
created.append(workspace_root)
|
|
127
|
+
|
|
128
|
+
agents_path = root / "AGENTS.md"
|
|
129
|
+
bucket = created if _write_text_if_needed(agents_path, AGENTS_MD, force=force) == "created" else kept
|
|
130
|
+
bucket.append(agents_path)
|
|
131
|
+
|
|
132
|
+
source_skill = repo_root() / ".agents" / "skills" / "airlock-mcp"
|
|
133
|
+
if not source_skill.exists():
|
|
134
|
+
raise FileNotFoundError(str(source_skill))
|
|
135
|
+
|
|
136
|
+
target_skill = root / ".agents" / "skills" / "airlock-mcp"
|
|
137
|
+
for source_file in sorted(path for path in source_skill.rglob("*") if path.is_file()):
|
|
138
|
+
relative = source_file.relative_to(source_skill)
|
|
139
|
+
target_file = target_skill / relative
|
|
140
|
+
bucket = created if _copy_file_if_needed(source_file, target_file, force=force) == "created" else kept
|
|
141
|
+
bucket.append(target_file)
|
|
142
|
+
|
|
143
|
+
return BootstrapResult(root=root, created=tuple(created), kept=tuple(kept))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_bootstrap_result(result: BootstrapResult) -> str:
|
|
147
|
+
lines = [f"initialized {result.root}"]
|
|
148
|
+
for path in result.created:
|
|
149
|
+
lines.append(f"created {path.relative_to(result.root)}")
|
|
150
|
+
for path in result.kept:
|
|
151
|
+
lines.append(f"kept {path.relative_to(result.root)}")
|
|
152
|
+
lines.extend(
|
|
153
|
+
[
|
|
154
|
+
"",
|
|
155
|
+
"next:",
|
|
156
|
+
"1. Open this repo in Codex.",
|
|
157
|
+
"2. Ask: Use Airlock to help me build and use specs.",
|
|
158
|
+
"3. Choose OODA brainstorming, a known process, or a posts feedback loop.",
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
return "\n".join(lines)
|