futura-scion 0.2.0 → 0.2.2

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.
Files changed (46) hide show
  1. package/README.md +205 -0
  2. package/bin/scion.js +253 -11
  3. package/knowledge/architecture.yaml +98 -0
  4. package/knowledge/core.yaml +99 -0
  5. package/knowledge/git-sdlc.yaml +82 -0
  6. package/knowledge/javascript.yaml +89 -0
  7. package/knowledge/lexicon.de.yaml +24 -0
  8. package/knowledge/lexicon.ja.yaml +25 -0
  9. package/knowledge/lexicon.tr.yaml +24 -0
  10. package/knowledge/performance.yaml +85 -0
  11. package/knowledge/python.yaml +85 -0
  12. package/knowledge/security.yaml +97 -0
  13. package/knowledge/testing.yaml +85 -0
  14. package/package.json +5 -1
  15. package/src/brain/conversations.js +84 -0
  16. package/src/brain/db.js +174 -1
  17. package/src/brain/embedding.js +120 -0
  18. package/src/brain/forgetting.js +222 -0
  19. package/src/brain/memory-graph.js +242 -0
  20. package/src/brain/memory.js +102 -9
  21. package/src/config.js +47 -3
  22. package/src/http.js +387 -0
  23. package/src/index.js +3 -1
  24. package/src/kernel/maintain.js +22 -2
  25. package/src/kernel/trail.js +37 -4
  26. package/src/kernel/watchdog.js +79 -0
  27. package/src/mind/brief.js +385 -0
  28. package/src/mind/calltree-context.js +129 -0
  29. package/src/mind/claims.js +348 -0
  30. package/src/mind/completeness.js +182 -0
  31. package/src/mind/generator.js +9 -1
  32. package/src/mind/grade.js +184 -0
  33. package/src/mind/harness-gate.js +235 -0
  34. package/src/mind/harness-miner.js +196 -0
  35. package/src/mind/harness-propose.js +163 -0
  36. package/src/mind/interventions.js +186 -0
  37. package/src/mind/knowledge.js +165 -0
  38. package/src/mind/langs.js +270 -0
  39. package/src/mind/learn-loop.js +204 -0
  40. package/src/mind/nlu.js +117 -4
  41. package/src/mind/provider.js +109 -0
  42. package/src/mind/tools.js +46 -0
  43. package/src/ui/app.js +434 -0
  44. package/src/ui/index.html +156 -0
  45. package/src/ui/style.css +175 -0
  46. package/src/worker.js +21 -0
package/README.md CHANGED
@@ -53,6 +53,63 @@ scion run "fix var in src/legacy.js" --intent fix # fix + gate-verify + certi
53
53
  scion brain --all # fleet-brain memory inventory
