@alphazede/bearing-lite 0.2.0 → 0.2.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 CHANGED
@@ -118,7 +118,7 @@ register hooks. That path remains first-class. See
118
118
 
119
119
  Bearing Lite never selects models, providers, credentials, or launchers. The
120
120
  owner provides each role's primary/fallback agent or harness, model, and
121
- reasoning level in `~/.agents/bearing-lite/default-role-lineup.md`, then confirms
121
+ reasoning level in `~/.agents/bearing-lite/lineups.json`, then confirms
122
122
  the applicable Journey snapshot before implementation.
123
123
 
124
124
  ## Routes and scaling
@@ -157,6 +157,7 @@ explain orientation; they never authorize a transition.
157
157
  | **Navigator** | Compatibility diagnostic | no | Not a normal role; existing plans reroute to Router |
158
158
  | **Explorer** | One-wave controller | no | Dispatches Crewmates; owns proven-independent lanes |
159
159
  | **Crewmate** | Bounded implementer | yes | Split test-writing versus product; neither self-certifies |
160
+ | **Light Implementer** | Mechanical implementer | yes | `work_class: light` slices only; verified by the packet's command; no repair loop |
160
161
  | **Scribe** | Event side lane | no | Transcribes; cannot activate authority |
161
162
  | **Plan Integrator** | Artifact reconciliation | no | Generates `implementation.json` and `review.html` |
162
163
  | **Systems Modeler** | Engineering views | no | After requirements; before design finalization |
@@ -201,6 +202,11 @@ Ordinary execution corrections remain bounded. The assurance gate allows one
201
202
  review-directed repair, followed by deterministic coordinator verification and
202
203
  no second review. Diagrams never create state or authorize transitions.
203
204
 
205
+ `hooks/reconcile.cjs` deterministically applies evidence events to Journey state.
206
+ It is a short-lived Router-run invocation, not a daemon or host event adapter;
207
+ hosts emit no events today, so invocation remains a procedural limitation.
208
+ It observes merge and issue closure but never grants acceptance, merges, or closes issues.
209
+
204
210
  ## Implementation process (explanatory)
205
211
 
206
212
  Default packet completion is author self-check plus coordinator confirmation.
@@ -9,25 +9,7 @@
9
9
 
10
10
  const fs = require("node:fs");
11
11
 
12
- const POLICY = Object.freeze({
13
- budget_scope: "per_declared_phase_or_wave",
14
- review_rounds: 1,
15
- aggregated_repairs_max: 1,
16
- post_repair_gate: "deterministic_PASS",
17
- automatic_phase_or_wave_end_review: "required",
18
- automatic_per_slice_review: "prohibited",
19
- post_repair_rereview: "prohibited",
20
- automatic_rereview_of_same_unit: "prohibited",
21
- budget_reset_on_candidate_change: false,
22
- budget_reset_on_model_change: false,
23
- budget_reset_on_harness_change: false,
24
- budget_reset_on_role_change: false,
25
- budget_reset_on_session_change: false,
26
- budget_reset_on_resume: false,
27
- budget_reset_on_alias_or_rename: false,
28
- budget_reset_condition:
29
- "next_distinct_declared_phase_or_wave_present_in_the_frozen_declaration",
30
- });
12
+ const { ASSURANCE_BUDGET_POLICY: POLICY } = require("./policy.cjs");
31
13
 
32
14
  /** The sentinel unit when the frozen declaration names neither waves nor phases. */
33
15
  const DIRECT = "direct";
@@ -39,6 +39,19 @@ const VERDICT_VALUES = new Set([
39
39
  "WAITING_ON",
40
40
  ]);
41
41
 
42
+ /** Closed verdicts that refuse protected completion (#55). */
43
+ const NEGATIVE_VERDICTS = new Set([
44
+ "BLOCK",
45
+ "FAIL",
46
+ "GAPS",
47
+ "NEEDS_MORE_EVIDENCE",
48
+ "OWNER_DECISION_REQUIRED",
49
+ "PARTIAL",
50
+ "REPAIR_REQUIRED",
51
+ "REROUTED",
52
+ "WAITING_ON",
53
+ ]);
54
+
42
55
  const RECOVERY_UNAVAILABLE =
43
56
  "Report UNAVAILABLE, complete the handoff checklist manually, and do not request protected completion until required fields and assurance are present";
44
57
  const RECOVERY_HANDOFF =
