steersync 0.2.0__tar.gz → 0.3.0__tar.gz

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 (42) hide show
  1. {steersync-0.2.0 → steersync-0.3.0}/CHANGELOG.md +46 -0
  2. {steersync-0.2.0 → steersync-0.3.0}/PKG-INFO +113 -48
  3. {steersync-0.2.0 → steersync-0.3.0}/README.md +112 -47
  4. {steersync-0.2.0 → steersync-0.3.0}/pyproject.toml +3 -2
  5. steersync-0.3.0/scripts/prepare_release.py +228 -0
  6. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/__init__.py +1 -1
  7. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/cli.py +152 -8
  8. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/config.py +98 -2
  9. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/errors.py +6 -0
  10. steersync-0.3.0/src/steersync/gitlab.py +211 -0
  11. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/gitops.py +64 -7
  12. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/hooks.py +86 -17
  13. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/lint.py +171 -41
  14. steersync-0.3.0/src/steersync/mcp_server.py +311 -0
  15. steersync-0.3.0/src/steersync/practices.py +533 -0
  16. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/resources/default_config.yaml +63 -7
  17. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/sync.py +205 -14
  18. {steersync-0.2.0 → steersync-0.3.0}/steersync.example.yaml +16 -2
  19. steersync-0.3.0/tests/test_automation.py +493 -0
  20. {steersync-0.2.0 → steersync-0.3.0}/tests/test_cli.py +14 -5
  21. {steersync-0.2.0 → steersync-0.3.0}/tests/test_collections.py +21 -9
  22. {steersync-0.2.0 → steersync-0.3.0}/tests/test_config.py +2 -0
  23. steersync-0.3.0/tests/test_gitlab.py +190 -0
  24. {steersync-0.2.0 → steersync-0.3.0}/tests/test_gitops.py +20 -6
  25. {steersync-0.2.0 → steersync-0.3.0}/tests/test_hardening.py +4 -4
  26. {steersync-0.2.0 → steersync-0.3.0}/tests/test_lint.py +11 -3
  27. {steersync-0.2.0 → steersync-0.3.0}/tests/test_mcp_server.py +28 -12
  28. steersync-0.3.0/tests/test_practices.py +216 -0
  29. steersync-0.3.0/tests/test_prepare_release.py +160 -0
  30. {steersync-0.2.0 → steersync-0.3.0}/tests/test_sync.py +16 -4
  31. steersync-0.2.0/src/steersync/mcp_server.py +0 -171
  32. {steersync-0.2.0 → steersync-0.3.0}/.gitignore +0 -0
  33. {steersync-0.2.0 → steersync-0.3.0}/LICENSE +0 -0
  34. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/__main__.py +0 -0
  35. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/locking.py +0 -0
  36. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/logging_utils.py +0 -0
  37. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/paths.py +0 -0
  38. {steersync-0.2.0 → steersync-0.3.0}/src/steersync/steering.py +0 -0
  39. {steersync-0.2.0 → steersync-0.3.0}/tests/__init__.py +0 -0
  40. {steersync-0.2.0 → steersync-0.3.0}/tests/conftest.py +0 -0
  41. {steersync-0.2.0 → steersync-0.3.0}/tests/test_hooks.py +0 -0
  42. {steersync-0.2.0 → steersync-0.3.0}/tests/test_steering.py +0 -0
@@ -5,6 +5,52 @@ Keep a Changelog and the project uses semantic versioning.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.3.0] - 2026-10-13
9
+
10
+ ### Added
11
+
12
+ - Two more collections from the Kiro configuration scopes: `prompts` (`.kiro/prompts` and
13
+ `~/.kiro/prompts`, the saved prompts that become slash commands) and `agents-md` (the
14
+ `AGENTS.md` at the repository root). Legacy `.kiro.hook` files are picked up by the hooks
15
+ collection and flagged.
16
+ - Best practice validation. Every collection profile now reports warnings taken from the Kiro
17
+ documentation (steering file naming and `auto` descriptions, the `globs` key, always on
18
+ context budget, missing foundation files, agent tool wildcards and startup globs, inline
19
+ agent hooks on CLI 3.0, hook timeouts and legacy schemas, skill name and description
20
+ limits, machine specific paths in `mcp.json`, prompt name length) plus the `settings` and
21
+ `prompt` profiles. Warnings never block; thresholds live under `lint.practice_limits` and
22
+ `lint.practices: false` turns them off. The rules are listed in `docs/validation.md`.
23
+ - `steersync init` writes the Kiro `SessionStart` hook and registers the steersync MCP server
24
+ in `.kiro/settings/mcp.json` (other servers kept, an existing entry left alone), so a Kiro
25
+ session in any repository refreshes and loads the configuration without being asked.
26
+ `steersync lint` warns when either piece is missing.
27
+ - GitLab merge requests through the REST API: `steersync mr` and the `raise_merge_request`
28
+ MCP tool open or find the merge request for the proposal branch, `push` confirms or creates
29
+ it and prints the URL when the token is present. The token comes from `GITLAB_TOKEN` (name
30
+ configurable with `gitlab.token_env`); `gitlab.api_url`, `gitlab.project` and
31
+ `gitlab.target_branch` are derived from `remote.url` and `remote.default_branch` unless set.
32
+ - `push` and `status` list other proposal branches of the same project still on the central
33
+ remote and warn about them (`branch.warn_existing`).
34
+ - Offline fallback: when the central repository is unreachable and a cache clone exists,
35
+ `pull` and `status` apply the cached default branch and say so (`sync.offline_fallback`).
36
+ `pull --best-effort` runs with the hook time budget.
37
+ - Automatic releases: merging a pull request to `main` computes the version from a
38
+ `release:*` label, a Conventional Commits title prefix or the Unreleased changelog section,
39
+ commits the changelog and version bump, publishes to PyPI and creates the tag and GitHub
40
+ release in the same run (`scripts/prepare_release.py`).
41
+
42
+ ### Changed
43
+
44
+ - Proposal branches are named `kiro-config/{project}/{user}-{hash}`; `{user}` now prefers the
45
+ operating system user name (letters, digits and dashes), then the git email, then the git
46
+ name (`branch.user_from`). The template must keep `{project}` and `{hash}`.
47
+ - MCP tools are named by intent and described for the agent: `pull_kiro_config`,
48
+ `push_kiro_config`, `raise_merge_request`, `diff_kiro_config`, `validate_kiro_config` and
49
+ `list_kiro_collections` replace `pull_steering`, `push_steering`, `diff_steering` and
50
+ `lint_steering`. The rendered `mcp.json` entry forwards the GitLab token variable.
51
+ - `pull` logs the central content's lint errors and a one line count of warnings instead of
52
+ every warning.
53
+
8
54
  ## [0.2.0] - 2026-10-12
