zikaron 0.1.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.
- zikaron/__init__.py +1 -0
- zikaron/cli/__init__.py +1 -0
- zikaron/cli/main.py +114 -0
- zikaron/core/__init__.py +1 -0
- zikaron/core/clock.py +78 -0
- zikaron/core/config/__init__.py +1 -0
- zikaron/core/config/keys.py +395 -0
- zikaron/core/config/resolution.py +267 -0
- zikaron/core/consolidation/__init__.py +1 -0
- zikaron/core/consolidation/authorization.py +316 -0
- zikaron/core/consolidation/candidates.py +147 -0
- zikaron/core/consolidation/context.py +166 -0
- zikaron/core/consolidation/grouping.py +383 -0
- zikaron/core/consolidation/groups.py +490 -0
- zikaron/core/consolidation/payload.py +246 -0
- zikaron/core/consolidation/planning.py +192 -0
- zikaron/core/consolidation/rowstate.py +68 -0
- zikaron/core/consolidation/runs.py +306 -0
- zikaron/core/consolidation/serving.py +462 -0
- zikaron/core/consolidation/verbs.py +500 -0
- zikaron/core/errors.py +355 -0
- zikaron/core/events.py +748 -0
- zikaron/core/indexing/__init__.py +1 -0
- zikaron/core/indexing/acquisition.py +255 -0
- zikaron/core/indexing/chunking.py +368 -0
- zikaron/core/indexing/encoder.py +537 -0
- zikaron/core/indexing/lexical.py +86 -0
- zikaron/core/indexing/model_cache.py +93 -0
- zikaron/core/indexing/model_pin.py +89 -0
- zikaron/core/indexing/vectors.py +223 -0
- zikaron/core/indexing/writes.py +461 -0
- zikaron/core/knowledge/__init__.py +5 -0
- zikaron/core/knowledge/arms.py +104 -0
- zikaron/core/knowledge/builds.py +204 -0
- zikaron/core/knowledge/candidates.py +130 -0
- zikaron/core/knowledge/changes.py +175 -0
- zikaron/core/knowledge/chunking.py +376 -0
- zikaron/core/knowledge/counters.py +228 -0
- zikaron/core/knowledge/database.py +380 -0
- zikaron/core/knowledge/ddl.py +196 -0
- zikaron/core/knowledge/disposal.py +213 -0
- zikaron/core/knowledge/errors.py +166 -0
- zikaron/core/knowledge/files.py +202 -0
- zikaron/core/knowledge/git.py +385 -0
- zikaron/core/knowledge/groups.py +450 -0
- zikaron/core/knowledge/lexical.py +64 -0
- zikaron/core/knowledge/lifecycle.py +418 -0
- zikaron/core/knowledge/lock.py +277 -0
- zikaron/core/knowledge/meta.py +393 -0
- zikaron/core/knowledge/paths.py +55 -0
- zikaron/core/knowledge/pending.py +59 -0
- zikaron/core/knowledge/registry.py +264 -0
- zikaron/core/knowledge/repair.py +152 -0
- zikaron/core/knowledge/reporting.py +436 -0
- zikaron/core/knowledge/roots.py +91 -0
- zikaron/core/knowledge/scan.py +429 -0
- zikaron/core/knowledge/search.py +346 -0
- zikaron/core/knowledge/state.py +174 -0
- zikaron/core/knowledge/text.py +166 -0
- zikaron/core/knowledge/vectors.py +102 -0
- zikaron/core/knowledge/walk.py +264 -0
- zikaron/core/knowledge/writes.py +127 -0
- zikaron/core/records/__init__.py +1 -0
- zikaron/core/records/memory.py +961 -0
- zikaron/core/records/receipts.py +161 -0
- zikaron/core/records/supersession.py +221 -0
- zikaron/core/retrieval/__init__.py +1 -0
- zikaron/core/retrieval/arms.py +318 -0
- zikaron/core/retrieval/block.py +107 -0
- zikaron/core/retrieval/eligibility.py +164 -0
- zikaron/core/retrieval/query.py +327 -0
- zikaron/core/retrieval/ranking.py +260 -0
- zikaron/core/retrieval/reads.py +294 -0
- zikaron/core/retrieval/retrieve.py +158 -0
- zikaron/core/retrieval/similarity.py +87 -0
- zikaron/core/signals/__init__.py +34 -0
- zikaron/core/signals/contention.py +106 -0
- zikaron/core/signals/dedup.py +201 -0
- zikaron/core/signals/horizon.py +47 -0
- zikaron/core/signals/repair.py +161 -0
- zikaron/core/signals/retirement.py +83 -0
- zikaron/core/signals/sessions.py +105 -0
- zikaron/core/signals/writes.py +200 -0
- zikaron/core/store/__init__.py +1 -0
- zikaron/core/store/connection.py +202 -0
- zikaron/core/store/ddl.py +215 -0
- zikaron/core/store/embedder.py +45 -0
- zikaron/core/store/meta.py +152 -0
- zikaron/core/store/permissions.py +160 -0
- zikaron/core/store/store.py +408 -0
- zikaron/core/store/transactions.py +181 -0
- zikaron/core/write/__init__.py +33 -0
- zikaron/core/write/dedup.py +145 -0
- zikaron/core/write/tools.py +290 -0
- zikaron/doctor/__init__.py +1 -0
- zikaron/doctor/checks.py +220 -0
- zikaron/doctor/main.py +64 -0
- zikaron/harness/__init__.py +1 -0
- zikaron/harness/detect.py +92 -0
- zikaron/harness/spec.py +320 -0
- zikaron/hook/__init__.py +1 -0
- zikaron/hook/connect.py +379 -0
- zikaron/hook/envelope.py +106 -0
- zikaron/hook/failure.py +104 -0
- zikaron/hook/limits.py +61 -0
- zikaron/hook/main.py +118 -0
- zikaron/hook/push.py +183 -0
- zikaron/hook/rpc.py +85 -0
- zikaron/hook/spawn_warm.py +81 -0
- zikaron/hook/subagent_policy.py +57 -0
- zikaron/hook/tripwire.py +54 -0
- zikaron/hook/warm_helper.py +137 -0
- zikaron/hook/write_policy.py +319 -0
- zikaron/install/__init__.py +4 -0
- zikaron/install/__main__.py +18 -0
- zikaron/install/assets.py +394 -0
- zikaron/install/entries.py +370 -0
- zikaron/install/harness.py +185 -0
- zikaron/install/main.py +375 -0
- zikaron/install/targets.py +789 -0
- zikaron/install/writer.py +973 -0
- zikaron/knowledge/__init__.py +1 -0
- zikaron/knowledge/__main__.py +17 -0
- zikaron/knowledge/indexer/__init__.py +1 -0
- zikaron/knowledge/indexer/__main__.py +17 -0
- zikaron/knowledge/indexer/detach.py +83 -0
- zikaron/knowledge/indexer/main.py +187 -0
- zikaron/knowledge/main.py +466 -0
- zikaron/knowledge/scope.py +133 -0
- zikaron/mcp/__init__.py +6 -0
- zikaron/mcp/connection.py +583 -0
- zikaron/mcp/consolidator.py +316 -0
- zikaron/mcp/errors.py +73 -0
- zikaron/mcp/main.py +66 -0
- zikaron/mcp/primary.py +420 -0
- zikaron/mcp/server.py +96 -0
- zikaron/mcp/spill.py +328 -0
- zikaron/mcp/tool_names.py +67 -0
- zikaron/py.typed +0 -0
- zikaron/service/__init__.py +1 -0
- zikaron/service/asyncio_compat.py +126 -0
- zikaron/service/context.py +251 -0
- zikaron/service/dispatch.py +332 -0
- zikaron/service/dispatch_consolidation.py +397 -0
- zikaron/service/dispatch_knowledge.py +469 -0
- zikaron/service/envelope.py +166 -0
- zikaron/service/lifecycle.py +467 -0
- zikaron/service/log.py +96 -0
- zikaron/service/main.py +531 -0
- zikaron/service/params.py +168 -0
- zikaron/service/paths.py +181 -0
- zikaron/service/rpc.py +176 -0
- zikaron/service/security.py +156 -0
- zikaron/service/serialize.py +204 -0
- zikaron/service/serialize_knowledge.py +238 -0
- zikaron/service/server.py +416 -0
- zikaron-0.1.0.dist-info/METADATA +770 -0
- zikaron-0.1.0.dist-info/RECORD +162 -0
- zikaron-0.1.0.dist-info/WHEEL +5 -0
- zikaron-0.1.0.dist-info/entry_points.txt +4 -0
- zikaron-0.1.0.dist-info/licenses/LICENSE +21 -0
- zikaron-0.1.0.dist-info/top_level.txt +1 -0
zikaron/install/main.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""`python -m zikaron.install` — write Zikaron's artefacts into a project, for either harness.
|
|
2
|
+
|
|
3
|
+
`architecture.md` §"The install contract" is normative for every refusal here; `harness.md`
|
|
4
|
+
§"The installer's two targets" for what varies between the two. The order of operations is the
|
|
5
|
+
contract's own: **validate before writing anything**, because a half-installed project whose
|
|
6
|
+
consolidator names a model the harness will silently substitute is the one outcome that makes two
|
|
7
|
+
consolidation runs incomparable while both look healthy.
|
|
8
|
+
|
|
9
|
+
**This module is written once.** Which files exist and how each is merged lives behind
|
|
10
|
+
`HarnessTarget`; flow, preflight order, collision policy and backup discipline are here and are
|
|
11
|
+
shared. If a harness name appears below outside `_resolve_harness`, the seam has sprung a leak.
|
|
12
|
+
|
|
13
|
+
Reached as `zikaron install` and as `python -m zikaron.install`. Both are supported because each is
|
|
14
|
+
unreachable in the other's case: `uv tool install` puts no interpreter on `PATH`, and a host-Python
|
|
15
|
+
install with several virtualenvs needs the form that names which interpreter's Zikaron is being
|
|
16
|
+
installed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Final
|
|
25
|
+
|
|
26
|
+
from zikaron.harness.spec import CLAUDE_CODE, Harness
|
|
27
|
+
from zikaron.install.entries import Commands, HookFormat
|
|
28
|
+
from zikaron.install.harness import InstallError
|
|
29
|
+
from zikaron.install.targets import HarnessTarget, target_for
|
|
30
|
+
from zikaron.install.writer import (
|
|
31
|
+
Plan,
|
|
32
|
+
Report,
|
|
33
|
+
ShippedFile,
|
|
34
|
+
commit_merge,
|
|
35
|
+
guard_shipped_targets,
|
|
36
|
+
write_shipped_files,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
#: `--harness auto`, the default. Resolved by evidence, and refused when there is none — see
|
|
40
|
+
#: `_resolve_harness`.
|
|
41
|
+
_AUTO = "auto"
|
|
42
|
+
|
|
43
|
+
#: The marker variable Claude Code exports into every process it spawns, **read from the seam**
|
|
44
|
+
#: rather than spelled again here: a second copy of the literal is the drift intent 1 exists to
|
|
45
|
+
#: prevent.
|
|
46
|
+
#:
|
|
47
|
+
#: What is deliberately *not* reused is `harness.detect.current_harness`, and the difference is the
|
|
48
|
+
#: whole point: that function answers "which harness am I running under", falling back to kiro when
|
|
49
|
+
#: the marker is absent because kiro exports none. The installer is asking a **different question**
|
|
50
|
+
#: — "which harness is this project set up for" — where that same fallback would silently write
|
|
51
|
+
#: kiro artefacts into a Claude Code project from a plain terminal and exit 0. So the *value* comes
|
|
52
|
+
#: from the seam and the *policy* does not.
|
|
53
|
+
_CLAUDE_MARKER: Final = CLAUDE_CODE.marker_variable
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_MODULE_INVOCATION: Final = "python -m zikaron.install"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parser(prog: str) -> argparse.ArgumentParser:
|
|
60
|
+
parser = argparse.ArgumentParser(
|
|
61
|
+
prog=prog,
|
|
62
|
+
description="Install Zikaron's consolidator, consolidation skill, hook and MCP entries.",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--project",
|
|
66
|
+
type=Path,
|
|
67
|
+
default=Path.cwd(),
|
|
68
|
+
help="the project to install into (default: the current directory). At runtime the "
|
|
69
|
+
"store is keyed by the harness's own project directory where it names one, else the "
|
|
70
|
+
"working directory (D17)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--harness",
|
|
74
|
+
choices=(_AUTO, *(harness.value for harness in Harness)),
|
|
75
|
+
default=_AUTO,
|
|
76
|
+
help="which harness to install for. The default resolves it from the project and the "
|
|
77
|
+
"environment, and refuses rather than guessing when neither says.",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--agent",
|
|
81
|
+
type=Path,
|
|
82
|
+
default=None,
|
|
83
|
+
help="kiro only: an existing agent config to merge the hook and MCP entries into. Backed "
|
|
84
|
+
"up to <config>.bak first.",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--print-only",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="print the entries instead of merging them, and write no shipped files either. Use "
|
|
90
|
+
"to see the config an install would add, in full, and which files it would create, "
|
|
91
|
+
"before letting it. The shipped files are listed by path, not by content.",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--model",
|
|
95
|
+
default=None,
|
|
96
|
+
help="the consolidator's model (default: this harness's own, which kiro validates against "
|
|
97
|
+
"its model list and Claude Code refuses at spawn if unknown)",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--format",
|
|
101
|
+
dest="hook_format",
|
|
102
|
+
type=HookFormat,
|
|
103
|
+
choices=tuple(HookFormat),
|
|
104
|
+
default=HookFormat.OBJECT,
|
|
105
|
+
help="kiro only: which hook format to write when the target config has none yet. A config "
|
|
106
|
+
"that already has hooks keeps its own format regardless.",
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--no-trust-tools",
|
|
110
|
+
dest="trust_tools",
|
|
111
|
+
action="store_false",
|
|
112
|
+
help="do not pre-approve Zikaron's own tools, so every tool call asks permission. The "
|
|
113
|
+
"default trusts them: per-write prompts push against the write policy the whole store "
|
|
114
|
+
"depends on.",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--force",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help="overwrite files this would otherwise refuse to touch",
|
|
120
|
+
)
|
|
121
|
+
return parser
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: Sequence[str] | None = None, *, prog: str = _MODULE_INVOCATION) -> int:
|
|
125
|
+
"""Install, printing an account of what happened. Returns a process exit status.
|
|
126
|
+
|
|
127
|
+
Returns rather than calling `sys.exit`, so a test drives the whole command in-process and reads
|
|
128
|
+
both the status and the output — the `__main__` block below is the only place a status becomes
|
|
129
|
+
an exit.
|
|
130
|
+
"""
|
|
131
|
+
args = _parser(prog).parse_args(sys.argv[1:] if argv is None else argv)
|
|
132
|
+
try:
|
|
133
|
+
report = _install(agent=args.agent, options=args)
|
|
134
|
+
except InstallError as exc:
|
|
135
|
+
print(f"install refused: {exc}", file=sys.stderr)
|
|
136
|
+
return 1
|
|
137
|
+
except OSError as exc:
|
|
138
|
+
# Every *predictable* path-shape problem is refused in the preflight above. This is the
|
|
139
|
+
# remainder — a full disk, a revoked permission, a filesystem that went read-only mid-run —
|
|
140
|
+
# reported as a failed install rather than as a traceback, which tells a user nothing they
|
|
141
|
+
# can act on.
|
|
142
|
+
print(f"install failed: {exc}", file=sys.stderr)
|
|
143
|
+
return 1
|
|
144
|
+
_print_report(report)
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _install(*, agent: Path | None, options: argparse.Namespace) -> Report:
|
|
149
|
+
"""The whole install, in the contract's order: check everything, then write everything.
|
|
150
|
+
|
|
151
|
+
The ordering is load-bearing rather than tidy. `plan_merges` parses the files it would touch and
|
|
152
|
+
runs every refusal against them **before** the shipped files are written, so a malformed config
|
|
153
|
+
or a conflicting entry leaves the project exactly as it was. An earlier arrangement wrote the
|
|
154
|
+
shipped files first and found the problem afterwards, which is the half-install this order
|
|
155
|
+
prevents.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
InstallError: any refusal. Nothing has been written when one is raised.
|
|
159
|
+
"""
|
|
160
|
+
project: Path = options.project
|
|
161
|
+
harness_choice, harness_notes = _resolve_harness(options.harness, project)
|
|
162
|
+
target = target_for(harness_choice)
|
|
163
|
+
commands = Commands.from_this_interpreter()
|
|
164
|
+
_refuse_missing_commands(commands)
|
|
165
|
+
_refuse_unusable_agent_flag(agent, target)
|
|
166
|
+
if not project.is_dir():
|
|
167
|
+
raise InstallError(f"{project} is not a directory")
|
|
168
|
+
if agent is not None and not agent.is_file():
|
|
169
|
+
raise InstallError(f"{agent} is not a file — --agent takes a path to an existing config")
|
|
170
|
+
if agent is not None and agent.is_symlink():
|
|
171
|
+
# Refused rather than followed, because publishing an atomic rewrite means replacing the
|
|
172
|
+
# directory entry — which would silently sever a config managed as a link into a dotfile
|
|
173
|
+
# repository, leaving a new local file and the real one untouched and disconnected. This is
|
|
174
|
+
# the opposite case from a symlinked *directory*, which a write through preserves.
|
|
175
|
+
raise InstallError(
|
|
176
|
+
f"{agent} is a symlink. Point --agent at the file it resolves to "
|
|
177
|
+
f"({agent.resolve()}), so this does not replace the link itself."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
plan = Plan(
|
|
181
|
+
project=project,
|
|
182
|
+
commands=commands,
|
|
183
|
+
# `is None`, not `or`: an explicit `--model ""` is a mistake worth reporting, and truthiness
|
|
184
|
+
# would silently substitute this harness's default for it. Found by the test written for the
|
|
185
|
+
# shape check rather than by the shape check itself, which never saw the value.
|
|
186
|
+
model=(target.spec.consolidator_model if options.model is None else options.model),
|
|
187
|
+
hook_format=options.hook_format,
|
|
188
|
+
force=options.force,
|
|
189
|
+
trust_tools=options.trust_tools,
|
|
190
|
+
)
|
|
191
|
+
report = Report()
|
|
192
|
+
report.notes.extend(harness_notes)
|
|
193
|
+
if options.print_only:
|
|
194
|
+
# **A preview refuses nothing it does not have to.** The model check asks a *remote* harness
|
|
195
|
+
# whether an id exists, and the fragment below neither contains the model nor mentions it —
|
|
196
|
+
# so on a machine whose harness cannot be reached, a hard refusal here would block a preview
|
|
197
|
+
# in order to validate something the preview does not show. Downgraded to a note; a real
|
|
198
|
+
# install still refuses.
|
|
199
|
+
try:
|
|
200
|
+
target.refuse_absent_harness()
|
|
201
|
+
target.refuse_unknown_model(plan.model)
|
|
202
|
+
except InstallError as exc:
|
|
203
|
+
# "refused", not "could not be validated": the two reachable causes read very
|
|
204
|
+
# differently to whoever ran this. Kiro's is a genuinely unreachable harness; the shape
|
|
205
|
+
# check is local and deterministic, so the model *was* checked and failed. The
|
|
206
|
+
# exception's own text distinguishes them, so it is quoted rather than paraphrased.
|
|
207
|
+
report.notes.append(
|
|
208
|
+
f"a real install would refuse here ({exc}) — this preview does not, because its "
|
|
209
|
+
"whole job is to show you what would be written."
|
|
210
|
+
)
|
|
211
|
+
shipped = target.shipped_files(plan)
|
|
212
|
+
report.notes.append(target.fragment(plan))
|
|
213
|
+
report.notes.append(
|
|
214
|
+
"An install would also write, in full:\n"
|
|
215
|
+
+ "\n".join(f"- {path}" for path, _content in shipped)
|
|
216
|
+
)
|
|
217
|
+
report.notes.extend(target.notes(plan))
|
|
218
|
+
return report
|
|
219
|
+
|
|
220
|
+
target.refuse_absent_harness()
|
|
221
|
+
target.refuse_unknown_model(plan.model)
|
|
222
|
+
shipped = target.shipped_files(plan)
|
|
223
|
+
_refuse_agent_aliasing_a_shipped_target(agent, shipped)
|
|
224
|
+
|
|
225
|
+
guard_shipped_targets(shipped, project)
|
|
226
|
+
merges = target.plan_merges(plan, agent)
|
|
227
|
+
|
|
228
|
+
write_shipped_files(shipped, plan, report)
|
|
229
|
+
for merge in merges:
|
|
230
|
+
commit_merge(merge, report)
|
|
231
|
+
if not merges:
|
|
232
|
+
# Kiro with no `--agent`: the shipped files land and the entries are printed for the user to
|
|
233
|
+
# paste, which is what a user re-running to refresh the consolidator expects. Expressed as
|
|
234
|
+
# "nothing was merged" rather than as "kiro without --agent" so it
|
|
235
|
+
# stays a fact about this install rather than a harness name leaking into shared flow.
|
|
236
|
+
report.notes.append(target.fragment(plan))
|
|
237
|
+
report.notes.extend(target.notes(plan))
|
|
238
|
+
report.notes.extend(
|
|
239
|
+
target.complaints_about([*report.created, *report.replaced, *report.merged])
|
|
240
|
+
)
|
|
241
|
+
return report
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _resolve_harness(requested: str, project: Path) -> tuple[Harness, list[str]]:
|
|
245
|
+
"""Which harness to install for, refusing rather than guessing, plus anything worth saying.
|
|
246
|
+
|
|
247
|
+
Explicit wins. `auto` then reads **the project first**, because that is what the install is
|
|
248
|
+
*for*: a `.claude/` or a `.kiro/` directory says which harness this project is already set up
|
|
249
|
+
with, and it says so more specifically than whatever terminal the installer happens to be run
|
|
250
|
+
from. Only when the project says nothing does the environment marker get a vote.
|
|
251
|
+
|
|
252
|
+
**Both present, or neither and no marker, is a refusal.** The alternative is the failure this
|
|
253
|
+
corpus has already paid for once, in an agent whose `mcpServers` was configured and whose
|
|
254
|
+
`tools` did not select it: a working-looking install, exit 0, and no Zikaron tools anywhere. A
|
|
255
|
+
wrong-harness install has exactly that shape, and it is worse than an install that did not
|
|
256
|
+
happen.
|
|
257
|
+
|
|
258
|
+
**When the two sources *contradict*, the install says so.** Project evidence still wins, but a
|
|
259
|
+
lone `.kiro/` in a project opened from a Claude Code session is a real configuration — this
|
|
260
|
+
corpus's own migration advice is to *keep* `.kiro/` — and installing for kiro there produces the
|
|
261
|
+
working-looking-inert result for the session the user is actually in. Reported rather than
|
|
262
|
+
enforced: the evidence genuinely is ambiguous, and a note costs nothing while a refusal would
|
|
263
|
+
block the common case of installing for kiro from a Claude Code terminal.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
InstallError: `auto` found no evidence, or found both.
|
|
267
|
+
"""
|
|
268
|
+
if requested != _AUTO:
|
|
269
|
+
return Harness(requested), []
|
|
270
|
+
has_claude = (project / ".claude").is_dir()
|
|
271
|
+
has_kiro = (project / ".kiro").is_dir()
|
|
272
|
+
marker_set = _CLAUDE_MARKER is not None and bool(os.environ.get(_CLAUDE_MARKER))
|
|
273
|
+
if has_claude and not has_kiro:
|
|
274
|
+
return Harness.CLAUDE_CODE, []
|
|
275
|
+
if has_kiro and not has_claude:
|
|
276
|
+
contradiction = (
|
|
277
|
+
[
|
|
278
|
+
f"{_CLAUDE_MARKER} is set, but {project} has only .kiro/ — installed for "
|
|
279
|
+
f"{Harness.KIRO.value}. Pass --harness {Harness.CLAUDE_CODE.value} if this project "
|
|
280
|
+
"is meant to use the session you are in."
|
|
281
|
+
]
|
|
282
|
+
if marker_set
|
|
283
|
+
else []
|
|
284
|
+
)
|
|
285
|
+
return Harness.KIRO, contradiction
|
|
286
|
+
if has_claude and has_kiro:
|
|
287
|
+
evidence = "it has both .kiro/ and .claude/"
|
|
288
|
+
elif marker_set:
|
|
289
|
+
return Harness.CLAUDE_CODE, []
|
|
290
|
+
else:
|
|
291
|
+
evidence = f"it has neither .kiro/ nor .claude/, and {_CLAUDE_MARKER} is not set"
|
|
292
|
+
raise InstallError(
|
|
293
|
+
f"could not tell which harness {project} is for — {evidence}. Pass "
|
|
294
|
+
f"--harness {Harness.KIRO.value} or --harness {Harness.CLAUDE_CODE.value}. Guessing here "
|
|
295
|
+
"would install artefacts the harness never reads, which exits 0 and leaves no Zikaron "
|
|
296
|
+
"tools at all."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _refuse_unusable_agent_flag(agent: Path | None, target: HarnessTarget) -> None:
|
|
301
|
+
"""Refuse `--agent` on a harness that has no user-named config to merge into.
|
|
302
|
+
|
|
303
|
+
Refused rather than ignored: the flag is the one instruction the user gave about where their
|
|
304
|
+
configuration lives, and silently discarding it would install into two fixed project files
|
|
305
|
+
while they believed they had chosen otherwise.
|
|
306
|
+
"""
|
|
307
|
+
if agent is None or target.accepts_agent_flag:
|
|
308
|
+
return
|
|
309
|
+
raise InstallError(
|
|
310
|
+
f"--agent has no meaning under --harness {target.name}: its hook and MCP entries go into "
|
|
311
|
+
"fixed project files (.claude/settings.local.json and .mcp.json), not into an agent config "
|
|
312
|
+
"you name. Drop the flag, or pass --print-only to see those entries in full."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _refuse_agent_aliasing_a_shipped_target(
|
|
317
|
+
agent: Path | None, shipped: tuple[ShippedFile, ...]
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Refuse an `--agent` that names one of the files this install writes itself.
|
|
320
|
+
|
|
321
|
+
`.kiro/agents/zikaron-consolidator.json` is a plausible thing to pick out of an agents
|
|
322
|
+
directory, and picking it used to produce a working-looking install that had broken
|
|
323
|
+
consolidation: the merge turned the consolidator's own config into a *primary* agent —
|
|
324
|
+
`--mode primary`, primary hooks — while keeping its identity and prompt, so the very first
|
|
325
|
+
`zikaron_memory_next_group` the prompt instructs has no tool behind it. Exit 0, and
|
|
326
|
+
consolidation
|
|
327
|
+
silently gone.
|
|
328
|
+
|
|
329
|
+
Compared by resolved path rather than by string, because the two can be the same file under
|
|
330
|
+
different spellings, and `--project .` makes that likely rather than exotic.
|
|
331
|
+
"""
|
|
332
|
+
if agent is None:
|
|
333
|
+
return
|
|
334
|
+
resolved = agent.resolve()
|
|
335
|
+
for path, _content in shipped:
|
|
336
|
+
if resolved == path.resolve():
|
|
337
|
+
raise InstallError(
|
|
338
|
+
f"--agent names {path.name}, which this install writes itself. Point --agent at "
|
|
339
|
+
"the config for the agent you work in, not at one of Zikaron's own."
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _refuse_missing_commands(commands: Commands) -> None:
|
|
344
|
+
missing = commands.missing()
|
|
345
|
+
if not missing:
|
|
346
|
+
return
|
|
347
|
+
listed = ", ".join(str(path) for path in missing)
|
|
348
|
+
raise InstallError(
|
|
349
|
+
f"the shipped commands are not installed for this interpreter ({listed}). Run "
|
|
350
|
+
"`pip install -e .` (or `pip install zikaron`) into the environment you are installing "
|
|
351
|
+
"from — a hook whose command does not exist fails silently, because the harness does not "
|
|
352
|
+
"surface a hook's stderr."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _print_report(report: Report) -> None:
|
|
357
|
+
for path in report.created:
|
|
358
|
+
print(f"wrote {path}")
|
|
359
|
+
for path in report.replaced:
|
|
360
|
+
print(f"replaced {path}")
|
|
361
|
+
for path in report.merged:
|
|
362
|
+
print(f"merged {path}")
|
|
363
|
+
for path in report.backed_up:
|
|
364
|
+
print(f"backed up {path}")
|
|
365
|
+
for path in report.skipped:
|
|
366
|
+
# The three reasons want different advice, and only one of them is still readable from the
|
|
367
|
+
# file afterwards.
|
|
368
|
+
if path in report.kept_unbacked:
|
|
369
|
+
print(f"kept {path} (differs, but could not be backed up — see below)")
|
|
370
|
+
elif path.is_symlink():
|
|
371
|
+
print(f"kept {path} (a symlink, left unfollowed — pass --force to replace it)")
|
|
372
|
+
else:
|
|
373
|
+
print(f"kept {path} (already exactly what this version ships)")
|
|
374
|
+
for note in report.notes:
|
|
375
|
+
print(f"\n{note}")
|