steersync 0.2.0__py3-none-any.whl
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.
- steersync/__init__.py +5 -0
- steersync/__main__.py +6 -0
- steersync/cli.py +768 -0
- steersync/config.py +532 -0
- steersync/errors.py +52 -0
- steersync/gitops.py +550 -0
- steersync/hooks.py +146 -0
- steersync/lint.py +631 -0
- steersync/locking.py +69 -0
- steersync/logging_utils.py +60 -0
- steersync/mcp_server.py +171 -0
- steersync/paths.py +98 -0
- steersync/resources/default_config.yaml +154 -0
- steersync/steering.py +457 -0
- steersync/sync.py +726 -0
- steersync-0.2.0.dist-info/METADATA +342 -0
- steersync-0.2.0.dist-info/RECORD +20 -0
- steersync-0.2.0.dist-info/WHEEL +4 -0
- steersync-0.2.0.dist-info/entry_points.txt +2 -0
- steersync-0.2.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Logging configuration with secret redaction and optional JSON output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
_REDACT_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
13
|
+
re.compile(r"(?<=://)[^/@\s]+@"),
|
|
14
|
+
re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}"),
|
|
15
|
+
re.compile(r"glpat-[A-Za-z0-9_-]{20,}"),
|
|
16
|
+
re.compile(r"(?<![A-Z0-9])(AKIA|ASIA)[0-9A-Z]{16}(?![A-Z0-9])"),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def redact(text: str) -> str:
|
|
21
|
+
"""Mask credential like substrings before they reach any log sink."""
|
|
22
|
+
for pattern in _REDACT_PATTERNS:
|
|
23
|
+
text = pattern.sub("***", text)
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _JsonFormatter(logging.Formatter):
|
|
28
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
29
|
+
payload = {
|
|
30
|
+
"time": datetime.fromtimestamp(record.created, tz=UTC).isoformat(
|
|
31
|
+
timespec="milliseconds"
|
|
32
|
+
),
|
|
33
|
+
"level": record.levelname,
|
|
34
|
+
"logger": record.name,
|
|
35
|
+
"message": redact(record.getMessage()),
|
|
36
|
+
}
|
|
37
|
+
if record.exc_info:
|
|
38
|
+
payload["exception"] = redact(self.formatException(record.exc_info))
|
|
39
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _TextFormatter(logging.Formatter):
|
|
43
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
44
|
+
return redact(super().format(record))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configure_logging(
|
|
48
|
+
level: str = "INFO", fmt: Literal["text", "json"] = "text", *, quiet: bool = False
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Configure the root logger for CLI use. Logs go to stderr so stdout stays machine readable."""
|
|
51
|
+
root = logging.getLogger()
|
|
52
|
+
for handler in list(root.handlers):
|
|
53
|
+
root.removeHandler(handler)
|
|
54
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
55
|
+
formatter: logging.Formatter = (
|
|
56
|
+
_JsonFormatter() if fmt == "json" else _TextFormatter("%(levelname)s %(name)s: %(message)s")
|
|
57
|
+
)
|
|
58
|
+
handler.setFormatter(formatter)
|
|
59
|
+
root.addHandler(handler)
|
|
60
|
+
root.setLevel("ERROR" if quiet else level)
|
steersync/mcp_server.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Optional MCP stdio server exposing steersync operations as tools.
|
|
2
|
+
|
|
3
|
+
Requires the ``mcp`` extra: ``pip install "steersync[mcp]"``. All tools call the
|
|
4
|
+
same functions the CLI uses, so behaviour and safety checks are identical. Every
|
|
5
|
+
tool takes an optional ``collections`` list (steering, specs, skills, agents,
|
|
6
|
+
hooks, settings or any configured name); the default is every enabled collection.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from steersync import gitops, sync
|
|
16
|
+
from steersync.config import Config, load_config
|
|
17
|
+
from steersync.errors import SteersyncError
|
|
18
|
+
from steersync.lint import lint_paths, merge_reports
|
|
19
|
+
from steersync.paths import workspace_collection_dir
|
|
20
|
+
from steersync.steering import FileSpec
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load(cwd: str | None) -> tuple[Config, Path | None]:
|
|
24
|
+
start = Path(cwd).expanduser() if cwd else Path.cwd()
|
|
25
|
+
bootstrap = load_config(workspace_root=None)
|
|
26
|
+
root = gitops.repo_toplevel(bootstrap, start, env=gitops.git_environment(os.environ))
|
|
27
|
+
return load_config(workspace_root=root), root
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _plan_dict(plan: Any) -> dict[str, Any]: # noqa: ANN401
|
|
31
|
+
if plan is None:
|
|
32
|
+
return {}
|
|
33
|
+
return {
|
|
34
|
+
"add": list(plan.add),
|
|
35
|
+
"update": list(plan.update),
|
|
36
|
+
"delete": list(plan.delete),
|
|
37
|
+
"conflict": list(plan.conflict),
|
|
38
|
+
"unchanged": len(plan.unchanged),
|
|
39
|
+
"local_only": list(plan.local_only),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _plans_dict(plans: dict[str, Any] | None) -> dict[str, Any]:
|
|
44
|
+
if plans is None:
|
|
45
|
+
return {}
|
|
46
|
+
return {name: _plan_dict(plan) for name, plan in plans.items()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def pull_steering(
|
|
50
|
+
cwd: str | None = None,
|
|
51
|
+
force: bool = False,
|
|
52
|
+
dry_run: bool = False,
|
|
53
|
+
collections: list[str] | None = None,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Pull global and workspace collections (steering, specs, skills, ...) from central."""
|
|
56
|
+
try:
|
|
57
|
+
config, root = _load(cwd)
|
|
58
|
+
result = sync.pull(
|
|
59
|
+
config, workspace_root=root, force=force, dry_run=dry_run, collections=collections
|
|
60
|
+
)
|
|
61
|
+
except (SteersyncError, OSError) as exc:
|
|
62
|
+
return {"ok": False, "error": str(exc)}
|
|
63
|
+
scopes: dict[str, dict[str, Any]] = {}
|
|
64
|
+
for item in result.results:
|
|
65
|
+
scopes.setdefault(item.scope, {})[item.collection] = {
|
|
66
|
+
"target": str(item.target_dir),
|
|
67
|
+
"plan": _plan_dict(item.plan),
|
|
68
|
+
"written": list(item.written),
|
|
69
|
+
}
|
|
70
|
+
return {"ok": True, "commit": result.commit, "dry_run": result.dry_run, "scopes": scopes}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def push_steering(
|
|
74
|
+
cwd: str | None = None,
|
|
75
|
+
dry_run: bool = False,
|
|
76
|
+
message: str | None = None,
|
|
77
|
+
collections: list[str] | None = None,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Propose the workspace collections as one branch on the central repository."""
|
|
80
|
+
try:
|
|
81
|
+
config, root = _load(cwd)
|
|
82
|
+
if root is None:
|
|
83
|
+
return {"ok": False, "error": "not inside a git repository"}
|
|
84
|
+
result = sync.push(
|
|
85
|
+
config, workspace_root=root, dry_run=dry_run, message=message, collections=collections
|
|
86
|
+
)
|
|
87
|
+
except (SteersyncError, OSError) as exc:
|
|
88
|
+
return {"ok": False, "error": str(exc)}
|
|
89
|
+
return {
|
|
90
|
+
"ok": True,
|
|
91
|
+
"status": result.status,
|
|
92
|
+
"project": result.project,
|
|
93
|
+
"collections": list(result.collections),
|
|
94
|
+
"branch": result.branch,
|
|
95
|
+
"commit": result.commit,
|
|
96
|
+
"message": result.message,
|
|
97
|
+
"plan": _plan_dict(result.plan),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def diff_steering(cwd: str | None = None, collections: list[str] | None = None) -> dict[str, Any]:
|
|
102
|
+
"""Compare local collections with the central repository without changing anything."""
|
|
103
|
+
try:
|
|
104
|
+
config, root = _load(cwd)
|
|
105
|
+
result = sync.status(config, workspace_root=root, collections=collections)
|
|
106
|
+
except (SteersyncError, OSError) as exc:
|
|
107
|
+
return {"ok": False, "error": str(exc)}
|
|
108
|
+
return {
|
|
109
|
+
"ok": True,
|
|
110
|
+
"project": result.project,
|
|
111
|
+
"remote_commit": result.remote_commit,
|
|
112
|
+
"workspace": _plans_dict(result.workspace),
|
|
113
|
+
"global": _plans_dict(result.global_),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def lint_steering(cwd: str | None = None, collections: list[str] | None = None) -> dict[str, Any]:
|
|
118
|
+
"""Lint the workspace collections with each collection's profile."""
|
|
119
|
+
try:
|
|
120
|
+
config, root = _load(cwd)
|
|
121
|
+
if root is None:
|
|
122
|
+
return {"ok": False, "error": "not inside a git repository"}
|
|
123
|
+
reports = []
|
|
124
|
+
for collection in config.select_collections(collections).values():
|
|
125
|
+
target = workspace_collection_dir(collection, root)
|
|
126
|
+
if target is None:
|
|
127
|
+
continue
|
|
128
|
+
reports.append(
|
|
129
|
+
lint_paths(
|
|
130
|
+
[target],
|
|
131
|
+
config.lint,
|
|
132
|
+
root,
|
|
133
|
+
profile=collection.lint,
|
|
134
|
+
spec=FileSpec(collection.include, collection.exclude, collection.max_depth),
|
|
135
|
+
max_file_bytes=collection.max_file_bytes,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
report = merge_reports(reports)
|
|
139
|
+
except (SteersyncError, OSError) as exc:
|
|
140
|
+
return {"ok": False, "error": str(exc)}
|
|
141
|
+
return {
|
|
142
|
+
"ok": report.ok,
|
|
143
|
+
"checked": report.checked,
|
|
144
|
+
"findings": [f._asdict() for f in report.findings],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_server() -> Any: # noqa: ANN401
|
|
149
|
+
"""Create the MCP server. Imported lazily so the core has no MCP dependency."""
|
|
150
|
+
try:
|
|
151
|
+
from mcp.server.mcpserver import MCPServer # noqa: PLC0415
|
|
152
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
153
|
+
msg = "the MCP server needs the optional dependency: pip install 'steersync[mcp]'"
|
|
154
|
+
raise SteersyncError(msg) from exc
|
|
155
|
+
server = MCPServer(
|
|
156
|
+
"steersync",
|
|
157
|
+
instructions=(
|
|
158
|
+
"Sync Kiro steering, specs, skills, agents, hooks and settings with the central "
|
|
159
|
+
"repository. Pass collections=[...] to limit a tool to some collections."
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
server.tool(name="pull_steering")(pull_steering)
|
|
163
|
+
server.tool(name="push_steering")(push_steering)
|
|
164
|
+
server.tool(name="diff_steering")(diff_steering)
|
|
165
|
+
server.tool(name="lint_steering")(lint_steering)
|
|
166
|
+
return server
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def serve() -> None:
|
|
170
|
+
"""Run the stdio MCP server until the client disconnects."""
|
|
171
|
+
build_server().run(transport="stdio")
|
steersync/paths.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Path resolution for Kiro directories, collection targets, locks and the clone cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from steersync.config import CollectionConfig, Config
|
|
10
|
+
from steersync.errors import UnsafePathError
|
|
11
|
+
|
|
12
|
+
DEFAULT_WORKSPACE_STEERING = ".kiro/steering"
|
|
13
|
+
DEFAULT_GLOBAL_STEERING = "steering"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def kiro_home(env: Mapping[str, str]) -> Path:
|
|
17
|
+
"""Return the Kiro home directory honouring ``KIRO_HOME`` like kiro-cli does."""
|
|
18
|
+
override = env.get("KIRO_HOME")
|
|
19
|
+
if override:
|
|
20
|
+
return Path(override).expanduser()
|
|
21
|
+
return Path(env.get("HOME", "~")).expanduser() / ".kiro"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def global_collection_dir(collection: CollectionConfig, env: Mapping[str, str]) -> Path | None:
|
|
25
|
+
"""Return the global target directory of a collection, or ``None`` without a global scope."""
|
|
26
|
+
if collection.global_path is None:
|
|
27
|
+
return None
|
|
28
|
+
return kiro_home(env) / collection.global_path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def workspace_collection_dir(collection: CollectionConfig, workspace_root: Path) -> Path | None:
|
|
32
|
+
"""Return the workspace target directory of a collection, or ``None`` without one."""
|
|
33
|
+
if collection.workspace_path is None:
|
|
34
|
+
return None
|
|
35
|
+
return workspace_root / collection.workspace_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def central_collection_dir(base: Path, collection: CollectionConfig) -> Path:
|
|
39
|
+
"""Return the collection directory under a central ``global/`` or project directory."""
|
|
40
|
+
return ensure_within(base, Path(collection.central_dir or ""))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def global_steering_dir(config: Config, env: Mapping[str, str]) -> Path:
|
|
44
|
+
"""Return the global steering directory (the ``steering`` collection or its default)."""
|
|
45
|
+
steering = config.collections.get("steering")
|
|
46
|
+
relative = (
|
|
47
|
+
steering.global_path
|
|
48
|
+
if steering is not None and steering.global_path is not None
|
|
49
|
+
else DEFAULT_GLOBAL_STEERING
|
|
50
|
+
)
|
|
51
|
+
return kiro_home(env) / relative
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def workspace_steering_dir(config: Config, workspace_root: Path) -> Path:
|
|
55
|
+
"""Return the workspace steering directory (the ``steering`` collection or its default)."""
|
|
56
|
+
steering = config.collections.get("steering")
|
|
57
|
+
relative = (
|
|
58
|
+
steering.workspace_path
|
|
59
|
+
if steering is not None and steering.workspace_path is not None
|
|
60
|
+
else DEFAULT_WORKSPACE_STEERING
|
|
61
|
+
)
|
|
62
|
+
return workspace_root / relative
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def global_lock_path(config: Config, env: Mapping[str, str]) -> Path:
|
|
66
|
+
return kiro_home(env) / config.paths.global_lock
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def workspace_lock_path(config: Config, workspace_root: Path) -> Path:
|
|
70
|
+
return workspace_root / config.paths.workspace_lock
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cache_root(config: Config, env: Mapping[str, str]) -> Path:
|
|
74
|
+
raw = config.paths.cache_dir
|
|
75
|
+
if raw.startswith("~"):
|
|
76
|
+
home = env.get("HOME")
|
|
77
|
+
return Path(home) / raw[2:] if home else Path(raw).expanduser()
|
|
78
|
+
return Path(raw)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cache_repo_dir(config: Config, env: Mapping[str, str], remote_url: str) -> Path:
|
|
82
|
+
"""Return a stable cache directory for a remote URL."""
|
|
83
|
+
digest = hashlib.sha256(remote_url.encode("utf-8")).hexdigest()[:16]
|
|
84
|
+
return cache_root(config, env) / "repos" / digest
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ensure_within(base: Path, candidate: Path) -> Path:
|
|
88
|
+
"""Resolve ``candidate`` and ensure it stays inside ``base``."""
|
|
89
|
+
base_resolved = base.resolve()
|
|
90
|
+
target = (
|
|
91
|
+
(base_resolved / candidate).resolve()
|
|
92
|
+
if not candidate.is_absolute()
|
|
93
|
+
else candidate.resolve()
|
|
94
|
+
)
|
|
95
|
+
if target != base_resolved and base_resolved not in target.parents:
|
|
96
|
+
msg = f"path {candidate} escapes {base}"
|
|
97
|
+
raise UnsafePathError(msg)
|
|
98
|
+
return target
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Default steersync configuration. User configuration files are deep merged over
|
|
2
|
+
# these values. Environment variables override individual settings; see README.
|
|
3
|
+
remote:
|
|
4
|
+
# URL of the central steering repository. SSH or HTTPS. Authentication is
|
|
5
|
+
# handled by the git binary (ssh-agent or credential helper), never by steersync.
|
|
6
|
+
url: ""
|
|
7
|
+
default_branch: main
|
|
8
|
+
# Directory in the central repository holding steering applied to every workspace.
|
|
9
|
+
global_dir: global
|
|
10
|
+
# Directory in the central repository holding per project steering, keyed by project slug.
|
|
11
|
+
projects_dir: projects
|
|
12
|
+
# Optional allow list of remote hosts. Empty means any host is accepted.
|
|
13
|
+
allowed_hosts: []
|
|
14
|
+
# Allow file:// and absolute local paths. Only for tests and offline evaluation.
|
|
15
|
+
allow_local: false
|
|
16
|
+
# Git push options sent with each proposal branch. Placeholders: {default_branch},
|
|
17
|
+
# {project}, {user}, {hash}. GitLab turns these into a merge request; other
|
|
18
|
+
# servers ignore them silently.
|
|
19
|
+
push_options:
|
|
20
|
+
- merge_request.create
|
|
21
|
+
- merge_request.target={default_branch}
|
|
22
|
+
- merge_request.remove_source_branch
|
|
23
|
+
- "merge_request.title=steering: update {project} from {user}"
|
|
24
|
+
|
|
25
|
+
git:
|
|
26
|
+
binary: git
|
|
27
|
+
# Per invocation timeout. Network operations (clone, fetch, ls-remote, push)
|
|
28
|
+
# are retried on transient failures with exponential backoff and jitter.
|
|
29
|
+
timeout_seconds: 120
|
|
30
|
+
clone_depth: 1
|
|
31
|
+
retries: 2
|
|
32
|
+
retry_backoff_seconds: 2.0
|
|
33
|
+
retry_max_backoff_seconds: 30.0
|
|
34
|
+
|
|
35
|
+
branch:
|
|
36
|
+
# Placeholders: {project}, {user}, {hash}
|
|
37
|
+
template: "steering/{project}/{user}-{hash}"
|
|
38
|
+
hash_length: 8
|
|
39
|
+
|
|
40
|
+
paths:
|
|
41
|
+
workspace_lock: .kiro/steersync.lock.json
|
|
42
|
+
# Resolved against KIRO_HOME when set, else ~/.kiro
|
|
43
|
+
global_lock: steersync.lock.json
|
|
44
|
+
cache_dir: ~/.cache/steersync
|
|
45
|
+
|
|
46
|
+
# Collections are the kinds of Kiro content that are synced. Each one maps a
|
|
47
|
+
# workspace directory (relative to the repository root) and, optionally, a global
|
|
48
|
+
# directory (relative to KIRO_HOME, default ~/.kiro) onto a directory in the
|
|
49
|
+
# central repository: global/<central_dir>/ and projects/<slug>/<central_dir>/.
|
|
50
|
+
#
|
|
51
|
+
# include and exclude are glob patterns relative to the collection directory
|
|
52
|
+
# (** spans directories). lint selects the validation profile: steering,
|
|
53
|
+
# markdown, skill, agent, hooks, json or generic. Set enabled: false to switch a
|
|
54
|
+
# collection off, or add a new key to sync something else. Hidden files and
|
|
55
|
+
# directories are never synced.
|
|
56
|
+
collections:
|
|
57
|
+
steering:
|
|
58
|
+
description: Kiro steering documents
|
|
59
|
+
workspace_path: .kiro/steering
|
|
60
|
+
global_path: steering
|
|
61
|
+
central_dir: steering
|
|
62
|
+
include: ["**/*.md"]
|
|
63
|
+
lint: steering
|
|
64
|
+
specs:
|
|
65
|
+
description: Spec driven development plans (requirements, design, tasks)
|
|
66
|
+
workspace_path: .kiro/specs
|
|
67
|
+
global_path: null
|
|
68
|
+
central_dir: specs
|
|
69
|
+
include: ["**/*.md"]
|
|
70
|
+
lint: markdown
|
|
71
|
+
skills:
|
|
72
|
+
description: Agent Skills (SKILL.md with scripts, references and assets)
|
|
73
|
+
workspace_path: .kiro/skills
|
|
74
|
+
global_path: skills
|
|
75
|
+
central_dir: skills
|
|
76
|
+
include: ["**/*"]
|
|
77
|
+
exclude: ["**/__pycache__/**", "**/node_modules/**"]
|
|
78
|
+
lint: skill
|
|
79
|
+
max_file_bytes: 1048576
|
|
80
|
+
agents:
|
|
81
|
+
description: Custom agent definitions and their prompt files
|
|
82
|
+
workspace_path: .kiro/agents
|
|
83
|
+
global_path: agents
|
|
84
|
+
central_dir: agents
|
|
85
|
+
include: ["**/*.json", "**/*.md"]
|
|
86
|
+
lint: agent
|
|
87
|
+
hooks:
|
|
88
|
+
description: Standalone Kiro hooks (V3 and IDE 1.0)
|
|
89
|
+
workspace_path: .kiro/hooks
|
|
90
|
+
global_path: hooks
|
|
91
|
+
central_dir: hooks
|
|
92
|
+
include: ["**/*.json"]
|
|
93
|
+
lint: hooks
|
|
94
|
+
settings:
|
|
95
|
+
description: Workspace settings such as the language server configuration
|
|
96
|
+
workspace_path: .kiro/settings
|
|
97
|
+
global_path: null
|
|
98
|
+
central_dir: settings
|
|
99
|
+
include: ["lsp.json", "mcp.json"]
|
|
100
|
+
lint: json
|
|
101
|
+
|
|
102
|
+
sync:
|
|
103
|
+
# Delete locally managed files that disappeared from the central repository.
|
|
104
|
+
prune: true
|
|
105
|
+
# Fail a pull when the remote content has lint errors.
|
|
106
|
+
strict_pull: false
|
|
107
|
+
# Wait this long for another steersync process using the same cache clone.
|
|
108
|
+
lock_timeout_seconds: 120
|
|
109
|
+
# Safety limits for content read from the central repository.
|
|
110
|
+
max_files: 1000
|
|
111
|
+
max_total_bytes: 10485760
|
|
112
|
+
|
|
113
|
+
lint:
|
|
114
|
+
max_file_bytes: 65536
|
|
115
|
+
require_h1: true
|
|
116
|
+
allowed_extra_keys:
|
|
117
|
+
- x-steersync
|
|
118
|
+
file_reference_check: true
|
|
119
|
+
pymarkdown:
|
|
120
|
+
enabled: true
|
|
121
|
+
severity: warning
|
|
122
|
+
disabled_rules:
|
|
123
|
+
- MD013 # line length is not meaningful for prose steering
|
|
124
|
+
- MD041 # first line heading is handled by require_h1 with front matter awareness
|
|
125
|
+
forbidden_patterns:
|
|
126
|
+
- id: aws-access-key
|
|
127
|
+
pattern: "(?<![A-Z0-9])(AKIA|ASIA)[0-9A-Z]{16}(?![A-Z0-9])"
|
|
128
|
+
message: AWS access key identifier
|
|
129
|
+
- id: private-key
|
|
130
|
+
pattern: "-----BEGIN (RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY"
|
|
131
|
+
message: Private key material
|
|
132
|
+
- id: github-token
|
|
133
|
+
pattern: "gh[pousr]_[A-Za-z0-9]{36,}"
|
|
134
|
+
message: GitHub token
|
|
135
|
+
- id: gitlab-token
|
|
136
|
+
pattern: "glpat-[A-Za-z0-9_-]{20,}"
|
|
137
|
+
message: GitLab personal access token
|
|
138
|
+
- id: slack-token
|
|
139
|
+
pattern: "xox[abpr]-[0-9A-Za-z-]{10,}"
|
|
140
|
+
message: Slack token
|
|
141
|
+
- id: generic-secret
|
|
142
|
+
pattern: "(?i)(api[_-]?key|secret|password|token)\\s*[:=]\\s*['\"][A-Za-z0-9/+_=-]{16,}['\"]"
|
|
143
|
+
message: Possible hard coded credential
|
|
144
|
+
|
|
145
|
+
hook:
|
|
146
|
+
# When true the pre-push hook aborts the project push if steering sync fails.
|
|
147
|
+
strict: false
|
|
148
|
+
# Per git call timeout while running from a hook, so a slow server never
|
|
149
|
+
# stalls the developer's push for long.
|
|
150
|
+
timeout_seconds: 60
|
|
151
|
+
|
|
152
|
+
logging:
|
|
153
|
+
level: INFO
|
|
154
|
+
format: text
|