futura-scion 0.2.5 → 0.2.7
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 +86 -5
- package/bin/scion.js +72 -2
- package/config/scion.config.yaml +5 -1
- package/package.json +1 -1
- package/recipes/dotnet.yaml +8 -0
- package/recipes/go.yaml +13 -0
- package/recipes/jvm.yaml +10 -0
- package/recipes/rust.yaml +13 -0
- package/src/brain/db.js +19 -0
- package/src/config.js +24 -7
- package/src/gate/recipes.js +6 -0
- package/src/gate/verifier.js +40 -4
- package/src/kernel/policy.js +1 -0
- package/src/kernel/roles.js +4 -0
- package/src/ladder.js +139 -7
- package/src/mind/architecture.js +177 -8
- package/src/mind/generator.js +8 -1
- package/src/mind/lessons.js +109 -0
- package/src/mind/plan-fullstack.js +22 -6
- package/src/mind/replay.js +10 -1
- package/src/mind/scaffold.js +17 -1
- package/src/mind/schematic.js +559 -0
- package/src/mind/task-brief.js +190 -0
- package/src/swarm.js +16 -3
- package/src/worker.js +73 -13
- package/templates/dotnet-api.yaml +72 -0
- package/templates/go-service.yaml +143 -0
- package/templates/rust-cli.yaml +62 -0
package/README.md
CHANGED
|
@@ -531,11 +531,14 @@ signature. Read-only plans need no ceremony.
|
|
|
531
531
|
|
|
532
532
|
Gate verifier BUNDLES per stack ship in `recipes/` as pure data (YAML —
|
|
533
533
|
description, required tools, verifier commands): **node** (node --check +
|
|
534
|
-
npm test), **python** (py_compile + pytest), **
|
|
535
|
-
**
|
|
536
|
-
|
|
537
|
-
**
|
|
538
|
-
|
|
534
|
+
npm test), **python** (py_compile + pytest), **go** (gofmt discipline +
|
|
535
|
+
go vet + go test), **rust** (cargo check + clippy discipline + cargo test),
|
|
536
|
+
**dotnet** (dotnet build + test), **jvm** (wrapper-aware Maven/Gradle test),
|
|
537
|
+
**docker** (image build), **terraform** (fmt -check + init + validate —
|
|
538
|
+
deliberately read-only; plan/apply belong in senior-review-approved workflow
|
|
539
|
+
steps, never a gate), **xcode** (xcodebuild, tuned by `SCION_XCODE_*` env),
|
|
540
|
+
**gradle** (wrapper build + test). POSIX-pipeline verifiers declare
|
|
541
|
+
`shell: bash` and run under Git Bash on Windows. Enable with one line:
|
|
539
542
|
|
|
540
543
|
```yaml
|
|
541
544
|
gate:
|
|
@@ -589,6 +592,40 @@ with the violation evidence attached — the human chooses refactor, move,
|
|
|
589
592
|
or amend the declaration, via the same `scion review` loop as every other
|
|
590
593
|
escalation.
|
|
591
594
|
|
|
595
|
+
#### Hub rules — shape, not just edges (`architecture.hubs`)
|
|
596
|
+
|
|
597
|
+
Edges police direction; HUBS police concentration, checked against the
|
|
598
|
+
schematic's measured fan-in/fan-out (`scion map` design data — curator
|
|
599
|
+
models derive the same numbers from edges, so no map build is required):
|
|
600
|
+
|
|
601
|
+
```yaml
|
|
602
|
+
architecture:
|
|
603
|
+
hubs:
|
|
604
|
+
max_fan_in: 100 # no file imported by more than N (change magnets)
|
|
605
|
+
max_fan_out: 20 # no file importing more than N (God modules)
|
|
606
|
+
max_layer_fan_in: 300 # no layer funneling more inbound edges than N
|
|
607
|
+
ignore: [vendor, gen] # path prefixes exempt from hub accounting
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Hub violations are analyzer-shaped **warnings** (`arch-hub-fan-in`,
|
|
611
|
+
`arch-hub-fan-out`, `arch-hub-layer-fan-in`) carrying the measured number,
|
|
612
|
+
the declared ceiling, and the file — a design smell escalates to the human
|
|
613
|
+
arch lane, never machine-patched. All thresholds are opt-in (empty = not
|
|
614
|
+
checked); malformed or typo'd keys fail loud. Live on FS's own source at
|
|
615
|
+
diagnostic thresholds: `kernel/trail.js` flagged at 173 inbound edges
|
|
616
|
+
(change magnet), `daemon.js` at 22 imports (God module).
|
|
617
|
+
|
|
618
|
+
#### Map-first THINK (`schematic.enabled`)
|
|
619
|
+
|
|
620
|
+
Every ladder decision attaches the schematic digest — layers, entries,
|
|
621
|
+
hubs, exported surface — as context for all downstream rungs (generator
|
|
622
|
+
prompts, analyzer reasoning, research queries). Served from a warm cache
|
|
623
|
+
(`schematic.digest_ttl_ms`, default 30000) so latency-bounded scans are
|
|
624
|
+
never starved: one refresh per TTL window, amortized across every worker
|
|
625
|
+
and daemon scan. `taskId` and `mapContext` are ambient keys — stripped
|
|
626
|
+
before signature hashing, so identical problems replay identically across
|
|
627
|
+
task ids and map refreshes.
|
|
628
|
+
|
|
592
629
|
### Command over tools — the research rung (`scion tools`)
|
|
593
630
|
|
|
594
631
|
FS's rungs answer from what it knows. The RESEARCH rung gives it hands:
|
|
@@ -777,6 +814,50 @@ With `learn: { auto: true }` in config, `scion serve` runs a cycle every
|
|
|
777
814
|
`learn.interval_ms` (default 6h) in-process — the harness improves itself
|
|
778
815
|
while it works. Zero-LLM by construction.
|
|
779
816
|
|
|
817
|
+
### Codebase schematics — single-pass understanding, incremental forever (`scion map`)
|
|
818
|
+
|
|
819
|
+
FS converts a complex codebase into a complete machine-navigable map — every
|
|
820
|
+
symbol (signature, kind, line, exported/async), file import edges AND
|
|
821
|
+
symbol-level call edges, entry points (bin scripts, HTTP route
|
|
822
|
+
registrations, handlers), the exported surface, per-layer fan-in/fan-out,
|
|
823
|
+
and deterministic pattern observations (factories, handlers, repositories).
|
|
824
|
+
Persisted in the brain with per-file content hashes.
|
|
825
|
+
|
|
826
|
+
The token contract: `mapFor()` re-parses ONLY files whose hash changed and
|
|
827
|
+
reuses persisted fragments for everything else — 89 files with 1 changed
|
|
828
|
+
costs 1 re-parse. Queries answer from the map with zero file reads:
|
|
829
|
+
impact (transitive blast radius of a symbol), who-imports/who-calls,
|
|
830
|
+
surface, entries.
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
scion map src # build-or-refresh + the one-screen digest
|
|
834
|
+
scion map src --impact=runTask # what breaks if this symbol changes
|
|
835
|
+
scion map src --who=enqueue # importers + callers, from the map
|
|
836
|
+
scion map src --entries # every door into the system
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
`digest()` renders the briefing a worker reads INSTEAD of source files;
|
|
840
|
+
subsequent passes re-read only what changed. First pass = understanding;
|
|
841
|
+
every pass after = surgical.
|
|
842
|
+
|
|
843
|
+
### The MoE retention discipline — ephemeral fuel, durable lessons
|
|
844
|
+
|
|
845
|
+
FS's memory mirrors a Mixture-of-Experts model's discipline: activate what
|
|
846
|
+
the current task needs, retain only what compounds.
|
|
847
|
+
|
|
848
|
+
- **Task working-set** (`scion working-set <taskId>`): gathered research
|
|
849
|
+
lives in an EPHEMERAL per-task brief — 64 KiB budget enforced by
|
|
850
|
+
oldest-first eviction, 1-hour TTL, swept at task end. Fuel, not furniture.
|
|
851
|
+
- **Earned research rung**: when every cheaper ladder rung abstains, the
|
|
852
|
+
worker researches the web (deterministic organ) into its working set and
|
|
853
|
+
escalates INFORMED — the reviewer sees findings and sources. Opt out
|
|
854
|
+
with `research: false`.
|
|
855
|
+
- **Lesson distiller**: exactly ONE durable lesson per gate verdict
|
|
856
|
+
(success / failure with the failing command / escalation), dedup-
|
|
857
|
+
reinforced. Lessons start at confidence 82 — just under the memory
|
|
858
|
+
rung's action threshold — so only REPETITION makes them actionable.
|
|
859
|
+
One occurrence is an observation; repetition is knowledge.
|
|
860
|
+
|
|
780
861
|
### FS Desktop — the standalone UI (chat / agent / plan / architect)
|
|
781
862
|
|
|
782
863
|
FS ships a real UI in two forms, both zero-build:
|
package/bin/scion.js
CHANGED
|
@@ -656,6 +656,28 @@ switch (cmd || '') {
|
|
|
656
656
|
else console.log(JSON.stringify(loop.cycle(), null, 2));
|
|
657
657
|
break;
|
|
658
658
|
}
|
|
659
|
+
case 'working-set': {
|
|
660
|
+
// Task working-set introspection (the ephemeral brief):
|
|
661
|
+
// scion working-set <taskId> what a task gathered + its budget posture
|
|
662
|
+
// scion working-set --sweep janitor: expire all past-TTL sets now
|
|
663
|
+
const tb = await import('../src/mind/task-brief.js');
|
|
664
|
+
if (arg === '--sweep') {
|
|
665
|
+
console.log(JSON.stringify(tb.briefSweep(), null, 2));
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
if (!arg) {
|
|
669
|
+
console.error('usage: scion working-set <taskId> | scion working-set --sweep');
|
|
670
|
+
process.exitCode = 2;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
const stats = tb.briefStats(arg);
|
|
674
|
+
const brief = tb.briefRead(arg, { limit: 12 });
|
|
675
|
+
console.log(`task ${arg}: ${stats.items} item(s), ${stats.bytes} bytes`);
|
|
676
|
+
for (const i of brief.items) {
|
|
677
|
+
console.log(` · [${i.source ?? 'no source'}] ${i.text.replace(/\s+/g, ' ').slice(0, 150)}`);
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
659
681
|
case 'evolve': {
|
|
660
682
|
// The nightly harness-evolution pass (A4): mine weaknesses → propose
|
|
661
683
|
// harness edits → gate them (SICA utility) → apply the accepted. Bounded
|
|
@@ -769,6 +791,43 @@ switch (cmd || '') {
|
|
|
769
791
|
if (arch.violations.length > 0) process.exitCode = 1; // CI-friendly
|
|
770
792
|
break;
|
|
771
793
|
}
|
|
794
|
+
case 'map': {
|
|
795
|
+
// THE CODEBASE SCHEMATICS (single-pass understanding, incremental after):
|
|
796
|
+
// scion map [dir] build-or-refresh + digest (one-screen briefing)
|
|
797
|
+
// scion map <dir> --impact X transitive blast radius of symbol X
|
|
798
|
+
// scion map <dir> --who X who imports/calls X
|
|
799
|
+
// scion map <dir> --entries every entry point
|
|
800
|
+
// All queries answer from the persisted map — zero file re-reads beyond
|
|
801
|
+
// the changed files' hashes.
|
|
802
|
+
const sch = await import('../src/mind/schematic.js');
|
|
803
|
+
const target = arg || 'src';
|
|
804
|
+
const r = sch.mapFor(target);
|
|
805
|
+
const m = r.model;
|
|
806
|
+
const rel = (p) => String(p).split('\\').join('/').replace(/^src\//, '');
|
|
807
|
+
if (restArgs.includes('--entries')) {
|
|
808
|
+
for (const e of m.entries) console.log(`${e.kind.padEnd(8)} ${rel(e.file)}${e.symbol ? '#' + e.symbol : ''} (${e.detail})`);
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
const impactOf = restArgs.find((a) => a.startsWith('--impact='))?.split('=')[1];
|
|
812
|
+
if (impactOf) {
|
|
813
|
+
const im = sch.impact(m, impactOf);
|
|
814
|
+
console.log(`impact of ${impactOf} (defined in ${rel(m.symbols.find((s) => s.name === impactOf)?.file ?? '?')}):`);
|
|
815
|
+
console.log(` direct (${im.direct.length}): ${im.direct.map(rel).join(', ')}`);
|
|
816
|
+
console.log(` transitive (${im.transitive.length}): ${im.transitive.map(rel).join(', ')}`);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
const whoOf = restArgs.find((a) => a.startsWith('--who='))?.split('=')[1];
|
|
820
|
+
if (whoOf) {
|
|
821
|
+
const uniq = (a) => [...new Set(a)];
|
|
822
|
+
console.log(`importers: ${uniq(sch.queries.whoImports(m, sch.queries.fileFor(m, whoOf) ?? whoOf)).map(rel).join(', ') || '(none)'}`);
|
|
823
|
+
console.log(`callers: ${uniq(sch.queries.callersOf(m, whoOf)).map(rel).join(', ') || '(none)'}`);
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
console.log(sch.digest(m));
|
|
827
|
+
console.log(`
|
|
828
|
+
refresh: ${r.reused} reused, ${r.changed.length} changed, ${r.added.length} added, ${r.removed.length} removed`);
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
772
831
|
case 'curate': {
|
|
773
832
|
// Build + persist the codebase model (cortex curator lineage).
|
|
774
833
|
// scion curate <dir> build + persist + summary
|
|
@@ -818,9 +877,11 @@ switch (cmd || '') {
|
|
|
818
877
|
const m = f.match(/^--([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
819
878
|
if (m) params[m[1]] = m[2];
|
|
820
879
|
}
|
|
880
|
+
const swarm = flags.includes('--swarm');
|
|
821
881
|
const manifest = planFullstack(resolve(target), {
|
|
822
882
|
name: params.name,
|
|
823
883
|
...(params.port !== undefined ? { port: Number(params.port) } : {}),
|
|
884
|
+
...(swarm ? { swarm: true } : {}),
|
|
824
885
|
});
|
|
825
886
|
if (flags.includes('--dry-run')) {
|
|
826
887
|
console.log(`fullstack plan for ${target}:`);
|
|
@@ -837,8 +898,17 @@ switch (cmd || '') {
|
|
|
837
898
|
}
|
|
838
899
|
const { runWorkflow } = await import('../src/kernel/workflow.js');
|
|
839
900
|
const conc = Number(flags.find((f) => f.startsWith('--concurrency='))?.split('=')[1]) || 3;
|
|
840
|
-
|
|
841
|
-
|
|
901
|
+
// SWARM mode needs a builder worker: register the construct role and
|
|
902
|
+
// bind this process's worker so the claim transaction routes to it.
|
|
903
|
+
const swarmOpts = {};
|
|
904
|
+
if (swarm) {
|
|
905
|
+
const roles = await import('../src/kernel/roles.js');
|
|
906
|
+
roles.registerRole({ id: 'builder', capabilities: ['construct'] });
|
|
907
|
+
roles.bindWorker('cli-builder', 'builder');
|
|
908
|
+
swarmOpts.role = 'builder';
|
|
909
|
+
}
|
|
910
|
+
const r = await runWorkflow(manifest, { concurrency: conc, approval: { reviewer }, ...swarmOpts });
|
|
911
|
+
console.log(`fullstack ${r.status}: ${r.completed}/${r.total} steps (concurrency ${conc}${swarm ? ', swarm-routed' : ''})`);
|
|
842
912
|
for (const t of r.tasks ?? []) {
|
|
843
913
|
if (t.status !== 'completed') console.log(` ${t.status.toUpperCase()}: ${t.manifestId ?? t.id} ${t.error ?? ''}`);
|
|
844
914
|
}
|
package/config/scion.config.yaml
CHANGED
|
@@ -11,8 +11,12 @@ ladder:
|
|
|
11
11
|
min_insight_confidence: 0.55 # reasoner rung admission floor
|
|
12
12
|
|
|
13
13
|
llm:
|
|
14
|
-
daily_tokens:
|
|
14
|
+
daily_tokens: 500
|
|
15
15
|
|
|
16
|
+
apiKey: null
|
|
17
|
+
model: "qwen2.5-coder:7b"
|
|
18
|
+
baseUrl: "http://localhost:11434/v1"
|
|
19
|
+
provider: "ollama"
|
|
16
20
|
bounds:
|
|
17
21
|
max_turns: 25
|
|
18
22
|
timeout_ms: 300000
|
package/package.json
CHANGED
package/recipes/go.yaml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# gate.verifiers — the REAL Gate oracle for Go projects (modules).
|
|
2
|
+
# gofmt discipline (any unformatted file fails), go vet, go test — real
|
|
3
|
+
# exit codes, fail-fast. Pipeline runs under bash (Git Bash on Windows).
|
|
4
|
+
description: Go (modules)
|
|
5
|
+
requires: [go]
|
|
6
|
+
verifiers:
|
|
7
|
+
- cmd: "gofmt -l . | grep -q . && exit 1 || exit 0"
|
|
8
|
+
timeout_ms: 60000
|
|
9
|
+
shell: bash
|
|
10
|
+
- cmd: "go vet ./..."
|
|
11
|
+
timeout_ms: 300000
|
|
12
|
+
- cmd: "go test ./..."
|
|
13
|
+
timeout_ms: 600000
|
package/recipes/jvm.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# gate.verifiers — the REAL Gate oracle for JVM projects (Maven or Gradle,
|
|
2
|
+
# whichever wrapper the project ships). Auto-detects the wrapper, falls
|
|
3
|
+
# back to the global tool. POSIX test syntax runs under bash (Git Bash on
|
|
4
|
+
# Windows).
|
|
5
|
+
description: JVM (Java / Kotlin via Maven or Gradle)
|
|
6
|
+
requires: [java]
|
|
7
|
+
verifiers:
|
|
8
|
+
- cmd: "if [ -f mvnw ]; then ./mvnw -q test; elif [ -f gradlew ]; then ./gradlew -q test; else mvn -q test 2>/dev/null || gradle -q test; fi"
|
|
9
|
+
timeout_ms: 600000
|
|
10
|
+
shell: bash
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# gate.verifiers — the REAL Gate oracle for Rust projects (cargo).
|
|
2
|
+
# Clippy is discipline, cargo test is truth. All judged by exit code.
|
|
3
|
+
# The clippy gate is a bash pipeline (Git Bash on Windows).
|
|
4
|
+
description: Rust (cargo)
|
|
5
|
+
requires: [cargo]
|
|
6
|
+
verifiers:
|
|
7
|
+
- cmd: "cargo check --quiet"
|
|
8
|
+
timeout_ms: 600000
|
|
9
|
+
- cmd: "cargo clippy --quiet 2>&1 | grep -E '^(warning|error)' && exit 1 || exit 0"
|
|
10
|
+
timeout_ms: 600000
|
|
11
|
+
shell: bash
|
|
12
|
+
- cmd: "cargo test --quiet"
|
|
13
|
+
timeout_ms: 600000
|
package/src/brain/db.js
CHANGED
|
@@ -256,6 +256,25 @@ export function ensureSchema(db) {
|
|
|
256
256
|
CREATE INDEX IF NOT EXISTS idx_req_status ON requirements(status);
|
|
257
257
|
`);
|
|
258
258
|
|
|
259
|
+
// ── Mind: task working-set (the EPHEMERAL brief) — gathered research
|
|
260
|
+
// held per-task for the task's duration, swept on completion. The MoE
|
|
261
|
+
// discipline: activate what THIS task needs, retain only the lessons.
|
|
262
|
+
// TTL (expires_at) and a size budget are enforced mechanically.
|
|
263
|
+
db.exec(`
|
|
264
|
+
CREATE TABLE IF NOT EXISTS task_working_set (
|
|
265
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
266
|
+
task_id TEXT NOT NULL,
|
|
267
|
+
kind TEXT NOT NULL DEFAULT 'finding',
|
|
268
|
+
text TEXT NOT NULL,
|
|
269
|
+
source TEXT,
|
|
270
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
271
|
+
expires_at TEXT,
|
|
272
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
273
|
+
);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_tws_task ON task_working_set(task_id);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_tws_expires ON task_working_set(expires_at);
|
|
276
|
+
`);
|
|
277
|
+
|
|
259
278
|
// ── Mind: replay index (Tier-0 learned decisions, now persistent) ──
|
|
260
279
|
db.exec(`
|
|
261
280
|
CREATE TABLE IF NOT EXISTS replay_index (
|
package/src/config.js
CHANGED
|
@@ -27,7 +27,8 @@ export const DEFAULTS = Object.freeze({
|
|
|
27
27
|
forge: { min_support: 3, min_precision: 0.7, max_trigger_terms: 3, max_seeds_per_run: 5 },
|
|
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
|
+
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)
|
|
31
32
|
daemon: { auto_fix: false, max_fixes_per_scan: 10 },
|
|
32
33
|
muscle: { path_deny: [], path_allow: [] }, // empty deny = shipped defaults
|
|
33
34
|
api: { token: null, port: 5107, host: '127.0.0.1' },
|
|
@@ -75,6 +76,19 @@ function dedentBlockLine(line, blockScalar) {
|
|
|
75
76
|
return line.slice(cut);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* YAML clip semantics: a literal/folded block scalar's value ends with a
|
|
81
|
+
* SINGLE trailing newline. Templates embed whole files in these blocks —
|
|
82
|
+
* gofmt, py_compile, and most linters require the final newline, and the
|
|
83
|
+
* old parser silently dropped it (a latent correctness bug the Go recipe's
|
|
84
|
+
* gofmt oracle caught first). Folded blocks join with spaces as before.
|
|
85
|
+
*/
|
|
86
|
+
function finalizeBlock(blockScalar) {
|
|
87
|
+
const text = blockScalar.text;
|
|
88
|
+
if (blockScalar.folded) return text.join(' ');
|
|
89
|
+
return text.length > 0 ? text.join('\n') + '\n' : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
/**
|
|
79
93
|
* Minimal YAML-subset parser: nested maps by indentation, scalars, inline
|
|
80
94
|
* arrays, `#` comments. Deliberately NOT full YAML — unsupported syntax
|
|
@@ -84,6 +98,10 @@ export function parseYaml(text, source = CONFIG_PATH) {
|
|
|
84
98
|
const root = {};
|
|
85
99
|
const stack = [{ indent: -1, obj: root }];
|
|
86
100
|
const lines = text.split(/\r?\n/);
|
|
101
|
+
// A trailing newline is a line TERMINATOR, not a blank line — drop the
|
|
102
|
+
// phantom final empty element so block scalars don't swallow it as an
|
|
103
|
+
// extra blank content line (gofmt-grade oracles reject the result).
|
|
104
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
87
105
|
let blockScalar = null; // open "key: >" / "key: |" scalar being collected
|
|
88
106
|
|
|
89
107
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -103,8 +121,8 @@ export function parseYaml(text, source = CONFIG_PATH) {
|
|
|
103
121
|
&& line.match(/^\s*/)[0].length > (blockScalar.indent ?? 0);
|
|
104
122
|
const isListItem = /^\s*-\s/.test(line);
|
|
105
123
|
if (!isContinuation || isListItem) {
|
|
106
|
-
const { parent, key
|
|
107
|
-
parent[key] =
|
|
124
|
+
const { parent, key } = blockScalar;
|
|
125
|
+
parent[key] = finalizeBlock(blockScalar);
|
|
108
126
|
blockScalar = null;
|
|
109
127
|
}
|
|
110
128
|
}
|
|
@@ -207,11 +225,10 @@ export function parseYaml(text, source = CONFIG_PATH) {
|
|
|
207
225
|
} else {
|
|
208
226
|
parent[key] = parseScalar(rest);
|
|
209
227
|
}
|
|
210
|
-
}
|
|
211
|
-
// Close a block scalar still open at EOF.
|
|
228
|
+
} // Close a block scalar still open at EOF.
|
|
212
229
|
if (blockScalar !== null) {
|
|
213
|
-
const { parent, key
|
|
214
|
-
parent[key] =
|
|
230
|
+
const { parent, key } = blockScalar;
|
|
231
|
+
parent[key] = finalizeBlock(blockScalar);
|
|
215
232
|
}
|
|
216
233
|
return root;
|
|
217
234
|
}
|
package/src/gate/recipes.js
CHANGED
|
@@ -129,8 +129,14 @@ export function probeRecipes(names = null) {
|
|
|
129
129
|
if (toolCache.has(tool)) return toolCache.get(tool);
|
|
130
130
|
let ok = false;
|
|
131
131
|
try {
|
|
132
|
+
// Go prints its version to STDOUT via `go version` (`go --version` is
|
|
133
|
+
// a usage error); everything else speaks `--version`. Probe both.
|
|
132
134
|
const r = spawnSync(`${tool} --version`, { shell: true, windowsHide: true, timeout: 10000 });
|
|
133
135
|
ok = r.status === 0 || (r.error === undefined && typeof r.stdout === 'string' && r.stdout.length > 0);
|
|
136
|
+
if (!ok && tool === 'go') {
|
|
137
|
+
const g = spawnSync('go version', { shell: true, windowsHide: true, timeout: 10000 });
|
|
138
|
+
ok = g.status === 0;
|
|
139
|
+
}
|
|
134
140
|
} catch { ok = false; }
|
|
135
141
|
toolCache.set(tool, ok);
|
|
136
142
|
return ok;
|
package/src/gate/verifier.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
'use strict';
|
|
30
30
|
|
|
31
31
|
import { spawn } from 'node:child_process';
|
|
32
|
+
import { accessSync, constants } from 'node:fs';
|
|
32
33
|
import { resolve, isAbsolute } from 'node:path';
|
|
33
34
|
import { load as loadConfig } from '../config.js';
|
|
34
35
|
import { redact, assertCommandAllowed } from '../kernel/safety.js';
|
|
@@ -59,6 +60,31 @@ export function parseCommand(cmd) {
|
|
|
59
60
|
return argv;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Locate a bash executable for portable POSIX recipes (shell:'bash').
|
|
65
|
+
* Order: SCION_BASH env override, then the well-known Git Bash paths on
|
|
66
|
+
* Windows, then PATH. Cached — a probe per command would be waste.
|
|
67
|
+
*/
|
|
68
|
+
let _bashPath = undefined;
|
|
69
|
+
function resolveBash() {
|
|
70
|
+
if (_bashPath !== undefined) return _bashPath;
|
|
71
|
+
const candidates = [
|
|
72
|
+
process.env.SCION_BASH,
|
|
73
|
+
'C:/Program Files/Git/bin/bash.exe',
|
|
74
|
+
'C:/Program Files (x86)/Git/bin/bash.exe',
|
|
75
|
+
].filter(Boolean);
|
|
76
|
+
if (process.platform !== 'win32') candidates.unshift('/bin/bash', '/usr/bin/bash');
|
|
77
|
+
for (const c of candidates) {
|
|
78
|
+
try {
|
|
79
|
+
accessSync(c, constants.X_OK);
|
|
80
|
+
_bashPath = c;
|
|
81
|
+
return c;
|
|
82
|
+
} catch { /* try next */ }
|
|
83
|
+
}
|
|
84
|
+
_bashPath = null;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
/**
|
|
63
89
|
* Run one command and capture its outcome.
|
|
64
90
|
* @param {object} spec — { cmd, shell?, timeout_ms?, cwd?, env? }
|
|
@@ -74,6 +100,14 @@ export function runCommand(spec) {
|
|
|
74
100
|
const cfg = loadConfig();
|
|
75
101
|
const timeoutMs = spec.timeout_ms ?? cfg.gate?.timeout_ms ?? 120000;
|
|
76
102
|
const useShell = spec.shell === true;
|
|
103
|
+
// shell:'bash' — portable POSIX recipes (gofmt/clippy pipelines, [ -f ]
|
|
104
|
+
// tests). Windows resolves Git Bash; POSIX shells already speak this.
|
|
105
|
+
const useBash = spec.shell === 'bash';
|
|
106
|
+
const bash = useBash ? resolveBash() : null;
|
|
107
|
+
if (useBash && !bash) {
|
|
108
|
+
resolvePromise({ cmd: spec.cmd, exit_code: -1, verified: false, error: 'verifier: shell "bash" requested but no bash found (install Git Bash or set SCION_BASH)' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
77
111
|
const cwd = spec.cwd ? resolve(spec.cwd) : process.cwd();
|
|
78
112
|
|
|
79
113
|
let argv;
|
|
@@ -85,10 +119,12 @@ export function runCommand(spec) {
|
|
|
85
119
|
return;
|
|
86
120
|
}
|
|
87
121
|
|
|
88
|
-
const file = useShell ? (process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh') : argv[0];
|
|
89
|
-
const args =
|
|
90
|
-
?
|
|
91
|
-
:
|
|
122
|
+
const file = useBash ? bash : useShell ? (process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh') : argv[0];
|
|
123
|
+
const args = useBash
|
|
124
|
+
? ['-c', spec.cmd]
|
|
125
|
+
: useShell
|
|
126
|
+
? (process.platform === 'win32' ? ['/d', '/s', '/c', spec.cmd] : ['-c', spec.cmd])
|
|
127
|
+
: argv.slice(1);
|
|
92
128
|
|
|
93
129
|
let child;
|
|
94
130
|
try {
|
package/src/kernel/policy.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const DEFAULT_RULES = [
|
|
13
13
|
{ re: /^(os|shell|exec|rm|del)\b/i, action: 'deny', reason: 'os-level tools require the curated-command path' },
|
|
14
14
|
{ re: /^(read|list|glob|analyze|reason|search)\b/i, action: 'allow' },
|
|
15
|
+
{ re: /^construct\b/i, action: 'gate', reason: 'construction is a mutation: the transactional applier writes and the Gate verifies' },
|
|
15
16
|
{ re: /^(write|edit|fix|apply)\b/i, action: 'gate', reason: 'mutations pass through the evidence gate' },
|
|
16
17
|
];
|
|
17
18
|
|
package/src/kernel/roles.js
CHANGED
|
@@ -100,6 +100,9 @@ export function liveWorkers() {
|
|
|
100
100
|
* - An UNBOUND worker (no role) may claim only capability-less tasks —
|
|
101
101
|
* legacy behavior survives only where it is safe.
|
|
102
102
|
* - Otherwise: every required capability must be in the role's set.
|
|
103
|
+
* - The role capability '*' is the GENERALIST wildcard: it satisfies any
|
|
104
|
+
* requirement (the swarm's default binding; explicit is better than
|
|
105
|
+
* implicit unrestrictedness — a generalist role is a declared contract).
|
|
103
106
|
* Pure predicate — the queue's claim transaction calls this.
|
|
104
107
|
*/
|
|
105
108
|
export function canClaim(workerId, taskCapabilities) {
|
|
@@ -107,6 +110,7 @@ export function canClaim(workerId, taskCapabilities) {
|
|
|
107
110
|
if (required.length === 0) return true; // open task
|
|
108
111
|
const role = roleOf(workerId);
|
|
109
112
|
if (!role) return false;
|
|
113
|
+
if (role.capabilities.includes('*')) return true; // generalist wildcard
|
|
110
114
|
return required.every((c) => role.capabilities.includes(c));
|
|
111
115
|
}
|
|
112
116
|
|