9
55
 
10
56
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: steersync
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Sync Kiro steering documents between developer machines and a central git repository.
5
5
  Project-URL: Homepage, https://github.com/SCGIS-Wales/steersync
6
6
  Project-URL: Repository, https://github.com/SCGIS-Wales/steersync
@@ -59,7 +59,9 @@ layout; each one is configurable and new ones are added with a single YAML block
59
59
  | `skills` | `.kiro/skills` | `skills` | `skills/` | everything (`SKILL.md`, `scripts/`, `references/`, `assets/`) | `skill`: `SKILL.md` needs `name` and `description`, secrets in scripts |
60
60
  | `agents` | `.kiro/agents` | `agents` | `agents/` | `**/*.json`, `**/*.md` | `agent`: valid JSON with a `name`, flags deprecated `toolsSettings` |
61
61
  | `hooks` | `.kiro/hooks` | `hooks` | `hooks/` | `**/*.json` | `hooks`: `version`, `hooks[]` with `name`, `trigger`, `action` |
62
- | `settings` | `.kiro/settings` | none | `settings/` | `lsp.json`, `mcp.json` | `json`: valid JSON and secrets |
62
+ | `settings` | `.kiro/settings` | none | `settings/` | `lsp.json`, `mcp.json` | `settings`: valid JSON, `mcpServers` and `languages` shapes, machine paths |
63
+ | `prompts` | `.kiro/prompts` | `prompts` | `prompts/` | `**/*.md` | `prompt`: markdown plus the 50 character name limit |
64
+ | `agents-md` | `AGENTS.md` at the root | none | `agents-md/` | `AGENTS.md` | `markdown` (kept in the project repository too) |
63
65
 
64
66
  Execute bits on skill scripts survive the round trip, hidden files are never synced, and
65
67
  files a collection's patterns do not select are never touched on either side. Limit any
@@ -67,6 +69,11 @@ command to some collections with `--collection NAME` (repeatable), list them wit
67
69
  `steersync config collections`, and see [docs/central-layout.md](docs/central-layout.md) for
68
70
  a worked example of a full `.kiro/` tree in the central repository.
69
71
 
72
+ Not synced on purpose: `~/.kiro/powers/` (installed by Kiro from a registry, not authored),
73
+ `~/.kiro/settings/cli.json` and `permissions.yaml` (per user, outside any repository) and
74
+ knowledge bases (declared in agent resources, indexed by Kiro). Every path above comes from
75
+ the Kiro configuration scopes documentation; a new kind of content is one YAML block.
76
+
70
77
  ## Install
71
78
 
72
79
  Requires Python 3.14 or newer and git 2.18 or newer. Every
@@ -74,7 +81,7 @@ Requires Python 3.14 or newer and git 2.18 or newer. Every
74
81
  sdist and a `SHA256SUMS` file, with signed build provenance.
75
82
 
76
83
  ```bash
77
- VERSION=0.2.0
84
+ VERSION=0.3.0
78
85
  uv tool install "https://github.com/SCGIS-Wales/steersync/releases/download/v${VERSION}/steersync-${VERSION}-py3-none-any.whl"
79
86
  # with the MCP server
80
87
  uv tool install "steersync[mcp] @ https://github.com/SCGIS-Wales/steersync/releases/download/v${VERSION}/steersync-${VERSION}-py3-none-any.whl"
@@ -103,7 +110,9 @@ files that the GitHub release carries.
103
110
  2. Prepare a project. This adds every synced directory (`.kiro/steering/`, `.kiro/specs/`,
104
111
  `.kiro/skills/`, ...) and the lock file to `.git/info/exclude` (so centrally managed
105
112
  content is never committed to the project by accident; set `git_exclude: false` on a
106
- collection to keep committing it) and installs a chaining pre-push hook:
113
+ collection to keep committing it), installs a chaining pre-push hook, writes the Kiro
114
+ `SessionStart` hook that pulls at every session start and registers the steersync MCP
115
+ server in `.kiro/settings/mcp.json`:
107
116
 
108
117
  ```bash
109
118
  cd ~/code/my-service
@@ -113,9 +122,28 @@ files that the GitHub release carries.
113
122
 
114
123
  3. Edit anything under `.kiro/`. On your next `git push`, or on demand with
115
124
  `steersync push`, every collection is proposed to the central repository as one merge
116
- request.
125
+ request. With `GITLAB_TOKEN` exported, the merge request URL is printed and
126
+ `steersync mr` opens or finds it at any time.
127
+
128
+ Run `steersync doctor` at any time to see what steersync has resolved, and `steersync lint`
129
+ to validate the configuration against Kiro's rules and best practices.
117
130
 
