@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 +7 -1
- package/hooks/assurance-budget.cjs +1 -19
- package/hooks/closeout.cjs +16 -0
- package/hooks/plan-package.cjs +177 -0
- package/hooks/planning-review.cjs +9 -14
- package/hooks/policy.cjs +45 -0
- package/hooks/reconcile.cjs +145 -0
- package/hooks/te-host.cjs +25 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/schemas/event.schema.json +59 -0
- package/schemas/implementation.schema.json +41 -18
- package/schemas/journey.schema.json +15 -2
- package/skills/bearing-lite/SKILL.md +11 -12
- package/skills/bearing-lite/references/lineups.md +26 -0
- package/skills/bearing-lite/templates/task.md +4 -1
- package/skills/light-implementer/SKILL.md +53 -0
- package/skills/map-the-route/SKILL.md +3 -2
- package/skills/map-the-route/references/artifact-grammar.md +23 -6
- package/skills/plan-integrator/SKILL.md +11 -2
- package/skills/requirements-engineer/SKILL.md +57 -0
- package/skills/set-bearings/SKILL.md +16 -7
- package/skills/set-bearings/templates/workspace.md +28 -0
- package/skills/test-engineer/SKILL.md +2 -0
- package/skills/bearing-lite/templates/default-role-lineup.md +0 -32
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/
|
|
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 =
|
|
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";
|
package/hooks/closeout.cjs
CHANGED
|
@@ -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 =
|
|
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);
|
package/hooks/policy.cjs
ADDED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
{
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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": {
|
|
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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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`.
|