54
54
  ```
55
55
 
56
+ ## The brief organ — context in, verified contract out
57
+
58
+ FS's rungs act on commands, but real work arrives as CONTEXT — tickets,
59
+ specs, conversations. The brief organ (`scion brief`, `docs/brief.md`)
60
+ ingests a context document and produces a structured problem model:
61
+ entities, business rules (`MUST`/`never` sentences), acceptance criteria
62
+ (Gherkin `Given/When/Then`, bullets). Each requirement is classified
63
+ **checkable** (compiles to a Gate verifier command — the same oracle that
64
+ judges code judges the business rules) or **judgment** (kept visible for
65
+ the clarify/review loop, never dropped). A failing requirement **vetoes
66
+ completeness**, so FS cannot declare work done while the contract is
67
+ broken.
68
+
69
+ ## Temporal memory: forgetting, supersession, semantic recall (cortex lineage)
70
+
71
+ The brain never grows stale or noisy — two deterministic organs (ported from
72
+ agentic-cortex, LLM-free throughout) keep it sharp:
73
+
74
+ - **Temporal expiry** — a memory whose text carries a deadline (`2026-12-25`,
75
+ `in 3 days`, `tomorrow`, or an explicit `ttlDays`) gets that lifespan on
76
+ save; `scion brain --sweep` soft-deletes everything whose time has passed.
77
+ - **Supersession** — saving a statement-like fact with a near-identical title
78
+ (`deploy is Monday` over `deploy is Friday`) marks the old fact
79
+ `superseded_by` the new one and excludes it from retrieval. Gate-verified
80
+ memories are append-only evidence and never auto-supersede.
81
+ - **Semantic recall** — every memory carries a deterministic 512-dim
82
+ hashed-feature vector (no model, no download, byte-identical on every
83
+ machine). Hybrid search blends FTS keyword rank with vector similarity;
84
+ when FTS cannot match at all (morphological near-misses like `pools
85
+ exhausting` vs `pool exhaustion`), retrieval falls back to vector-only.
86
+ `scion brain` reports the forgetting ledger (`superseded` / `expired`).
87
+
88
+ ## FS Desktop (the UI)
89
+
90
+ The web UI ships inside the npm package — one command after install:
91
+
92
+ ```bash
93
+ scion ui # boots the kernel + opens FS Desktop in the browser
94
+ # or: scion serve, then open http://127.0.0.1:5107/ui
95
+ ```
96
+
97
+ ### Windows installer (two-tier)
98
+
99
+ A native Windows app is also built from this repo:
100
+
101
+ ```bash
102
+ cd desktop && npm install && npm run dist # → dist-desktop/FS-Desktop-Setup-<ver>.exe
103
+ ```
104
+
105
+ The shell is a thin Electron host (~78 MB installer) that **detects the npm kernel at
106
+ launch**: if `futura-scion` is installed globally (or `SCION_BIN` is set) it spawns
107
+ `scion serve` itself and opens the UI window; otherwise it shows a guided setup screen
108
+ with the one-line install command and a Retry button. The kernel always runs under the
109
+ user's own Node — updates flow through `npm update -g futura-scion` without
110
+ re-installing the shell. Silent install: `FS-Desktop-Setup-<ver>.exe /S`.
111
+
112
+
56
113
  `better-sqlite3` ships prebuilt binaries for common platforms (Linux/macOS/Windows,
57
114
  x64/arm64); exotic platforms need build tools. FS keeps its brain per project
58
115
  (`.scion/`), reads config from `config/scion.config.yaml` (or shipped defaults),
@@ -617,6 +674,154 @@ default) keeps it fully inert even for tasks that opt in with
617
674
  `payload.generate: true`. The doctrine is unchanged — the LLM proposes,
618
675
  the Gate disposes, the certificate proves it.
619
676
 
677
+ ### Self-improvement — the harness evolves under its own Gate (`scion evolve`)
678
+
679
+ FS applies its verification doctrine to ITSELF. The nightly evolution loop
680
+ (imp_doc adoption, zero-LLM) is: **mine → propose → gate → apply**.
681
+
682
+ - **Weakness miner** — failures from the queue and trail are normalized to
683
+ signature hashes (`failureSignatureOf`); a signature with ≥3 occurrences
684
+ across ≥2 goal families is a HARNESS weakness (not one goal's bad luck),
685
+ classified into the 4-layer taxonomy (environment-contract /
686
+ procedural-skill / action-realization / trajectory-regulation).
687
+ - **Proposals** — deterministic per layer (bounds-field, verifier-recipe,
688
+ tool-filter-rule, spec-note), hard 20-line diff budget, targets must
689
+ exist. No LLM anywhere; a generator can be attached later but is still
690
+ gate-checked.
691
+ - **The harness-edit Gate** — SICA utility scoring
692
+ (`U = 0.5·passRate + 0.25·(1−cost) + 0.25·(1−time)`) over the replay
693
+ window: accept only if utility improves AND pass-rate does not regress.
694
+ Every decision lands in the `harness_edits` audit table; rejected
695
+ edit-hashes are remembered (never re-proposed); bounded to ≤3 accepted
696
+ edits per batch; addressed signatures skip (idempotent). Gate-passing but
697
+ utility-unproven edits ARCHIVE in `harness_variants` for monthly re-scoring
698
+ (`scion evolve variants`) — population thinking, nothing promising is lost.
699
+
700
+ ```bash
701
+ scion evolve # one bounded, idempotent batch
702
+ scion evolve --dry-run # mine + propose only
703
+ scion evolve variants # re-score the archive (G4)
704
+ ```
705
+
706
+ ### Stuck-trajectory watchdog + completeness + grading (the guardrails)
707
+
708
+ - **Watchdog** (`src/kernel/watchdog.js`) — hash consecutive `(action,
709
+ state)` pairs; 3 identical → stuck, abort before burning the bounds.
710
+ Key-order-insensitive, pure, also usable as an audit (`findStuckPoint`).
711
+ - **Completeness** (`src/mind/completeness.js`) — every research bundle now
712
+ carries a deterministic completeness verdict: entity coverage,
713
+ subquestion corroboration (stem-folded), source-quality floor. Incomplete
714
+ results produce gap-targeted `replan_hints` and a bounded replan loop
715
+ (`verifyWithReplan`, ≤2 replans) — the VMAO orchestration-level check.
716
+ - **Run records + grading** (`scion eval`) — every task appends a portable
717
+ JSONL run record to `eval/runs.jsonl` (trajectory, cost, outcome); the
718
+ 3-scorer grader (outcome 0.5 / trajectory 0.3 / economy 0.2) emits
719
+ per-run grades and a trend file. `scion eval regression` gates on
720
+ per-family pass-rate drops; `scion eval capability` trends pass-rate per
721
+ difficulty tier.
722
+
723
+ ### Graph-linked memory + interventions (the brain connects and travels)
724
+
725
+ - **Memory graph** — every save links into `memory_links`:
726
+ token-overlap → `related` (weight = Jaccard), same family + opposite
727
+ outcome polarity → `contradicts` (the older side ranks lower —
728
+ suppression, never deletion), newer-higher-confidence → `supersedes`.
729
+ Recall does 1-hop expansion (neighbors at ×0.5) and trust ranking:
730
+ gate-verified memories weigh 1.0, observed 0.95, web 0.7, LLM 0.6.
731
+ - **Interventions** (`scion interventions import [dir]`) — accepted harness
732
+ edits export as `interventions/<hash>.intervention.yaml` artifacts
733
+ (signature, layer, edit, evidence, provenance) and import into another
734
+ FS instance ONLY if that instance's own history shows the same failure
735
+ signature ≥2 times: no blind cross-pollination. This is the fleet brain's
736
+ transport format for learned fixes.
737
+
738
+ ### Call-tree context + indexed actions (Jev/LLM-as-code discipline)
739
+
740
+ - **Call-tree context** (`src/mind/calltree-context.js`) — the generate
741
+ rung's context is the task's ANCESTOR CHAIN from the trail, each ancestor
742
+ budget-capped by depth (deeper → less), total ≤4000 chars — replacing
743
+ flat truncation. Flat inputs stay backward compatible.
744
+ - **Indexed action space** — clarification options render as numbered
745
+ ACTIONS with executable consequences (`[2] analyze the named file and
746
+ apply a gate-verified fix`); answering `2` is a complete decision.
747
+ - **Pre-execution revalidation** — tool calls re-check freshness before
748
+ spawning: file targets that vanished since the decision refuse loudly
749
+ (`stale decision`) instead of executing into a world that moved.
750
+
751
+ ### The learn → build → improve → repeat layer (`scion learn`)
752
+
753
+ The mandate: FS must get BETTER at every kind of problem it takes on —
754
+ software, research, finding things — from its own mistakes, without anyone
755
+ remembering to run a command. This layer is the always-on circulatory
756
+ system connecting every learning organ:
757
+
758
+ - **LEARN** — every task outcome (verified fix, gate failure, escalation,
759
+ error) is observed automatically by the worker and written as a graded
760
+ run record. Failures are the raw material; nothing escapes the loop.
761
+ - **BUILD** — repeated verified fixes forge into seeds (precision-floored);
762
+ clarifications teach the NLU lexicon; interventions export to the fleet.
763
+ - **IMPROVE** — the weakness miner finds recurring failure patterns across
764
+ ALL domains; the harness gate utility-verifies bounded self-edits; the
765
+ variant archive re-scores what didn't win yet.
766
+ - **REPEAT** — every cycle is measured into the `learn_cycles` ledger
767
+ (pass rate, replay hit rate, deterministic share, seeds, edits accepted);
768
+ improvement is a TREND over rows, never a claim.
769
+
770
+ ```bash
771
+ scion learn # one full cycle now
772
+ scion learn trend # is FS improving? (ledger-backed, ≥2 cycles)
773
+ scion learn vitals # current vital signs
774
+ ```
775
+
776
+ With `learn: { auto: true }` in config, `scion serve` runs a cycle every
777
+ `learn.interval_ms` (default 6h) in-process — the harness improves itself
778
+ while it works. Zero-LLM by construction.
779
+
780
+ ### FS Desktop — the standalone UI (chat / agent / plan / architect)
781
+
782
+ FS ships a real UI in two forms, both zero-build:
783
+
784
+ - **Web console (ships in the npm package):** `scion serve` now serves the
785
+ FS Desktop SPA at `http://127.0.0.1:5107/ui` — open it in any browser.
786
+ Nothing extra to install; works over SSH tunnels and on remote machines.
787
+ - **Electron desktop shell (`desktop/`):** a thin host that boots the kernel
788
+ as a child process and renders the same UI in a native window.
789
+ ```bash
790
+ npm run desktop # dev (needs: cd desktop && npm install)
791
+ npm run desktop:build # installers (NSIS / DMG / AppImage) via electron-builder
792
+ ```
793
+
794
+ **Surfaces** (every action goes through the same kernel routes the CLI and
795
+ MCP use — the UI can never do more than the kernel allows):
796
+
797
+ - **Chat with streaming + history** — replies stream over SSE with live
798
+ NLU/agent progress events; every conversation persists in the brain with a
799
+ sidebar (new / open / rename / delete), auto-titling, and secret redaction
800
+ at write time — history survives restarts and travels with the DB.
801
+ - **Chat mode** — plain language in, state-grounded answers out: status,
802
+ brain/economy/queue lookups, brain search. Deterministic NLU; no LLM.
803
+ - **Agent mode** — real work: your utterance is NLU-interpreted, slotted,
804
+ enqueued, and run through the full ladder→Gate pipeline with a rung+
805
+ certificate summary in the transcript.
806
+ - **Plan mode** — research with tools + the deterministic completeness
807
+ verdict (entity coverage, corroboration, source floor) as evidence.
808
+ - **Architect mode** — one-click declared-architecture scan with violations.
809
+ - **Review queue** — the human gate in the loop: escalations with the
810
+ ladder's last proposal, Approve/Reject/Defer, badge counts live.
811
+ - **Trail / Brain / Economy dashboards** — the journaled decision trail,
812
+ brain stats, and the token-economy report as live views.
813
+ - **Autonomy toggle** — semi-autonomous (default: each agent action asks
814
+ through the Gate + Review flow) vs autonomous (apply directly; the Gate
815
+ still verifies everything, destructive work still escalates).
816
+
817
+ **LLM attach (optional):** Settings → Provider covers Ollama, LM Studio,
818
+ OpenAI, Anthropic, OpenRouter, and any OpenAI-compatible endpoint, with a
819
+ connection test. Saving sets `llm.provider/baseUrl/model/apiKey` +
820
+ `llm.daily_tokens` in config, which arms rung G (the governance-shell
821
+ generator): the model PROPOSES patches, the Gate verifies them by exit code,
822
+ and nothing unverified is learned or shipped. `daily_tokens: 0` keeps FS
823
+ fully deterministic — a local model (Ollama/LM Studio) works offline.
824
+
620
825
  ### Semi-autonomous auto-fix (`scion watch` + `daemon.auto_fix`)
