runward 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +27 -3
- package/dist/commands/check.js +26 -1
- package/dist/commands/status.js +2 -2
- package/dist/lib/behavioral-proof.js +46 -0
- package/dist/lib/conformance.js +9 -2
- package/dist/lib/mission.js +23 -4
- package/examples/request-triage/runward/floor.md +4 -0
- package/package.json +1 -1
- package/templates/mission/floor.md +5 -0
- package/templates/workflows/architect.md +3 -0
- package/templates/workflows/frame.md +2 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**After the spec, the hard part starts. Runward ships it and runs it.**
|
|
8
8
|
|
|
9
|
-
*
|
|
9
|
+
*The floor that keeps an agentic system alive. Run-grade engineering, from business need to production.*
|
|
10
10
|
|
|
11
11
|
Your agent builds fast. Runward frames what it builds — six gated phases your agent executes and one human signs off — so what ships actually holds.
|
|
12
12
|
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,8 @@ import { doctorCommand } from "./commands/doctor.js";
|
|
|
13
13
|
import { updateCommand } from "./commands/update.js";
|
|
14
14
|
import { characterizeCommand } from "./commands/characterize.js";
|
|
15
15
|
import { complianceCommand } from "./commands/compliance.js";
|
|
16
|
-
|
|
16
|
+
import { TOOL_IDS } from "./lib/tools.js";
|
|
17
|
+
// Exit codes: 0 = success · 1 = gaps/warnings · 2 = missing prerequisite or CLI misuse (typo, unknown flag)
|
|
17
18
|
process.on("uncaughtException", (err) => {
|
|
18
19
|
if (err.name === "ExitPromptError")
|
|
19
20
|
process.exit(130); // Ctrl+C in a prompt
|
|
@@ -52,7 +53,7 @@ program
|
|
|
52
53
|
.command("init")
|
|
53
54
|
.description("scaffold the mission structure (interactive wizard, or --yes)")
|
|
54
55
|
.option("-p, --path <path>", "project directory (default: prompt, or . with --yes)")
|
|
55
|
-
.option("-t, --tools <list>",
|
|
56
|
+
.option("-t, --tools <list>", `comma-separated tool profiles: ${TOOL_IDS.join(",")}`)
|
|
56
57
|
.option("--force", "overwrite existing files")
|
|
57
58
|
.option("--example", "scaffold a filled reference mission (request-triage) — the whole chain is green out of the box")
|
|
58
59
|
.action(initCommand);
|
|
@@ -91,4 +92,27 @@ program
|
|
|
91
92
|
.argument("[regime]", "iso-42001 | nist-ai-rmf | eu-ai-act")
|
|
92
93
|
.option("-p, --path <path>", "project directory")
|
|
93
94
|
.action(complianceCommand);
|
|
94
|
-
|
|
95
|
+
// exitOverride lets us map Commander's own errors onto runward's exit-code contract:
|
|
96
|
+
// a parse error (unknown command/option, missing/excess argument) is operator misuse → 2,
|
|
97
|
+
// so CI can tell a typo from a legitimate gate failure (exit 1). Help/version exit 0.
|
|
98
|
+
// Applied to the root AND every subcommand — it does not propagate on its own.
|
|
99
|
+
const MISUSE = new Set([
|
|
100
|
+
"commander.unknownCommand", "commander.unknownOption", "commander.invalidArgument",
|
|
101
|
+
"commander.missingArgument", "commander.excessArguments",
|
|
102
|
+
"commander.missingMandatoryOptionValue", "commander.optionMissingArgument",
|
|
103
|
+
]);
|
|
104
|
+
const onCommanderExit = (err) => {
|
|
105
|
+
const code = err?.code ?? "";
|
|
106
|
+
if (code === "commander.helpDisplayed" || code === "commander.version" || code === "commander.help")
|
|
107
|
+
process.exit(0);
|
|
108
|
+
process.exit(MISUSE.has(code) ? 2 : (err?.exitCode ?? 1)); // Commander already wrote the message
|
|
109
|
+
};
|
|
110
|
+
program.exitOverride(onCommanderExit);
|
|
111
|
+
program.commands.forEach((cmd) => cmd.exitOverride(onCommanderExit));
|
|
112
|
+
program.parseAsync().catch((err) => {
|
|
113
|
+
// An error escaping an async action — Commander's own exits are handled above.
|
|
114
|
+
console.error("\n " + c.error("✗") + " " + (err?.message ?? String(err)));
|
|
115
|
+
if (process.env.VERBOSE && err?.stack)
|
|
116
|
+
console.error(err.stack);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
package/dist/commands/check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
3
3
|
import { conformance, driftReport, unratifiedAdrs, decisionCoverage } from "../lib/conformance.js";
|
|
4
|
+
import { behavioralProof } from "../lib/behavioral-proof.js";
|
|
4
5
|
import { runHooks } from "../lib/hooks.js";
|
|
5
6
|
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
6
7
|
import { VERSION } from "../lib/paths.js";
|
|
@@ -67,7 +68,10 @@ export async function checkCommand(opts) {
|
|
|
67
68
|
for (const d of driftReport(mission, deliverable))
|
|
68
69
|
drift.push(`${label} · ${d.rule} — ${d.problem}`);
|
|
69
70
|
const { expected, violations } = conformance(mission, phase, deliverable);
|
|
70
|
-
|
|
71
|
+
// Non-vacuity (ADR-0002): when no rules are currently mapped to a phase, conformance()
|
|
72
|
+
// still raises a `(mapping)` violation if the mapping was stripped below its pinned
|
|
73
|
+
// floor. Only skip when there is genuinely nothing to report — never discard that signal.
|
|
74
|
+
if (expected.length === 0 && violations.length === 0)
|
|
71
75
|
continue;
|
|
72
76
|
checked++;
|
|
73
77
|
if (violations.length === 0) {
|
|
@@ -96,6 +100,27 @@ export async function checkCommand(opts) {
|
|
|
96
100
|
strictGaps += unratified.length;
|
|
97
101
|
}
|
|
98
102
|
if (checked > 0 && strictGaps === 0) {
|
|
103
|
+
// The two proofs, made legible together: this gate is the DOCUMENTARY proof (decisions traced);
|
|
104
|
+
// the BEHAVIORAL proof is the operator's test suite. runward reads the pointer, never runs the code.
|
|
105
|
+
console.log(section("Behavioral proof (advisory, above the gate)"));
|
|
106
|
+
console.log(" " + c.darkGray("this gate is the documentary proof: the decisions are traced. runward did not run your code — it is not a runtime. The behavioral proof is your test suite."));
|
|
107
|
+
const bp = behavioralProof(mission, root);
|
|
108
|
+
if (!bp.declared) {
|
|
109
|
+
console.log(" " + c.darkGray("no behavioral proof declared — add `Behavioral proof: <command>` to runward/floor.md §2 (and optionally `Proof artifact: <path>`)."));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
if (bp.command)
|
|
113
|
+
console.log(` ${c.darkGray("prove behavior with:")} ${c.white(bp.command)}`);
|
|
114
|
+
if (bp.artifact) {
|
|
115
|
+
if (!bp.present)
|
|
116
|
+
console.log(` ${c.warning("◑")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.warning("missing — run the command to produce it")}`);
|
|
117
|
+
else {
|
|
118
|
+
const fresh = bp.fresh === undefined ? "" : bp.fresh ? c.success(" · fresh") : c.warning(" · stale (older than the code — re-run)");
|
|
119
|
+
console.log(` ${c.success("✓")} ${c.darkGray("proof artifact")} ${c.white(bp.artifact)} ${c.darkGray(`(${bp.date})`)}${fresh}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log(" " + c.darkGray("advisory — runward reports presence and freshness, never runs or reads the result. You cross on both proofs."));
|
|
123
|
+
}
|
|
99
124
|
console.log(section("Semantic check (advisory, above the gate)"));
|
|
100
125
|
console.log(" " + c.darkGray("the gate proved every CRITICAL/HIGH rule was traced — not that the code applies it. Before you cross, run the verify workflow (runward/workflows/verify.md): an adversarial cite-vs-apply pass, ideally on a different model. Advisory, agent-executed, never blocks the gate (ADR-0007)."));
|
|
101
126
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
3
|
+
import { analyze, findMissionRoot, isRealAdr } from "../lib/mission.js";
|
|
4
4
|
import { c, createHeader, section } from "../lib/styles.js";
|
|
5
5
|
import { VERSION, WORKFLOWS } from "../lib/paths.js";
|
|
6
6
|
/** Mission snapshot: phase progress, decision journal, activity, workflows —
|
|
@@ -44,7 +44,7 @@ export async function statusCommand(opts) {
|
|
|
44
44
|
console.log(section("Decision journal"));
|
|
45
45
|
const adrDir = join(mission, "adr");
|
|
46
46
|
const adrs = existsSync(adrDir)
|
|
47
|
-
? readdirSync(adrDir).filter(
|
|
47
|
+
? readdirSync(adrDir).filter(isRealAdr).sort()
|
|
48
48
|
: [];
|
|
49
49
|
if (adrs.length === 0) {
|
|
50
50
|
console.log(c.darkGray(" no ADR yet — every structural decision must be locked"));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function behavioralProof(mission, root) {
|
|
4
|
+
const floor = join(mission, "floor.md");
|
|
5
|
+
if (!existsSync(floor))
|
|
6
|
+
return { declared: false };
|
|
7
|
+
const text = readFileSync(floor, "utf8");
|
|
8
|
+
const cmd = text.match(/^\s*[*_]*Behavioral proof[*_]*\s*:\s*`?([^`\n]+?)`?\s*$/mi);
|
|
9
|
+
const art = text.match(/^\s*[*_]*Proof artifact[*_]*\s*:\s*`?([^`\n]+?)`?\s*$/mi);
|
|
10
|
+
if (!cmd && !art)
|
|
11
|
+
return { declared: false };
|
|
12
|
+
const proof = { declared: true, command: cmd?.[1]?.trim(), artifact: art?.[1]?.trim() };
|
|
13
|
+
if (proof.artifact) {
|
|
14
|
+
const abs = join(root, proof.artifact);
|
|
15
|
+
proof.present = existsSync(abs);
|
|
16
|
+
if (proof.present) {
|
|
17
|
+
const st = statSync(abs);
|
|
18
|
+
proof.date = st.mtime.toISOString().slice(0, 10);
|
|
19
|
+
const codeDir = join(root, "code");
|
|
20
|
+
const newest = existsSync(codeDir) ? newestSourceMtime(codeDir) : 0;
|
|
21
|
+
proof.fresh = newest === 0 ? undefined : st.mtimeMs >= newest;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return proof;
|
|
25
|
+
}
|
|
26
|
+
/** Newest mtime of a tracked source file under a dir — read-only, node_modules and dotfiles skipped. */
|
|
27
|
+
function newestSourceMtime(dir) {
|
|
28
|
+
let newest = 0;
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
if (e.name === "node_modules" || e.name.startsWith("."))
|
|
38
|
+
continue;
|
|
39
|
+
const p = join(dir, e.name);
|
|
40
|
+
if (e.isDirectory())
|
|
41
|
+
newest = Math.max(newest, newestSourceMtime(p));
|
|
42
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs|java|rb)$/.test(e.name))
|
|
43
|
+
newest = Math.max(newest, statSync(p).mtimeMs);
|
|
44
|
+
}
|
|
45
|
+
return newest;
|
|
46
|
+
}
|
package/dist/lib/conformance.js
CHANGED
|
@@ -59,7 +59,9 @@ export function parseManifest(content) {
|
|
|
59
59
|
const cols = line.split("|").slice(1, -1).map((c) => c.replace(/`/g, "").trim());
|
|
60
60
|
if (cols.length < 3)
|
|
61
61
|
continue;
|
|
62
|
-
|
|
62
|
+
// Evidence may itself contain a pipe (a TS union `a | b`, a table hint) — rejoin the
|
|
63
|
+
// tail so it is not truncated, which would wrongly fail an n/a row on a trivial reason.
|
|
64
|
+
const rule = cols[0], status = cols[1], evidence = cols.slice(2).join(" | ");
|
|
63
65
|
if (/^rule$/i.test(rule) || /^:?-+:?$/.test(rule))
|
|
64
66
|
continue; // header / separator
|
|
65
67
|
rows.push({ rule, status: status.toLowerCase(), evidence });
|
|
@@ -71,7 +73,12 @@ function adrExists(missionDir, evidence) {
|
|
|
71
73
|
if (!id)
|
|
72
74
|
return false;
|
|
73
75
|
const dir = join(missionDir, "adr");
|
|
74
|
-
|
|
76
|
+
// Anchor on a digit boundary so a `deviated` row citing ADR-1 is not satisfied by
|
|
77
|
+
// ADR-10 / ADR-12 when filenames are unpadded.
|
|
78
|
+
return existsSync(dir) && readdirSync(dir).some((f) => {
|
|
79
|
+
const u = f.toUpperCase();
|
|
80
|
+
return u.startsWith(id) && !/[0-9]/.test(u.charAt(id.length));
|
|
81
|
+
});
|
|
75
82
|
}
|
|
76
83
|
/**
|
|
77
84
|
* The reconstruction lifecycle (ADR-0013/0014). A retroactively reconstructed decision is a
|
package/dist/lib/mission.js
CHANGED
|
@@ -54,13 +54,19 @@ export function findMissionRoot(cwd) {
|
|
|
54
54
|
// which distinguishes it from cross-references like [ADR-0001] or [framing.md].
|
|
55
55
|
// A closing bracket followed by "(" is a markdown link ([floor note](floor.md)) — never a placeholder.
|
|
56
56
|
const PLACEHOLDER = /\[[^\]\n]*\s[^\]\n]{1,80}\](?!\()/g;
|
|
57
|
+
/** A real ADR file: ADR-<n>-*.md, excluding the scaffolded template. Single source of
|
|
58
|
+
* truth so the mission, status, and conformance paths agree (they used to diverge:
|
|
59
|
+
* a `!f.includes("0000")` filter wrongly dropped e.g. ADR-0021-…-10000-ms.md). */
|
|
60
|
+
export function isRealAdr(f) {
|
|
61
|
+
return /^ADR-\d+/.test(f) && f.endsWith(".md") && f !== "ADR-0000-template.md";
|
|
62
|
+
}
|
|
57
63
|
export function artifactState(missionDir, a) {
|
|
58
64
|
const path = join(missionDir, a.relPath);
|
|
59
65
|
if (!existsSync(path))
|
|
60
66
|
return "missing";
|
|
61
67
|
// Special case: ADR directory — count real ADRs beyond the template.
|
|
62
68
|
if (a.relPath === "adr") {
|
|
63
|
-
const adrs = readdirSync(path).filter(
|
|
69
|
+
const adrs = readdirSync(path).filter(isRealAdr);
|
|
64
70
|
return adrs.length > 0 ? "filled" : "untouched";
|
|
65
71
|
}
|
|
66
72
|
// Special case: contracts directory — filled as soon as one .md is not the raw port-contract template.
|
|
@@ -77,9 +83,22 @@ export function artifactState(missionDir, a) {
|
|
|
77
83
|
const template = readFileSync(join(TEMPLATES, "mission", a.templateKey), "utf8");
|
|
78
84
|
if (content.trim() === template.trim())
|
|
79
85
|
return "untouched";
|
|
86
|
+
if ((content.match(PLACEHOLDER) || []).length >= 3)
|
|
87
|
+
return "in-progress";
|
|
88
|
+
// Divergence guard: a deliverable is "filled" only when it departs meaningfully from
|
|
89
|
+
// the scaffold. Templates with few placeholders (decision-matrix, execution-topology)
|
|
90
|
+
// cannot lean on the placeholder floor, so a one-byte interior edit would otherwise
|
|
91
|
+
// pass. Require several lines of genuinely new content beyond the template. Calibrated
|
|
92
|
+
// against the reference mission (its lightest fill adds 5 lines / 215 words).
|
|
93
|
+
const lines = (s) => s.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
94
|
+
const templateLines = new Set(lines(template));
|
|
95
|
+
const added = lines(content).filter((l) => !templateLines.has(l));
|
|
96
|
+
const addedWords = added.reduce((n, l) => n + l.split(/\s+/).filter(Boolean).length, 0);
|
|
97
|
+
if (added.length < 3 || addedWords < 20)
|
|
98
|
+
return "in-progress";
|
|
99
|
+
return "filled";
|
|
80
100
|
}
|
|
81
|
-
|
|
82
|
-
if (placeholders >= 3)
|
|
101
|
+
if ((content.match(PLACEHOLDER) || []).length >= 3)
|
|
83
102
|
return "in-progress";
|
|
84
103
|
return "filled";
|
|
85
104
|
}
|
|
@@ -90,7 +109,7 @@ export function analyze(missionDir) {
|
|
|
90
109
|
});
|
|
91
110
|
const adrDir = join(missionDir, "adr");
|
|
92
111
|
const adrCount = existsSync(adrDir)
|
|
93
|
-
? readdirSync(adrDir).filter(
|
|
112
|
+
? readdirSync(adrDir).filter(isRealAdr).length
|
|
94
113
|
: 0;
|
|
95
114
|
const firstIncomplete = phases.find((p) => !p.complete);
|
|
96
115
|
return { phases, adrCount, currentPhase: firstIncomplete ? firstIncomplete.spec.label : "all gates passed" };
|
|
@@ -37,6 +37,10 @@
|
|
|
37
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
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
39
|
|
|
40
|
+
**Behavioral proof**: `cd code && npm test`
|
|
41
|
+
|
|
42
|
+
> 14 deterministic tests, no key, no network — including the guard refusing, fail-closed, a record whose action-bearing fields are still model-proposed (ADR-0002). The gate above proves the *decision* to guard was traced; this proves the guard *runs*. runward reports the pointer, never runs it.
|
|
43
|
+
|
|
40
44
|
## 3. Gaps and deviations
|
|
41
45
|
|
|
42
46
|
| Gap / deviation | Impact | Agreed with sponsor |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runward",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "After the spec: ship and run. A delivery framework for agentic systems — floor first, evolution on evidence, governance from day zero, handover with proof.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
- **Verdict**: [criterion met / partially met / not met — and what that means for the gate.]
|
|
35
35
|
- **Observability check**: [confirmation that a full trajectory reconstructs from a single request ID.]
|
|
36
36
|
|
|
37
|
+
**Behavioral proof**: `[the command that proves the floor's behavior, e.g. cd code && npm test]`
|
|
38
|
+
**Proof artifact**: [optional — a result file the command writes, e.g. code/test-results.json; runward reports it present/fresh, never runs or reads it]
|
|
39
|
+
|
|
40
|
+
> The gate above is the *documentary* proof (the decisions are traced). This line is the *behavioral* proof (your code actually runs). runward never executes it — it is not a runtime; on a green `--strict` it only reports the pointer, and the artifact's presence and freshness if you name one.
|
|
41
|
+
|
|
37
42
|
## 3. Gaps and deviations
|
|
38
43
|
|
|
39
44
|
[Anything shipped differently from the architecture note, and anything the measurement revealed. Deviations were agreed with the sponsor, never silent.]
|
|
@@ -8,12 +8,14 @@ Use this workflow once framing is decided and structure must follow: "how do we
|
|
|
8
8
|
|
|
9
9
|
- The framing note: floor/target split, success criterion, hard constraints, presumed boundaries.
|
|
10
10
|
- The `mission/architecture.md`, `mission/port-contract.md`, and `mission/adr/ADR-0000-template.md` templates.
|
|
11
|
+
- The `mission/decision-matrix.md` template (the structuring defaults and the trigger that switches each).
|
|
11
12
|
- The `mission/shared-bricks.md` and `mission/execution-topology.md` templates (the infrastructure vision).
|
|
12
13
|
|
|
13
14
|
## Outputs
|
|
14
15
|
|
|
15
16
|
- A light architecture note.
|
|
16
17
|
- The port list with contracts and the integration protocol.
|
|
18
|
+
- The decision matrix adopted for the mission: each structuring choice with its sober default and the objective trigger that switches it (`mission/decision-matrix.md`).
|
|
17
19
|
- The execution-topology note: each port placed behind its location family, with data class and sovereignty.
|
|
18
20
|
- One ADR per structuring decision, including each non-in-app placement.
|
|
19
21
|
|
|
@@ -41,6 +43,7 @@ Use this workflow once framing is decided and structure must follow: "how do we
|
|
|
41
43
|
## Definition of Done
|
|
42
44
|
|
|
43
45
|
- Architecture note produced, boundaries first, stack open.
|
|
46
|
+
- Decision matrix adopted for the mission: every structuring choice carries a sober default and an objective trigger; the positions taken so far are recorded (`mission/decision-matrix.md`).
|
|
44
47
|
- Every port named with contract and initial version; integration protocol stated.
|
|
45
48
|
- Execution-topology note produced: every port placed behind a location family, with its data class(es) and sovereignty; non-in-app placements carry an ADR; the usage registry is seeded.
|
|
46
49
|
- One ADR per structuring choice, locked before the note mentions it.
|
|
@@ -13,6 +13,7 @@ Use this workflow at the start of any agentic-system mission, or whenever the pe
|
|
|
13
13
|
## Outputs
|
|
14
14
|
|
|
15
15
|
- A framing note (use the `mission/framing.md` template).
|
|
16
|
+
- A steering contract (use the `mission/mission-contract.md` template): what is delivered, on what condition it is accepted, and along which roadmap — filled with the sponsor, tying the success criterion to the deliverables and the milestones.
|
|
16
17
|
- The floor/target split with named deferrals.
|
|
17
18
|
- Presumed architecture boundaries — ports and integration protocol, stack left open.
|
|
18
19
|
|
|
@@ -55,6 +56,7 @@ Lock any truly structuring decision through `decision-loop` before committing it
|
|
|
55
56
|
## Definition of Done
|
|
56
57
|
|
|
57
58
|
- Framing note produced: problem, value, observable success criterion, hard constraints — one page.
|
|
59
|
+
- Steering contract filled with the sponsor: the success criterion tied to the deliverables and to what closes each engagement (`mission/mission-contract.md`).
|
|
58
60
|
- Floor perimeter listed, every deferral named with its trigger.
|
|
59
61
|
- Presumed boundaries stated; language and topology explicitly left open.
|
|
60
62
|
- Note reviewed via `review` before circulation.
|