arkaos 2.61.0 → 2.63.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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.61.0
1
+ 2.63.0
@@ -66,6 +66,10 @@ enforcement_levels:
66
66
  rule: "Forge plans must pass critic validation and governance check before approval"
67
67
  enforcement: "Plan Critic validates constitution compliance; PostToolUse monitors execution"
68
68
 
69
+ - id: mandatory-skill-evaluation
70
+ rule: "After every completed substantive task (Phase 13 marker fires), the system MUST evaluate whether the work just done represents a repeatable capability that should be captured as a permanent skill. Proposals are written to ~/.arkaos/skill-proposals/<date>-<slug>.md for operator review. Bypass via [arka:trivial] or [arka:skill-skip]."
71
+ enforcement: "Stop hook calls core.governance.skill_proposer.evaluate on the closing transcript tail. When the classifier returns should_propose=True a SKILL.md draft is written. Operator promotes via /arka skill-promote (PR45) or manual scaffold under departments/<dept>/skills/<slug>/."
72
+
69
73
  - id: mandatory-flow
70
74
  rule: "Every non-trivial request executes the 13-phase canonical flow defined in arka/skills/flow/SKILL.md. No task type, no context, no runtime setting can opt out. The only bypass is [arka:trivial] for single-file edits under 10 lines."
71
75
  enforcement: "UserPromptSubmit hook classifies the turn; SessionStart systemMessage embeds the flow at system-prompt priority; arka/SKILL.md references it as default; violation is a constitution breach, not a style issue."
@@ -162,6 +162,17 @@ try:
162
162
  except Exception:
163
163
  pass
164
164
 
165
+ # PR44 v2.63.0 — Mandatory post-task skill evaluation
166
+ # (NON-NEGOTIABLE constitution rule mandatory-skill-evaluation).
167
+ # Classifier decides whether closing message represents a repeatable
168
+ # capability worth capturing as a permanent skill. Proposals written
169
+ # to ~/.arkaos/skill-proposals/<date>-<slug>.md. Never raises.
170
+ try:
171
+ from core.governance.skill_proposer import evaluate as _eval_skill
172
+ _eval_skill(last)
173
+ except Exception:
174
+ pass
175
+
165
176
  # PR30 v2.49.0 — Meta-tag soft block. Mirrors the KB cite-check
166
177
  # pipeline. Records whether the closing message carried the required
167
178
  # [arka:meta] one-liner; persists result to /tmp/arkaos-meta/<session>.json
