runward 0.14.2 → 0.16.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 +12 -5
- package/dist/cli.js +27 -2
- package/dist/commands/check.js +63 -21
- package/dist/commands/compliance.js +20 -8
- package/dist/commands/init.js +14 -2
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/rules.js +76 -0
- package/dist/lib/compliance.js +40 -50
- package/dist/lib/conformance.js +62 -15
- package/dist/lib/constants.js +3 -3
- package/dist/lib/evidence.js +222 -0
- package/dist/lib/manifest-sync.js +111 -0
- package/dist/lib/mission.js +4 -1
- package/dist/lib/paths.js +3 -0
- package/dist/lib/regimes.js +32 -0
- package/dist/lib/rules.js +44 -0
- package/dist/lib/tools.js +16 -1
- package/dist/lib/verify-findings.js +24 -0
- package/examples/request-triage/AGENTS.md +33 -0
- package/examples/request-triage/code/README.md +74 -0
- package/examples/request-triage/code/package.json +19 -0
- package/examples/request-triage/code/src/adapters/hardcoded-account-registry.adapter.ts +23 -0
- package/examples/request-triage/code/src/adapters/in-memory-routing.adapter.ts +79 -0
- package/examples/request-triage/code/src/adapters/in-memory-triage-log.adapter.ts +42 -0
- package/examples/request-triage/code/src/adapters/keyword-model.adapter.ts +57 -0
- package/examples/request-triage/code/src/core/application/triage-request.usecase.ts +195 -0
- package/examples/request-triage/code/src/core/domain/guard.ts +121 -0
- package/examples/request-triage/code/src/core/domain/triage.ts +88 -0
- package/examples/request-triage/code/src/core/ports/account-registry.port.ts +15 -0
- package/examples/request-triage/code/src/core/ports/model-provider.port.ts +26 -0
- package/examples/request-triage/code/src/core/ports/routing.port.ts +32 -0
- package/examples/request-triage/code/src/core/ports/triage-log.port.ts +33 -0
- package/examples/request-triage/code/src/demo.ts +80 -0
- package/examples/request-triage/code/test/triage.test.ts +243 -0
- package/examples/request-triage/code/tsconfig.json +14 -0
- package/examples/request-triage/runward/adr/ADR-0003-port-placement-and-sovereignty.md +44 -0
- package/examples/request-triage/runward/architecture.md +4 -3
- package/examples/request-triage/runward/decision-matrix.md +1 -1
- package/examples/request-triage/runward/execution-topology.md +3 -3
- package/examples/request-triage/runward/floor.md +4 -4
- package/examples/request-triage/runward/handover.md +47 -0
- package/package.json +21 -8
- package/regimes/eu-ai-act@2024-1689.json +31 -0
- package/regimes/iso-42001@2023.json +22 -0
- package/regimes/nist-ai-rmf@1.0.json +19 -0
- package/templates/adapters/README.md +8 -0
- package/templates/adapters/gitlab-ci.yml +18 -0
- package/templates/adapters/kiro-hooks.json +12 -0
- package/templates/mission/architecture.md +2 -0
- package/templates/mission/execution-topology.md +2 -0
- package/templates/mission/floor.md +2 -0
- package/templates/mission/handover.md +52 -0
- package/templates/mission/threat-model.md +2 -0
- package/templates/rules/frontier-deterministic-boundary.md +1 -0
- package/templates/rules/handover-agents-charter-final.md +45 -0
- package/templates/rules/handover-redone-task-proof.md +46 -0
- package/templates/rules/handover-runbook-executable.md +50 -0
- package/templates/rules/handover-succession-named.md +46 -0
- package/templates/targets/AGENTS.md +2 -1
- package/templates/workflows/floor.md +1 -1
- package/templates/workflows/handover.md +7 -2
- package/templates/workflows/verify.md +2 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Test floor of the triage example: classification, deterministic guard on
|
|
2
|
+
// extracted fields, routing, fail-closed compliance (suspend-and-rehydrate),
|
|
3
|
+
// abstention on unknown. Everything deterministic: no model key, no network.
|
|
4
|
+
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { TriageRequestUseCase } from "../src/core/application/triage-request.usecase.ts";
|
|
8
|
+
import { KeywordModelAdapter } from "../src/adapters/keyword-model.adapter.ts";
|
|
9
|
+
import { HardcodedAccountRegistry } from "../src/adapters/hardcoded-account-registry.adapter.ts";
|
|
10
|
+
import {
|
|
11
|
+
InMemoryRoutingAdapter,
|
|
12
|
+
ProvenanceRefusalError,
|
|
13
|
+
ApprovalMissingError,
|
|
14
|
+
} from "../src/adapters/in-memory-routing.adapter.ts";
|
|
15
|
+
import { InMemoryTriageLog } from "../src/adapters/in-memory-triage-log.adapter.ts";
|
|
16
|
+
import { parseDeadline } from "../src/core/domain/guard.ts";
|
|
17
|
+
import type { TriageModelPort } from "../src/core/ports/model-provider.port.ts";
|
|
18
|
+
import type { TriageRecord } from "../src/core/domain/triage.ts";
|
|
19
|
+
|
|
20
|
+
const fixedClock = { nowIso: () => "2026-06-01T00:00:00.000Z" };
|
|
21
|
+
|
|
22
|
+
function buildRig(model?: TriageModelPort) {
|
|
23
|
+
const routing = new InMemoryRoutingAdapter();
|
|
24
|
+
const log = new InMemoryTriageLog();
|
|
25
|
+
const useCase = new TriageRequestUseCase({
|
|
26
|
+
model: model ?? new KeywordModelAdapter(),
|
|
27
|
+
registry: new HardcodedAccountRegistry(),
|
|
28
|
+
routing,
|
|
29
|
+
log,
|
|
30
|
+
clock: fixedClock,
|
|
31
|
+
});
|
|
32
|
+
return { useCase, routing, log };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Classification and routing ---------------------------------------------
|
|
36
|
+
|
|
37
|
+
test("support request: classified and routed to the support queue", async () => {
|
|
38
|
+
const { useCase } = buildRig();
|
|
39
|
+
const res = await useCase.triage({
|
|
40
|
+
requestId: "r1",
|
|
41
|
+
senderAddress: "jane@acme.example",
|
|
42
|
+
body: "The export keeps failing with an error, please help. Account ACC-1001.",
|
|
43
|
+
});
|
|
44
|
+
assert.equal(res.record.category, "support");
|
|
45
|
+
assert.equal(res.status, "routed");
|
|
46
|
+
assert.equal(res.targetQueue, "support-queue");
|
|
47
|
+
assert.ok(res.ticketRef);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("sales request: classified and routed to the sales queue", async () => {
|
|
51
|
+
const { useCase } = buildRig();
|
|
52
|
+
const res = await useCase.triage({
|
|
53
|
+
requestId: "r2",
|
|
54
|
+
senderAddress: "buyer@globex.example",
|
|
55
|
+
body: "We would like a pricing quote for 50 seats.",
|
|
56
|
+
});
|
|
57
|
+
assert.equal(res.record.category, "sales");
|
|
58
|
+
assert.equal(res.status, "routed");
|
|
59
|
+
assert.equal(res.targetQueue, "sales-queue");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("compliance beats overlapping signals: a privacy request that also says 'help' is compliance", async () => {
|
|
63
|
+
const { useCase } = buildRig();
|
|
64
|
+
const res = await useCase.triage({
|
|
65
|
+
requestId: "r3",
|
|
66
|
+
senderAddress: "dpo@initech.example",
|
|
67
|
+
body: "Please help us process this GDPR data deletion request.",
|
|
68
|
+
});
|
|
69
|
+
assert.equal(res.record.category, "compliance");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- Abstention ---------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test("unknown category: the system abstains, nothing is routed", async () => {
|
|
75
|
+
const { useCase, routing } = buildRig();
|
|
76
|
+
const res = await useCase.triage({
|
|
77
|
+
requestId: "r4",
|
|
78
|
+
senderAddress: "someone@example.org",
|
|
79
|
+
body: "Following up on the thing from last month.",
|
|
80
|
+
});
|
|
81
|
+
assert.equal(res.record.category, "unknown");
|
|
82
|
+
assert.equal(res.status, "needs_review");
|
|
83
|
+
assert.equal(res.targetQueue, "review-queue");
|
|
84
|
+
assert.equal(routing.assignments, 0); // abstention: no action taken
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- Deterministic guard on extracted fields (ADR-0002) -----------------------
|
|
88
|
+
|
|
89
|
+
test("guard: an account reference that resolves in the registry is verified", async () => {
|
|
90
|
+
const { useCase } = buildRig();
|
|
91
|
+
const res = await useCase.triage({
|
|
92
|
+
requestId: "r5",
|
|
93
|
+
senderAddress: "jane@acme.example",
|
|
94
|
+
body: "Login is broken for account ACC-1001.",
|
|
95
|
+
});
|
|
96
|
+
assert.equal(res.record.accountRef?.value, "ACC-1001");
|
|
97
|
+
assert.equal(res.record.accountRef?.provenance, "verified");
|
|
98
|
+
assert.equal(res.record.accountRef?.accountName, "Acme Corp");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("guard: a fabricated account reference never routes — escalated to review", async () => {
|
|
102
|
+
const { useCase, routing } = buildRig();
|
|
103
|
+
const res = await useCase.triage({
|
|
104
|
+
requestId: "r6",
|
|
105
|
+
senderAddress: "jane@acme.example",
|
|
106
|
+
body: "Our app crashes constantly. Account ACC-9999.", // not in the registry
|
|
107
|
+
});
|
|
108
|
+
assert.equal(res.status, "needs_review");
|
|
109
|
+
assert.ok(res.reason?.includes("ACC-9999"));
|
|
110
|
+
assert.equal(routing.assignments, 0); // fail-closed: no routing happened
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("guard: the deadline is re-parsed from the source text; the model's proposal is discarded", async () => {
|
|
114
|
+
// Adversarial model stub: proposes a WRONG deadline (and a valid category).
|
|
115
|
+
const lyingModel: TriageModelPort = {
|
|
116
|
+
async propose() {
|
|
117
|
+
return {
|
|
118
|
+
category: "support",
|
|
119
|
+
confidence: "high",
|
|
120
|
+
fields: [{ name: "deadline", value: "2099-12-31", sourceSpan: "made up" }],
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const { useCase } = buildRig(lyingModel);
|
|
125
|
+
const res = await useCase.triage({
|
|
126
|
+
requestId: "r7",
|
|
127
|
+
senderAddress: "jane@acme.example",
|
|
128
|
+
body: "The report is broken and we need it fixed by 2026-07-15.",
|
|
129
|
+
});
|
|
130
|
+
// The system's value comes from the deterministic parser, not the model.
|
|
131
|
+
assert.equal(res.record.deadline?.value, "2026-07-15");
|
|
132
|
+
assert.equal(res.record.deadline?.provenance, "computed");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("guard: the deadline parser rejects well-shaped non-dates", () => {
|
|
136
|
+
assert.equal(parseDeadline("due 2026-13-45, then 2026-02-30"), null);
|
|
137
|
+
assert.equal(parseDeadline("due 2026-02-28"), "2026-02-28");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// --- Fail-closed compliance: suspend and rehydrate ----------------------------
|
|
141
|
+
|
|
142
|
+
test("compliance: suspended awaiting approval, never routed silently", async () => {
|
|
143
|
+
const { useCase, routing, log } = buildRig();
|
|
144
|
+
const res = await useCase.triage({
|
|
145
|
+
requestId: "r8",
|
|
146
|
+
senderAddress: "dpo@initech.example",
|
|
147
|
+
body: "GDPR erasure request for ACC-3003, respond by 2026-08-01.",
|
|
148
|
+
});
|
|
149
|
+
assert.equal(res.status, "suspended");
|
|
150
|
+
assert.equal(routing.assignments, 0); // fail-closed: nothing routed yet
|
|
151
|
+
// The trajectory is serialized: the exact validated record awaits.
|
|
152
|
+
const pending = await log.getPending("r8");
|
|
153
|
+
assert.equal(pending?.record.accountRef?.value, "ACC-3003");
|
|
154
|
+
// The approval summary is deterministic, built from the validated fields.
|
|
155
|
+
assert.ok(pending?.summary.includes("ACC-3003 (verified)"));
|
|
156
|
+
assert.ok(pending?.summary.includes("2026-08-01 (computed)"));
|
|
157
|
+
assert.equal(res.reason, pending?.summary);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("compliance resume approve: routed to the compliance queue with the approval recorded", async () => {
|
|
161
|
+
const { useCase, log } = buildRig();
|
|
162
|
+
await useCase.triage({
|
|
163
|
+
requestId: "r9",
|
|
164
|
+
senderAddress: "dpo@initech.example",
|
|
165
|
+
body: "GDPR data access request for ACC-3003.",
|
|
166
|
+
});
|
|
167
|
+
const resumed = await useCase.resumeTriage("r9", "approve", "ops-coordinator");
|
|
168
|
+
assert.equal(resumed.status, "routed");
|
|
169
|
+
assert.equal(resumed.targetQueue, "compliance-queue");
|
|
170
|
+
assert.ok(resumed.ticketRef);
|
|
171
|
+
assert.equal(await log.getPending("r9"), null); // suspension point cleared
|
|
172
|
+
const journal = await log.read("r9");
|
|
173
|
+
assert.ok(journal.some((e) => e.event === "approval_decision"));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("compliance resume reject: the routing never happens", async () => {
|
|
177
|
+
const { useCase, routing, log } = buildRig();
|
|
178
|
+
await useCase.triage({
|
|
179
|
+
requestId: "r10",
|
|
180
|
+
senderAddress: "dpo@initech.example",
|
|
181
|
+
body: "Privacy complaint escalated by the regulator, account ACC-2002.",
|
|
182
|
+
});
|
|
183
|
+
const resumed = await useCase.resumeTriage("r10", "reject", "ops-coordinator");
|
|
184
|
+
assert.equal(resumed.status, "rejected");
|
|
185
|
+
assert.equal(routing.assignments, 0); // never executed
|
|
186
|
+
assert.equal(await log.getPending("r10"), null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// --- Routing port invariants ---------------------------------------------------
|
|
190
|
+
|
|
191
|
+
test("routing port refuses, fail-closed, a record with model-proposed action-bearing fields", async () => {
|
|
192
|
+
const routing = new InMemoryRoutingAdapter();
|
|
193
|
+
const record: TriageRecord = {
|
|
194
|
+
requestId: "r11",
|
|
195
|
+
category: "support",
|
|
196
|
+
confidence: "high",
|
|
197
|
+
accountRef: { value: "ACC-1001", provenance: "model-proposed" },
|
|
198
|
+
};
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
() => routing.assign(record, "support-queue"),
|
|
201
|
+
ProvenanceRefusalError,
|
|
202
|
+
);
|
|
203
|
+
// And a compliance record without a recorded approval is refused too.
|
|
204
|
+
const compliance: TriageRecord = {
|
|
205
|
+
requestId: "r12",
|
|
206
|
+
category: "compliance",
|
|
207
|
+
confidence: "high",
|
|
208
|
+
};
|
|
209
|
+
await assert.rejects(
|
|
210
|
+
() => routing.assign(compliance, "compliance-queue"),
|
|
211
|
+
ApprovalMissingError,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("routing port is idempotent on requestId: no duplicate tickets", async () => {
|
|
216
|
+
const routing = new InMemoryRoutingAdapter();
|
|
217
|
+
const record: TriageRecord = {
|
|
218
|
+
requestId: "r13",
|
|
219
|
+
category: "support",
|
|
220
|
+
confidence: "high",
|
|
221
|
+
};
|
|
222
|
+
const first = await routing.assign(record, "support-queue");
|
|
223
|
+
const second = await routing.assign(record, "support-queue");
|
|
224
|
+
assert.equal(first.ticketRef, second.ticketRef);
|
|
225
|
+
assert.equal(routing.assignments, 1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// --- Boundary -------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
test("input boundary: a malformed payload is rejected, unknown keys included", async () => {
|
|
231
|
+
const { useCase } = buildRig();
|
|
232
|
+
await assert.rejects(() =>
|
|
233
|
+
useCase.triage({ requestId: "", senderAddress: "a@b.c", body: "x" }),
|
|
234
|
+
);
|
|
235
|
+
await assert.rejects(() =>
|
|
236
|
+
useCase.triage({
|
|
237
|
+
requestId: "r14",
|
|
238
|
+
senderAddress: "a@b.c",
|
|
239
|
+
body: "hello",
|
|
240
|
+
role: "admin",
|
|
241
|
+
} as never),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"allowImportingTsExtensions": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "test/**/*.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ADR-0003: port placement and sovereignty behind the four ports
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check against the org's existing infrastructure inventory, challenge on sovereignty per data class, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The architecture note names four ports; two of their adapters run beyond the process. The model adapter calls a vendor-hosted deployment, and the routing adapter writes into the ticketing system the organization already operates. Placement is an adapter decision behind a stable port — the domain does not change with it — but it is a decision, and it carries the mission's most sensitive constraint: inbound request text may contain personal data, so where each adapter runs decides which jurisdiction and which operator can read it. This ADR is the first member of the mission's infra ADR family (placement, sovereignty, trace export); it locks the placement and sovereignty rows recorded in `execution-topology.md`.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
One placement per port, sovereignty graduated by the class of data crossing it — never a wholesale switch.
|
|
15
|
+
|
|
16
|
+
- **ModelPort → managed model-vendor runtime**, bound to the organization's approved deployment. Residency is enforced at the adapter: the deployment is region-pinned, and request text (internal, possibly personal) never leaves the approved region. Sovereignty: **raised**. The port keeps the placement reversible — a self-hosted or gateway-fronted deployment is an adapter swap, not a rework.
|
|
17
|
+
- **RoutingPort → existing infrastructure** (the ticketing system of record), reached only through the anticorruption adapter and the deterministic guard (ADR-0002). Only the validated `TriageRecord` (internal business data) crosses; compliance-flagged records are approval-gated. Sovereignty: **standard**.
|
|
18
|
+
- **RequestIntakePort and PersistencePort → in-app**, the sober default; the intake adapter consumes the existing mailbox, the log is in-process and append-only.
|
|
19
|
+
- **No third-party trace export.** Observability is in-app structured logs (`governance/observability-schema.md`). Traces carry request text — exporting them would be a data transfer requiring its own ADR naming recipient, data class and retention.
|
|
20
|
+
|
|
21
|
+
## Alternatives discarded
|
|
22
|
+
|
|
23
|
+
- **Self-hosted model runtime**: discarded at the floor. Operating a model server costs more than the floor's value proof justifies, and the port makes the move cheap later. Trigger named below.
|
|
24
|
+
- **A dedicated intake queue now**: discarded. No volume signal; in-process handling holds the observed load. Premature distribution would buy eventual consistency and idempotence work with no gain.
|
|
25
|
+
- **Exporting traces to a managed observability service**: discarded. Traces contain the prompts and the extracted fields — the most sensitive payloads in the system. Convenience does not clear a data transfer; if the need appears, it is its own decision with recipient, class and retention named.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- **Positive**: every port's placement and sovereignty is a traced, reversible decision; residency is pinned where it is actually enforced (the adapter binding, not a policy document); the two non-in-app placements are auditable from one table.
|
|
30
|
+
- **Negative, accepted**: the model path depends on a vendor runtime — bounded by the port and the approved-deployment binding; the ticketing coupling is real but contained in the anticorruption adapter.
|
|
31
|
+
- **On other boundaries**: `execution-topology.md` §2 records the rows this ADR locks; the usage registry (§3) classes the deployment's risk; the threat model treats both external surfaces as untrusted.
|
|
32
|
+
|
|
33
|
+
## Reevaluation trigger (mandatory, dated)
|
|
34
|
+
|
|
35
|
+
Reopen this decision if any of: a second application needs the same model routing, quotas or audit (→ shared gateway on existing infra); the ticketing system is migrated or multi-tenanted; anyone requests third-party trace export; or the data classification of inbound requests changes (e.g. confirmed regulated data). Until a trigger fires, apply without reopening.
|
|
36
|
+
|
|
37
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: the usage registry review at each iterate gate, and the architecture review before any new consumer of the model path
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- [execution-topology.md](../execution-topology.md) §2 — the port → placement map this ADR locks.
|
|
42
|
+
- [architecture.md](../architecture.md) §2, §3 — the ports and the data residency constraint.
|
|
43
|
+
- [shared-bricks.md](../shared-bricks.md) — the five location families and six criteria used to arbitrate.
|
|
44
|
+
- [ADR-0002](ADR-0002-deterministic-guard-on-extracted-fields.md) — the guard standing before RoutingPort.
|
|
@@ -39,8 +39,8 @@ The output contract is the **TriageRecord v1.0** schema: closed category vocabul
|
|
|
39
39
|
| contracts-governance | applied | §3 TriageRecord v1.0 — versioned, additive, tolerant reader, fail-closed; contracts/ |
|
|
40
40
|
| hexa-architecture | applied | §2 pure triage domain, four ports; code/src/core/ |
|
|
41
41
|
| hexa-adapter-pattern | applied | §3 every dependency behind a port; code/src/adapters/ |
|
|
42
|
-
| hexa-typescript-native | n/a | language deliberately left open at this note (§5); locked at floor kickoff (ADR-
|
|
43
|
-
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002 — dated decisions with reevaluation triggers |
|
|
42
|
+
| hexa-typescript-native | n/a | language deliberately left open at this note (§5); locked at floor kickoff (ADR-0004 pending) |
|
|
43
|
+
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002, adr/ADR-0003 — dated decisions with reevaluation triggers |
|
|
44
44
|
| security-mcp-server-pinning | n/a | the floor consumes no MCP or external tool server; tools are in-process and deterministic |
|
|
45
45
|
|
|
46
46
|
## 5. What stays open
|
|
@@ -61,4 +61,5 @@ Per the framing note: auto-drafted acknowledgments under approval, priority scor
|
|
|
61
61
|
|---|---|
|
|
62
62
|
| Single orchestrator, sequential triage | [ADR-0001](adr/ADR-0001-single-orchestrator.md) |
|
|
63
63
|
| Deterministic guard on extracted fields | [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md) |
|
|
64
|
-
|
|
|
64
|
+
| Port placement and sovereignty (the infra ADR family) | [ADR-0003](adr/ADR-0003-port-placement-and-sovereignty.md) |
|
|
65
|
+
| Core language (open — to be locked at floor kickoff) | ADR-0004 (pending) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Decision Matrix
|
|
2
2
|
|
|
3
|
-
**Mission state (2026-05-12, Architect gate)** — matrix adopted as the arbitration reference for this mission. Positions taken so far, all on the sober default: number of agents → single orchestrator, locked in [ADR-0001](adr/ADR-0001-single-orchestrator.md); untrusted input and deterministic frontier → guard on extracted fields, locked in [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md); core language → open, to be locked at floor kickoff (ADR-
|
|
3
|
+
**Mission state (2026-05-12, Architect gate)** — matrix adopted as the arbitration reference for this mission. Positions taken so far, all on the sober default: number of agents → single orchestrator, locked in [ADR-0001](adr/ADR-0001-single-orchestrator.md); untrusted input and deterministic frontier → guard on extracted fields, locked in [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md); port placement and sovereignty → the two non-in-app placements locked in [ADR-0003](adr/ADR-0003-port-placement-and-sovereignty.md); core language → open, to be locked at floor kickoff (ADR-0004, pending). No trigger has fired; watched signals are tracked in [floor.md](floor.md) §4.
|
|
4
4
|
|
|
5
5
|
How to use this file: every structural decision below carries **one sober default and one explicit trigger**. You start on the default. You only move when the trigger fires — measured, observed, named. **No trigger, no change. Every switch is an ADR** (`runward/adr/`), with the evidence that fired the trigger and a date to re-evaluate. Fill the matrix during the Architect phase; reopen it at every Iterate gate.
|
|
6
6
|
|
|
@@ -11,8 +11,8 @@ The triage domain says *what* the system does (raw request in, validated `Triage
|
|
|
11
11
|
| Port | Adapter (what runs) | Location family | Data class(es) crossing it | Sovereignty level | ADR / evidence | Re-evaluation trigger |
|
|
12
12
|
|---|---|---|---|---|---|---|
|
|
13
13
|
| RequestIntakePort | mailbox / web-form adapter, in-process | In-app, consuming existing infra (mailbox) | inbound request text (internal, may carry personal data) | raised | architecture.md §3 | intake volume outgrows in-process handling → dedicated queue |
|
|
14
|
-
| ModelPort | model adapter bound to the approved deployment | Managed model-vendor runtime | request text (internal / personal) | raised — residency enforced at the adapter, not in the domain | architecture.md §2
|
|
15
|
-
| RoutingPort | anticorruption adapter → ticketing system of record | Existing infrastructure | `TriageRecord` (internal business) | standard; approval-gated for compliance-flagged records |
|
|
14
|
+
| ModelPort | model adapter bound to the approved deployment | Managed model-vendor runtime | request text (internal / personal) | raised — residency enforced at the adapter, not in the domain | ADR-0003 (placement + residency); architecture.md §2 | a second application needs the same routing/quotas → shared model gateway |
|
|
15
|
+
| RoutingPort | anticorruption adapter → ticketing system of record | Existing infrastructure | `TriageRecord` (internal business) | standard; approval-gated for compliance-flagged records | ADR-0003 (placement); ADR-0002 (deterministic guard) | the ticketing system moves or multi-tenants → re-evaluate the adapter placement |
|
|
16
16
|
| PersistencePort | append-only log, in-process | In-app | `TriageRecord` plus provenance (internal) | standard | architecture.md §3 | multi-instance required → externalized store (iterate) |
|
|
17
17
|
|
|
18
18
|
Traces are data: this floor exports none to a third party (see the conformance note below).
|
|
@@ -29,7 +29,7 @@ Risk is classed by deployment, not by platform. This floor is one deployment.
|
|
|
29
29
|
|
|
30
30
|
| Rule | Status | Evidence |
|
|
31
31
|
|---|---|---|
|
|
32
|
-
| topology-port-placement-mapped | applied | §2 map — all four ports placed; the two non-in-app placements (ModelPort → managed vendor runtime
|
|
32
|
+
| topology-port-placement-mapped | applied | §2 map — all four ports placed; the two non-in-app placements (ModelPort → managed vendor runtime, RoutingPort → existing ticketing infra) are locked in ADR-0003 (the infra ADR family) |
|
|
33
33
|
| topology-sovereignty-by-data-class | applied | §2 map — a data class and a sovereignty level per port; request text is bound to the approved model deployment (residency), the `TriageRecord` is kept internal |
|
|
34
34
|
| topology-trace-export-decision | n/a | the floor exports no execution traces to a third party; observability is in-app structured logs per governance/observability-schema.md |
|
|
35
35
|
| topology-usage-registry-present | applied | §3 usage registry — the single prod deployment with its risk class, data classes, action scopes and owner |
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
|
|
20
20
|
| Rule | Status | Evidence |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| frontier-deterministic-boundary | applied | code/src/core/domain/guard.ts — every action-bearing field recomputed/verified (ADR-0002), fail-closed |
|
|
23
|
-
| hexa-move-deterministic-out | applied | code/src/core/domain/guard.ts
|
|
22
|
+
| frontier-deterministic-boundary | applied | file:code/src/core/domain/guard.ts#guardFields; test:code/test/triage.test.ts — every action-bearing field recomputed/verified (ADR-0002), fail-closed |
|
|
23
|
+
| hexa-move-deterministic-out | applied | file:code/src/core/domain/guard.ts#parseDeadline — classification and validation are deterministic, out of the model |
|
|
24
24
|
| config-secrets-boundary | n/a | the illustrative floor runs the deterministic keyword classifier; no provider secret is read in this example code |
|
|
25
25
|
| provider-llm-auto-detection | n/a | only the deterministic keyword adapter ships here; no real provider to auto-detect |
|
|
26
26
|
| security-prompt-injection | applied | threat-model §3 + ADR-0002 — model-proposed values never act; request text is data, not instruction |
|
|
27
27
|
| hexa-architecture | applied | code/src/core/ pure domain behind four ports |
|
|
28
28
|
| hexa-adapter-pattern | applied | code/src/adapters/ — mailbox/web, keyword-model, routing, log behind ports |
|
|
29
|
-
| provider-no-crash-missing-env | applied | code/src/adapters/keyword-model.adapter.ts — deterministic fallback runs with no key |
|
|
30
|
-
| state-event-sourcing | applied | code/src/adapters/in-memory-triage-log.adapter.ts — append-only, keyed by request ID |
|
|
29
|
+
| provider-no-crash-missing-env | applied | file:code/src/adapters/keyword-model.adapter.ts — deterministic fallback runs with no key |
|
|
30
|
+
| state-event-sourcing | applied | file:code/src/adapters/in-memory-triage-log.adapter.ts — append-only, keyed by request ID |
|
|
31
31
|
| tools-scope-atomicity | applied | architecture §2 middleware chain + approval on RoutingPort for compliance records |
|
|
32
32
|
|
|
33
33
|
## 2. Proof against the success criterion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Hand-over Note — Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-07-03 · **Departing builder**: the mission operator · **Receiving side**: operations team (triage product owner) · All names and dates **illustrative**, like the rest of the reference mission.
|
|
4
|
+
|
|
5
|
+
## 1. The kit
|
|
6
|
+
|
|
7
|
+
| Artifact | Where | State | Exercised by the receiving side? |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Recovery runbook | `runbook.md` | ready | 2026-06-30 — ops engineer ran the recovery replay (§3) during the drill |
|
|
10
|
+
| Agent charter (finalized) | `AGENTS.md` (project root) | finalized at hand-over | read and countersigned at the second transfer session |
|
|
11
|
+
| Architecture note + decision journal | `architecture.md` · `adr/` (3 ADRs, triggers dated) | current | walked through, hands on keyboard |
|
|
12
|
+
| Evaluation bench | `cd code && npm test` + the replay set (floor.md §2) | ready | 2026-07-02 — re-run alone (see §2) |
|
|
13
|
+
| Evidence pack | `compliance/` — regenerate with `runward compliance <regime>` | regenerable on demand | generated once by the ops engineer during the drill |
|
|
14
|
+
|
|
15
|
+
## 2. The redone task (the proof)
|
|
16
|
+
|
|
17
|
+
- **Task**: after the illustrative provider incident of 2026-06-29 (gateway timeouts, fallback classifier engaged — runbook §4 first row), re-run the full behavioral proof and the demo, then process the suspended compliance approval that had queued during the incident — end to end, from the leave-behinds alone.
|
|
18
|
+
- **Date / doer**: 2026-07-02, the operations engineer of the receiving team; the builder unavailable by agreement (out of the loop for the day).
|
|
19
|
+
- **Evidence**: file:code/test/triage.test.ts; file:runward/runbook.md#Recovery — the proof re-ran green, the replay recovered the queued state, the suspended approval was released through the approval path (ADR-0002 provenance rules held).
|
|
20
|
+
- **Gaps found**: two, both folded back the same day — the runbook did not name where the review queue persists (added to §3), and the demo's expected output was undocumented (added to the code README).
|
|
21
|
+
|
|
22
|
+
## 3. Succession
|
|
23
|
+
|
|
24
|
+
- **Owner after departure**: the triage product owner (operations) — incidents, cost, credentials, and the three open ADR re-evaluation triggers.
|
|
25
|
+
- **Escalation path**: compliance officer for category/vocabulary questions; the sponsor (Head of Operations) for scope; provider support through the gateway contract.
|
|
26
|
+
- **Review cadence**: the usage registry and the ADR triggers are re-read at the weekly observability review (the guard-escalation rate of ADR-0002 is already on its agenda); the evidence seal is re-taken at each gate crossing.
|
|
27
|
+
- **Credentials boundary**: ticketing and gateway credentials live in the ops vault, injected at the network boundary; the builder's accesses were revoked on 2026-07-03 as part of this note.
|
|
28
|
+
|
|
29
|
+
## 4. Provider-swap drill
|
|
30
|
+
|
|
31
|
+
2026-06-27 — swapped the model adapter from the approved gateway deployment to the deterministic keyword fallback and back (the adapter switch of architecture.md §2), then re-ran the bench: accuracy on the replay set within the expected fallback envelope, provenance rules unaffected. The port held; the domain never noticed. Next drill on the shared-gateway trigger (ADR-0003).
|
|
32
|
+
|
|
33
|
+
## Rule conformance
|
|
34
|
+
|
|
35
|
+
| Rule | Status | Evidence |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| handover-redone-task-proof | applied | §2 — 2026-07-02, ops engineer, incident-recovery task end to end without the builder; file:code/test/triage.test.ts; file:runward/runbook.md#Recovery |
|
|
38
|
+
| handover-runbook-executable | applied | file:runward/runbook.md#Recovery — the seven gestures carry commands/paths (start §1, observe §1, debug §4, resume §3, swap §2+§4, bench: cd code && npm test, approvals §3); exercised during the 2026-07-02 task |
|
|
39
|
+
| handover-agents-charter-final | applied | file:AGENTS.md#Never — finalized at hand-over: mission-specific boundaries (registry read-only, compliance always approval-gated), exact verification commands, never/PR rules |
|
|
40
|
+
| handover-succession-named | applied | §3 — named owner (triage product owner), escalation path, weekly review cadence, builder's accesses revoked 2026-07-03 |
|
|
41
|
+
|
|
42
|
+
## Cross-references
|
|
43
|
+
|
|
44
|
+
- `workflows/handover.md` — the phase this note closes.
|
|
45
|
+
- `runbook.md` — the operational half of the kit.
|
|
46
|
+
- `AGENTS.md` — the charter the next agent inherits.
|
|
47
|
+
- `adr/` — the open re-evaluation triggers the owner now watches.
|
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.16.0",
|
|
4
|
+
"description": "After the spec: ship and run. A delivery framework for agentic systems \u2014 floor first, evolution on evidence, governance from day zero, handover with proof.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic",
|
|
7
7
|
"ai-agents",
|
|
@@ -22,31 +22,44 @@
|
|
|
22
22
|
"url": "https://github.com/stranxik/runward/issues"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
25
28
|
"bin": {
|
|
26
29
|
"runward": "dist/cli.js"
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
32
|
"dist",
|
|
30
33
|
"templates",
|
|
34
|
+
"regimes",
|
|
35
|
+
"examples/request-triage/AGENTS.md",
|
|
31
36
|
"examples/request-triage/runward",
|
|
37
|
+
"examples/request-triage/code/src",
|
|
38
|
+
"examples/request-triage/code/test",
|
|
39
|
+
"examples/request-triage/code/package.json",
|
|
40
|
+
"examples/request-triage/code/tsconfig.json",
|
|
41
|
+
"examples/request-triage/code/README.md",
|
|
32
42
|
"README.md",
|
|
33
|
-
"NOTICE.md"
|
|
43
|
+
"NOTICE.md",
|
|
44
|
+
"LICENSE"
|
|
34
45
|
],
|
|
35
46
|
"engines": {
|
|
36
47
|
"node": ">=20"
|
|
37
48
|
},
|
|
38
49
|
"scripts": {
|
|
39
50
|
"build": "tsc",
|
|
40
|
-
"test": "npm run build && node test/smoke.js && node test/oscal-schema.js",
|
|
41
|
-
"
|
|
51
|
+
"test": "npm run build && node --test test/unit/*.test.js && node test/smoke.js && node test/oscal-schema.js",
|
|
52
|
+
"test:unit": "npm run build && node --test test/unit/*.test.js",
|
|
53
|
+
"coverage": "npm run build && node --test --experimental-test-coverage test/unit/*.test.js",
|
|
54
|
+
"prepublishOnly": "npm run build && node --test test/unit/*.test.js && node test/smoke.js && node test/oscal-schema.js"
|
|
42
55
|
},
|
|
43
56
|
"dependencies": {
|
|
44
|
-
"@inquirer/prompts": "^
|
|
57
|
+
"@inquirer/prompts": "^8.5.2",
|
|
45
58
|
"chalk": "^5.6.0",
|
|
46
|
-
"commander": "^
|
|
59
|
+
"commander": "^15.0.0"
|
|
47
60
|
},
|
|
48
61
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^
|
|
62
|
+
"@types/node": "^26.1.1",
|
|
50
63
|
"ajv": "^8.20.0",
|
|
51
64
|
"ajv-formats": "^3.0.1",
|
|
52
65
|
"js-yaml": "^5.2.1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks Regulation (EU) 2024/1689 — the Annex III applicability date, the article references, the Annex IV point-by-point coverage rows, and the provider-required list. runward's stable product voice stays in src/lib/compliance.ts. An Annex IV amendment or corrigendum is a NEW regimes/eu-ai-act@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "eu-ai-act",
|
|
4
|
+
"label": "EU AI Act (Annex IV)",
|
|
5
|
+
"version": "2024-1689",
|
|
6
|
+
"notes": "Mapped 2026-07 against Regulation (EU) 2024/1689 (OJ L, 12 July 2024). High-risk (Annex III) obligations bind from 2 August 2026. Annex IV wording moves — confirm against the Official Journal before filing.",
|
|
7
|
+
"highRisk": {
|
|
8
|
+
"bindFrom": "2 August 2026",
|
|
9
|
+
"scope": "Annex III"
|
|
10
|
+
},
|
|
11
|
+
"articles": {
|
|
12
|
+
"runtimeLogging": "art. 12"
|
|
13
|
+
},
|
|
14
|
+
"annexIv": [
|
|
15
|
+
{ "point": "1. General description", "supplies": "UI, HW/SW/firmware notes", "provider": "intended purpose, provider, versioning, distribution" },
|
|
16
|
+
{ "point": "2. Elements & development", "supplies": "**architecture, validation procedures + metrics, cybersecurity (manifest + rubric + threat model); design choices, alternatives, assumptions, pre-determined changes = the ADR journal**", "provider": "third-party sourcing/licensing, sign-off on test logs" },
|
|
17
|
+
{ "point": "3. Monitoring & control", "supplies": "accuracy characterization, input-data specs, oversight tooling", "provider": "fundamental-rights / discrimination risk sourcing" },
|
|
18
|
+
{ "point": "4. Performance metrics", "supplies": "metric-choice justification (rubric)", "provider": "—" },
|
|
19
|
+
{ "point": "5. Risk management (art. 9)", "supplies": "technical inputs (threat model, testing)", "provider": "**risk acceptance / RMS governance**" },
|
|
20
|
+
{ "point": "6. Lifecycle changes", "supplies": "engineering change record (ADR journal)", "provider": "release/change-management governance" },
|
|
21
|
+
{ "point": "7. Harmonised standards", "supplies": "technical notes", "provider": "**standards selection** (compliance strategy)" },
|
|
22
|
+
{ "point": "8. Declaration of conformity", "supplies": "—", "provider": "**signed legal act (art. 47)**" },
|
|
23
|
+
{ "point": "9. Post-market monitoring", "supplies": "telemetry/logging backbone", "provider": "**post-market monitoring plan (art. 72)**" }
|
|
24
|
+
],
|
|
25
|
+
"operatorRequired": [
|
|
26
|
+
"**Point 1** general description (intended purpose, provider, versioning) · **Point 5** RMS governance & risk acceptance (art. 9).",
|
|
27
|
+
"**Point 7** harmonised-standards selection · **Point 8** the signed **EU declaration of conformity** (art. 47) · **Point 9** the **post-market monitoring plan** (art. 72).",
|
|
28
|
+
"**Art. 12 runtime event logs** — produced by the running system, not by runward."
|
|
29
|
+
],
|
|
30
|
+
"disclaimerTail": "Annex IV wording moves — confirm against the Official Journal (Reg. (EU) 2024/1689) before filing."
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks the ISO/IEC 42001:2023 text — clause references used in the draft, the operator-required clause list, and the dated caveat. runward's stable product voice (draft framing, section narrative, table glue) stays in src/lib/compliance.ts. A revision of the standard is a NEW regimes/iso-42001@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "iso-42001",
|
|
4
|
+
"label": "ISO/IEC 42001",
|
|
5
|
+
"version": "2023",
|
|
6
|
+
"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
|
+
"clauses": {
|
|
8
|
+
"riskAssessment": "6.1.2",
|
|
9
|
+
"controlSelection": "6.1.3",
|
|
10
|
+
"annexControls": "Annex A"
|
|
11
|
+
},
|
|
12
|
+
"operatorRequired": [
|
|
13
|
+
"**AI policy** (5.2) and **AIMS scope** (4.3).",
|
|
14
|
+
"**Statement of Applicability — the applicability decisions and inclusion/exclusion justifications** (6.1.3): runward supplies the status + evidence columns; the *applicability* judgment is yours.",
|
|
15
|
+
"**Risk methodology, acceptance criteria and risk-acceptance decisions** (6.1.2, 8.3).",
|
|
16
|
+
"**AI system impact assessment report and deployment authorization** (6.1.4).",
|
|
17
|
+
"**Objectives and targets** (6.2), **roles and competence** (A.3, 7.2).",
|
|
18
|
+
"**Internal audit** (9.2) and **management review** minutes (9.3).",
|
|
19
|
+
"**Runtime AI event logs** (A.6.2.8) — produced by the running system, not by runward."
|
|
20
|
+
],
|
|
21
|
+
"disclaimerTail": "ISO Annex A control counts/templates are behind the paywalled standard — confirm against the purchased text."
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Regime mapping data (ADR-0022). Cut line: this file carries only what tracks NIST AI 100-1 (AI RMF 1.0, January 2023) — the ASI crosswalk target functions, the MEASURE reference, and the operator-required function list. runward's stable product voice stays in src/lib/compliance.ts. An AI RMF update is a NEW regimes/nist-ai-rmf@<version>.json, never an edit of this file.",
|
|
3
|
+
"regime": "nist-ai-rmf",
|
|
4
|
+
"label": "NIST AI RMF",
|
|
5
|
+
"version": "1.0",
|
|
6
|
+
"notes": "Mapped 2026-07 against NIST AI 100-1 (AI RMF 1.0, January 2023) and the companion Playbook.",
|
|
7
|
+
"crosswalk": {
|
|
8
|
+
"primary": "each agentic-security risk lands primarily under **MEASURE** (test & evaluate, esp. security & resilience) and **MANAGE** (risk treatment)",
|
|
9
|
+
"confirmAgainst": "AI RMF §5"
|
|
10
|
+
},
|
|
11
|
+
"measureRef": "MEASURE 2.x",
|
|
12
|
+
"operatorRequired": [
|
|
13
|
+
"**GOVERN** — policies, roles, accountability, **risk tolerance** (almost entirely organizational).",
|
|
14
|
+
"**MAP** — intended purpose, business/legal context, use-case risk enumeration.",
|
|
15
|
+
"**MANAGE** — the **go/no-go acceptance** decision, resourcing, response planning.",
|
|
16
|
+
"**Profiles** — Current/Target selection, prioritization, the risk-tolerance choices behind them."
|
|
17
|
+
],
|
|
18
|
+
"disclaimerTail": "NIST prescribes no report template — confirm against AI 100-1 and the Playbook."
|
|
19
|
+
}
|
|
@@ -39,12 +39,20 @@ A non-zero exit aborts the commit. Bypass a single commit with `git commit --no-
|
|
|
39
39
|
|
|
40
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
41
|
|
|
42
|
+
## `gitlab-ci.yml` — the same required check, GitLab flavor
|
|
43
|
+
|
|
44
|
+
Merge the job into your `.gitlab-ci.yml` and require the pipeline on your protected branch (Settings → Merge requests). Same port, same one line — the gate does not care which forge reads its exit code.
|
|
45
|
+
|
|
42
46
|
## `claude-code-settings.json` — run the gate at the agent's turn-end (one example)
|
|
43
47
|
|
|
44
48
|
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
49
|
|
|
46
50
|
This is **one example** of a per-harness turn-end hook, not a privileged one — Claude Code just happens to expose a clean, documented seam. Any agent harness that can run a command at turn-end (Codex, and others as they add the capability) wires the *same* one line: `runward check --strict`. And where a harness offers no such seam, the `pre-commit` and CI adapters above already gate the code **whatever agent produced it** — the port is the exit code, not the agent.
|
|
47
51
|
|
|
52
|
+
## `kiro-hooks.json` — the same turn-end gate, Kiro flavor
|
|
53
|
+
|
|
54
|
+
Copy the file into `.kiro/hooks/` in your workspace. The `Stop` trigger runs `runward check --strict` when the agent finishes, and surfaces the verdict in the session — the same one line as the Claude Code hook, another non-privileged example of a per-harness turn-end seam. Kiro also reads `AGENTS.md` natively, and `runward init --tools kiro` mirrors the phase skills as steering files (`.kiro/steering/`, relevance-loaded) — the traced decisions inform the session; the gate stays the only authority.
|
|
55
|
+
|
|
48
56
|
---
|
|
49
57
|
|
|
50
58
|
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,18 @@
|
|
|
1
|
+
# runward gate — GitLab CI adapter (sample).
|
|
2
|
+
# Inert until you install it: see runward/adapters/README.md.
|
|
3
|
+
# Merge this job into your .gitlab-ci.yml, then make the pipeline required
|
|
4
|
+
# on your protected branch (Settings → Merge requests → pipelines must
|
|
5
|
+
# succeed) so no gap merges.
|
|
6
|
+
#
|
|
7
|
+
# The gate is deterministic and zero-LLM: no secrets, no model key needed.
|
|
8
|
+
# Exit codes (the port contract): 0 clean · 1 gaps · 2 no mission found.
|
|
9
|
+
|
|
10
|
+
runward-gate:
|
|
11
|
+
image: node:20
|
|
12
|
+
# Use --strict to also enforce the rule-conformance manifests.
|
|
13
|
+
# Drop --strict for the deliverable audit only.
|
|
14
|
+
script:
|
|
15
|
+
- npx --yes runward check --strict
|
|
16
|
+
rules:
|
|
17
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
18
|
+
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
## Rule conformance
|
|
41
41
|
|
|
42
42
|
> Account for every CRITICAL/HIGH craft rule mapped to the architect phase (`runward/rules/`, frontmatter `phases: [architect]`). `applied` needs a pointer; `deviated` needs an ADR; `n/a` needs a reason. `runward check --strict` verifies this table is complete — it checks a traced decision, not the quality of your architecture.
|
|
43
|
+
>
|
|
44
|
+
> Evidence can be **typed**, and typed pointers are verified deterministically at the gate: `file:PATH[:LINE][#SYMBOL]` · `test:PATH[::NAME]` · `adr:NNNN` — several per cell, separated by `;`. The gate checks resolution, non-emptiness, line count, symbol/test-name presence, and the rule's `signature:` when it declares one (ADR-0019/0020). Free prose stays valid — it is your judgment; a path it cites must simply not point at an empty file.
|
|
43
45
|
|
|
44
46
|
| Rule | Status | Evidence |
|
|
45
47
|
|---|---|---|
|