@@ -169,6 +182,8 @@ function evaluate(input) {
169
182
  const blockers = [];
170
183
  const handoffProblems = [...missing, ...invalid];
171
184
  if (handoffProblems.length > 0) blockers.push("handoff:" + handoffProblems.join(","));
185
+ const verdict = String((isPlainObject(input.handoff) ? input.handoff : input).verdict ?? "").trim();
186
+ if (NEGATIVE_VERDICTS.has(verdict)) blockers.push("verdict:" + verdict);
172
187
  if (missingAssurance.length > 0) {
173
188
  blockers.push("assurance:" + missingAssurance.join(","));
174
189
  }
@@ -283,6 +298,7 @@ module.exports = {
283
298
  ENFORCEMENT,
284
299
  HANDOFF_FIELDS,
285
300
  VERDICT_VALUES,
301
+ NEGATIVE_VERDICTS,
286
302
  evaluate,
287
303
  };
288
304
 
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Map the Route freeze (#70, #73, #77, #80): pure checks over a planning package.
5
+ * checkRoles: every command a slice runs whose seit procedure names an actor
6
+ * must name the slice's role. checkWorkClass validates light slices;
7
+ * checkPlanningRoles excludes planning-only roles from Expedition slices.
8
+ * verifyDigests: every planning input digest
9
+ * embedded in seit.json matches the file on disk; the manifest digest over
10
+ * those inputs plus seit.json matches implementation.json's
11
+ * planning_review.candidate_digest; missing artifacts, inputs, digests, or a
12
+ * specification Journey's SDoc register fail closed.
13
+ * CLI: node <plugin root>/hooks/plan-package.cjs <plan dir> -> JSON, exit 1 on any finding.
14
+ */
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+ const crypto = require("node:crypto");
19
+
20
+ const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex");
21
+
22
+ /** Walk any object graph and yield slice-like nodes. */
23
+ function* slices(node) {
24
+ if (Array.isArray(node)) {
25
+ for (const item of node) yield* slices(item);
26
+ } else if (node && typeof node === "object") {
27
+ const ids = [...(node.command_ids || []), ...(node.post_step_command_ids || [])];
28
+ if (typeof node.id === "string" && typeof node.role === "string") {
29
+ yield { id: node.id, role: node.role, commandIds: ids };
30
+ }
31
+ for (const value of Object.values(node)) yield* slices(value);
32
+ }
33
+ }
34
+
35
+ function checkRoles(seit, implementation) {
36
+ const procedures = new Map(
37
+ (seit?.procedures_and_commands || []).map((p) => [p.id, p])
38
+ );
39
+ const findings = [];
40
+ for (const slice of slices(implementation)) {
41
+ for (const command_id of slice.commandIds) {
42
+ const procedure = procedures.get(command_id);
43
+ if (!procedure) {
44
+ findings.push({ code: "unknown_command_id", step: slice.id, command_id });
45
+ } else if (typeof procedure.actor === "string" && procedure.actor !== slice.role) {
46
+ findings.push({
47
+ code: "actor_role_mismatch",
48
+ step: slice.id,
49
+ command_id,
50
+ seit_actor: procedure.actor,
51
+ implementation_role: slice.role,
52
+ files: ["seit.json", "implementation.json"],
53
+ });
54
+ }
55
+ }
56
+ }
57
+ return findings;
58
+ }
59
+
60
+ /** #77: a light slice must run at least one command and be routed to the Light Implementer. */
61
+ function checkWorkClass(node, findings = []) {
62
+ if (Array.isArray(node)) node.forEach((item) => checkWorkClass(item, findings));
63
+ else if (node && typeof node === "object") {
64
+ if (node.work_class === "light" && typeof node.id === "string") {
65
+ if (!(node.command_ids || []).length) findings.push({ code: "light_slice_without_command", step: node.id });
66
+ if (node.role !== "Light Implementer") findings.push({ code: "light_slice_role", step: node.id, role: node.role });
67
+ }
68
+ for (const value of Object.values(node)) checkWorkClass(value, findings);
69
+ }
70
+ return findings;
71
+ }
72
+
73
+ /** #80: Requirements Engineer is planning-only and never an Expedition slice. */
74
+ function checkPlanningRoles(node, findings = []) {
75
+ if (Array.isArray(node)) node.forEach((item) => checkPlanningRoles(item, findings));
76
+ else if (node && typeof node === "object") {
77
+ if (typeof node.role === "string" && node.role.startsWith("Requirements Engineer")) {
78
+ findings.push({
79
+ code: "planning_role_in_expedition",
80
+ step: typeof node.id === "string" ? node.id : null,
81
+ role: node.role,
82
+ });
83
+ }
84
+ for (const value of Object.values(node)) checkPlanningRoles(value, findings);
85
+ }
86
+ return findings;
87
+ }
88
+
89
+ function repoRoot(dir) {
90
+ let current = path.resolve(dir);
91
+ for (;;) {
92
+ if (fs.existsSync(path.join(current, ".git"))) return current;
93
+ const parent = path.dirname(current);
94
+ if (parent === current) return path.resolve(dir);
95
+ current = parent;
96
+ }
97
+ }
98
+
99
+ function readJson(file) {
100
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null;
101
+ }
102
+
103
+ function verifyDigests(dir) {
104
+ const findings = [];
105
+ for (const name of ["seit.json", "implementation.json"]) {
106
+ if (!fs.existsSync(path.join(dir, name))) findings.push({ code: "missing_artifact", path: name });
107
+ }
108
+ if (findings.length) return { manifest_digest: null, findings };
109
+ const seit = readJson(path.join(dir, "seit.json"));
110
+ const implementation = readJson(path.join(dir, "implementation.json"));
111
+ const root = repoRoot(dir);
112
+ const inputs = seit?.source_baseline?.planning_inputs;
113
+ if (!Array.isArray(inputs) || !inputs.length) {
114
+ findings.push({ code: "missing_planning_inputs", path: "seit.json" });
115
+ }
116
+ const actual = [];
117
+ for (const input of inputs || []) {
118
+ if (typeof input?.path !== "string" || !/^[0-9a-f]{64}$/.test(input?.sha256 || "")) {
119
+ findings.push({ code: "malformed_planning_input", input });
120
+ continue;
121
+ }
122
+ const file = path.resolve(root, input.path);
123
+ if (!fs.existsSync(file)) {
124
+ findings.push({ code: "missing_input", path: input.path });
125
+ continue;
126
+ }
127
+ const digest = sha256(fs.readFileSync(file));
128
+ actual.push(digest);
129
+ if (digest !== input.sha256) {
130
+ findings.push({ code: "digest_mismatch", path: input.path, recorded: input.sha256, actual: digest });
131
+ }
132
+ }
133
+ // The manifest binds the planning inputs and seit.json itself (#70).
134
+ actual.push(sha256(fs.readFileSync(path.join(dir, "seit.json"))));
135
+ const manifest_digest = sha256(actual.join("\n"));
136
+ const recorded = implementation?.journey_settings?.planning_review?.candidate_digest;
137
+ if (typeof recorded !== "string" || !recorded) {
138
+ findings.push({ code: "missing_candidate_digest", actual: manifest_digest });
139
+ } else if (recorded !== manifest_digest) {
140
+ findings.push({ code: "candidate_digest_mismatch", recorded, actual: manifest_digest });
141
+ }
142
+ // #69: a specification Journey records an existing SDoc register at planning.
143
+ const settings = implementation?.journey_settings || {};
144
+ if (settings.journey_type === "specification") {
145
+ const register = settings.requirement_register;
146
+ if (typeof register !== "string" || !register.endsWith(".sdoc") || !fs.existsSync(path.resolve(root, register))) {
147
+ findings.push({ code: "missing_requirement_register", recorded: register ?? null });
148
+ }
149
+ }
150
+ return { manifest_digest, findings };
151
+ }
152
+
153
+ function freeze(dir) {
154
+ const digests = verifyDigests(dir);
155
+ const seit = readJson(path.join(dir, "seit.json"));
156
+ const implementation = readJson(path.join(dir, "implementation.json"));
157
+ const findings = [
158
+ ...checkRoles(seit, implementation),
159
+ ...checkWorkClass(implementation),
160
+ ...checkPlanningRoles(implementation),
161
+ ...digests.findings,
162
+ ];
163
+ return { outcome: findings.length ? "FAIL" : "PASS", manifest_digest: digests.manifest_digest, findings };
164
+ }
165
+
166
+ module.exports = { checkRoles, checkWorkClass, checkPlanningRoles, verifyDigests, freeze };
167
+
168
+ if (require.main === module) {
169
+ const dir = process.argv[2];
170
+ if (!dir) {
171
+ process.stderr.write("usage: node hooks/plan-package.cjs <plan dir>\n");
172
+ process.exit(2);
173
+ }
174
+ const result = freeze(dir);
175
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
176
+ process.exit(result.outcome === "PASS" ? 0 : 1);
177
+ }
@@ -1,19 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const POLICY = Object.freeze({
4
- reviewer_slots_min: 2,
5
- reviewer_slots_max: 2,
6
- independence_required: true,
7
- isolated_findings_until_aggregation: true,
8
- candidate_fields: ["candidate_ref", "candidate_revision", "candidate_digest"],
9
- shared_candidate_required: true,
10
- review_rounds: 1,
11
- aggregated_repairs_max: 1,
12
- post_repair_gate: "deterministic_PASS",
13
- automatic_rereview: "prohibited",
14
- slot_exhaustion_outcome: "FAIL_ROUND",
15
- terminal_outcomes: ["HALT", "OWNER_AMENDMENT_REQUIRED"],
16
- });
3
+ const { PLANNING_REVIEW_POLICY: POLICY } = require("./policy.cjs");
17
4
 
18
5
  const sameCandidate = (a, b) =>
19
6
  POLICY.candidate_fields.every(
@@ -24,6 +11,14 @@ function evaluatePlanningReview(input) {
24
11
  if (!input || typeof input !== "object" || Array.isArray(input)) {
25
12
  return { outcome: "NEEDS_MORE_EVIDENCE", reason: "planning_review_missing" };
26
13
  }
14
+ // #69: a specification Journey gates its requirement register at planning;
15
+ // existence on disk is checked by hooks/plan-package.cjs at the freeze.
16
+ if (
17
+ input.journey_type === "specification" &&
18
+ !(typeof input.requirement_register === "string" && input.requirement_register.endsWith(".sdoc"))
19
+ ) {
20
+ return { outcome: "NEEDS_MORE_EVIDENCE", reason: "missing_requirement_register" };
21
+ }
27
22
  const slots = Array.isArray(input.reviewer_slots) ? input.reviewer_slots : [];
28
23
  const ids = slots.map((slot) => slot?.slot_id);
29
24
  const primaryRoutes = slots.map((slot) => slot?.primary_route_ref);
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Single policy source for the planning-review and assurance-budget
5
+ * evaluators (#63). The JSON blocks in
6
+ * skills/bearing-lite/references/review-policy.md and assurance-policy.md
7
+ * must equal these objects; test/policy-drift.test.mjs enforces it.
8
+ */
9
+
10
+ const PLANNING_REVIEW_POLICY = Object.freeze({
11
+ reviewer_slots_min: 2,
12
+ reviewer_slots_max: 2,
13
+ independence_required: true,
14
+ isolated_findings_until_aggregation: true,
15
+ candidate_fields: ["candidate_ref", "candidate_revision", "candidate_digest"],
16
+ shared_candidate_required: true,
17
+ review_rounds: 1,
18
+ aggregated_repairs_max: 1,
19
+ post_repair_gate: "deterministic_PASS",
20
+ automatic_rereview: "prohibited",
21
+ slot_exhaustion_outcome: "FAIL_ROUND",
22
+ terminal_outcomes: ["HALT", "OWNER_AMENDMENT_REQUIRED"],
23
+ });
24
+
25
+ const ASSURANCE_BUDGET_POLICY = Object.freeze({
26
+ budget_scope: "per_declared_phase_or_wave",
27
+ review_rounds: 1,
28
+ aggregated_repairs_max: 1,
29
+ post_repair_gate: "deterministic_PASS",
30
+ automatic_phase_or_wave_end_review: "required",
31
+ automatic_per_slice_review: "prohibited",
32
+ post_repair_rereview: "prohibited",
33
+ automatic_rereview_of_same_unit: "prohibited",
34
+ budget_reset_on_candidate_change: false,
35
+ budget_reset_on_model_change: false,
36
+ budget_reset_on_harness_change: false,
37
+ budget_reset_on_role_change: false,
38
+ budget_reset_on_session_change: false,
39
+ budget_reset_on_resume: false,
40
+ budget_reset_on_alias_or_rename: false,
41
+ budget_reset_condition:
42
+ "next_distinct_declared_phase_or_wave_present_in_the_frozen_declaration",
43
+ });
44
+
45
+ module.exports = { PLANNING_REVIEW_POLICY, ASSURANCE_BUDGET_POLICY };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ /** Deterministically apply evidence events to Journey state (#64). */
4
+ const fs = require("node:fs");
5
+
6
+ const REQUIRED = [
7
+ "schema_version", "event_id", "source", "journey", "repository", "unit",
8
+ "candidate_revision", "generation", "evidence", "transition", "occurred_at",
9
+ ];
10
+ const KINDS = new Set(["implementation", "verification", "assurance", "acceptance", "merge", "issue_close"]);
11
+ const FIELDS = {
12
+ implementation: "implementation",
13
+ verification: "verification",
14
+ assurance: "assurance",
15
+ acceptance: "acceptance",
16
+ merge: "observed_merge",
17
+ issue_close: "observed_issue_close",
18
+ };
19
+ const PREDECESSORS = { verification: "implementation", assurance: "verification", acceptance: "assurance" };
20
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
21
+ const nonempty = (value) => typeof value === "string" && value.length > 0;
22
+ const only = (value, names) => Object.keys(value).every((name) => names.includes(name));
23
+ const dateTime = (value) => nonempty(value) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
24
+
25
+ function receiptFor(event) {
26
+ return {
27
+ event_id: event?.event_id ?? null,
28
+ applied: false,
29
+ code: "invalid_event",
30
+ transition: event?.transition?.kind ?? null,
31
+ slice: event?.unit?.slice ?? null,
32
+ candidate_revision: event?.candidate_revision ?? null,
33
+ evidence_sha256: event?.evidence?.sha256 ?? null,
34
+ };
35
+ }
36
+
37
+ function structuralError(event) {
38
+ if (!object(event) || !REQUIRED.every((name) => Object.hasOwn(event, name)) || !only(event, REQUIRED)) return true;
39
+ if (event.schema_version !== 1 || !nonempty(event.event_id) || !nonempty(event.source) ||
40
+ !nonempty(event.journey) || !nonempty(event.repository) || !nonempty(event.candidate_revision) ||
41
+ !Number.isInteger(event.generation) || event.generation < 0 || !dateTime(event.occurred_at)) return true;
42
+ if (!object(event.unit) || !only(event.unit, ["wave", "slice"]) || !nonempty(event.unit.slice) ||
43
+ (Object.hasOwn(event.unit, "wave") && !nonempty(event.unit.wave))) return true;
44
+ if (!object(event.evidence) || !only(event.evidence, ["ref", "sha256"])) return true;
45
+ if (!object(event.transition) || !only(event.transition, ["kind", "verdict"]) ||
46
+ !KINDS.has(event.transition.kind) || !nonempty(event.transition.verdict)) return true;
47
+ return false;
48
+ }
49
+
50
+ function reject(state, receipt, code) {
51
+ return { state, receipt: { ...receipt, code, reason: code } };
52
+ }
53
+
54
+ function applyEvent(state, event, options = {}) {
55
+ const receipt = receiptFor(event);
56
+ if (structuralError(event)) return reject(state, receipt, "invalid_event");
57
+ if (options.expected_version !== undefined && options.expected_version !== state.version) {
58
+ return reject(state, receipt, "stale_write");
59
+ }
60
+ if (state.applied_event_ids.includes(event.event_id)) return reject(state, receipt, "duplicate_event");
61
+ if (event.journey !== state.journey || event.repository !== state.repository) {
62
+ return reject(state, receipt, "unrelated_journey");
63
+ }
64
+ if (event.generation < state.generation) return reject(state, receipt, "stale_generation");
65
+
66
+ const hasEvidence = nonempty(event.evidence.ref) && nonempty(event.evidence.sha256);
67
+ if (!hasEvidence && event.transition.verdict === "PASS") return reject(state, receipt, "bare_pass");
68
+ if (!hasEvidence) return reject(state, receipt, "missing_evidence");
69
+ if (!/^[0-9a-f]{64}$/.test(event.evidence.sha256)) return reject(state, receipt, "invalid_event");
70
+
71
+ const kind = event.transition.kind;
72
+ const slice = state.slices[event.unit.slice] || {};
73
+ const field = FIELDS[kind];
74
+ const prior = slice[field];
75
+ const history = prior ? {
76
+ ...prior.history,
77
+ [prior.candidate_revision]: { verdict: prior.verdict, sha256: prior.evidence.sha256 },
78
+ } : {};
79
+ const h = Object.hasOwn(history, event.candidate_revision) ? history[event.candidate_revision] : undefined;
80
+ if (h && (h.verdict !== event.transition.verdict || h.sha256 !== event.evidence.sha256)) {
81
+ return reject(state, receipt, "conflicting_receipt");
82
+ }
83
+ if (h) {
84
+ return reject(state, receipt, event.candidate_revision === prior.candidate_revision ? "duplicate_event" : "stale_candidate");
85
+ }
86
+
87
+ const implementation = slice.implementation;
88
+ if (["verification", "assurance", "acceptance"].includes(kind) &&
89
+ implementation?.candidate_revision !== event.candidate_revision) {
90
+ return reject(state, receipt, implementation ? "stale_candidate" : "predecessor_missing");
91
+ }
92
+ const predecessor = PREDECESSORS[kind];
93
+ if (predecessor && slice[predecessor]?.candidate_revision !== event.candidate_revision) {
94
+ return reject(state, receipt, "predecessor_missing");
95
+ }
96
+
97
+ const entry = {
98
+ candidate_revision: event.candidate_revision,
99
+ verdict: event.transition.verdict,
100
+ evidence: { ...event.evidence },
101
+ event_id: event.event_id,
102
+ history: { ...history, [event.candidate_revision]: { verdict: event.transition.verdict, sha256: event.evidence.sha256 } },
103
+ };
104
+ const next = {
105
+ ...state,
106
+ generation: Math.max(state.generation, event.generation),
107
+ version: state.version + 1,
108
+ applied_event_ids: [...state.applied_event_ids, event.event_id],
109
+ slices: { ...state.slices, [event.unit.slice]: { ...slice, [field]: entry } },
110
+ };
111
+ return { state: next, receipt: { ...receipt, applied: true, code: "applied" } };
112
+ }
113
+
114
+ function replay(state, events) {
115
+ const receipts = [];
116
+ let current = state;
117
+ for (const event of events) {
118
+ const result = applyEvent(current, event);
119
+ current = result.state;
120
+ receipts.push(result.receipt);
121
+ }
122
+ return { state: current, receipts };
123
+ }
124
+
125
+ function main(args = process.argv.slice(2)) {
126
+ const [stateFile, eventsFile] = args;
127
+ try {
128
+ if (!stateFile || !eventsFile) throw new Error("usage: node hooks/reconcile.cjs <state.json> <events.ndjson>");
129
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
130
+ const events = fs.readFileSync(eventsFile, "utf8").split(/\r?\n/).filter(Boolean).map(JSON.parse);
131
+ const result = replay(state, events);
132
+ const temporary = `${stateFile}.tmp-${process.pid}`;
133
+ fs.writeFileSync(temporary, JSON.stringify(result.state, null, 2) + "\n");
134
+ fs.renameSync(temporary, stateFile);
135
+ for (const receipt of result.receipts) process.stdout.write(JSON.stringify(receipt) + "\n");
136
+ return 0;
137
+ } catch (error) {
138
+ process.stderr.write(`${error.message}\n`);
139
+ return 1;
140
+ }
141
+ }
142
+
143
+ module.exports = { applyEvent, replay };
144
+
145
+ if (require.main === module) process.exitCode = main();
package/hooks/te-host.cjs CHANGED
@@ -584,6 +584,28 @@ function readStdinSync() {
584
584
  }
585
585
  }
586
586
 
587
+ /**
588
+ * Project the internal response onto the host wire (#71). Codex validates
589
+ * hookSpecificOutput with additionalProperties:false per event, so only host
590
+ * fields go to stdout: a PreToolUse deny, a Stop/SubagentStop block, or an
591
+ * empty object (allow / fail-open). The full internal verdict goes to stderr
592
+ * for audit; the exit stays 0 so no host reads stderr as a blocking reason.
593
+ */
594
+ function toWire(response) {
595
+ const inner = isPlainObject(response.hookSpecificOutput) ? response.hookSpecificOutput : {};
596
+ if (inner.permissionDecision === "deny") {
597
+ return {
598
+ hookSpecificOutput: {
599
+ hookEventName: "PreToolUse",
600
+ permissionDecision: "deny",
601
+ permissionDecisionReason: inner.permissionDecisionReason,
602
+ },
603
+ };
604
+ }
605
+ if (response.decision === "block") return { decision: "block", reason: response.reason };
606
+ return {};
607
+ }
608
+
587
609
  function main() {
588
610
  let raw = "";
589
611
  let response;
@@ -600,7 +622,8 @@ function main() {
600
622
  "adapter_failure: the test-engineering adapter could not run; unavailable"
601
623
  );
602
624
  }
603
- process.stdout.write(JSON.stringify(response) + "\n");
625
+ process.stderr.write(JSON.stringify(response) + "\n");
626
+ process.stdout.write(JSON.stringify(toWire(response)) + "\n");
604
627
  process.exit(0);
605
628
  }
606
629
 
@@ -613,6 +636,7 @@ module.exports = {
613
636
  supportChannel,
614
637
  handle,
615
638
  toHostResponse,
639
+ toWire,
616
640
  };
617
641
 
618
642
  if (require.main === module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphazede/bearing-lite",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Skills-first Agent Plugin for planning, routing, bounded execution, and independent review of repository work—without CLI, MCP, server, or hidden runtime state.",
5
5
  "keywords": [
6
6
  "agent-plugins",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "bearing-lite",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "Skills-first portable plugin that routes repository work through the smallest valid planning stages and agent roles, using project Markdown as the only task record.",
6
6
  "author": {
7
7
  "name": "William Rumph / AlphaZede",
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/alphazede/bearing-lite/schemas/event.schema.json",
4
+ "title": "Bearing Lite evidence event",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "event_id",
10
+ "source",
11
+ "journey",
12
+ "repository",
13
+ "unit",
14
+ "candidate_revision",
15
+ "generation",
16
+ "evidence",
17
+ "transition",
18
+ "occurred_at"
19
+ ],
20
+ "properties": {
21
+ "schema_version": { "const": 1 },
22
+ "event_id": { "type": "string", "minLength": 1 },
23
+ "source": { "type": "string", "minLength": 1 },
24
+ "journey": { "type": "string", "minLength": 1 },
25
+ "repository": { "type": "string", "minLength": 1 },
26
+ "unit": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["slice"],
30
+ "properties": {
31
+ "wave": { "type": "string", "minLength": 1 },
32
+ "slice": { "type": "string", "minLength": 1 }
33
+ }
34
+ },
35
+ "candidate_revision": { "type": "string", "minLength": 1 },
36
+ "generation": { "type": "integer", "minimum": 0 },
37
+ "evidence": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["ref", "sha256"],
41
+ "properties": {
42
+ "ref": { "type": "string", "minLength": 1 },
43
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
44
+ }
45
+ },
46
+ "transition": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["kind", "verdict"],
50
+ "properties": {
51
+ "kind": {
52
+ "enum": ["implementation", "verification", "assurance", "acceptance", "merge", "issue_close"]
53
+ },
54
+ "verdict": { "type": "string", "minLength": 1 }
55
+ }
56
+ },
57
+ "occurred_at": { "type": "string", "format": "date-time" }
58
+ }
59
+ }
@@ -64,27 +64,50 @@
64
64
  "human_decision": { "type": ["string", "object", "null"] },
65
65
  "authority_id": { "type": ["string", "null"] },
66
66
  "dispatchable": { "type": "boolean" },
67
- "slice_status": { "type": "string" }
67
+ "slice_status": { "type": "string" },
68
+ "work_class": {
69
+ "type": "string",
70
+ "enum": ["light", "judgement"],
71
+ "description": "light: inputs fully determine the output and the packet's command verifies it (skills/light-implementer). Default judgement."
72
+ },
73
+ "work_class_reason": { "type": "string", "minLength": 1 }
68
74
  },
