runward 0.16.0 → 0.17.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 CHANGED
@@ -85,6 +85,7 @@ your-project/
85
85
  │ ├── framing.md # problem, value, observable success criterion, floor vs target
86
86
  │ ├── mission-contract.md # one-page steering contract: engagements, DoD, gates
87
87
  │ ├── architecture.md # boundaries, ports, integration protocol — stack stays open
88
+ │ ├── execution-topology.md # per-port placement: location family, data class, sovereignty (gated)
88
89
  │ ├── decision-matrix.md # 22 arbitrations: one sober default + one explicit trigger each
89
90
  │ ├── reference-stack.md # default adapter kit per layer, with evolution triggers
90
91
  │ ├── shared-bricks.md # bricks beyond the app: placement families, brick matrix, sovereignty by data class
@@ -98,7 +99,7 @@ your-project/
98
99
  │ │ └── observability-schema.md
99
100
  │ ├── contracts/ # port contracts (versioned, additive, tolerant reader)
100
101
  │ ├── runbook.md # recovery runbook for the team that inherits the system
101
- │ ├── adapters/ # inert samples that run the gate at each harness seam (git, GitHub/GitLab CI, Claude Code)
102
+ │ ├── adapters/ # inert samples that run the gate at each harness seam (git, GitHub/GitLab CI, Claude Code, Kiro, BMAD)
102
103
  │ └── workflows/ # the method, executable by your agent
103
104
  └── .claude/ | .cursor/ # tool profiles (--tools)
