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 +28 -8
- package/dist/cli.js +11 -2
- package/dist/commands/check.js +94 -56
- package/dist/commands/init.js +7 -3
- package/dist/commands/wire.js +58 -0
- package/dist/lib/compliance.js +1 -1
- package/dist/lib/evidence.js +14 -8
- package/dist/lib/harness.js +73 -0
- package/dist/lib/hooks.js +6 -2
- package/dist/lib/styles.js +9 -1
- package/dist/lib/tools.js +1 -1
- package/package.json +3 -3
- package/regimes/eu-ai-act@2024-1689.json +1 -0
- package/regimes/iso-42001@2023.json +1 -0
- package/regimes/nist-ai-rmf@1.0.json +1 -0
- package/templates/targets/AGENTS.md +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
# Runward
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/runward) [](https://runward.dev/docs) [](https://github.com/marketplace/actions/runward-gate) [](LICENSE) [](https://github.com/stranxik/designing-and-running-agentic-systems) [](https://ko-fi.com/stranxik)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**AI writes the code. Runward verifies the engineering decisions behind it.**
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
5
|
-
* compliance (evidence pack), manifest (table plumbing),
|
|
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)")
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
71
|
+
log(section(phase.spec.label));
|
|
59
72
|
for (const { artifact, state } of phase.artifacts) {
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
log(` ${status.success(`${label}: ${expected.length} rule(s) accounted for`)}`);
|
|
84
98
|
}
|
|
85
99
|
else {
|
|
86
|
-
for (const v of violations)
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
log(section("Evidence seal (--strict)"));
|
|
99
115
|
if (seal.violations.length === 0) {
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
for (const u of unratified)
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
log(` ${c.darkGray("prove behavior with:")} ${c.white(bp.command)}`);
|
|
128
148
|
if (bp.artifact) {
|
|
129
149
|
if (!bp.present)
|
|
130
|
-
|
|
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
|
-
|
|
153
|
+
log(` ${c.success("✓")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.darkGray(`(${bp.date})`)}${fresh}`);
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
|
-
|
|
156
|
+
log(" " + c.darkGray("advisory — runward reports presence and freshness, never runs or reads the result. You cross on both proofs."));
|
|
137
157
|
}
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
log(` ${c.primaryBold("Deliverables")} ${c.white(`${filled}/${totalArt} filled`)}`);
|
|
161
181
|
const dc = decisionCoverage(mission);
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
log(section("Evidence seal — freeze (ADR-0021)"));
|
|
194
215
|
if (gaps || strictGaps || hookFailed) {
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
? [
|
|
43
|
+
? []
|
|
40
44
|
: await checkbox({
|
|
41
|
-
message: "Tool profiles (AGENTS.md
|
|
42
|
-
choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked:
|
|
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
|
+
}
|
package/dist/lib/compliance.js
CHANGED
|
@@ -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.
|
|
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
|
package/dist/lib/evidence.js
CHANGED
|
@@ -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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
|
|
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
|
|
23
|
+
execSync(cmd, { cwd, stdio });
|
|
20
24
|
}
|
|
21
25
|
catch {
|
|
22
26
|
result.failed.push(cmd);
|
package/dist/lib/styles.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "After the spec: ship and run. A delivery framework for agentic systems
|
|
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": ">=
|
|
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
|
|
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
|
|