arkaos 4.33.0 → 4.34.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.
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills, 297 commands, 16 ADRs.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills, 297 commands, 16 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.33.0
1
+ 4.34.0
@@ -32,6 +32,6 @@ fi
32
32
 
33
33
  # ─── Degraded fallback: static banner, valid JSON, exit 0 ──────────────
34
34
  cat <<'EOF'
35
- {"systemMessage": "\n╔══════════════════════════════════════════════╗\n║ A R K A O S ║\nThe Operating System for AI Teams ║\n╚══════════════════════════════════════════════╝\nOlá, founder (WizardingCode)\nArkaOS (degraded: no usable interpreter — run npx arkaos doctor)"}
35
+ {"systemMessage": "\nA R K A O S\n The Operating System for AI Agent Teams\n\n Olá, founder\n degraded: no usable interpreter — run npx arkaos doctor"}
36
36
  EOF
37
37
  exit 0
@@ -173,7 +173,12 @@ if [ -f "$ARKA_CONFIG" ]; then
173
173
  fi
174
174
 
175
175
  # ─── Build Line 1: Context bar ───────────────────────────────────────────
176
- LINE1="${C_CYAN}▲ARKA${C_RESET} ${C_WHITE}${DIR_NAME}${C_RESET}"
176
+ # Version from the stable snapshot (one cat, no spawn storm on re-render).
177
+ ARKA_VER=""
178
+ if [ -f "$HOME/.arkaos/lib/VERSION" ]; then
179
+ ARKA_VER=$(tr -d '[:space:]' < "$HOME/.arkaos/lib/VERSION" 2>/dev/null)
180
+ fi
181
+ LINE1="${C_CYAN}▲ARKA${C_RESET}${ARKA_VER:+ ${C_DIM}v${ARKA_VER}${C_RESET}} ${C_WHITE}${DIR_NAME}${C_RESET}"
177
182
 
178
183
  # Git branch (hidden on main/master to reduce noise)
179
184
  if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
@@ -7,13 +7,15 @@ print) with ONE python process that emits the full ``systemMessage``.
7
7
  Measured baseline before this consolidation: 251ms p50 (isolated floor,
8
8
  benchmarks/results.md); each spawn costs ~20-80ms.
9
9
 
10
- Section order preserved exactly from the shell version:
11
- banner+greeting workflow line version+forge+drift →
12
- evidence-flow contract meta-tag contract Model Fabric directive
13
- [SESSION] rehydrator resume → [SESSION-MEMORY] recap JSON out.
10
+ Presentation contract (Foundation PR-2): the user sees a compact branded
11
+ greeting ``systemMessage`` carries ONLY banner+greeting, workflow/forge
12
+ state, and the drift warning. Everything the MODEL needs but the user
13
+ should not scroll through (evidence-flow contract, meta-tag contract,
14
+ authority brief, Model Fabric directive, [SESSION] resume,
15
+ [SESSION-MEMORY] recap) ships via ``hookSpecificOutput.
16
+ additionalContext`` — same enforcement, zero wall of text.
14
17
  Background side effects (reorganizer trigger, dashboard ensure) stay
15
- detached and are config-gated; the NEW ``cognition.reorganize_on_session``
16
- gate lands here (QG F2-1 follow-up, endorsed for this PR).
18
+ detached and are config-gated (``cognition.reorganize_on_session``).
17
19
 
18
20
  The shell wrapper only resolves the interpreter and ``exec``s this
19
21
  module; with no usable venv it emits a static banner (fail-open).
@@ -35,17 +37,21 @@ _BUDGET_MS = 300
35
37
  _RECAP_ITEMS = 3
36
38
  _SUMMARY_CHARS = 130
37
39
 