104
105
  ```
@@ -146,7 +147,7 @@ Read the doctrine: [Concevoir et exécuter des systèmes agentiques (FR)](https:
146
147
 
147
148
  Two vendor-neutral files are **always written**, so the method works on any agent with no profile at all: `AGENTS.md` (the standard charter read by Codex, Cursor, Copilot, Windsurf, Cline, Zed, Amp, opencode, Junie, Warp and more), and the **phase skills** at `.agents/skills/runward-<phase>/SKILL.md` — the converged `SKILL.md` seam read by 14+ harnesses, which surface each build phase's craft rules *by relevance* at the point of action (subordinate to the gate: a skill loaded but not applied still fails `check --strict`).
148
149
 
149
- Tool profiles (`--tools`) add harness-specific wiring on top: **Claude Code** (`/rw-*` commands + `.claude/skills/`), **Cursor**, **GitHub Copilot**, **Gemini CLI**, **Windsurf** (charter files), **Continue.dev** (`.continue/rules/`), **JetBrains Junie** and **Trae** (`.<harness>/skills/`). The mission structure itself is plain markdown in your repo. More profiles welcome (see [ROADMAP.md](ROADMAP.md)).
150
+ Tool profiles (`--tools`) add harness-specific wiring on top: **Claude Code** (`/rw-*` commands + `.claude/skills/`), **Cursor**, **GitHub Copilot**, **Gemini CLI**, **Windsurf** (charter files), **Continue.dev** (`.continue/rules/`), **JetBrains Junie** and **Trae** (`.<harness>/skills/`), and **Kiro** (`.kiro/steering/`, relevance-loaded). The mission structure itself is plain markdown in your repo. More profiles welcome (see [ROADMAP.md](ROADMAP.md)).
150
151
 
151
152
  ## License
152
153
 
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { buildInventory, renderCharacterization, mineDrafts, renderDraft } from "../lib/characterize.js";
4
4
  import { makeWriter } from "../lib/write.js";
5
- import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
5
+ import { c, createHeader, generationDate, section, status } from "../lib/styles.js";
6
6
  import { VERSION } from "../lib/paths.js";
7
7
  /**
8
8
  * Characterize an existing codebase (ADR-0014) — read-only, deterministic, zero-LLM.
@@ -22,7 +22,7 @@ export async function characterizeCommand(opts) {
22
22
  console.log(section("Reading (read-only)"));
23
23
  const inv = buildInventory(root);
24
24
  const dryRun = process.env.RUNWARD_DRY_RUN === "1";
25
- const generatedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
25
+ const generatedAt = generationDate();
26
26
  const md = renderCharacterization(inv, generatedAt);
27
27
  // Generated artifact, not mission state: always refresh it (idempotent).
28
28
  const w = makeWriter({ force: true, dryRun, root });
@@ -6,7 +6,7 @@ import { evidenceReport, verifyEvidenceLock, renderEvidenceLock, EVIDENCE_LOCK }
6
6
  import { behavioralProof } from "../lib/behavioral-proof.js";
7
7
  import { verifyFindings, VERIFY_FINDINGS } from "../lib/verify-findings.js";
8
8
  import { runHooks } from "../lib/hooks.js";
9
- import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
9
+ import { c, createHeader, generationDate, section, status } from "../lib/styles.js";
10
10
  import { VERSION } from "../lib/paths.js";
11
11
  /**
12
12
  * Gate audit — the gap analysis: which deliverable, expected at which
@@ -195,7 +195,7 @@ export async function checkCommand(opts) {
195
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
196
  }
197
197
  else {
198
- const sealedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
198
+ const sealedAt = generationDate();
199
199
  const content = renderEvidenceLock(mission, sealedAt);
200
200
  const count = Object.keys(JSON.parse(content).files).length;
201
201
  if (process.env.RUNWARD_DRY_RUN === "1") {
@@ -3,7 +3,7 @@ import { findMissionRoot } from "../lib/mission.js";
3
3
  import { gatherComplianceInputs, renderIso42001Readiness, renderNistAiRmf, renderEuAiAct, renderOscal } from "../lib/compliance.js";
4
4
  import { loadRegime, regimeLensId } from "../lib/regimes.js";
5
5
  import { makeWriter } from "../lib/write.js";
6
- import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
6
+ import { c, createHeader, generationDate, section, status } from "../lib/styles.js";
7
7
  import { VERSION } from "../lib/paths.js";
8
8
  /**
9
9
  * Assemble a regime-framed compliance evidence pack (ADR-0016) — deterministic, read-only, zero-LLM,
@@ -44,7 +44,7 @@ export async function complianceCommand(regime, opts) {
44
44
  }
45
45
  const mission = join(root, "runward");
46
46
  const dryRun = process.env.RUNWARD_DRY_RUN === "1";
47
- const generatedAt = isNonInteractive() && process.env.RUNWARD_NOW ? process.env.RUNWARD_NOW : new Date().toISOString().slice(0, 10);
47
+ const generatedAt = generationDate();
48
48
  console.log(section("Assembling (read-only, deterministic)"));
49
49
  const inputs = gatherComplianceInputs(mission);
50
50
  const md = spec.render(inputs, generatedAt, lens);
@@ -36,7 +36,8 @@ export async function doctorCommand() {
36
36
  const ruleCount = existsSync(rulesDir) ? readdirSync(rulesDir).filter((f) => f.endsWith(".md")).length : 0;
37
37
  ruleCount === EXPECTED_RULES ? ok(`${ruleCount} craft rules`) : fail(`craft rules mismatch: ${ruleCount}/${EXPECTED_RULES}`);
38
38
  const adaptersDir = join(TEMPLATES, "adapters");
39
- const adapterCount = existsSync(adaptersDir) ? readdirSync(adaptersDir).length : 0;
39
+ // Count real adapters, not the README — else substituting an adapter for a stray file passes.
40
+ const adapterCount = existsSync(adaptersDir) ? readdirSync(adaptersDir).filter((f) => f !== "README.md").length : 0;
40
41
  adapterCount === EXPECTED_ADAPTERS ? ok(`${adapterCount} gate adapters`) : fail(`gate adapters mismatch: ${adapterCount}/${EXPECTED_ADAPTERS}`);
41
42
  // Routed-count floor (ADR-0002): the phases: mapping must not be stripped below its pinned minimum.
42
43
  const belowFloor = Object.entries(EXPECTED_MAPPED).filter(([phase, floor]) => expectedRules(TEMPLATES, phase).length < floor);
@@ -34,7 +34,9 @@ function readRules(missionDir) {
34
34
  if (!existsSync(dir))
35
35
  return [];
36
36
  const out = [];
37
- for (const f of readdirSync(dir)) {
37
+ // Sorted: the ASI coverage lists and the OSCAL derive from this order, so it must be deterministic
38
+ // across filesystems (readdirSync order is not guaranteed) for the byte-identity invariant to hold.
39
+ for (const f of readdirSync(dir).sort()) {
38
40
  if (!f.endsWith(".md"))
39
41
  continue;
40
42
  let fm = "";
@@ -324,9 +326,15 @@ const ASI_CATALOG = "https://genai.owasp.org/resource/owasp-top-10-for-agentic-a
324
326
  */
325
327
  export function renderOscal(inputs, missionName, generatedAt, lensId) {
326
328
  const ns = missionName || "mission";
329
+ // The link points at the readiness draft co-generated with this pack — the one whose lens framed it.
330
+ const regime = lensId ? lensId.split("@")[0] : "iso-42001";
331
+ const readinessHref = `./${regime}-readiness.md`;
327
332
  const irs = Object.keys(ASI_LABELS).map((id) => {
328
333
  const slugs = inputs.asiCoverage.get(id) ?? [];
329
- const statuses = slugs.map((s) => inputs.conformance.find((c) => c.rule === s)?.status).filter(Boolean);
334
+ // Aggregate EVERY manifest row of EVERY rule mapping this ASI, across all gated deliverables —
335
+ // a rule can appear in more than one manifest (spec §3). `find` (first row) made the status depend
336
+ // on deliverable order and could report `implemented` where a later `deviated` row means `partial`.
337
+ const statuses = slugs.flatMap((s) => inputs.conformance.filter((c) => c.rule === s).map((c) => c.status));
330
338
  let impl;
331
339
  if (slugs.length === 0)
332
340
  impl = "planned"; // no rule maps this risk — a gap
@@ -339,7 +347,7 @@ export function renderOscal(inputs, missionName, generatedAt, lensId) {
339
347
  "control-id": `asi-${id.slice(3)}`,
340
348
  description: `${id} ${ASI_LABELS[id]}. ${slugs.length ? "Addressed by rules: " + slugs.join(", ") + "." : "No rule mapped — gap to assess."}`,
341
349
  props: [{ name: "implementation-status", value: impl }],
342
- links: [{ href: "./iso-42001-readiness.md", rel: "reference", text: "runward assessment-readiness draft" }],
350
+ links: [{ href: readinessHref, rel: "reference", text: "runward assessment-readiness draft" }],
343
351
  };
344
352
  });
345
353
  const doc = {
@@ -1,6 +1,9 @@
1
1
  /** Total number of craft rules shipped under templates/rules/. */
2
2
  export const EXPECTED_RULES = 64;
3
- /** Gate adapters shipped under templates/adapters/ (ADR-0012): one per harness seam, plus the port-contract README. */
3
+ /** Gate adapters shipped under templates/adapters/ (ADR-0012): one per harness seam. Counts the real
4
+ * adapters only — the port-contract README.md is excluded, so swapping an adapter for a stray file
5
+ * cannot pass the count. pre-commit, github-actions, gitlab-ci, claude-code-settings, kiro-hooks,
6
+ * bmad-review-layer. */
4
7
  export const EXPECTED_ADAPTERS = 6;
5
8
  /** Routed-count floor: minimum CRITICAL/HIGH rules mapped to each build phase (ADR-0002).
6
9
  * Lowering a floor is a deliberate, tracked edit — the `phases:` mapping cannot be silently
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { dirname, isAbsolute, join, relative } from "node:path";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { parseManifest, evidencePathTokens, adrIdExists, GATED_DELIVERABLES } from "./conformance.js";
5
5
  const POINTER_PREFIX = /\b(file|test|adr):(\S.*)$/;
6
6
  /** Parse the typed pointers out of an Evidence cell. One pointer per `;`-separated segment. */
@@ -53,16 +53,33 @@ export function parseEvidencePointers(evidence) {
53
53
  function clean(token) {
54
54
  return token.replace(/[),.:]+$/, "");
55
55
  }
56
+ /**
57
+ * A rule signature is operator-authored data the gate compiles into a RegExp and runs against file
58
+ * content — so a catastrophic-backtracking pattern (nested quantifiers like `(a+)+`) could hang the
59
+ * gate on adversarial input (a self-inflicted DoS in CI). Reject the known-dangerous shape up front,
60
+ * deterministically: a quantifier applied to a group that itself contains a quantifier. This is a
61
+ * conservative screen (it may reject a rare safe pattern — the operator rewrites it), never a promise
62
+ * to catch every pathological regex. Signatures are simple token alternations in practice. See ADR-0020.
63
+ */
64
+ export function unsafeSignature(source) {
65
+ // group whose body holds a quantifier, immediately followed by another quantifier: (…+…)+ (…*…)* etc.
66
+ return /\((?![?])[^()]*[+*}][^()]*\)[+*{]/.test(source);
67
+ }
56
68
  /** The same three resolution bases as the drift pass (ADR-0004). */
57
69
  export function resolutionBases(missionDir, deliverable) {
58
70
  return [dirname(missionDir), missionDir, dirname(join(missionDir, deliverable))];
59
71
  }
72
+ /** Resolve a pointer strictly WITHIN one of the three project bases (ADR-0019): an absolute
73
+ * path, or a `../` that climbs out of every base, is not "evidence in your project" — it is
74
+ * rejected, not resolved. `resolve` normalizes `..`; the containment check is a prefix test on
75
+ * the normalized path (with a separator, so `/a/project-evil` never counts as under `/a/project`). */
60
76
  function resolveFile(p, bases) {
61
77
  if (isAbsolute(p))
62
- return existsSync(p) ? p : null;
78
+ return null; // an absolute evidence path escapes the project — never valid
63
79
  for (const b of bases) {
64
- const abs = join(b, p);
65
- if (existsSync(abs))
80
+ const baseAbs = resolve(b);
81
+ const abs = resolve(baseAbs, p);
82
+ if ((abs === baseAbs || abs.startsWith(baseAbs + sep)) && existsSync(abs))
66
83
  return abs;
67
84
  }
68
85
  return null;
@@ -139,6 +156,10 @@ export function evidenceReport(missionDir, deliverable, signatures) {
139
156
  // Signature (ADR-0020): a signed rule's applied evidence must contain the rule's shape.
140
157
  const sig = signatures[row.rule];
141
158
  if (sig) {
159
+ if (unsafeSignature(sig)) {
160
+ out.push({ rule: row.rule, problem: `unsafe signature regex (nested quantifiers risk catastrophic backtracking): /${sig}/ — simplify it in runward/rules/${row.rule}.md` });
161
+ continue;
162
+ }
142
163
  let re;
143
164
  try {
144
165
  re = new RegExp(sig, "i");
@@ -54,6 +54,19 @@ export function section(title) {
54
54
  export function isNonInteractive() {
55
55
  return process.env.RUNWARD_YES === "1" || !process.stdout.isTTY;
56
56
  }
57
+ /** Today (UTC, YYYY-MM-DD), or the RUNWARD_NOW override in non-interactive runs. The override is
58
+ * VALIDATED as a real YYYY-MM-DD date: an unvalidated value flowed straight into the OSCAL and the
59
+ * seal, so a malformed env var produced schema-invalid output. A bad value falls back to today. */
60
+ export function generationDate() {
61
+ const today = new Date().toISOString().slice(0, 10);
62
+ const override = process.env.RUNWARD_NOW;
63
+ if (!isNonInteractive() || !override)
64
+ return today;
65
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(override))
66
+ return today;
67
+ const d = new Date(override + "T00:00:00Z");
68
+ return !isNaN(d.getTime()) && d.toISOString().slice(0, 10) === override ? override : today;
69
+ }
57
70
  function stripAnsi(s) {
58
71
  // eslint-disable-next-line no-control-regex
59
72
  return s.replace(/\[[0-9;]*m/g, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runward",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "After the spec: ship and run. A delivery framework for agentic systems \u2014 floor first, evolution on evidence, governance from day zero, handover with proof.",
5
5
  "keywords": [
6
6
  "agentic",
@@ -3,10 +3,10 @@
3
3
  "regime": "eu-ai-act",
4
4
  "label": "EU AI Act (Annex IV)",
5
5
  "version": "2024-1689",
6
- "notes": "Mapped 2026-07 against Regulation (EU) 2024/1689 (OJ L, 12 July 2024). High-risk (Annex III) obligations bind from 2 August 2026. Annex IV wording moves — confirm against the Official Journal before filing.",
6
+ "notes": "Mapped against Regulation (EU) 2024/1689 (OJ L, 12 July 2024), as amended by the Digital Omnibus on AI (Council final green light 29 June 2026), which postponed the Annex III high-risk obligations to 2 December 2027 (Annex I to 2 August 2028). The 2 August 2026 milestone remains real for other obligations (Article 50 transparency for systems placed on the market after that date, governance, GPAI enforcement) but is NOT the high-risk / Annex IV deadline. Annex IV wording moves — confirm against the Official Journal before filing.",
7
7
  "highRisk": {
8
- "bindFrom": "2 August 2026",
9
- "scope": "Annex III"
8
+ "bindFrom": "2 December 2027",
9
+ "scope": "Annex III, as postponed by the Digital Omnibus on AI (June 2026)"
10
10
  },
11
11
  "articles": {
12
12
  "runtimeLogging": "art. 12"
@@ -53,6 +53,10 @@ This is **one example** of a per-harness turn-end hook, not a privileged one —
53
53
 
54
54
  Copy the file into `.kiro/hooks/` in your workspace. The `Stop` trigger runs `runward check --strict` when the agent finishes, and surfaces the verdict in the session — the same one line as the Claude Code hook, another non-privileged example of a per-harness turn-end seam. Kiro also reads `AGENTS.md` natively, and `runward init --tools kiro` mirrors the phase skills as steering files (`.kiro/steering/`, relevance-loaded) — the traced decisions inform the session; the gate stays the only authority.
55
55
 
56
+ ## `bmad-review-layer.toml` — the gate as a review layer BMAD calls
57
+
58
+ Copy the `[[workflow.review_layers]]` block into the customize.toml of BMAD's `bmad-code-review` skill. It adds runward's deterministic gate as one review layer beside BMAD's adversarial LLM reviewers (their extension point explicitly allows "an external reviewer via bash"): they judge the code's craft, runward verifies the traced decisions and their evidence. A complement running on BMAD's flow, not a competitor to it — same inert posture, same exit-code port (ADR-0027).
59
+
56
60
  ---
57
61
 
58
62
  Adapters are runward-owned templates: `runward update` refreshes them, `runward doctor` verifies them. They are never mission state. Add a new harness by dropping a new sample here — the port contract above does not change.
@@ -0,0 +1,32 @@
1
+ # runward gate — BMAD review-layer adapter (sample).
2
+ # Inert until you install it: see runward/adapters/README.md.
3
+ #
4
+ # BMAD's bmad-code-review skill exposes a review-layers extension point whose
5
+ # `instruction` may "run anything (e.g. an external reviewer via bash)". This
6
+ # adds runward's deterministic, zero-LLM gate as one review layer BESIDE BMAD's
7
+ # LLM reviewers — a complement, not a replacement: BMAD's subagents judge the
8
+ # code's craft; runward verifies, deterministically, that every CRITICAL/HIGH
9
+ # rule mapped to the phase is accounted for with real, resolving evidence.
10
+ #
11
+ # To install: copy this [[workflow.review_layers]] block into the customize.toml
12
+ # of BMAD's bmad-code-review skill (src/bmm-skills/4-implementation/bmad-code-review/).
13
+ # runward writes nothing into BMAD and runs nothing on its own — you wire it (ADR-0012).
14
+ #
15
+ # Exit codes (the port contract): 0 clean · 1 gaps · 2 no mission found.
16
+
17
+ [[workflow.review_layers]]
18
+ id = "runward-gate"
19
+ name = "runward deterministic gate"
20
+ # `when` (optional) could gate this layer on a runward/ mission being present.
21
+ instruction = """
22
+ Run the runward deterministic conformance gate as an external reviewer, via bash:
23
+
24
+ npx --yes runward check --strict
25
+
26
+ Report its exit code and output verbatim as this layer's finding:
27
+ - exit 0 → the conformance manifests are complete and their evidence resolves (a PASS finding, no severity).
28
+ - exit 1 → report each printed gap as a finding the operator must close before merge (a traced rule is unaccounted for, or an evidence pointer is hollow/stale). Do NOT paraphrase or soften: the gate is deterministic and its verdict is not a matter of judgment.
29
+ - exit 2 → no runward/ mission here; skip this layer (it does not apply to this repo).
30
+
31
+ This layer verifies traced decisions and their evidence; it never judges code quality — that stays the other review layers' job and the operator's at the gate.
32
+ """