118
- Run `steersync doctor` at any time to see what steersync has resolved.
131
+ ## Starting Kiro in any repository
132
+
133
+ The goal is that anyone who clones a project and starts Kiro gets the whole configuration
134
+ without asking for it. `steersync init` makes that true for a repository:
135
+
136
+ | Piece | What it does | Written by |
137
+ |---|---|---|
138
+ | `.kiro/hooks/steersync.json` | A V3 `SessionStart` hook running `steersync pull --quiet --best-effort`, so the session starts from the latest merged configuration. Bounded git time; never fails the session. | `steersync init` (or `steersync kiro-hook --write`) |
139
+ | `.kiro/settings/mcp.json` | The steersync MCP server, so "pull the kiro config", "push my steering", "raise an MR for the kiro config" or "validate the agents" work in chat. | `steersync init` (merged into an existing file, other servers kept) |
140
+ | `.git/hooks/pre-push` | Proposes `.kiro/` on every push of the project. | `steersync init --hook` |
141
+
142
+ Kiro then loads what it finds: always on steering in every session, `fileMatch`, `manual`
143
+ and `auto` steering on CLI V3, skills on demand, prompts as slash commands, hooks and
144
+ settings. `steersync lint` warns (`autoload-hook-missing`, `autoload-mcp-missing`) when a
145
+ repository lacks either piece. On CLI 2.x, where standalone hooks do not exist, paste
146
+ `steersync kiro-hook --engine v2` into the custom agent's `hooks` block.
119
147
 
120
148
  ## Central repository layout
121
149
 
@@ -150,7 +178,8 @@ subdirectory; `pull` and `status` print a warning pointing at them until that is
150
178
  | `steersync push [--dry-run] [--scope global] [--no-lint] [-m MSG] [-c NAME] [--json]` | Propose local collections centrally in one branch. `--from-hook` is used by git hooks and never blocks the push unless strict. |
151
179
  | `steersync status [-c NAME] [--json]` | Show adds, updates, deletes and local edits per collection in both directions. |
152
180
  | `steersync lint [PATHS] [-c NAME] [--profile P] [--json]` | Validate every collection with its profile, or explicit paths. Exit code 4 on errors. |
153
- | `steersync init [--write-config] [--hook] [--no-exclude]` | Prepare a repository. |
181
+ | `steersync mr [--branch NAME] [--title T] [-c NAME] [--json]` | Open, or find, the GitLab merge request for the pushed proposal branch. Needs the token in `GITLAB_TOKEN` (configurable). |
182
+ | `steersync init [--write-config] [--hook] [--no-exclude] [--no-kiro] [--mcp-command PATH]` | Prepare a repository: excludes, git hook, Kiro session hook and MCP server entry. |
154
183
  | `steersync install-hook [--force]` | Install or update the chaining pre-push hook. |
155
184
  | `steersync kiro-hook [--engine v2\|v3] [--write]` | Render a Kiro hook that runs `steersync pull --quiet` at session start. |
156
185
  | `steersync mcp-config [--remote URL]` | Render an `mcp.json` fragment. |
@@ -176,6 +205,8 @@ for the packaged defaults.
176
205
  | `STEERSYNC_GIT_BINARY` | `git.binary` |
177
206
  | `STEERSYNC_LOG_LEVEL`, `STEERSYNC_LOG_FORMAT` | `logging.*` |
178
207
  | `STEERSYNC_STRICT` | `hook.strict` |
208
+ | `STEERSYNC_GITLAB_API_URL`, `STEERSYNC_GITLAB_PROJECT` | `gitlab.api_url`, `gitlab.project` |
209
+ | `GITLAB_TOKEN` (name set by `gitlab.token_env`) | Token with the `api` scope used only for merge requests |
179
210
  | `KIRO_HOME` | Location of `~/.kiro`, as honoured by kiro-cli |
180
211
 
181
212
  Unknown keys, invalid patterns, unsafe paths and malformed push options are rejected at load
@@ -221,6 +252,8 @@ share a directory on either side.
221
252
  | A failed push always resets the cache clone to a clean default branch. | automatic |
222
253
  | From a pre-push hook each git call is capped and retried at most once, and no failure, including an unexpected bug, blocks the push unless strict mode is on. | `hook.timeout_seconds`, `hook.strict` |
223
254
  | Central and local content larger than the limits is refused. | `sync.max_files`, `sync.max_total_bytes` |
255
+ | When the central repository cannot be reached and a cache clone exists, `pull` and `status` apply the cached copy of the default branch and say so (`offline` in JSON output). Permanent errors such as authentication failures still fail, and `push` never uses the cache. | `sync.offline_fallback` |
256
+ | `pull --best-effort` (the Kiro session hook) runs with the hook time budget, so a slow server never stalls a session start. | `hook.timeout_seconds` |
224
257
 
225
258
  ## How pull decides what to write
226
259
 
@@ -257,7 +290,7 @@ Teams that already use [pre-commit](https://pre-commit.com) can use the publishe
257
290
  default_install_hook_types: [pre-commit, pre-push]
258
291
  repos:
259
292
  - repo: https://github.com/SCGIS-Wales/steersync
260
- rev: v0.2.0
293
+ rev: v0.3.0
261
294
  hooks:
262
295
  - id: steersync-push
263
296
  - id: steersync-lint
@@ -266,52 +299,84 @@ repos:
266
299
  The lint hook receives the changed files under `.kiro/` and lints each with the profile of
267
300
  the collection it belongs to.
268
301
 
269
- ## GitLab merge requests
270
-
271
- The default push options create a merge request against the default branch, remove the source
272
- branch on merge and set a title. Branch names follow `steering/{project}/{user}-{hash}`, where
273
- the hash covers every file, so two people proposing identical content share one branch and a
274
- second identical push is reported as `already-proposed`. Servers that do not advertise push
275
- options (GitHub, plain bare repositories) make git fail, so steersync retries once without them
276
- and says so. Push options may use `{default_branch}`, `{project}`, `{user}` and `{hash}`.
302
+ ## Proposal branches and GitLab merge requests
277
303
 
278
- ## Lint rules
304
+ Pulls always come from the central repository's default branch (`remote.default_branch`,
305
+ `main` unless configured). Pushes go to a proposal branch named after the project repository
306
+ you are working in:
279
307
 