69
- "if": {
70
- "not": {
71
- "anyOf": [
72
- {
73
- "properties": { "dispatchable": { "const": false } },
74
- "required": ["dispatchable"]
75
- },
76
- {
77
- "properties": { "slice_status": { "const": "publication-blocked" } },
78
- "required": ["slice_status"]
75
+ "allOf": [
76
+ {
77
+ "if": {
78
+ "not": {
79
+ "anyOf": [
80
+ {
81
+ "properties": { "dispatchable": { "const": false } },
82
+ "required": ["dispatchable"]
83
+ },
84
+ {
85
+ "properties": { "slice_status": { "const": "publication-blocked" } },
86
+ "required": ["slice_status"]
87
+ }
88
+ ]
79
89
  }
80
- ]
81
- }
82
- },
83
- "then": {
84
- "properties": {
85
- "authority_id": { "type": "string", "minLength": 1 }
90
+ },
91
+ "then": {
92
+ "properties": {
93
+ "authority_id": { "type": "string", "minLength": 1 }
94
+ }
95
+ }
96
+ },
97
+ {
98
+ "if": {
99
+ "properties": { "work_class": { "const": "light" } },
100
+ "required": ["work_class"]
101
+ },
102
+ "then": {
103
+ "required": ["work_class_reason"],
104
+ "properties": {
105
+ "role": { "const": "Light Implementer" },
106
+ "command_ids": { "minItems": 1 }
107
+ }
108
+ }
86
109
  }
87
- }
110
+ ]
88
111
  }
89
112
  },