38
- _BANNER = (
39
- "\n╔══════════════════════════════════════════════╗\n"
40
- "║ ║\n"
41
- "A R K A O S ║\n"
42
- "║ ║\n"
43
- "║ The Operating System for AI Teams ║\n"
44
- "║ by WizardingCode ║\n"
45
- "║ ║\n"
46
- "╚══════════════════════════════════════════════╝\n"
40
+ # Compact wordmark (Levitation identity: flat apex + floating bar) — the
41
+ # box-drawing wall it replaces read as a default terminal artifact.
42
+ _FALLBACK_BANNER = (
43
+ "\n ▲ A R K A O S\n"
44
+ " The Operating System for AI Agent Teams\n"
47
45
  )
48
46
 
47
+
48
+ def _banner(version: str, name: str, company: str) -> str:
49
+ return (
50
+ f"\n ▲ A R K A O S — v{version}\n"
51
+ f" The Operating System for AI Agent Teams · {company}\n"
52
+ f"\n Olá, {name}\n"
53
+ )
54
+
49
55
  _EVIDENCE_CONTRACT = (
50
56
  "\n\n[ARKA:EVIDENCE-FLOW] NON-NEGOTIABLE. Every non-trivial request runs"
51
57
  " the 4-gate evidence flow (constitution rule evidence-flow; source"
@@ -266,27 +272,52 @@ def _authority_brief(cwd: str) -> str:
266
272
  return f"\n\n{brief}" if brief else ""
267
273
 
268
274
 
269
- def build_message(cwd: str) -> str:
275
+ def build_visible(cwd: str) -> str:
276
+ """User-facing ``systemMessage``: branded greeting + live state only.
277
+
278
+ Owns the background side effects (reorganizer, dashboard ensure) so
279
+ they fire exactly once per session regardless of which builder a
280
+ caller combines.
281
+ """
270
282
  repo = repo_path()
271
283
  config = _config()
272
284
  name, company = _profile()
273
285
  version = _version(repo)
274
- msg = _BANNER + f"\nOlá, {name} ({company})\n"
286
+ msg = _banner(version, name, company)
275
287
  msg += _workflow_line()
276
- msg += f"ArkaOS v{version}"
277
288
  msg += _forge_line()
278
289
  msg += _drift(version)
279
- msg += _EVIDENCE_CONTRACT
280
- msg += _META_TAG_CONTRACT
281
- msg += _authority_brief(cwd)
282
- msg += _model_fabric()
283
290
  _trigger_reorganizer(repo, config)
284
291
  _ensure_dashboard(repo, config)
285
- msg += _session_resume()
292
+ return msg
293
+
294
+
295
+ def build_context(cwd: str) -> str:
296
+ """Model-only ``additionalContext``: the operating contracts.
297
+
298
+ Same text that used to flood the visible banner — the enforcement
299
+ surfaces (PreToolUse gate, Stop hook) read the model's OUTPUT
300
+ markers, so moving the injected contracts off-screen changes nothing
301
+ about enforcement.
302
+ """
303
+ parts = [
304
+ _EVIDENCE_CONTRACT,
305
+ _META_TAG_CONTRACT,
306
+ _authority_brief(cwd),
307
+ _model_fabric(),
308
+ _session_resume(),
309
+ ]
286
310
  recap = build_recap(cwd)
287
311
  if recap:
288
- msg += f"\n\n{recap}"
289
- return msg
312
+ parts.append(f"\n\n{recap}")
313
+ return "".join(parts).lstrip("\n")
314
+
315
+
316
+ def build_message(cwd: str) -> str:
317
+ """Full text (visible + contracts) — legacy single-string view."""
318
+ visible = build_visible(cwd)
319
+ context = build_context(cwd)
320
+ return visible + (f"\n\n{context}" if context else "")
290
321
 
291
322
 
292
323
  def build_recap(cwd: str, budget_ms: int = _BUDGET_MS) -> str:
@@ -336,10 +367,20 @@ def main(stdin_json: dict | None = None) -> int:
336
367
  or os.getcwd()
337
368
  )
338
369
  try:
339
- message = build_message(cwd)
370
+ visible = build_visible(cwd)
340
371
  except Exception: # absolute fail-open: static banner, exit 0
341
- message = _BANNER + "\nOlá, founder (WizardingCode)\nArkaOS"
342
- print(json.dumps({"systemMessage": message}))
372
+ visible = _FALLBACK_BANNER + "\n Olá, founder\n"
373
+ try:
374
+ context = build_context(cwd)
375
+ except Exception: # contracts are best-effort; greeting never breaks
376
+ context = ""
377
+ payload: dict = {"systemMessage": visible}
378
+ if context:
379
+ payload["hookSpecificOutput"] = {
380
+ "hookEventName": "SessionStart",
381
+ "additionalContext": context,
382
+ }
383
+ print(json.dumps(payload))
343
384
  return 0
344
385
 
345
386
 
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: ArkaOS v4.33.0 agent-team contract
2
+ description: ArkaOS v4.34.0 agent-team contract
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
6
  # ArkaOS — The Operating System for AI Agent Teams
7
7
 
8
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
8
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
9
9
 
10
10
  You are operating within ArkaOS. Every request routes through the
11
11
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.33.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.34.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Background auto-update daemon for ArkaOS (Foundation PR-1).
3
+ *
4
+ * Users were found running v2 while v4.33 was live — the update path
5
+ * (`npx arkaos@latest update`) only fires when the user remembers to run
6
+ * it. This installs a per-OS scheduled unit that runs
7
+ * `scripts/auto-update.sh` daily (and at load): the script checks the npm
8
+ * registry, applies the core update headlessly, and notifies the user.
9
+ * Project sync stays supervised — the next Claude session picks it up via
10
+ * [arka:update-available].
11
+ *
12
+ * Opt-in by default on install/update (`ensureDefaultEnabled`), with a
13
+ * persisted opt-out marker so `npx arkaos autoupdate disable` survives
14
+ * future updates. ESM, Node + Bun, no interactive prompts. Unit
15
+ * generation (`unitFor`) is pure so it can be unit-tested without
16
+ * touching the OS (autostart.js precedent).
17
+ */
18
+
19
+ import { execSync } from "node:child_process";
20
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
21
+ import { homedir } from "node:os";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ export const AUTOUPDATE_LABEL = "io.wizardingcode.arkaos.updater";
26
+ const SERVICE_NAME = "arkaos-updater.service";
27
+ const TIMER_NAME = "arkaos-updater.timer";
28
+
29
+ function defaultRepoRoot() {
30
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
31
+ }
32
+
33
+ /** Pure: the opt-out marker path — written by disable(), honored by
34
+ * ensureDefaultEnabled() and by scripts/auto-update.sh itself. */
35
+ export function optoutPath(home = homedir()) {
36
+ return join(home, ".arkaos", "autoupdate.optout");
37
+ }
38
+
39
+ /** Anchor the daemon at the stable ~/.arkaos/lib snapshot, never at the
40
+ * package root when it can be an npx cache: `npm cache clean` purges
41
+ * that location at any time, and a default-on daemon anchored there
42
+ * dies silently — with no surviving daemon to self-heal (QG blocker,
43
+ * Foundation PR-1). core-snapshot.js ships scripts/ into the snapshot;
44
+ * repoRoot is the fallback for dev checkouts before the first deploy. */
45
+ export function stableRoot({ home = homedir(), repoRoot = defaultRepoRoot() } = {}) {
46
+ const lib = join(home, ".arkaos", "lib");
47
+ return existsSync(join(lib, "scripts", "auto-update.sh")) ? lib : repoRoot;
48
+ }
49
+
50
+ /** Pure: the scheduled unit(s) for a given platform. Throws on
51
+ * unsupported OS. Returns { kind, files: [{ path, content }] } —
52
+ * systemd needs a service + timer pair, launchd a single plist. */
53
+ export function unitFor(os, { repoRoot, home }) {
54
+ const script = `${repoRoot}/scripts/auto-update.sh`;
55
+ const log = `${home}/.arkaos/logs/auto-update-daemon.log`;
56
+
57
+ if (os === "darwin") {
58
+ // launchd runs with a minimal PATH (/usr/bin:/bin:...) — node/npx
59
+ // live in /opt/homebrew/bin or /usr/local/bin, so the plist must
60
+ // carry a usable PATH or the daemon dies on `command -v npx`.
61
+ // StartCalendarInterval fires daily; RunAtLoad covers machines that
62
+ // were asleep/off at that hour (login catches up).
63
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
64
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
65
+ <plist version="1.0">
66
+ <dict>
67
+ <key>Label</key>
68
+ <string>${AUTOUPDATE_LABEL}</string>
69
+ <key>ProgramArguments</key>
70
+ <array>
71
+ <string>/bin/bash</string>
72
+ <string>${script}</string>
73
+ </array>
74
+ <key>EnvironmentVariables</key>
75
+ <dict>
76
+ <key>PATH</key>
77
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
78
+ </dict>
79
+ <key>StartCalendarInterval</key>
80
+ <dict>
81
+ <key>Hour</key>
82
+ <integer>10</integer>
83
+ <key>Minute</key>
84
+ <integer>30</integer>
85
+ </dict>
86
+ <key>RunAtLoad</key>
87
+ <true/>
88
+ <key>StandardOutPath</key>
89
+ <string>${log}</string>
90
+ <key>StandardErrorPath</key>
91
+ <string>${log}</string>
92
+ </dict>
93
+ </plist>
94
+ `;
95
+ return {
96
+ kind: "launchd",
97
+ files: [
98
+ {
99
+ path: join(home, "Library", "LaunchAgents", `${AUTOUPDATE_LABEL}.plist`),
100
+ content,
101
+ },
102
+ ],
103
+ };
104
+ }
105
+
106
+ if (os === "linux") {
107
+ const service = `[Unit]
108
+ Description=ArkaOS auto-update (npm registry watch + headless core update)
109
+ After=network-online.target
110
+
111
+ [Service]
112
+ Type=oneshot
113
+ ExecStart=/bin/bash ${script}
114
+ StandardOutput=append:${log}
115
+ StandardError=append:${log}
116
+ `;
117
+ // Persistent=true replays a missed daily run at next boot — the
118
+ // laptop-that-was-off case that left users on v2.
119
+ const timer = `[Unit]
120
+ Description=Daily ArkaOS auto-update check
121
+
122
+ [Timer]
123
+ OnCalendar=daily
124
+ Persistent=true
125
+
126
+ [Install]
127
+ WantedBy=timers.target
128
+ `;
129
+ const unitDir = join(home, ".config", "systemd", "user");
130
+ return {
131
+ kind: "systemd",
132
+ files: [
133
+ { path: join(unitDir, SERVICE_NAME), content: service },
134
+ { path: join(unitDir, TIMER_NAME), content: timer },
135
+ ],
136
+ };
137
+ }
138
+
139
+ throw new Error(`unsupported platform for autoupdate: ${os}`);
140
+ }
141
+
142
+ function _silent(cmd) {
143
+ try {
144
+ execSync(cmd, { stdio: "pipe" });
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ export function enable({ repoRoot = defaultRepoRoot() } = {}) {
152
+ const root = stableRoot({ repoRoot });
153
+ const unit = unitFor(process.platform, { repoRoot: root, home: homedir() });
154
+ mkdirSync(join(homedir(), ".arkaos", "logs"), { recursive: true });
155
+ for (const f of unit.files) {
156
+ mkdirSync(dirname(f.path), { recursive: true });
157
+ writeFileSync(f.path, f.content, "utf8");
158
+ }
159
+ if (existsSync(optoutPath())) rmSync(optoutPath());
160
+ const mainPath = unit.files[0].path;
161
+ if (unit.kind === "launchd") {
162
+ _silent(`launchctl unload "${mainPath}"`);
163
+ if (!_silent(`launchctl load -w "${mainPath}"`)) {
164
+ return { ok: false, path: mainPath,
165
+ message: "Plist written but launchctl load failed — it will still run at next login." };
166
+ }
167
+ } else if (unit.kind === "systemd") {
168
+ _silent("systemctl --user daemon-reload");
169
+ if (!_silent(`systemctl --user enable --now ${TIMER_NAME}`)) {
170
+ return { ok: false, path: mainPath,
171
+ message: "Timer written but systemctl enable failed — check 'systemctl --user' availability." };
172
+ }
173
+ }
174
+ return { ok: true, path: mainPath,
175
+ message: "Auto-update enabled. ArkaOS checks npm daily and updates itself." };
176
+ }
177
+
178
+ export function disable() {
179
+ const unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
180
+ const mainPath = unit.files[0].path;
181
+ if (unit.kind === "launchd") _silent(`launchctl unload "${mainPath}"`);
182
+ else if (unit.kind === "systemd") _silent(`systemctl --user disable --now ${TIMER_NAME}`);
183
+ for (const f of unit.files) if (existsSync(f.path)) rmSync(f.path);
184
+ // Persisted opt-out: future `npx arkaos update` runs must not re-enable.
185
+ mkdirSync(join(homedir(), ".arkaos"), { recursive: true });
186
+ writeFileSync(optoutPath(), new Date().toISOString(), "utf8");
187
+ return { ok: true, path: mainPath, message: "Auto-update disabled (opt-out persisted)." };
188
+ }
189
+
190
+ export function status() {
191
+ let unit;
192
+ try {
193
+ unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
194
+ } catch (err) {
195
+ return { installed: false, supported: false, optout: false, message: err.message };
196
+ }
197
+ return {
198
+ installed: unit.files.every((f) => existsSync(f.path)),
199
+ supported: true,
200
+ optout: existsSync(optoutPath()),
201
+ kind: unit.kind,
202
+ path: unit.files[0].path,
203
+ };
204
+ }
205
+
206
+ /** Default-on wiring for install/update flows: enable unless the user
207
+ * opted out or the platform is unsupported. Never throws. */
208
+ export function ensureDefaultEnabled({ repoRoot = defaultRepoRoot() } = {}) {
209
+ const s = status();
210
+ if (!s.supported) return { action: "unsupported" };
211
+ if (s.optout) return { action: "optout" };
212
+ if (s.installed) {
213
+ // Refresh unit content in place (paths/schedule may change between
214
+ // versions) but do not force a reload storm on every update.
215
+ try {
216
+ const root = stableRoot({ repoRoot });
217
+ const unit = unitFor(process.platform, { repoRoot: root, home: homedir() });
218
+ for (const f of unit.files) writeFileSync(f.path, f.content, "utf8");
219
+ } catch {}
220
+ return { action: "already-enabled" };
221
+ }
222
+ const r = enable({ repoRoot });
223
+ return { action: r.ok ? "enabled" : "partial", message: r.message };
224
+ }
225
+
226
+ export async function autoupdate(args = []) {
227
+ const action = (args[0] || "status").toLowerCase();
228
+ if (action === "enable") {
229
+ const r = enable();
230
+ console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}\n ${r.path}`);
231
+ } else if (action === "disable") {
232
+ const r = disable();
233
+ console.log(` ✓ ${r.message}`);
234
+ } else if (action === "run") {
235
+ // Foreground one-shot check — same script (and anchor) the daemon runs.
236
+ const script = join(stableRoot(), "scripts", "auto-update.sh");
237
+ try {
238
+ execSync(`/bin/bash "${script}" --force`, { stdio: "inherit" });
239
+ } catch { process.exitCode = 1; }
240
+ } else if (action === "status") {
241
+ const s = status();
242
+ if (!s.supported) console.log(` Auto-update not supported here: ${s.message}`);
243
+ else if (s.optout) console.log(` Auto-update: DISABLED by user opt-out\n (re-enable: npx arkaos autoupdate enable)`);
244
+ else console.log(` Auto-update: ${s.installed ? "ENABLED" : "disabled"} (${s.kind})\n ${s.path}`);
245
+ } else {
246
+ console.error(` Unknown autoupdate action: ${action}. Use enable | disable | status | run.`);
247
+ process.exitCode = 1;
248
+ }
249
+ }
package/installer/cli.js CHANGED
@@ -63,6 +63,7 @@ Usage:
63
63
  npx arkaos migrate-user-data Move user data (~/.claude/skills/arka/ → ~/.arkaos/)