280
- Each collection runs one lint profile. The `steering` profile applies every rule below.
281
- `markdown` (specs, prompts, references) applies the encoding, size, secret and markdown rules
282
- without Kiro front matter checks. `skill` adds `skill-front-matter-required`,
283
- `skill-requires-metadata` and `skill-name-mismatch` for `SKILL.md`. `json` adds `json-syntax`,
284
- `hooks` adds `hook-schema`, `hook-trigger` and `hook-action`, `agent` adds `agent-schema`,
285
- `agent-name` and `agent-deprecated-key`, and `generic` only enforces the size limit and secret
286
- scanning, allowing binary assets. New profiles are one function in `steersync.lint.PROFILES`.
308
+ ```text
309
+ kiro-config/{project}/{user}-{hash}
310
+ kiro-config/acme/platform/entra-app-registration/dgregor-3f9a1c2e
311
+ ```
287
312
 
288
- | Rule | Severity | Check |
289
- |---|---|---|
290
- | `front-matter-position` | error | Front matter must start on line 1; Kiro ignores it after a blank line. |
291
- | `front-matter-yaml` | error | Front matter must be valid YAML. |
292
- | `inclusion-value` | error | `inclusion` is `always`, `fileMatch`, `manual` or `auto`. |
293
- | `filematch-pattern-required`, `filematch-pattern-type` | error | `fileMatch` needs a string or list of strings. |
294
- | `auto-requires-metadata` | error | `auto` needs `name` and `description`. |
295
- | `unknown-key` | warning | Keys Kiro does not know are ignored; `x-steersync` is allowed. |
296
- | `encoding-utf8`, `max-size` | error | UTF-8 only; default limit 64 KiB because always included bytes cost context. |
297
- | `require-h1` | warning | A level one heading is expected. |
298
- | `file-reference-missing`, `file-reference-escape` | warning, error | `#[[file:path]]`, `#[[file:path:line]]`, `#[[file:path:start-end]]` and `#[[folder:path]]` targets must exist inside the workspace. |
299
- | `forbidden-*` | error | AWS keys, private keys, GitHub, GitLab and Slack tokens, generic credentials. Patterns are configurable. |
300
- | `markdown-MD*` | configurable | pymarkdown rules with the front matter extension enabled. |
313
+ `{project}` is the slug of the project's origin URL, `{user}` comes from the sources listed
314
+ in `branch.user_from` (the operating system user name first, reduced to letters, digits and
315
+ dashes, then the local part of the git `user.email`, then the git `user.name`) and `{hash}`
316
+ covers every synced file, so two people proposing identical content share one branch and a
317
+ second identical push is reported as `already-proposed`. The template is configurable; it
318
+ must keep `{project}` and `{hash}`.
319
+
320
+ Before pushing, steersync lists the other proposal branches of the same project still on
321
+ the remote and warns about them, so unmerged work is reviewed before more is added
322
+ (`branch.warn_existing`). `steersync status` prints the same list.
323
+
324
+ The merge request is opened in two ways:
325
+
326
+ 1. **Push options.** The default `remote.push_options` ask GitLab to create a merge request
327
+ against the default branch, remove the source branch on merge and set a title. Servers
328
+ that do not advertise push options (GitHub, plain bare repositories) make git fail, so
329
+ steersync retries once without them and says so.
330
+ 2. **GitLab API.** When the token variable is set (`GITLAB_TOKEN`, name configurable with
331
+ `gitlab.token_env`), `push` confirms or creates the merge request through the REST API and
332
+ prints its URL, and `steersync mr` (or the `raise_merge_request` MCP tool) does the same on
333
+ demand. The API base URL and project path are derived from `remote.url` and can be set
334
+ with `gitlab.api_url` and `gitlab.project`; `gitlab.target_branch` defaults to
335
+ `remote.default_branch`. The token is read from the environment only, never from a file,
336
+ and never logged.
337
+
338
+ ## Validation
339
+
340
+ Each collection runs one lint profile: `steering`, `agent`, `hooks`, `skill`, `settings`,
341
+ `prompt`, `markdown`, `json` or `generic`. **Errors** are things Kiro cannot read (front
342
+ matter after a blank line, an unknown inclusion mode, invalid JSON, a `SKILL.md` without
343
+ `name` and `description`) or secrets; they stop a push. **Warnings** are best practices from
344
+ the Kiro documentation, reported objectively and never blocking: a steering file with a
345
+ numeric prefix, an `auto` description that is not written as a trigger, a `globs` key where
346
+ Kiro reads `fileMatchPattern`, an agent with `tools: ["*"]` or a `file://.kiro/steering/**`
347
+ resource that loads everything at startup, inline agent hooks on CLI 3.0, a hook with a
348
+ 15 minute timeout, a skill name that is not kebab case, an `mcp.json` with a home directory
349
+ path, more than 48 KiB of always on steering, missing `product.md`, `tech.md` and
350
+ `structure.md`. Every rule, its severity and the documentation page it comes from are listed
351
+ in [docs/validation.md](docs/validation.md); thresholds live under `lint.practice_limits`
352
+ and `lint.practices: false` turns the warnings off.
301
353
 
302
354
  ## Kiro integration
303
355
 