@@ -0,0 +1,133 @@
1
+ """Post-task skill proposer (PR44 v2.63.0).
2
+
3
+ Constitution rule (NON-NEGOTIABLE): after every completed substantive
4
+ task, the system MUST evaluate whether the work just done represents a
5
+ repeatable capability that should be promoted to a permanent skill.
6
+
7
+ Mirror of the PR20 reorganizer pattern but focused on capability-capture.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ _COMPLETION_PATTERNS: tuple[re.Pattern[str], ...] = (
18
+ re.compile(r"\[arka:phase:13\]", re.IGNORECASE),
19
+ re.compile(r"\barc complete\b", re.IGNORECASE),
20
+ )
21
+
22
+ _BYPASS_PATTERNS: tuple[re.Pattern[str], ...] = (
23
+ re.compile(r"\[arka:trivial\]", re.IGNORECASE),
24
+ re.compile(r"\[arka:skill-skip\]", re.IGNORECASE),
25
+ )
26
+
27
+ _SKILL_WORTHY_HINTS: tuple[re.Pattern[str], ...] = (
28
+ re.compile(r"\b\d+-phase\b", re.IGNORECASE),
29
+ re.compile(r"\bworkflow\b", re.IGNORECASE),
30
+ re.compile(r"\bskill\b", re.IGNORECASE),
31
+ re.compile(r"\btemplate\b", re.IGNORECASE),
32
+ re.compile(r"\bprocedure\b", re.IGNORECASE),
33
+ re.compile(r"\bplaybook\b", re.IGNORECASE),
34
+ re.compile(r"\bchecklist\b", re.IGNORECASE),
35
+ )
36
+
37
+ _TRIVIAL_WORD_THRESHOLD: int = 15
38
+ _DEFAULT_OUTPUT_DIR: Path = Path.home() / ".arkaos" / "skill-proposals"
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class SkillProposal:
43
+ """Outcome of a skill-evaluation pass."""
44
+ should_propose: bool
45
+ reason: str
46
+ suggested_slug: str | None
47
+ proposal_path: Path | None
48
+ proposal_markdown: str | None
49
+
50
+
51
+ def evaluate(
52
+ transcript_tail: str,
53
+ *,
54
+ output_dir: Path | None = None,
55
+ today: str | None = None,
56
+ ) -> SkillProposal:
57
+ """Classify the closing transcript tail; propose a skill if warranted."""
58
+ text = transcript_tail or ""
59
+
60
+ if _has_bypass(text):
61
+ return SkillProposal(False, "bypass-marker", None, None, None)
62
+
63
+ if not _has_completion_signal(text):
64
+ return SkillProposal(False, "no-completion-signal", None, None, None)
65
+
66
+ if _is_trivial_length(text):
67
+ return SkillProposal(False, "trivial-length", None, None, None)
68
+
69
+ hint_count = sum(1 for pat in _SKILL_WORTHY_HINTS if pat.search(text))
70
+ if hint_count < 2:
71
+ return SkillProposal(False, "below-skill-hint-floor", None, None, None)
72
+
73
+ slug = _suggest_slug(text)
74
+ markdown = _render_proposal(text, slug, today=today)
75
+ out_dir = output_dir or _DEFAULT_OUTPUT_DIR
76
+ out_dir.mkdir(parents=True, exist_ok=True)
77
+ iso_today = today or datetime.now(timezone.utc).strftime("%Y-%m-%d")
78
+ path = out_dir / f"{iso_today}-{slug}.md"
79
+ path.write_text(markdown, encoding="utf-8")
80
+ return SkillProposal(True, "proposed", slug, path, markdown)
81
+
82
+
83
+ def _has_completion_signal(text: str) -> bool:
84
+ return any(p.search(text) for p in _COMPLETION_PATTERNS)
85
+
86
+
87
+ def _has_bypass(text: str) -> bool:
88
+ return any(p.search(text) for p in _BYPASS_PATTERNS)
89
+
90
+
91
+ def _is_trivial_length(text: str) -> bool:
92
+ return len(text.split()) < _TRIVIAL_WORD_THRESHOLD
93
+
94
+
95
+ def _suggest_slug(text: str) -> str:
96
+ for pat in _SKILL_WORTHY_HINTS:
97
+ m = pat.search(text)
98
+ if m:
99
+ anchor = m.group(0).lower()
100
+ return _slugify(f"capability-{anchor}")
101
+ return "capability-task"
102
+
103
+
104
+ def _slugify(value: str) -> str:
105
+ out = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
106
+ return out[:60] if out else "capability"
107
+
108
+
109
+ def _render_proposal(text: str, slug: str, *, today: str | None) -> str:
110
+ iso = today or datetime.now(timezone.utc).strftime("%Y-%m-%d")
111
+ excerpt = text.strip()
112
+ if len(excerpt) > 1000:
113
+ excerpt = excerpt[:1000].rstrip() + "..."
114
+ return (
115
+ f"# Skill Proposal — {slug}\n\n"
116
+ f"> Auto-generated by skill_proposer.evaluate on {iso}. "
117
+ "Review before promoting.\n\n"
118
+ "## Why this proposal exists\n\n"
119
+ "The constitution rule mandatory-skill-evaluation (NON-NEGOTIABLE) "
120
+ "fires after every completed substantive task. This proposal "
121
+ "represents a candidate capability the system thinks could be "
122
+ "captured as a permanent skill in a department.\n\n"
123
+ "## Transcript excerpt (last 1000 chars)\n\n"
124
+ f"```\n{excerpt}\n```\n\n"
125
+ "## Suggested next steps\n\n"
126
+ "1. Decide if this capability deserves its own skill.\n"
127
+ "2. Pick the target department (/dev, /brand, /saas, ...).\n"
128
+ "3. Manually scaffold departments/<dept>/skills/<slug>/SKILL.md.\n"
129
+ "4. Delete this proposal file once acted on.\n\n"
130
+ "## Status\n\n"
131
+ "- Reviewed by operator: NO\n"
132
+ "- Promoted to skill: NO\n"
133
+ )
@@ -0,0 +1,86 @@
1
+ // Auto-install official Claude Code plugins on `npx arkaos install` and
2
+ // `npx arkaos@latest update` (PR43 v2.62.0).
3
+ //
4
+ // Behaviour:
5
+ // - No-op when runtime is not Claude Code
6
+ // - Idempotent: skips plugins already in
7
+ // ~/.claude/plugins/installed_plugins.json
8
+ // - Surfaces a one-line status per plugin (installed | already-present | failed)
9
+ // - Never raises — install failures are logged but don't break the installer
10
+ //
11
+ // Plugin list is intentionally short. Add new defaults here when the
12
+ // operator decides a plugin should ship as a standard ArkaOS dependency.
13
+
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { execSync, spawnSync } from "node:child_process";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ // Each entry is "name@marketplace" matching the `claude plugin install`
20
+ // CLI argument format.
21
+ export const DEFAULT_CLAUDE_PLUGINS = [
22
+ "frontend-design@claude-plugins-official",
23
+ ];
24
+
25
+ const _INSTALLED_REGISTRY = join(
26
+ homedir(), ".claude", "plugins", "installed_plugins.json",
27
+ );
28
+
29
+ export function installDefaultClaudePlugins({
30
+ runtime = "claude-code",
31
+ plugins = DEFAULT_CLAUDE_PLUGINS,
32
+ home = homedir(),
33
+ } = {}) {
34
+ if (runtime !== "claude-code") {
35
+ return { skipped: "runtime-not-claude-code", results: [] };
36
+ }
37
+ if (!isClaudeCliAvailable()) {
38
+ return { skipped: "claude-cli-not-found", results: [] };
39
+ }
40
+ const alreadyInstalled = readInstalledRegistry(home);
41
+ const results = plugins.map((p) =>
42
+ installOne(p, alreadyInstalled),
43
+ );
44
+ return { skipped: null, results };
45
+ }
46
+
47
+ function isClaudeCliAvailable() {
48
+ try {
49
+ execSync("claude --version", { stdio: "pipe", timeout: 5000 });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function readInstalledRegistry(home) {
57
+ const path = join(home, ".claude", "plugins", "installed_plugins.json");
58
+ if (!existsSync(path)) {
59
+ return new Set();
60
+ }
61
+ try {
62
+ const data = JSON.parse(readFileSync(path, "utf-8"));
63
+ return new Set(Object.keys(data.plugins || {}));
64
+ } catch {
65
+ return new Set();
66
+ }
67
+ }
68
+
69
+ function installOne(plugin, alreadyInstalled) {
70
+ if (alreadyInstalled.has(plugin)) {
71
+ return { plugin, action: "already-present" };
72
+ }
73
+ // Use spawnSync so we can capture exit code without throwing on non-zero.
74
+ // Pass --silent equivalents if available; otherwise default verbosity is OK
75
+ // — the installer is interactive at install time.
76
+ const out = spawnSync("claude", ["plugin", "install", plugin], {
77
+ timeout: 60_000,
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ encoding: "utf-8",
80
+ });
81
+ if (out.error || out.status !== 0) {
82
+ const msg = (out.stderr || out.error?.message || "unknown").trim().slice(0, 200);
83
+ return { plugin, action: "failed", reason: msg };
84
+ }
85
+ return { plugin, action: "installed" };
86
+ }
@@ -311,6 +311,27 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
311
311
  console.log(` Warning: could not scaffold user-data (${err.message})`);
312
312
  }
313
313
 
314
+ // PR43 v2.62.0 — auto-install default Claude Code plugins. Only runs
315
+ // when runtime is Claude Code AND the `claude` CLI is available.
316
+ // Idempotent: skips plugins already in installed_plugins.json.
317
+ try {
318
+ const { installDefaultClaudePlugins } = await import("./claude-plugins.js");
319
+ const pluginResult = installDefaultClaudePlugins({ runtime });
320
+ if (!pluginResult.skipped) {
321
+ for (const r of pluginResult.results) {
322
+ if (r.action === "installed") {
323
+ console.log(` ${r.plugin} installed.`);
324
+ } else if (r.action === "already-present") {
325
+ console.log(` ${r.plugin} already installed (skipped).`);
326
+ } else if (r.action === "failed") {
327
+ console.log(` ${r.plugin} install failed (${r.reason}).`);
328
+ }
329
+ }
330
+ }
331
+ } catch (err) {
332
+ console.log(` Warning: could not install default Claude plugins (${err.message})`);
333
+ }
334
+
314
335
  const manifest = {
315
336
  version: VERSION,
316
337
  runtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.61.0",
3
+ "version": "2.63.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 = "2.61.0"
3
+ version = "2.63.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"}