futura-scion 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -858,6 +858,99 @@ the current task needs, retain only what compounds.
858
858
  rung's action threshold — so only REPETITION makes them actionable.
859
859
  One occurrence is an observation; repetition is knowledge.
860
860
 
861
+ ### FS as the learner of its own harness — Codebuff adoption organs (v0.2.8)
862
+
863
+ From studying the open-source Codebuff/Freebuff harnesses, six patterns were
864
+ adopted — each rebuilt to fit the zero-LLM doctrine, none copied as LLM
865
+ machinery:
866
+
867
+ - **Agent templates as data** (`agent-templates/`, `scion templates`): an
868
+ agent is a YAML contract — `spawner_prompt` (how a parent chooses it),
869
+ `input_schema`/`output_schema` (validated at the spawn boundary),
870
+ `tool_allowlist` (least privilege), `capabilities` (routed into the
871
+ kernel's role registry), `bounds`, and `spawnable_agents` — a spawn graph
872
+ that is cycle-checked at load and depth-bounded at spawn. Six templates
873
+ ship (fixer, researcher, mapper, librarian, constructor→verifier,
874
+ verifier). Routing is scored keyword matching, deterministic and journaled:
875
+
876
+ ```bash
877
+ scion templates load|list|route "fix the bug"|spawn <id>
878
+ ```
879
+
880
+ - **Skills — procedural knowledge packs** (`skills/`, `scion skills`):
881
+ `SKILL.md` files with a small frontmatter block (name, description,
882
+ triggers). Parsed loud, name/filename discipline enforced, trigger match
883
+ exact. Wired into ladder THINK: a task mentioning "ship a release"
884
+ absorbs the release-checklist skill into its ephemeral working set
885
+ automatically; swept at task end. New procedures = drop a file in
886
+ `skills/`, zero code.
887
+
888
+ - **The trust gate** (`scion trust`): content-addressed trust for every
889
+ loaded pack. `unseen` → observed; `pin` → checksum-pinned; a PINNED pack
890
+ that changes refuses loudly (with both hashes) until explicitly re-pinned.
891
+
892
+ - **The repo librarian** (`scion librarian <owner/repo> "<question>"`):
893
+ shallow-clone an external repo, run the codebase schematics over it, and
894
+ answer from the map with ranked relevant files — one deterministic pass
895
+ over a foreign codebase (Codebuff's librarian, no LLM).
896
+
897
+ - **Commit-reconstruction benchmark** (`src/mind/commitbench.js`): BuffBench's
898
+ honest methodology with the judge replaced by arithmetic — reconstruct
899
+ real commits against ground-truth diffs, scored on file recall/precision
900
+ + token-level F1, hard subset reported. Oracle = 1.0, empty = 0, proven.
901
+
902
+ - **Declared brief budgets** (`config.brief`): the task working-set's
903
+ TTL/budget/read-limit are config policy (Codebuff CompactionPolicy
904
+ lineage), not module constants.
905
+
906
+ ### The recursive software world — EvoX Genesis adoption organs (v0.2.9)
907
+
908
+ From studying EvoX Genesis (`EMI-Group/genesis`, the recursive software-
909
+ evolution system), eight patterns were adopted — the same "transient agents,
910
+ persistent world" doctrine FS already holds, taken further:
911
+
912
+ - **Recursive decomposition** (`src/kernel/decompose.js`, `scion decompose`):
913
+ large objectives become a delegation tree — managers decompose, executors
914
+ implement, read-only investigators recurse. Depth is bounded
915
+ (`decomposition.max_depth`, default 5); a child may only spawn templates
916
+ its parent declares in `spawnable_agents` — the spawn graph IS authority.
917
+
918
+ - **Foreign-repo write authority**: read-only access to any repo is
919
+ unrestricted; WRITABLE access to a non-primary repo is ROOT-ONLY (depth 0)
920
+ and SERIALIZED (one open writable delegation per repo — parallel writes
921
+ create merge conflicts the spawner cannot control). Violations fail loud.
922
+
923
+ - **Skill distillation** (`distillSkill`, wired to verified outcomes): a
924
+ gate-verified task is distilled into a durable SKILL.md procedure pack —
925
+ the approach plus the oracle commands that proved it. Idempotent: the
926
+ same problem shape updates its skill in place.
927
+
928
+ - **Executable skills** (`src/mind/skill-exec.js`): skills may declare
929
+ `parameters:` frontmatter and a bash body with `{{param}}` placeholders.
930
+ Missing params and unfilled holes refuse loud; execution crosses the
931
+ Gate's blocklist rails — a skill that executes is a tool.
932
+
933
+ - **Context contracts** (`src/mind/contracts.js`): every directory may
934
+ carry a `CONTEXT.md` (Genesis-compatible) declaring its intent; tasks
935
+ targeting a file inherit the chain root→node at THINK time
936
+ ("current state, not history"). Containment is enforced — a contract
937
+ walk never escapes the tree.
938
+
939
+ - **Security-leveled commands** (`src/kernel/levels.js`): level 1
940
+ read-only (runs without ceremony), level 2 attention (reviewable state
941
+ change — unclassified commands are NEVER trusted), level 3 side-effect
942
+ (irreversible; the Gate's blocklist floor). Worst class wins.
943
+
944
+ - **The evolution DAG** (`scion history`, `src/mind/phylogeny.js`):
945
+ accepted verdicts form a phylogenetic tree — same signature reinforces
946
+ its node; a new problem shape ≥ 0.5 Jaccard-similar to an accepted
947
+ ancestor becomes its descendant. The codebase's evolution is navigable.
948
+
949
+ - **Operational hygiene** (`src/kernel/ops.js`, `scion ops`): per-task
950
+ scratch dirs reclaimed EVENT-DRIVEN at terminal status (containment-
951
+ guarded), and peak/off-peak concurrency windows (`ops.peak_windows`)
952
+ scale the swarm down during shared-machine hours.
953
+
861
954
  ### FS Desktop — the standalone UI (chat / agent / plan / architect)
862
955
 
863
956
  FS ships a real UI in two forms, both zero-build:
package/bin/scion.js CHANGED
@@ -678,6 +678,157 @@ switch (cmd || '') {
678
678
  }
679
679
  break;
680
680
  }
681
+ case 'templates': {
682
+ // Declarative agent templates (Codebuff agents-as-data lineage):
683
+ // scion templates load [dir] load every template yaml (default: agent-templates/)
684
+ // scion templates list the registry, with capabilities + spawn graph
685
+ // scion templates route <text> deterministic routing: which template for this problem
686
+ // scion templates spawn <id> bind the template's role (printed, not bound to a live worker)
687
+ const at = await import('../src/mind/agent-templates.js');
688
+ const sub = arg || 'list';
689
+ if (sub === 'load') {
690
+ const dir = restArgs[0] || 'agent-templates';
691
+ const loaded = at.loadTemplates(dir);
692
+ at.checkSpawnGraph();
693
+ console.log(JSON.stringify({ ok: true, dir, loaded }, null, 2));
694
+ break;
695
+ }
696
+ if (sub === 'list') {
697
+ for (const t of at.listTemplates()) {
698
+ console.log(`${t.id.padEnd(14)} ${t.display_name.padEnd(14)} caps: ${t.capabilities.join(',')}${t.spawnable_agents?.length ? ` spawns: ${t.spawnable_agents.join(',')}` : ''}`);
699
+ }
700
+ break;
701
+ }
702
+ if (sub === 'route') {
703
+ const text = restArgs.join(' ');
704
+ if (!text) { console.error('usage: scion templates route <problem text>'); process.exitCode = 2; break; }
705
+ const ranked = at.routeToTemplate(text);
706
+ for (const r of ranked) console.log(`${r.score} ${r.id.padEnd(14)} hits: ${r.hits.join(',') || '(capabilities)'}`);
707
+ if (ranked.length === 0) console.log('(no template matches — the ladder routes to the generalist)');
708
+ break;
709
+ }
710
+ if (sub === 'spawn') {
711
+ if (!restArgs[0]) { console.error('usage: scion templates spawn <id>'); process.exitCode = 2; break; }
712
+ const r = at.spawnTemplate(restArgs[0]);
713
+ console.log(JSON.stringify({ ok: true, template: r.template.id, role: r.role.id, capabilities: r.role.capabilities }, null, 2));
714
+ break;
715
+ }
716
+ console.error('usage: scion templates [load|list|route|spawn]');
717
+ process.exitCode = 2;
718
+ break;
719
+ }
720
+ case 'skills': {
721
+ // Procedural knowledge packs (Codebuff SKILL.md lineage):
722
+ // scion skills load [dir...] load every SKILL.md / *.skill.md under the dirs
723
+ // scion skills list name + description of every loaded skill
724
+ // scion skills match <text> deterministic trigger match, scored
725
+ // scion skills show <name> the full body
726
+ const sk = await import('../src/mind/skills.js');
727
+ const sub = arg || 'list';
728
+ if (sub === 'load') {
729
+ const dirs = restArgs.length ? restArgs : ['skills'];
730
+ const loaded = sk.loadSkills(dirs);
731
+ console.log(JSON.stringify({ ok: true, loaded }, null, 2));
732
+ break;
733
+ }
734
+ if (sub === 'match') {
735
+ const text = restArgs.join(' ');
736
+ if (!text) { console.error('usage: scion skills match <task text>'); process.exitCode = 2; break; }
737
+ for (const m of sk.matchSkills(text)) console.log(`${m.score} ${m.name.padEnd(24)} ${m.description}`);
738
+ if (sk.matchSkills(text).length === 0) console.log('(no skill matches)');
739
+ break;
740
+ }
741
+ if (sub === 'show') {
742
+ const s = sk.getSkill(restArgs[0]);
743
+ if (!s) { console.error(`skills: unknown skill ${JSON.stringify(restArgs[0])} — known: ${sk.listSkills().map(x => x.name).join(', ') || '(none loaded)'}`); process.exitCode = 1; break; }
744
+ console.log(`# ${s.name} — ${s.description}\n(source: ${s.source})\n\n${s.body}`);
745
+ break;
746
+ }
747
+ for (const s of sk.listSkills()) console.log(`${s.name.padEnd(24)} ${s.description}`);
748
+ break;
749
+ }
750
+ case 'trust': {
751
+ // The trust gate for loaded packs (checksum pins, loud refusals):
752
+ // scion trust status <file> state: unseen | quarantine | pinned | changed
753
+ // scion trust pin <file> pin current content (operator action)
754
+ // scion trust unpin <file> remove the pin
755
+ // scion trust list every pin
756
+ const tr = await import('../src/mind/trust.js');
757
+ const sub = arg || 'list';
758
+ if (sub === 'pin') {
759
+ if (!restArgs[0]) { console.error('usage: scion trust pin <file>'); process.exitCode = 2; break; }
760
+ console.log(JSON.stringify(tr.pin(restArgs[0]), null, 2));
761
+ break;
762
+ }
763
+ if (sub === 'unpin') {
764
+ if (!restArgs[0]) { console.error('usage: scion trust unpin <file>'); process.exitCode = 2; break; }
765
+ console.log(JSON.stringify(tr.unpin(restArgs[0]), null, 2));
766
+ break;
767
+ }
768
+ if (sub === 'status') {
769
+ if (!restArgs[0]) { console.error('usage: scion trust status <file>'); process.exitCode = 2; break; }
770
+ console.log(JSON.stringify(tr.trustStatus(restArgs[0]), null, 2));
771
+ break;
772
+ }
773
+ for (const p of tr.listTrusted()) console.log(`${p.sha256.slice(0, 12)} ${p.path} pinned ${p.pinned_at}`);
774
+ break;
775
+ }
776
+ case 'librarian': {
777
+ // External repo comprehension: clone → schematic → structured answer.
778
+ // scion librarian <owner/repo|url> "<question>"
779
+ const lib = await import('../src/mind/librarian.js');
780
+ if (!arg || restArgs.length === 0) { console.error('usage: scion librarian <owner/repo> "<question>"'); process.exitCode = 2; break; }
781
+ const r = lib.askRepo(arg, restArgs.join(' '));
782
+ console.log(r.digest);
783
+ console.log(`\nrelevant files:`);
784
+ for (const f of r.relevant_files) console.log(` ${f}`);
785
+ console.log(`\nclone (kept for inspection): ${r.clone_dir} [files=${r.stats.files} symbols=${r.stats.symbols} ${r.stats.ms}ms]`);
786
+ break;
787
+ }
788
+ case 'decompose': {
789
+ // Recursive decomposition (Genesis lineage):
790
+ // scion decompose open <templateId> "<objective>" [--repo path]
791
+ // scion decompose tree <runId> the delegation tree
792
+ const dc = await import('../src/kernel/decompose.js');
793
+ const sub = arg;
794
+ if (sub === 'open') {
795
+ const templateId = restArgs[0];
796
+ const objective = restArgs.slice(1).join(' ');
797
+ if (!templateId || !objective) { console.error('usage: scion decompose open <templateId> "<objective>" [--repo path]'); process.exitCode = 2; break; }
798
+ const repo = restArgs.find(a => a.startsWith('--repo='))?.split('=')[1];
799
+ const root = dc.decompose({ text: objective, rootTemplate: templateId, repo });
800
+ console.log(JSON.stringify({ ok: true, run: root.id, template: templateId, repo: repo ?? null }, null, 2));
801
+ break;
802
+ }
803
+ if (sub === 'tree') {
804
+ if (!restArgs[0]) { console.error('usage: scion decompose tree <runId>'); process.exitCode = 2; break; }
805
+ const t = dc.delegationTree(restArgs[0]);
806
+ const render = (n, d) => {
807
+ console.log(`${' '.repeat(d)}• [${n.template_id}] ${String(n.objective || n.task_id).slice(0, 70)} (${n.status})`);
808
+ for (const c of n.children) render(c, d + 1);
809
+ };
810
+ for (const r0 of t.roots) render(r0, 0);
811
+ console.log(`— ${t.count} node(s)`);
812
+ break;
813
+ }
814
+ console.error('usage: scion decompose [open|tree]');
815
+ process.exitCode = 2;
816
+ break;
817
+ }
818
+ case 'history': {
819
+ // The evolution DAG (phylogeny): accepted verdicts as a tree.
820
+ const ph = await import('../src/mind/phylogeny.js');
821
+ console.log(ph.renderEvolution());
822
+ break;
823
+ }
824
+ case 'ops': {
825
+ // Operational hygiene status: scratch dirs + peak windows.
826
+ const ops = await import('../src/kernel/ops.js');
827
+ const st = ops.opsStatus();
828
+ console.log(`task scratch dirs: ${st.task_dirs} (${st.root})`);
829
+ console.log(`in peak window: ${ops.inPeak()}`);
830
+ break;
831
+ }
681
832
  case 'evolve': {
682
833
  // The nightly harness-evolution pass (A4): mine weaknesses → propose
683
834
  // harness edits → gate them (SICA utility) → apply the accepted. Bounded
@@ -11,12 +11,8 @@ ladder:
11
11
  min_insight_confidence: 0.55 # reasoner rung admission floor
12
12
 
13
13
  llm:
14
- daily_tokens: 500
14
+ daily_tokens: 0 # 0 = LLM rung off (zero-LLM doctrine)
15
15
 
16
- apiKey: null
17
- model: "qwen2.5-coder:7b"
18
- baseUrl: "http://localhost:11434/v1"
19
- provider: "ollama"
20
16
  bounds:
21
17
  max_turns: 25
22
18
  timeout_ms: 300000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "futura-scion",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "The fused scion of cortex-os-agent + persona: one zero-LLM-dependent agent stack — Mind proposes, Muscle executes, Gate disposes.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -28,6 +28,9 @@ export const DEFAULTS = Object.freeze({
28
28
  queue: { max_attempts: 3 },
29
29
  analyzer: { enabled: true },
30
30
  schematic: { enabled: true, digest_ttl_ms: 30000 }, // map-first THINK: inject the codebase digest into every ladder decide (warm-cache TTL)
31
+ brief: { ttl_s: 3600, budget_bytes: 64000, read_limit: 24, max_item_bytes: 8000 }, // the working-set policy, declared (Codebuff CompactionPolicy lineage) — task-brief defaults defer to this
32
+ decomposition: { max_depth: 5 }, // recursive decomposition depth cap (Genesis lineage) — kernel/decompose.js
33
+ ops: { peak_windows: [], peak_concurrency: null }, // peak windows ("HH:MM-HH:MM" list) scale swarm concurrency down (Genesis PeakHours lineage)
31
34
  architecture: { rules: [], hubs: { max_fan_in: null, max_fan_out: null, max_layer_fan_in: null, ignore: [] } }, // declared layer rules (mind/architecture.js); hubs thresholds are the schematic's measured shape (null = not checked)
32
35
  daemon: { auto_fix: false, max_fixes_per_scan: 10 },
33
36
  muscle: { path_deny: [], path_allow: [] }, // empty deny = shipped defaults
@@ -0,0 +1,214 @@
1
+ /**
2
+ * kernel/decompose.js — RECURSIVE DECOMPOSITION + FOREIGN-REPO WRITE
3
+ * AUTHORITY (EvoX Genesis lineage, rebuilt on the kernel's own rails).
4
+ *
5
+ * Genesis's insight: large objectives are not one queue task — they are a
6
+ * RECURSIVE hierarchy. A manager decomposes; executors implement at the
7
+ * leaves; investigators analyze read-only and may recurse. Every agent is
8
+ * finite-lived; only accepted work advances the world. FS already has the
9
+ * acceptance machinery (the Gate) and the role machinery (kernel/roles);
10
+ * what it lacked is the DELEGATION SPINE.
11
+ *
12
+ * Delegation contracts, enforced by THIS module (not by any agent's
13
+ * judgment — there is no agent judgment here):
14
+ *
15
+ * - depth is bounded (config decomposition.max_depth, default 5);
16
+ * - a child may only spawn templates its parent's template declares in
17
+ * spawnable_agents (the spawn graph from mind/agent-templates.js);
18
+ * - FOREIGN-REPO WRITE AUTHORITY: read-only access to any repo is
19
+ * unrestricted; WRITABLE access to a repo that is not the task's
20
+ * primary repo is ROOT-ONLY (depth 0) and SERIALIZED (one at a time —
21
+ * parallel writes to a foreign repo create merge conflicts the
22
+ * spawner cannot control). Violations fail loud at delegate() time.
23
+ * - every delegation and every terminal child outcome journals.
24
+ *
25
+ * decompose(taskId, { text, rootTemplate, repo }) → the recursion tree
26
+ * delegate(parent, templateId, objective, opts) → spawn a child task
27
+ * completeDelegation(taskId, outcome) → record + roll up
28
+ * delegationTree(taskId) → inspect a run
29
+ *
30
+ * @module kernel/decompose
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ import { getDb } from '../brain/db.js';
36
+ import * as roles from './roles.js';
37
+ import { getTemplate } from '../mind/agent-templates.js';
38
+ import { journal as trailJournal } from './trail.js';
39
+ import { get as getConfig } from '../config.js';
40
+
41
+ function ensureTables(db) {
42
+ db.exec(`CREATE TABLE IF NOT EXISTS delegations (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ node_id TEXT NOT NULL UNIQUE,
45
+ task_id TEXT NOT NULL,
46
+ parent_id TEXT,
47
+ template_id TEXT NOT NULL,
48
+ objective TEXT NOT NULL,
49
+ depth INTEGER NOT NULL,
50
+ repo TEXT,
51
+ write INTEGER NOT NULL DEFAULT 0,
52
+ status TEXT NOT NULL DEFAULT 'pending',
53
+ outcome TEXT,
54
+ created_at TEXT NOT NULL,
55
+ closed_at TEXT
56
+ )`);
57
+ db.prepare('CREATE INDEX IF NOT EXISTS idx_delegations_task ON delegations (task_id)').run();
58
+ }
59
+
60
+ /** Max recursion depth — declared, loud, never vibes. */
61
+ function maxDepth() {
62
+ try {
63
+ const d = getConfig()?.decomposition?.max_depth;
64
+ if (d !== undefined) return Math.max(1, Math.floor(Number(d)));
65
+ } catch { /* config not loaded (tests) — shipped default */ }
66
+ return 5;
67
+ }
68
+
69
+ function childId(parentId) {
70
+ // Deterministic, finite, sortable: depth is embedded in the id.
71
+ return `${parentId}.${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
72
+ }
73
+
74
+ /* ------------------------------------------------------------------ *
75
+ * Delegation — the ONLY way a decomposition spawns.
76
+ * ------------------------------------------------------------------ */
77
+
78
+ /**
79
+ * Spawn a child task under a parent's authority.
80
+ * @param {object} parent — the delegating node: { id, depth, template_id, repo, root? }
81
+ * @param {string} templateId — the child's agent-template id (must be in the
82
+ * parent's spawnable_agents, or the parent must BE the root request)
83
+ * @param {string} objective — what the child must accomplish
84
+ * @param {{ repo?: string, write?: boolean }} [opts] — repo targets the work;
85
+ * write:true requests WRITABLE access (subject to foreign-repo authority)
86
+ * @returns {{ id, template_id, depth, repo, write }}
87
+ */
88
+ export function delegate(parent, templateId, objective, opts = {}) {
89
+ if (!parent?.id || !Number.isFinite(parent.depth)) {
90
+ throw new Error('decompose.delegate: parent must be a delegation node { id, depth, template_id, repo }');
91
+ }
92
+ if (typeof objective !== 'string' || !objective.trim()) {
93
+ throw new Error('decompose.delegate: objective must be a non-empty string');
94
+ }
95
+ const at = maxDepth();
96
+ if (parent.depth + 1 > at) {
97
+ throw new Error(`decompose.delegate: depth ${parent.depth + 1} exceeds the declared max_depth ${at} — decompose smaller, or raise decomposition.max_depth`);
98
+ }
99
+
100
+ // Spawn-graph authority: the parent template must declare the child.
101
+ assertSpawnAllowed(parent.template_id, templateId);
102
+
103
+ // Foreign-repo write authority: root-only + serialized.
104
+ const repo = opts.repo ?? parent.repo ?? null;
105
+ const write = opts.write === true;
106
+ const db = getDb();
107
+ ensureTables(db);
108
+ let holdsWriteLock = false;
109
+ if (write && repo && repo !== (parent.root?.repo ?? parent.repo)) {
110
+ if (parent.depth !== 0) {
111
+ throw new Error(`decompose.delegate: WRITABLE access to foreign repo ${JSON.stringify(repo)} is ROOT-ONLY — node ${parent.id} is at depth ${parent.depth}; nested agents report the need upward (Genesis authority model)`);
112
+ }
113
+ const wlockKey = `wlock:${repo}`;
114
+ if (_writeLocks.get(wlockKey)) {
115
+ throw new Error(`decompose.delegate: foreign repo ${JSON.stringify(repo)} already has an open writable delegation (${_writeLocks.get(wlockKey)}) — writes to a foreign repo are SERIALIZED; delegate the next one after this completes`);
116
+ }
117
+ holdsWriteLock = true;
118
+ }
119
+
120
+ const id = childId(parent.id);
121
+ const now = new Date().toISOString();
122
+ db.prepare(
123
+ `INSERT INTO delegations (node_id, task_id, parent_id, template_id, objective, depth, repo, write, status, created_at)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`
125
+ ).run(id, parent.root?.id ?? parent.id, parent.id, templateId, objective.slice(0, 2000), parent.depth + 1, repo, write ? 1 : 0, now);
126
+ trailJournal('decompose.delegated', { id, parent: parent.id, template: templateId, depth: parent.depth + 1, repo, write });
127
+ if (holdsWriteLock) _writeLocks.set(`wlock:${repo}`, id);
128
+ return { id, parent_id: parent.id, template_id: templateId, depth: parent.depth + 1, repo, write };
129
+ }
130
+
131
+ // Write-lock registry (in-process; cross-process serialization rides the
132
+ // queue's own lease — a foreign-repo writer is an ordinary bounded task).
133
+ const _writeLocks = new Map();
134
+
135
+ /**
136
+ * The spawn-graph authority check — the runner calls this BEFORE executing
137
+ * a delegated child. Fail loud, never guess.
138
+ */
139
+ export function assertSpawnAllowed(parentTemplateId, childTemplateId) {
140
+ const parent = getTemplate(parentTemplateId);
141
+ if (!parent) throw new Error(`decompose: unknown parent template ${JSON.stringify(parentTemplateId)}`);
142
+ const allowed = parent.spawnable_agents ?? [];
143
+ if (!allowed.includes(childTemplateId)) {
144
+ throw new Error(`decompose: template ${JSON.stringify(parentTemplateId)} does not declare ${JSON.stringify(childTemplateId)} in spawnable_agents — the spawn graph is authority, extend the parent's declaration to delegate deeper`);
145
+ }
146
+ }
147
+
148
+ /* ------------------------------------------------------------------ *
149
+ * Completion — outcomes roll up; the write lock releases.
150
+ * ------------------------------------------------------------------ */
151
+
152
+ export function completeDelegation(id, outcome = {}) {
153
+ const db = getDb();
154
+ ensureTables(db);
155
+ const row = db.prepare('SELECT * FROM delegations WHERE node_id = ?').get(id);
156
+ if (!row) throw new Error(`decompose.complete: unknown delegation ${JSON.stringify(id)}`);
157
+ const status = outcome.ok === true ? 'accepted' : outcome.ok === false ? 'rejected' : 'closed';
158
+ db.prepare(
159
+ `UPDATE delegations SET status = ?, outcome = ?, closed_at = ? WHERE node_id = ?`
160
+ ).run(status, JSON.stringify(outcome ?? {}), new Date().toISOString(), id);
161
+ // Release the foreign write lock only if THIS node held it.
162
+ if (row.write === 1 && row.repo) _writeLocks.delete(`wlock:${row.repo}`);
163
+ trailJournal('decompose.completed', { id, status, depth: row.depth });
164
+ return { id, status };
165
+ }
166
+
167
+ /* ------------------------------------------------------------------ *
168
+ * Decompose — the root entry: one objective → a delegation tree.
169
+ * The MANAGER template drives recursion; this module records and gates.
170
+ * ------------------------------------------------------------------ */
171
+
172
+ /**
173
+ * Open a decomposition run: validate the root template, record the root
174
+ * node, and return the handle the runner (or a template implementation)
175
+ * recurses through.
176
+ * @param {{ text, rootTemplate, repo? }} req
177
+ */
178
+ export function decompose(req) {
179
+ const text = String(req?.text ?? '').trim();
180
+ if (!text) throw new Error('decompose.decompose: req.text is required (the objective)');
181
+ if (!req.rootTemplate) throw new Error('decompose.decompose: req.rootTemplate is required (the manager template id)');
182
+ const db = getDb();
183
+ ensureTables(db);
184
+ const id = `d${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
185
+ db.prepare(
186
+ `INSERT INTO delegations (node_id, task_id, parent_id, template_id, objective, depth, repo, write, status, created_at)
187
+ VALUES (?, ?, NULL, ?, ?, 0, ?, 0, 'root', ?)`
188
+ ).run(id, id, req.rootTemplate, text.slice(0, 2000), req.repo ?? null, new Date().toISOString());
189
+ const root = { id, root: { id, repo: req.repo ?? null }, parent_id: null, template_id: req.rootTemplate, depth: 0, repo: req.repo ?? null };
190
+ trailJournal('decompose.opened', { id, template: req.rootTemplate, repo: req.repo ?? null });
191
+ return root;
192
+ }
193
+
194
+ /** The full delegation tree of a run (audit + the dashboard view). */
195
+ export function delegationTree(taskId) {
196
+ const db = getDb();
197
+ ensureTables(db);
198
+ const rows = db.prepare('SELECT * FROM delegations WHERE task_id = ? ORDER BY id').all(taskId);
199
+ const byId = new Map(rows.map(r => [r.node_id, { ...r, write: r.write === 1, outcome: r.outcome ? JSON.parse(r.outcome) : null, children: [] }]));
200
+ const roots = [];
201
+ for (const node of byId.values()) {
202
+ const parent = node.parent_id ? byId.get(node.parent_id) : null;
203
+ if (parent) parent.children.push(node);
204
+ else roots.push(node);
205
+ }
206
+ return { task_id: taskId, roots, count: rows.length };
207
+ }
208
+
209
+ export function reset() {
210
+ const db = getDb();
211
+ ensureTables(db);
212
+ db.prepare('DELETE FROM delegations').run();
213
+ _writeLocks.clear();
214
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * kernel/levels.js — SECURITY-LEVELED COMMANDS (EvoX Genesis CommandShell
3
+ * lineage).
4
+ *
5
+ * FS's safety posture was binary: read-only vs mutating. Genesis's command
6
+ * shell carries three levels with distinct UX semantics, and the distinction
7
+ * is genuinely useful — it lets a reviewer see AT A GLANCE which actions
8
+ * need eyes and which need a signature:
9
+ *
10
+ * level 1 read-only — observes; runs without ceremony (ls, git status,
11
+ * grep, cat, node --check)
12
+ * level 2 attention — changes state the human cares about but that is
13
+ * reviewable/reversible (npm install, file writes);
14
+ * the senior-review lane wants these NAMED
15
+ * level 3 side-effect — destructive or irreversible (rm -rf, force push,
16
+ * drop table, mkfs); approval-gated, fail-closed
17
+ *
18
+ * Classification is deterministic: pattern tables, shape-based, no model.
19
+ * The immovable floor is level 3's blocklist — identical to the Gate's.
20
+ * The most dangerous class wins when a command matches several.
21
+ *
22
+ * classifyCommand(cmd) → { level: 1|2|3, reason }
23
+ * levelOfSteps(steps) → aggregate for a workflow/plan
24
+ *
25
+ * @module kernel/levels
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ import { redact } from './safety.js';
31
+
32
+ /** Level 3 — irreversible. The Gate's blocklist, restated here as the floor. */
33
+ const L3_PATTERNS = [
34
+ ['recursive-delete', /\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\b/i],
35
+ ['delete-root', /\b(del|rmdir)\s+\/[sq]\b/i],
36
+ ['force-push', /\bgit\s+push\s+(--force|-f)\b/i],
37
+ ['hard-reset', /\bgit\s+reset\s+--hard\b/i],
38
+ ['drop-table', /\bdrop\s+table\b/i],
39
+ ['disk-destroy', /\b(mkfs|format\s+[a-z]:|diskpart)\b/i],
40
+ ];
41
+
42
+ /** Level 2 — state-changing but reviewable. */
43
+ const L2_PATTERNS = [
44
+ ['pkg-mutate', /\b(npm|yarn|pnpm)\s+(install|i|ci|uninstall|update|link)\b/],
45
+ ['pkg-install-alt', /\b(pip3?\s+install|cargo\s+(install|add)|apt(-get)?\s+install|brew\s+install|gem\s+install)\b/],
46
+ ['git-write', /\bgit\s+(add|commit|push|pull|merge|rebase|checkout|restore|clean|cherry-pick)\b/],
47
+ ['file-delete', /\b(rm|rmdir|unlink|del)\b/i],
48
+ ['file-move', /\b(mv|move|rename)\b/i],
49
+ ['db-write', /\b(DROP|DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET)\b/],
50
+ ];
51
+
52
+ /** Level 1 — observation. Explicit allowlist; everything not matching any
53
+ * table is level 2 by default (UNSAFE default: unclassified ≠ trusted). */
54
+ const L1_PATTERNS = [
55
+ /\bgit\s+(status|log|diff|show|branch|remote|blame|shortlog|describe|rev-parse)\b/,
56
+ /\b(ls|dir|cat|head|tail|wc|find|grep|rg|tree|stat|file|which|where|whoami|pwd|env)\b/,
57
+ /\b(node|python3?|go|cargo|dotnet|java|mvn|gradle)\s+(-[a-zA-Z-]+\s+)*--?(check|version|--help|-h)\b/,
58
+ /\bnode\s+--check\b/,
59
+ /\b(npm|yarn|pnpm|cargo|go|dotnet|mvn|gradle)\s+(test|run\s+test|check|lint|vet|audit|outdated|list)\b/,
60
+ /\bnpm\s+view\b/,
61
+ /\bcurl\s+(-[a-zA-Z-]+\s+)*https?:\/\//, // GET-shaped curl (no -X POST/-d)
62
+ /\bwget\b/,
63
+ /\bscion\s+[a-z-]+\b/,
64
+ ];
65
+
66
+ /**
67
+ * Classify one command string.
68
+ * @param {string} cmd
69
+ * @returns {{ level: 1|2|3, reason: string|null }}
70
+ */
71
+ export function classifyCommand(cmd) {
72
+ const text = String(cmd || '');
73
+ for (const [name, re] of L3_PATTERNS) {
74
+ if (re.test(text)) return { level: 3, reason: name };
75
+ }
76
+ for (const [name, re] of L2_PATTERNS) {
77
+ if (re.test(text)) return { level: 2, reason: name };
78
+ }
79
+ for (const re of L1_PATTERNS) {
80
+ if (re.test(text)) return { level: 1, reason: null };
81
+ }
82
+ return { level: 2, reason: 'unclassified' }; // unclassified is NEVER trusted
83
+ }
84
+
85
+ /** Aggregate a step list (workflow manifests, plans, skill batches). */
86
+ export function levelOfSteps(steps) {
87
+ let worst = 1, reason = null;
88
+ for (const s of steps) {
89
+ const cmd = typeof s === 'string' ? s : (s?.cmd ?? s?.command ?? '');
90
+ const { level, reason: r } = classifyCommand(cmd);
91
+ if (level > worst) { worst = level; reason = r; }
92
+ }
93
+ return { level: worst, reason };
94
+ }
95
+
96
+ /** Human-readable rendering for review surfaces. */
97
+ export function levelLabel(level) {
98
+ return level === 1 ? 'read-only' : level === 2 ? 'attention' : 'side-effect';
99
+ }
100
+
101
+ /** A review-surface row: redacted command + its level. */
102
+ export function reviewRow(cmd) {
103
+ const { level, reason } = classifyCommand(cmd);
104
+ return { command: redact(String(cmd)).text, level, label: levelLabel(level), reason };
105
+ }