304
- **Session hook (V3).** `steersync kiro-hook --write` creates `.kiro/hooks/steersync.json` with a
305
- `SessionStart` trigger that runs `steersync pull --quiet --best-effort`. For CLI 2.x agents paste the output of
306
- `steersync kiro-hook --engine v2` into the agent's `hooks` block. Field reports say Kiro hooks fire
307
- only in headless runs or with a custom agent active, so treat them as a convenience and keep the
308
- git hook as the dependable trigger.
309
-
310
- **MCP server.** Add the fragment from `steersync mcp-config` to `.kiro/settings/mcp.json`. The
311
- server exposes `pull_steering`, `push_steering`, `diff_steering` and `lint_steering`. Kiro does
312
- not pass the shell's `PATH`, `SSH_AUTH_SOCK` or proxy variables to MCP servers, so the rendered
313
- fragment forwards them explicitly through `env`; use an absolute `--command` if `uvx` is not on
314
- Kiro's path.
356
+ **Session hook (V3).** `steersync init` (or `steersync kiro-hook --write`) creates
357
+ `.kiro/hooks/steersync.json` with a `SessionStart` trigger that runs
358
+ `steersync pull --quiet --best-effort`. For CLI 2.x agents paste the output of
359
+ `steersync kiro-hook --engine v2` into the agent's `hooks` block. Field reports say Kiro hooks
360
+ fire only in headless runs or with a custom agent active, so treat them as a convenience and
361
+ keep the git hook as the dependable trigger.
362
+
363
+ **MCP server.** `steersync init` registers the server in `.kiro/settings/mcp.json`
364
+ (`steersync mcp-config` prints the fragment). The server tells the agent which tool answers
365
+ which request, so plain sentences work:
366
+
367
+ | Say | Tool |
368
+ |---|---|
369
+ | "pull / sync / refresh the kiro config", "update the steering and skills" | `pull_kiro_config` |
370
+ | "push / propose / share my kiro config", "sync my steering up" | `push_kiro_config` |
371
+ | "raise / open an MR for the kiro config", "merge request link" | `raise_merge_request` |
372
+ | "what changed", "status", "any open proposals for this repo" | `diff_kiro_config` |
373
+ | "validate / lint / check the kiro config, agents, hooks, skills" | `validate_kiro_config` |
374
+ | "what is synced and where" | `list_kiro_collections` |
375
+
376
+ Every tool accepts a `collections` list. Kiro does not pass the shell's `PATH`,
377
+ `SSH_AUTH_SOCK`, proxy variables or `GITLAB_TOKEN` to MCP servers, so the rendered entry
378
+ forwards them explicitly through `env`; use `steersync init --mcp-command /abs/path/uvx`
379
+ if `uvx` is not on Kiro's path.
315
380
 
316
381
  **Custom agents.** On Kiro CLI V3 custom agents inherit steering, skills and `AGENTS.md` by
317
382
  default, so list only the always on steering files in `resources` and reference skills with
@@ -28,7 +28,9 @@ layout; each one is configurable and new ones are added with a single YAML block
28
28
  | `skills` | `.kiro/skills` | `skills` | `skills/` | everything (`SKILL.md`, `scripts/`, `references/`, `assets/`) | `skill`: `SKILL.md` needs `name` and `description`, secrets in scripts |
29
29
  | `agents` | `.kiro/agents` | `agents` | `agents/` | `**/*.json`, `**/*.md` | `agent`: valid JSON with a `name`, flags deprecated `toolsSettings` |
30
30
  | `hooks` | `.kiro/hooks` | `hooks` | `hooks/` | `**/*.json` | `hooks`: `version`, `hooks[]` with `name`, `trigger`, `action` |
31
- | `settings` | `.kiro/settings` | none | `settings/` | `lsp.json`, `mcp.json` | `json`: valid JSON and secrets |
31
+ | `settings` | `.kiro/settings` | none | `settings/` | `lsp.json`, `mcp.json` | `settings`: valid JSON, `mcpServers` and `languages` shapes, machine paths |
32
+ | `prompts` | `.kiro/prompts` | `prompts` | `prompts/` | `**/*.md` | `prompt`: markdown plus the 50 character name limit |
33
+ | `agents-md` | `AGENTS.md` at the root | none | `agents-md/` | `AGENTS.md` | `markdown` (kept in the project repository too) |
32
34
 
33
35
  Execute bits on skill scripts survive the round trip, hidden files are never synced, and
34
36
  files a collection's patterns do not select are never touched on either side. Limit any
@@ -36,6 +38,11 @@ command to some collections with `--collection NAME` (repeatable), list them wit
36
38
  `steersync config collections`, and see [docs/central-layout.md](docs/central-layout.md) for
37
39
  a worked example of a full `.kiro/` tree in the central repository.
38
40
 
41
+ Not synced on purpose: `~/.kiro/powers/` (installed by Kiro from a registry, not authored),
42
+ `~/.kiro/settings/cli.json` and `permissions.yaml` (per user, outside any repository) and
43
+ knowledge bases (declared in agent resources, indexed by Kiro). Every path above comes from
44
+ the Kiro configuration scopes documentation; a new kind of content is one YAML block.
45
+
39
46
  ## Install
40
47
 
41
48
  Requires Python 3.14 or newer and git 2.18 or newer. Every
@@ -43,7 +50,7 @@ Requires Python 3.14 or newer and git 2.18 or newer. Every
43
50
  sdist and a `SHA256SUMS` file, with signed build provenance.
44
51
 