621
826
 
622
827
  By default the daemon only **detects and remembers**. With `daemon.auto_fix: true`
package/bin/scion.js CHANGED
@@ -16,8 +16,10 @@
16
16
  'use strict';
17
17
 
18
18
  import { createInterface } from 'node:readline';
19
- import { existsSync } from 'node:fs';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
20
  import { resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { spawnSync } from 'node:child_process';
21
23
  import * as trail from '../src/kernel/trail.js';
22
24
  import {
23
25
  enqueue, runSwarm, economy, trailList, runTask, claim as claimTask,
@@ -90,7 +92,7 @@ async function oneShot(text, opts = {}, depth = 0) {
90
92
  // persisted scopes (brain config). interpret() strips the @tokens itself.
91
93
  const projectScopes = opts.noProjectScopes ? [] : getProjectScopes();
92
94
  const mergedScopes = resolveScopes(text, { flag: opts.scope ?? null, projectScopes });
93
- const interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
95
+ let interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
94
96
  trail.journal('nlu.interpret', {
95
97
  utterance: String(text).slice(0, 200),
96
98
  intent: interp.intent,
@@ -102,6 +104,26 @@ async function oneShot(text, opts = {}, depth = 0) {
102
104
  override: opts.intentOverride ?? null,
103
105
  });
104
106
 
107
+ // Tier 3 — the System-1 language-parse rung: ONLY when the deterministic
108
+ // interpreter found no evidence at all, and an llm provider is configured
109
+ // AND budgeted (daily_tokens > 0 — off by default). The model parses the
110
+ // utterance into intent JSON; the SAME schema check validates it; an
111
+ // abstention falls through to the clarify loop. Never a guess.
112
+ if (interp.intentEvidence.length === 0 && interp.confidence === 0 && !interp.question && !opts.intentOverride) {
113
+ const cfg = loadConfig();
114
+ if (cfg.llm?.daily_tokens > 0 && cfg.llm?.baseUrl && cfg.llm?.model) {
115
+ const { llmInterpret } = await import('../src/mind/nlu.js');
116
+ const parsed = await llmInterpret(text, { llmCfg: cfg.llm });
117
+ trail.journal('nlu.llm-parse', {
118
+ utterance: String(text).slice(0, 200),
119
+ accepted: !!parsed,
120
+ intent: parsed?.intent ?? null,
121
+ confidence: parsed?.confidence ?? null,
122
+ });
123
+ if (parsed) interp = parsed;
124
+ }
125
+ }
126
+
105
127
  // Route pure service intents to their kernel surface — a question about
106
128
  // state should not become a queue task.
107
129
  if (interp.intent === 'show') {
@@ -158,7 +180,7 @@ async function oneShot(text, opts = {}, depth = 0) {
158
180
  // Unresolved answer: fall through and surface the question.
159
181
  }
160
182
  }
161
- return { ok: false, intent: 'clarify', prompt: cl.prompt, options: cl.options, open: cl.open, gap_id: gap.id };
183
+ return { ok: false, intent: 'clarify', prompt: cl.prompt, options: cl.options, open: cl.open, gap_id: gap.id, lang_notice: interp.lang_notice ?? null };
162
184
  }
163
185
 
164
186
  // Ordinary task — enriched with extracted slots.
@@ -310,17 +332,130 @@ switch (cmd || '') {
310
332
  const db = getDb();
311
333
  const scope = restArgs[0] === '--all' ? 'all'
312
334
  : restArgs[0] === '--project' ? resolve(restArgs[1] || '.').replace(/\\/g, '/') : null;
335
+ // Temporal forgetting (cortex lineage): --sweep expires every memory
336
+ // whose lifespan has passed; --sweep --dry-run previews it.
337
+ if (arg === '--sweep' || restArgs.includes('--sweep')) {
338
+ const { sweepExpires } = await import('../src/brain/forgetting.js');
339
+ console.log(JSON.stringify(sweepExpires({ dryRun: arg === '--dry-run' || restArgs.includes('--dry-run') }), null, 2));
340
+ break;
341
+ }
313
342
  const perProject = db.prepare(
314
343
  'SELECT project, COUNT(*) as memories, SUM(is_active) as active FROM memories GROUP BY project ORDER BY memories DESC'
315
344
  ).all();
345
+ const forgetting = db.prepare(
346
+ `SELECT SUM(CASE WHEN is_active = 0 AND superseded_by IS NOT NULL THEN 1 ELSE 0 END) as superseded,
347
+ SUM(CASE WHEN is_active = 0 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expired,
348
+ SUM(CASE WHEN is_active = 1 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expiring_future
349
+ FROM memories`
350
+ ).get();
351
+ // Claims (YOINK lineage): open bets, settled record, the overconfidence
352
+ // gap, the Brier score, and the dead-memory census.
353
+ const { calibration, deadMemories } = await import('../src/mind/claims.js');
354
+ const claims = db.prepare(
355
+ 'SELECT COUNT(*) as total, SUM(CASE WHEN outcome IS NULL THEN 1 ELSE 0 END) as open, SUM(CASE WHEN outcome IS NOT NULL THEN 1 ELSE 0 END) as settled FROM claims'
356
+ ).get();
316
357
  console.log(JSON.stringify({
317
358
  ...brainStats(),
318
359
  ...(scope ? { scope } : {}),
319
360
  projects: perProject,
361
+ forgetting,
362
+ claims: { ...claims, record: calibration() },
363
+ dead_memories: deadMemories().length,
320
364
  replay_conflicts: listConflicts(20),
321
365
  }, null, 2));
322
366
  break;
323
367
  }
368
+ case 'brief': {
369
+ // The problem-understanding stage: ingest a context document (file or
370
+ // inline text), compile its requirements to Gate checks, optionally
371
+ // verify now. scion brief <file> [--verify] | scion brief show <id>
372
+ const briefs = await import('../src/mind/brief.js');
373
+ if (arg === 'show') {
374
+ try { console.log(JSON.stringify(briefs.getBrief(Number(restArgs[0])), null, 2)); }
375
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
376
+ break;
377
+ }
378
+ if (arg === 'list') {
379
+ console.log(JSON.stringify(briefs.listBriefs(), null, 2));
380
+ break;
381
+ }
382
+ try {
383
+ const { readFileSync } = await import('node:fs');
384
+ const text = arg && arg !== '-' && existsSync(arg) ? readFileSync(arg, 'utf8') : (arg || '');
385
+ if (!text.trim()) { console.error('✗ brief: provide a file path or inline text'); process.exitCode = 1; break; }
386
+ const b = briefs.ingest({ text, title: restArgs.find(x => !x.startsWith('--')), source: 'cli' });
387
+ const c = briefs.compileBrief(b.brief.id);
388
+ const out = { brief: b.brief, entities: b.entities.length, requirements: b.requirements.length, compiled: c.compiled, judgment: c.judgment };
389
+ if (process.argv.includes('--verify')) {
390
+ const v = await briefs.verifyBrief(b.brief.id);
391
+ out.verification = { passed: v.passed, failed: v.failed, coverage: v.coverage, uncheckable: v.uncheckable };
392
+ out.results = v.results;
393
+ }
394
+ console.log(JSON.stringify(out, null, 2));
395
+ } catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
396
+ break;
397
+ }
398
+ case 'lang': {
399
+ // The multilingual layer: list configured languages, or set the active
400
+ // one (persisted per brain, like project scopes).
401
+ const langs = await import('../src/mind/langs.js');
402
+ if (!arg || arg === 'list') {
403
+ const packs = langs.loadPacks();
404
+ console.log(JSON.stringify({
405
+ active: langs.activeLanguage(),
406
+ languages: ['en', ...langs.configuredLanguages()],
407
+ packs: Object.values(packs).map(p => ({ lang: p.lang, name: p.name, script: p.script, entries: p.entries.length })),
408
+ }, null, 2));
409
+ } else if (arg === 'set') {
410
+ try { console.log(JSON.stringify(langs.setActiveLanguage(restArgs[0]), null, 2)); }
411
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
412
+ } else {
413
+ try { console.log(JSON.stringify(langs.setActiveLanguage(arg), null, 2)); }
414
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
415
+ }
416
+ break;
417
+ }
418
+ case 'claim': {
419
+ // YOINK lineage: turn a memory into a bet with a date on it.
420
+ // scion claim <memoryId> --claim "..." --settles 2027-01-01 --reads file:...#json.path --test "gte 10" --confidence 0.7
421
+ const { declare } = await import('../src/mind/claims.js');
422
+ const flag = (name) => {
423
+ const i = restArgs.indexOf('--' + name);
424
+ return i >= 0 ? restArgs[i + 1] : undefined;
425
+ };
426
+ try {
427
+ const r = declare({
428
+ memoryId: Number(arg),
429
+ claim: flag('claim'),
430
+ settles: flag('settles'),
431
+ reads: flag('reads'),
432
+ test: flag('test'),
433
+ confidence: Number(flag('confidence')),
434
+ });
435
+ console.log(JSON.stringify(r, null, 2));
436
+ } catch (e) {
437
+ console.error('✗ ' + e.message); process.exitCode = 1;
438
+ }
439
+ break;
440
+ }
441
+ case 'settle': {
442
+ // Settle everything due: read the sources, apply the tests, write the
443
+ // outcomes back beside the thoughts. Unreadable sources stay open.
444
+ const { settleAll } = await import('../src/mind/claims.js');
445
+ const r = await settleAll();
446
+ console.log(JSON.stringify(r, null, 2));
447
+ break;
448
+ }
449
+ case 'calibrate': {
450
+ // The record: said vs right — the gap and the Brier score.
451
+ const { calibration, deadMemories } = await import('../src/mind/claims.js');
452
+ console.log(JSON.stringify({
453
+ record: calibration(),
454
+ dead: deadMemories({ minAgeDays: Number(restArgs[0]) || 14 }),
455
+ trail_chain: (await import('../src/kernel/trail.js')).verifyChain(),
456
+ }, null, 2));
457
+ break;
458
+ }
324
459
  case 'conflicts': {
325
460
  const { listConflicts, clearConflicts } = await import('../src/mind/replay.js');
326
461
  if (arg === '--clear') {
@@ -441,6 +576,54 @@ switch (cmd || '') {
441
576
  console.log(JSON.stringify({ dry_run: dryRun, ...result }, null, 2));
442
577
  break;
443
578
  }
579
+ case 'eval': {
580
+ // Portable run records + grading (workstream F):
581
+ // scion eval regression per-family pass-rate drop check (gate-able)
582
+ // scion eval capability pass-rate per difficulty tier (trend only)
583
+ const { regressionCheck, capabilityReport } = await import('../src/mind/grade.js');
584
+ if (arg === 'capability') console.log(JSON.stringify(capabilityReport(), null, 2));
585
+ else console.log(JSON.stringify(regressionCheck(), null, 2));
586
+ const reg = regressionCheck();
587
+ if (arg !== 'capability' && Object.values(reg).some(f => f.ok === false)) process.exitCode = 1;
588
+ break;
589
+ }
590
+ case 'interventions': {
591
+ // Shared failure-intervention library (workstream E):
592
+ // scion interventions import [dir] import artifacts with the local-occurrence floor
593
+ const { importInterventions } = await import('../src/mind/interventions.js');
594
+ const dir = arg && arg !== 'import' ? arg : 'interventions';
595
+ console.log(JSON.stringify(importInterventions(dir), null, 2));
596
+ break;
597
+ }
598
+ case 'learn': {
599
+ // THE learn → build → improve → repeat layer:
600
+ // scion learn one full cycle (mine → gate → forge → rescore → ledger)
601
+ // scion learn trend is FS improving? (ledger-backed, ≥2 cycles)
602
+ // scion learn vitals the current vital signs
603
+ const loop = await import('../src/mind/learn-loop.js');
604
+ if (arg === 'trend') console.log(JSON.stringify(loop.trend(), null, 2));
605
+ else if (arg === 'vitals') console.log(JSON.stringify(loop.vitals(), null, 2));
606
+ else console.log(JSON.stringify(loop.cycle(), null, 2));
607
+ break;
608
+ }
609
+ case 'evolve': {
610
+ // The nightly harness-evolution pass (A4): mine weaknesses → propose
611
+ // harness edits → gate them (SICA utility) → apply the accepted. Bounded
612
+ // (≤3 accepted per batch), idempotent (addressed signatures skip),
613
+ // zero-LLM. `scion evolve --dry-run` mines+proposes but applies nothing.
614
+ // scion evolve one bounded batch
615
+ // scion evolve --dry-run mine + propose only
616
+ // scion evolve variants re-score archived harness variants (G4)
617
+ const hGate = await import('../src/mind/harness-gate.js');
618
+ if (arg === 'variants') {
619
+ console.log(JSON.stringify(hGate.rescoreVariants({}), null, 2));
620
+ break;
621
+ }
622
+ const dryRun = arg === '--dry-run';
623
+ const result = hGate.evolveBatch(dryRun ? { applyAfter: undefined } : {});
624
+ console.log(JSON.stringify({ dry_run: dryRun, ...result }, null, 2));
625
+ break;
626
+ }
444
627
  case 'recipes': {
445
628
  // The recipe library: gate verifier bundles per stack.
446
629
  // scion recipes list the library
@@ -565,9 +748,21 @@ switch (cmd || '') {
565
748
  // scion conventions <file> deviations of one file (evidence rows)
566
749
  const c = await import('../src/mind/conventions.js');
567
750
  if (arg && !arg.startsWith('--')) {
568
- const { readFileSync } = await import('node:fs');
569
- const source = readFileSync(arg, 'utf8');
570
- console.log(JSON.stringify({ file: arg, deviations: c.deviationsFor(source) }, null, 2));
751
+ const { readFileSync, statSync } = await import('node:fs');
752
+ let st = null;
753
+ try { st = statSync(arg); } catch { /* handled below */ }
754
+ if (st?.isDirectory()) {
755
+ // A directory argument means: discover + persist over that tree.
756
+ const result = c.discoverConventions(arg);
757
+ c.saveConventions(result);
758
+ console.log(JSON.stringify({ ok: true, files: result.files, conventions: result.conventions }, null, 2));
759
+ } else if (st?.isFile()) {
760
+ const source = readFileSync(arg, 'utf8');
761
+ console.log(JSON.stringify({ file: arg, deviations: c.deviationsFor(source) }, null, 2));
762
+ } else {
763
+ console.error(`conventions: no such file or directory: ${arg}`);
764
+ process.exitCode = 1;
765
+ }
571
766
  } else {
572
767
  const result = c.discoverConventions(arg && arg !== '--all' ? arg : 'src');
573
768
  c.saveConventions(result);
@@ -641,13 +836,37 @@ switch (cmd || '') {
641
836
  });
642
837
  const api = await startApi({ port: Number(arg) || undefined, leader });
643
838
  leader.start();
839
+ // FS Desktop support: provider-backed generator (rung G) from config,
840
+ // and the default agent-mode runTask over the real oracle.
841
+ const { makeGenerator } = await import('../src/mind/provider.js');
842
+ const { configureLlm } = await import('../src/ladder.js');
843
+ const generator = makeGenerator(cfg.llm);
844
+ configureLlm({ generator, daily_tokens: cfg.llm?.daily_tokens ?? 0 });
845
+ const { setRunTask } = await import('../src/http.js');
846
+ setRunTask(async (task) => runTask(claimTask('ui', task.id), oracle));
847
+ // The always-on learn→build→improve→repeat layer (config learn.auto).
848
+ if (cfg.learn?.auto) {
849
+ const { startAuto } = await import('../src/mind/learn-loop.js');
850
+ startAuto();
851
+ console.log(` learn-loop: auto-cycle every ${Math.round((cfg.learn.interval_ms ?? 21600000) / 60000)} min`);
852
+ }
644
853
  // Observability (env-gated): OTLP log export + ntfy push.
645
854
  const { startObservers } = await import('../src/kernel/observe.js');
646
855
  startObservers();
647
856
  console.log(`scion serve: http://127.0.0.1:${api.port} role=${leader.role}${leader.primaryUrl ? ` primary=${leader.primaryUrl}` : ''} (SCION_TOKEN guards routes when set)`);
648
- console.log(' flags: --follower --lease-ms N --leader-poll-ms N --primary-url URL');
857
+ console.log(` UI: http://127.0.0.1:${api.port}/ui ← FS Desktop (chat, agent, dashboards)`);
858
+ console.log(' flags: --follower --lease-ms N --leader-poll-ms N --primary-url URL [--open]');
859
+ // --open: launch the default browser at the UI (best-effort, every OS).
860
+ if (process.argv.includes('--open')) {
861
+ const { spawn } = await import('node:child_process');
862
+ const url = `http://127.0.0.1:${api.port}/ui`;
863
+ const opener = process.platform === 'win32' ? spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' })
864
+ : process.platform === 'darwin' ? spawn('open', [url], { detached: true, stdio: 'ignore' })
865
+ : spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
866
+ opener.unref();
867
+ }
649
868
  const sweep = setInterval(() => {
650
- try { dailyMaintenance(); } catch { /* maintenance never kills the server */ }
869
+ dailyMaintenance().catch(() => { /* maintenance never kills the server */ });
651
870
  }, 60_000);
652
871
  for (const sig of ['SIGINT', 'SIGTERM']) {
653
872
  process.on(sig, async () => {
@@ -659,6 +878,16 @@ switch (cmd || '') {
659
878
  }
660
879
  break;
661
880
  }
881
+ case 'ui': {
882
+ // `scion ui` — the one-command desktop experience: serve + open the UI.
883
+ // Identical to `scion serve --open`; a separate word because that's what
884
+ // people type after `npm i -g futura-scion`.
885
+ const self = process.argv[1];
886
+ const rest = process.argv.slice(2).filter(a => a !== 'ui');
887
+ const r = spawnSync(process.execPath, [self, 'serve', ...rest, '--open'], { stdio: 'inherit', env: process.env });
888
+ process.exitCode = r.status ?? 0;
889
+ break;
890
+ }
662
891
  case 'remote': {
663
892
  // Distributed tier: drain a remote scion's queue from THIS machine.
664
893
  // scion remote --url http://host:5107 --workerId rw-1 [--max N] [--token T]
@@ -687,9 +916,13 @@ switch (cmd || '') {
687
916
  }
688
917
  case 'dash': {
689
918
  // Live TUI dashboard — one ANSI refresh loop over kernel state.
919
+ // `scion dash <frames> <intervalMs>` runs N frames then exits (CI/trial
920
+ // friendly); bare `scion dash` loops until Ctrl+C as before.
690
921
  const { runDashboard } = await import('../src/kernel/dash.js');
691
- const intervalMs = Number(restArgs.find(a => /^\d+$/.test(a))) || 2000;
692
- await runDashboard({ intervalMs });
922
+ const nums = [arg, ...restArgs].filter(a => /^\d+$/.test(a)).map(Number);
923
+ const frames = nums[0] ?? null;
924
+ const intervalMs = nums[1] ?? 2000;
925
+ await runDashboard({ intervalMs, ...(frames ? { maxFrames: frames } : {}) });
693
926
  break;
694
927
  }
695
928
  case 'mcp': {
@@ -777,7 +1010,16 @@ switch (cmd || '') {
777
1010
  }
778
1011
  break;
779
1012
  }
1013
+ case '--version':
1014
+ case '-v': {
1015
+ // Self-reported version — the FS Desktop shell probes this to detect the
1016
+ // kernel and warn on shell/kernel version skew.
1017
+ let kernelPkg = { version: 'dev' };
1018
+ try { kernelPkg = JSON.parse(readFileSync(resolve(fileURLToPath(import.meta.url), '../../package.json'), 'utf8')); } catch { /* dev checkout without package.json */ }
1019
+ console.log(kernelPkg.version);
1020
+ break;
1021
+ }
780
1022
  default:
781
- console.error(`unknown command: ${cmd}\nusage: scion [run "<task>" | swarm [n] | plan <target> [--out m.json] [--run] | workflow <manifest.json> | analyze [dir] | fix <file> | watch [dir] [--auto-fix] [--concurrency N] | economy | brain | remember "<c>" | search "<q>" | reason "<topic>" | forge [--dry-run] | review [-i] | resolve <id> approve "<fix>"|reject|defer | gaps [teach <id> <intent> | forget <id> | --all] | scopes [list|add <name>|rm <name>] | serve [port] [--follower --lease-ms N --leader-poll-ms N --primary-url URL] | remote --url URL --workerId ID | leader | recipes [doctor | show <name>]`);
1023
+ console.error(`unknown command: ${cmd}\nusage: scion [run "<task>" | swarm [n] | plan <target> [--out m.json] [--run] | workflow <manifest.json> | analyze [dir] | fix <file> | watch [dir] [--auto-fix] [--concurrency N] | economy | brain | remember "<c>" | search "<q>" | reason "<topic>" | forge [--dry-run] | review [-i] | resolve <id> approve "<fix>"|reject|defer | gaps [teach <id> <intent> | forget <id> | --all] | scopes [list|add <name>|rm <name>] | ui | serve [port] [--follower --lease-ms N --leader-poll-ms N --primary-url URL] | remote --url URL --workerId ID | leader | recipes [doctor | show <name>]`);
782
1024
  process.exitCode = 1;
783
1025
  }