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 +93 -0
- package/bin/scion.js +151 -0
- package/config/scion.config.yaml +1 -5
- package/package.json +1 -1
- package/src/config.js +3 -0
- package/src/kernel/decompose.js +214 -0
- package/src/kernel/levels.js +105 -0
- package/src/kernel/ops.js +104 -0
- package/src/ladder.js +32 -0
- package/src/mind/agent-templates.js +287 -0
- package/src/mind/commitbench.js +194 -0
- package/src/mind/contracts.js +78 -0
- package/src/mind/librarian.js +109 -0
- package/src/mind/phylogeny.js +129 -0
- package/src/mind/skill-exec.js +138 -0
- package/src/mind/skills.js +314 -0
- package/src/mind/task-brief.js +24 -7
- package/src/mind/trust.js +118 -0
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
|
package/config/scion.config.yaml
CHANGED
|
@@ -11,12 +11,8 @@ ladder:
|
|
|
11
11
|
min_insight_confidence: 0.55 # reasoner rung admission floor
|
|
12
12
|
|
|
13
13
|
llm:
|
|
14
|
-
daily_tokens:
|
|
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
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
|
+
}
|