45
52
  ```bash
46
- VERSION=0.2.0
53
+ VERSION=0.3.0
47
54
  uv tool install "https://github.com/SCGIS-Wales/steersync/releases/download/v${VERSION}/steersync-${VERSION}-py3-none-any.whl"
48
55
  # with the MCP server
49
56
  uv tool install "steersync[mcp] @ https://github.com/SCGIS-Wales/steersync/releases/download/v${VERSION}/steersync-${VERSION}-py3-none-any.whl"
@@ -72,7 +79,9 @@ files that the GitHub release carries.
72
79
  2. Prepare a project. This adds every synced directory (`.kiro/steering/`, `.kiro/specs/`,
73
80
  `.kiro/skills/`, ...) and the lock file to `.git/info/exclude` (so centrally managed
74
81
  content is never committed to the project by accident; set `git_exclude: false` on a
75
- collection to keep committing it) and installs a chaining pre-push hook:
82
+ collection to keep committing it), installs a chaining pre-push hook, writes the Kiro
83
+ `SessionStart` hook that pulls at every session start and registers the steersync MCP
84
+ server in `.kiro/settings/mcp.json`:
76
85
 
77
86
  ```bash
78
87
  cd ~/code/my-service
@@ -82,9 +91,28 @@ files that the GitHub release carries.
82
91
 
83
92
  3. Edit anything under `.kiro/`. On your next `git push`, or on demand with
84
93
  `steersync push`, every collection is proposed to the central repository as one merge
85
- request.
94
+ request. With `GITLAB_TOKEN` exported, the merge request URL is printed and
95
+ `steersync mr` opens or finds it at any time.
96
+
97
+ Run `steersync doctor` at any time to see what steersync has resolved, and `steersync lint`
98
+ to validate the configuration against Kiro's rules and best practices.
86
99
 
87
- Run `steersync doctor` at any time to see what steersync has resolved.
100
+ ## Starting Kiro in any repository
101
+
102
+ The goal is that anyone who clones a project and starts Kiro gets the whole configuration
103
+ without asking for it. `steersync init` makes that true for a repository:
104
+
105
+ | Piece | What it does | Written by |
106
+ |---|---|---|
107
+ | `.kiro/hooks/steersync.json` | A V3 `SessionStart` hook running `steersync pull --quiet --best-effort`, so the session starts from the latest merged configuration. Bounded git time; never fails the session. | `steersync init` (or `steersync kiro-hook --write`) |
108
+ | `.kiro/settings/mcp.json` | The steersync MCP server, so "pull the kiro config", "push my steering", "raise an MR for the kiro config" or "validate the agents" work in chat. | `steersync init` (merged into an existing file, other servers kept) |
109
+ | `.git/hooks/pre-push` | Proposes `.kiro/` on every push of the project. | `steersync init --hook` |
110
+
111
+ Kiro then loads what it finds: always on steering in every session, `fileMatch`, `manual`
112
+ and `auto` steering on CLI V3, skills on demand, prompts as slash commands, hooks and
113
+ settings. `steersync lint` warns (`autoload-hook-missing`, `autoload-mcp-missing`) when a
114
+ repository lacks either piece. On CLI 2.x, where standalone hooks do not exist, paste
115
+ `steersync kiro-hook --engine v2` into the custom agent's `hooks` block.
88
116
 
89
117
  ## Central repository layout
90
118
 
@@ -119,7 +147,8 @@ subdirectory; `pull` and `status` print a warning pointing at them until that is
119
147
  | `steersync push [--dry-run] [--scope global] [--no-lint] [-m MSG] [-c NAME] [--json]` | Propose local collections centrally in one branch. `--from-hook` is used by git hooks and never blocks the push unless strict. |
120
148
  | `steersync status [-c NAME] [--json]` | Show adds, updates, deletes and local edits per collection in both directions. |
121
149
  | `steersync lint [PATHS] [-c NAME] [--profile P] [--json]` | Validate every collection with its profile, or explicit paths. Exit code 4 on errors. |
122
- | `steersync init [--write-config] [--hook] [--no-exclude]` | Prepare a repository. |
150
+ | `steersync mr [--branch NAME] [--title T] [-c NAME] [--json]` | Open, or find, the GitLab merge request for the pushed proposal branch. Needs the token in `GITLAB_TOKEN` (configurable). |
151
+ | `steersync init [--write-config] [--hook] [--no-exclude] [--no-kiro] [--mcp-command PATH]` | Prepare a repository: excludes, git hook, Kiro session hook and MCP server entry. |
123
152
  | `steersync install-hook [--force]` | Install or update the chaining pre-push hook. |
124
153
  | `steersync kiro-hook [--engine v2\|v3] [--write]` | Render a Kiro hook that runs `steersync pull --quiet` at session start. |
125
154
  | `steersync mcp-config [--remote URL]` | Render an `mcp.json` fragment. |
@@ -145,6 +174,8 @@ for the packaged defaults.
145
174
  | `STEERSYNC_GIT_BINARY` | `git.binary` |
146
175
  | `STEERSYNC_LOG_LEVEL`, `STEERSYNC_LOG_FORMAT` | `logging.*` |
147
176
  | `STEERSYNC_STRICT` | `hook.strict` |
177
+ | `STEERSYNC_GITLAB_API_URL`, `STEERSYNC_GITLAB_PROJECT` | `gitlab.api_url`, `gitlab.project` |
178
+ | `GITLAB_TOKEN` (name set by `gitlab.token_env`) | Token with the `api` scope used only for merge requests |
148
179
  | `KIRO_HOME` | Location of `~/.kiro`, as honoured by kiro-cli |
149
180
 
150
181
  Unknown keys, invalid patterns, unsafe paths and malformed push options are rejected at load
@@ -190,6 +221,8 @@ share a directory on either side.
190
221
  | A failed push always resets the cache clone to a clean default branch. | automatic |
191
222
  | From a pre-push hook each git call is capped and retried at most once, and no failure, including an unexpected bug, blocks the push unless strict mode is on. | `hook.timeout_seconds`, `hook.strict` |
192
223
  | Central and local content larger than the limits is refused. | `sync.max_files`, `sync.max_total_bytes` |