90
113
  "properties": {
@@ -23,7 +23,10 @@
23
23
  "properties": {
24
24
  "id": { "type": "string", "minLength": 1 },
25
25
  "title": { "type": "string", "minLength": 1 },
26
- "status": { "type": "string", "minLength": 1 },
26
+ "status": {
27
+ "type": "string",
28
+ "enum": ["planning", "implementation", "complete", "cancelled"]
29
+ },
27
30
  "planning_repository": { "type": "string", "minLength": 1 }
28
31
  }
29
32
  },
@@ -50,7 +53,17 @@
50
53
  "candidate_revision": { "type": "string", "minLength": 1 },
51
54
  "acquired_at": { "type": "string", "minLength": 1 },
52
55
  "generation": { "type": "integer", "minimum": 1 },
53
- "state": { "enum": ["active", "released"] }
56
+ "state": { "enum": ["active", "released"] },
57
+ "released_at": { "type": "string", "format": "date-time" },
58
+ "release_reason": { "type": "string", "minLength": 1 }
59
+ },
60
+ "if": {
61
+ "properties": {
62
+ "state": { "const": "released" }
63
+ }
64
+ },
65
+ "then": {
66
+ "required": ["released_at", "release_reason"]
54
67
  }
55
68
  },
56
69
  "history": {
@@ -10,21 +10,22 @@ planning nodes return owner questions. Plugin hosts are partial; skill-copy is s
10
10
  planning or dispatch. A live same-checkout competitor returns `WAITING_ON` with sanitized identity.
11
11
  2. Resume the next incomplete stage in the same generation; refresh `candidate_revision`.
12
12
  Never replay accepted stages or duplicate dispatch. Invalid leases fail closed.
13
- 3. If `~/.agents/bearing-lite/default-role-lineup.md` is absent, create a
14
- proposed copy; never infer identity values. The recorded snapshot is authoritative for this Journey.
15
- Later edits to
16
- `~/.agents/bearing-lite/default-role-lineup.md` have no effect on it except through an
13
+ 3. Lineup comes only from `~/.agents/bearing-lite/lineups.json` per
14
+ `references/lineups.md`; a missing catalog returns `no_named_profiles`,
15
+ never a generated file; never infer identity values. The Router is observed, not selected.
16
+ The recorded snapshot is authoritative for this Journey. Later edits to
17
+ `~/.agents/bearing-lite/lineups.json` have no effect on it except through an
17
18
  explicit owner-confirmed dated visible amendment. Dispatch uses lineup identity from the recorded snapshot.
18
19
  4. Run Repository Fit → Set Bearings → Gather Supplies; unresolved material intent blocks Map the Route.
19
20
  5. Invoke Map the Route after settled intent. Do not ask for lineup or route
20
21
  before it; carry owner-supplied lineup and `review_cadence: at-end` as proposals.
21
- Follow `references/lineups.md` for catalog selection and save.
22
22
  6. Enforce `references/review-policy.md`. Show one integrated
23
23
  approval-or-change gate for outcome, design, route, lineup, role states,
24
- reasoning, cadence, and plan. Record the approved Journey type and snapshot; regenerate changes.
24
+ reasoning, cadence, and plan. Record the approved Journey type and snapshot.
25
25
  Never add a staged lineup or route-review gate.
26
26
  Dispatch only after approval.
27
- 7. Crewmate and Explorer may continue in-wave.
27
+ 7. Crewmate and Explorer may continue in-wave. `work_class: light` slices go to the
28
+ Light Implementer; a `reclassify: judgement` return re-dispatches to the Crewmate.
28
29
  Use the visible wave receipt and update implementation and review once per wave.
29
30
 
30
31
  Return `READY`, `WAITING_ON`, `OWNER_DECISION_REQUIRED`, or `COMPLETE`.
@@ -35,12 +36,10 @@ deterministically without another review; failed repair/scope change returns
35
36
  `OWNER_DECISION_REQUIRED` naming the candidate and count. `COMPLETE` ends Bearing assurance.
36
37
  Authorized deployment without reopening review.
37
38
  Planning review is a separate pre-dispatch gate; it never consumes implementation
38
- `required_assurance`, `assurance_rounds`, or `max_assurance_rounds`.
39
+ assurance.
39
40
  Release the lease once: release the checkout lease exactly once on `COMPLETE` or `CANCELLED`;
40
- recovery needs explicit recorded generation increment.
41
- Recovery cannot steal a live lease.
41
+ recovery needs explicit recorded generation increment and cannot steal a live lease.
42
42
  Selected or required capabilities activate. Unavailability of selected-or-required
43
43
  capability is a typed capability gap, not success and not invented behavior.
44
- Selected-only missing and required-only missing are typed gaps. Unselected
45
- and unrequired absence remains inactive, not a global failure.
44
+ Unselected and unrequired absence remains inactive, not a global failure.
46
45
  Never implement, self-assure, select models, or publish.
@@ -22,6 +22,32 @@ Missing user file means no named profiles. Do not auto-create the live
22
22
  catalog. An empty `lineups` object is valid and is not an error. A
23
23
  malformed unused catalog cannot override explicit inline owner choices.
24
24
 
25
+ ## Configurable roles
26
+
27
+ Catalog entries assign Explorer, Crewmate, Light Implementer, Test Engineer,
28
+ Scribe, Plan Integrator, Systems Modeler, Integration Engineer, Requirements
29
+ Engineer, Park Ranger, and Surveyor. The Light Implementer takes only slices
30
+ whose `work_class` is `light` (criteria in `skills/light-implementer`); it
31
+ has its own primary and fallbacks, usually a lighter and cheaper route. Navigator and Validator are not lineup roles;
32
+ existing plans that still assign them use the compatibility diagnostics and
33
+ treat the assignment as unused. Never fill agent, model, or reasoning values
34
+ on the user's behalf. `review_cadence` is `at-end`.
35
+
36
+ The Router is not a configurable role: it is whatever session is running
37
+ planning. The Journey snapshot records the observed Router identity
38
+ (harness, model, reasoning at that time). A catalog or snapshot entry with
39
+ `role: Router` is ignored with the typed note `router_row_ignored`; it is
40
+ never a deviation.
41
+
42
+ Only verified primary unavailability activates its approved fallback. If
43
+ both are unavailable, return `OWNER_DECISION_REQUIRED`.
44
+
45
+ This catalog is the single lineup source. A legacy
46
+ `~/.agents/bearing-lite/default-role-lineup.md` is never read or created;
47
+ when one is present it is ignored with the typed note
48
+ `legacy_lineup_md_ignored`. A missing catalog returns the typed outcome
49
+ `no_named_profiles` and an inline-selection prompt.
50
+
25
51
  ## Validity
26
52
 
27
53
  Named selection and save require a valid catalog. Bind
@@ -40,7 +40,10 @@ the integrated owner approval; do not offer `per-slice` or `per-round`.
40
40
  `journey` stays a proposal until the mapped implementation graph exists and the
41
41
  integrated owner review approves it. `lineup_snapshot` is authoritative after
42
42
  that approval. Later
43
- edits to `~/.agents/bearing-lite/default-role-lineup.md` have no effect.
43
+ edits to `~/.agents/bearing-lite/lineups.json` have no effect. The `Router`
44
+ row of the snapshot is the observed identity of the session that ran planning
45
+ (harness, model, reasoning at that time); it is never a catalog selection and
46
+ never a deviation.
44
47
  Replace it only through an explicit owner-confirmed dated visible amendment.
45
48
  Record the amendment date beside the replacement values. Dispatch identities
46
49
  come from this snapshot, not from the current global defaults file.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: light-implementer
3
+ description: >
4
+ Execute one approved light slice exactly as its packet states, verified by
5
+ the packet's deterministic command. Use for Light Implementer, light,
6
+ mechanical, scaffold, bind, assemble, regenerate, or runbook packets. Do
7
+ not use for judgement work, authoring, repair, review, or any decision.
8
+ ---
9
+
10
+ # Light Implementer
11
+
12
+ The cheapest route in the ladder. Inputs fully determine the output, a
13
+ command decides pass or fail, and nothing is decided in-session.
14
+
15
+ ## Inputs and match
16
+
17
+ - **Inputs:** the Crewmate packet contract (baseline, objective, exact write
18
+ set, authority, commands, stop rule, return schema, visible wave receipt,
19
+ lineup identity from the recorded snapshot) for a slice whose
20
+ `work_class` is `light`.
21
+ - **Match:** every criterion holds:
22
+ 1. Inputs are all named and present: paths, digests, UIDs, a runbook.
23
+ Nothing is discovered or interpreted.
24
+ 2. The transformation is mechanical: copy, assemble, format, fill a
25
+ template, append rows, run a documented command, record its output.
26
+ 3. The packet names the command whose exit status verifies the output.
27
+ 4. No decision: no value chosen, ambiguity resolved, candidate selected,
28
+ requirement, contract, rationale, or verification case written.
29
+ 5. Bounded blast radius: one write set, no product source, no host or
30
+ runtime mutation beyond a documented read-only or prepare call.
31
+ - **Non-match:** a `judgement` slice; a packet whose LOOP says repair your
32
+ own findings; an `[OPEN]` value; a register conflict to weigh.
33
+
34
+ ## Algorithm
35
+
36
+ 1. Revalidate the checkout lease exactly as the Crewmate does. On mismatch
37
+ return `WAITING_ON` without writing.
38
+ 2. Do exactly what the packet states, inside the write set, and nothing else.
39
+ 3. Run the packet's verification command at the candidate revision. Record
40
+ exit status, output digest, and changed paths.
41
+ 4. If any step needs a choice the packet did not make, stop before writing
42
+ further and return `NEEDS_MORE_EVIDENCE` with `reclassify: judgement`
43
+ and the exact question. Never guess, never escalate silently.
44
+ 5. There is no in-wave repair loop. A failing command returns the typed
45
+ failure with its output; the Router decides.
46
+
47
+ ## Return and recovery
48
+
49
+ Return `CANDIDATE_READY`, `NEEDS_MORE_EVIDENCE`, `WAITING_ON`, or
50
+ `OWNER_DECISION_REQUIRED` with verdict, candidate_ref, changed_paths, tests,
51
+ findings, blocker, and `reclassify` when set.
52
+
53
+ Never author, repair, self-certify, expand the write set, or publish.
@@ -15,7 +15,7 @@ Fresh planning node. The Router writes Journey state and owns owner conversation
15
15
 
16
16
  - **Match:** material intent is settled and any technical-plan, design, SEIT,
17
17
  implementation graph, or review HTML is missing.
18
- - **Inputs:** confirmed decisions, repository map and evidence, artifact status,
18
+ - **Inputs:** confirmed decisions, workspace.md and evidence, artifact status,
19
19
  requirements register, repository rules, proposed owner-supplied lineup and
20
20
  `review_cadence: at-end`, plus the return schema.
21
21
  - **Non-match:** unresolved material scope, behavior, authority, risk, or
@@ -38,7 +38,8 @@ Fresh planning node. The Router writes Journey state and owns owner conversation
38
38
  `review_cadence: at-end`. Bind the planning-review slots to owner-supplied
39
39
  primary and ordered fallback route references under one candidate ref,
40
40
  revision, and digest. Use supplied identities; never invent them.
41
- 4. After those stable source inputs, generate `implementation.json` and the
41
+ 4. After those stable source inputs, freeze: `node <plugin root>/hooks/plan-package.cjs <plan dir>`
42
+ must PASS; any finding halts. Then generate `implementation.json` and the
42
43
  self-contained offline `review.html` together. Each includes the proposed
43
44
  route, lineup, role states, reasoning, cadence, traceability, waves,
44
45
  recovery, approval boundaries, and register references versus Journey-local
@@ -56,6 +56,13 @@ optional derived export and is never authority.
56
56
  Journey-local, so a reviewer can tell which artifact owns each statement.
57
57
  6. `design.md` is unaffected: it records how the work is built, which no
58
58
  requirements register covers.
59
+ 7. A specification-authoring Journey carries its requirement register (UID,
60
+ statement, rationale, verification method, allocation) as a planning
61
+ artifact: a draft `.sdoc` path (Markdown sections are not lint-checkable),
62
+ recorded as `implementation.json` `journey_settings.journey_type:
63
+ specification` plus `journey_settings.requirement_register`. The freeze
64
+ fails without an existing register; the Requirements Engineer gates it
65
+ before the integrated owner review; no Expedition wave re-gates it.
59
66
 
60
67
  ## Published standards
61
68
 
@@ -82,7 +89,9 @@ optional derived export and is never authority.
82
89
  case | Negative/failure case | Command/procedure ID | Evidence.
83
90
  5. Every row carries exactly one SEIT row ID, requirement ID, design ID, and
84
91
  command ID, and names an observable failure.
85
- 6. Bind a stable decision-baseline projection of confirmed decision identities
92
+ 6. For a specification wave, each case-authoring proof row cites the planning
93
+ register gate receipt.
94
+ 7. Bind a stable decision-baseline projection of confirmed decision identities
86
95
  and open-item statuses rather than the whole-file `journey.json` digest.
87
96
 
88
97
  ## Implementation rules
@@ -94,18 +103,26 @@ optional derived export and is never authority.
94
103
  owner-selected model route, reasoning, review path, write set, command IDs,
95
104
  stop condition, human decision, and `authority_id`. Goals are at most 512
96
105
  characters. Slice actions and write sets are subsets of current authority.
97
- 3. Optional fields are Shared interfaces (`path#Symbol`), Integration
106
+ 3. `work_class` is `light` or `judgement` (default). A `light` slice names
107
+ the Light Implementer role, at least one command id, and a
108
+ `work_class_reason`; see `skills/light-implementer` for the criteria.
109
+ 4. For `journey_type: specification`, a specification-authoring Expedition
110
+ wave defaults to scaffold → author from the gated register → Test Engineer
111
+ verification cases → bind to the host → readback of the bound revision and
112
+ digest from the host → Park Ranger with both named in the review
113
+ request → owner decision. A deviation is a planning-review finding.
114
+ 5. Optional fields are Shared interfaces (`path#Symbol`), Integration
98
115
  boundary, Published standard (`doc#clause`) when applicable, SysML and
99
116
  integration fields when selected, and Parallel safe (`yes` or `no` plus
100
117
  reason).
101
- 4. Write sets use one line: `Write only `path``. Paths are bounded, normalized,
118
+ 6. Write sets use one line: `Write only `path``. Paths are bounded, normalized,
102
119
  repository-relative literals. Put prohibitions in prose, not the write set.
103
- 5. Multi-slice plans declare consecutive `Wave <n>: <ids>` lines. Every slice
120
+ 7. Multi-slice plans declare consecutive `Wave <n>: <ids>` lines. Every slice
104
121
  belongs to one wave. Dependencies use acyclic `S1 --> S2` arrows.
105
- 6. Ordered integration steps, resources, ownership, and rollback live here.
122
+ 8. Ordered integration steps, resources, ownership, and rollback live here.
106
123
  Owner-configured reviewer count `n`, repair bound `k`, and confirmation
107
124
  count `c` are explicit fields with no assistant default integers.
108
- 7. Plans may contain at most 128 slices, manifests, write paths, and commands.
125
+ 9. Plans may contain at most 128 slices, manifests, write paths, and commands.
109
126
  Aim for at most 500 estimated tokens per slice plus manifest; split larger
110
127
  packets when practical.
111
128
 
@@ -23,9 +23,18 @@ the five artifacts, and generates `implementation.json` and two-state
23
23
 
24
24
  1. Cross-validate the five canonical artifacts. Copy design-lens names
25
25
  from design.md; never invent lens IDs.
26
- 2. Generate `implementation.json` and `review.html` (`planning-review`,
26
+ 2. For `journey_type: specification`, default an Expedition wave to scaffold →
27
+ author from the gated register → Test Engineer verification cases → bind to the host
28
+ → readback of the bound revision and digest from the host → Park
29
+ Ranger with both named in the review request → owner decision. A deviation
30
+ is a planning-review finding.
31
+ 3. Generate `implementation.json` and `review.html` (`planning-review`,
27
32
  then `final-closeout`) when inputs are stable.
28
- 3. Request a Planning Test Engineer delta after relevant decision,
33
+ 4. Classify every slice `work_class: light` or `judgement` with a
34
+ `work_class_reason`, using the five criteria in `skills/light-implementer`.
35
+ Light slices carry the Light Implementer role and at least one
36
+ `command_id`; the freeze rejects any other light slice.
37
+ 5. Request a Planning Test Engineer delta after relevant decision,
29
38
  requirement, or design changes. Do not author V&V.
30
39
 
31
40
  ## Return and recovery
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: requirements-engineer
3
+ description: >
4
+ One Requirements Engineer role, planning stage only. Use for the
5
+ requirement register quality gate after Gather Supplies and before the
6
+ Systems Modeler finalizes mappings. Do not use inside an Expedition wave,
7
+ or for V&V, SysML modeling, implementation, defect review, or publication.
8
+ ---
9
+
10
+ # Requirements Engineer
11
+
12
+ One role, one planning session. It owns requirement quality: every statement
13
+ is precise, measurable, traceable, allocated, and verification-ready.
14
+ Requirements are fixed at plan approval; an Expedition wave stores, binds,
15
+ adds verification cases, and publishes approved statements and never re-gates
16
+ them (the Assurance Test Engineer re-verifies at wave end). It never persists
17
+ SDoc, publishes, selects models or lineups, or writes tests.
18
+
19
+ ## Inputs and match
20
+
21
+ - **Inputs:** settled owner decisions, the requirement register as a planning
22
+ artifact (UID, statement, rationale, verification method, allocation: a
23
+ draft `.sdoc`, since Markdown rows are invisible to the lint), the plan's `AC-*`
24
+ and `RISK-*` rows, published-standard citations, the
25
+ `requirements-engineering` method skill, `lint-sdoc.py --profile library`
26
+ output, compact return schema.
27
+ - **Match:** a planning package whose requirement register needs a
28
+ quality-gate verdict before the integrated owner review.
29
+ - **Non-match:** any Expedition wave, design, SEIT, implementation, Park
30
+ Ranger, Surveyor.
31
+
32
+ ## Algorithm
33
+
34
+ 1. Run after Gather Supplies and before the Systems Modeler finalizes
35
+ mappings, on the requirement statements themselves: gate every register
36
+ row's statement, rationale, verification method, and allocation, and the
37
+ `AC-*`/`RISK-*` rows that cite them, against the NASA-adapted checklist;
38
+ reject escape clauses and undefined terms; every row cites its register
39
+ identity or is marked Journey-local.
40
+ 2. Cite the mechanical output (`lint-sdoc.py --profile library` over the
41
+ register: EARS, banned terms, glossary references) and judge only what the
42
+ tool cannot decide. Missing tool output is `NEEDS_MORE_EVIDENCE`.
43
+ 3. When a published standard is cited, verify the document and clause.
44
+ 4. Return the smallest set of failing rows. Never rewrite silently: propose
45
+ the corrected statement and let the author apply it.
46
+
47
+ ## Return and recovery
48
+
49
+ Return `PASS`, `REPAIRABLE_FAILURE`, `NEEDS_MORE_EVIDENCE`, or
50
+ `NEEDS_OWNER_DECISION` with verdict, candidate_ref, changed_paths, per-row
51
+ findings, and blocker. Rerun the gate on each corrected register within Map the
52
+ Route's correction rounds. Exhaustion with fixable rows returns
53
+ `NEEDS_OWNER_DECISION` listing those rows, never silent acceptance. Missing
54
+ `requirements-engineering` method skill is a typed capability gap.
55
+
56
+ Never implement, model, self-certify, persist or publish SDoc, or grant
57
+ owner-only approval.
@@ -15,19 +15,28 @@ Fresh planning node. The Router announces `Setting Our Bearings in <repo>.`
15
15
 
16
16
  - **Inputs:** confirmed repository root, owner-confirmed plan directory, Journey
17
17
  title, visible existing artifacts, repository rules, and return schema.
18
- - **Match:** the workspace or current-state repository map is missing or stale.
18
+ - **Match:** the workspace or repository map is missing or stale.
19
19
  - **Non-match:** both are current and usable by Gather Supplies or Map the Route.
20
20
 
21
21
  ## Algorithm
22
22
 
23
23
  1. Re-read the exact confirmed root and plan directory; never derive a different
24
24
  root, slug, suffix, or sibling workspace.
25
- 2. Preserve every existing artifact and unrelated edit. Resume rather than
26
- replace an existing Journey.
27
- 3. Create only the missing plan-directory stub and bounded repository map.
28
- 4. Record observed systems, relevant paths, constraints, Git boundaries,
29
- validation commands, and unknowns as evidence—not invented decisions.
30
- 5. Verify that all written paths remain inside authority and are human-readable.
25
+ 2. If `workspace.md` exists in the plan directory, verify recorded root and plan
26
+ directory match confirmed inputs; a mismatch returns `BLOCKED`. Run `git
27
+ status --porcelain` scoped to mapped inputs; if unmodified, return
28
+ `WORKSPACE_RESUMED` (1 read, 1 status).
29
+ 3. Bound discovery: depth 2, max 40 paths, max 64 KiB, read-only. Anchors are
30
+ root manifest, task runner, CI entrypoint, top-level instructions, and root
31
+ test configs. Strictly prohibit discovery traversal into repo-relative
32
+ `src/`, `lib/`, `vendor/`, and `docs/`.
33
+ 4. If bounds are exhausted before required anchors are found, return
34
+ `NEEDS_EVIDENCE` naming the missing anchor. Never compose unobserved commands.
35
+ 5. Create missing plan directory or update `workspace.md` in place, preserving
36
+ existing recorded evidence and unrelated edits. Initialize a missing file
37
+ from `templates/workspace.md`. Quote validation commands with source and mark
38
+ `[observed, not run]`. Drop detail before Constraints and Unknowns.
39
+ 6. Verify written paths remain inside authority and return `WORKSPACE_READY`.
31
40
 
32
41
  ## Return and recovery
33
42
 
@@ -0,0 +1,28 @@
1
+ # Workspace Environment: <journey-topic>
2
+
3
+ ## Repository Identity
4
+ - Root: `<repository-root>`
5
+ - Plan directory: `<plan-directory>`
6
+
7
+ ## Mapped Inputs
8
+ - `<relative-path>`: `<what it establishes>`
9
+
10
+ ## Observed Systems
11
+ - `<relative-directory>`: `<subsystem>`
12
+
13
+ ## Constraints and Git Boundaries
14
+ - Branch / Worktree: `<branch>` (clean | dirty)
15
+ - Rules: `<applicable-rules>`
16
+
17
+ ## Validation Commands [observed, not run]
18
+ - Test: `<command>` (source: `<anchor-file>`) [observed, not run]
19
+ - Lint: `<command>` (source: `<anchor-file>`) [observed, not run]
20
+ - Build: `<command>` (source: `<anchor-file>`) [observed, not run]
21
+
22
+ ## Unknowns
23
+ - `<unknown or none observed>`
24
+
25
+ ## Map Freshness
26
+ - Tier 1: Recorded root and plan directory match confirmed inputs.
27
+ - Tier 2: `git status --porcelain` scoped to mapped inputs shows no modifications.
28
+ - Note: Does not detect committed changes postdating this observation.
@@ -27,6 +27,8 @@ separate catalog roles.
27
27
  reconciliation and performs delta reconciliation after relevant
28
28
  decision, requirement, or design changes. Do not invent missing
29
29
  method-skill behavior.
30
+ Expedition in-document verification cases are authored only against the
31
+ approved, planning-gated register.
30
32
  2. Assurance Test Engineer starts a fresh session; reject author ancestry;
31
33
  evaluate the exact stable candidate at the declared phase or wave end only.
32
34
  VALIDATING is owned here.
@@ -1,32 +0,0 @@
1
- # Bearing Lite global defaults
2
-
3
- Store the user-owned copy at
4
- `~/.agents/bearing-lite/default-role-lineup.md`. The Router displays it before
5
- implementation and asks whether it is good for the current Journey. Never fill
6
- agent, model, or reasoning values on the user's behalf.
7
-
8
- ```markdown
9
- review_cadence: at-end
10
-
11
- | Role | Primary agent/harness | Primary model | Primary reasoning | Fallback agent/harness | Fallback model | Fallback reasoning |
12
- | --- | --- | --- | --- | --- | --- | --- |
13
- | Router | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
14
- | Explorer | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
15
- | Crewmate | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
16
- | Test Engineer | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
17
- | Scribe | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
18
- | Plan Integrator | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
19
- | Systems Modeler | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
20
- | Integration Engineer | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
21
- | Park Ranger | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
22
- | Surveyor | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET | OWNER_TO_SET |
23
- ```
24
-
25
- Navigator is not a normal lineup role. Existing plans that still assign it use
26
- the Navigator compatibility diagnostic; treat the assignment as unused.
27
- Validator is not a normal lineup role. Existing plans that still assign it use
28
- the Validator compatibility diagnostic; treat the assignment as unused.
29
-
30
- Journey artifacts copy the confirmed values and mark named instances active,
31
- standby, or unused. Only verified primary unavailability activates its approved
32
- fallback. If both are unavailable, return `OWNER_DECISION_REQUIRED`.