runward 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/cli.js +1 -0
- package/dist/commands/check.js +31 -2
- package/dist/commands/doctor.js +10 -1
- package/dist/commands/init.js +4 -0
- package/dist/commands/update.js +3 -2
- package/dist/lib/conformance.js +68 -7
- package/dist/lib/constants.js +7 -1
- package/dist/lib/hooks.js +26 -0
- package/dist/lib/paths.js +1 -1
- package/dist/lib/rule-migrations.js +7 -0
- package/package.json +29 -7
- package/templates/adapters/README.md +48 -0
- package/templates/adapters/claude-code-settings.json +15 -0
- package/templates/adapters/github-actions.yml +26 -0
- package/templates/adapters/pre-commit +18 -0
- package/templates/mission/decision-matrix.md +2 -2
- package/templates/mission/reference-stack.md +10 -5
- package/templates/rules/async-job-guardrails.md +1 -0
- package/templates/rules/checklist-pre-production-resilience.md +1 -0
- package/templates/rules/checklist-pre-production-security.md +1 -0
- package/templates/rules/config-secrets-boundary.md +1 -0
- package/templates/rules/contracts-governance.md +1 -0
- package/templates/rules/data-memory-provenance.md +27 -0
- package/templates/rules/eval-loop.md +1 -0
- package/templates/rules/frontier-deterministic-boundary.md +1 -0
- package/templates/rules/hexa-move-deterministic-out.md +1 -0
- package/templates/rules/patterns-memory-router-tiered.md +1 -0
- package/templates/rules/resilience-fail-open.md +1 -0
- package/templates/rules/resilience-multi-provider-fallback.md +1 -0
- package/templates/rules/resilience-retry-backoff.md +1 -0
- package/templates/rules/scaling-distributed-rate-limiting.md +1 -0
- package/templates/rules/security-mcp-server-pinning.md +26 -0
- package/templates/rules/security-prompt-injection.md +1 -0
- package/templates/rules/security-tool-change-reapproval.md +25 -0
- package/templates/rules/state-event-sourcing.md +1 -0
- package/templates/rules/tools-registry-pattern.md +1 -0
- package/templates/rules/tools-scope-atomicity.md +1 -0
- package/templates/workflows/handover.md +3 -2
- package/templates/workflows/verify.md +33 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Runward
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/runward) [](LICENSE) [](https://github.com/stranxik/designing-and-running-agentic-systems) [](https://ko-fi.com/stranxik)
|
|
4
6
|
|
|
5
7
|
**From business need to a system that holds in production.**
|
|
@@ -8,7 +10,7 @@ Your agent builds fast. Runward frames what it builds — six gated phases your
|
|
|
8
10
|
|
|
9
11
|
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.
|
|
10
12
|
|
|
11
|
-
> Spec Kit, OpenSpec and BMAD
|
|
13
|
+
> 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.
|
|
12
14
|
|
|
13
15
|
## Why
|
|
14
16
|
|
|
@@ -18,7 +20,7 @@ Spec-driven frameworks answered the first half of that problem: write the right
|
|
|
18
20
|
|
|
19
21
|
Agentic systems break four assumptions of classical distributed engineering. The core component is **non-deterministic**: same input, different output, by design. **Input is indistinguishable from instruction**: anything the model reads can try to command it, so prompt injection is structural, not a bug to patch. **Forgetting becomes an engineering problem**: memory that only grows drowns the signal, so decay, invalidation and consolidation have to be designed, not hoped for. And the **blast radius is unprecedented**: an agent with tools acts on the world, so a bad output is no longer just a bad answer.
|
|
20
22
|
|
|
21
|
-
Runward's answer opens with a founding inversion — the **LLM Boundary Principle**: the architecture constrains the model, never the other way around. That opening posture unfolds into five gestures: boundaries before the stack; start simple, isolate by contract, grow on evidence; keep the deterministic out of the model; explicit state and governed memory; govern, trace and evaluate from day zero. The full base is the method itself: six gated phases (a gate: a checkpoint you cross on evidence, never on assertion), each with a Definition of Ready and a Definition of Done, a 22-arbitration decision matrix, and
|
|
23
|
+
Runward's answer opens with a founding inversion — the **LLM Boundary Principle**: the architecture constrains the model, never the other way around. That opening posture unfolds into five gestures: boundaries before the stack; start simple, isolate by contract, grow on evidence; keep the deterministic out of the model; explicit state and governed memory; govern, trace and evaluate from day zero. The full base is the method itself: six gated phases (a gate: a checkpoint you cross on evidence, never on assertion), each with a Definition of Ready and a Definition of Done, a 22-arbitration decision matrix, and 54 craft rules. The inversion opens the method; the phases, gestures and rules are what carry it.
|
|
22
24
|
|
|
23
25
|
## Who it's for — and when
|
|
24
26
|
|
|
@@ -60,6 +62,8 @@ New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
|
60
62
|
|
|
61
63
|
Global flags: `--yes`, `--dry-run`, `--verbose`, `--no-color`. Exit codes: 0 success, 1 gaps/warnings, 2 missing prerequisite.
|
|
62
64
|
|
|
65
|
+
The gate's exit code is a **port**: `runward/adapters/` ships inert sample wiring — a git `pre-commit` hook, a CI job, a Claude Code `Stop`-hook — so the gate runs at each harness's natural moment (commit, merge, agent turn-end). You copy an adapter in; runward never installs or runs one for you.
|
|
66
|
+
|
|
63
67
|
`init` creates:
|
|
64
68
|
|
|
65
69
|
```
|
|
@@ -74,13 +78,14 @@ your-project/
|
|
|
74
78
|
│ ├── shared-bricks.md # bricks beyond the app: placement families, brick matrix, sovereignty by data class
|
|
75
79
|
│ ├── floor.md # the smallest system that proves value on real traffic
|
|
76
80
|
│ ├── adr/ # one ADR per structural decision, with re-evaluation trigger
|
|
77
|
-
│ ├── rules/ #
|
|
81
|
+
│ ├── rules/ # 54 craft rules your agent applies while building
|
|
78
82
|
│ ├── governance/
|
|
79
83
|
│ │ ├── threat-model.md # lethal trifecta, 2-of-3 rule on the context window
|
|
80
84
|
│ │ ├── evaluation-rubric.md # test the deterministic, evaluate the non-deterministic
|
|
81
85
|
│ │ └── observability-schema.md
|
|
82
86
|
│ ├── contracts/ # port contracts (versioned, additive, tolerant reader)
|
|
83
87
|
│ ├── runbook.md # recovery runbook for the team that inherits the system
|
|
88
|
+
│ ├── adapters/ # inert samples that run the gate at each harness seam (git, CI, Claude Code)
|
|
84
89
|
│ └── workflows/ # the method, executable by your agent
|
|
85
90
|
└── .claude/ | .cursor/ # tool profiles (--tools)
|
|
86
91
|
```
|
|
@@ -105,7 +110,7 @@ Phase 5 is transverse: it starts at day zero, not after the incident.
|
|
|
105
110
|
- **Floor, not MVP deck.** The floor is the smallest *running* system that proves value on real traffic. A presentation is not a floor.
|
|
106
111
|
- **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.
|
|
107
112
|
- **Security by architecture, not detection.** Prompt injection is constrained structurally (lethal trifecta, 2-of-3 rule), not filtered heuristically.
|
|
108
|
-
- **Craft rules, not vibes.**
|
|
113
|
+
- **Craft rules, not vibes.** 54 engineering craft rules ship with the mission (memory scoring, tiered retrieval, event sourcing, request-id propagation, multi-provider fallback, cost routing, post-turn pipelines, prompt-injection defenses…) — your agent applies them, `runward check` will not invent them. Code examples follow the reference-stack default (a single language in the core — TypeScript by default, an adapter decision like any other: see the decision matrix). The patterns are the contract; the language is the adapter.
|
|
109
114
|
- **One accountable operator, not a simulated team.** One human owns every gate while the agent executes the workflows — see [the operator role](docs/operator-role.md).
|
|
110
115
|
- **Handover as a deliverable.** The mission ends when the receiving team is autonomous, with proof.
|
|
111
116
|
|
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,7 @@ program
|
|
|
58
58
|
.description("can I cross the gate — gate audit, exit 1 on gaps (CI-friendly)")
|
|
59
59
|
.option("-p, --path <path>", "project directory")
|
|
60
60
|
.option("--strict", "also verify the floor rule-conformance manifest (deterministic)")
|
|
61
|
+
.option("--hooks", "run operator hooks from runward/hooks.json around the audit (opt-in)")
|
|
61
62
|
.action(checkCommand);
|
|
62
63
|
program
|
|
63
64
|
.command("status")
|
package/dist/commands/check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
3
|
-
import { conformance } from "../lib/conformance.js";
|
|
3
|
+
import { conformance, driftReport } from "../lib/conformance.js";
|
|
4
|
+
import { runHooks } from "../lib/hooks.js";
|
|
4
5
|
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
5
6
|
import { VERSION } from "../lib/paths.js";
|
|
6
7
|
/**
|
|
@@ -21,6 +22,15 @@ export async function checkCommand(opts) {
|
|
|
21
22
|
const mission = join(root, "runward");
|
|
22
23
|
const report = analyze(mission);
|
|
23
24
|
console.log(createHeader(`Runward v${VERSION} — gate audit`, root));
|
|
25
|
+
let hookFailed = 0;
|
|
26
|
+
if (opts.hooks) {
|
|
27
|
+
const before = runHooks(mission, "before", root);
|
|
28
|
+
if (before.ran > 0) {
|
|
29
|
+
console.log(section("Hooks · before"));
|
|
30
|
+
console.log(" " + (before.failed.length ? status.error(`${before.failed.length}/${before.ran} failed`) : status.success(`${before.ran} ok`)));
|
|
31
|
+
hookFailed += before.failed.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
const glyph = {
|
|
25
35
|
"filled": c.success("✓"),
|
|
26
36
|
"in-progress": c.warning("◐"),
|
|
@@ -51,7 +61,10 @@ export async function checkCommand(opts) {
|
|
|
51
61
|
];
|
|
52
62
|
console.log(section("Rule conformance (--strict)"));
|
|
53
63
|
let checked = 0;
|
|
64
|
+
const drift = [];
|
|
54
65
|
for (const { phase, deliverable, label } of CONFORMANCE) {
|
|
66
|
+
for (const d of driftReport(mission, deliverable))
|
|
67
|
+
drift.push(`${label} · ${d.rule} — ${d.problem}`);
|
|
55
68
|
const { expected, violations } = conformance(mission, phase, deliverable);
|
|
56
69
|
if (expected.length === 0)
|
|
57
70
|
continue;
|
|
@@ -67,11 +80,25 @@ export async function checkCommand(opts) {
|
|
|
67
80
|
}
|
|
68
81
|
if (checked === 0)
|
|
69
82
|
console.log(" " + c.darkGray("no CRITICAL/HIGH rules mapped to a build phase"));
|
|
83
|
+
if (drift.length > 0) {
|
|
84
|
+
console.log(section("Drift (advisory)"));
|
|
85
|
+
for (const d of drift)
|
|
86
|
+
console.log(` ${c.warning("◑")} ${c.white(d)}`);
|
|
87
|
+
console.log(" " + c.darkGray("advisory — an applied pointer no longer resolves; verify it. Does not fail the gate."));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (opts.hooks) {
|
|
91
|
+
const after = runHooks(mission, "after", root);
|
|
92
|
+
if (after.ran > 0) {
|
|
93
|
+
console.log(section("Hooks · after"));
|
|
94
|
+
console.log(" " + (after.failed.length ? status.error(`${after.failed.length}/${after.ran} failed`) : status.success(`${after.ran} ok`)));
|
|
95
|
+
hookFailed += after.failed.length;
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
console.log(section("Summary"));
|
|
72
99
|
console.log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
|
|
73
100
|
console.log(` ${c.primaryBold("ADRs")} ${c.white(String(report.adrCount))}${report.adrCount === 0 ? c.warning(" — no structural decision locked yet") : ""}`);
|
|
74
|
-
if (gaps === 0 && strictGaps === 0) {
|
|
101
|
+
if (gaps === 0 && strictGaps === 0 && hookFailed === 0) {
|
|
75
102
|
console.log("\n" + status.success("All expected deliverables are filled. Cross gates on evidence, not on paperwork."));
|
|
76
103
|
}
|
|
77
104
|
else {
|
|
@@ -80,6 +107,8 @@ export async function checkCommand(opts) {
|
|
|
80
107
|
parts.push(`${gaps} deliverable(s) not filled`);
|
|
81
108
|
if (strictGaps)
|
|
82
109
|
parts.push(`${strictGaps} floor rule-conformance gap(s)`);
|
|
110
|
+
if (hookFailed)
|
|
111
|
+
parts.push(`${hookFailed} hook(s) failed`);
|
|
83
112
|
console.log("\n" + status.warning(`${parts.join(" · ")}. No phase closes without its artifact — and, under --strict, without its CRITICAL/HIGH rules accounted for.`));
|
|
84
113
|
process.exitCode = 1;
|
|
85
114
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { TEMPLATES, MISSION_LAYOUT, VERSION, WORKFLOWS } from "../lib/paths.js";
|
|
5
|
-
import { EXPECTED_RULES } from "../lib/constants.js";
|
|
5
|
+
import { EXPECTED_RULES, EXPECTED_MAPPED, EXPECTED_ADAPTERS } from "../lib/constants.js";
|
|
6
|
+
import { expectedRules } from "../lib/conformance.js";
|
|
6
7
|
import { findMissionRoot } from "../lib/mission.js";
|
|
7
8
|
import { createHeader, section, status } from "../lib/styles.js";
|
|
8
9
|
/**
|
|
@@ -34,6 +35,14 @@ export async function doctorCommand() {
|
|
|
34
35
|
const rulesDir = join(TEMPLATES, "rules");
|
|
35
36
|
const ruleCount = existsSync(rulesDir) ? readdirSync(rulesDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
36
37
|
ruleCount === EXPECTED_RULES ? ok(`${ruleCount} craft rules`) : fail(`craft rules mismatch: ${ruleCount}/${EXPECTED_RULES}`);
|
|
38
|
+
const adaptersDir = join(TEMPLATES, "adapters");
|
|
39
|
+
const adapterCount = existsSync(adaptersDir) ? readdirSync(adaptersDir).length : 0;
|
|
40
|
+
adapterCount === EXPECTED_ADAPTERS ? ok(`${adapterCount} gate adapters`) : fail(`gate adapters mismatch: ${adapterCount}/${EXPECTED_ADAPTERS}`);
|
|
41
|
+
// Routed-count floor (ADR-0002): the phases: mapping must not be stripped below its pinned minimum.
|
|
42
|
+
const belowFloor = Object.entries(EXPECTED_MAPPED).filter(([phase, floor]) => expectedRules(TEMPLATES, phase).length < floor);
|
|
43
|
+
belowFloor.length === 0
|
|
44
|
+
? ok(`rule mapping floors met (${Object.entries(EXPECTED_MAPPED).map(([p, f]) => `${p}≥${f}`).join(", ")})`)
|
|
45
|
+
: fail(`rule mapping below floor: ${belowFloor.map(([p, f]) => `${p} ${expectedRules(TEMPLATES, p).length}/${f}`).join(", ")}`);
|
|
37
46
|
console.log(section("Current directory"));
|
|
38
47
|
const root = findMissionRoot(process.cwd());
|
|
39
48
|
if (!root) {
|
package/dist/commands/init.js
CHANGED
|
@@ -71,6 +71,10 @@ export async function initCommand(opts) {
|
|
|
71
71
|
for (const r of rules) {
|
|
72
72
|
w.copy(join(TEMPLATES, "rules", r), join(mission, "rules", r));
|
|
73
73
|
}
|
|
74
|
+
console.log(section("Gate adapters"));
|
|
75
|
+
for (const a of readdirSync(join(TEMPLATES, "adapters"))) {
|
|
76
|
+
w.copy(join(TEMPLATES, "adapters", a), join(mission, "adapters", a));
|
|
77
|
+
}
|
|
74
78
|
console.log(section("Agent charter"));
|
|
75
79
|
w.copy(join(TEMPLATES, "targets", "AGENTS.md"), join(root, "AGENTS.md"));
|
|
76
80
|
for (const profile of TOOL_PROFILES.filter((p) => tools.includes(p.id))) {
|
package/dist/commands/update.js
CHANGED
|
@@ -19,8 +19,9 @@ export async function updateCommand(opts) {
|
|
|
19
19
|
console.log(createHeader(`Runward v${VERSION} — update workflows & rules`, root));
|
|
20
20
|
let same = 0, drifted = 0, added = 0;
|
|
21
21
|
const w = makeWriter({ force: true, dryRun, root });
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const LABELS = { workflows: "Workflows", rules: "Craft rules", adapters: "Gate adapters" };
|
|
23
|
+
for (const dir of ["workflows", "rules", "adapters"]) {
|
|
24
|
+
console.log(section(LABELS[dir]));
|
|
24
25
|
const dest = join(root, "runward", dir);
|
|
25
26
|
for (const file of readdirSync(join(TEMPLATES, dir))) {
|
|
26
27
|
const src = readFileSync(join(TEMPLATES, dir, file), "utf8");
|
package/dist/lib/conformance.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
3
|
import { TEMPLATES } from "./paths.js";
|
|
4
|
+
import { EXPECTED_MAPPED } from "./constants.js";
|
|
5
|
+
import { RULE_MIGRATIONS } from "./rule-migrations.js";
|
|
4
6
|
const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
|
|
5
7
|
const VALID_STATUS = new Set(["applied", "deviated", "n/a"]);
|
|
8
|
+
/** An n/a reason must be more than a placeholder: real length, not a bracketed template token. */
|
|
9
|
+
function trivialReason(s) {
|
|
10
|
+
const t = s.trim();
|
|
11
|
+
return t.length < 8 || /^\[.*\]$/.test(t);
|
|
12
|
+
}
|
|
6
13
|
function parseRuleMeta(content) {
|
|
7
14
|
const fm = content.match(FRONTMATTER)?.[1] ?? "";
|
|
8
15
|
const impact = (fm.match(/^impact:\s*(.+)$/m)?.[1] ?? "").trim();
|
|
@@ -28,6 +35,14 @@ export function expectedRules(missionDir, phaseId) {
|
|
|
28
35
|
.map((f) => f.replace(/\.md$/, ""))
|
|
29
36
|
.sort();
|
|
30
37
|
}
|
|
38
|
+
/** Every rule slug in the set (mission's own, else the package) — the universe a manifest row must belong to. */
|
|
39
|
+
export function allRules(missionDir) {
|
|
40
|
+
const missionRules = join(missionDir, "rules");
|
|
41
|
+
const dir = existsSync(missionRules) ? missionRules : join(TEMPLATES, "rules");
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
return [];
|
|
44
|
+
return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
45
|
+
}
|
|
31
46
|
/** Parse the "## Rule conformance" markdown table from a deliverable. */
|
|
32
47
|
export function parseManifest(content) {
|
|
33
48
|
const lines = content.split("\n");
|
|
@@ -58,19 +73,65 @@ function adrExists(missionDir, evidence) {
|
|
|
58
73
|
const dir = join(missionDir, "adr");
|
|
59
74
|
return existsSync(dir) && readdirSync(dir).some((f) => f.toUpperCase().startsWith(id));
|
|
60
75
|
}
|
|
76
|
+
// A path token: a file with a known code/doc extension (excludes version numbers like v1.0, "§2").
|
|
77
|
+
const PATH_TOKEN = /[\w./-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|md|json|ya?ml|toml|go|rs|java|rb|php|sql|sh|css|scss|html|txt)\b/g;
|
|
78
|
+
/** Advisory drift (ADR-0004): applied pointers whose file path no longer resolves. Existence only. */
|
|
79
|
+
export function driftReport(missionDir, deliverable) {
|
|
80
|
+
const path = join(missionDir, deliverable);
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
return [];
|
|
83
|
+
const bases = [dirname(missionDir), missionDir, dirname(path)];
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const row of parseManifest(readFileSync(path, "utf8"))) {
|
|
86
|
+
if (row.status !== "applied")
|
|
87
|
+
continue;
|
|
88
|
+
const tokens = row.evidence.match(PATH_TOKEN) ?? [];
|
|
89
|
+
if (tokens.length === 0)
|
|
90
|
+
continue; // pure prose reference — the operator's judgment
|
|
91
|
+
const resolves = tokens.some((t) => bases.some((b) => existsSync(join(b, t))));
|
|
92
|
+
if (!resolves)
|
|
93
|
+
out.push({ rule: row.rule, problem: `applied pointer does not resolve (drift?): ${row.evidence} — update the pointer or remove the row` });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
61
97
|
/** Verify a build phase's conformance manifest against its expected rule set. */
|
|
62
98
|
export function conformance(missionDir, phaseId, deliverable) {
|
|
63
99
|
const expected = expectedRules(missionDir, phaseId);
|
|
64
100
|
const violations = [];
|
|
101
|
+
// Non-vacuity (ADR-0002): the mapping cannot be stripped below its pinned floor.
|
|
102
|
+
const floor = EXPECTED_MAPPED[phaseId];
|
|
103
|
+
if (floor !== undefined && expected.length < floor) {
|
|
104
|
+
violations.push({ rule: "(mapping)", problem: `only ${expected.length} CRITICAL/HIGH rules mapped to '${phaseId}', floor is ${floor} — the mapping may have been stripped; restore the phases: [...] frontmatter on this phase's rules` });
|
|
105
|
+
}
|
|
65
106
|
const path = join(missionDir, deliverable);
|
|
66
107
|
if (!existsSync(path)) {
|
|
67
108
|
return { expected, violations: expected.map((rule) => ({ rule, problem: `${deliverable} missing` })) };
|
|
68
109
|
}
|
|
69
|
-
const
|
|
110
|
+
const rows = parseManifest(readFileSync(path, "utf8"));
|
|
111
|
+
// Form-lint (ADR-0003): well-formedness before the semantic check. Skip template placeholder tokens.
|
|
112
|
+
const known = new Set(allRules(missionDir));
|
|
113
|
+
const counts = new Map();
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
if (/^\[.*\]$/.test(r.rule))
|
|
116
|
+
continue;
|
|
117
|
+
counts.set(r.rule, (counts.get(r.rule) ?? 0) + 1);
|
|
118
|
+
}
|
|
119
|
+
for (const [rule, n] of counts) {
|
|
120
|
+
if (!known.has(rule)) {
|
|
121
|
+
const m = RULE_MIGRATIONS[rule];
|
|
122
|
+
const hint = m
|
|
123
|
+
? (m.to ? ` — renamed to '${m.to}' in ${m.since} (${m.reason})` : ` — removed in ${m.since} (${m.reason})`)
|
|
124
|
+
: " (typo? not in runward/rules/)";
|
|
125
|
+
violations.push({ rule, problem: `unknown rule${hint}` });
|
|
126
|
+
}
|
|
127
|
+
if (n > 1)
|
|
128
|
+
violations.push({ rule, problem: `listed ${n} times in the manifest — keep a single row per rule` });
|
|
129
|
+
}
|
|
130
|
+
const byRule = new Map(rows.map((r) => [r.rule, r]));
|
|
70
131
|
for (const rule of expected) {
|
|
71
132
|
const row = byRule.get(rule);
|
|
72
133
|
if (!row) {
|
|
73
|
-
violations.push({ rule, problem: "not accounted for in the Rule conformance manifest" });
|
|
134
|
+
violations.push({ rule, problem: "not accounted for in the Rule conformance manifest — add a row: applied with a file:line/test, deviated with an ADR, or n/a with a reason" });
|
|
74
135
|
continue;
|
|
75
136
|
}
|
|
76
137
|
if (!VALID_STATUS.has(row.status)) {
|
|
@@ -78,11 +139,11 @@ export function conformance(missionDir, phaseId, deliverable) {
|
|
|
78
139
|
continue;
|
|
79
140
|
}
|
|
80
141
|
if (row.status === "applied" && !row.evidence)
|
|
81
|
-
violations.push({ rule, problem: "applied without an evidence pointer" });
|
|
142
|
+
violations.push({ rule, problem: "applied without an evidence pointer — put a file:line or a test in the Evidence column" });
|
|
82
143
|
if (row.status === "deviated" && !adrExists(missionDir, row.evidence))
|
|
83
|
-
violations.push({ rule, problem: "deviated but no matching ADR in runward/adr/" });
|
|
84
|
-
if (row.status === "n/a" &&
|
|
85
|
-
violations.push({ rule, problem: "n/a
|
|
144
|
+
violations.push({ rule, problem: "deviated but no matching ADR in runward/adr/ — reference an ADR that exists there" });
|
|
145
|
+
if (row.status === "n/a" && trivialReason(row.evidence))
|
|
146
|
+
violations.push({ rule, problem: "n/a with an empty or placeholder reason — give a real one-line reason why it does not apply here" });
|
|
86
147
|
}
|
|
87
148
|
return { expected, violations };
|
|
88
149
|
}
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
/** Total number of craft rules shipped under templates/rules/. */
|
|
2
|
-
export const EXPECTED_RULES =
|
|
2
|
+
export const EXPECTED_RULES = 54;
|
|
3
|
+
/** Gate adapters shipped under templates/adapters/ (ADR-0012): one per harness seam, plus the port-contract README. */
|
|
4
|
+
export const EXPECTED_ADAPTERS = 4;
|
|
5
|
+
/** Routed-count floor: minimum CRITICAL/HIGH rules mapped to each build phase (ADR-0002).
|
|
6
|
+
* Lowering a floor is a deliberate, tracked edit — the `phases:` mapping cannot be silently
|
|
7
|
+
* stripped to make `check --strict` pass vacuously. */
|
|
8
|
+
export const EXPECTED_MAPPED = { architect: 6, floor: 10, govern: 10 };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function runHooks(missionDir, phase, cwd) {
|
|
5
|
+
const result = { ran: 0, failed: [] };
|
|
6
|
+
const path = join(missionDir, "hooks.json");
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return result;
|
|
9
|
+
let cfg;
|
|
10
|
+
try {
|
|
11
|
+
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
for (const cmd of cfg[phase] ?? []) {
|
|
17
|
+
result.ran++;
|
|
18
|
+
try {
|
|
19
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
result.failed.push(cmd);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
package/dist/lib/paths.js
CHANGED
|
@@ -24,5 +24,5 @@ export const MISSION_LAYOUT = {
|
|
|
24
24
|
};
|
|
25
25
|
export const WORKFLOWS = [
|
|
26
26
|
"method", "frame", "architect", "floor", "iterate",
|
|
27
|
-
"govern", "handover", "brownfield", "review", "decision-loop",
|
|
27
|
+
"govern", "handover", "brownfield", "review", "decision-loop", "verify",
|
|
28
28
|
];
|
package/package.json
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runward",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
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
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agentic",
|
|
7
|
+
"ai-agents",
|
|
8
|
+
"spec-driven",
|
|
9
|
+
"delivery",
|
|
10
|
+
"governance",
|
|
11
|
+
"llm",
|
|
12
|
+
"architecture"
|
|
13
|
+
],
|
|
6
14
|
"author": "Thibault Souris",
|
|
7
15
|
"license": "MIT",
|
|
8
|
-
"repository": {
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/stranxik/runward.git"
|
|
19
|
+
},
|
|
9
20
|
"homepage": "https://runward.dev",
|
|
10
|
-
"bugs": {
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/stranxik/runward/issues"
|
|
23
|
+
},
|
|
11
24
|
"type": "module",
|
|
12
|
-
"bin": {
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
"bin": {
|
|
26
|
+
"runward": "dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"templates",
|
|
31
|
+
"README.md",
|
|
32
|
+
"NOTICE.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
15
37
|
"scripts": {
|
|
16
38
|
"build": "tsc",
|
|
17
39
|
"test": "npm run build && node test/smoke.js",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Gate adapters
|
|
2
|
+
|
|
3
|
+
The runward gate is a **port**. Its whole contract is an exit code:
|
|
4
|
+
|
|
5
|
+
| Exit | Meaning |
|
|
6
|
+
|------|---------|
|
|
7
|
+
| `0` | current gate clean — every expected deliverable is filled (and, under `--strict`, every CRITICAL/HIGH rule mapped to a build phase is accounted for) |
|
|
8
|
+
| `1` | gaps — a deliverable is not filled, a `--strict` rule-conformance gap remains, or an enabled hook failed |
|
|
9
|
+
| `2` | no `runward/` mission found here or above |
|
|
10
|
+
|
|
11
|
+
A human reads that verdict by typing `runward check`. These adapters let a **harness** read the same verdict at the moment it matters — a commit, a CI run, an agent finishing a turn — without runward running, watching, or installing anything.
|
|
12
|
+
|
|
13
|
+
> **runward never wires these for you.** Each file below is an inert sample. Nothing here executes on `init`, on clone, or on `check`. You copy it into your harness, in a repo you trust. This is the same opt-in posture as the `--hooks` seam (ADR-0008), one step stronger: runward does not even run these — it hands them over.
|
|
14
|
+
|
|
15
|
+
The command inside every adapter is runward's own deterministic, zero-LLM gate — not arbitrary shell. Choose `runward check` for the deliverable audit, or `runward check --strict` to also enforce the floor rule-conformance manifest.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `pre-commit` — block a commit on an open gate
|
|
20
|
+
|
|
21
|
+
Copy it into your repo's git hooks and make it executable:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
cp runward/adapters/pre-commit .git/hooks/pre-commit
|
|
25
|
+
chmod +x .git/hooks/pre-commit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or keep hooks in-tree and point git at them:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
mkdir -p .githooks && cp runward/adapters/pre-commit .githooks/pre-commit
|
|
32
|
+
chmod +x .githooks/pre-commit
|
|
33
|
+
git config core.hooksPath .githooks
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A non-zero exit aborts the commit. Bypass a single commit with `git commit --no-verify` when you have a reason.
|
|
37
|
+
|
|
38
|
+
## `github-actions.yml` — the gate as a required check
|
|
39
|
+
|
|
40
|
+
Copy the job into a workflow under `.github/workflows/` (or merge it into an existing one). Make it a required status check on your protected branch so no gap merges. This is the audit-evidence seam for a regulated pipeline: a dated, versioned record that the gate passed on every merge.
|
|
41
|
+
|
|
42
|
+
## `claude-code-settings.json` — run the gate when the agent stops
|
|
43
|
+
|
|
44
|
+
Merge the `hooks` block into your `.claude/settings.json` (or `.claude/settings.local.json`). The `Stop` hook runs the gate when the agent finishes a turn and surfaces the verdict in the loop — so an agent can no longer close out with the gate never run.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
Adapters are runward-owned templates: `runward update` refreshes them, `runward doctor` verifies them. They are never mission state. Add a new harness by dropping a new sample here — the port contract above does not change.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "runward gate — Claude Code adapter (sample). Inert until you install it: see runward/adapters/README.md. Merge the hooks block below into .claude/settings.json (or settings.local.json). The Stop hook runs the gate when the agent finishes a turn and surfaces the verdict in the loop. Exit codes (the port contract): 0 clean, 1 gaps, 2 no mission found. Use 'runward check --strict' to also enforce the floor rule-conformance manifest; drop --strict for the deliverable audit only.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"Stop": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "runward check --strict 2>&1 || true"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# runward gate — CI adapter (sample).
|
|
2
|
+
# Inert until you install it: see runward/adapters/README.md.
|
|
3
|
+
# Copy this job into a workflow under .github/workflows/, then make it a
|
|
4
|
+
# required status check on your protected branch so no gap merges.
|
|
5
|
+
#
|
|
6
|
+
# The gate is deterministic and zero-LLM: no secrets, no model key needed.
|
|
7
|
+
# Exit codes (the port contract): 0 clean · 1 gaps · 2 no mission found.
|
|
8
|
+
name: runward gate
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
pull_request:
|
|
12
|
+
push:
|
|
13
|
+
branches: [main]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
gate:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: 20
|
|
23
|
+
# Use --strict to also enforce the floor rule-conformance manifest.
|
|
24
|
+
# Drop --strict for the deliverable audit only.
|
|
25
|
+
- name: runward check
|
|
26
|
+
run: npx --yes runward check --strict
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# runward gate — git pre-commit adapter (sample).
|
|
3
|
+
# Inert until you install it: see runward/adapters/README.md.
|
|
4
|
+
# cp runward/adapters/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
|
5
|
+
#
|
|
6
|
+
# Blocks the commit if the runward gate is not clean.
|
|
7
|
+
# Exit codes (the port contract): 0 clean · 1 gaps · 2 no mission found.
|
|
8
|
+
# Bypass one commit with: git commit --no-verify
|
|
9
|
+
|
|
10
|
+
set -e
|
|
11
|
+
|
|
12
|
+
# Use --strict to also enforce the floor rule-conformance manifest.
|
|
13
|
+
# Drop --strict for the deliverable audit only.
|
|
14
|
+
if command -v runward >/dev/null 2>&1; then
|
|
15
|
+
runward check --strict
|
|
16
|
+
else
|
|
17
|
+
npx --yes runward check --strict
|
|
18
|
+
fi
|
|
@@ -12,7 +12,7 @@ How to use this file: every structural decision below carries **one sober defaul
|
|
|
12
12
|
| Number of agents | One agent, one loop | The task decomposes into parallelizable subtasks of real complexity, or context isolation is required — several agents under a single orchestrator |
|
|
13
13
|
| Model abstraction | Every model call goes through a single port, one default model | Measured cost or quality spread justifies tiered routing behind the same port; bypassing the port for a direct SDK call requires an ADR of its own |
|
|
14
14
|
| State | Stateless reducer: state in, decision out, nothing hidden inside the process | Audit, replay, or debugging needs history — immutable journal of events plus derived views |
|
|
15
|
-
| Crossing the process boundary | In-process function calls behind ports | A consumer moves to another process — promote the port contract to a versioned network API; nothing else changes |
|
|
15
|
+
| Crossing the process boundary | In-process function calls behind ports | A consumer moves to another process — promote the port contract to a versioned network API (MCP for tool/capability providers, A2A for agent-to-agent federation), pinned by version; nothing else changes |
|
|
16
16
|
| Contract evolution | Versioned contracts, additive changes only, consumers read tolerantly (unknown fields ignored) | A breaking change is unavoidable — expand-then-contract: serve both shapes, migrate every consumer, then retire the old one |
|
|
17
17
|
| Memory & forgetting | Score each memory for value and decay; invalidate rather than delete, so nothing silently disappears | Memory grows past useful recall — consolidate into summaries that keep pointers back to their sources |
|
|
18
18
|
| Evaluation | Deterministic tests for deterministic code, plus a guarded continuous evaluation loop for model behavior | The loop is stable and trusted — allow bounded auto-tuning: parameters only, inside hard limits, owned by a human |
|
|
@@ -23,7 +23,7 @@ How to use this file: every structural decision below carries **one sober defaul
|
|
|
23
23
|
| Non-critical vs sensitive | Non-critical dependencies fail open: the feature degrades, the system continues | The action touches money, private data leaving the system, or anything irreversible — fail closed: no guard response, no action |
|
|
24
24
|
| Delegation autonomy | Bounded autonomy: the agent acts within templates, budgets, and pre-approved action lists | Evidence of consistent reliability on a class of actions — widen the bounds for that class, one ADR at a time |
|
|
25
25
|
| Untrusted input | Least privilege everywhere, human approval on consequential actions, and never all three of: private data, untrusted content, external egress (2-of-3 rule) | A workflow claims to need all three — re-architect to break the triad: isolate, sanitize, or gate the exfiltration channel |
|
|
26
|
-
| Agent identity | Each agent runs as its own principal,
|
|
26
|
+
| Agent identity | Each agent runs as its own principal — one scoped identity, short-lived least-privilege credentials, explicit ownership and audit trail (OAuth 2.1/PKCE, SPIFFE, Entra Agent ID) | The agent must act on behalf of a user — explicit, bounded, revocable delegation; never shared credentials |
|
|
27
27
|
| Execution secrets | The agent holds only a proxy token; real keys are injected at the network boundary, outside the model's reach | A target system cannot sit behind the proxy — isolate that call in a dedicated adapter the agent cannot introspect |
|
|
28
28
|
| Shared brick | Consumed through a port; discovered through an index — and the index stays an index, not a brain | A brick gains multiple consumers — version its contract and run it as a product, still behind the port |
|
|
29
29
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This note is a **kit of default adapters**: enough to ship a first increment fast, without reopening every arbitration from scratch. Read it for what it is — and for what it is not.
|
|
4
4
|
|
|
5
|
-
> **Framing warning.** These choices are **reversible adapter decisions**, not dogma. The boundary is the domain ports and the inter-process tool protocol; behind it, an adapter's language and a store's technology are implementation details you replace without touching the business. A default holds until an objective trigger commands a change. The transverse rule is constant: start simple, isolate by contract, add complexity only on evidence.
|
|
5
|
+
> **Framing warning.** These choices are **reversible adapter decisions**, not dogma. The boundary is the domain ports and the inter-process tool protocol; behind it, an adapter's language and a store's technology are implementation details you replace without touching the business. A default holds until an objective trigger commands a change. The transverse rule is constant: start simple, isolate by contract, add complexity only on evidence. When a port's contract is a public standard — the tool protocol, the telemetry convention, the identity scheme — name the standard as the default and **pin its version in an ADR**: these specs revise on their own cadence, so isolate them behind the port and re-test on each revision. runward references and pins the standard; it never implements it, exports the telemetry, or ships the runtime.
|
|
6
6
|
|
|
7
7
|
The runnable implementation of these defaults lives in the reference floor (`floor-ts/` in this repo). This note decides; the floor shows.
|
|
8
8
|
|
|
@@ -13,11 +13,12 @@ The runnable implementation of these defaults lives in the reference floor (`flo
|
|
|
13
13
|
| Layer | Recommended default | Evolution trigger |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| Core language | One typed language for orchestration and interface (e.g. TypeScript) | Never without a technical reason; polyglot goes through a sidecar or a service, never a mix inside the core. |
|
|
16
|
-
| Specialized capability | Sidecar in the library's language, exposed through the tool protocol | As soon as a capability depends on a mature ecosystem in another language (browser automation, scientific computing, specialized models). |
|
|
16
|
+
| Specialized capability | Sidecar in the library's language, exposed through the tool protocol (**MCP** by default, version-pinned; a consumed server pinned by version + hash) | As soon as a capability depends on a mature ecosystem in another language (browser automation, scientific computing, specialized models). |
|
|
17
17
|
| Hot-path component | Stay in the core; compiled service behind a contract only on evidence | Latency or throughput proven insufficient on the hot path. |
|
|
18
18
|
| Model gateway | Single port, direct SDK, three tiers (fast / balanced / deep), governance and routing centralized | Never a heavy chain framework by default; refine routing when a task's measured quality demands it. |
|
|
19
19
|
| Persistence | Local first, in-memory single-instance state, immutable journal plus derived working view | Shared store as soon as a multi-instance trigger appears (load, availability, state shared across processes). |
|
|
20
|
-
| Observability | Structured logs, cycle events, per-call metrics, propagated request id | Active from the first increment; it gets completed, never retrofitted. |
|
|
20
|
+
| Observability | Structured logs, cycle events, per-call metrics, propagated request id; **OpenTelemetry GenAI** semantic conventions as the span/metric contract | Active from the first increment; it gets completed, never retrofitted. |
|
|
21
|
+
| Identity & delegation | One scoped identity per agent, short-lived credentials, explicit ownership and lifecycle (**OAuth 2.1/PKCE, SPIFFE, Entra Agent ID**) | Harden on an objective trigger (privilege boundary, non-human-identity sprawl, regulated audit), locked in an ADR. |
|
|
21
22
|
| Tests & evaluation | Pyramid: unit without network, schema contract, integration through the DI container, behavioral evaluation at the top, as a continuous loop | Re-run the evaluation bench on any change touching memory, prompt or routing. |
|
|
22
23
|
|
|
23
24
|
---
|
|
@@ -30,7 +31,7 @@ One typed language for the agentic core (orchestrator, tool registry, use cases)
|
|
|
30
31
|
|
|
31
32
|
### Specialized capability
|
|
32
33
|
|
|
33
|
-
When a capability depends on a mature ecosystem elsewhere, isolate it in its own process, consumed by the core as a tool provider through the tool protocol. The cross-language boundary stays sharp and stable. **Trigger:** a mature library lives in another ecosystem and no acceptable equivalent exists in the core language.
|
|
34
|
+
When a capability depends on a mature ecosystem elsewhere, isolate it in its own process, consumed by the core as a tool provider through the tool protocol. The default protocol is **MCP**, pinned to a spec version behind the port, with a consumed server pinned by version and hash (see the `security-mcp-server-pinning` rule) — the spec revises on its own cadence, so it lives behind the port and is re-tested on each revision. The cross-language boundary stays sharp and stable. Cross-**agent** federation, when a single agent no longer suffices, speaks **A2A** — a deferred default crossed on a distributed-topology trigger, never a starting choice. **Trigger:** a mature library lives in another ecosystem and no acceptable equivalent exists in the core language.
|
|
34
35
|
|
|
35
36
|
### Hot-path component
|
|
36
37
|
|
|
@@ -50,7 +51,11 @@ The agent is a stateless reducer; **state lives outside, in three layers**: an i
|
|
|
50
51
|
|
|
51
52
|
### Observability
|
|
52
53
|
|
|
53
|
-
Structured logs, cycle events, per-model-call metrics, and a request id propagated everywhere, including to sub-agents through parent/child lineage. **The same trace feeds the dashboard, the continuous behavioral evaluation, and provenance.** Explicit cost caps per root task, stop-and-synthesize on overrun. **Trigger:** observability gets completed, never retrofitted; unfolding a consolidated memory back to its raw facts becomes necessary as soon as a regulated audit demands it.
|
|
54
|
+
Structured logs, cycle events, per-model-call metrics, and a request id propagated everywhere, including to sub-agents through parent/child lineage. **The same trace feeds the dashboard, the continuous behavioral evaluation, and provenance.** The span and metric shape follows the **OpenTelemetry GenAI** semantic conventions (`invoke_agent`, `execute_tool`), pinned while the spec is experimental: runward prescribes the port and the span contract; the operator plugs the backend (a managed GenAI observability, an open collector) as an adapter — runward exports nothing itself. Explicit cost caps per root task, stop-and-synthesize on overrun. **Trigger:** observability gets completed, never retrofitted; unfolding a consolidated memory back to its raw facts becomes necessary as soon as a regulated audit demands it.
|
|
55
|
+
|
|
56
|
+
### Identity & delegation
|
|
57
|
+
|
|
58
|
+
Agents are non-human identities, soon far outnumbering the humans that run them; treating them with legacy IAM is where privilege abuse begins. Default: **one scoped identity per agent**, short-lived least-privilege credentials, an explicit owner and lifecycle — no shared secrets, no standing broad grants. The contract follows the converging neutral standards (**OAuth 2.1/PKCE** for delegated access, **SPIFFE** or **Entra Agent ID** for workload identity, the IETF `agent_assertion` draft for agent-to-service claims), named as the default and pinned in an ADR. runward references the standard; it never becomes an identity broker. **Trigger:** harden the scheme on an objective signal (a new privilege boundary, non-human-identity sprawl, a regulated audit), locked in an ADR — never built in on day one.
|
|
54
59
|
|
|
55
60
|
### Tests & evaluation
|
|
56
61
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Background Job Guardrails (Retry, Idempotency, Bounded Concurrency, Job Observability)
|
|
3
3
|
impact: HIGH
|
|
4
|
+
asi: [ASI08]
|
|
4
5
|
phases: [govern]
|
|
5
6
|
impactDescription: The four non-negotiables of any background job — bounded retry per step, idempotency under concurrency, capped and partitioned concurrency, and queue lag plus failure rate as first-class metrics
|
|
6
7
|
tags: [async, jobs, idempotency, concurrency, retry, observability]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Secrets at the Network Boundary, Never in the Model
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI03, ASI10]
|
|
4
5
|
phases: [floor, govern]
|
|
5
6
|
impactDescription: Makes secret disclosure structurally impossible by keeping the real key out of the model and the domain
|
|
6
7
|
tags: [security, secrets, configuration, boundary]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Contract Governance (Versioned, Additive, Expand-then-Contract)
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI04, ASI07]
|
|
4
5
|
phases: [architect]
|
|
5
6
|
impactDescription: Lets the system evolve without breaking consumers, by governing the contract at the boundary rather than the implementation behind it
|
|
6
7
|
tags: [architecture, contracts, ports, versioning, compatibility]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Memory Carries Provenance; an Untrusted Write Cannot Act Unreviewed
|
|
3
|
+
impact: HIGH
|
|
4
|
+
asi: [ASI06]
|
|
5
|
+
phases: [govern]
|
|
6
|
+
impactDescription: Every stored memory item carries its origin and trust tier; a memory written from untrusted content is quarantined and cannot influence a privileged action without human review
|
|
7
|
+
tags: [security, memory, provenance, poisoning]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Memory carries provenance; an untrusted write cannot act unreviewed
|
|
11
|
+
|
|
12
|
+
> Prompt-injection defense guards the context window at read time. Memory poisoning is the same attack shifted in time: hostile content is written to memory now and replayed as trusted instruction later, long after the injection is out of the window. The write path needs its own guard.
|
|
13
|
+
|
|
14
|
+
**The rule.** Every stored memory item carries provenance: its origin, its trust tier (trusted / derived / untrusted), and its write time. An item written from untrusted content (a retrieved page, a tool output, a user message) is stored **quarantined** — usable as *data* for recall, never promotable to instruction, and never load-bearing for a privileged action without human review. Trust is a property of the write, recorded once and never silently upgraded on read.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
memory.write(item, source):
|
|
18
|
+
item.provenance = { origin: source.id, trust: source.trust, at: now }
|
|
19
|
+
store(item)
|
|
20
|
+
|
|
21
|
+
memory.recall(query) -> items # returns data; provenance travels with each item
|
|
22
|
+
guardPrivilegedAction(item):
|
|
23
|
+
if item.provenance.trust != "trusted":
|
|
24
|
+
require(humanReview) # a quarantined memory cannot act alone
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Boundary.** Deterministic provenance tagging plus a fail-closed guard on the action path — no model judgment of trust. Complements `security-prompt-injection` (read-time), `data-memory-invalidation` (revocation) and `state-event-sourcing` (traceability). Maps to OWASP Agentic ASI06 (memory & context poisoning).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Evaluation Loop (Test + Evaluate, Hold-out, Anchored Judge)
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI08]
|
|
4
5
|
phases: [govern]
|
|
5
6
|
impactDescription: Gives a non-deterministic system a trustworthy way to detect behavioural regressions without letting it optimise its own grader
|
|
6
7
|
tags: [evaluation, testing, llm, quality, governance]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Deterministic Boundary of the Model
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI01]
|
|
4
5
|
phases: [floor]
|
|
5
6
|
impactDescription: Keeps every fact, figure and decision that can be checked out of the model, so the system is verifiable and cannot hallucinate load-bearing values
|
|
6
7
|
tags: [architecture, llm, frontier, grounding, safety, determinism]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Move the Deterministic out of the Model
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI01, ASI02]
|
|
4
5
|
phases: [floor]
|
|
5
6
|
impactDescription: Reduces LLM costs and latency by moving deterministic logic out of LLM calls
|
|
6
7
|
tags: [architecture, llm, cost-optimization, performance]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Tiered Memory Router
|
|
3
3
|
impact: HIGH
|
|
4
|
+
asi: [ASI06]
|
|
4
5
|
impactDescription: Order-of-magnitude latency cut on simple queries by fetching only the memory tier the query needs, measured on a reference system — recalibrate on your traffic
|
|
5
6
|
tags: [patterns, memory, performance, architecture]
|
|
6
7
|
---
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Fail-Open for the Non-Critical, Fail-Closed for the Sensitive
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI02, ASI08]
|
|
4
5
|
phases: [govern]
|
|
5
6
|
impactDescription: Degrades gracefully where safe and denies safely where it matters, instead of applying one failure policy everywhere
|
|
6
7
|
tags: [resilience, error-handling, availability, safety]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pin Every Consumed Tool/MCP Server by Version and Hash
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
asi: [ASI04, ASI10]
|
|
5
|
+
phases: [architect, govern]
|
|
6
|
+
impactDescription: A consumed tool or MCP server is pinned by version and content hash, so a supply-chain swap or rug-pull cannot silently change what the agent may call
|
|
7
|
+
tags: [security, supply-chain, mcp, tools, integrity]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Pin every consumed tool/MCP server by version and hash
|
|
11
|
+
|
|
12
|
+
> Everything the agent calls is part of your attack surface. A server you trusted at approval time can change its tool set after the fact (a "rug-pull"): a tool that summarized yesterday exfiltrates today. Trust the version you reviewed, not the endpoint.
|
|
13
|
+
|
|
14
|
+
**The rule.** Every external tool provider — an MCP server, a plugin, a remote tool endpoint — is pinned to a specific version **and** a content hash of its declared tool set (names, schemas, descriptions, permissions). The pin is recorded in the mission, versioned with the code. On connect, the agent verifies the live tool set against the pinned hash. A mismatch **fails closed**: the tools are not exposed to the model until the change is re-reviewed and the pin is updated on purpose (see `security-tool-change-reapproval`).
|
|
15
|
+
|
|
16
|
+
**Why structured trust is not enough.** A schema tells you a tool is well-formed, not that it is the tool you approved. Only a pinned hash of the declared surface catches a silent redefinition.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
onConnect(server):
|
|
20
|
+
live = hash(server.declaredTools) # names + schemas + descriptions + permissions
|
|
21
|
+
if live != pinned[server].hash:
|
|
22
|
+
reject(server) # fail closed — do NOT expose to the model
|
|
23
|
+
raise ToolSurfaceChanged(server, pinned[server].version)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Boundary.** This is a deterministic integrity check on the tool surface, not a judgment of the tool's behavior. It belongs at the integration boundary (architect) and is enforced at run (govern). It maps to OWASP Agentic ASI04 (supply-chain) and ASI10 (rogue resources).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Untrusted Input, Prompt Injection and the Lethal Trifecta
|
|
3
3
|
impact: CRITICAL
|
|
4
|
+
asi: [ASI01, ASI06]
|
|
4
5
|
phases: [floor, govern]
|
|
5
6
|
impactDescription: Treats prompt injection as a first-rank, structural threat constrained by architecture rather than detected by heuristics
|
|
6
7
|
tags: [security, llm, injection, trifecta, untrusted-input]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Re-approve a Tool on Any Change to Its Signed Definition
|
|
3
|
+
impact: HIGH
|
|
4
|
+
asi: [ASI02, ASI04]
|
|
5
|
+
phases: [govern]
|
|
6
|
+
impactDescription: A change to a registered tool's definition (name, schema, description, permissions) forces re-approval before the agent may call it, closing the rug-pull window
|
|
7
|
+
tags: [security, tools, approval, supply-chain]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Re-approve a tool on any change to its signed definition
|
|
11
|
+
|
|
12
|
+
> Approval is not a one-time event. A tool approved once and never re-checked is a standing invitation to a rug-pull: change the description or widen the permissions after approval, and the agent keeps calling it.
|
|
13
|
+
|
|
14
|
+
**The rule.** The tool registry records, per tool, a signature over its full definition (name, input/output schema, description, declared permissions). Any change to that signature **revokes the prior approval**: the tool moves to a pending state and the agent may not call it until an operator re-approves the new definition. Re-approval is deterministic and human-owned — the registry gates the call, never the model's discretion.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
registry.resolve(toolCall):
|
|
18
|
+
tool = registry[toolCall.name]
|
|
19
|
+
if sign(tool.definition) != tool.approvedSignature:
|
|
20
|
+
tool.state = "pending-reapproval"
|
|
21
|
+
deny(toolCall) # gated by infrastructure, not by the model
|
|
22
|
+
notifyOperator(tool, diff(tool.definition, tool.approvedDefinition))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Boundary.** Deterministic, infrastructure-enforced, operator-owned — it never asks the model to judge whether a change is safe. Pairs with `security-mcp-server-pinning` (pin the surface) and `tools-registry-pattern` (filter by role). Maps to OWASP Agentic ASI02 (tool misuse) and ASI04 (supply-chain).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: State as an Immutable Journal (Event Sourcing, Replay, Provenance)
|
|
3
3
|
impact: HIGH
|
|
4
|
+
asi: [ASI06]
|
|
4
5
|
phases: [floor]
|
|
5
6
|
impactDescription: Makes the agent a stateless reducer over an external, replayable journal, so truth, audit and recovery are structural rather than bolted on
|
|
6
7
|
tags: [state, event-sourcing, replay, provenance, memory]
|
|
@@ -19,13 +19,14 @@ Use this workflow when a delivery approaches its end or a team must become self-
|
|
|
19
19
|
|
|
20
20
|
**Enforce the entry rule.** You transfer autonomy, not documents. Handover is not a folder delivered; it is proven by the team repeating a task alone. Until that proof exists, this phase is open.
|
|
21
21
|
|
|
22
|
-
**Assemble the handover kit.**
|
|
22
|
+
**Assemble the handover kit.** Six components:
|
|
23
23
|
|
|
24
24
|
1. **Reusable assets.** The populated scaffold, the adapters, the versioned contract schemas, the evaluation sets. Things that replay and get reused, not just things that get read.
|
|
25
25
|
2. **The architecture note.** Boundaries, ports, integration protocol, the named target, and the default topology with its triggers. The map that says why the system is shaped this way.
|
|
26
26
|
3. **The ADR journal.** Every structuring decision locked in Context / Decision / Consequences form, with the discarded alternative and the reevaluation trigger. The team learns not only what was decided, but under which signal to reopen it.
|
|
27
27
|
4. **The operations runbook.** How to start, observe, debug, resume from a checkpoint, switch a model provider, rerun the evaluation bench, process a suspended approval. Operating gestures, not theory. Use `mission/runbook.md`.
|
|
28
28
|
5. **Proof in the code.** Unit and contract tests, evaluation scenarios, replayable traces — the demonstration that the principles are not speculative; they live in the code.
|
|
29
|
+
6. **The agent charter (`AGENTS.md`), finalized as the leave-behind.** The `AGENTS.md` written at `init` becomes the receiving team's charter, in the standard format every harness already reads (Codex, Copilot, Cursor, Gemini, Amp, and Claude Code via `@AGENTS.md`): the craft rules that govern the system, the **judgment boundaries** (what the operator decides versus what an agent may do), the verification commands (including `runward check --strict`), and the never/PR rules. Finalize it so any harness the team later adopts is productive from day one — the leave-behind is portable, not tied to your tool.
|
|
29
30
|
|
|
30
31
|
**Run the transfer, don't just deliver it.**
|
|
31
32
|
|
|
@@ -39,7 +40,7 @@ Use this workflow when a delivery approaches its end or a team must become self-
|
|
|
39
40
|
|
|
40
41
|
## Definition of Done
|
|
41
42
|
|
|
42
|
-
- Handover kit complete: assets, architecture note, ADR journal, runbook, proof in code.
|
|
43
|
+
- Handover kit complete: assets, architecture note, ADR journal, runbook, proof in code, and the finalized `AGENTS.md` charter (the harness-neutral leave-behind).
|
|
43
44
|
- Transfer sessions held with the team operating, not observing.
|
|
44
45
|
- The team has repeated at least one real task end to end, alone — demonstrated, witnessed, recorded.
|
|
45
46
|
- Recurring patterns escalated and fed back into the framework's templates.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Verify — Advisory Cite-vs-Apply Review (above the gate, never in it)
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Use this **after** `runward check --strict` is green, and before you cross the gate as the operator. The deterministic gate proves a decision was *traced* for every CRITICAL/HIGH rule (applied with a pointer, deviated with an ADR, n/a with a reason). It cannot prove the code an `applied` row points at **actually applies** the rule rather than merely citing it — that is a semantic judgment. This workflow makes that judgment, adversarially, to inform yours.
|
|
6
|
+
|
|
7
|
+
It is **advisory**. It produces findings, never a verdict that gates. `runward check --strict` stays the load-bearing decision; you cross it. See [ADR-0007].
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
|
|
11
|
+
- The `Rule conformance` manifests (`floor.md`, `architecture.md`, `governance/threat-model.md`).
|
|
12
|
+
- The code each `applied` row points at.
|
|
13
|
+
|
|
14
|
+
## Procedure
|
|
15
|
+
|
|
16
|
+
**Challenge each `applied` claim, adversarially.** For every `applied` row, open its evidence (the `file:line` or test) and default to skepticism: *does this code actually apply the rule it names, or does it only cite it?* Judge through distinct lenses — correctness, does-it-reproduce, security-relevance — and, where you can, run this pass on a **different model** than the one that built the floor, so it does not agree with itself.
|
|
17
|
+
|
|
18
|
+
**Return a finding per row**: `confirmed` (the code applies the rule), `cited-not-applied` (the row claims applied but the code does not carry the rule), or `uncertain` (needs a human look). Deduplicate overlapping findings; group them by severity.
|
|
19
|
+
|
|
20
|
+
**Hand the findings to the operator.** They are input to the operator's gate decision, not a gate of their own. A `cited-not-applied` finding is a prompt to fix the code (or downgrade the row to `deviated`/`n/a`) and re-run `check --strict` — never a silent override of the deterministic gate.
|
|
21
|
+
|
|
22
|
+
## Definition of Done
|
|
23
|
+
|
|
24
|
+
- Every `applied` row reviewed, with a confirmed / cited-not-applied / uncertain finding.
|
|
25
|
+
- Findings handed to the operator; no exit code produced, no gate blocked.
|
|
26
|
+
- Any `cited-not-applied` finding routed back to the code and re-checked deterministically.
|
|
27
|
+
|
|
28
|
+
## Anti-patterns
|
|
29
|
+
|
|
30
|
+
- Treating a clean `verify` pass as "the gate passed" — it is advisory; the deterministic `check --strict` is the gate.
|
|
31
|
+
- Wiring `verify` into CI as a blocking step, or into an exit code — it never blocks.
|
|
32
|
+
- Running `verify` **instead of** `check --strict` — it sits above the deterministic gate, it does not replace it.
|
|
33
|
+
- Running it on the same model that wrote the code and trusting the agreement — use a different lens, ideally a different model.
|