224
+ | When the central repository cannot be reached and a cache clone exists, `pull` and `status` apply the cached copy of the default branch and say so (`offline` in JSON output). Permanent errors such as authentication failures still fail, and `push` never uses the cache. | `sync.offline_fallback` |
225
+ | `pull --best-effort` (the Kiro session hook) runs with the hook time budget, so a slow server never stalls a session start. | `hook.timeout_seconds` |
193
226
 
194
227
  ## How pull decides what to write
195
228
 
@@ -226,7 +259,7 @@ Teams that already use [pre-commit](https://pre-commit.com) can use the publishe
226
259
  default_install_hook_types: [pre-commit, pre-push]
227
260
  repos:
228
261
  - repo: https://github.com/SCGIS-Wales/steersync
229
- rev: v0.2.0
262
+ rev: v0.3.0
230
263
  hooks:
231
264
  - id: steersync-push
232
265
  - id: steersync-lint
@@ -235,52 +268,84 @@ repos:
235
268
  The lint hook receives the changed files under `.kiro/` and lints each with the profile of
236
269
  the collection it belongs to.
237
270
 
238
- ## GitLab merge requests
239
-
240
- The default push options create a merge request against the default branch, remove the source
241
- branch on merge and set a title. Branch names follow `steering/{project}/{user}-{hash}`, where
242
- the hash covers every file, so two people proposing identical content share one branch and a
243
- second identical push is reported as `already-proposed`. Servers that do not advertise push
244
- options (GitHub, plain bare repositories) make git fail, so steersync retries once without them
245
- and says so. Push options may use `{default_branch}`, `{project}`, `{user}` and `{hash}`.
271
+ ## Proposal branches and GitLab merge requests
246
272
 
247
- ## Lint rules
273
+ Pulls always come from the central repository's default branch (`remote.default_branch`,
274
+ `main` unless configured). Pushes go to a proposal branch named after the project repository
275
+ you are working in:
248
276
 
249
- Each collection runs one lint profile. The `steering` profile applies every rule below.
250
- `markdown` (specs, prompts, references) applies the encoding, size, secret and markdown rules
251
- without Kiro front matter checks. `skill` adds `skill-front-matter-required`,
252
- `skill-requires-metadata` and `skill-name-mismatch` for `SKILL.md`. `json` adds `json-syntax`,
253
- `hooks` adds `hook-schema`, `hook-trigger` and `hook-action`, `agent` adds `agent-schema`,
254
- `agent-name` and `agent-deprecated-key`, and `generic` only enforces the size limit and secret
255
- scanning, allowing binary assets. New profiles are one function in `steersync.lint.PROFILES`.
277
+ ```text
278
+ kiro-config/{project}/{user}-{hash}
279
+ kiro-config/acme/platform/entra-app-registration/dgregor-3f9a1c2e
280
+ ```
256
281
 
257
- | Rule | Severity | Check |
258
- |---|---|---|
259
- | `front-matter-position` | error | Front matter must start on line 1; Kiro ignores it after a blank line. |
260
- | `front-matter-yaml` | error | Front matter must be valid YAML. |
261
- | `inclusion-value` | error | `inclusion` is `always`, `fileMatch`, `manual` or `auto`. |
262
- | `filematch-pattern-required`, `filematch-pattern-type` | error | `fileMatch` needs a string or list of strings. |
263
- | `auto-requires-metadata` | error | `auto` needs `name` and `description`. |
264
- | `unknown-key` | warning | Keys Kiro does not know are ignored; `x-steersync` is allowed. |
265
- | `encoding-utf8`, `max-size` | error | UTF-8 only; default limit 64 KiB because always included bytes cost context. |
266
- | `require-h1` | warning | A level one heading is expected. |
267
- | `file-reference-missing`, `file-reference-escape` | warning, error | `#[[file:path]]`, `#[[file:path:line]]`, `#[[file:path:start-end]]` and `#[[folder:path]]` targets must exist inside the workspace. |
268
- | `forbidden-*` | error | AWS keys, private keys, GitHub, GitLab and Slack tokens, generic credentials. Patterns are configurable. |
269
- | `markdown-MD*` | configurable | pymarkdown rules with the front matter extension enabled. |
282
+ `{project}` is the slug of the project's origin URL, `{user}` comes from the sources listed
283
+ in `branch.user_from` (the operating system user name first, reduced to letters, digits and
284
+ dashes, then the local part of the git `user.email`, then the git `user.name`) and `{hash}`
285
+ covers every synced file, so two people proposing identical content share one branch and a
286
+ second identical push is reported as `already-proposed`. The template is configurable; it
287
+ must keep `{project}` and `{hash}`.
288
+
289
+ Before pushing, steersync lists the other proposal branches of the same project still on
290
+ the remote and warns about them, so unmerged work is reviewed before more is added
291
+ (`branch.warn_existing`). `steersync status` prints the same list.
292
+
293
+ The merge request is opened in two ways:
294
+
295
+ 1. **Push options.** The default `remote.push_options` ask GitLab to create a merge request
296
+ against the default branch, remove the source branch on merge and set a title. Servers
297
+ that do not advertise push options (GitHub, plain bare repositories) make git fail, so
298
+ steersync retries once without them and says so.
299
+ 2. **GitLab API.** When the token variable is set (`GITLAB_TOKEN`, name configurable with
300
+ `gitlab.token_env`), `push` confirms or creates the merge request through the REST API and
301
+ prints its URL, and `steersync mr` (or the `raise_merge_request` MCP tool) does the same on
302
+ demand. The API base URL and project path are derived from `remote.url` and can be set
303
+ with `gitlab.api_url` and `gitlab.project`; `gitlab.target_branch` defaults to
304
+ `remote.default_branch`. The token is read from the environment only, never from a file,
305
+ and never logged.
306
+
307
+ ## Validation
308
+
309
+ Each collection runs one lint profile: `steering`, `agent`, `hooks`, `skill`, `settings`,
310
+ `prompt`, `markdown`, `json` or `generic`. **Errors** are things Kiro cannot read (front
311
+ matter after a blank line, an unknown inclusion mode, invalid JSON, a `SKILL.md` without
312
+ `name` and `description`) or secrets; they stop a push. **Warnings** are best practices from
313
+ the Kiro documentation, reported objectively and never blocking: a steering file with a
314
+ numeric prefix, an `auto` description that is not written as a trigger, a `globs` key where
315
+ Kiro reads `fileMatchPattern`, an agent with `tools: ["*"]` or a `file://.kiro/steering/**`
316
+ resource that loads everything at startup, inline agent hooks on CLI 3.0, a hook with a
317
+ 15 minute timeout, a skill name that is not kebab case, an `mcp.json` with a home directory
318
+ path, more than 48 KiB of always on steering, missing `product.md`, `tech.md` and
319
+ `structure.md`. Every rule, its severity and the documentation page it comes from are listed
320
+ in [docs/validation.md](docs/validation.md); thresholds live under `lint.practice_limits`
321
+ and `lint.practices: false` turns the warnings off.
270
322
 