64
64
  npx arkaos dashboard Start monitoring dashboard
65
65
  npx arkaos autostart <enable|disable|status> Start dashboard on boot
66
+ npx arkaos autoupdate <enable|disable|status|run> Keep ArkaOS updated automatically (daily check + notification)
66
67
  npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
67
68
  npx arkaos models Model Fabric: which model runs each role
68
69
  npx arkaos models set <role> <provider>/<model> Re-route a role
@@ -134,6 +135,12 @@ async function main() {
134
135
  break;
135
136
  }
136
137
 
138
+ case "autoupdate": {
139
+ const { autoupdate } = await import("./autoupdate.js");
140
+ await autoupdate(positionals.slice(1));
141
+ break;
142
+ }
143
+
137
144
  case "uninstall":
138
145
  const { uninstall } = await import("./uninstall.js");
139
146
  await uninstall();
@@ -35,7 +35,11 @@ import { join, basename } from "node:path";
35
35
  // resolve_arkaos_root() checks, so a crash mid-deploy on a fresh install
36
36
  // leaves a snapshot that fails validation instead of a core-only root
37
37
  // that validates but lacks config/departments/knowledge.
38
- const SNAPSHOT_DIRS = ["config", "departments", "knowledge", "core"];
38
+ // scripts/ joined in Foundation PR-1: the auto-update daemon's launchd/
39
+ // systemd unit anchors at the snapshot (a unit pointing into the npx
40
+ // cache dies silently on `npm cache clean` — QG blocker). Under 0.5MB
41
+ // after the __pycache__ filter.
42
+ const SNAPSHOT_DIRS = ["config", "departments", "knowledge", "scripts", "core"];
39
43
 
