runward 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/cli.js +27 -2
- package/dist/commands/check.js +63 -21
- package/dist/commands/compliance.js +20 -8
- package/dist/commands/init.js +9 -2
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/rules.js +76 -0
- package/dist/lib/compliance.js +40 -50
- package/dist/lib/conformance.js +61 -15
- package/dist/lib/constants.js +1 -1
- package/dist/lib/evidence.js +222 -0
- package/dist/lib/manifest-sync.js +111 -0
- package/dist/lib/paths.js +2 -0
- package/dist/lib/regimes.js +32 -0
- package/dist/lib/rules.js +44 -0
- package/dist/lib/tools.js +1 -1
- package/dist/lib/verify-findings.js +24 -0
- package/examples/request-triage/code/README.md +74 -0
- package/examples/request-triage/code/package.json +19 -0
- package/examples/request-triage/code/src/adapters/hardcoded-account-registry.adapter.ts +23 -0
- package/examples/request-triage/code/src/adapters/in-memory-routing.adapter.ts +79 -0
- package/examples/request-triage/code/src/adapters/in-memory-triage-log.adapter.ts +42 -0
- package/examples/request-triage/code/src/adapters/keyword-model.adapter.ts +57 -0
- package/examples/request-triage/code/src/core/application/triage-request.usecase.ts +195 -0
- package/examples/request-triage/code/src/core/domain/guard.ts +121 -0
- package/examples/request-triage/code/src/core/domain/triage.ts +88 -0
- package/examples/request-triage/code/src/core/ports/account-registry.port.ts +15 -0
- package/examples/request-triage/code/src/core/ports/model-provider.port.ts +26 -0
- package/examples/request-triage/code/src/core/ports/routing.port.ts +32 -0
- package/examples/request-triage/code/src/core/ports/triage-log.port.ts +33 -0
- package/examples/request-triage/code/src/demo.ts +80 -0
- package/examples/request-triage/code/test/triage.test.ts +243 -0
- package/examples/request-triage/code/tsconfig.json +14 -0
- package/examples/request-triage/runward/adr/ADR-0003-port-placement-and-sovereignty.md +44 -0
- package/examples/request-triage/runward/architecture.md +4 -3
- package/examples/request-triage/runward/decision-matrix.md +1 -1
- package/examples/request-triage/runward/execution-topology.md +3 -3
- package/examples/request-triage/runward/floor.md +4 -4
- package/package.json +20 -8
- package/regimes/eu-ai-act@2024-1689.json +31 -0
- package/regimes/iso-42001@2023.json +22 -0
- package/regimes/nist-ai-rmf@1.0.json +19 -0
- package/templates/adapters/README.md +4 -0
- package/templates/adapters/gitlab-ci.yml +18 -0
- package/templates/mission/architecture.md +2 -0
- package/templates/mission/execution-topology.md +2 -0
- package/templates/mission/floor.md +2 -0
- package/templates/mission/threat-model.md +2 -0
- package/templates/rules/frontier-deterministic-boundary.md +1 -0
- package/templates/targets/AGENTS.md +1 -1
- package/templates/workflows/floor.md +1 -1
- package/templates/workflows/verify.md +2 -2
package/README.md
CHANGED
|
@@ -61,12 +61,16 @@ New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
|
61
61
|
|---|---|
|
|
62
62
|
| `runward init` | Scaffold the mission structure (wizard: entry mode, stopping tier, tool profiles) |
|
|
63
63
|
| `runward check` | **Gate audit**: which deliverable, expected at which phase, is missing, started, or filled — exit code 1 on gaps |
|
|
64
|
-
| `runward check --strict` | **Conformance gate**: verifies each phase's rule-conformance manifest — every CRITICAL/HIGH craft rule mapped to the phase is accounted for (applied with
|
|
64
|
+
| `runward check --strict` | **Conformance gate**: verifies each phase's rule-conformance manifest — every CRITICAL/HIGH craft rule mapped to the phase is accounted for (applied with evidence, deviated with an ADR, or reasoned `n/a`) — **and verifies the evidence itself**: typed pointers (`file:PATH[:LINE][#SYMBOL]`, `test:PATH[::NAME]`, `adr:NNNN`) must resolve to real, non-empty content; a signed rule's evidence must match the rule's `signature:`; a stale pointer (drift) fails the gate. **Deterministic and zero-LLM**: it proves a decision was traced and points at something real, never the code's quality, and cannot be jailbroken by injection — rerun it and get the same verdict |
|
|
65
|
+
| `runward check --freeze` | **Seal a green gate**: hashes every resolvable evidence file into `runward/evidence-lock.json` (SHA-256, committed). A sealed file that later changes or disappears fails `check --strict` until you re-verify and re-seal — silent post-merge erosion becomes loud. Refuses to seal a red gate |
|
|
66
|
+
| `runward manifest` | **Conformance-table plumbing**: overview of the four gated manifests; `--sync` scaffolds the *form* — missing rows appended with an empty status (the gate refuses them until you decide), renamed rules migrated in place — and never touches the *content*: no status set, no evidence written, no row deleted |
|
|
67
|
+
| `runward rules` | **The rule set as data**: the effective set (your mission's copy, else the package's) with impact, phases, OWASP ASI and signatures; `--json` is a stable, versioned machine contract for dashboards, editor extensions and CI annotations |
|
|
68
|
+
| `runward explain <rule>` | **The why, inline**: a rule's contract (impact, phases, ASI, signature, rationale) and its full text — no doctrine excavation; renamed slugs answer with their migration |
|
|
65
69
|
| `runward status` | Mission snapshot: current gate, decision journal (ADRs), workflows |
|
|
66
70
|
| `runward doctor` | Environment and installation checks |
|
|
67
71
|
| `runward update` | Refresh `runward/workflows/` from the package — mission state never touched, local edits preserved unless `--force` |
|
|
68
72
|
| `runward characterize` | **Read-only inventory** of an existing codebase (brownfield / retro-documentation) → `runward/characterization.md`: dependencies and lockfiles, entrypoints, CI, tests, git-log shape. Deterministic and zero-LLM; it parses artifacts at rest and never runs, builds, or writes to your code. Facts, not decisions — reconstructing the *why* stays yours |
|
|
69
|
-
| `runward compliance <regime>` | **Assemble a regime-framed evidence pack** (`iso-42001`, `nist-ai-rmf`, `eu-ai-act`) from the mission's artifacts → `runward/compliance/…`: a human readiness *draft* (OWASP ASI coverage, rule-conformance status, ADR journal, with the operator-only sections flagged) **and a machine-readable OSCAL** component-definition to feed your GRC/auditor tool. Deterministic, read-only, zero-LLM — supporting evidence, never a compliance claim |
|
|
73
|
+
| `runward compliance <regime>` | **Assemble a regime-framed evidence pack** (`iso-42001`, `nist-ai-rmf`, `eu-ai-act`) from the mission's artifacts → `runward/compliance/…`: a human readiness *draft* (OWASP ASI coverage, rule-conformance status, ADR journal, with the operator-only sections flagged) **and a machine-readable OSCAL** component-definition to feed your GRC/auditor tool. Deterministic, read-only, zero-LLM — supporting evidence, never a compliance claim. The regime mapping is **versioned data** (`--regime-version` pins the lens; the pack stamps which version produced it) |
|
|
70
74
|
|
|
71
75
|
Global flags: `--yes`, `--dry-run`, `--verbose`, `--no-color`. Exit codes: 0 success, 1 gaps/warnings, 2 missing prerequisite.
|
|
72
76
|
|
|
@@ -93,7 +97,7 @@ your-project/
|
|
|
93
97
|
│ │ └── observability-schema.md
|
|
94
98
|
│ ├── contracts/ # port contracts (versioned, additive, tolerant reader)
|
|
95
99
|
│ ├── runbook.md # recovery runbook for the team that inherits the system
|
|
96
|
-
│ ├── adapters/ # inert samples that run the gate at each harness seam (git, CI, Claude Code)
|
|
100
|
+
│ ├── adapters/ # inert samples that run the gate at each harness seam (git, GitHub/GitLab CI, Claude Code)
|
|
97
101
|
│ └── workflows/ # the method, executable by your agent
|
|
98
102
|
└── .claude/ | .cursor/ # tool profiles (--tools)
|
|
99
103
|
```
|
|
@@ -116,6 +120,8 @@ Phase 5 is transverse: it starts at day zero, not after the incident.
|
|
|
116
120
|
## What makes it different
|
|
117
121
|
|
|
118
122
|
- **The gate you own — deterministic and zero-LLM.** `runward check --strict` is a non-probabilistic, operator-owned exit-code gate: it cannot be jailbroken by injection (no model in the gate path) and reruns byte-for-byte. Every competitor gates on LLM generation plus human prose review; this one does not depend on an LLM to pass.
|
|
123
|
+
- **The gate verifies the evidence, not just the row.** A pointer must resolve to real, non-empty content; a symbol or test name must actually be there; a rule can declare the shape its evidence must contain (`signature:` — the founding cited-not-applied incident is now caught deterministically); a stale pointer fails the gate; a sealed gate (`--freeze`) makes later erosion loud. Still bytes, never judgment — and never a model.
|
|
124
|
+
- **runward gates itself.** This repository carries its own `runward/` mission, and CI requires `runward check --strict` green on runward's own code — including under network isolation. The discipline is demonstrated, not claimed.
|
|
119
125
|
- **Audit-ready evidence, not compliance theatre.** Craft rules carry an OWASP ASI (Top 10 for Agentic Applications) mapping — a universal, region-agnostic security posture — so the conformance manifest reads as supporting evidence. Security-only by default; it maps to your regime when you need one — ISO/IEC 42001 (global), NIST AI RMF (US), or the EU AI Act art. 13 technical file (EU) — see [`docs/compliance/`](docs/compliance/). An input to that work — never a conformity assessment, never a claim to certify.
|
|
120
126
|
- **Never a runtime.** Runward writes nothing into `.git/`, installs nothing, and runs nothing of yours — it frames, gates and hands off. Your runtime and model are swappable adapters behind a port; the thing you depend on is never Runward.
|
|
121
127
|
- **Floor, not MVP deck.** The floor is the smallest *running* system that proves value on real traffic. A presentation is not a floor.
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Runward CLI — after the spec: ship and run.
|
|
4
|
-
* Commands: init (wizard), check (gate audit), status, doctor, update, characterize
|
|
4
|
+
* Commands: init (wizard), check (gate audit), status, doctor, update, characterize,
|
|
5
|
+
* compliance (evidence pack), manifest (table plumbing), rules / explain (rule-set surface).
|
|
5
6
|
*/
|
|
6
7
|
import { Command } from "commander";
|
|
7
8
|
import { VERSION } from "./lib/paths.js";
|
|
@@ -13,6 +14,8 @@ import { doctorCommand } from "./commands/doctor.js";
|
|
|
13
14
|
import { updateCommand } from "./commands/update.js";
|
|
14
15
|
import { characterizeCommand } from "./commands/characterize.js";
|
|
15
16
|
import { complianceCommand } from "./commands/compliance.js";
|
|
17
|
+
import { manifestCommand } from "./commands/manifest.js";
|
|
18
|
+
import { rulesCommand, explainCommand } from "./commands/rules.js";
|
|
16
19
|
import { TOOL_IDS } from "./lib/tools.js";
|
|
17
20
|
// Exit codes: 0 = success · 1 = gaps/warnings · 2 = missing prerequisite or CLI misuse (typo, unknown flag)
|
|
18
21
|
process.on("uncaughtException", (err) => {
|
|
@@ -61,7 +64,8 @@ program
|
|
|
61
64
|
.command("check")
|
|
62
65
|
.description("can I cross the gate — gate audit, exit 1 on gaps (CI-friendly)")
|
|
63
66
|
.option("-p, --path <path>", "project directory")
|
|
64
|
-
.option("--strict", "also verify the
|
|
67
|
+
.option("--strict", "also verify the rule-conformance manifests: rows, typed pointers, signatures, drift, seal (deterministic)")
|
|
68
|
+
.option("--freeze", "seal a green strict gate: hash the evidence into runward/evidence-lock.json (implies --strict)")
|
|
65
69
|
.option("--hooks", "run operator hooks from runward/hooks.json around the audit (opt-in)")
|
|
66
70
|
.option("--coverage", "advisory: report deliverable + decision-ratification coverage (does not gate)")
|
|
67
71
|
.action(checkCommand);
|
|
@@ -86,11 +90,32 @@ program
|
|
|
86
90
|
.option("-p, --path <path>", "project directory (default: .)")
|
|
87
91
|
.option("--mine", "also propose candidate retroactive ADRs as DRAFT hypotheses (deterministic git archaeology, no model call)")
|
|
88
92
|
.action(characterizeCommand);
|
|
93
|
+
program
|
|
94
|
+
.command("manifest")
|
|
95
|
+
.description("rule-conformance manifest overview; --sync scaffolds missing rows and migrates renamed slugs (form only, never content)")
|
|
96
|
+
.option("-p, --path <path>", "project directory")
|
|
97
|
+
.option("--sync", "write: append missing rows with an empty status, rewrite renamed slugs, create missing sections")
|
|
98
|
+
.action(manifestCommand);
|
|
99
|
+
program
|
|
100
|
+
.command("rules")
|
|
101
|
+
.description("the effective rule set (mission copy, else package); --json is a stable machine contract")
|
|
102
|
+
.option("-p, --path <path>", "project directory")
|
|
103
|
+
.option("--json", "machine output: { runward, source, count, rules } sorted by slug (versioned, additive)")
|
|
104
|
+
.option("--phase <id>", "only the rules mapped to this phase (architect | topology | floor | govern)")
|
|
105
|
+
.action(rulesCommand);
|
|
106
|
+
program
|
|
107
|
+
.command("explain")
|
|
108
|
+
.description("print a rule's contract (impact, phases, why, signature) and its full text — the rationale inline")
|
|
109
|
+
.argument("<rule>", "rule slug (see `runward rules`)")
|
|
110
|
+
.option("-p, --path <path>", "project directory")
|
|
111
|
+
.option("--json", "machine output: the rule plus its body")
|
|
112
|
+
.action(explainCommand);
|
|
89
113
|
program
|
|
90
114
|
.command("compliance")
|
|
91
115
|
.description("assemble a regime-framed evidence pack from the mission (deterministic, read-only; a readiness draft, never a compliance claim)")
|
|
92
116
|
.argument("[regime]", "iso-42001 | nist-ai-rmf | eu-ai-act")
|
|
93
117
|
.option("-p, --path <path>", "project directory")
|
|
118
|
+
.option("--regime-version <version>", "regime mapping version (default: highest shipped, see regimes/)")
|
|
94
119
|
.action(complianceCommand);
|
|
95
120
|
// exitOverride lets us map Commander's own errors onto runward's exit-code contract:
|
|
96
121
|
// a parse error (unknown command/option, missing/excess argument) is operator misuse → 2,
|
package/dist/commands/check.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
1
2
|
import { join, resolve } from "node:path";
|
|
2
3
|
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
3
|
-
import { conformance, driftReport, unratifiedAdrs, decisionCoverage } from "../lib/conformance.js";
|
|
4
|
+
import { conformance, driftReport, unratifiedAdrs, decisionCoverage, ruleSignatures, GATED_DELIVERABLES } from "../lib/conformance.js";
|
|
5
|
+
import { evidenceReport, verifyEvidenceLock, renderEvidenceLock, EVIDENCE_LOCK } from "../lib/evidence.js";
|
|
4
6
|
import { behavioralProof } from "../lib/behavioral-proof.js";
|
|
7
|
+
import { verifyFindings, VERIFY_FINDINGS } from "../lib/verify-findings.js";
|
|
5
8
|
import { runHooks } from "../lib/hooks.js";
|
|
6
|
-
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
9
|
+
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
7
10
|
import { VERSION } from "../lib/paths.js";
|
|
8
11
|
/**
|
|
9
12
|
* Gate audit — the gap analysis: which deliverable, expected at which
|
|
10
13
|
* phase, is present, started, or still a raw template.
|
|
11
|
-
* With --strict, also verifies the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* With --strict, also verifies the rule-conformance manifests (ADR-0001): every
|
|
15
|
+
* CRITICAL/HIGH rule mapped to a build phase must be accounted for, applied
|
|
16
|
+
* evidence must point at something real (ADR-0019/0020/0021 — typed pointers,
|
|
17
|
+
* non-vacuity, signatures, drift, seal). It checks the presence and shape of a
|
|
18
|
+
* traced decision, never the quality of the implementation — that stays the
|
|
19
|
+
* operator's judgment at the gate.
|
|
20
|
+
* With --freeze (implies --strict), a green gate is sealed: the resolvable
|
|
21
|
+
* evidence files are hashed into runward/evidence-lock.json (ADR-0021).
|
|
15
22
|
* Exit codes: 0 = current gate clean, 1 = gaps, 2 = no mission found.
|
|
16
23
|
*/
|
|
17
24
|
export async function checkCommand(opts) {
|
|
25
|
+
if (opts.freeze)
|
|
26
|
+
opts.strict = true; // a seal certifies a strict crossing
|
|
18
27
|
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
19
28
|
if (!root) {
|
|
20
29
|
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
|
@@ -55,19 +64,15 @@ export async function checkCommand(opts) {
|
|
|
55
64
|
}
|
|
56
65
|
let strictGaps = 0;
|
|
57
66
|
if (opts.strict) {
|
|
58
|
-
const CONFORMANCE = [
|
|
59
|
-
{ phase: "architect", deliverable: "architecture.md", label: "Architect" },
|
|
60
|
-
{ phase: "topology", deliverable: "execution-topology.md", label: "Topology" },
|
|
61
|
-
{ phase: "floor", deliverable: "floor.md", label: "Floor" },
|
|
62
|
-
{ phase: "govern", deliverable: "governance/threat-model.md", label: "Govern" },
|
|
63
|
-
];
|
|
64
67
|
console.log(section("Rule conformance (--strict)"));
|
|
65
68
|
let checked = 0;
|
|
66
|
-
const
|
|
67
|
-
for (const { phase, deliverable, label } of
|
|
68
|
-
for (const d of driftReport(mission, deliverable))
|
|
69
|
-
drift.push(`${label} · ${d.rule} — ${d.problem}`);
|
|
69
|
+
const signatures = ruleSignatures(mission);
|
|
70
|
+
for (const { phase, deliverable, label } of GATED_DELIVERABLES) {
|
|
70
71
|
const { expected, violations } = conformance(mission, phase, deliverable);
|
|
72
|
+
// The evidence layer (ADR-0019/0020) and drift (blocking since ADR-0021) join the
|
|
73
|
+
// same verdict: a traced decision whose pointer is hollow or stale is a gap.
|
|
74
|
+
violations.push(...evidenceReport(mission, deliverable, signatures));
|
|
75
|
+
violations.push(...driftReport(mission, deliverable));
|
|
71
76
|
// Non-vacuity (ADR-0002): when no rules are currently mapped to a phase, conformance()
|
|
72
77
|
// still raises a `(mapping)` violation if the mapping was stripped below its pinned
|
|
73
78
|
// floor. Only skip when there is genuinely nothing to report — never discard that signal.
|
|
@@ -85,11 +90,20 @@ export async function checkCommand(opts) {
|
|
|
85
90
|
}
|
|
86
91
|
if (checked === 0)
|
|
87
92
|
console.log(" " + c.darkGray("no CRITICAL/HIGH rules mapped to a build phase"));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// Under --freeze the old seal is being replaced, not verified — otherwise a changed
|
|
94
|
+
// sealed file would make re-sealing impossible (the seal violation reddens the gate
|
|
95
|
+
// that freeze requires green). Everything else must still be green to seal.
|
|
96
|
+
const seal = opts.freeze ? { present: false, count: 0, violations: [] } : verifyEvidenceLock(mission);
|
|
97
|
+
if (seal.present) {
|
|
98
|
+
console.log(section("Evidence seal (--strict)"));
|
|
99
|
+
if (seal.violations.length === 0) {
|
|
100
|
+
console.log(` ${status.success(`seal intact — ${seal.count} evidence file(s), sealed ${seal.sealedAt ?? "?"}`)}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
for (const v of seal.violations)
|
|
104
|
+
console.log(` ${c.error("✗")} ${c.white(v.rule)}${c.darkGray(" — " + v.problem)}`);
|
|
105
|
+
strictGaps += seal.violations.length;
|
|
106
|
+
}
|
|
93
107
|
}
|
|
94
108
|
const unratified = unratifiedAdrs(mission);
|
|
95
109
|
if (unratified.length > 0) {
|
|
@@ -123,6 +137,15 @@ export async function checkCommand(opts) {
|
|
|
123
137
|
}
|
|
124
138
|
console.log(section("Semantic check (advisory, above the gate)"));
|
|
125
139
|
console.log(" " + c.darkGray("the gate proved every CRITICAL/HIGH rule was traced — not that the code applies it. Before you cross, run the verify workflow (runward/workflows/verify.md): an adversarial cite-vs-apply pass, ideally on a different model. Advisory, agent-executed, never blocks the gate (ADR-0007)."));
|
|
140
|
+
const vf = verifyFindings(mission);
|
|
141
|
+
if (!vf.present) {
|
|
142
|
+
console.log(" " + c.darkGray(`no verify findings recorded yet — the workflow writes them to runward/${VERIFY_FINDINGS}.`));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const freshness = vf.fresh ? c.success(" · fresh") : c.warning(" · stale (a gated manifest changed since — re-run the verify workflow)");
|
|
146
|
+
console.log(` ${c.success("✓")} ${c.darkGray("verify findings")} ${c.white(`runward/${VERIFY_FINDINGS}`)} ${c.darkGray(`(${vf.date})`)}${freshness}`);
|
|
147
|
+
console.log(" " + c.darkGray("advisory — runward reports presence and freshness, never reads a verdict. The findings inform your crossing; they never gate it."));
|
|
148
|
+
}
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
if (opts.coverage) {
|
|
@@ -166,6 +189,25 @@ export async function checkCommand(opts) {
|
|
|
166
189
|
console.log("\n" + status.warning(`${parts.join(" · ")}. No phase closes without its artifact — and, under --strict, without its CRITICAL/HIGH rules accounted for.`));
|
|
167
190
|
process.exitCode = 1;
|
|
168
191
|
}
|
|
192
|
+
if (opts.freeze) {
|
|
193
|
+
console.log(section("Evidence seal — freeze (ADR-0021)"));
|
|
194
|
+
if (gaps || strictGaps || hookFailed) {
|
|
195
|
+
console.log(" " + status.error("refusing to seal a red gate — a seal certifies a crossing, not a hope. Close the gaps above, then re-run `runward check --freeze`."));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const sealedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
|
|
199
|
+
const content = renderEvidenceLock(mission, sealedAt);
|
|
200
|
+
const count = Object.keys(JSON.parse(content).files).length;
|
|
201
|
+
if (process.env.RUNWARD_DRY_RUN === "1") {
|
|
202
|
+
console.log(" " + c.darkGray(`dry-run — would seal ${count} evidence file(s) into runward/${EVIDENCE_LOCK}`));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
writeFileSync(join(mission, EVIDENCE_LOCK), content);
|
|
206
|
+
console.log(` ${status.success(`sealed ${count} evidence file(s) into runward/${EVIDENCE_LOCK} — commit it`)}`);
|
|
207
|
+
console.log(" " + c.darkGray("a sealed file that later changes or disappears fails `check --strict` until you re-verify and re-seal."));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
169
211
|
// Transmission surface: name the next gesture, so the operating agent can hand the human a decision.
|
|
170
212
|
console.log(section("Next"));
|
|
171
213
|
if (gaps === 0 && strictGaps === 0 && hookFailed === 0) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, join, resolve } from "node:path";
|
|
2
2
|
import { findMissionRoot } from "../lib/mission.js";
|
|
3
3
|
import { gatherComplianceInputs, renderIso42001Readiness, renderNistAiRmf, renderEuAiAct, renderOscal } from "../lib/compliance.js";
|
|
4
|
+
import { loadRegime, regimeLensId } from "../lib/regimes.js";
|
|
4
5
|
import { makeWriter } from "../lib/write.js";
|
|
5
6
|
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
6
7
|
import { VERSION } from "../lib/paths.js";
|
|
@@ -8,23 +9,34 @@ import { VERSION } from "../lib/paths.js";
|
|
|
8
9
|
* Assemble a regime-framed compliance evidence pack (ADR-0016) — deterministic, read-only, zero-LLM,
|
|
9
10
|
* outside the gate. Reads the mission's real artifacts (rule→OWASP ASI, conformance manifests, ADR
|
|
10
11
|
* journal, governance docs) and writes an assessment-readiness *draft*, explicitly flagging what only
|
|
11
|
-
* the operator can supply. Never a compliance claim.
|
|
12
|
-
*
|
|
12
|
+
* the operator can supply. Never a compliance claim. The regime mapping is versioned data (ADR-0022):
|
|
13
|
+
* `--regime-version` pins the lens; the selected lens is stamped into the draft and the OSCAL.
|
|
14
|
+
* Exit codes: 0 = draft written, 2 = no mission / unsupported regime / unknown mapping version.
|
|
13
15
|
*/
|
|
14
16
|
const REGIMES = {
|
|
15
|
-
"iso-42001": {
|
|
16
|
-
"nist-ai-rmf": {
|
|
17
|
-
"eu-ai-act": {
|
|
17
|
+
"iso-42001": { render: renderIso42001Readiness, file: "iso-42001-readiness.md" },
|
|
18
|
+
"nist-ai-rmf": { render: renderNistAiRmf, file: "nist-ai-rmf-readiness.md" },
|
|
19
|
+
"eu-ai-act": { render: renderEuAiAct, file: "eu-ai-act-readiness.md" },
|
|
18
20
|
};
|
|
19
21
|
export async function complianceCommand(regime, opts) {
|
|
20
22
|
const key = (regime ?? "").toLowerCase();
|
|
21
|
-
console.log(createHeader(`Runward v${VERSION} — compliance`, key ? REGIMES[key]?.label ?? key : "regime required"));
|
|
22
23
|
if (!key || !(key in REGIMES)) {
|
|
24
|
+
console.log(createHeader(`Runward v${VERSION} — compliance`, key || "regime required"));
|
|
23
25
|
console.error(status.error(`Usage: runward compliance <regime>. Supported: ${Object.keys(REGIMES).join(", ")}.`));
|
|
24
26
|
console.log(" " + c.darkGray("The manifest is universal (OWASP ASI); the regime is a lens (ADR-0015). Default posture is security-only — no regime named."));
|
|
25
27
|
process.exit(2);
|
|
26
28
|
}
|
|
27
29
|
const spec = REGIMES[key];
|
|
30
|
+
let lens;
|
|
31
|
+
try {
|
|
32
|
+
lens = loadRegime(key, opts.regimeVersion);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.log(createHeader(`Runward v${VERSION} — compliance`, key));
|
|
36
|
+
console.error(status.error(e instanceof Error ? e.message : String(e)));
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
console.log(createHeader(`Runward v${VERSION} — compliance`, `${lens.label} — mapping version ${lens.version}`));
|
|
28
40
|
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
29
41
|
if (!root) {
|
|
30
42
|
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
|
@@ -35,11 +47,11 @@ export async function complianceCommand(regime, opts) {
|
|
|
35
47
|
const generatedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
|
|
36
48
|
console.log(section("Assembling (read-only, deterministic)"));
|
|
37
49
|
const inputs = gatherComplianceInputs(mission);
|
|
38
|
-
const md = spec.render(inputs, generatedAt);
|
|
50
|
+
const md = spec.render(inputs, generatedAt, lens);
|
|
39
51
|
const w = makeWriter({ force: true, dryRun, root }); // generated artifacts — always refresh
|
|
40
52
|
w.write(join(mission, "compliance", spec.file), md);
|
|
41
53
|
// OSCAL export — regime-neutral, machine-readable; the interop layer (ADR-0016) so the evidence flows into GRC/auditor tools.
|
|
42
|
-
w.write(join(mission, "compliance", "oscal-component-definition.json"), renderOscal(inputs, basename(root), generatedAt));
|
|
54
|
+
w.write(join(mission, "compliance", "oscal-component-definition.json"), renderOscal(inputs, basename(root), generatedAt, regimeLensId(lens)));
|
|
43
55
|
const mappedAsi = [...inputs.asiCoverage.values()].filter((v) => v.length).length;
|
|
44
56
|
console.log(section("Assembled"));
|
|
45
57
|
console.log(` ${c.primaryBold("ASI coverage")} ${c.white(`${mappedAsi}/10 categories mapped to a rule`)}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
-
import { readdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
4
|
-
import { TEMPLATES, EXAMPLE_MISSION, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
4
|
+
import { TEMPLATES, EXAMPLE_MISSION, EXAMPLE_CODE, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
5
5
|
import { TOOL_PROFILES, TOOL_IDS, baselineSkills } from "../lib/tools.js";
|
|
6
6
|
import { makeWriter } from "../lib/write.js";
|
|
7
7
|
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
8
8
|
/** Recursively copy a shipped directory tree (used to lay down the filled reference mission). */
|
|
9
|
+
const COPY_SKIP = new Set(["node_modules", "package-lock.json", ".DS_Store"]);
|
|
9
10
|
function copyTree(w, srcDir, destDir) {
|
|
10
11
|
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
12
|
+
if (COPY_SKIP.has(entry.name))
|
|
13
|
+
continue;
|
|
11
14
|
const src = join(srcDir, entry.name);
|
|
12
15
|
const dest = join(destDir, entry.name);
|
|
13
16
|
if (entry.isDirectory())
|
|
@@ -64,6 +67,10 @@ export async function initCommand(opts) {
|
|
|
64
67
|
if (example) {
|
|
65
68
|
// Lay down the filled request-triage reference: every gate is green out of the box.
|
|
66
69
|
copyTree(w, EXAMPLE_MISSION, mission);
|
|
70
|
+
// The manifests carry verified evidence pointers into code/ (ADR-0019): ship the reference
|
|
71
|
+
// floor with them, so the gate is green because the evidence resolves — not unchecked.
|
|
72
|
+
if (existsSync(EXAMPLE_CODE))
|
|
73
|
+
copyTree(w, EXAMPLE_CODE, join(root, "code"));
|
|
67
74
|
// The reference omits the three non-gated scaffolding notes; add them as blank templates so the scaffold stays complete.
|
|
68
75
|
for (const extra of ["reference-stack.md", "shared-bricks.md", "gap-analysis.md"]) {
|
|
69
76
|
w.copy(join(TEMPLATES, "mission", extra), join(mission, extra));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { findMissionRoot } from "../lib/mission.js";
|
|
4
|
+
import { GATED_DELIVERABLES } from "../lib/conformance.js";
|
|
5
|
+
import { syncManifest } from "../lib/manifest-sync.js";
|
|
6
|
+
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
7
|
+
import { VERSION } from "../lib/paths.js";
|
|
8
|
+
/**
|
|
9
|
+
* Rule-conformance manifest plumbing (ADR-0023). Read-only overview by default;
|
|
10
|
+
* --sync scaffolds the FORM (missing rows with empty status, renamed slugs migrated,
|
|
11
|
+
* missing sections created) and never touches the content: no status set, no evidence
|
|
12
|
+
* written, no row deleted. The decision stays the operator's.
|
|
13
|
+
* Exit codes: 0 = done, 2 = no mission found.
|
|
14
|
+
*/
|
|
15
|
+
export async function manifestCommand(opts) {
|
|
16
|
+
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
17
|
+
if (!root) {
|
|
18
|
+
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
const mission = join(root, "runward");
|
|
22
|
+
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
23
|
+
console.log(createHeader(`Runward v${VERSION} — manifest ${opts.sync ? "sync" : "overview"}`, root));
|
|
24
|
+
let toFill = 0;
|
|
25
|
+
for (const { phase, deliverable, label } of GATED_DELIVERABLES) {
|
|
26
|
+
const r = syncManifest(mission, phase, deliverable, label);
|
|
27
|
+
console.log(section(`${label} (runward/${deliverable})`));
|
|
28
|
+
if (r.fileMissing) {
|
|
29
|
+
console.log(" " + c.darkGray("deliverable missing — `runward init` scaffolds it; sync only edits existing files"));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (r.migrated.length)
|
|
33
|
+
for (const m of r.migrated)
|
|
34
|
+
console.log(` ${c.warning("◑")} renamed slug: ${c.white(`${m.from} → ${m.to}`)}${opts.sync ? c.darkGray(" — rewritten, status and evidence untouched") : c.darkGray(" — --sync rewrites it")}`);
|
|
35
|
+
if (r.removed.length)
|
|
36
|
+
for (const m of r.removed)
|
|
37
|
+
console.log(` ${c.warning("◑")} ${c.white(m.slug)}${c.darkGray(` — removed in ${m.since} (${m.reason}); delete the row yourself`)}`);
|
|
38
|
+
if (r.duplicates.length)
|
|
39
|
+
for (const d of r.duplicates)
|
|
40
|
+
console.log(` ${c.warning("◑")} duplicate: ${c.white(d)}`);
|
|
41
|
+
if (r.unknown.length)
|
|
42
|
+
for (const u of r.unknown)
|
|
43
|
+
console.log(` ${c.warning("◑")} unknown rule slug: ${c.white(u)}${c.darkGray(" — typo? not in runward/rules/; fix or remove the row yourself")}`);
|
|
44
|
+
if (r.added.length) {
|
|
45
|
+
toFill += r.added.length;
|
|
46
|
+
const verb = opts.sync ? (r.sectionCreated ? "section created, rows scaffolded" : "rows scaffolded (empty status — the gate refuses them until you decide)") : "missing — --sync scaffolds the rows";
|
|
47
|
+
console.log(` ${opts.sync ? c.success("✓") : c.error("✗")} ${c.white(`${r.added.length} expected rule(s) not accounted for`)} ${c.darkGray("— " + verb)}`);
|
|
48
|
+
for (const a of r.added)
|
|
49
|
+
console.log(` ${c.darkGray("·")} ${c.white(a)}`);
|
|
50
|
+
}
|
|
51
|
+
if (!r.migrated.length && !r.removed.length && !r.duplicates.length && !r.unknown.length && !r.added.length) {
|
|
52
|
+
console.log(" " + status.success("table in sync with the mapped rule set"));
|
|
53
|
+
}
|
|
54
|
+
if (opts.sync && r.content !== null) {
|
|
55
|
+
if (dryRun)
|
|
56
|
+
console.log(" " + c.info("dry-run — would rewrite ") + c.white(`runward/${deliverable}`));
|
|
57
|
+
else
|
|
58
|
+
writeFileSync(join(mission, deliverable), r.content);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log(section("Next"));
|
|
62
|
+
if (opts.sync && toFill > 0) {
|
|
63
|
+
console.log(` Fill each scaffolded row — a status (${c.primary("applied | deviated | n/a")}) and its evidence (${c.primary("file:PATH[:LINE][#SYMBOL]")}, ${c.primary("test:PATH[::NAME]")}, ${c.primary("adr:NNNN")} or prose) — then ${c.primary("runward check --strict")}.`);
|
|
64
|
+
}
|
|
65
|
+
else if (!opts.sync && toFill > 0) {
|
|
66
|
+
console.log(` Run ${c.primary("runward manifest --sync")} to scaffold the missing rows, then fill them and ${c.primary("runward check --strict")}.`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(` Nothing to scaffold. ${c.primary("runward check --strict")} ${c.darkGray("verifies the decisions themselves.")}`);
|
|
70
|
+
}
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { findMissionRoot } from "../lib/mission.js";
|
|
4
|
+
import { readRuleSet, ruleSetDir, parseRule, ruleBody } from "../lib/rules.js";
|
|
5
|
+
import { RULE_MIGRATIONS } from "../lib/rule-migrations.js";
|
|
6
|
+
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
7
|
+
import { VERSION } from "../lib/paths.js";
|
|
8
|
+
/**
|
|
9
|
+
* The machine surface of the rule set (ADR-0024). `rules` lists the effective set
|
|
10
|
+
* (mission copy when present, else the package); `--json` is a versioned, additive
|
|
11
|
+
* contract: { runward, source, count, rules } sorted by slug. `explain <rule>` prints
|
|
12
|
+
* the rule's contract surface and its full body — the why, inline. Read-only, zero-LLM.
|
|
13
|
+
* Exit codes: 0 = ok, 2 = unknown rule slug.
|
|
14
|
+
*/
|
|
15
|
+
function effectiveDir(path) {
|
|
16
|
+
const root = findMissionRoot(resolve(process.cwd(), path ?? "."));
|
|
17
|
+
return ruleSetDir(root ? join(root, "runward") : null);
|
|
18
|
+
}
|
|
19
|
+
export async function rulesCommand(opts) {
|
|
20
|
+
const { dir, source } = effectiveDir(opts.path);
|
|
21
|
+
let rules = readRuleSet(dir);
|
|
22
|
+
if (opts.phase)
|
|
23
|
+
rules = rules.filter((r) => r.phases.includes(opts.phase));
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
// Versioned, additive contract (ADR-0024) — fields are added, never renamed or repurposed.
|
|
26
|
+
console.log(JSON.stringify({ runward: VERSION, source, count: rules.length, rules }, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(createHeader(`Runward v${VERSION} — rules`, `${rules.length} rule(s) · source: ${source}${opts.phase ? ` · phase: ${opts.phase}` : ""}`));
|
|
30
|
+
const phases = ["architect", "topology", "floor", "govern"];
|
|
31
|
+
for (const ph of opts.phase ? [opts.phase] : [...phases, ""]) {
|
|
32
|
+
const subset = ph === "" ? rules.filter((r) => r.phases.length === 0 || r.phases.every((p) => !phases.includes(p))) : rules.filter((r) => r.phases.includes(ph));
|
|
33
|
+
if (subset.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
console.log(section(ph === "" ? "Unmapped (advisory)" : `Phase · ${ph}`));
|
|
36
|
+
for (const r of subset) {
|
|
37
|
+
const sig = r.signature ? c.primary(" ·signed") : "";
|
|
38
|
+
console.log(` ${c.white(r.slug.padEnd(42))} ${c.darkGray(r.impact.padEnd(9))}${c.darkGray(r.asi.join(","))}${sig}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log(section("Next"));
|
|
42
|
+
console.log(` ${c.primary("runward explain <rule>")} ${c.darkGray("prints a rule's why and full text ·")} ${c.primary("runward rules --json")} ${c.darkGray("is the machine contract.")}`);
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
export async function explainCommand(slug, opts) {
|
|
46
|
+
const { dir, source } = effectiveDir(opts.path);
|
|
47
|
+
const file = join(dir, `${slug}.md`);
|
|
48
|
+
if (!existsSync(file)) {
|
|
49
|
+
const m = RULE_MIGRATIONS[slug];
|
|
50
|
+
if (m?.to)
|
|
51
|
+
console.error(status.error(`Unknown rule "${slug}" — renamed to '${m.to}' in ${m.since} (${m.reason}). Try: runward explain ${m.to}`));
|
|
52
|
+
else if (m)
|
|
53
|
+
console.error(status.error(`Unknown rule "${slug}" — removed in ${m.since} (${m.reason}).`));
|
|
54
|
+
else
|
|
55
|
+
console.error(status.error(`Unknown rule "${slug}" — not in ${source === "mission" ? "runward/rules/" : "the package rule set"}. \`runward rules\` lists the slugs.`));
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const content = readFileSync(file, "utf8");
|
|
59
|
+
const rule = parseRule(slug, content);
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
console.log(JSON.stringify({ runward: VERSION, source, rule: { ...rule, body: ruleBody(content) } }, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(createHeader(`Runward v${VERSION} — explain`, rule.title));
|
|
65
|
+
console.log(section("Contract"));
|
|
66
|
+
console.log(` ${c.primaryBold("Slug")} ${c.white(rule.slug)}`);
|
|
67
|
+
console.log(` ${c.primaryBold("Impact")} ${c.white(rule.impact)}${rule.phases.length ? c.darkGray(` · phases: ${rule.phases.join(", ")}`) : c.darkGray(" · advisory (no gated phase)")}`);
|
|
68
|
+
if (rule.asi.length)
|
|
69
|
+
console.log(` ${c.primaryBold("OWASP ASI")} ${c.white(rule.asi.join(", "))}`);
|
|
70
|
+
if (rule.signature)
|
|
71
|
+
console.log(` ${c.primaryBold("Signature")} ${c.white(`/${rule.signature}/i`)} ${c.darkGray("— applied evidence must point at content matching this (ADR-0020)")}`);
|
|
72
|
+
if (rule.why)
|
|
73
|
+
console.log(` ${c.primaryBold("Why")} ${c.white(rule.why)}`);
|
|
74
|
+
console.log(section("Rule"));
|
|
75
|
+
console.log(ruleBody(content));
|
|
76
|
+
}
|