runward 0.14.1 → 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.
Files changed (56) hide show
  1. package/README.md +9 -3
  2. package/dist/cli.js +27 -2
  3. package/dist/commands/check.js +72 -21
  4. package/dist/commands/compliance.js +20 -8
  5. package/dist/commands/doctor.js +6 -1
  6. package/dist/commands/init.js +9 -2
  7. package/dist/commands/manifest.js +72 -0
  8. package/dist/commands/rules.js +76 -0
  9. package/dist/commands/update.js +5 -1
  10. package/dist/lib/compliance.js +40 -50
  11. package/dist/lib/conformance.js +61 -15
  12. package/dist/lib/constants.js +1 -1
  13. package/dist/lib/evidence.js +222 -0
  14. package/dist/lib/manifest-sync.js +111 -0
  15. package/dist/lib/paths.js +2 -0
  16. package/dist/lib/regimes.js +32 -0
  17. package/dist/lib/rules.js +44 -0
  18. package/dist/lib/tools.js +1 -1
  19. package/dist/lib/verify-findings.js +24 -0
  20. package/examples/request-triage/code/README.md +74 -0
  21. package/examples/request-triage/code/package.json +19 -0
  22. package/examples/request-triage/code/src/adapters/hardcoded-account-registry.adapter.ts +23 -0
  23. package/examples/request-triage/code/src/adapters/in-memory-routing.adapter.ts +79 -0
  24. package/examples/request-triage/code/src/adapters/in-memory-triage-log.adapter.ts +42 -0
  25. package/examples/request-triage/code/src/adapters/keyword-model.adapter.ts +57 -0
  26. package/examples/request-triage/code/src/core/application/triage-request.usecase.ts +195 -0
  27. package/examples/request-triage/code/src/core/domain/guard.ts +121 -0
  28. package/examples/request-triage/code/src/core/domain/triage.ts +88 -0
  29. package/examples/request-triage/code/src/core/ports/account-registry.port.ts +15 -0
  30. package/examples/request-triage/code/src/core/ports/model-provider.port.ts +26 -0
  31. package/examples/request-triage/code/src/core/ports/routing.port.ts +32 -0
  32. package/examples/request-triage/code/src/core/ports/triage-log.port.ts +33 -0
  33. package/examples/request-triage/code/src/demo.ts +80 -0
  34. package/examples/request-triage/code/test/triage.test.ts +243 -0
  35. package/examples/request-triage/code/tsconfig.json +14 -0
  36. package/examples/request-triage/runward/adr/ADR-0003-port-placement-and-sovereignty.md +44 -0
  37. package/examples/request-triage/runward/architecture.md +4 -3
  38. package/examples/request-triage/runward/decision-matrix.md +1 -1
  39. package/examples/request-triage/runward/execution-topology.md +3 -3
  40. package/examples/request-triage/runward/floor.md +4 -4
  41. package/package.json +20 -8
  42. package/regimes/eu-ai-act@2024-1689.json +31 -0
  43. package/regimes/iso-42001@2023.json +22 -0
  44. package/regimes/nist-ai-rmf@1.0.json +19 -0
  45. package/templates/adapters/README.md +4 -0
  46. package/templates/adapters/gitlab-ci.yml +18 -0
  47. package/templates/mission/architecture.md +2 -0
  48. package/templates/mission/execution-topology.md +2 -0
  49. package/templates/mission/floor.md +2 -0
  50. package/templates/mission/threat-model.md +2 -0
  51. package/templates/rules/frontier-deterministic-boundary.md +1 -0
  52. package/templates/targets/AGENTS.md +1 -1
  53. package/templates/workflows/floor.md +1 -1
  54. package/templates/workflows/verify.md +2 -2
  55. package/examples/request-triage/runward/compliance/eu-ai-act-readiness.md +0 -89
  56. package/examples/request-triage/runward/compliance/oscal-component-definition.json +0 -209
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 a `file:line`/test, deviated with an ADR, or reasoned `n/a`). **Deterministic and zero-LLM**: it proves a decision was traced, never the code's quality, and cannot be jailbroken by injection — rerun it and get the same verdict |
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 floor rule-conformance manifest (deterministic)")
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,
@@ -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 floor rule-conformance manifest (see
12
- * docs/adr/ADR-0001): every CRITICAL/HIGH rule mapped to the floor phase must be
13
- * accounted for. It checks the presence of a traced decision, never the quality
14
- * of the implementation that stays the operator's judgment at the gate.
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 drift = [];
67
- for (const { phase, deliverable, label } of CONFORMANCE) {
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
- if (drift.length > 0) {
89
- console.log(section("Drift (advisory)"));
90
- for (const d of drift)
91
- console.log(` ${c.warning("◑")} ${c.white(d)}`);
92
- console.log(" " + c.darkGray("advisory — an applied pointer no longer resolves; verify it. Does not fail the gate."));
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,4 +189,32 @@ 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
+ }
211
+ // Transmission surface: name the next gesture, so the operating agent can hand the human a decision.
212
+ console.log(section("Next"));
213
+ if (gaps === 0 && strictGaps === 0 && hookFailed === 0) {
214
+ console.log(` Assemble the evidence pack with ${c.primary("runward compliance <regime>")} ${c.darkGray("(iso-42001 · nist-ai-rmf · eu-ai-act), or")} ${c.primary("runward status")} ${c.darkGray("for a handover snapshot.")}`);
215
+ }
216
+ else {
217
+ console.log(` Fill the deliverable(s) named above, then re-run ${c.primary("runward check")}. ${c.primary("runward status")} ${c.darkGray("names exactly what is open at the current gate.")}`);
218
+ }
219
+ console.log();
169
220
  }