40
44
  export function deployCoreSnapshot(arkaosRoot, installDir) {
41
45
  if (!existsSync(join(arkaosRoot, "core", "sync", "__init__.py"))) return false;
@@ -6,6 +6,7 @@ import { getArkaosPython, getVenvPython, canImportCore, getRepoRoot, diagnoseVen
6
6
  import { IS_WINDOWS, HOOK_EXT, CMD_FINDER } from "./platform.js";
7
7
  import { checkNode, checkObsidian, checkOllama } from "./system-tools.js";
8
8
  import { graphifyDoctor } from "./graphify.js";
9
+ import { status as autoupdateStatus } from "./autoupdate.js";
9
10
 
10
11
  const INSTALL_DIR = join(homedir(), ".arkaos");
11
12
 
@@ -563,6 +564,16 @@ export const checks = [
563
564
  check: () => statuslineConfigured(),
564
565
  fix: () => "Run: npx arkaos install --force (redeploys and wires the statusline)",
565
566
  },
567
+ {
568
+ name: "autoupdate",
569
+ description: "Auto-update daemon installed (or explicit user opt-out)",
570
+ severity: "warn",
571
+ check: () => {
572
+ const s = autoupdateStatus();
573
+ return !s.supported || s.optout || s.installed;
574
+ },
575
+ fix: () => "Run: npx arkaos autoupdate enable",
576
+ },
566
577
  {
567
578
  name: "hooks-wired",
568
579
  description: "Hook chain referenced by ~/.claude/settings.json (governance live)",
@@ -324,6 +324,17 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
324
324
  console.log(` Warning: could not seed config.json (${err.message})`);
325
325
  }
326
326
 
327
+ // Foundation PR-1 — auto-update daemon, default-on (opt-out persists
328
+ // across installs via ~/.arkaos/autoupdate.optout).
329
+ try {
330
+ const { ensureDefaultEnabled } = await import("./autoupdate.js");
331
+ const au = ensureDefaultEnabled({ repoRoot: ARKAOS_ROOT });
332
+ if (au.action === "enabled") console.log(` Auto-update daemon enabled (npx arkaos autoupdate status).`);
333
+ else if (au.action === "optout") console.log(` Auto-update daemon: user opt-out respected.`);
334
+ } catch (err) {
335
+ console.log(` Warning: auto-update daemon not enabled (${err.message})`);
336
+ }
337
+
327
338
  // PR28 v2.47.0 — scaffold the user-mutable files the discipline-arc
328
339
  // commands depend on: redaction-clients.json (leak scanner config)
329
340
  // and reorganize-proposals/ (daily proposal output). Idempotent.
@@ -534,6 +534,17 @@ export async function update({ skillsFlag = "" } = {}) {
534
534
  console.log(` ⚠ Could not set up Graphify (${err.message})`);
535
535
  }
536
536
 
537
+ // ── 7b. Auto-update daemon (Foundation PR-1) — default-on, opt-out kept ──
538
+ try {
539
+ const { ensureDefaultEnabled } = await import("./autoupdate.js");
540
+ const au = ensureDefaultEnabled({ repoRoot: ARKAOS_ROOT });
541
+ if (au.action === "enabled") console.log(" ✓ Auto-update daemon enabled (daily npm check + notification)");
542
+ else if (au.action === "already-enabled") console.log(" ✓ Auto-update daemon active");
543
+ else if (au.action === "optout") console.log(" · Auto-update daemon: user opt-out respected");
544
+ } catch (err) {
545
+ console.log(` ⚠ Auto-update daemon not enabled (${err.message})`);
546
+ }
547
+
537
548
  // ── 8. Update manifest ──
538
549
  console.log(" [8/8] Finalizing...");
539
550
  manifest.version = VERSION;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_meta": {
3
3
  "generator": "scripts/marketplace_gen.py",
4
- "version": "4.33.0",
4
+ "version": "4.34.0",
5
5
  "marketplace": "arkaos"
6
6
  },
7
7
  "structural": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "4.33.0",
3
+ "version": "4.34.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "4.33.0"
3
+ version = "4.34.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ArkaOS — background auto-update (Foundation PR-1)
4
+ #
5
+ # Invoked daily by the io.wizardingcode.arkaos.updater unit (see
6
+ # installer/autoupdate.js) or manually via `npx arkaos autoupdate run`.
7
+ #
8
+ # Flow: registry check (curl, short timeout) → compare with the installed
9
+ # version (~/.arkaos/install-manifest.json) → headless `npx -y
10
+ # arkaos@latest update` → OS notification with the outcome. Project sync
11
+ # stays supervised: update.js resets sync-state.json, so the next Claude
12
+ # session surfaces [arka:update-available] and /arka update runs there.
13
+ #
14
+ # Every failure path logs and exits 0 — a broken check must never surface
15
+ # as a crashing login item.
16
+ # ============================================================================
17
+ set -u
18
+
19
+ ARKA_HOME="${HOME}/.arkaos"
20
+ LOG_DIR="${ARKA_HOME}/logs"
21
+ LOG="${LOG_DIR}/auto-update.log"
22
+ LOCK_DIR="${ARKA_HOME}/auto-update.lock"
23
+ MANIFEST="${ARKA_HOME}/install-manifest.json"
24
+ OPTOUT="${ARKA_HOME}/autoupdate.optout"
25
+ PROFILE="${ARKA_HOME}/profile.json"
26
+ REGISTRY_URL="https://registry.npmjs.org/arkaos/latest"
27
+
28
+ FORCE=0
29
+ [ "${1:-}" = "--force" ] && FORCE=1
30
+
31
+ mkdir -p "$LOG_DIR"
32
+
33
+ log() {
34
+ printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$1" >> "$LOG"
35
+ }
36
+
37
+ # Keep the log bounded (~1MB cap, keep the newest half).
38
+ rotate_log() {
39
+ local size
40
+ size=$(wc -c < "$LOG" 2>/dev/null || echo 0)
41
+ if [ "${size:-0}" -gt 1048576 ]; then
42
+ tail -c 524288 "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"
43
+ fi
44
+ }
45
+
46
+ # JSON field reader: venv python → python3 → sed (last resort).
47
+ json_field() {
48
+ local file="$1" field="$2"
49
+ local py="${ARKA_HOME}/venv/bin/python"
50
+ [ -x "$py" ] || py="$(command -v python3 || true)"
51
+ if [ -n "$py" ]; then
52
+ "$py" -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2],''))" \
53
+ "$file" "$field" 2>/dev/null && return 0
54
+ fi
55
+ sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" 2>/dev/null | head -1
56
+ }
57
+
58
+ notify() {
59
+ local msg="$1"
60
+ log "notify: $msg"
61
+ case "$(uname -s)" in
62
+ Darwin)
63
+ command -v osascript >/dev/null 2>&1 && \
64
+ osascript -e "display notification \"${msg}\" with title \"ArkaOS\"" >/dev/null 2>&1
65
+ ;;
66
+ Linux)
67
+ command -v notify-send >/dev/null 2>&1 && notify-send "ArkaOS" "$msg" >/dev/null 2>&1
68
+ ;;
69
+ esac
70
+ }
71
+
72
+ # Notification copy follows the installed profile language (pt → pt-PT).
73
+ LANG_CODE=""
74
+ [ -f "$PROFILE" ] && LANG_CODE="$(json_field "$PROFILE" language)"
75
+
76
+ msg_updated() {
77
+ if [ "$LANG_CODE" = "pt" ]; then
78
+ echo "Atualizado para v$1. Os projetos sincronizam na próxima sessão Claude."
79
+ else
80
+ echo "Updated to v$1. Projects sync on your next Claude session."
81
+ fi
82
+ }
83
+ msg_failed() {
84
+ if [ "$LANG_CODE" = "pt" ]; then
85
+ echo "Falha no auto-update (v$1). Corre: npx arkaos@latest update"
86
+ else
87
+ echo "Auto-update failed (v$1). Run: npx arkaos@latest update"
88
+ fi
89
+ }
90
+
91
+ rotate_log
92
+
93
+ # ── Opt-out and install guards ─────────────────────────────────────────
94
+ if [ -f "$OPTOUT" ]; then
95
+ log "skip: user opt-out marker present"
96
+ exit 0
97
+ fi
98
+ if [ ! -f "$MANIFEST" ]; then
99
+ log "skip: no install-manifest.json — ArkaOS not installed"
100
+ exit 0
101
+ fi
102
+
103
+ # ── Lock (mkdir is atomic); reclaim stale locks older than 2h ──────────
104
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
105
+ if [ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +120 2>/dev/null)" ]; then
106
+ log "reclaiming stale lock"
107
+ rmdir "$LOCK_DIR" 2>/dev/null || true
108
+ mkdir "$LOCK_DIR" 2>/dev/null || { log "skip: lock contention"; exit 0; }
109
+ else
110
+ log "skip: another auto-update run holds the lock"
111
+ exit 0
112
+ fi
113
+ fi
114
+ trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT
115
+
116
+ # launchd/systemd environments miss the node toolchain locations; append
117
+ # (not prepend — explicit PATH entries, e.g. test stubs, must win).
118
+ export PATH="${PATH}:/opt/homebrew/bin:/usr/local/bin"
119
+
120
+ INSTALLED="$(json_field "$MANIFEST" version)"
121
+ if [ -z "$INSTALLED" ]; then
122
+ log "skip: could not read installed version from manifest"
123
+ exit 0
124
+ fi
125
+
126
+ if ! command -v curl >/dev/null 2>&1; then
127
+ log "skip: curl unavailable"
128
+ exit 0
129
+ fi
130
+ LATEST_JSON="$(curl -sf --max-time 15 "$REGISTRY_URL" 2>/dev/null || true)"
131
+ if [ -z "$LATEST_JSON" ]; then
132
+ log "skip: registry unreachable (offline?)"
133
+ exit 0
134
+ fi
135
+ TMP_JSON="${ARKA_HOME}/.autoupdate-latest.json"
136
+ printf '%s' "$LATEST_JSON" > "$TMP_JSON"
137
+ LATEST="$(json_field "$TMP_JSON" version)"
138
+ rm -f "$TMP_JSON"
139
+ if [ -z "$LATEST" ]; then
140
+ log "skip: could not parse registry response"
141
+ exit 0
142
+ fi
143
+
144
+ # Only ever move FORWARD: a dev/prerelease install newer than the
145
+ # registry `latest` must not be silently downgraded (QG, Francisca).
146
+ # Ordering compare via python; degraded fallback is plain inequality.
147
+ is_newer() { # is_newer LATEST INSTALLED → exit 0 when LATEST > INSTALLED
148
+ local py="${ARKA_HOME}/venv/bin/python"
149
+ [ -x "$py" ] || py="$(command -v python3 || true)"
150
+ if [ -n "$py" ]; then
151
+ "$py" -c '
152
+ import re, sys
153
+ def key(v):
154
+ core = re.split(r"[-+]", v, maxsplit=1)[0]
155
+ nums = [int(x) for x in re.findall(r"\d+", core)[:3]]
156
+ nums += [0] * (3 - len(nums))
157
+ return (nums, "-" not in v) # prerelease sorts below its release
158
+ sys.exit(0 if key(sys.argv[1]) > key(sys.argv[2]) else 1)' "$1" "$2" 2>/dev/null
159
+ return $?
160
+ fi
161
+ [ "$1" != "$2" ]
162
+ }
163
+
164
+ if [ "$FORCE" -ne 1 ]; then
165
+ if [ "$LATEST" = "$INSTALLED" ]; then
166
+ log "up to date (v${INSTALLED})"
167
+ exit 0
168
+ fi
169
+ if ! is_newer "$LATEST" "$INSTALLED"; then
170
+ log "installed v${INSTALLED} is ahead of registry v${LATEST} — skip"
171
+ exit 0
172
+ fi
173
+ fi
174
+
175
+ if ! command -v npx >/dev/null 2>&1; then
176
+ log "skip: npx unavailable — cannot apply v${LATEST}"
177
+ exit 0
178
+ fi
179
+
180
+ log "updating v${INSTALLED} → v${LATEST}"
181
+ if npx -y arkaos@latest update >> "$LOG" 2>&1; then
182
+ log "update to v${LATEST} succeeded"
183
+ notify "$(msg_updated "$LATEST")"
184
+ else
185
+ log "update to v${LATEST} FAILED (see log above)"
186
+ notify "$(msg_failed "$LATEST")"
187
+ fi
188
+ exit 0