runward 0.18.1 → 0.20.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
@@ -1,19 +1,29 @@
1
1
  # Runward
2
2
 
3
- ![Runward — the working frame for your agent, from idea to production](assets/og-image-en.jpg)
3
+ ![Runward — verify the engineering decisions behind AI-written code](https://raw.githubusercontent.com/stranxik/runward/main/assets/og-image-en.jpg)
4
4
 
5
- [![npm](https://img.shields.io/npm/v/runward?style=for-the-badge&color=0a0a0a&label=npm)](https://www.npmjs.com/package/runward) [![License: MIT](https://img.shields.io/badge/License-MIT-0a0a0a?style=for-the-badge)](LICENSE) [![Doctrine: CC BY-ND 4.0](https://img.shields.io/badge/Doctrine-CC%20BY--ND%204.0-C9A45C?style=for-the-badge)](https://github.com/stranxik/designing-and-running-agentic-systems) [![Support via Ko-fi](https://img.shields.io/badge/%E2%98%95%20Support%20via%20Ko--fi-orange?style=for-the-badge)](https://ko-fi.com/stranxik)
5
+ [![npm](https://img.shields.io/badge/npm-install-0a0a0a?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/package/runward) [![Docs](https://img.shields.io/badge/Docs-runward.dev-0a0a0a?style=for-the-badge)](https://runward.dev/docs) [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-runward%20gate-2ea44f?style=for-the-badge&logo=github)](https://github.com/marketplace/actions/runward-gate) [![License: MIT](https://img.shields.io/badge/License-MIT-0a0a0a?style=for-the-badge)](LICENSE) [![Doctrine: CC BY-ND 4.0](https://img.shields.io/badge/Doctrine-CC%20BY--ND%204.0-C9A45C?style=for-the-badge)](https://github.com/stranxik/designing-and-running-agentic-systems) [![Support via Ko-fi](https://img.shields.io/badge/%E2%98%95%20Support%20via%20Ko--fi-orange?style=for-the-badge)](https://ko-fi.com/stranxik)
6
6
 
7
- **After the spec, the hard part starts. Runward ships it and runs it.**
7
+ **AI writes the code. Runward verifies the engineering decisions behind it.**
8
8
 
9
- *The floor that keeps an agentic system alive. Run-grade engineering, from business need to production.*
9
+ Your coding agent (Claude Code, Cursor, Codex…) is great at producing code. The harder question is who checks the decisions that make a system survive *after* the code ships — the architecture, where things run, how it's secured, how it's handed over.
10
10
 
11
- Your agent builds fast. Runward frames what it buildssix gated phases your agent executes and one human signs off so what ships actually holds.
11
+ Runward is an open-source delivery methodology for AI-assisted software engineering a discipline made executable, not a library you import. It doesn't replace your agent; it gives it a way to work through a full engineering mission, from framing to handover, and then **verifies, deterministically**, that the load-bearing decisions were actually made and written down. Same input, same verdict; no prompt can talk it into passing.
12
12
 
13
- Runward is a delivery framework for agentic systems. It covers the entire mission, from framing to handover — it opens before any spec in greenfield, or picks up where the spec tools stop, and carries through to the run and the handover either way. Three ways in: day one of a new project (Frame is the first gate, before any spec exists), from an existing spec (Spec Kit, OpenSpec or in-house — it becomes the input of framing), or from an existing prototype (brownfield: characterize before touching anything). Then: floor first (the floor: the smallest *running* system that proves value on real traffic), evolution on evidence, governance from day zero, and a handover that makes your team autonomous.
13
+ The core idea: don't ask an LLM whether the engineering process was followed. Verify it with plain code.
14
14
 
15
15
  > Spec Kit, OpenSpec and BMAD take you to tested, sometimes merged code. Runward pilots the whole mission — and picks up their output if you use them. What none of them *structures* is the run: governed memory, resilience, execution security, continuous evaluation, transmission.
16
16
 
17
+ ## Documentation
18
+
19
+ The full documentation is at **[runward.dev/docs](https://runward.dev/docs)** — written for humans and coding agents alike, with a Markdown twin (`.md`) and a copy-to-clipboard button on every page.
20
+
21
+ - **[Quickstart](https://runward.dev/docs/getting-started/quickstart/)** — from an empty directory to a green `runward check`.
22
+ - **[Concepts: the deterministic gate](https://runward.dev/docs/concepts/the-gate/)** — what the gate verifies, and why it is deterministic and zero-LLM.
23
+ - **[The six phases](https://runward.dev/docs/concepts/six-phases/)** — the gated delivery spine, phase by phase.
24
+ - **[From an AI agent](https://runward.dev/docs/operating/from-an-agent/)** — how an agent installs and drives runward end to end.
25
+ - **[Compliance evidence](https://runward.dev/docs/compliance/evidence/)** — framed for ISO 42001, NIST AI RMF and the EU AI Act, exported as OSCAL.
26
+
17
27
  ## Why
18
28
 
19
29
  Nearly everything that calls itself agentic today dies somewhere between the demo and production. Not because the model is weak, but because nobody can evaluate a non-deterministic behavior, govern it, or say in advance when it fails. These are architecture problems, not model problems. The industry's own signal is unambiguous: model vendors now deploy engineers directly into client organizations, because the bottleneck has moved from the model to its integration into the real world.
@@ -53,6 +63,15 @@ npx runward init --tools claude,cursor,copilot,gemini,windsurf
53
63
 
54
64
  **Fastest way to see what runward does:** `npx runward init --example` scaffolds the `request-triage` reference mission already filled, so `runward check` passes green and `runward compliance iso-42001` emits an audit-ready OSCAL pack out of the box. Then `npx runward init` (no `--example`) starts your own mission from blank templates.
55
65
 
66
+ **Install the gate where you already work — honestly tiered by how hard it blocks:**
67
+
68
+ - **Hard, at merge (CI):** the [GitHub Action](https://github.com/marketplace/actions/runward-gate) — `uses: stranxik/runward@<sha>` as a required status check.
69
+ - **Hard, at turn end:** plugins/hooks for Claude Code (`/plugin marketplace add stranxik/runward` → `/plugin install runward-gate@runward`), Gemini CLI, Codex, and Copilot.
70
+ - **Soft, per-tool:** Cursor and Kiro (their end-of-turn seam can't hard-block; the per-tool hook can).
71
+ - **Discovery only, never a gate:** an MCP descriptor — an MCP tool is model-controlled, so a check behind it is skippable, and a skippable check is not a gate (ADR-0029).
72
+
73
+ The full per-channel map, with install commands, is [`docs/distribution.md`](docs/distribution.md). The operator installs; none is privileged.
74
+
56
75
  New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
57
76
 
58
77
  ### Commands
@@ -68,6 +87,7 @@ New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
68
87
  | `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 |
69
88
  | `runward status` | Mission snapshot: current gate, decision journal (ADRs), workflows |
70
89
  | `runward doctor` | Environment and installation checks |
90
+ | `runward wire` | **Harness → channel**: detect the AI harness running the command (via a runtime signal, else a config file) and recommend the matching auto-trigger channel. Read-only — it detects and points at the inert sample, it never wires anything; the operator does (ADR-0012). On `undetermined`, the agent asks the operator which tool they use. `--json` for agent consumption |
71
91
  | `runward update` | Refresh `runward/workflows/` from the package — mission state never touched, local edits preserved unless `--force` |
72
92
  | `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 |
73
93
  | `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). The mapping itself is a published, citable mini-spec: [docs/spec/runward-oscal-mapping.md](docs/spec/runward-oscal-mapping.md) |
@@ -76,7 +96,7 @@ Global flags: `--yes`, `--dry-run`, `--verbose`, `--no-color`. Exit codes: 0 suc
76
96
 
77
97
  The gate's exit code is a **port**: `runward/adapters/` ships inert sample wiring so the gate runs at each harness's natural moment.
78
98
 
79
- **Install runward from where you already work.** Beyond the copy-in adapters, runward publishes distributable packagings a **GitHub Action** (`uses: stranxik/runward@<sha>` as a required check, the hard governance gate at merge), a **Claude Code plugin** (`/plugin marketplace add stranxik/runward`), and siblings for Gemini CLI, Codex, Copilot, Cursor and Kiro each honestly tiered by how hard its gate can block, plus an MCP descriptor for discovery only (an MCP tool is model-controlled — findable, never a gate). The full, honestly-tiered map is [`docs/distribution.md`](docs/distribution.md). The operator installs; none is privileged. The git `pre-commit` and CI adapters are **agent-agnostic** — they gate whatever agent produced the code (Codex, Claude, Cursor, Copilot, Gemini); the Claude Code `Stop`-hook is one example of a per-harness turn-end hook, not a privileged one. You copy an adapter in; runward never installs or runs one for you.
99
+ **Install runward from where you already work.** Beyond the copy-in adapters, runward publishes distributable packagings across every channel that can carry a real gate — see the install block above and the full map in [`docs/distribution.md`](docs/distribution.md). The git `pre-commit` and CI adapters are **agent-agnostic** — they gate whatever agent produced the code (Codex, Claude, Cursor, Copilot, Gemini); the Claude Code `Stop`-hook is one example of a per-harness turn-end hook, not a privileged one. You copy an adapter in; runward never installs or runs one for you.
80
100
 
81
101
  `init` creates:
82
102
 
@@ -128,7 +148,7 @@ Four things, and a code-level competitive check (July 2026) confirmed no other s
128
148
  - **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's *code* check is judged by the model; this one does not depend on an LLM to pass. (Two 2026 signals back the stance: FedRAMP RFC-0024 forbids GenAI-produced factual evidence, and the Delve affair showed why AI-produced audit evidence is not trusted.)
129
149
  - **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.
130
150
  - **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.
131
- - **Audit-ready evidence, in a published format.** 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 and exports as a machine-readable OSCAL pack. The decision → ADR → manifest → OSCAL chain is a **versioned, citable mini-spec** ([`docs/spec/runward-oscal-mapping.md`](docs/spec/runward-oscal-mapping.md)) anyone can reproduce or critique — the reference implementation of gate-level agentic-delivery evidence, not a black box. 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.
151
+ - **Audit-ready evidence, in a published format.** 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 and exports as a machine-readable OSCAL pack. The decision → ADR → manifest → OSCAL chain is a **versioned, citable mini-spec** ([`docs/spec/runward-oscal-mapping.md`](docs/spec/runward-oscal-mapping.md)) anyone can reproduce or critique — the reference implementation of gate-level agentic-delivery evidence, not a black box. 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 technical documentation (art. 11 / Annex IV) (EU) — see [`docs/compliance/`](docs/compliance/). An input to that work — never a conformity assessment, never a claim to certify. For a regulated procurement / TPRM review — what applies, what is moot because runward is **local with no data flow**, the supply-chain evidence (OIDC provenance + an attested CycloneDX **SBOM** on every release), the OpenSSF OSPS Baseline alignment, the licence framing and the honest limits — see [`docs/compliance/regulated-adoption.md`](docs/compliance/regulated-adoption.md) ([ADR-0031](docs/adr/ADR-0031-sovereign-engineering-evidence-for-regulated-environments.md)).
132
152
  - **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.
133
153
  - **Floor, not MVP deck.** The floor is the smallest *running* system that proves value on real traffic. A presentation is not a floor.
134
154
  - **Evolution on evidence.** Multi-agent, long-term memory, microservices, a bigger model: each has a sober default and an explicit trigger. No trigger, no complexity. Every switch is an ADR.
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
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,
5
- * compliance (evidence pack), manifest (table plumbing), rules / explain (rule-set surface).
4
+ * Commands: init (wizard), check (gate audit), status, doctor, wire (harness→channel),
5
+ * update, characterize, compliance (evidence pack), manifest (table plumbing),
6
+ * rules / explain (rule-set surface).
6
7
  */
7
8
  import { Command } from "commander";
8
9
  import { VERSION } from "./lib/paths.js";
@@ -11,6 +12,7 @@ import { initCommand } from "./commands/init.js";
11
12
  import { checkCommand } from "./commands/check.js";
12
13
  import { statusCommand } from "./commands/status.js";
13
14
  import { doctorCommand } from "./commands/doctor.js";
15
+ import { wireCommand } from "./commands/wire.js";
14
16
  import { updateCommand } from "./commands/update.js";
15
17
  import { characterizeCommand } from "./commands/characterize.js";
16
18
  import { complianceCommand } from "./commands/compliance.js";
@@ -68,6 +70,7 @@ program
68
70
  .option("--freeze", "seal a green strict gate: hash the evidence into runward/evidence-lock.json (implies --strict)")
69
71
  .option("--hooks", "run operator hooks from runward/hooks.json around the audit (opt-in)")
70
72
  .option("--coverage", "advisory: report deliverable + decision-ratification coverage (does not gate)")
73
+ .option("--json", "machine output: verdict, current gate, deliverable states, conformance gaps (stable contract, for agent-driven runs)")
71
74
  .action(checkCommand);
72
75
  program
73
76
  .command("status")
@@ -78,6 +81,12 @@ program
78
81
  .command("doctor")
79
82
  .description("environment and installation checks")
80
83
  .action(doctorCommand);
84
+ program
85
+ .command("wire")
86
+ .description("recommend the auto-trigger channel for the AI harness running this command (read-only; never wires — the operator does, ADR-0012)")
87
+ .option("-p, --path <path>", "project directory")
88
+ .option("--json", "machine output: detection status, harness, recommended + candidate channels (stable contract)")
89
+ .action(wireCommand);
81
90
  program
82
91
  .command("update")
83
92
  .description("refresh runward/workflows/ and runward/rules/ from this package version (mission state untouched)")
@@ -19,25 +19,35 @@ import { VERSION } from "../lib/paths.js";
19
19
  * operator's judgment at the gate.
20
20
  * With --freeze (implies --strict), a green gate is sealed: the resolvable
21
21
  * evidence files are hashed into runward/evidence-lock.json (ADR-0021).
22
+ * With --json, the same verdict is emitted as a stable machine contract (ADR-0030)
23
+ * so an agent drives on data, not scraped text — the exit-code contract is unchanged.
22
24
  * Exit codes: 0 = current gate clean, 1 = gaps, 2 = no mission found.
23
25
  */
24
26
  export async function checkCommand(opts) {
25
27
  if (opts.freeze)
26
28
  opts.strict = true; // a seal certifies a strict crossing
29
+ // In --json mode every human line is suppressed; the sole output is one JSON object at the end.
30
+ const log = (s = "") => { if (!opts.json)
31
+ console.log(s); };
27
32
  const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
28
33
  if (!root) {
29
- console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
34
+ if (opts.json) {
35
+ process.stdout.write(JSON.stringify({ runward: VERSION, mission: null, verdict: "no-mission", exitCode: 2 }) + "\n");
36
+ }
37
+ else {
38
+ console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
39
+ }
30
40
  process.exit(2);
31
41
  }
32
42
  const mission = join(root, "runward");
33
43
  const report = analyze(mission);
34
- console.log(createHeader(`Runward v${VERSION} — gate audit`, root));
44
+ log(createHeader(`Runward v${VERSION} — gate audit`, root));
35
45
  let hookFailed = 0;
36
46
  if (opts.hooks) {
37
- const before = runHooks(mission, "before", root);
47
+ const before = runHooks(mission, "before", root, { quietStdout: !!opts.json });
38
48
  if (before.ran > 0) {
39
- console.log(section("Hooks · before"));
40
- console.log(" " + (before.failed.length ? status.error(`${before.failed.length}/${before.ran} failed`) : status.success(`${before.ran} ok`)));
49
+ log(section("Hooks · before"));
50
+ log(" " + (before.failed.length ? status.error(`${before.failed.length}/${before.ran} failed`) : status.success(`${before.ran} ok`)));
41
51
  hookFailed += before.failed.length;
42
52
  }
43
53
  }
@@ -53,18 +63,22 @@ export async function checkCommand(opts) {
53
63
  "untouched": c.darkGray(" — raw template"),
54
64
  "missing": c.error(" — file missing"),
55
65
  };
66
+ // Structured collectors for --json (ADR-0030) — populated alongside the human render.
67
+ const deliverablesData = [];
68
+ const conformanceData = [];
56
69
  let gaps = 0;
57
70
  for (const phase of report.phases) {
58
- console.log(section(phase.spec.label));
71
+ log(section(phase.spec.label));
59
72
  for (const { artifact, state } of phase.artifacts) {
60
- console.log(` ${glyph[state]} ${c.white(artifact.label)} ${c.darkGray(`(runward/${artifact.relPath})`)}${legendNote[state]}`);
73
+ log(` ${glyph[state]} ${c.white(artifact.label)} ${c.darkGray(`(runward/${artifact.relPath})`)}${legendNote[state]}`);
61
74
  if (state !== "filled")
62
75
  gaps++;
76
+ deliverablesData.push({ phase: phase.spec.label, artifact: artifact.label, relPath: artifact.relPath, state });
63
77
  }
64
78
  }
65
79
  let strictGaps = 0;
66
80
  if (opts.strict) {
67
- console.log(section("Rule conformance (--strict)"));
81
+ log(section("Rule conformance (--strict)"));
68
82
  let checked = 0;
69
83
  const signatures = ruleSignatures(mission);
70
84
  for (const { phase, deliverable, label } of GATED_DELIVERABLES) {
@@ -80,76 +94,82 @@ export async function checkCommand(opts) {
80
94
  continue;
81
95
  checked++;
82
96
  if (violations.length === 0) {
83
- console.log(` ${status.success(`${label}: ${expected.length} rule(s) accounted for`)}`);
97
+ log(` ${status.success(`${label}: ${expected.length} rule(s) accounted for`)}`);
84
98
  }
85
99
  else {
86
- for (const v of violations)
87
- console.log(` ${c.error("✗")} ${c.darkGray(label + " · ")}${c.white(v.rule)}${c.darkGray(" — " + v.problem)}`);
100
+ for (const v of violations) {
101
+ log(` ${c.error("✗")} ${c.darkGray(label + " · ")}${c.white(v.rule)}${c.darkGray(" — " + v.problem)}`);
102
+ conformanceData.push({ scope: label, rule: v.rule, problem: v.problem });
103
+ }
88
104
  strictGaps += violations.length;
89
105
  }
90
106
  }
91
107
  if (checked === 0)
92
- console.log(" " + c.darkGray("no CRITICAL/HIGH rules mapped to a build phase"));
108
+ log(" " + c.darkGray("no CRITICAL/HIGH rules mapped to a build phase"));
93
109
  // Under --freeze the old seal is being replaced, not verified — otherwise a changed
94
110
  // sealed file would make re-sealing impossible (the seal violation reddens the gate
95
111
  // that freeze requires green). Everything else must still be green to seal.
96
112
  const seal = opts.freeze ? { present: false, count: 0, violations: [] } : verifyEvidenceLock(mission);
97
113
  if (seal.present) {
98
- console.log(section("Evidence seal (--strict)"));
114
+ log(section("Evidence seal (--strict)"));
99
115
  if (seal.violations.length === 0) {
100
- console.log(` ${status.success(`seal intact — ${seal.count} evidence file(s), sealed ${seal.sealedAt ?? "?"}`)}`);
116
+ log(` ${status.success(`seal intact — ${seal.count} evidence file(s), sealed ${seal.sealedAt ?? "?"}`)}`);
101
117
  }
102
118
  else {
103
- for (const v of seal.violations)
104
- console.log(` ${c.error("✗")} ${c.white(v.rule)}${c.darkGray(" — " + v.problem)}`);
119
+ for (const v of seal.violations) {
120
+ log(` ${c.error("✗")} ${c.white(v.rule)}${c.darkGray(" — " + v.problem)}`);
121
+ conformanceData.push({ scope: "evidence-seal", rule: v.rule, problem: v.problem });
122
+ }
105
123
  strictGaps += seal.violations.length;
106
124
  }
107
125
  }
108
126
  const unratified = unratifiedAdrs(mission);
109
127
  if (unratified.length > 0) {
110
- console.log(section("Reconstruction lifecycle (--strict)"));
111
- for (const u of unratified)
112
- console.log(` ${c.error("✗")} ${c.white(u.file)}${c.darkGray(" — " + u.reason)}`);
113
- console.log(" " + c.darkGray("ratify each: write the real why + a re-evaluation trigger and set Status: accepted (rename DRAFT→ADR), or remove it. A hypothesis is not a decision."));
128
+ log(section("Reconstruction lifecycle (--strict)"));
129
+ for (const u of unratified) {
130
+ log(` ${c.error("✗")} ${c.white(u.file)}${c.darkGray(" — " + u.reason)}`);
131
+ conformanceData.push({ scope: "reconstruction", rule: u.file, problem: u.reason });
132
+ }
133
+ log(" " + c.darkGray("ratify each: write the real why + a re-evaluation trigger and set Status: accepted (rename DRAFT→ADR), or remove it. A hypothesis is not a decision."));
114
134
  strictGaps += unratified.length;
115
135
  }
116
136
  if (checked > 0 && strictGaps === 0) {
117
137
  // The two proofs, made legible together: this gate is the DOCUMENTARY proof (decisions traced);
118
138
  // the BEHAVIORAL proof is the operator's test suite. runward reads the pointer, never runs the code.
119
- console.log(section("Behavioral proof (advisory, above the gate)"));
120
- console.log(" " + c.darkGray("this gate is the documentary proof: the decisions are traced. runward did not run your code — it is not a runtime. The behavioral proof is your test suite."));
139
+ log(section("Behavioral proof (advisory, above the gate)"));
140
+ log(" " + c.darkGray("this gate is the documentary proof: the decisions are traced. runward did not run your code — it is not a runtime. The behavioral proof is your test suite."));
121
141
  const bp = behavioralProof(mission, root);
122
142
  if (!bp.declared) {
123
- console.log(" " + c.darkGray("no behavioral proof declared — add `Behavioral proof: <command>` to runward/floor.md §2 (and optionally `Proof artifact: <path>`)."));
143
+ log(" " + c.darkGray("no behavioral proof declared — add `Behavioral proof: <command>` to runward/floor.md §2 (and optionally `Proof artifact: <path>`)."));
124
144
  }
125
145
  else {
126
146
  if (bp.command)
127
- console.log(` ${c.darkGray("prove behavior with:")} ${c.white(bp.command)}`);
147
+ log(` ${c.darkGray("prove behavior with:")} ${c.white(bp.command)}`);
128
148
  if (bp.artifact) {
129
149
  if (!bp.present)
130
- console.log(` ${c.warning("◑")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.warning("missing — run the command to produce it")}`);
150
+ log(` ${c.warning("◑")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.warning("missing — run the command to produce it")}`);
131
151
  else {
132
152
  const fresh = bp.fresh === undefined ? "" : bp.fresh ? c.success(" · fresh") : c.warning(" · stale (older than the code — re-run)");
133
- console.log(` ${c.success("✓")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.darkGray(`(${bp.date})`)}${fresh}`);
153
+ log(` ${c.success("✓")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.darkGray(`(${bp.date})`)}${fresh}`);
134
154
  }
135
155
  }
136
- console.log(" " + c.darkGray("advisory — runward reports presence and freshness, never runs or reads the result. You cross on both proofs."));
156
+ log(" " + c.darkGray("advisory — runward reports presence and freshness, never runs or reads the result. You cross on both proofs."));
137
157
  }
138
- console.log(section("Semantic check (advisory, above the gate)"));
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)."));
158
+ log(section("Semantic check (advisory, above the gate)"));
159
+ 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
160
  const vf = verifyFindings(mission);
141
161
  if (!vf.present) {
142
- console.log(" " + c.darkGray(`no verify findings recorded yet — the workflow writes them to runward/${VERIFY_FINDINGS}.`));
162
+ log(" " + c.darkGray(`no verify findings recorded yet — the workflow writes them to runward/${VERIFY_FINDINGS}.`));
143
163
  }
144
164
  else {
145
165
  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."));
166
+ log(` ${c.success("✓")} ${c.darkGray("verify findings")} ${c.white(`runward/${VERIFY_FINDINGS}`)} ${c.darkGray(`(${vf.date})`)}${freshness}`);
167
+ log(" " + c.darkGray("advisory — runward reports presence and freshness, never reads a verdict. The findings inform your crossing; they never gate it."));
148
168
  }
149
169
  }
150
170
  }
151
171
  if (opts.coverage) {
152
- console.log(section("Documentation coverage (advisory)"));
172
+ log(section("Documentation coverage (advisory)"));
153
173
  let filled = 0, totalArt = 0;
154
174
  for (const phase of report.phases)
155
175
  for (const { state } of phase.artifacts) {
@@ -157,26 +177,27 @@ export async function checkCommand(opts) {
157
177
  if (state === "filled")
158
178
  filled++;
159
179
  }
160
- console.log(` ${c.primaryBold("Deliverables")} ${c.white(`${filled}/${totalArt} filled`)}`);
180
+ log(` ${c.primaryBold("Deliverables")} ${c.white(`${filled}/${totalArt} filled`)}`);
161
181
  const dc = decisionCoverage(mission);
162
- console.log(` ${c.primaryBold("Decisions")} ${c.white(`${dc.ratified}/${dc.total} ratified`)}${dc.unratified.length ? c.warning(` (${dc.unratified.length} to ratify)`) : ""}`);
182
+ log(` ${c.primaryBold("Decisions")} ${c.white(`${dc.ratified}/${dc.total} ratified`)}${dc.unratified.length ? c.warning(` (${dc.unratified.length} to ratify)`) : ""}`);
163
183
  for (const u of dc.unratified)
164
- console.log(` ${c.warning("◑")} ${c.white(u.file)}${c.darkGray(" — " + u.reason)}`);
165
- console.log(" " + c.darkGray("advisory — a ratio of what is documented and ratified, not a claim of completeness. Does not affect the verdict."));
184
+ log(` ${c.warning("◑")} ${c.white(u.file)}${c.darkGray(" — " + u.reason)}`);
185
+ log(" " + c.darkGray("advisory — a ratio of what is documented and ratified, not a claim of completeness. Does not affect the verdict."));
166
186
  }
167
187
  if (opts.hooks) {
168
- const after = runHooks(mission, "after", root);
188
+ const after = runHooks(mission, "after", root, { quietStdout: !!opts.json });
169
189
  if (after.ran > 0) {
170
- console.log(section("Hooks · after"));
171
- console.log(" " + (after.failed.length ? status.error(`${after.failed.length}/${after.ran} failed`) : status.success(`${after.ran} ok`)));
190
+ log(section("Hooks · after"));
191
+ log(" " + (after.failed.length ? status.error(`${after.failed.length}/${after.ran} failed`) : status.success(`${after.ran} ok`)));
172
192
  hookFailed += after.failed.length;
173
193
  }
174
194
  }
175
- console.log(section("Summary"));
176
- console.log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
177
- console.log(` ${c.primaryBold("ADRs")} ${c.white(String(report.adrCount))}${report.adrCount === 0 ? c.warning(" — no structural decision locked yet") : ""}`);
178
- if (gaps === 0 && strictGaps === 0 && hookFailed === 0) {
179
- console.log("\n" + status.success("All expected deliverables are filled. Cross gates on evidence, not on paperwork."));
195
+ const clean = gaps === 0 && strictGaps === 0 && hookFailed === 0;
196
+ log(section("Summary"));
197
+ log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
198
+ log(` ${c.primaryBold("ADRs")} ${c.white(String(report.adrCount))}${report.adrCount === 0 ? c.warning(" — no structural decision locked yet") : ""}`);
199
+ if (clean) {
200
+ log("\n" + status.success("All expected deliverables are filled. Cross gates on evidence, not on paperwork."));
180
201
  }
181
202
  else {
182
203
  const parts = [];
@@ -186,35 +207,52 @@ export async function checkCommand(opts) {
186
207
  parts.push(`${strictGaps} floor rule-conformance gap(s)`);
187
208
  if (hookFailed)
188
209
  parts.push(`${hookFailed} hook(s) failed`);
189
- console.log("\n" + status.warning(`${parts.join(" · ")}. No phase closes without its artifact — and, under --strict, without its CRITICAL/HIGH rules accounted for.`));
210
+ log("\n" + status.warning(`${parts.join(" · ")}. No phase closes without its artifact — and, under --strict, without its CRITICAL/HIGH rules accounted for.`));
190
211
  process.exitCode = 1;
191
212
  }
192
213
  if (opts.freeze) {
193
- console.log(section("Evidence seal — freeze (ADR-0021)"));
214
+ log(section("Evidence seal — freeze (ADR-0021)"));
194
215
  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`."));
216
+ 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
217
  }
197
218
  else {
198
219
  const sealedAt = generationDate();
199
220
  const content = renderEvidenceLock(mission, sealedAt);
200
221
  const count = Object.keys(JSON.parse(content).files).length;
201
222
  if (process.env.RUNWARD_DRY_RUN === "1") {
202
- console.log(" " + c.darkGray(`dry-run — would seal ${count} evidence file(s) into runward/${EVIDENCE_LOCK}`));
223
+ log(" " + c.darkGray(`dry-run — would seal ${count} evidence file(s) into runward/${EVIDENCE_LOCK}`));
203
224
  }
204
225
  else {
205
226
  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."));
227
+ log(` ${status.success(`sealed ${count} evidence file(s) into runward/${EVIDENCE_LOCK} — commit it`)}`);
228
+ log(" " + c.darkGray("a sealed file that later changes or disappears fails `check --strict` until you re-verify and re-seal."));
208
229
  }
209
230
  }
210
231
  }
211
232
  // 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.")}`);
233
+ log(section("Next"));
234
+ if (clean) {
235
+ 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
236
  }
216
237
  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.")}`);
238
+ 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.")}`);
239
+ }
240
+ log();
241
+ // ── Machine contract (ADR-0030) ──────────────────────────────────────
242
+ // One deterministic JSON object; the exit code (set above) stays the primary signal.
243
+ if (opts.json) {
244
+ const payload = {
245
+ runward: VERSION,
246
+ mission: root,
247
+ currentGate: report.currentPhase,
248
+ adrCount: report.adrCount,
249
+ strict: !!opts.strict,
250
+ verdict: clean ? "clean" : "gaps",
251
+ exitCode: clean ? 0 : 1,
252
+ gaps: { deliverables: gaps, conformance: strictGaps, hooks: hookFailed },
253
+ deliverables: deliverablesData,
254
+ ...(opts.strict ? { conformance: conformanceData } : {}),
255
+ };
256
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
218
257
  }
219
- console.log();
220
258
  }
@@ -33,13 +33,17 @@ export async function initCommand(opts) {
33
33
  message: "What are you building? (one line — it seeds your framing note)",
34
34
  default: "",
35
35
  });
36
+ // Vendor-neutral by default (ADR-0030): with no explicit --tools, write only the neutral
37
+ // baseline (AGENTS.md + .agents/skills). No harness is privileged — under --yes the profile
38
+ // list is empty, and the wizard pre-checks nothing. A per-harness channel is an opt-in the
39
+ // operator adds afterward (the agent may recommend one for the detected harness, on approval).
36
40
  const tools = opts.tools !== undefined
37
41
  ? opts.tools.split(",").map((s) => s.trim()).filter(Boolean)
38
42
  : yes
39
- ? ["claude"]
43
+ ? []
40
44
  : await checkbox({
41
- message: "Tool profiles (AGENTS.md is always written)",
42
- choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked: t.id === "claude" })),
45
+ message: "Tool profiles (optional — AGENTS.md + .agents/skills are always written)",
46
+ choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked: false })),
43
47
  });
44
48
  const entryMode = (yes || example) ? "greenfield" : await select({
45
49
  message: "Entry mode",
@@ -0,0 +1,58 @@
1
+ import { resolve } from "node:path";
2
+ import { findMissionRoot } from "../lib/mission.js";
3
+ import { detectHarness } from "../lib/harness.js";
4
+ import { c, createHeader, section, status } from "../lib/styles.js";
5
+ import { VERSION } from "../lib/paths.js";
6
+ /**
7
+ * Recommend the auto-trigger channel for the AI harness running this command (ADR-0030).
8
+ * Read-only: it detects and prints, it never wires anything — the operator installs (ADR-0012).
9
+ * It never prompts, so an agent-driven run never hangs; `undetermined` is a normal outcome the
10
+ * agent resolves by asking the operator (doctrine in AGENTS.md / the SKILL.md). Exit code: always 0.
11
+ */
12
+ export async function wireCommand(opts) {
13
+ const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
14
+ const det = detectHarness(process.env, root);
15
+ if (opts.json) {
16
+ process.stdout.write(JSON.stringify({ runward: VERSION, mission: root, ...det }, null, 2) + "\n");
17
+ return;
18
+ }
19
+ const renderChannel = (ch) => {
20
+ const target = ch.sample ? c.white(ch.sample) : c.darkGray(ch.note ?? "via distribution packaging");
21
+ return ` ${c.primary(ch.channel.padEnd(18))} ${target}`;
22
+ };
23
+ console.log(createHeader(`Runward v${VERSION} — wire`, root ?? "no mission here"));
24
+ console.log(section("Harness"));
25
+ if (det.status === "detected") {
26
+ console.log(" " + status.success(`${det.label}`) + c.darkGray(` (runtime signal ${det.signal})`));
27
+ }
28
+ else if (det.status === "config-detected") {
29
+ console.log(" " + status.info(`${det.label}`) + c.darkGray(" (config file present — weaker than a runtime signal; confirm with the operator)"));
30
+ }
31
+ else {
32
+ console.log(" " + status.warning("undetermined — no runtime signal, no known config file"));
33
+ console.log(" " + c.darkGray("expected for Copilot CLI, Windsurf, Kiro, Continue, Aider, Trae"));
34
+ }
35
+ if (det.recommendedChannel) {
36
+ console.log(section("Recommended channel"));
37
+ console.log(renderChannel(det.recommendedChannel));
38
+ if (det.recommendedChannel.note)
39
+ console.log(" " + c.darkGray(det.recommendedChannel.note));
40
+ }
41
+ console.log(section("Always available (any harness)"));
42
+ for (const ch of det.candidateChannels)
43
+ console.log(renderChannel(ch));
44
+ console.log(section("Next"));
45
+ if (det.status === "undetermined") {
46
+ console.log(" " + c.white("Ask the operator which AI tool this is, then wire the matching sample on their approval."));
47
+ }
48
+ else if (det.recommendedChannel) {
49
+ console.log(" " + c.white(`Offer to wire the sample above from ${c.primary("runward/adapters/")}, on the operator's approval.`));
50
+ }
51
+ else {
52
+ console.log(" " + c.white(`No turn-end sample ships for ${det.label} — use a universal channel above (pre-commit or CI), on the operator's approval.`));
53
+ }
54
+ console.log(" " + c.darkGray("runward wires nothing — you are the operator's hands (ADR-0012). The baseline `runward check` already runs here with nothing wired."));
55
+ if (!root)
56
+ console.log(" " + c.warning("no runward/ mission here — run `runward init` first; the adapter samples live in runward/adapters/."));
57
+ console.log();
58
+ }
@@ -314,7 +314,7 @@ function detUuid(seed) {
314
314
  const s = b.join("");
315
315
  return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
316
316
  }
317
- const OSCAL_VERSION = "1.1.2";
317
+ const OSCAL_VERSION = "1.2.2";
318
318
  const ASI_CATALOG = "https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/";
319
319
  /**
320
320
  * Render the OWASP ASI control coverage as an OSCAL component-definition (JSON) — regime-neutral, the
@@ -55,11 +55,15 @@ function clean(token) {
55
55
  }
56
56
  /**
57
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 patternthe operator rewrites it), never a promise
62
- * to catch every pathological regex. Signatures are simple token alternations in practice. See ADR-0020.
58
+ * content — so a catastrophic-backtracking pattern could hang the gate on adversarial input (a
59
+ * self-inflicted DoS in CI). Reject the two known-dangerous shapes up front, deterministically:
60
+ * 1. a quantifier applied to a group that itself contains a quantifier `(a+)+`, `([a-z]+)*`;
61
+ * 2. a quantifier applied to a group holding an alternation `(a|a)+`, `(ab|a)+`, `(\d|\d)*`
62
+ * whose branches can overlap and blow up (the class the shape-1 screen misses).
63
+ * This is a conservative screen (it may reject a rare safe pattern like `(a|b)+` — the operator
64
+ * rewrites it), never a promise to catch every pathological regex; a linear-time engine (RE2) would
65
+ * be the complete fix, at the cost of a native dependency this zero-dep core avoids. Signatures are
66
+ * simple token alternations in practice. See ADR-0020.
63
67
  */
64
68
  export function unsafeSignature(source) {
65
69
  // Normalize character classes ([...], including [^()]) to a single token first: a quantifier INSIDE
@@ -67,9 +71,11 @@ export function unsafeSignature(source) {
67
71
  // own `[^()]` stops at the `(` that lives literally inside the class. After normalization the class
68
72
  // becomes `C`, so `([^()]+)+` reads as `(C+)+` and is caught.
69
73
  const norm = source.replace(/\[(?:\\.|[^\]\\])*\]/g, "C");
70
- // group whose body holds a quantifier, immediately followed by another quantifier: (…+…)+ (…*…)* etc.
74
+ // 1. group whose body holds a quantifier, immediately followed by another quantifier: (…+…)+ (…*…)* etc.
71
75
  const NESTED = /\((?![?])[^()]*[+*}][^()]*\)[+*{]/;
72
- return NESTED.test(norm) || NESTED.test(source);
76
+ // 2. group holding an alternation, immediately followed by a quantifier: (…|…)+ (…|…)* etc.
77
+ const ALT = /\((?![?])[^()]*\|[^()]*\)[+*{]/;
78
+ return NESTED.test(norm) || NESTED.test(source) || ALT.test(norm) || ALT.test(source);
73
79
  }
74
80
  /** The same three resolution bases as the drift pass (ADR-0004). */
75
81
  export function resolutionBases(missionDir, deliverable) {
@@ -163,7 +169,7 @@ export function evidenceReport(missionDir, deliverable, signatures) {
163
169
  const sig = signatures[row.rule];
164
170
  if (sig) {
165
171
  if (unsafeSignature(sig)) {
166
- out.push({ rule: row.rule, problem: `unsafe signature regex (nested quantifiers risk catastrophic backtracking): /${sig}/ — simplify it in runward/rules/${row.rule}.md` });
172
+ out.push({ rule: row.rule, problem: `unsafe signature regex (nested or overlapping-alternation quantifiers risk catastrophic backtracking): /${sig}/ — simplify it in runward/rules/${row.rule}.md` });
167
173
  continue;
168
174
  }
169
175
  let re;
@@ -0,0 +1,73 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Runtime signals — verified environment markers injected into the child process (survey 2026-07-16).
5
+ * `CLAUDECODE=1` covers Claude Code CLI *and* Cowork: Cowork runs the same Claude Code family and
6
+ * exposes no distinct marker, so it is named but never branched on a guessed value. `CURSOR_AGENT=1`
7
+ * is the agent-mode marker (not `CURSOR_TRACE_ID`, which is broader than agent mode).
8
+ */
9
+ const RUNTIME_SIGNALS = [
10
+ { env: "CLAUDECODE", value: "1", harness: "claude-code", label: "Claude Code / Cowork", family: "claude" },
11
+ { env: "GEMINI_CLI", value: "1", harness: "gemini-cli", label: "Gemini CLI", family: "gemini" },
12
+ { env: "CURSOR_AGENT", value: "1", harness: "cursor", label: "Cursor (agent mode)", family: "cursor" },
13
+ ];
14
+ /** Config-file markers a tool profile already laid down (mirrors doctor's detection). Weaker signal. */
15
+ const CONFIG_MARKERS = [
16
+ { marker: ".claude/commands/rw-*", family: "claude", harness: "claude-code", label: "Claude Code / Cowork",
17
+ present: (r) => existsSync(join(r, ".claude", "commands")) && readdirSync(join(r, ".claude", "commands")).some((f) => f.startsWith("rw-")) },
18
+ { marker: ".cursor/rules/runward.mdc", family: "cursor", harness: "cursor", label: "Cursor",
19
+ present: (r) => existsSync(join(r, ".cursor", "rules", "runward.mdc")) },
20
+ { marker: "GEMINI.md", family: "gemini", harness: "gemini-cli", label: "Gemini CLI",
21
+ present: (r) => existsSync(join(r, "GEMINI.md")) },
22
+ { marker: ".github/copilot-instructions.md", family: "copilot", harness: "copilot", label: "GitHub Copilot",
23
+ present: (r) => existsSync(join(r, ".github", "copilot-instructions.md")) },
24
+ { marker: ".windsurf/rules/runward.md", family: "windsurf", harness: "windsurf", label: "Windsurf",
25
+ present: (r) => existsSync(join(r, ".windsurf", "rules", "runward.md")) },
26
+ { marker: ".kiro/steering/", family: "kiro", harness: "kiro", label: "Kiro",
27
+ present: (r) => existsSync(join(r, ".kiro", "steering")) },
28
+ ];
29
+ /**
30
+ * The harness-specific auto-trigger channel. `sample` points at an inert mission adapter where one
31
+ * ships; where runward ships no mission adapter (gemini/cursor/copilot), the channel lives in the
32
+ * distribution packaging and `sample` is null with a note. Never a promise that runward wires it.
33
+ */
34
+ const FAMILY_CHANNEL = {
35
+ claude: { channel: "turn-end-hook", sample: "runward/adapters/claude-code-settings.json" },
36
+ kiro: { channel: "per-tool-hook", sample: "runward/adapters/kiro-hooks.json" },
37
+ gemini: { channel: "turn-end-hook", sample: null, note: "the Gemini extension (packaging/gemini) carries the AfterAgent hook" },
38
+ cursor: { channel: "advisory-hook", sample: null, note: "Cursor's hook is advisory; its blocking seam is per-tool, deliberately not shipped" },
39
+ copilot: { channel: "turn-end-hook", sample: null, note: "copy packaging/copilot/hooks/runward-gate.json into .github/hooks/" },
40
+ };
41
+ /** Universal hard channels — present in every mission after init, valid whatever the harness. */
42
+ export const UNIVERSAL_CHANNELS = [
43
+ { channel: "pre-commit", sample: "runward/adapters/pre-commit" },
44
+ { channel: "ci-required-check", sample: "runward/adapters/github-actions.yml" },
45
+ ];
46
+ /** Detect the harness from the environment (runtime signal) and, failing that, the repo (config file). */
47
+ export function detectHarness(env, root) {
48
+ const base = { schemaVersion: 1, candidateChannels: UNIVERSAL_CHANNELS, wires: false };
49
+ for (const s of RUNTIME_SIGNALS) {
50
+ if (env[s.env] === s.value) {
51
+ return {
52
+ ...base, status: "detected", harness: s.harness, label: s.label, family: s.family,
53
+ detectedVia: "runtime-signal", signal: s.env,
54
+ recommendedChannel: FAMILY_CHANNEL[s.family] ?? null, operatorAction: "offer-to-wire-sample",
55
+ };
56
+ }
57
+ }
58
+ if (root) {
59
+ for (const m of CONFIG_MARKERS) {
60
+ if (m.present(root)) {
61
+ return {
62
+ ...base, status: "config-detected", harness: m.harness, label: m.label, family: m.family,
63
+ detectedVia: "config-file", signal: null,
64
+ recommendedChannel: FAMILY_CHANNEL[m.family] ?? null, operatorAction: "offer-to-wire-sample",
65
+ };
66
+ }
67
+ }
68
+ }
69
+ return {
70
+ ...base, status: "undetermined", harness: null, label: null, family: null,
71
+ detectedVia: null, signal: null, recommendedChannel: null, operatorAction: "ask-operator-which-harness",
72
+ };
73
+ }
package/dist/lib/hooks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- export function runHooks(missionDir, phase, cwd) {
4
+ export function runHooks(missionDir, phase, cwd, opts = {}) {
5
5
  const result = { ran: 0, failed: [] };
6
6
  const path = join(missionDir, "hooks.json");
7
7
  if (!existsSync(path))
@@ -13,10 +13,14 @@ export function runHooks(missionDir, phase, cwd) {
13
13
  catch {
14
14
  return result;
15
15
  }
16
+ // Under --json (quietStdout), route the hook's own stdout to the parent's stderr (fd 2): a
17
+ // subprocess writing to stdout would otherwise corrupt the single-JSON-object contract, which
18
+ // log() cannot suppress. The hook's output stays visible on stderr; stdout carries only the JSON.
19
+ const stdio = opts.quietStdout ? ["inherit", 2, "inherit"] : "inherit";
16
20
  for (const cmd of cfg[phase] ?? []) {
17
21
  result.ran++;
18
22
  try {
19
- execSync(cmd, { cwd, stdio: "inherit" });
23
+ execSync(cmd, { cwd, stdio });
20
24
  }
21
25
  catch {
22
26
  result.failed.push(cmd);
@@ -52,7 +52,15 @@ export function section(title) {
52
52
  return "\n" + c.primaryBold(title) + "\n" + c.darkGray("─".repeat(Math.min(title.length + 8, 60)));
53
53
  }
54
54
  export function isNonInteractive() {
55
- return process.env.RUNWARD_YES === "1" || !process.stdout.isTTY;
55
+ // An autonomous agent must never hang on a prompt it cannot answer (ADR-0030). Beyond the
56
+ // explicit --yes, treat as non-interactive when CI is set or either standard stream is not a
57
+ // TTY: a prompt reads stdin, so a non-TTY stdin (piped, or none) cannot answer it.
58
+ if (process.env.RUNWARD_YES === "1")
59
+ return true;
60
+ const ci = process.env.CI;
61
+ if (ci && ci !== "0" && ci.toLowerCase() !== "false")
62
+ return true;
63
+ return !process.stdout.isTTY || !process.stdin.isTTY;
56
64
  }
57
65
  /** Today (UTC, YYYY-MM-DD), or the RUNWARD_NOW override in non-interactive runs. The override is
58
66
  * VALIDATED as a real YYYY-MM-DD date: an unvalidated value flowed straight into the OSCAL and the
package/dist/lib/tools.js CHANGED
@@ -64,7 +64,7 @@ const skillMd = (s) => [
64
64
  "",
65
65
  skillBody(s),
66
66
  ].join("\n");
67
- /** Emit the four phase skills as SKILL.md folders under `<root>/<...dir>/runward-<phase>/SKILL.md`. */
67
+ /** Emit the five phase skills as SKILL.md folders under `<root>/<...dir>/runward-<phase>/SKILL.md`. */
68
68
  const skillsAt = (root, ...dir) => PHASE_SKILLS.map((s) => ({ path: join(root, ...dir, `runward-${s.phase}`, "SKILL.md"), content: skillMd(s) }));
69
69
  /**
70
70
  * The vendor-neutral phase skills at `.agents/skills/` — the converged SKILL.md alias read by
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "runward",
3
- "version": "0.18.1",
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.",
3
+ "version": "0.20.0",
4
+ "description": "After the spec: ship and run. A delivery framework for agentic systems floor first, evolution on evidence, governance from day zero, handover with proof.",
5
5
  "keywords": [
6
6
  "agentic",
7
7
  "ai-agents",
@@ -44,7 +44,7 @@
44
44
  "LICENSE"
45
45
  ],
46
46
  "engines": {
47
- "node": ">=20"
47
+ "node": ">=22.12.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc",
@@ -3,6 +3,7 @@
3
3
  "regime": "eu-ai-act",
4
4
  "label": "EU AI Act (Annex IV)",
5
5
  "version": "2024-1689",
6
+ "reviewBy": "2026-08-02",
6
7
  "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
8
  "highRisk": {
8
9
  "bindFrom": "2 December 2027",
@@ -3,6 +3,7 @@
3
3
  "regime": "iso-42001",
4
4
  "label": "ISO/IEC 42001",
5
5
  "version": "2023",
6
+ "reviewBy": "2027-01-01",
6
7
  "notes": "Mapped 2026-07 against ISO/IEC 42001:2023 (first edition, 2023-12). ISO Annex A control counts/templates are behind the paywalled standard — confirm against the purchased text.",
7
8
  "clauses": {
8
9
  "riskAssessment": "6.1.2",
@@ -3,6 +3,7 @@
3
3
  "regime": "nist-ai-rmf",
4
4
  "label": "NIST AI RMF",
5
5
  "version": "1.0",
6
+ "reviewBy": "2027-01-01",
6
7
  "notes": "Mapped 2026-07 against NIST AI 100-1 (AI RMF 1.0, January 2023) and the companion Playbook.",
7
8
  "crosswalk": {
8
9
  "primary": "each agentic-security risk lands primarily under **MEASURE** (test & evaluate, esp. security & resilience) and **MANAGE** (risk treatment)",
@@ -17,7 +17,7 @@ This project is delivered with the Runward method: floor first, evolution on evi
17
17
  - Before any structural decision, run `runward/workflows/decision-loop.md`: verify in the real code, check the sourced state of the art, challenge the source, take a durable position, lock it in an ADR — only then edit.
18
18
  - One ADR per structural decision, in `runward/adr/`, with a dated re-evaluation trigger. Use the template `runward/adr/ADR-0000-template.md`.
19
19
  - Current phase and gates: see `runward/framing.md` (Definition of Ready) and each workflow's Definition of Done. Do not skip a gate on assertion; pass it on evidence.
20
- - Offer to wire the gate at your harness's lifecycle seam — git `pre-commit`, a CI required check, or a turn-end hook — from the inert samples in `runward/adapters/` (its README has the exact command). Propose it to the operator and act only on their explicit approval; never wire it silently. runward installs nothing itself (ADR-0012): you are the operator's hands, and the operator owns the gate.
20
+ - Offer to wire the gate at your harness's lifecycle seam — git `pre-commit`, a CI required check, or a turn-end hook — from the inert samples in `runward/adapters/`. Run `runward wire` (or `runward wire --json`) to identify the channel for the harness running you: if it reports a harness (`status: detected` / `config-detected`), offer to wire its recommended sample. **If it reports `status: undetermined`, do not guess — ask the operator, in plain language, which AI tool they use, then wire the matching sample.** Propose it and act only on explicit approval; never wire it silently. `runward wire` is read-only and runward installs nothing itself (ADR-0012): you are the operator's hands, and the operator owns the gate.
21
21
  - Show before you build: for any deliverable meant for humans, produce a reviewable preview first.
22
22
  - Never mark a phase done if its Definition of Done is not demonstrably met.
23
23