arkaos 4.46.0 → 4.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/core/harness/cli.py +143 -0
- package/core/harness/drift.py +160 -9
- package/core/harness/manager.py +577 -0
- package/core/harness/manifest.py +5 -1
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/installer/cli.js +27 -0
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/THE-ARKAOS-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The ArkaOS Guide
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.47.0 — 89 agents, 17 departments, 332 skills, 297 commands, 19 ADRs.
|
|
4
4
|
> One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
|
|
5
5
|
|
|
6
6
|
## What it is
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.47.0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""CLI for the harness manager — ``npx arkaos harness <verb>``.
|
|
2
|
+
|
|
3
|
+
Verbs: ``status`` (read-only), ``assert`` (apply policies), ``restore``
|
|
4
|
+
(assert + re-seed adopted surfaces), ``harden`` (assert + scanner
|
|
5
|
+
grade, nonzero exit below B), ``flags`` (read, or set with an explicit
|
|
6
|
+
``--set name=value``). Every verb but ``flags`` takes ``--json``.
|
|
7
|
+
Exit codes are the contract the Node wrapper propagates verbatim.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from core.harness.manager import ClaudeConfigManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
args = _parser().parse_args(argv)
|
|
21
|
+
handler = {
|
|
22
|
+
"status": _status,
|
|
23
|
+
"assert": _assert,
|
|
24
|
+
"restore": _restore,
|
|
25
|
+
"harden": _harden,
|
|
26
|
+
"flags": _flags,
|
|
27
|
+
}[args.verb]
|
|
28
|
+
try:
|
|
29
|
+
return handler(ClaudeConfigManager(), args)
|
|
30
|
+
except Exception as exc: # operators get a message, never a traceback
|
|
31
|
+
print(f"harness {args.verb} failed: {exc}", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parser() -> argparse.ArgumentParser:
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="python -m core.harness.cli",
|
|
38
|
+
description="Assert and report ArkaOS ownership of the harness.",
|
|
39
|
+
)
|
|
40
|
+
sub = parser.add_subparsers(dest="verb", required=True)
|
|
41
|
+
for verb in ("status", "assert", "restore", "harden"):
|
|
42
|
+
sub.add_parser(verb).add_argument(
|
|
43
|
+
"--json", action="store_true", dest="as_json"
|
|
44
|
+
)
|
|
45
|
+
flags = sub.add_parser("flags")
|
|
46
|
+
flags.add_argument(
|
|
47
|
+
"--set", dest="assignment", default=None,
|
|
48
|
+
help=(
|
|
49
|
+
"hardEnforcement|specialistEnforcement: true|false; "
|
|
50
|
+
"frontendGate: off|warn|hard; read-only without it"
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _status(manager: ClaudeConfigManager, args) -> int:
|
|
57
|
+
report = manager.status()
|
|
58
|
+
if getattr(args, "as_json", False):
|
|
59
|
+
print(json.dumps(report, indent=2))
|
|
60
|
+
return 0
|
|
61
|
+
_print_drift(report["drift"])
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _print_drift(drift_report: dict) -> None:
|
|
66
|
+
print(f"settings: {drift_report['settings_path']}")
|
|
67
|
+
print(f"ok: {drift_report['ok']}")
|
|
68
|
+
for finding in drift_report["findings"]:
|
|
69
|
+
print(
|
|
70
|
+
f" [{finding['status']}] {finding['where']} — "
|
|
71
|
+
f"{finding['detail']}"
|
|
72
|
+
)
|
|
73
|
+
if not drift_report["findings"]:
|
|
74
|
+
print(" no drift")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _assert(manager: ClaudeConfigManager, args) -> int:
|
|
78
|
+
return _print_report(manager.assert_ownership(), args)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _restore(manager: ClaudeConfigManager, args) -> int:
|
|
82
|
+
return _print_report(manager.restore(), args)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _harden(manager: ClaudeConfigManager, args) -> int:
|
|
86
|
+
report, grade = manager.harden()
|
|
87
|
+
code = _print_report(report, args, extra={"scan_grade": grade})
|
|
88
|
+
if not getattr(args, "as_json", False):
|
|
89
|
+
print(f"post-assert scan grade: {grade}")
|
|
90
|
+
if code:
|
|
91
|
+
return code
|
|
92
|
+
return 0 if grade in ("A", "B") else 2
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _flags(manager: ClaudeConfigManager, args) -> int:
|
|
96
|
+
if args.assignment:
|
|
97
|
+
name, separator, raw = args.assignment.partition("=")
|
|
98
|
+
if not separator or not raw.strip():
|
|
99
|
+
print("flags --set expects name=value", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
try:
|
|
102
|
+
flags = manager.set_flag(name.strip(), _parse_value(raw.strip()))
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
print(str(exc), file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
else:
|
|
107
|
+
flags = manager.read_flags()
|
|
108
|
+
print(json.dumps(flags, indent=2))
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _parse_value(raw: str) -> object:
|
|
113
|
+
if raw.lower() in ("true", "false"):
|
|
114
|
+
return raw.lower() == "true"
|
|
115
|
+
return raw.lower()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _print_report(report, args=None, extra: dict | None = None) -> int:
|
|
119
|
+
if getattr(args, "as_json", False):
|
|
120
|
+
print(json.dumps({**report.to_dict(), **(extra or {})}, indent=2))
|
|
121
|
+
return 1 if report.refused or _has_refusal(report) else 0
|
|
122
|
+
if report.refused:
|
|
123
|
+
print(f"{report.verb}: refused — {report.refused}", file=sys.stderr)
|
|
124
|
+
print(
|
|
125
|
+
" nothing was written; fix or remove the file and re-run",
|
|
126
|
+
file=sys.stderr,
|
|
127
|
+
)
|
|
128
|
+
return 1
|
|
129
|
+
print(f"{report.verb}: changed={report.changed}")
|
|
130
|
+
for action in report.actions:
|
|
131
|
+
if action.action == "noop":
|
|
132
|
+
continue
|
|
133
|
+
detail = f" ({action.detail})" if action.detail else ""
|
|
134
|
+
print(f" {action.action}: {action.surface}{detail}")
|
|
135
|
+
return 1 if _has_refusal(report) else 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _has_refusal(report) -> bool:
|
|
139
|
+
return any(a.action == "refused" for a in report.actions)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
raise SystemExit(main())
|
package/core/harness/drift.py
CHANGED
|
@@ -26,6 +26,9 @@ ignore the report (the harness_scanner noise lesson).
|
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import shlex
|
|
29
32
|
import sys
|
|
30
33
|
from dataclasses import dataclass, field
|
|
31
34
|
from enum import StrEnum
|
|
@@ -187,16 +190,147 @@ def _check_hooks(
|
|
|
187
190
|
hooks = settings.get("hooks")
|
|
188
191
|
hooks = hooks if isinstance(hooks, dict) else {}
|
|
189
192
|
is_windows = (platform or sys.platform) == "win32"
|
|
193
|
+
accepted = _accepted_hook_dirs(report.settings_path, hooks_root)
|
|
190
194
|
for reg in spec.hook_registrations:
|
|
191
195
|
if reg.posix_only and is_windows:
|
|
192
196
|
continue
|
|
193
197
|
if reg.conditional and not _script_deployed(reg, hooks_root):
|
|
194
198
|
continue
|
|
195
|
-
_check_registration(report, reg, hooks.get(reg.event))
|
|
199
|
+
_check_registration(report, reg, hooks.get(reg.event), accepted)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _accepted_hook_dirs(
|
|
203
|
+
settings_path: Path, hooks_root: str | None
|
|
204
|
+
) -> frozenset[str] | None:
|
|
205
|
+
"""Directories an ArkaOS hook command may legitimately live in.
|
|
206
|
+
|
|
207
|
+
Three: what the installer writes (``~/.arkaos/config/hooks`` —
|
|
208
|
+
adapters/claude-code.js joins installDir with config/hooks;
|
|
209
|
+
omitting it read every healthy install as stale and repointed it
|
|
210
|
+
at the purgeable npx cache, QG C2 r1 Francisca B1), the
|
|
211
|
+
``~/.arkaos/lib`` snapshot, and the current resolved root.
|
|
212
|
+
Anything else with an ArkaOS basename is a STALE root — the
|
|
213
|
+
split-root failure mode basename matching hid (#439 M6).
|
|
214
|
+
|
|
215
|
+
None when the root cannot be resolved: without a reference point
|
|
216
|
+
staleness cannot be judged, and flagging everything is noise.
|
|
217
|
+
"""
|
|
218
|
+
home = settings_path.parent.parent # <home>/.claude/settings.json
|
|
219
|
+
try:
|
|
220
|
+
arkaos = paths.arkaos_home(home)
|
|
221
|
+
return frozenset(
|
|
222
|
+
_normalised(candidate)
|
|
223
|
+
for candidate in (
|
|
224
|
+
arkaos / "config" / "hooks",
|
|
225
|
+
arkaos / "lib" / "config" / "hooks",
|
|
226
|
+
paths.hooks_dir(hooks_root),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
except OSError:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _normalised(path: Path) -> str:
|
|
234
|
+
"""Comparable form of a directory path.
|
|
235
|
+
|
|
236
|
+
Case-folded and lexically normalised: a case variant or a ``..``
|
|
237
|
+
segment names the same directory on the operator's filesystem and
|
|
238
|
+
must not read as a different root (QG C2 r1 M2). ``resolve()`` is
|
|
239
|
+
deliberately not used — it hits the filesystem and would make a
|
|
240
|
+
read-only scan depend on what happens to exist.
|
|
241
|
+
"""
|
|
242
|
+
return os.path.normcase(os.path.normpath(str(path)))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def hook_command_path(command: object) -> Path:
|
|
246
|
+
"""The script path inside a hook ``command`` string.
|
|
247
|
+
|
|
248
|
+
Operators write commands with surrounding quotes and with
|
|
249
|
+
interpreter prefixes (``bash /path/hook.sh``); matching on the raw
|
|
250
|
+
string appended a DUPLICATE registration and stripped the prefix
|
|
251
|
+
(QG C2 r1, Francisca B7). One normaliser, used by every consumer.
|
|
252
|
+
"""
|
|
253
|
+
text = str(command or "").strip()
|
|
254
|
+
if not text:
|
|
255
|
+
return Path("")
|
|
256
|
+
parts = shlex.split(text) if _splittable(text) else [text]
|
|
257
|
+
parts = _before_shell_operator(parts)
|
|
258
|
+
for token in reversed(parts):
|
|
259
|
+
if token.endswith(_HOOK_SUFFIXES):
|
|
260
|
+
return Path(token)
|
|
261
|
+
for token in reversed(parts):
|
|
262
|
+
if "/" in token or "\\" in token:
|
|
263
|
+
return Path(token)
|
|
264
|
+
return Path(parts[-1]) if parts else Path("")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
_SHELL_OPERATOR_RE = re.compile(r"[|;&>]")
|
|
268
|
+
_HOOK_SUFFIXES = (".sh", ".ps1", ".cjs")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _before_shell_operator(parts: list[str]) -> list[str]:
|
|
272
|
+
"""Tokens up to the first shell operator.
|
|
273
|
+
|
|
274
|
+
Load-bearing whenever the SECOND command or the redirect target
|
|
275
|
+
would win the scan: ``stop.sh 2>/tmp/hook-debug.sh`` ends in a
|
|
276
|
+
hook suffix, ``stop.sh && /usr/local/bin/notify.sh`` chains a
|
|
277
|
+
second script, and ``bash <dir>/stop 2>/dev/null`` has no suffix
|
|
278
|
+
at all — in each case the wrong token is read as the script, the
|
|
279
|
+
ArkaOS entry goes unrecognised, and assert appends a SECOND
|
|
280
|
+
registration that fires the hook twice (QG C2 r2 Francisca B2;
|
|
281
|
+
the ``2>/dev/null`` example first documented here was already
|
|
282
|
+
handled by the suffix scan, QG C2 r3 Eduardo).
|
|
283
|
+
|
|
284
|
+
Operators attach to the previous token as often as they stand
|
|
285
|
+
alone (``stop.sh;`` vs ``stop.sh ;``), so both forms cut.
|
|
286
|
+
"""
|
|
287
|
+
kept: list[str] = []
|
|
288
|
+
for token in parts:
|
|
289
|
+
head = _operator_head(token)
|
|
290
|
+
if head is None:
|
|
291
|
+
kept.append(token)
|
|
292
|
+
continue
|
|
293
|
+
if head:
|
|
294
|
+
kept.append(head)
|
|
295
|
+
break
|
|
296
|
+
return kept or parts
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _operator_head(token: str) -> str | None:
|
|
300
|
+
"""Text before a genuine shell operator in ``token``, else None.
|
|
301
|
+
|
|
302
|
+
An operator CHARACTER is not an operator POSITION: a directory
|
|
303
|
+
named ``R&D`` or ``a;b`` is an ordinary path, and cutting there
|
|
304
|
+
made the manager stop recognising the entry it had just written —
|
|
305
|
+
assert went non-idempotent, appending a Stop group per run
|
|
306
|
+
(QG C2 r4, Francisca B2; the regression came from the r3 fix for
|
|
307
|
+
the duplication bug, not from the original code). A cut is
|
|
308
|
+
genuine only where a shell would see one: at the start of the
|
|
309
|
+
token, right after a hook script, or after a file-descriptor
|
|
310
|
+
number.
|
|
311
|
+
"""
|
|
312
|
+
match = _SHELL_OPERATOR_RE.search(token)
|
|
313
|
+
if match is None:
|
|
314
|
+
return None
|
|
315
|
+
head = token[: match.start()]
|
|
316
|
+
if not head or head.isdigit() or head.endswith(_HOOK_SUFFIXES):
|
|
317
|
+
return head
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _splittable(text: str) -> bool:
|
|
322
|
+
try:
|
|
323
|
+
shlex.split(text)
|
|
324
|
+
return True
|
|
325
|
+
except ValueError:
|
|
326
|
+
return False
|
|
196
327
|
|
|
197
328
|
|
|
198
329
|
def _check_registration(
|
|
199
|
-
report: DriftReport,
|
|
330
|
+
report: DriftReport,
|
|
331
|
+
reg: HookRegistration,
|
|
332
|
+
entries: Any,
|
|
333
|
+
accepted: frozenset[str] | None,
|
|
200
334
|
) -> None:
|
|
201
335
|
where = f"hooks.{reg.event}" + (
|
|
202
336
|
f"[matcher={reg.matcher}]" if reg.matcher else ""
|
|
@@ -210,14 +344,31 @@ def _check_registration(
|
|
|
210
344
|
)
|
|
211
345
|
)
|
|
212
346
|
return
|
|
347
|
+
for detail in entry_divergences(entry, reg, accepted):
|
|
348
|
+
report.findings.append(
|
|
349
|
+
DriftFinding("settings:hooks", DriftStatus.DIVERGED, where, detail)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def entry_divergences(
|
|
354
|
+
entry: dict, reg: HookRegistration, accepted: frozenset[str] | None
|
|
355
|
+
) -> list[str]:
|
|
356
|
+
"""Why an existing ArkaOS entry diverges from spec — [] when clean.
|
|
357
|
+
|
|
358
|
+
Shared with the C2 manager so drift and repair can never disagree
|
|
359
|
+
about what counts as divergent.
|
|
360
|
+
"""
|
|
361
|
+
divergences = []
|
|
213
362
|
timeout = entry.get("timeout")
|
|
214
363
|
if timeout != reg.timeout:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
364
|
+
divergences.append(f"timeout is {timeout!r}, spec says {reg.timeout}")
|
|
365
|
+
command_dir = hook_command_path(entry.get("command")).parent
|
|
366
|
+
if accepted is not None and _normalised(command_dir) not in accepted:
|
|
367
|
+
divergences.append(
|
|
368
|
+
f"stale-root: {reg.script} points at {command_dir}, not "
|
|
369
|
+
f"the current ArkaOS hooks dir"
|
|
220
370
|
)
|
|
371
|
+
return divergences
|
|
221
372
|
|
|
222
373
|
|
|
223
374
|
def _find_entry(reg: HookRegistration, entries: Any) -> dict | None:
|
|
@@ -233,8 +384,8 @@ def _find_entry(reg: HookRegistration, entries: Any) -> dict | None:
|
|
|
233
384
|
for inner in group.get("hooks") or []:
|
|
234
385
|
if not isinstance(inner, dict):
|
|
235
386
|
continue
|
|
236
|
-
|
|
237
|
-
if
|
|
387
|
+
name = hook_command_path(inner.get("command")).name
|
|
388
|
+
if name and name in wanted:
|
|
238
389
|
return inner
|
|
239
390
|
return None
|
|
240
391
|
|