271
323
  ## Kiro integration
272
324
 
273
- **Session hook (V3).** `steersync kiro-hook --write` creates `.kiro/hooks/steersync.json` with a
274
- `SessionStart` trigger that runs `steersync pull --quiet --best-effort`. For CLI 2.x agents paste the output of
275
- `steersync kiro-hook --engine v2` into the agent's `hooks` block. Field reports say Kiro hooks fire
276
- only in headless runs or with a custom agent active, so treat them as a convenience and keep the
277
- git hook as the dependable trigger.
278
-
279
- **MCP server.** Add the fragment from `steersync mcp-config` to `.kiro/settings/mcp.json`. The
280
- server exposes `pull_steering`, `push_steering`, `diff_steering` and `lint_steering`. Kiro does
281
- not pass the shell's `PATH`, `SSH_AUTH_SOCK` or proxy variables to MCP servers, so the rendered
282
- fragment forwards them explicitly through `env`; use an absolute `--command` if `uvx` is not on
283
- Kiro's path.
325
+ **Session hook (V3).** `steersync init` (or `steersync kiro-hook --write`) creates
326
+ `.kiro/hooks/steersync.json` with a `SessionStart` trigger that runs
327
+ `steersync pull --quiet --best-effort`. For CLI 2.x agents paste the output of
328
+ `steersync kiro-hook --engine v2` into the agent's `hooks` block. Field reports say Kiro hooks
329
+ fire only in headless runs or with a custom agent active, so treat them as a convenience and
330
+ keep the git hook as the dependable trigger.
331
+
332
+ **MCP server.** `steersync init` registers the server in `.kiro/settings/mcp.json`
333
+ (`steersync mcp-config` prints the fragment). The server tells the agent which tool answers
334
+ which request, so plain sentences work:
335
+
336
+ | Say | Tool |
337
+ |---|---|
338
+ | "pull / sync / refresh the kiro config", "update the steering and skills" | `pull_kiro_config` |
339
+ | "push / propose / share my kiro config", "sync my steering up" | `push_kiro_config` |
340
+ | "raise / open an MR for the kiro config", "merge request link" | `raise_merge_request` |
341
+ | "what changed", "status", "any open proposals for this repo" | `diff_kiro_config` |
342
+ | "validate / lint / check the kiro config, agents, hooks, skills" | `validate_kiro_config` |
343
+ | "what is synced and where" | `list_kiro_collections` |
344
+
345
+ Every tool accepts a `collections` list. Kiro does not pass the shell's `PATH`,
346
+ `SSH_AUTH_SOCK`, proxy variables or `GITLAB_TOKEN` to MCP servers, so the rendered entry
347
+ forwards them explicitly through `env`; use `steersync init --mcp-command /abs/path/uvx`
348
+ if `uvx` is not on Kiro's path.
284
349
 
285
350
  **Custom agents.** On Kiro CLI V3 custom agents inherit steering, skills and `AGENTS.md` by
286
351
  default, so list only the always on steering files in `resources` and reference skills with
@@ -62,7 +62,7 @@ path = "src/steersync/__init__.py"
62
62
  packages = ["src/steersync"]
63
63
 
64
64
  [tool.hatch.build.targets.sdist]
65
- include = ["src/steersync", "tests", "README.md", "CHANGELOG.md", "LICENSE", "steersync.example.yaml"]
65
+ include = ["src/steersync", "tests", "scripts", "README.md", "CHANGELOG.md", "LICENSE", "steersync.example.yaml"]
66
66
 
67
67
  [tool.ruff]
68
68
  target-version = "py314"
@@ -89,6 +89,7 @@ ignore = [
89
89
 
90
90
  [tool.ruff.lint.per-file-ignores]
91
91
  "tests/**" = ["S101", "S108", "ANN", "PLR2004", "ARG001", "ARG005", "S106", "S105", "E501", "PLC0415"]
92
+ "scripts/**" = ["T201"]
92
93
 
93
94
  [tool.ruff.lint.isort]
94
95
  known-first-party = ["steersync"]
@@ -101,7 +102,7 @@ python_version = "3.14"
101
102
  strict = true
102
103
  warn_unreachable = true
103
104
  show_error_codes = true
104
- files = ["src", "tests"]
105
+ files = ["src", "tests", "scripts"]
105
106
 
106
107
  [[tool.mypy.overrides]]
107
108
  module = ["frontmatter", "frontmatter.*", "pymarkdown.*", "mcp.*"]