@@ -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
- * Exit codes: 0 = draft written, 2 = no mission / unsupported regime.
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": { label: "ISO/IEC 42001", render: renderIso42001Readiness, file: "iso-42001-readiness.md" },
16
- "nist-ai-rmf": { label: "NIST AI RMF", render: renderNistAiRmf, file: "nist-ai-rmf-readiness.md" },
17
- "eu-ai-act": { label: "EU AI Act (Annex IV)", render: renderEuAiAct, file: "eu-ai-act-readiness.md" },
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`)}`);
@@ -5,7 +5,7 @@ import { TEMPLATES, MISSION_LAYOUT, VERSION, WORKFLOWS } from "../lib/paths.js";
5
5
  import { EXPECTED_RULES, EXPECTED_MAPPED, EXPECTED_ADAPTERS } from "../lib/constants.js";
6
6
  import { expectedRules } from "../lib/conformance.js";
7
7
  import { findMissionRoot } from "../lib/mission.js";
8
- import { createHeader, section, status } from "../lib/styles.js";
8
+ import { c, createHeader, section, status } from "../lib/styles.js";
9
9
  /**
10
10
  * Environment and installation checks.
11
11
  * Exit codes: 0 = all good, 1 = warnings, 2 = critical failure.
@@ -80,5 +80,10 @@ export async function doctorCommand() {
80
80
  }
81
81
  else
82
82
  console.log(" " + status.success("all checks passed"));
83
+ // Transmission surface: name the next gesture.
84
+ console.log(section("Next"));
85
+ console.log(" " + (root
86
+ ? `Run ${c.primary("runward check")} to see which gate this mission is at.`
87
+ : `Run ${c.primary("runward init")} to scaffold a mission, then ${c.primary("runward check")} to see where you stand.`));
83
88
  console.log();
84
89
  }
@@ -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
+ }
@@ -45,5 +45,9 @@ export async function updateCommand(opts) {
45
45
  }
46
46
  console.log(section("Summary"));
47
47
  console.log(` ${status.success(`${same} up to date`)} ${added ? status.info(`${added} added`) + " " : ""}${drifted ? status.warning(`${drifted} drifted${opts.force ? " (overwritten)" : ""}`) : ""}`);
48
- console.log(c.darkGray("\n Mission state (framing, architecture, ADRs, governance) is never touched by update.\n"));
48
+ console.log(c.darkGray("\n Mission state (framing, architecture, ADRs, governance) is never touched by update."));
49
+ // Transmission surface: name the next gesture.
50
+ console.log(section("Next"));
51
+ console.log(` Re-run ${c.primary("runward check")} to re-verify the gate against the refreshed rules and workflows.`);
52
+ console.log();
49
53
  }