runward 0.12.2 → 0.13.1
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 +4 -1
- package/dist/cli.js +1 -0
- package/dist/commands/init.js +52 -18
- package/dist/commands/status.js +54 -2
- package/dist/lib/paths.js +2 -0
- package/examples/request-triage/README.md +34 -0
- package/examples/request-triage/runward/adr/ADR-0001-single-orchestrator.md +37 -0
- package/examples/request-triage/runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md +38 -0
- package/examples/request-triage/runward/architecture.md +64 -0
- package/examples/request-triage/runward/contracts/model-port.md +55 -0
- package/examples/request-triage/runward/contracts/persistence-port.md +55 -0
- package/examples/request-triage/runward/contracts/request-intake.md +58 -0
- package/examples/request-triage/runward/contracts/routing-port.md +56 -0
- package/examples/request-triage/runward/decision-matrix.md +39 -0
- package/examples/request-triage/runward/execution-topology.md +41 -0
- package/examples/request-triage/runward/floor.md +60 -0
- package/examples/request-triage/runward/framing.md +57 -0
- package/examples/request-triage/runward/governance/evaluation-rubric.md +62 -0
- package/examples/request-triage/runward/governance/observability-schema.md +43 -0
- package/examples/request-triage/runward/governance/threat-model.md +66 -0
- package/examples/request-triage/runward/mission-contract.md +51 -0
- package/examples/request-triage/runward/runbook.md +63 -0
- package/package.json +2 -1
- package/templates/targets/AGENTS.md +1 -0
package/README.md
CHANGED
|
@@ -45,11 +45,14 @@ The cost is discipline: one accountable operator, governance from day zero, and
|
|
|
45
45
|
## Install
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
npx runward init
|
|
48
|
+
npx runward init --example # a filled reference mission — see the whole chain green in one command
|
|
49
|
+
npx runward init # interactive wizard (blank templates for your own mission)
|
|
49
50
|
npx runward --yes init # non-interactive, defaults (CI-friendly)
|
|
50
51
|
npx runward init --tools claude,cursor,copilot,gemini,windsurf
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
**Fastest way to see what runward does:** `npx runward init --example` scaffolds the `request-triage` reference mission already filled, so `runward check` passes green and `runward compliance iso-42001` emits an audit-ready OSCAL pack out of the box. Then `npx runward init` (no `--example`) starts your own mission from blank templates.
|
|
55
|
+
|
|
53
56
|
New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
54
57
|
|
|
55
58
|
### Commands
|
package/dist/cli.js
CHANGED
|
@@ -54,6 +54,7 @@ program
|
|
|
54
54
|
.option("-p, --path <path>", "project directory (default: prompt, or . with --yes)")
|
|
55
55
|
.option("-t, --tools <list>", "comma-separated tool profiles: claude,cursor,copilot,gemini,windsurf")
|
|
56
56
|
.option("--force", "overwrite existing files")
|
|
57
|
+
.option("--example", "scaffold a filled reference mission (request-triage) — the whole chain is green out of the box")
|
|
57
58
|
.action(initCommand);
|
|
58
59
|
program
|
|
59
60
|
.command("check")
|
package/dist/commands/init.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
4
|
-
import { TEMPLATES, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
4
|
+
import { TEMPLATES, EXAMPLE_MISSION, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
5
5
|
import { TOOL_PROFILES, TOOL_IDS } from "../lib/tools.js";
|
|
6
6
|
import { makeWriter } from "../lib/write.js";
|
|
7
7
|
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
8
|
+
/** Recursively copy a shipped directory tree (used to lay down the filled reference mission). */
|
|
9
|
+
function copyTree(w, srcDir, destDir) {
|
|
10
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
11
|
+
const src = join(srcDir, entry.name);
|
|
12
|
+
const dest = join(destDir, entry.name);
|
|
13
|
+
if (entry.isDirectory())
|
|
14
|
+
copyTree(w, src, dest);
|
|
15
|
+
else
|
|
16
|
+
w.copy(src, dest);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
export async function initCommand(opts) {
|
|
9
20
|
console.log(createHeader(`Runward v${VERSION}`, "After the spec: ship and run"));
|
|
10
21
|
const yes = isNonInteractive();
|
|
22
|
+
const example = opts.example ?? false;
|
|
11
23
|
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
12
24
|
// ── Gather answers (wizard, or defaults with --yes) ──────────────
|
|
13
25
|
const dir = opts.path
|
|
14
26
|
?? (yes ? "." : await input({ message: "Project directory", default: "." }));
|
|
15
27
|
// The idea seeds the framing note: you arrive with a project, not with paperwork.
|
|
16
|
-
|
|
28
|
+
// In example mode the framing is already filled from the reference, so we skip it.
|
|
29
|
+
const idea = (yes || example) ? "" : await input({
|
|
17
30
|
message: "What are you building? (one line — it seeds your framing note)",
|
|
18
31
|
default: "",
|
|
19
32
|
});
|
|
@@ -25,14 +38,14 @@ export async function initCommand(opts) {
|
|
|
25
38
|
message: "Tool profiles (AGENTS.md is always written)",
|
|
26
39
|
choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked: t.id === "claude" })),
|
|
27
40
|
});
|
|
28
|
-
const entryMode = yes ? "greenfield" : await select({
|
|
41
|
+
const entryMode = (yes || example) ? "greenfield" : await select({
|
|
29
42
|
message: "Entry mode",
|
|
30
43
|
choices: [
|
|
31
44
|
{ value: "greenfield", name: "Greenfield — new system, run the chain from the top" },
|
|
32
45
|
{ value: "brownfield", name: "Brownfield — existing system, characterize before touching" },
|
|
33
46
|
],
|
|
34
47
|
});
|
|
35
|
-
const tier = yes ? "floor" : await select({
|
|
48
|
+
const tier = (yes || example) ? "floor" : await select({
|
|
36
49
|
message: "Stopping tier (the sponsor's choice — can be revised)",
|
|
37
50
|
choices: [
|
|
38
51
|
{ value: "framing", name: "Framing only" },
|
|
@@ -47,20 +60,30 @@ export async function initCommand(opts) {
|
|
|
47
60
|
const root = resolve(process.cwd(), dir);
|
|
48
61
|
const mission = join(root, "runward");
|
|
49
62
|
const w = makeWriter({ force: opts.force ?? false, dryRun, root });
|
|
50
|
-
console.log(section("Mission structure"));
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
console.log(section(example ? "Mission structure (filled reference)" : "Mission structure"));
|
|
64
|
+
if (example) {
|
|
65
|
+
// Lay down the filled request-triage reference: every gate is green out of the box.
|
|
66
|
+
copyTree(w, EXAMPLE_MISSION, mission);
|
|
67
|
+
// The reference omits the three non-gated scaffolding notes; add them as blank templates so the scaffold stays complete.
|
|
68
|
+
for (const extra of ["reference-stack.md", "shared-bricks.md", "gap-analysis.md"]) {
|
|
69
|
+
w.copy(join(TEMPLATES, "mission", extra), join(mission, extra));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const prefill = (s) => {
|
|
74
|
+
let out = s
|
|
75
|
+
.replace("[greenfield | brownfield M1–M4]", entryMode)
|
|
76
|
+
.replace("[framing | floor | full chain]", tier);
|
|
77
|
+
if (idea) {
|
|
78
|
+
out = out
|
|
79
|
+
.replace("[system or mission name]", idea)
|
|
80
|
+
.replace("## 1. Problem\n", `## 1. Problem\n\n> Seed idea (from init): "${idea}" — now replace this with the process as actually observed.\n`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
for (const [src, dest] of Object.entries(MISSION_LAYOUT)) {
|
|
85
|
+
w.copy(join(TEMPLATES, "mission", src), join(mission, dest), src === "framing.md" ? prefill : undefined);
|
|
59
86
|
}
|
|
60
|
-
return out;
|
|
61
|
-
};
|
|
62
|
-
for (const [src, dest] of Object.entries(MISSION_LAYOUT)) {
|
|
63
|
-
w.copy(join(TEMPLATES, "mission", src), join(mission, dest), src === "framing.md" ? prefill : undefined);
|
|
64
87
|
}
|
|
65
88
|
console.log(section("Workflows"));
|
|
66
89
|
for (const wf of readdirSync(join(TEMPLATES, "workflows"))) {
|
|
@@ -85,10 +108,21 @@ export async function initCommand(opts) {
|
|
|
85
108
|
// ── Summary ───────────────────────────────────────────────────────
|
|
86
109
|
console.log(section("Done"));
|
|
87
110
|
console.log(status.success(`${w.stats.written} file(s) ${dryRun ? "planned" : "written"}, ${w.stats.skipped} skipped`));
|
|
88
|
-
|
|
111
|
+
if (example) {
|
|
112
|
+
console.log(`
|
|
113
|
+
${c.primaryBold("Filled reference mission — the whole chain is already green.")}
|
|
114
|
+
1. Run ${c.primary("runward check")} — every gate passes; the deliverables are filled, not blank.
|
|
115
|
+
2. Run ${c.primary("runward compliance iso-42001")} — an audit-ready evidence pack (OSCAL) derived from the traced decisions.
|
|
116
|
+
3. Read ${c.white("runward/framing.md")}, the ADRs in ${c.white("runward/adr/")}, and ${c.white("runward/governance/threat-model.md")} to see how a real mission is traced end to end.
|
|
117
|
+
|
|
118
|
+
${c.gray("Start your own mission with")} ${c.primary("runward init")} ${c.gray("(without --example) to get blank templates.")}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(`
|
|
89
122
|
${c.primaryBold("Next steps")}
|
|
90
123
|
1. Fill ${c.white("runward/framing.md")} — do not architect before the framing gate passes.
|
|
91
124
|
2. Point your agent at ${c.white("AGENTS.md")} and ${c.white("runward/workflows/method.md")}.
|
|
92
125
|
3. Run ${c.primary("runward check")} anytime to see which gate you are at.
|
|
93
126
|
${entryMode === "brownfield" ? c.warning("\n Brownfield entry: start with runward/workflows/brownfield.md — characterize before touching.\n") : ""}`);
|
|
127
|
+
}
|
|
94
128
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -3,7 +3,8 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
4
4
|
import { c, createHeader, section } from "../lib/styles.js";
|
|
5
5
|
import { VERSION, WORKFLOWS } from "../lib/paths.js";
|
|
6
|
-
/** Mission snapshot: phase,
|
|
6
|
+
/** Mission snapshot: phase progress, decision journal, activity, workflows —
|
|
7
|
+
* a transmission-ready "where the mission stands", read-only from the mission files. */
|
|
7
8
|
export async function statusCommand(opts) {
|
|
8
9
|
const root = findMissionRoot(resolve(process.cwd(), opts.path ?? "."));
|
|
9
10
|
if (!root) {
|
|
@@ -13,7 +14,7 @@ export async function statusCommand(opts) {
|
|
|
13
14
|
const mission = join(root, "runward");
|
|
14
15
|
const report = analyze(mission);
|
|
15
16
|
console.log(createHeader(`Runward v${VERSION} — mission status`, root));
|
|
16
|
-
//
|
|
17
|
+
// Mission title + current gate
|
|
17
18
|
console.log(section("Mission"));
|
|
18
19
|
const framingPath = join(mission, "framing.md");
|
|
19
20
|
if (existsSync(framingPath)) {
|
|
@@ -21,6 +22,25 @@ export async function statusCommand(opts) {
|
|
|
21
22
|
console.log(` ${c.white(title)}`);
|
|
22
23
|
}
|
|
23
24
|
console.log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
|
|
25
|
+
// Phase progress across the gated arc — where the mission stands, gate by gate
|
|
26
|
+
console.log(section("Phase progress"));
|
|
27
|
+
const currentIndex = report.phases.findIndex((p) => !p.complete);
|
|
28
|
+
report.phases.forEach((p, i) => {
|
|
29
|
+
const filled = p.artifacts.filter((a) => a.state === "filled").length;
|
|
30
|
+
const total = p.artifacts.length;
|
|
31
|
+
const isCurrent = i === currentIndex;
|
|
32
|
+
const mark = p.complete ? c.success("✓") : isCurrent ? c.primary("▸") : c.darkGray("○");
|
|
33
|
+
const label = p.complete ? c.white(p.spec.label) : isCurrent ? c.primaryBold(p.spec.label) : c.gray(p.spec.label);
|
|
34
|
+
const count = p.complete ? c.darkGray(`${filled}/${total}`) : c.white(`${filled}/${total}`);
|
|
35
|
+
console.log(` ${mark} ${label} ${count}${isCurrent ? c.primary(" ← you are here") : ""}`);
|
|
36
|
+
// Under the current phase, name exactly what is still open.
|
|
37
|
+
if (isCurrent) {
|
|
38
|
+
for (const a of p.artifacts.filter((a) => a.state !== "filled")) {
|
|
39
|
+
console.log(` ${c.darkGray("○")} ${c.gray(a.artifact.label)} ${c.darkGray(`(${a.state})`)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Decision journal — the ADRs, dated
|
|
24
44
|
console.log(section("Decision journal"));
|
|
25
45
|
const adrDir = join(mission, "adr");
|
|
26
46
|
const adrs = existsSync(adrDir)
|
|
@@ -38,12 +58,44 @@ export async function statusCommand(opts) {
|
|
|
38
58
|
}
|
|
39
59
|
if (adrs.length > 5)
|
|
40
60
|
console.log(c.darkGray(` … and ${adrs.length - 5} more`));
|
|
61
|
+
console.log(c.darkGray(` ${adrs.length} decision(s) traced`));
|
|
62
|
+
}
|
|
63
|
+
// Activity — the most recently touched deliverable, so a receiving team sees the last movement
|
|
64
|
+
console.log(section("Activity"));
|
|
65
|
+
let latest = null;
|
|
66
|
+
for (const p of report.phases) {
|
|
67
|
+
for (const a of p.artifacts) {
|
|
68
|
+
const abs = join(mission, a.artifact.relPath);
|
|
69
|
+
if (!existsSync(abs))
|
|
70
|
+
continue;
|
|
71
|
+
const st = statSync(abs);
|
|
72
|
+
if (!st.isFile())
|
|
73
|
+
continue;
|
|
74
|
+
const date = st.mtime.toISOString().slice(0, 10);
|
|
75
|
+
if (!latest || date > latest.date)
|
|
76
|
+
latest = { date, label: a.artifact.label };
|
|
77
|
+
}
|
|
41
78
|
}
|
|
79
|
+
if (latest)
|
|
80
|
+
console.log(` ${c.primaryBold("Last touched")} ${c.white(latest.date)} ${c.darkGray(latest.label)}`);
|
|
81
|
+
else
|
|
82
|
+
console.log(c.darkGray(" no deliverable written yet"));
|
|
83
|
+
// Workflows
|
|
42
84
|
console.log(section("Workflows"));
|
|
43
85
|
const missing = WORKFLOWS.filter((wf) => !existsSync(join(mission, "workflows", `${wf}.md`)));
|
|
44
86
|
if (missing.length === 0)
|
|
45
87
|
console.log(c.success(" ✓ ") + c.white(`all ${WORKFLOWS.length} workflows present`));
|
|
46
88
|
else
|
|
47
89
|
console.log(c.warning(" ! ") + c.white(`missing: ${missing.join(", ")} — run \`runward update\``));
|
|
90
|
+
// Next — the transmission surface: name the next gesture, never leave the reader guessing
|
|
91
|
+
console.log(section("Next"));
|
|
92
|
+
if (currentIndex === -1) {
|
|
93
|
+
console.log(` All gated deliverables are filled. Run ${c.primary("runward check --strict")} to confirm on evidence, then ${c.primary("runward compliance <regime>")} for the evidence pack.`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const cur = report.phases[currentIndex];
|
|
97
|
+
const open = cur.artifacts.filter((a) => a.state !== "filled").length;
|
|
98
|
+
console.log(` Fill the ${open} open deliverable(s) in ${c.white(cur.spec.label)}, then run ${c.primary("runward check")} to cross the gate on evidence.`);
|
|
99
|
+
}
|
|
48
100
|
console.log();
|
|
49
101
|
}
|
package/dist/lib/paths.js
CHANGED
|
@@ -5,6 +5,8 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
5
5
|
/** Package root (works from dist/lib/ at runtime). */
|
|
6
6
|
export const PKG_ROOT = join(HERE, "..", "..");
|
|
7
7
|
export const TEMPLATES = join(PKG_ROOT, "templates");
|
|
8
|
+
/** Filled reference mission (shipped) — used by `init --example`. */
|
|
9
|
+
export const EXAMPLE_MISSION = join(PKG_ROOT, "examples", "request-triage", "runward");
|
|
8
10
|
export const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
9
11
|
/** Mission layout: template file -> destination inside runward/ */
|
|
10
12
|
export const MISSION_LAYOUT = {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Example mission: request triage
|
|
2
|
+
|
|
3
|
+
A complete Runward mission, filled end to end, on a deliberately ordinary case: an organization receives heterogeneous inbound requests (support, sales, compliance) and triages them by hand. The floor is a qualifier that classifies each request, extracts the key fields, and routes it — with an observable success criterion measured on real traffic against the manual baseline.
|
|
4
|
+
|
|
5
|
+
Every document follows the corresponding template in `templates/mission/`, placeholders replaced. All names, volumes and figures are **illustrative**: they show what a filled note looks like, not a real engagement.
|
|
6
|
+
|
|
7
|
+
## How to read it
|
|
8
|
+
|
|
9
|
+
Read in mission order — the order the workflows produce them:
|
|
10
|
+
|
|
11
|
+
1. `runward/framing.md` — the problem, the observable success criterion, floor vs target, named deferrals, DoR check (one condition carried as a named risk).
|
|
12
|
+
2. `runward/mission-contract.md` — the one-page steering contract signed with the sponsor: engagements, DoD, gates.
|
|
13
|
+
3. `runward/architecture.md` — boundaries before stack: ports, versioned contract, default topology with triggers, language explicitly left open.
|
|
14
|
+
4. `runward/decision-matrix.md` — the arbitration reference, adopted at the Architect gate; every position on its sober default.
|
|
15
|
+
5. `runward/contracts/` — the four port contracts the architecture note declares: request intake, model, routing, persistence.
|
|
16
|
+
6. `runward/adr/ADR-0001-single-orchestrator.md` — the sober default, with its dated reevaluation trigger.
|
|
17
|
+
7. `runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md` — the model never supplies a value the system can compute or verify deterministically.
|
|
18
|
+
8. `runward/governance/` — instrumented from day zero, not retrofitted: threat model (the 2-of-3 rule held by construction), evaluation rubric (floor-tier capabilities: routing fidelity, abstention, criterion compliance — no memory bench, because the floor has no memory), observability schema.
|
|
19
|
+
9. `runward/floor.md` — the proof record: what shipped, what was measured, gaps, and which trigger is watched next.
|
|
20
|
+
10. `runward/runbook.md` — what the receiving team inherits: startup, degraded modes, recovery, failover.
|
|
21
|
+
|
|
22
|
+
The chain passes the gate audit: `runward check -p examples/request-triage` exits 0 (the smoke test asserts it).
|
|
23
|
+
|
|
24
|
+
Notice what the documents *refuse* to do: no stack picked at framing, no complexity without a trigger, no proof on hand-picked cases, no evaluation of capabilities the floor does not have.
|
|
25
|
+
|
|
26
|
+
## Runnable floor
|
|
27
|
+
|
|
28
|
+
The mission is also executable: [`code/`](code/) contains a standalone
|
|
29
|
+
TypeScript package (no key, no network, deterministic model adapter) that
|
|
30
|
+
implements what the documents decide — closed-vocabulary classification,
|
|
31
|
+
the deterministic guard on extracted fields (ADR-0002), guarded routing,
|
|
32
|
+
fail-closed compliance through suspend-and-rehydrate, abstention on unknown.
|
|
33
|
+
`npm install && npm test` in `code/` runs its 14 deterministic tests; see
|
|
34
|
+
[`code/README.md`](code/README.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADR-0001: single orchestrator, sequential triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check on the observed process, sourced state of the art, challenge, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Triaging one request is a short sequence of dependent steps: normalize the raw input, propose a classification and field extractions (model), validate or recompute the extracted fields (deterministic, see ADR-0002), resolve the target queue, route or escalate to human review, persist. Each step consumes the output of the previous one; none is independent enough to parallelize, none handles content requiring an isolated context. Volume is ~400 requests per week — throughput is not a force here. The decision is taken at the orchestration boundary, behind the primary RequestIntakePort; it is structuring because topology is expensive to unwind once adapters and observability are built around it.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
One orchestrator, running a fixed sequential plan per request, holding no instance state. It composes the steps and carries no business logic: classification rules live in the domain, validation in the guards, translation in the adapters. This applies the framework's sober default: no multi-agent until a genuinely parallelizable or isolation-requiring subtask exists.
|
|
15
|
+
|
|
16
|
+
## Alternatives discarded
|
|
17
|
+
|
|
18
|
+
- **Multi-agent (one agent per step)**: discarded. No step is isolable enough to deserve its own context; the coordination and token overhead buys nothing measurable at this volume.
|
|
19
|
+
- **Model-driven dynamic planning**: discarded. The triage sequence is known and stable; letting the model decide the plan adds non-determinism, cost, and an attack surface without adding value.
|
|
20
|
+
- **Event-driven pipeline (queue between each step)**: tempered rather than eliminated. Useful at scale for backpressure, but at 400 requests/week it multiplies moving parts for no observed load. The persistence log already gives replayability.
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
- **Positive**: deterministic, replayable trajectories; a single point of cost accounting and tracing; one request ID covers the whole run with no parent-child lineage to manage; the fixed plan keeps model spend to the two calls that need it.
|
|
25
|
+
- **Negative, accepted**: if one step becomes heavy and independent — for example untrusted attachment processing that should run quarantined — the topology must be revisited. Cost taken on knowingly.
|
|
26
|
+
- **On other boundaries**: observability stays flat (one trajectory per request); security review has one path to guard; the contract between steps is internal and free to change until a step crosses a process boundary.
|
|
27
|
+
|
|
28
|
+
## Reevaluation trigger (mandatory, dated)
|
|
29
|
+
|
|
30
|
+
Reopen this decision when either observable signal appears: (1) a subtask requiring an isolated context or genuine parallelism enters scope — the named candidate is attachment processing, currently out of scope; or (2) end-to-end triage latency exceeds 60 seconds at p95 over a full week, measured on the observability schema, indicating the sequential plan no longer holds the load. Until then, apply without reopening.
|
|
31
|
+
|
|
32
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: weekly review of the p95 latency metric and the scope backlog
|
|
33
|
+
|
|
34
|
+
## References
|
|
35
|
+
|
|
36
|
+
- [architecture.md](../architecture.md) §4 — default topology and triggers.
|
|
37
|
+
- [ADR-0002](ADR-0002-deterministic-guard-on-extracted-fields.md) — the guard step this plan sequences.
|
package/examples/request-triage/runward/adr/ADR-0002-deterministic-guard-on-extracted-fields.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ADR-0002: deterministic guard on model-extracted fields
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12
|
|
4
|
+
**Status**: accepted
|
|
5
|
+
**Deciders**: operator, Head of Operations (sponsor)
|
|
6
|
+
**Method**: decision-loop: reality-check on the TriageRecord contract, challenge on failure modes, durable position
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The model proposes the classification and the extracted key fields (requester identity, account reference, stated deadline). Those fields drive an action: routing into the ticketing system, where a wrong account reference attaches the request to the wrong customer and a missed deadline flag can breach a regulatory response window. A model-proposed value is a hypothesis, not a fact — plausible-looking account references and dates are exactly what a language model fabricates most fluently. The framing note's attached condition (no silent miss on compliance requests) makes this the highest-stakes boundary in the system. The decision is taken at the domain's output boundary, between the model port and the routing port.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Every model-extracted field is validated or recomputed deterministically before any action is taken on it. **The model never supplies a value the system can compute or verify itself.** Concretely: account references are resolved against the account registry (a deterministic tool) — a reference that does not resolve is dropped to `unverified`, never routed on; deadlines are re-parsed from the source text by a deterministic date parser, and the model's proposal is discarded in favor of the parsed value; requester identity is matched against the sender address before being trusted. Each field in the TriageRecord carries a provenance marker (`computed`, `verified`, `model-proposed`); the RoutingPort refuses, fail-closed, any record whose action-bearing fields are still `model-proposed`. Records that fail the guard go to the human review queue with the failure reason attached.
|
|
15
|
+
|
|
16
|
+
## Alternatives discarded
|
|
17
|
+
|
|
18
|
+
- **Trust the model's structured output**: discarded. Schema-valid is not correct — a well-formed fabricated account reference passes every JSON check and still routes to the wrong customer. Structure guarantees shape, not truth.
|
|
19
|
+
- **Second model call as verifier**: discarded. A verifier model has the same failure class as the extractor; it converts a deterministic check the system can do for free into a probabilistic one that costs tokens. Verification that can be computed must be computed.
|
|
20
|
+
- **Post-hoc detection (flag anomalies after routing)**: discarded. Detection after the action is too late for deadline-bearing compliance requests; the framing note's attached condition demands the miss never happen, not that it be noticed.
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
- **Positive**: no fabricated value can trigger an action; the guard is pure and unit-testable without a model in the loop; provenance markers make every routing decision auditable field by field; regressions surface at the boundary, in tests, not in the ticketing system.
|
|
25
|
+
- **Negative, accepted**: every deterministic check needs a maintained data source (account registry access, date-parsing rules), and legitimate requests with unresolvable references land in human review — an accepted false-positive cost, since review is cheaper than misrouting.
|
|
26
|
+
- **On other boundaries**: the TriageRecord contract gains the provenance marker (additive, v1.0); the observability schema logs guard outcomes per field, which feeds the evaluation rubric; the middleware chain enforces that RoutingPort is unreachable except through the guard.
|
|
27
|
+
|
|
28
|
+
## Reevaluation trigger (mandatory, dated)
|
|
29
|
+
|
|
30
|
+
Reopen this decision if the guard's escalation rate — the share of requests sent to human review because fields stayed unverified — exceeds 25% over two consecutive weeks, measured on the observability schema. That level would mean the deterministic sources are too weak for real traffic and the verification strategy (not the principle) needs redesign. Until then, apply without reopening.
|
|
31
|
+
|
|
32
|
+
**Trigger set on**: 2026-05-12 · **Watched via**: guard-escalation rate on the weekly observability review
|
|
33
|
+
|
|
34
|
+
## References
|
|
35
|
+
|
|
36
|
+
- [architecture.md](../architecture.md) §3 — TriageRecord contract and provenance markers.
|
|
37
|
+
- [ADR-0001](ADR-0001-single-orchestrator.md) — the sequential plan that places this guard before RoutingPort.
|
|
38
|
+
- Framing note §3 — attached condition on compliance-category requests.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Architecture Note: Inbound Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-12 · **Version**: v0.1 · **Framing note**: [framing.md](framing.md) · **ADR journal**: [adr/](adr/)
|
|
4
|
+
|
|
5
|
+
## 1. Context
|
|
6
|
+
|
|
7
|
+
The organization triages ~400 heterogeneous inbound requests per week by hand; the success criterion is a first-assignment routing accuracy above the manual baseline, measured on real traffic, with zero silent misses on compliance-category requests. This architecture must carry the floor defined in the framing note: classify, extract key fields, route — nothing more. See [framing.md](framing.md).
|
|
8
|
+
|
|
9
|
+
## 2. Boundaries
|
|
10
|
+
|
|
11
|
+
- **Domain ports.** The triage domain is pure: it turns a raw request into a validated triage record and knows nothing about mailboxes, ticketing APIs, or model providers. Every dependency is a contract. The model port is a port like any other — the reasoning engine is bound by its contract (input text in, candidate classification and field extractions out), not by its brand. It is a replaceable adapter behind a stable port, which is also where the data-residency constraint is enforced: the adapter binds to the approved deployment; the domain never sees a provider.
|
|
12
|
+
- **Integration protocol.** Deterministic capabilities (field validation, account lookup, queue resolution) are exposed as tools through a single registry with a middleware chain (logging, access control, cost accounting, approval). Should any capability later move out of process, it is exposed over the standardized tool protocol: a service is an adapter that moved into its own process; the domain does not change.
|
|
13
|
+
|
|
14
|
+
## 3. Ports
|
|
15
|
+
|
|
16
|
+
| Port | Direction | Intent | Contract version | Spec |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| RequestIntakePort | primary | receive a raw inbound request (mailbox adapter, web-form adapter) | v1.0 | contracts/request-intake.md |
|
|
19
|
+
| ModelPort | secondary | propose classification and field extractions from raw text | v1.0 | contracts/model-port.md |
|
|
20
|
+
| RoutingPort | secondary | assign a validated triage record to a queue in the ticketing system — approval required: only for compliance-flagged records | v1.0 | contracts/routing-port.md |
|
|
21
|
+
| PersistencePort | secondary | append the triage decision to an immutable log, keyed by request ID | v1.0 | contracts/persistence-port.md |
|
|
22
|
+
|
|
23
|
+
The output contract is the **TriageRecord v1.0** schema: closed category vocabulary (`support`, `sales`, `compliance`, `unknown`), extracted fields each carrying a provenance marker (`computed`, `verified`, `model-proposed`), target queue, confidence level. The contract is versioned and evolves additively; consumers are tolerant readers. A record that fails schema validation is rejected fail-closed, never repaired silently.
|
|
24
|
+
|
|
25
|
+
## 4. Default topology and triggers
|
|
26
|
+
|
|
27
|
+
| Default | Rationale | Evolution trigger |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Modular hexagonal monolith | one deployable; pure domain plus adapters; 400 req/week needs no distribution | throughput or availability measurably insufficient; multi-instance required |
|
|
30
|
+
| Single orchestrator (ADR-0001) | triage is a short dependent sequence; composes, carries no business logic | a genuinely parallelizable or isolation-requiring subtask appears |
|
|
31
|
+
| Tool registry + middleware chain | single transversal surface for logging, access, cost, approval | — |
|
|
32
|
+
| One core language, thin model SDK | no heavy chain framework; the model is one port among four | mature library or proven performance need → sidecar behind the same port |
|
|
33
|
+
| Deterministic guard before RoutingPort (ADR-0002) | no model-proposed value acts unverified | — (a floor invariant, not a default to outgrow) |
|
|
34
|
+
|
|
35
|
+
## Rule conformance
|
|
36
|
+
|
|
37
|
+
| Rule | Status | Evidence |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| contracts-governance | applied | §3 TriageRecord v1.0 — versioned, additive, tolerant reader, fail-closed; contracts/ |
|
|
40
|
+
| hexa-architecture | applied | §2 pure triage domain, four ports; code/src/core/ |
|
|
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-0003 pending) |
|
|
43
|
+
| process-adr-and-journal | applied | adr/ADR-0001, adr/ADR-0002 — dated decisions with reevaluation triggers |
|
|
44
|
+
| security-mcp-server-pinning | n/a | the floor consumes no MCP or external tool server; tools are in-process and deterministic |
|
|
45
|
+
|
|
46
|
+
## 5. What stays open
|
|
47
|
+
|
|
48
|
+
Language, web framework, model provider, and hosting are explicitly undecided at this note's version. Each is an adapter decision, taken behind the contracts above and justified by a local technical reason — the core language will be locked in its own ADR at floor kickoff, chosen for team fluency and SDK maturity, not for the domain, which is language-agnostic by construction.
|
|
49
|
+
|
|
50
|
+
## 6. Legacy integration
|
|
51
|
+
|
|
52
|
+
The ticketing system is the system of record and predates this mission. An anticorruption adapter implements RoutingPort and translates the domain's `TriageRecord` into the ticketing API's dialect (its queue identifiers, its custom-field encoding, its pagination quirks). Named cost: the translation table must be maintained when the ticketing configuration changes, and queue resolution requires one extra lookup per routing. Accepted — the alternative is the ticketing dialect leaking into the domain.
|
|
53
|
+
|
|
54
|
+
## 7. Target, named
|
|
55
|
+
|
|
56
|
+
Per the framing note: auto-drafted acknowledgments under approval, priority scoring, requester memory, and a reassignment-to-evaluation feedback loop. Each would enter as a new adapter behind a new or existing port, without rewriting the core. Named for direction, not built.
|
|
57
|
+
|
|
58
|
+
## 8. Decisions
|
|
59
|
+
|
|
60
|
+
| Decision | ADR |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Single orchestrator, sequential triage | [ADR-0001](adr/ADR-0001-single-orchestrator.md) |
|
|
63
|
+
| Deterministic guard on extracted fields | [ADR-0002](adr/ADR-0002-deterministic-guard-on-extracted-fields.md) |
|
|
64
|
+
| Core language (open — to be locked at floor kickoff) | ADR-0003 (pending) |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Port Contract: ModelPort
|
|
2
|
+
|
|
3
|
+
## Port: ModelPort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: secondary (driven by the domain)
|
|
7
|
+
**Known adapters**: approved-deployment gateway adapter; deterministic keyword fallback classifier
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Propose a classification and key-field extractions for one raw request text. The output is a **hypothesis, never a fact**: every proposed value is marked `model-proposed` and nothing downstream may act on it before the deterministic guard (ADR-0002). The reasoning engine is bound by this contract, not by its brand — the fallback classifier honors the same contract with lower recall.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
propose(requestText) -> triageProposal — not idempotent, sync, no approval (proposes only, acts on nothing)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Input schema
|
|
20
|
+
|
|
21
|
+
| Field | Type | Required | Constraint |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| requestId | uuid | yes | propagated for tracing |
|
|
24
|
+
| requestText | string | yes | subject + body, untrusted; passed as data inside a fixed instruction frame |
|
|
25
|
+
|
|
26
|
+
## Output schema
|
|
27
|
+
|
|
28
|
+
| Field | Type | Always present | Constraint |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| category | enum | yes | closed vocabulary: `support`, `sales`, `compliance`, `unknown` |
|
|
31
|
+
| fields | list | yes | may be empty; each entry: name, proposed value, source span in the text |
|
|
32
|
+
| confidence | enum | yes | `high`, `medium`, `low` |
|
|
33
|
+
|
|
34
|
+
## Invariants
|
|
35
|
+
|
|
36
|
+
- Every proposed field carries the span of source text it was read from — a value with no span is rejected at the schema.
|
|
37
|
+
- The category never leaves the closed vocabulary; anything else is a validation failure, not a new category.
|
|
38
|
+
- All output is provenance-marked `model-proposed`; this port cannot produce `verified` or `computed` values.
|
|
39
|
+
|
|
40
|
+
## Errors
|
|
41
|
+
|
|
42
|
+
| Error | Type | Meaning for the consumer |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| timeout / overload | transient | retry with bounded backoff, then fall back |
|
|
45
|
+
| non-conforming output | validation | single retry with the diagnostic fed back; then treat as `unknown`, low confidence |
|
|
46
|
+
| gateway unavailable | unavailable | switch to the fallback adapter behind this same port; the domain does not notice |
|
|
47
|
+
|
|
48
|
+
## Evolution rule
|
|
49
|
+
|
|
50
|
+
Versioned; the category vocabulary belongs to the TriageRecord contract — extending it (e.g. v1.1 for the unknown-rate trigger, floor.md §5) is a governed, additive contract change gated by a labeled sample, never a free prompt edit.
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
- [../adr/ADR-0002-deterministic-guard-on-extracted-fields.md](../adr/ADR-0002-deterministic-guard-on-extracted-fields.md) — why this port's output never acts unguarded.
|
|
55
|
+
- [../governance/evaluation-rubric.md](../governance/evaluation-rubric.md) — how this port's behavior is evaluated.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Port Contract: PersistencePort
|
|
2
|
+
|
|
3
|
+
## Port: PersistencePort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: secondary (driven by the domain)
|
|
7
|
+
**Known adapters**: append-only store adapter
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Append every triage decision to an immutable log, keyed by request ID. The log is the system's truth: audit, replay and recovery all read from it; queue state and metrics are derived views. Nothing is ever updated or deleted — corrections are new entries that reference what they correct.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
append(triageDecision) -> logPosition — idempotent on (requestId, step), sync
|
|
17
|
+
readTrajectory(requestId) -> decisionList — idempotent, sync, read-only
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Input schema
|
|
21
|
+
|
|
22
|
+
| Field | Type | Required | Constraint |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| requestId | uuid | yes | one trajectory per request |
|
|
25
|
+
| step | enum | yes | `intake`, `proposal`, `guard`, `routing`, `escalation` |
|
|
26
|
+
| payload | object | yes | the step's full record, provenance markers included |
|
|
27
|
+
| recordedAt | timestamp | yes | UTC |
|
|
28
|
+
|
|
29
|
+
## Output schema
|
|
30
|
+
|
|
31
|
+
| Field | Type | Always present | Constraint |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| logPosition | integer | yes | strictly increasing; gap-free per trajectory |
|
|
34
|
+
|
|
35
|
+
## Invariants
|
|
36
|
+
|
|
37
|
+
- Append-only: no operation on this port mutates or removes an existing entry.
|
|
38
|
+
- A read never mutates state.
|
|
39
|
+
- A trajectory replays in order and completely from its requestId alone — the property verified at the floor gate (floor.md §2, observability check).
|
|
40
|
+
|
|
41
|
+
## Errors
|
|
42
|
+
|
|
43
|
+
| Error | Type | Meaning for the consumer |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| store unavailable | unavailable | fail closed for action-bearing steps: a decision that cannot be persisted does not act (routing blocks); intake buffers at the channel |
|
|
46
|
+
| idempotency conflict | business | same (requestId, step) with different payload — a bug upstream; the entry is refused and the run escalates |
|
|
47
|
+
|
|
48
|
+
## Evolution rule
|
|
49
|
+
|
|
50
|
+
Versioned; additive by default — new step types and optional payload fields extend the enum and schema without breaking readers, which tolerate unknown steps. Migrations are forward-only; the log is never rewritten.
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
- [../governance/observability-schema.md](../governance/observability-schema.md) — lifecycle events persisted through this port.
|
|
55
|
+
- [../runbook.md](../runbook.md) §3 — recovery replays this log, never the model.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Port Contract: RequestIntakePort
|
|
2
|
+
|
|
3
|
+
## Port: RequestIntakePort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: primary (drives the domain)
|
|
7
|
+
**Known adapters**: mailbox adapter, web-form adapter
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Deliver one raw inbound request into the triage domain, whatever channel it arrived on. A "request" here is a single message from one requester asking the organization for something — the channel's envelope (mail headers, form metadata) is normalized at this boundary and never leaks into the domain.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
submit(rawRequest) -> intakeReceipt — idempotent on sourceMessageId, sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Input schema
|
|
20
|
+
|
|
21
|
+
| Field | Type | Required | Constraint |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| source | enum | yes | `mailbox` or `web-form` |
|
|
24
|
+
| sourceMessageId | string | yes | channel-native ID; idempotency key |
|
|
25
|
+
| receivedAt | timestamp | yes | channel receipt time, UTC |
|
|
26
|
+
| senderAddress | string | yes | as given by the channel; verified later by the guard, not here |
|
|
27
|
+
| subject | string | no | absent for some form submissions |
|
|
28
|
+
| body | string | yes | raw free text, untrusted by definition |
|
|
29
|
+
|
|
30
|
+
## Output schema
|
|
31
|
+
|
|
32
|
+
| Field | Type | Always present | Constraint |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| requestId | uuid | yes | unique; the ID propagated through the whole trajectory |
|
|
35
|
+
| accepted | boolean | yes | false only on validation rejection |
|
|
36
|
+
|
|
37
|
+
## Invariants
|
|
38
|
+
|
|
39
|
+
- Every accepted request yields exactly one requestId; resubmitting the same sourceMessageId returns the same one.
|
|
40
|
+
- Intake never mutates the source channel and never triages — it accepts and hands over.
|
|
41
|
+
- The body is stored verbatim; normalization is recorded as derived data, never destructive.
|
|
42
|
+
|
|
43
|
+
## Errors
|
|
44
|
+
|
|
45
|
+
| Error | Type | Meaning for the consumer |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| malformed payload | validation | rejected at the boundary; the channel adapter keeps the original for manual pickup |
|
|
48
|
+
| duplicate sourceMessageId | business | not an error — same receipt returned (idempotent) |
|
|
49
|
+
| store unavailable | unavailable | fail closed: the adapter retries from the channel; nothing is acknowledged unpersisted |
|
|
50
|
+
|
|
51
|
+
## Evolution rule
|
|
52
|
+
|
|
53
|
+
Versioned; additive by default (new optional fields, tolerant readers); a breaking change goes expand-then-contract across both channel adapters before the old shape retires.
|
|
54
|
+
|
|
55
|
+
## References
|
|
56
|
+
|
|
57
|
+
- [../architecture.md](../architecture.md) §3 — port table.
|
|
58
|
+
- [model-port.md](model-port.md) — next step in the fixed sequential plan.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Port Contract: RoutingPort
|
|
2
|
+
|
|
3
|
+
## Port: RoutingPort
|
|
4
|
+
|
|
5
|
+
**Contract version**: v1.0
|
|
6
|
+
**Port type**: secondary (driven by the domain)
|
|
7
|
+
**Known adapters**: ticketing anticorruption adapter (translates TriageRecord into the ticketing API's dialect — see architecture.md §6)
|
|
8
|
+
|
|
9
|
+
## Business intent
|
|
10
|
+
|
|
11
|
+
Assign one validated triage record to a queue in the ticketing system — the organization's system of record. This is the system's only action on the world, which makes it the most guarded boundary: it acts on verified facts only, and fails closed.
|
|
12
|
+
|
|
13
|
+
## Signature
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
assign(triageRecord) -> routingConfirmation — idempotent on requestId, sync
|
|
17
|
+
— approval required: compliance-flagged records, always
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Input schema
|
|
21
|
+
|
|
22
|
+
| Field | Type | Required | Constraint |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| triageRecord | TriageRecord v1.0 | yes | schema-validated; every field carries a provenance marker |
|
|
25
|
+
| targetQueue | string | yes | resolved deterministically by queue resolution, never model-proposed |
|
|
26
|
+
|
|
27
|
+
## Output schema
|
|
28
|
+
|
|
29
|
+
| Field | Type | Always present | Constraint |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| ticketRef | string | yes | the ticketing system's reference for the routed request |
|
|
32
|
+
| routedAt | timestamp | yes | UTC |
|
|
33
|
+
|
|
34
|
+
## Invariants
|
|
35
|
+
|
|
36
|
+
- **Fail-closed on provenance**: any record whose action-bearing fields are still `model-proposed` is refused — it goes to human review, never through (ADR-0002).
|
|
37
|
+
- A compliance-flagged record is never assigned without a recorded human approval.
|
|
38
|
+
- Assigning the same requestId twice returns the existing ticketRef; no duplicate tickets.
|
|
39
|
+
- The ticketing dialect never crosses this boundary inward — translation lives entirely in the adapter.
|
|
40
|
+
|
|
41
|
+
## Errors
|
|
42
|
+
|
|
43
|
+
| Error | Type | Meaning for the consumer |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| provenance refusal | business | expected guard path — record escalates to review with per-field reasons |
|
|
46
|
+
| unknown target queue | business | ticketing configuration drifted; escalate to the on-call, translation table needs an update |
|
|
47
|
+
| ticketing API unavailable | unavailable | fail closed: record queues durably, retry with backoff — nothing routes blind, nothing is dropped |
|
|
48
|
+
|
|
49
|
+
## Evolution rule
|
|
50
|
+
|
|
51
|
+
Versioned; additive by default. The translation table in the anticorruption adapter is versioned with the contract and re-verified against the ticketing configuration on every ticketing-side change (the named cost of architecture.md §6).
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
|
|
55
|
+
- [../architecture.md](../architecture.md) §6 — the anticorruption boundary this port sits behind.
|
|
56
|
+
- [../governance/threat-model.md](../governance/threat-model.md) §4 — the approval points enforced here.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Decision Matrix
|
|
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-0003, pending). No trigger has fired; watched signals are tracked in [floor.md](floor.md) §4.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
| Decision | Sober default | Switch when |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Core language | One typed language for the whole core, chosen for team fluency and ecosystem depth | A capability genuinely requires another runtime, and the need is measured and isolated |
|
|
10
|
+
| Specialized capability | Implement it in the core language | A mature library exists only in another ecosystem (OCR, NLP, scientific computing) — wrap it as a sidecar behind a contract |
|
|
11
|
+
| Hot-path component | Same runtime as the rest of the core | Profiling shows this component dominates latency or cost, and a faster runtime demonstrably fixes it |
|
|
12
|
+
| Service split | Modular monolith, single process, boundaries enforced by ports | A module needs its own scaling profile, release cycle, or failure isolation — observed, not predicted |
|
|
13
|
+
| Legacy integration | A thin adapter wrapping direct calls to the legacy system | Legacy concepts start leaking into your domain — install a full anticorruption layer that translates and validates at the boundary |
|
|
14
|
+
| 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 |
|
|
15
|
+
| 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 |
|
|
16
|
+
| 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 |
|
|
17
|
+
| 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 |
|
|
18
|
+
| 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 |
|
|
19
|
+
| 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 |
|
|
20
|
+
| 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 |
|
|
21
|
+
| Budget & discovery (multi-agent) | One central counter caps total spend across all agents | Contention on the counter slows the fleet — hand out lease blocks of budget that agents consume locally and return |
|
|
22
|
+
| Model unavailable | Retry with backoff against the primary provider | The outage persists — fall back to a second provider behind the same port; the domain never notices the swap |
|
|
23
|
+
| Model change | Current model pinned; no silent upgrades | A candidate beats the incumbent in shadow deployment on your own rubric — staged rollout with rollback ready |
|
|
24
|
+
| Waiting on human approval | Suspend the run, persist its state, rehydrate when the decision lands — never hold a process open | Approvals become a chronic bottleneck — do not start blocking; revisit the autonomy boundary instead |
|
|
25
|
+
| 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 |
|
|
26
|
+
| 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 |
|
|
27
|
+
| 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 |
|
|
28
|
+
| Agent identity | Each agent runs as its own principal, with its own permissions and audit trail | The agent must act on behalf of a user — explicit, bounded, revocable delegation; never shared credentials |
|
|
29
|
+
| 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 |
|
|
30
|
+
| 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 |
|
|
31
|
+
|
|
32
|
+
## Express decision tree
|
|
33
|
+
|
|
34
|
+
Four questions, in order. If none fires, stay on the sober default.
|
|
35
|
+
|
|
36
|
+
1. **Does the task need a specialized library from another ecosystem?** → Sidecar behind a contract.
|
|
37
|
+
2. **Does a component have its own load profile, release cycle, or isolation requirement?** → Split that service.
|
|
38
|
+
3. **Is the work parallelizable, with real complexity in each branch?** → Multiple agents under one orchestrator.
|
|
39
|
+
4. **Does the load require more than one instance?** → Externalize state first, then replicate.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Execution Topology — Request Triage
|
|
2
|
+
|
|
3
|
+
> The bridge between the two visions: `architecture.md` names the four ports; this note records, port by port, where each adapter runs and under which sovereignty. runward traces the placement decision; it deploys nothing.
|
|
4
|
+
|
|
5
|
+
## 1. The two visions, behind the same ports
|
|
6
|
+
|
|
7
|
+
The triage domain says *what* the system does (raw request in, validated `TriageRecord` out). The execution topology says *where* each port's adapter runs. They are not two subjects: a placement is an adapter decision behind a stable port. Two of the four ports already reach beyond the process, and the domain does not notice.
|
|
8
|
+
|
|
9
|
+
## 2. Port → placement map
|
|
10
|
+
|
|
11
|
+
| Port | Adapter (what runs) | Location family | Data class(es) crossing it | Sovereignty level | ADR / evidence | Re-evaluation trigger |
|
|
12
|
+
|---|---|---|---|---|---|---|
|
|
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 (data residency at the port) | 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 | architecture.md §"Legacy integration"; ADR-0002 (deterministic guard) | the ticketing system moves or multi-tenants → re-evaluate the adapter placement |
|
|
16
|
+
| PersistencePort | append-only log, in-process | In-app | `TriageRecord` plus provenance (internal) | standard | architecture.md §3 | multi-instance required → externalized store (iterate) |
|
|
17
|
+
|
|
18
|
+
Traces are data: this floor exports none to a third party (see the conformance note below).
|
|
19
|
+
|
|
20
|
+
## 3. Usage registry
|
|
21
|
+
|
|
22
|
+
Risk is classed by deployment, not by platform. This floor is one deployment.
|
|
23
|
+
|
|
24
|
+
| Deployment | Risk class | Data classes touched | Action scopes | Owner / responsible | Last review |
|
|
25
|
+
|---|---|---|---|---|---|
|
|
26
|
+
| triage-bot / prod | medium | request text (personal possible), `TriageRecord` | read intake; write to ticketing (approval-gated for compliance-flagged records) | triage product owner | — |
|
|
27
|
+
|
|
28
|
+
## Rule conformance
|
|
29
|
+
|
|
30
|
+
| Rule | Status | Evidence |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| topology-port-placement-mapped | applied | §2 map — all four ports placed; the two non-in-app placements (ModelPort → managed vendor runtime under an approved deployment, RoutingPort → existing ticketing infra) are recorded in architecture.md §2 and the legacy-integration note |
|
|
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
|
+
| 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
|
+
| topology-usage-registry-present | applied | §3 usage registry — the single prod deployment with its risk class, data classes, action scopes and owner |
|
|
36
|
+
|
|
37
|
+
## Cross-references
|
|
38
|
+
|
|
39
|
+
- `architecture.md` — the four ports this note places.
|
|
40
|
+
- `shared-bricks.md` — placement families, criteria, brick matrix, sovereignty by data class.
|
|
41
|
+
- `governance/threat-model.md` — the ticketing system and model provider are untrusted surfaces.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Floor Note: Inbound Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-06-19 · **Version**: v0.1 · **Architecture note**: [architecture.md](architecture.md) · **Success criterion**: "The share of requests routed to the correct team on first assignment, measured on real inbound traffic over at least two weeks, exceeds the manual baseline measured over the same period the previous month. Attached condition: no compliance-category request may be routed to a non-compliance queue without human review."
|
|
4
|
+
|
|
5
|
+
> **Note.** All figures below are **illustrative**. They exist to show what a proof record contains and how it reads — not to report a real engagement.
|
|
6
|
+
|
|
7
|
+
## 1. Scope shipped
|
|
8
|
+
|
|
9
|
+
| Component | Status | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Entry point (mailbox + web-form adapters on RequestIntakePort) | shipped | wired to the real shared mailbox — actual traffic, not a demo feed |
|
|
12
|
+
| Single orchestrator | shipped | fixed sequential plan per ADR-0001; composes only, no business logic |
|
|
13
|
+
| Model port (real adapter, provider-agnostic) | shipped | active against the approved deployment with a key; deterministic keyword-based fallback classifier without |
|
|
14
|
+
| Persistence (immutable interaction log) | shipped | append-only, attached to entity: inbound request (one trajectory per request ID) |
|
|
15
|
+
| Deterministic guardrails | shipped | ADR-0002 guard on all action-bearing fields; provenance markers enforced at RoutingPort, fail-closed |
|
|
16
|
+
| Baseline observability + cost ceiling | shipped | request ID propagated end to end; per-run ceiling: 2 model calls, hard stop with escalation to review |
|
|
17
|
+
|
|
18
|
+
## Rule conformance
|
|
19
|
+
|
|
20
|
+
| Rule | Status | Evidence |
|
|
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 + keyword classifier — classification and validation are deterministic |
|
|
24
|
+
| config-secrets-boundary | n/a | the illustrative floor runs the deterministic keyword classifier; no provider secret is read in this example code |
|
|
25
|
+
| provider-llm-auto-detection | n/a | only the deterministic keyword adapter ships here; no real provider to auto-detect |
|
|
26
|
+
| security-prompt-injection | applied | threat-model §3 + ADR-0002 — model-proposed values never act; request text is data, not instruction |
|
|
27
|
+
| hexa-architecture | applied | code/src/core/ pure domain behind four ports |
|
|
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 |
|
|
31
|
+
| tools-scope-atomicity | applied | architecture §2 middleware chain + approval on RoutingPort for compliance records |
|
|
32
|
+
|
|
33
|
+
## 2. Proof against the success criterion
|
|
34
|
+
|
|
35
|
+
- **Traffic used**: 200 real requests replayed from the previous month's mailbox archive (stratified across the three categories to match observed proportions), then one week of live shadow traffic (~380 requests) routed in parallel with the manual process. No hand-picked cases.
|
|
36
|
+
- **Measured result** *(illustrative)*: routing accuracy 87% on first assignment on the replayed set, against a manual baseline of 71% reconstructed from the ticketing system's reassignment history for the same month. Live shadow week: 84%, baseline that week 73%. Attached condition held: 100% of compliance-category requests reached the compliance queue or human review; zero silent misses. Guard escalation rate: 14% (under the 25% reevaluation trigger of ADR-0002).
|
|
37
|
+
- **Verdict**: criterion met on the replayed sample and the first live week. The gate to `iterate` requires the full two-week live window per the criterion's wording — one more week of live measurement before the gate is crossed. Partial by duration, not by result.
|
|
38
|
+
- **Observability check**: confirmed — a full trajectory (intake, model proposal, per-field guard outcome, routing decision, persistence) reconstructs from a single request ID; verified on 10 randomly drawn requests.
|
|
39
|
+
|
|
40
|
+
## 3. Gaps and deviations
|
|
41
|
+
|
|
42
|
+
| Gap / deviation | Impact | Agreed with sponsor |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Manual baseline reconstructed from reassignment history, not observed live (the framing DoR risk) | baseline may understate manual accuracy — reassignments not logged in the ticketing system are invisible | 2026-06-05, sponsor accepted the reconstruction method and owns the residual uncertainty |
|
|
45
|
+
| Web-form adapter shipped one week after the mailbox adapter | first replay set is mailbox-only; live shadow week covers both | 2026-06-12 |
|
|
46
|
+
| `unknown` category runs at 9% of live traffic, above the 5% assumed at framing | more human-review load than planned; absorbed by coordinators so far | 2026-06-19, watched weekly |
|
|
47
|
+
|
|
48
|
+
## 4. Deferrals confirmed
|
|
49
|
+
|
|
50
|
+
| Deferred capability | Trigger being watched | Signal observed so far |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Auto-drafted acknowledgments | accuracy above baseline for 4 consecutive weeks | 1 of 4 weeks accumulated |
|
|
53
|
+
| Priority scoring in queues | deadline misses under FIFO in receiving teams | none |
|
|
54
|
+
| Requester memory / continuity | measured repeat-requester rate where prior context changes routing | early signs — 6% of live requests were repeat requesters; routing unchanged in all observed cases |
|
|
55
|
+
| Multi-agent decomposition | parallelizable or isolation-requiring subtask (ADR-0001) | none — attachments remain out of scope |
|
|
56
|
+
| Externalized state | multi-instance need or replay-on-restart failure | none |
|
|
57
|
+
|
|
58
|
+
## 5. Next tier
|
|
59
|
+
|
|
60
|
+
Hold the floor and complete the second live week to close the gate. The closest trigger is the `unknown`-category rate: if it stays above 5%, the evidence points to extending the category vocabulary — a governed, versioned contract change (TriageRecord v1.1), gated by a labeled sample of the `unknown` cases, not a free edit. No other trigger is near firing; no complexity is added without one.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Framing Note: Inbound Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-04 · **Sponsor**: Head of Operations · **Entry mode**: greenfield · **Stopping tier**: full chain
|
|
4
|
+
|
|
5
|
+
## 1. Problem
|
|
6
|
+
|
|
7
|
+
The organization receives roughly 400 inbound requests per week through a shared mailbox and a web form. They are heterogeneous — support issues, sales inquiries, compliance and data-privacy requests — and arrive as free text, often incomplete. Two operations coordinators triage them by hand: read, guess the category, hunt for the requester's identity and account, forward to one of three teams. Triage consumes about half of each coordinator's day. Misrouted requests bounce between teams for one to three days before reaching the right owner; compliance requests carry a regulatory response deadline, so a bounce there is not just friction, it is exposure. Routing quality varies with who triages and at what hour.
|
|
8
|
+
|
|
9
|
+
## 2. Value
|
|
10
|
+
|
|
11
|
+
Faster, more consistent routing. The receiving teams start work on day zero instead of day two; the coordinators spend their time on the ambiguous cases that actually need judgment instead of on the obvious bulk; deadline-bearing compliance requests stop losing days in transit. Value accrues on every request, every day — this is a high-frequency, low-glamour process, which is exactly what makes it worth automating.
|
|
12
|
+
|
|
13
|
+
## 3. Observable success criterion
|
|
14
|
+
|
|
15
|
+
**The share of requests routed to the correct team on first assignment, measured on real inbound traffic over at least two weeks, exceeds the manual baseline measured over the same period the previous month.** "Correct" is judged by the receiving team accepting the request without reassignment. Attached condition: no compliance-category request may be routed to a non-compliance queue without human review — a single silent miss there fails the gate regardless of the aggregate number.
|
|
16
|
+
|
|
17
|
+
## 4. Floor
|
|
18
|
+
|
|
19
|
+
The smallest system that proves value on real traffic: a qualifier that takes one raw request and produces a structured triage record — category (support / sales / compliance / unknown), extracted key fields (requester identity, account reference, stated deadline if any), a target queue, and a confidence level. One orchestrator, a model port for classification and extraction, deterministic guards that validate or recompute every extracted field before routing (see ADR-0002), persistence of every triage decision as an immutable log, baseline observability with a propagated request ID, and a per-run cost ceiling. Low-confidence and compliance-flagged requests route to a human review queue, never straight through. The floor routes; it does not answer requesters.
|
|
20
|
+
|
|
21
|
+
## 5. Target (named, not built)
|
|
22
|
+
|
|
23
|
+
Auto-drafted acknowledgments to requesters (under human approval); priority scoring within each queue; memory of past requester interactions for continuity; a feedback loop where reassignments automatically become labeled evaluation cases. Named to give direction only.
|
|
24
|
+
|
|
25
|
+
## 6. Named deferrals
|
|
26
|
+
|
|
27
|
+
| Deferred capability | Lean default in place | Trigger to revisit |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Auto-drafted acknowledgments | none — humans reply as today | routing accuracy holds above baseline for 4 consecutive weeks |
|
|
30
|
+
| Priority scoring in queues | FIFO within each queue | receiving team measurably fails deadline-bearing requests under FIFO |
|
|
31
|
+
| Requester memory / continuity | each request triaged independently | measured rate of repeat requesters where prior context changes the routing |
|
|
32
|
+
| Multi-agent decomposition | single orchestrator (ADR-0001) | a genuinely parallelizable or isolation-requiring subtask appears |
|
|
33
|
+
| Externalized state | in-memory queue state, single instance | move to multi-instance, or replay-on-restart proves insufficient |
|
|
34
|
+
|
|
35
|
+
## 7. Hard constraints
|
|
36
|
+
|
|
37
|
+
- Requests contain personal data; nothing leaves the organization's approved infrastructure. The model provider is an adapter behind a port, bound to the approved deployment.
|
|
38
|
+
- Compliance-category requests carry regulatory deadlines: they must never be silently misrouted (attached condition in §3) and always pass human review at the floor.
|
|
39
|
+
- The system routes and records; it never replies to a requester at the floor tier.
|
|
40
|
+
- The existing ticketing system is the system of record; the qualifier writes to it, it does not replace it.
|
|
41
|
+
|
|
42
|
+
## 8. Presumed boundaries
|
|
43
|
+
|
|
44
|
+
Foreseen ports: an inbound request port (primary — mailbox and web form feed it), a model port (classification and extraction behind a stable contract), a routing/output port toward the ticketing system, a persistence port for the triage log. Integration with the ticketing system will go through an anticorruption adapter — its API dialect must not leak into the domain. Language and topology are explicitly left open; they are adapter decisions for the `architect` phase.
|
|
45
|
+
|
|
46
|
+
## 9. Definition of Ready check
|
|
47
|
+
|
|
48
|
+
| Condition | Status | If missing: named risk |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Real problem, identified sponsor | met | — |
|
|
51
|
+
| Observable success criterion | met | — |
|
|
52
|
+
| Floor-first principle accepted | met | — |
|
|
53
|
+
| Access to the real process and people | met | — |
|
|
54
|
+
| Usable data or a path to it | **risk** | The manual baseline (correct-first-assignment rate) was never measured; historical reassignment data in the ticketing system is believed sufficient to reconstruct it, but this is unverified. **Owned by the sponsor**; measuring the baseline is the first object of discovery — without it the success criterion cannot be judged. |
|
|
55
|
+
| Access to technical infrastructure | met | — |
|
|
56
|
+
| Hard constraints known | met | — |
|
|
57
|
+
| Human available to decide and approve | met | — |
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Evaluation Rubric: Triage Qualifier (classification and extraction)
|
|
2
|
+
|
|
3
|
+
**Version**: v1.0 · **Anchored judge**: judge model pinned at the version deployed 2026-06-02; anchor set of 30 labeled requests replayed at every judge change · **Rerun triggers**: any change to the classification prompt, the model version or tier, or the category vocabulary (TriageRecord contract change)
|
|
4
|
+
|
|
5
|
+
> Figures and set sizes below are **illustrative**. The deterministic tests (guards, schema, queue resolution) live in the test suite; this rubric covers only the non-deterministic half — the model's proposals.
|
|
6
|
+
|
|
7
|
+
## 1. Capabilities evaluated
|
|
8
|
+
|
|
9
|
+
Floor-tier capabilities only. The long-memory capability set does not apply: the floor has no memory — each request is triaged independently (named deferral, framing §6). The rubric extends when that deferral trigger fires, not before.
|
|
10
|
+
|
|
11
|
+
| Capability | What it verifies |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Routing fidelity | the proposed category matches the labeled ground truth on real requests, across the closed vocabulary (support / sales / compliance / unknown) |
|
|
14
|
+
| Extraction fidelity | proposed key fields (requester identity, account reference, stated deadline) match what is actually present in the source text |
|
|
15
|
+
| Abstention | when the request is ambiguous or the information is absent, the model proposes `unknown` or omits the field — it never invents a category, an account reference or a deadline |
|
|
16
|
+
| Criterion compliance | no compliance-category request is proposed for a non-compliance queue; borderline compliance signals must lower confidence, which sends the record to human review |
|
|
17
|
+
|
|
18
|
+
## 2. Scoring scale
|
|
19
|
+
|
|
20
|
+
| Answer quality | Score |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Correct category and all present fields extracted, absent fields omitted | 2 |
|
|
23
|
+
| Correct category, one field missed or over-extracted | 1 |
|
|
24
|
+
| Wrong category between support and sales (recoverable misroute) | 0 |
|
|
25
|
+
| Invented field value, or a compliance request proposed outside compliance/review | −2, and the case is flagged for review of the guard path |
|
|
26
|
+
|
|
27
|
+
Deterministic scoring against labels wherever ground truth exists; the judge model is used only to grade borderline category judgment on ambiguous requests.
|
|
28
|
+
|
|
29
|
+
## 3. Scenarios
|
|
30
|
+
|
|
31
|
+
### Scenario RT-01 — capability: routing fidelity
|
|
32
|
+
- **Input**: a labeled replay request: password-reset complaint referencing an existing account.
|
|
33
|
+
- **Expected terms**: category `support`; the account reference exactly as written in the text.
|
|
34
|
+
- **Forbidden terms**: category `sales` or `compliance`; any account reference not present in the text.
|
|
35
|
+
|
|
36
|
+
### Scenario RT-07 — capability: abstention
|
|
37
|
+
- **Input**: a two-line request with no account reference and no identifiable ask ("hello, following up on my situation, please advise").
|
|
38
|
+
- **Expected terms**: category `unknown`, no extracted account reference, low confidence.
|
|
39
|
+
- **Forbidden terms**: any invented account reference or deadline; any confident category. The right answer is a motivated abstention that routes to human review.
|
|
40
|
+
|
|
41
|
+
### Scenario RT-12 — capability: criterion compliance
|
|
42
|
+
- **Input**: a request mixing a sales question with a personal-data deletion demand buried in the third paragraph.
|
|
43
|
+
- **Expected terms**: category `compliance` (or `unknown` with the compliance flag raised), deadline field extracted from the text.
|
|
44
|
+
- **Forbidden terms**: category `sales` with high confidence — the buried deletion demand is the regulatory payload; missing it silently is the single failure the criterion forbids.
|
|
45
|
+
|
|
46
|
+
## 4. Hold-out (non-gameable)
|
|
47
|
+
|
|
48
|
+
- **Composition**: 40 labeled requests drawn from the replay archive, stratified across categories, never used for prompt tuning — invisible to whoever adjusts the prompt.
|
|
49
|
+
- **Use**: replayed after every rerun trigger; a drop on the hold-out rolls the change back regardless of the visible-set score.
|
|
50
|
+
- **Limit**: ambiguous-category judgment has soft ground truth; for those cases the safety net is a weekly human sample from the review queue.
|
|
51
|
+
|
|
52
|
+
## 5. Judge anchoring
|
|
53
|
+
|
|
54
|
+
- **Anchoring method**: judge version pinned; the 30-request anchor set is replayed whenever the judge changes, to recalibrate the series before comparing new scores to old ones.
|
|
55
|
+
- **Judge scope**: only borderline category quality. The hard floor — the deterministic guard, the schema, the compliance attached condition — is tested deterministically, never delegated to the judge.
|
|
56
|
+
- **Action boundary**: the loop produces scores and flags; prompt or routing changes it motivates go through the operator and, when structural, an ADR — never autonomous self-rewriting.
|
|
57
|
+
|
|
58
|
+
## References
|
|
59
|
+
|
|
60
|
+
- [observability-schema.md](observability-schema.md) — the trace stream this loop samples from, off the hot path.
|
|
61
|
+
- [ADR-0002](../adr/ADR-0002-deterministic-guard-on-extracted-fields.md) — why invented values score −2 but never act regardless.
|
|
62
|
+
- [floor.md](../floor.md) §2 — the measured proof this rubric's replay set descends from.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Observability Schema: Inbound Request Triage Qualifier
|
|
2
|
+
|
|
3
|
+
**Version**: v1.0 · **Last review**: 2026-06-19
|
|
4
|
+
|
|
5
|
+
Wired from day zero through the middleware chain — the floor's proof (floor.md §2) was measured on this instrumentation, not retrofitted. Ceilings and field names are **illustrative**.
|
|
6
|
+
|
|
7
|
+
## 1. The three levels
|
|
8
|
+
|
|
9
|
+
| Level | What | Fields | Use |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| **Structured logs** | one JSON line per event | module, request_id, timestamp, level, event | aggregation, search, alerting on guard-escalation and unknown-category rates |
|
|
12
|
+
| **Lifecycle events** | every orchestrator step and tool call, persisted with the triage log | step, tool, input digest, output digest, status, duration | trajectory replay, behavioral audit, weekly observability review |
|
|
13
|
+
| **Per-model-call metrics** | one measurement per inference (max two per run) | input tokens, output tokens, model version, duration, status, attempt | cost tracking per request; feeds the per-run ceiling |
|
|
14
|
+
|
|
15
|
+
## 2. Propagated request ID
|
|
16
|
+
|
|
17
|
+
- **Origin**: generated at intake — one UUID per accepted request, mapped from the mailbox message ID or form submission ID (both stored for cross-reference).
|
|
18
|
+
- **Propagation**: passed to both model calls, every deterministic tool call, the guard, RoutingPort and PersistencePort.
|
|
19
|
+
- **Parent/child lineage**: not needed at the floor — single orchestrator, no sub-agents (ADR-0001); one trajectory per request ID. The field structure reserves a parent_id, empty until a topology trigger fires.
|
|
20
|
+
- **Carrier field**: `request_id` in logs, events and metrics.
|
|
21
|
+
|
|
22
|
+
## 3. Provenance
|
|
23
|
+
|
|
24
|
+
- **Per-inference fingerprint**: hash of the exact prompt frame plus raw request text injected on each call, stored with the lifecycle event.
|
|
25
|
+
- **Associated versions**: prompt version and model version recorded per inference, so a behavior change is attributable to the exact pair that produced it.
|
|
26
|
+
- **Linkage**: fingerprint, versions, guard outcomes and routing decision all share the request_id — one identifier unfolds the full decision.
|
|
27
|
+
|
|
28
|
+
## 4. Unfolding a decision (audit)
|
|
29
|
+
|
|
30
|
+
No consolidated memory exists at the floor — the triage log is raw and append-only, so unfolding is direct: given a request_id, replay intake payload, model proposal, per-field guard outcome with provenance marker (`computed`, `verified`, `model-proposed`), routing or escalation, and persistence — verified on 10 randomly drawn requests at the floor gate (floor.md §2). When requester memory enters (named deferral), consolidation pointers become mandatory here before the first consolidated item is written.
|
|
31
|
+
|
|
32
|
+
## 5. Cost ceilings
|
|
33
|
+
|
|
34
|
+
- **Per-run ceiling**: 2 model calls per request, hard stop — an overrun escalates the request to human review with the partial record, never loops.
|
|
35
|
+
- **Aggregate counter**: weekly model-spend counter across all runs; alert threshold set with the sponsor (illustrative: alert at the cost of 600 requests/week, ~1.5× observed volume).
|
|
36
|
+
- **Behavior on overrun**: intake continues, model calls pause, every request routes to the review queue via the deterministic fallback classifier — degraded but honest.
|
|
37
|
+
- **Structural cost levers**: the deterministic frontier (validation, parsing and queue resolution pay no model call); a single default tier (tier routing is a matrix arbitration, untriggered at 400 req/week); stable prompt frame for cache hits.
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- [evaluation-rubric.md](evaluation-rubric.md) — sampled off this trace stream.
|
|
42
|
+
- [threat-model.md](threat-model.md) — the immutable log guardrail this schema implements.
|
|
43
|
+
- [../runbook.md](../runbook.md) — incident diagnosis starts from the request_id.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Threat Model: Inbound Request Triage Qualifier
|
|
2
|
+
|
|
3
|
+
**Version**: v1.0 · **Last review**: 2026-06-19 · **Agent privilege level**: low
|
|
4
|
+
|
|
5
|
+
Principle applied: injection is constrained by architecture, not detection. The model is not a trust boundary — every request the system exists to process is untrusted content by definition, so the design assumes hostile text on every run.
|
|
6
|
+
|
|
7
|
+
## 1. Attack surfaces
|
|
8
|
+
|
|
9
|
+
| Surface | Description | Trust | Primary risk |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| **Untrusted input (direct)** | the raw request text itself — mailbox body, web-form fields; the entire workload | untrusted | direct injection: text crafted to steer classification or fabricate extractions |
|
|
12
|
+
| **Untrusted input (indirect)** | none at the floor — no retrieval, no external documents; attachments are out of scope (ADR-0001) | — | — |
|
|
13
|
+
| **Memory** | none at the floor — each request is triaged independently (named deferral, framing §6) | — | no persisted-injection surface exists yet; revisit when requester memory enters |
|
|
14
|
+
| **Tools** | account registry lookup, deterministic date parser, queue resolution — registry with middleware chain | guarded | a fabricated account reference resolving to the wrong customer |
|
|
15
|
+
| **Exposed surface** | none published — the qualifier consumes; it exposes no tool server | — | — |
|
|
16
|
+
| **Secrets and sensitive data** | model gateway credential, ticketing API credential, account registry access | internal | exfiltration via routed content |
|
|
17
|
+
|
|
18
|
+
## 2. Lethal trifecta
|
|
19
|
+
|
|
20
|
+
| Path / context window | Private data | Untrusted content ingested | Outbound communication | Verdict |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| Triage run (classify → guard → route) | yes — account registry, requester identity | yes — the request text is in context | **no** — the floor never replies to requesters; routing writes only to the internal ticketing system | safe (2 of 3) |
|
|
23
|
+
| Target tier: auto-drafted acknowledgments | yes | yes | yes — mail back to the requester | 3 of 3 → human approval on every send, already recorded in framing §5 |
|
|
24
|
+
|
|
25
|
+
**Context-window rule**: the floor holds at two of three by construction — outbound communication is removed from the tier, not filtered. The target's acknowledgment feature is the known 3-of-3 path; it enters only under per-send human approval, never autonomous.
|
|
26
|
+
|
|
27
|
+
## 3. Guardrails
|
|
28
|
+
|
|
29
|
+
- **Separation of the untrusted**: request text is passed to the model as data with a fixed instruction frame; nothing in the request can add tools or change the plan — the plan is fixed and sequential (ADR-0001).
|
|
30
|
+
- **Least privilege on tools**: the model proposes; it calls nothing. Deterministic tools run outside the model loop, on the orchestrator's fixed plan.
|
|
31
|
+
- **Deterministic guard**: no model-proposed value acts — every action-bearing field is recomputed or verified before RoutingPort (ADR-0002), fail-closed.
|
|
32
|
+
- **Human approval**: every compliance-flagged record passes human review before routing (framing §3 attached condition).
|
|
33
|
+
- **Output validation**: TriageRecord v1.0 schema enforced at the boundary; non-conforming records rejected, never repaired silently.
|
|
34
|
+
- **Immutable log**: every triage decision appended with provenance markers — any injected influence stays traceable per field.
|
|
35
|
+
|
|
36
|
+
## 4. Approval points
|
|
37
|
+
|
|
38
|
+
| Action | Approval trigger | Presentation to the human | If no response |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| Route a compliance-flagged record | always | deterministic summary: category, extracted fields with provenance markers, target queue — built from the record, never a model paraphrase | record waits in the review queue; the regulatory-deadline field is displayed so the queue is worked by deadline |
|
|
41
|
+
| Route a record whose fields stayed unverified | always (guard escalation, ADR-0002) | same deterministic summary, with the per-field guard failure reason | same review queue |
|
|
42
|
+
|
|
43
|
+
The review queue is prioritized (compliance deadline first) and summaries are uniform, so reviewers judge content, not layout — the rubber-stamp risk is watched via the guard-escalation rate (25% reevaluation trigger, ADR-0002).
|
|
44
|
+
|
|
45
|
+
## Rule conformance
|
|
46
|
+
|
|
47
|
+
| Rule | Status | Evidence |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| eval-loop | applied | evaluation-rubric.md — abstention scenarios, guard-escalation rate watched off the hot path |
|
|
50
|
+
| security-prompt-injection | applied | §3 guardrails — untrusted request text is data; deterministic guard before RoutingPort (ADR-0002) |
|
|
51
|
+
| config-secrets-boundary | n/a | the illustrative floor reads no provider secret (deterministic keyword classifier) |
|
|
52
|
+
| resilience-fail-open | applied | §3 — sensitive routing fails closed; the guard rejects on doubt (ADR-0002) |
|
|
53
|
+
| resilience-multi-provider-fallback | n/a | single deterministic classifier; no second provider in this floor |
|
|
54
|
+
| resilience-retry-backoff | n/a | in-memory adapters; no external call to retry in the shipped floor |
|
|
55
|
+
| async-job-guardrails | n/a | synchronous request triage; no background jobs at the floor |
|
|
56
|
+
| security-mcp-server-pinning | n/a | no MCP or external tool server consumed at the floor |
|
|
57
|
+
| security-tool-change-reapproval | n/a | tools are in-process and deterministic; no signed external tool to re-approve |
|
|
58
|
+
| data-memory-provenance | n/a | no persistent memory; each request is triaged independently (named deferral) |
|
|
59
|
+
| security-code-execution-sandbox | n/a | the floor runs no model-generated or tool-invoked code; the classifier is deterministic in-process code and RoutingPort calls a typed ticketing API, not code |
|
|
60
|
+
| security-human-agent-trust | applied | §3 — each TriageRecord field carries a provenance marker (computed / verified / model-proposed); RoutingPort approval on a compliance-flagged record shows provenance before the human decides (ADR-0002) |
|
|
61
|
+
|
|
62
|
+
## References
|
|
63
|
+
|
|
64
|
+
- [ADR-0002](../adr/ADR-0002-deterministic-guard-on-extracted-fields.md) — the structural defense on the action path.
|
|
65
|
+
- [observability-schema.md](observability-schema.md) — per-field guard outcomes feeding the audit trail.
|
|
66
|
+
- [evaluation-rubric.md](evaluation-rubric.md) — abstention scenarios exercising this model's failure modes.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Mission Contract: Inbound Request Triage
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-05-04 · **Sponsor**: Head of Operations · **Operator(s)**: one operator (anonymized) · **Indicative horizon**: framing in one week, floor proven in six weeks
|
|
4
|
+
|
|
5
|
+
> All dates, volumes and amounts in this contract are **illustrative** — they show what a signed steering contract looks like, not a real engagement.
|
|
6
|
+
|
|
7
|
+
## Principle
|
|
8
|
+
|
|
9
|
+
A deliverable is judged by its **acceptance against an observable criterion**, not by its form. Every engagement below carries two inseparable faces: what is handed over, and the condition that says it is done. That condition always ties back to the success criterion set in [framing.md](framing.md) §3.
|
|
10
|
+
|
|
11
|
+
## Engagements
|
|
12
|
+
|
|
13
|
+
| Engagement | Deliverables | Definition of Done |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **Flash framing** | Framing note: manual triage problem, value, observable success criterion, floor vs target split, hard constraints | Sponsor validates the criterion and the floor scope; deferrals named — **done 2026-05-06** |
|
|
16
|
+
| **Executable floor** | Qualifier wired to the real mailbox and web form; deterministic guards as testable code (ADR-0002); baseline observability; architecture note | The system triages real traffic and a first proof is measured against the criterion — see [floor.md](floor.md) §2 |
|
|
17
|
+
| **Staged iteration** | Increments on evidence only; structural decisions locked as ADRs; governance instrumented from day zero | Every increment measured; every added complexity traced to a fired trigger; every decision in the ADR journal |
|
|
18
|
+
| **Handover** | Runbook, contracts, evaluation set, transfer sessions | The operations team runs and evolves the system without the operator — demonstrated on a task redone autonomously |
|
|
19
|
+
|
|
20
|
+
## Acceptance of the whole mission
|
|
21
|
+
|
|
22
|
+
1. The qualifier **holds in production on real inbound traffic**, not in a demo.
|
|
23
|
+
2. First-assignment routing accuracy is **measured and exceeds the manual baseline**, or the gap is explained; zero silent compliance misses.
|
|
24
|
+
3. **Governance is in place**: threat model, evaluation rubric, observability schema, human review on compliance-flagged records.
|
|
25
|
+
4. Assets are **handed over and the team is autonomous** — demonstrated, not declared.
|
|
26
|
+
|
|
27
|
+
## Milestones and decision gates (illustrative dates)
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
DoR check --> Framing --> Executable floor --> Increments --> Handover
|
|
31
|
+
2026-05-04 2026-05-06 2026-06-19 on evidence autonomy
|
|
32
|
+
gate: floor proven? gate: increment holds?
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **After the floor** (2026-06-19): first proof measured on replay + one live shadow week; the gate to iterate closes only after the full two-week live window required by the criterion's wording.
|
|
36
|
+
- **After each increment**: gain measured and held; the next increment needs a fired trigger from the deferral table.
|
|
37
|
+
- **Before handover**: system governed, runbook exercised by the receiving team.
|
|
38
|
+
|
|
39
|
+
## The contract, filled with the sponsor
|
|
40
|
+
|
|
41
|
+
| Field | Agreed |
|
|
42
|
+
|---|---|
|
|
43
|
+
| **Problem** | ~400 heterogeneous inbound requests/week triaged by hand; misroutes cost 1–3 days; compliance requests carry regulatory deadlines |
|
|
44
|
+
| **Success criterion** | First-assignment routing accuracy on real traffic over ≥2 weeks exceeds the manual baseline of the same period the previous month; attached condition: no compliance request routed past human review |
|
|
45
|
+
| **Floor** | Classify, extract key fields under deterministic guard, route or escalate; immutable triage log; the floor never replies to requesters |
|
|
46
|
+
| **Target** | Auto-drafted acknowledgments under approval, priority scoring, requester memory, reassignment-to-evaluation loop — named, not built |
|
|
47
|
+
| **Engagements retained** | All four: flash framing, executable floor, staged iteration, handover |
|
|
48
|
+
| **Milestones & gates** | See arc above; a gate is crossed on measured evidence, never by calendar |
|
|
49
|
+
| **Deliverables & DoD** | Taken from the engagements table above |
|
|
50
|
+
| **Hard constraints** | Personal data stays on approved infrastructure; ticketing system remains the system of record; no outbound replies at the floor tier |
|
|
51
|
+
| **Risks owned by the sponsor** | The manual baseline was never measured live; reconstructed from ticketing reassignment history — owned by the sponsor since 2026-05-04 (framing DoR) |
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Runbook: Inbound Request Triage Qualifier
|
|
2
|
+
|
|
3
|
+
**Version**: v1.0 · **Last review**: 2026-06-19 · **Owner**: operations team (receiving team after handover)
|
|
4
|
+
|
|
5
|
+
This runbook lets the team that did not build the qualifier start it, keep it running, and bring it back after an incident. Names, channels and thresholds are **illustrative**.
|
|
6
|
+
|
|
7
|
+
## 1. Startup
|
|
8
|
+
|
|
9
|
+
- **Prerequisites**: ticketing API credential (required); model gateway credential (optional — without it the deterministic keyword fallback classifier runs and everything low-confidence goes to review); access to the shared mailbox and the web-form webhook; typed configuration validated at boot.
|
|
10
|
+
- **Start command**: single deployable — start the process; it validates configuration, then opens intake.
|
|
11
|
+
- **Feature detection at boot**: the model adapter probes the gateway once; if unreachable, the fallback classifier is enabled and the degradation is logged and announced on the ops channel. A missing observability sink degrades silently; a missing ticketing credential stops boot — routing without the system of record is not a mode.
|
|
12
|
+
- **Health check**: the startup log prints one line per port with its adapter and status. Watch: model gateway health, ticketing API health, triage-log store health.
|
|
13
|
+
|
|
14
|
+
## 2. Dependencies and degraded modes
|
|
15
|
+
|
|
16
|
+
| Dependency | Role | Criticality | Behavior on failure |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| Model gateway | classification and extraction proposals | degraded-capable | automatic switch to the keyword fallback classifier; low-confidence records flood the review queue — noisy but safe |
|
|
19
|
+
| Ticketing API | routing (system of record) | critical | fail-closed: validated records queue durably, routing retries with backoff; nothing is dropped, nothing routes blind |
|
|
20
|
+
| Triage-log store | immutable decision log | critical | fail-closed on writes: a decision that cannot be persisted does not act |
|
|
21
|
+
| Account registry | deterministic verification of account references | critical for the guard | affected fields stay `unverified`; records escalate to human review (ADR-0002 path) |
|
|
22
|
+
| Observability sink | traces, metrics | non-critical | silent degraded mode; local buffer replays on recovery |
|
|
23
|
+
|
|
24
|
+
**Transverse rule**: degrade reading and proposing, never acting. Routing fails closed, explicit and traced, rather than executing in doubt.
|
|
25
|
+
|
|
26
|
+
## 3. Checkpoints and recovery
|
|
27
|
+
|
|
28
|
+
- **State model**: the append-only triage log is the truth; queue state is derived and rebuilt from it. No hidden state in the process.
|
|
29
|
+
- **Recovery**: on restart, replay the triage log from the last routed position — reread recorded decisions; never re-call the model for an already-triaged request.
|
|
30
|
+
- **Replication**: single instance at the floor (named deferral, framing §6); the externalized-state row of the decision matrix fires before any second instance starts.
|
|
31
|
+
- **Records awaiting review**: the review queue is persisted; a restart loses nothing, reviewers resume exactly where the queue stood.
|
|
32
|
+
|
|
33
|
+
## 4. Common incidents
|
|
34
|
+
|
|
35
|
+
| Symptom | Error type | Diagnosis | Action |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| Gateway timeouts | transient | gateway health endpoint | bounded exponential backoff; fallback classifier after the retry budget |
|
|
38
|
+
| TriageRecord fails schema validation | validation | read the per-field diagnostic in the lifecycle event | single retry with the diagnostic fed back; then human review |
|
|
39
|
+
| Account reference does not resolve | business | trace the trajectory via request_id | expected guard behavior — record is in review with the failure reason; no action needed |
|
|
40
|
+
| Review queue growing (unknown rate above 5%) | capacity | weekly observability review; unknown-category rate | known watched signal (floor.md §5) — do not widen the category vocabulary ad hoc; it is a governed contract change |
|
|
41
|
+
| Weekly cost alert fires | ceiling | aggregate counter vs. intake volume | check for an intake loop or duplicate feed first; raising the ceiling is a sponsor decision |
|
|
42
|
+
|
|
43
|
+
## 5. Contacts
|
|
44
|
+
|
|
45
|
+
| Role | Person | Channel | Scope |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| Technical on-call | ops engineer on rotation | ops channel | operations, restarts, failover |
|
|
48
|
+
| Product owner | Head of Operations | direct | business decisions, ceiling changes |
|
|
49
|
+
| Sensitive-action approver | operations coordinators | review queue | compliance-flagged and guard-escalated records |
|
|
50
|
+
| Model infrastructure | platform team | platform channel | gateway escalation |
|
|
51
|
+
|
|
52
|
+
## 6. Model provider failover
|
|
53
|
+
|
|
54
|
+
- **Availability failover (immediate)**: the gateway drops → the keyword fallback classifier takes over behind the same ModelPort, automatically. Verify the review queue absorbs the extra load; coordinators are the backstop.
|
|
55
|
+
- **Manual override**: a configuration flag forces the fallback adapter; use it when the gateway degrades without failing outright.
|
|
56
|
+
- **Promoting a new model (earned)**: never in one move. Replay the labeled evaluation set (governance/evaluation-rubric.md), run one live shadow week against the incumbent, compare on the same rubric with the anchored judge, then stage the rollout with instant rollback.
|
|
57
|
+
- **Rollback**: repoint the adapter to the previous model version; the port contract is unchanged by design, so rollback is a configuration change, not a release.
|
|
58
|
+
|
|
59
|
+
## References
|
|
60
|
+
|
|
61
|
+
- [governance/observability-schema.md](governance/observability-schema.md) — every diagnosis starts from a request_id.
|
|
62
|
+
- [governance/threat-model.md](governance/threat-model.md) — why the review queue is the safe direction to degrade toward.
|
|
63
|
+
- [adr/ADR-0002-deterministic-guard-on-extracted-fields.md](adr/ADR-0002-deterministic-guard-on-extracted-fields.md) — the guard path incidents 3 and 4 traverse.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runward",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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
5
|
"keywords": [
|
|
6
6
|
"agentic",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
30
|
"templates",
|
|
31
|
+
"examples/request-triage/runward",
|
|
31
32
|
"README.md",
|
|
32
33
|
"NOTICE.md"
|
|
33
34
|
],
|
|
@@ -17,6 +17,7 @@ This project is delivered with the Runward method: floor first, evolution on evi
|
|
|
17
17
|
- Before any structural decision, run `runward/workflows/decision-loop.md`: verify in the real code, check the sourced state of the art, challenge the source, take a durable position, lock it in an ADR — only then edit.
|
|
18
18
|
- One ADR per structural decision, in `runward/adr/`, with a dated re-evaluation trigger. Use the template `runward/adr/ADR-0000-template.md`.
|
|
19
19
|
- Current phase and gates: see `runward/framing.md` (Definition of Ready) and each workflow's Definition of Done. Do not skip a gate on assertion; pass it on evidence.
|
|
20
|
+
- Offer to wire the gate at your harness's lifecycle seam — git `pre-commit`, a CI required check, or a turn-end hook — from the inert samples in `runward/adapters/` (its README has the exact command). Propose it to the operator and act only on their explicit approval; never wire it silently. runward installs nothing itself (ADR-0012): you are the operator's hands, and the operator owns the gate.
|
|
20
21
|
- Show before you build: for any deliverable meant for humans, produce a reviewable preview first.
|
|
21
22
|
- Never mark a phase done if its Definition of Done is not demonstrably met.
|
|
22
23
|
|