@alphazede/bearing-lite 0.2.1 → 0.2.2

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
@@ -151,6 +151,16 @@ explain orientation; they never authorize a transition.
151
151
 
152
152
  ## Roles and authority
153
153
 
154
+ Owner questions follow [the owner-stop policy](skills/bearing-lite/references/owner-stops.md).
155
+ The integrated approval records bounded continuation with exclusions and expiry.
156
+ The Router applies approved policy and fallbacks, batches nonblocking questions,
157
+ and continues ready independent work while dependent work waits. Approval shows
158
+ the first-package summary or subsequent changes with the complete package available.
159
+ Local receipt metrics distinguish questions, round trips, response intervals and
160
+ fully blocked time; no telemetry leaves the checkout. The transition preflight
161
+ and scheduling remain procedural, including on skills-only hosts. A legacy
162
+ Journey without explicit timing coverage reports unavailable, not zero wait.
163
+
154
164
  | Role | What it is | Executes | Notes |
155
165
  |---|---|---|---|
156
166
  | **Router** | Stateful planning controller | no | User-facing; planning-state writer; Expedition sequencing |
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
+ const { ownerWaitMetrics } = require("./owner-stops.cjs");
5
+
4
6
  /**
5
7
  * Bearing Lite return/closeout adapter (CONTRACT-HOOK-01).
6
8
  * Advisory for ordinary handoff reminders; may BLOCK only protected completion.
@@ -154,6 +156,21 @@ function evaluate(input) {
154
156
  return result("ADVISE", "channel_open", RECOVERY_CHANNEL);
155
157
  }
156
158
 
159
+ // Explicit invocation by Router; native Stop mappings do not supply Journey JSON.
160
+ if (input.action_kind === "owner_wait_summary") {
161
+ if (input.mode === "protected_completion" || input.protected_completion === true) {
162
+ return unavailable("owner_wait_summary_is_not_completion");
163
+ }
164
+ try {
165
+ const metrics = ownerWaitMetrics(input.journey, input.as_of);
166
+ return { ...result("ADVISE", "owner_wait:" + metrics.status,
167
+ "Render metrics and coverage in the wave or closeout receipt; no authority is granted"),
168
+ owner_wait: metrics };
169
+ } catch {
170
+ return unavailable("owner_wait_records_invalid");
171
+ }
172
+ }
173
+
157
174
  const mode =
158
175
  input.mode === "protected_completion" || input.protected_completion === true
159
176
  ? "protected_completion"
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ // Procedural owner-stop preflight and local receipt metrics; never grants authority.
4
+ const fs = require("node:fs");
5
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
6
+ const text = (v) => typeof v === "string" && v.trim().length > 0;
7
+ const classes = ["A", "B", "C", "D", "E", "F"];
8
+ const exclusions = ["scope_change", "budget_exhaustion", "owner_hold", "owner_only_actions"];
9
+
10
+ function timestamp(value) {
11
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)) {
12
+ throw new Error("invalid_utc_timestamp");
13
+ }
14
+ const n = Date.parse(value);
15
+ if (!Number.isFinite(n) || new Date(n).toISOString() !== (value.includes(".") ? value : value.replace("Z", ".000Z"))) {
16
+ throw new Error("invalid_utc_timestamp");
17
+ }
18
+ return n;
19
+ }
20
+
21
+ function evaluateOwnerStop(input) {
22
+ const unknown = (reason) => ({ disposition: "NEEDS_MORE_EVIDENCE", reason });
23
+ if (!object(input) || !classes.includes(input.class) || typeof input.blocking !== "boolean" || !text(input.evidence_ref)) {
24
+ return unknown("owner_stop_input_missing");
25
+ }
26
+ if (["C", "E", "F"].includes(input.class)) {
27
+ return { disposition: input.blocking ? "ASK" : "QUEUE", reason: "owner_boundary" };
28
+ }
29
+ const a = input.authority;
30
+ const grant = a?.continuation;
31
+ if (!object(a) || a.effective !== true || a.state !== "active" || !text(a.id) ||
32
+ !object(a.approval_receipt) || Object.keys(a.approval_receipt).length === 0 ||
33
+ !object(grant) || grant.granted !== true || !text(grant.owner_decision_id) ||
34
+ !Array.isArray(a.granting_owner_decisions) || !a.granting_owner_decisions.includes(grant.owner_decision_id) ||
35
+ !Array.isArray(a.expiry_conditions) || !a.expiry_conditions.length || !a.expiry_conditions.every(text) ||
36
+ !Array.isArray(grant.exclusions) || !exclusions.every((item) => grant.exclusions.includes(item))) {
37
+ return unknown("continuation_grant_missing");
38
+ }
39
+ try {
40
+ const now = timestamp(input.checked_at);
41
+ if (grant.expires_at !== null && timestamp(grant.expires_at) <= now) return unknown("continuation_expired");
42
+ } catch {
43
+ return unknown("continuation_expiry_unverified");
44
+ }
45
+ const checks = ["scope", "budget", "exclusions", "expiry", "owner_hold"];
46
+ if (input.class === "A") checks.push("policy");
47
+ if (input.class === "B") checks.push("fallback");
48
+ if (!object(input.checks) || !checks.every((key) => input.checks[key] === true) || !text(input.resolution_ref)) {
49
+ return unknown("continuation_conditions_unverified");
50
+ }
51
+ return { disposition: "CONTINUE", reason: "approved_resolution", authority_id: a.id };
52
+ }
53
+
54
+ function unionLength(intervals) {
55
+ let total = 0;
56
+ let end = -Infinity;
57
+ for (const [start, stop] of intervals.sort((a, b) => a[0] - b[0])) {
58
+ total += Math.max(0, stop - Math.max(start, end));
59
+ end = Math.max(end, stop);
60
+ }
61
+ return total;
62
+ }
63
+
64
+ function ownerWaitMetrics(journey, asOf) {
65
+ if (!object(journey)) throw new Error("journey_missing");
66
+ if (journey.owner_wait_tracking === undefined || journey.owner_wait_tracking === false) {
67
+ return { status: "unavailable", reason: "owner_wait_not_tracked" };
68
+ }
69
+ if (journey.owner_wait_tracking !== true || !Array.isArray(journey.decisions) ||
70
+ !Array.isArray(journey.open_decisions) || !Array.isArray(journey.owner_blocked_intervals)) {
71
+ throw new Error("owner_wait_records_missing");
72
+ }
73
+ const now = timestamp(asOf);
74
+ const counts = Object.fromEntries(classes.map((key) => [key, 0]));
75
+ const metrics = { status: "measured", as_of: asOf, decisions_asked: 0, round_trips: 0,
76
+ queued: 0, pending: 0, cancelled: 0, by_class: counts,
77
+ response_ms: 0, pending_response_ms: 0, response_window_ms: 0, blocked_ms: 0 };
78
+ const ids = new Set();
79
+ const rounds = new Map();
80
+ const responses = [];
81
+ const windows = new Map();
82
+ for (const q of [...journey.decisions, ...journey.open_decisions]) {
83
+ if (!object(q) || q.record_type !== "owner_stop") continue;
84
+ if (!text(q.id) || ids.has(q.id)) throw new Error("duplicate_or_missing_decision_id");
85
+ ids.add(q.id);
86
+ if (!classes.includes(q.class) || !["queued", "asked", "answered", "cancelled"].includes(q.status) ||
87
+ !text(q.question) || !text(q.evidence_ref) || !text(q.why_owner) ||
88
+ typeof q.blocking !== "boolean" || !Array.isArray(q.affected_slices) || !q.affected_slices.every(text) ||
89
+ !["asked_at", "answered_at", "cancelled_at", "round_trip_id"].every((key) => Object.hasOwn(q, key))) {
90
+ throw new Error("invalid_owner_stop_record");
91
+ }
92
+ const asked = q.asked_at === null ? null : timestamp(q.asked_at);
93
+ const answered = q.answered_at === null ? null : timestamp(q.answered_at);
94
+ const cancelled = q.cancelled_at === null ? null : timestamp(q.cancelled_at);
95
+ const created = timestamp(q.created_at);
96
+ if (created > now || (asked !== null && (asked < created || asked > now))) throw new Error("invalid_question_time");
97
+ if (q.status === "queued" && (asked !== null || answered !== null || cancelled !== null)) throw new Error("invalid_queued_question");
98
+ if (q.status === "asked" && (asked === null || answered !== null || cancelled !== null)) throw new Error("invalid_pending_question");
99
+ if (q.status === "answered" && (asked === null || answered === null || cancelled !== null || !text(q.answer_ref))) throw new Error("invalid_answered_question");
100
+ if (q.status === "cancelled" && (cancelled === null || answered !== null)) throw new Error("invalid_cancelled_question");
101
+ const end = answered ?? cancelled ?? now;
102
+ if (end < (asked ?? created) || end > now) throw new Error("invalid_response_time");
103
+ if (q.status === "queued") metrics.queued++;
104
+ if (q.status === "asked") metrics.pending++;
105
+ if (q.status === "cancelled") metrics.cancelled++;
106
+ if (asked === null) {
107
+ if (q.round_trip_id !== null) throw new Error("unasked_round_trip");
108
+ continue;
109
+ }
110
+ if (!text(q.round_trip_id)) throw new Error("round_trip_missing");
111
+ if (rounds.has(q.round_trip_id) && rounds.get(q.round_trip_id) !== asked) {
112
+ throw new Error("inconsistent_round_trip_time");
113
+ }
114
+ rounds.set(q.round_trip_id, asked);
115
+ metrics.decisions_asked++;
116
+ counts[q.class]++;
117
+ responses.push([asked, end]);
118
+ windows.set(q.id, [asked, end]);
119
+ if (answered !== null) metrics.response_ms += end - asked;
120
+ if (q.status === "asked") metrics.pending_response_ms += now - asked;
121
+ }
122
+ const blocked = [];
123
+ for (const interval of journey.owner_blocked_intervals) {
124
+ if (!object(interval) || !Array.isArray(interval.decision_ids) || !interval.decision_ids.length ||
125
+ !interval.decision_ids.every(text) || new Set(interval.decision_ids).size !== interval.decision_ids.length ||
126
+ !Object.hasOwn(interval, "ended_at")) throw new Error("invalid_blocked_interval");
127
+ const start = timestamp(interval.started_at);
128
+ const end = interval.ended_at === null ? now : timestamp(interval.ended_at);
129
+ if (start > end || end > now) throw new Error("invalid_blocked_time");
130
+ for (const id of interval.decision_ids) {
131
+ const window = windows.get(id);
132
+ if (!window || start < window[0] || end > window[1]) throw new Error("blocked_interval_outside_question");
133
+ }
134
+ blocked.push([start, end]);
135
+ }
136
+ metrics.round_trips = rounds.size;
137
+ metrics.response_window_ms = unionLength(responses);
138
+ metrics.blocked_ms = unionLength(blocked);
139
+ return metrics;
140
+ }
141
+
142
+ module.exports = { evaluateOwnerStop, ownerWaitMetrics };
143
+
144
+ if (require.main === module) {
145
+ try {
146
+ const [file, asOf] = process.argv.slice(2);
147
+ if (!file || !asOf) throw new Error("usage: owner-stops.cjs <journey.json> <as-of-UTC>");
148
+ console.log(JSON.stringify(ownerWaitMetrics(JSON.parse(fs.readFileSync(file, "utf8")), asOf)));
149
+ } catch (error) {
150
+ console.log(JSON.stringify({ status: "invalid", reason: error.code ? "journey_unreadable" :
151
+ /^[a-z_]+$/.test(error.message) ? error.message : "invalid_journey_input" }));
152
+ process.exitCode = 1;
153
+ }
154
+ }
@@ -12,6 +12,7 @@ const OUTCOMES = Object.freeze(["ADVISE", "REROUTE", "BLOCK", "UNAVAILABLE"]);
12
12
  const ENFORCEMENT = "procedural";
13
13
  const { evaluatePlanningReview } = require("./planning-review.cjs");
14
14
  const { evaluateAssuranceBudget } = require("./assurance-budget.cjs");
15
+ const { evaluateOwnerStop } = require("./owner-stops.cjs");
15
16
 
16
17
  const RECOVERY_UNAVAILABLE =
17
18
  "Keep the current state, run the transition checklist procedurally, and record the result before retrying";
@@ -132,6 +133,17 @@ function evaluate(input) {
132
133
  return result("ADVISE", "channel_open", RECOVERY_CHANNEL);
133
134
  }
134
135
 
136
+ if (input.action_kind === "owner_stop_check") {
137
+ const stop = evaluateOwnerStop(input.owner_stop);
138
+ return {
139
+ ...result(stop.disposition === "NEEDS_MORE_EVIDENCE" ? "UNAVAILABLE" :
140
+ stop.disposition === "ASK" ? "REROUTE" : "ADVISE",
141
+ "owner_stop:" + stop.disposition + ":" + stop.reason,
142
+ "Apply references/owner-stops.md; this check grants no action or dispatch authority"),
143
+ owner_stop: stop,
144
+ };
145
+ }
146
+
135
147
  if (input.action_kind === "planning_review_transition") {
136
148
  const verdict = evaluatePlanningReview(input.planning_review);
137
149
  if (verdict.outcome === "PASS") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphazede/bearing-lite",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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.1",
4
+ "version": "0.2.2",
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",
@@ -23,28 +23,22 @@
23
23
  "approval_receipt"
24
24
  ],
25
25
  "properties": {
26
- "schema_version": { "type": "string", "minLength": 1 },
27
- "schema": { "type": "string", "minLength": 1, "description": "Envelope schema identity" },
28
- "id": { "type": "string", "minLength": 1, "description": "Authority envelope ID" },
29
- "state": { "type": "string", "minLength": 1 },
30
- "subject": { "type": "object", "minProperties": 1, "additionalProperties": true },
31
- "repositories": {
32
- "type": "array",
33
- "items": { "type": "object", "additionalProperties": true }
34
- },
35
- "baseline": { "type": "object", "minProperties": 1, "additionalProperties": true },
36
- "granting_owner_decisions": {
37
- "type": "array",
38
- "items": { "type": "string", "minLength": 1 }
39
- },
26
+ "schema_version": {"type": "string", "minLength": 1},
27
+ "schema": {"type": "string", "minLength": 1, "description": "Envelope schema identity"},
28
+ "id": {"type": "string", "minLength": 1, "description": "Authority envelope ID"},
29
+ "state": {"type": "string", "minLength": 1},
30
+ "subject": {"type": "object", "minProperties": 1, "additionalProperties": true},
31
+ "repositories": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
32
+ "baseline": {"type": "object", "minProperties": 1, "additionalProperties": true},
33
+ "granting_owner_decisions": {"type": "array", "items": {"type": "string", "minLength": 1}},
40
34
  "allowed": {
41
35
  "type": "object",
42
36
  "additionalProperties": true,
43
37
  "required": ["scope", "actions", "paths"],
44
38
  "properties": {
45
- "scope": { "type": "array", "items": { "type": "string" } },
46
- "actions": { "type": "array", "items": { "type": "string" } },
47
- "paths": { "type": "array", "items": { "type": "string" } }
39
+ "scope": {"type": "array", "items": {"type": "string"}},
40
+ "actions": {"type": "array", "items": {"type": "string"}},
41
+ "paths": {"type": "array", "items": {"type": "string"}}
48
42
  }
49
43
  },
50
44
  "prohibited": {
@@ -52,24 +46,51 @@
52
46
  "additionalProperties": true,
53
47
  "required": ["scope", "actions", "paths"],
54
48
  "properties": {
55
- "scope": { "type": "array", "items": { "type": "string" } },
56
- "actions": { "type": "array", "items": { "type": "string" } },
57
- "paths": { "type": "array", "items": { "type": "string" } }
49
+ "scope": {"type": "array", "items": {"type": "string"}},
50
+ "actions": {"type": "array", "items": {"type": "string"}},
51
+ "paths": {"type": "array", "items": {"type": "string"}}
58
52
  }
59
53
  },
60
- "role_grants": {
61
- "type": "array",
62
- "items": { "type": "object", "additionalProperties": true }
63
- },
64
- "effective": { "type": "boolean", "description": "Machine-enforced effectiveness. Boolean only: a string is not an effectiveness decision." },
65
- "expiry_conditions": {
66
- "type": "array",
67
- "items": { "type": "string", "minLength": 1 }
68
- },
69
- "supersedes": {
70
- "type": "array",
71
- "items": { "type": "string" }
54
+ "role_grants": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
55
+ "effective": {
56
+ "type": "boolean",
57
+ "description": "Machine-enforced effectiveness. Boolean only: a string is not an effectiveness decision."
72
58
  },
73
- "approval_receipt": { "type": "object", "additionalProperties": true }
74
- }
59
+ "expiry_conditions": {"type": "array", "items": {"type": "string", "minLength": 1}},
60
+ "supersedes": {"type": "array", "items": {"type": "string"}},
61
+ "approval_receipt": {"type": "object", "additionalProperties": true},
62
+ "continuation": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "required": ["granted", "owner_decision_id", "exclusions", "expires_at"],
66
+ "properties": {
67
+ "granted": {"type": "boolean"},
68
+ "owner_decision_id": {"type": "string", "minLength": 1},
69
+ "exclusions": {
70
+ "type": "array",
71
+ "uniqueItems": true,
72
+ "items": {"type": "string", "minLength": 1},
73
+ "allOf": [
74
+ {"contains": {"const": "scope_change"}},
75
+ {"contains": {"const": "budget_exhaustion"}},
76
+ {"contains": {"const": "owner_hold"}},
77
+ {"contains": {"const": "owner_only_actions"}}
78
+ ]
79
+ },
80
+ "expires_at": {"type": ["string", "null"], "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"}
81
+ }
82
+ }
83
+ },
84
+ "allOf": [
85
+ {
86
+ "if": {"required": ["continuation"]},
87
+ "then": {
88
+ "properties": {
89
+ "expiry_conditions": {"minItems": 1},
90
+ "granting_owner_decisions": {"minItems": 1},
91
+ "approval_receipt": {"minProperties": 1}
92
+ }
93
+ }
94
+ }
95
+ ]
75
96
  }
@@ -5,29 +5,18 @@
5
5
  "description": "Append-only decisions and authority-event history plus the generation-bound checkout lease. The full mutable file digest is not the stable SEIT execution baseline. Extra lifecycle metadata is allowed at the document root. Lease identity fields stay closed.",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
- "required": [
9
- "schema_version",
10
- "journey",
11
- "checkout_lease",
12
- "decisions",
13
- "open_decisions",
14
- "planning_receipts",
15
- "lineup_selection"
16
- ],
8
+ "required": ["schema_version", "journey", "checkout_lease", "decisions", "open_decisions", "planning_receipts", "lineup_selection"],
17
9
  "properties": {
18
- "schema_version": { "type": "string", "minLength": 1 },
10
+ "schema_version": {"type": "string", "minLength": 1},
19
11
  "journey": {
20
12
  "type": "object",
21
13
  "additionalProperties": true,
22
14
  "required": ["id", "title", "status", "planning_repository"],
23
15
  "properties": {
24
- "id": { "type": "string", "minLength": 1 },
25
- "title": { "type": "string", "minLength": 1 },
26
- "status": {
27
- "type": "string",
28
- "enum": ["planning", "implementation", "complete", "cancelled"]
29
- },
30
- "planning_repository": { "type": "string", "minLength": 1 }
16
+ "id": {"type": "string", "minLength": 1},
17
+ "title": {"type": "string", "minLength": 1},
18
+ "status": {"type": "string", "enum": ["planning", "implementation", "complete", "cancelled"]},
19
+ "planning_repository": {"type": "string", "minLength": 1}
31
20
  }
32
21
  },
33
22
  "checkout_lease": {
@@ -45,57 +34,162 @@
45
34
  "state"
46
35
  ],
47
36
  "properties": {
48
- "journey": { "type": "string", "minLength": 1 },
49
- "controller": { "type": "string", "minLength": 1 },
50
- "repository": { "type": "string", "minLength": 1 },
51
- "checkout": { "type": "string", "minLength": 1 },
52
- "branch": { "type": "string", "minLength": 1 },
53
- "candidate_revision": { "type": "string", "minLength": 1 },
54
- "acquired_at": { "type": "string", "minLength": 1 },
55
- "generation": { "type": "integer", "minimum": 1 },
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
- }
37
+ "journey": {"type": "string", "minLength": 1},
38
+ "controller": {"type": "string", "minLength": 1},
39
+ "repository": {"type": "string", "minLength": 1},
40
+ "checkout": {"type": "string", "minLength": 1},
41
+ "branch": {"type": "string", "minLength": 1},
42
+ "candidate_revision": {"type": "string", "minLength": 1},
43
+ "acquired_at": {"type": "string", "minLength": 1},
44
+ "generation": {"type": "integer", "minimum": 1},
45
+ "state": {"enum": ["active", "released"]},
46
+ "released_at": {"type": "string", "format": "date-time"},
47
+ "release_reason": {"type": "string", "minLength": 1}
64
48
  },
65
- "then": {
66
- "required": ["released_at", "release_reason"]
67
- }
49
+ "if": {"properties": {"state": {"const": "released"}}},
50
+ "then": {"required": ["released_at", "release_reason"]}
68
51
  },
69
52
  "history": {
70
53
  "type": "object",
71
54
  "additionalProperties": true,
72
55
  "description": "Optional append-only Journey history wrapper",
73
56
  "properties": {
74
- "decisions": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
75
- "authority_events": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
57
+ "decisions": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
58
+ "authority_events": {"type": "array", "items": {"type": "object", "additionalProperties": true}}
76
59
  }
77
60
  },
78
61
  "decisions": {
79
62
  "type": "array",
80
63
  "description": "Append-only confirmed owner decisions",
81
- "items": { "type": "object", "additionalProperties": true }
64
+ "items": {
65
+ "type": "object",
66
+ "additionalProperties": true,
67
+ "allOf": [
68
+ {
69
+ "if": {"required": ["record_type"], "properties": {"record_type": {"const": "owner_stop"}}},
70
+ "then": {"$ref": "#/$defs/ownerStop"}
71
+ }
72
+ ]
73
+ }
82
74
  },
83
75
  "authority_events": {
84
76
  "type": "array",
85
77
  "description": "Append-only authority grant, amend, revoke, or supersede events",
86
- "items": { "type": "object", "additionalProperties": true }
78
+ "items": {"type": "object", "additionalProperties": true}
87
79
  },
88
80
  "open_decisions": {
89
81
  "type": "array",
90
- "items": { "type": "object", "additionalProperties": true }
82
+ "items": {
83
+ "type": "object",
84
+ "additionalProperties": true,
85
+ "allOf": [
86
+ {
87
+ "if": {"required": ["record_type"], "properties": {"record_type": {"const": "owner_stop"}}},
88
+ "then": {"$ref": "#/$defs/ownerStop"}
89
+ }
90
+ ]
91
+ }
91
92
  },
92
- "planning_receipts": {
93
+ "planning_receipts": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
94
+ "lineup_selection": {"type": "object", "additionalProperties": true},
95
+ "owner_wait_tracking": {"type": "boolean", "description": "Explicit coverage marker; absence is unavailable, never zero owner wait."},
96
+ "owner_blocked_intervals": {
93
97
  "type": "array",
94
- "items": { "type": "object", "additionalProperties": true }
95
- },
96
- "lineup_selection": {
98
+ "items": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": ["decision_ids", "started_at", "ended_at"],
102
+ "properties": {
103
+ "decision_ids": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
104
+ "started_at": {"type": "string", "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"},
105
+ "ended_at": {"type": ["string", "null"], "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"}
106
+ }
107
+ }
108
+ }
109
+ },
110
+ "$defs": {
111
+ "ownerStop": {
97
112
  "type": "object",
98
- "additionalProperties": true
113
+ "additionalProperties": true,
114
+ "required": [
115
+ "record_type",
116
+ "id",
117
+ "class",
118
+ "question",
119
+ "why_owner",
120
+ "evidence_ref",
121
+ "affected_slices",
122
+ "blocking",
123
+ "status",
124
+ "created_at",
125
+ "asked_at",
126
+ "answered_at",
127
+ "cancelled_at",
128
+ "round_trip_id"
129
+ ],
130
+ "properties": {
131
+ "record_type": {"const": "owner_stop"},
132
+ "id": {"type": "string", "minLength": 1},
133
+ "class": {"enum": ["A", "B", "C", "D", "E", "F"]},
134
+ "question": {"type": "string", "minLength": 1},
135
+ "why_owner": {"type": "string", "minLength": 1},
136
+ "evidence_ref": {"type": "string", "minLength": 1},
137
+ "affected_slices": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
138
+ "blocking": {"type": "boolean"},
139
+ "status": {"enum": ["queued", "asked", "answered", "cancelled"]},
140
+ "created_at": {"type": "string", "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"},
141
+ "asked_at": {"type": ["string", "null"], "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"},
142
+ "answered_at": {"type": ["string", "null"], "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"},
143
+ "cancelled_at": {"type": ["string", "null"], "format": "date-time", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$"},
144
+ "round_trip_id": {"type": ["string", "null"], "minLength": 1},
145
+ "answer_ref": {"type": "string", "minLength": 1}
146
+ },
147
+ "allOf": [
148
+ {
149
+ "if": {"properties": {"status": {"const": "queued"}}},
150
+ "then": {
151
+ "properties": {
152
+ "asked_at": {"type": "null"},
153
+ "answered_at": {"type": "null"},
154
+ "cancelled_at": {"type": "null"},
155
+ "round_trip_id": {"type": "null"}
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "if": {"properties": {"status": {"const": "asked"}}},
161
+ "then": {
162
+ "properties": {
163
+ "asked_at": {"type": "string"},
164
+ "round_trip_id": {"type": "string", "minLength": 1},
165
+ "answered_at": {"type": "null"},
166
+ "cancelled_at": {"type": "null"}
167
+ }
168
+ }
169
+ },
170
+ {
171
+ "if": {"properties": {"status": {"const": "answered"}}},
172
+ "then": {
173
+ "properties": {
174
+ "asked_at": {"type": "string"},
175
+ "answered_at": {"type": "string"},
176
+ "cancelled_at": {"type": "null"},
177
+ "round_trip_id": {"type": "string", "minLength": 1}
178
+ },
179
+ "required": ["answer_ref"]
180
+ }
181
+ },
182
+ {
183
+ "if": {"properties": {"status": {"const": "cancelled"}}},
184
+ "then": {"properties": {"cancelled_at": {"type": "string"}, "answered_at": {"type": "null"}}}
185
+ }
186
+ ]
187
+ }
188
+ },
189
+ "allOf": [
190
+ {
191
+ "if": {"required": ["owner_wait_tracking"], "properties": {"owner_wait_tracking": {"const": true}}},
192
+ "then": {"required": ["owner_blocked_intervals"]}
99
193
  }
100
- }
194
+ ]
101
195
  }
@@ -3,7 +3,7 @@ name: bearing-lite
3
3
  description: Bearing Lite Router for Journeys. Not for ordinary work, assigned packets, implementation, or publication.
4
4
  ---
5
5
 
6
- The Router alone writes Journey planning state, owns owner conversation, and owns Expedition sequencing;
6
+ Router alone writes Journey planning state, owns owner conversation, and owns Expedition sequencing;
7
7
  planning nodes return owner questions. Plugin hosts are partial; skill-copy is skills-only.
8
8
 
9
9
  1. Say `Preparing this Journey.` Acquire or resume a generation-bound checkout lease before
@@ -19,14 +19,14 @@ planning nodes return owner questions. Plugin hosts are partial; skill-copy is s
19
19
  4. Run Repository Fit → Set Bearings → Gather Supplies; unresolved material intent blocks Map the Route.
20
20
  5. Invoke Map the Route after settled intent. Do not ask for lineup or route
21
21
  before it; carry owner-supplied lineup and `review_cadence: at-end` as proposals.
22
- 6. Enforce `references/review-policy.md`. Show one integrated
22
+ 6. Enforce `references/review-policy.md` and `references/owner-stops.md`. Show one integrated
23
23
  approval-or-change gate for outcome, design, route, lineup, role states,
24
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
27
  7. Crewmate and Explorer may continue in-wave. `work_class: light` slices go to the
28
28
  Light Implementer; a `reclassify: judgement` return re-dispatches to the Crewmate.
29
- Use the visible wave receipt and update implementation and review once per wave.
29
+ Use visible wave receipts and update implementation and review once per wave.
30
30
 
31
31
  Return `READY`, `WAITING_ON`, `OWNER_DECISION_REQUIRED`, or `COMPLETE`.
32
32
  `max_assurance_rounds` is 1 per declared phase or wave-end, not per Journey;
@@ -41,6 +41,11 @@ never a deviation.
41
41
 
42
42
  Only verified primary unavailability activates its approved fallback. If
43
43
  both are unavailable, return `OWNER_DECISION_REQUIRED`.
44
+ Activation within the frozen ordered fallbacks is a dated execution receipt,
45
+ not a lineup amendment or a new owner approval. Verify the approved activation
46
+ condition and remaining eligible routes before escalating exhaustion. A new
47
+ identity or changed fallback condition requires an owner amendment; follow
48
+ `owner-stops.md` for classification and batching.
44
49
 
45
50
  This catalog is the single lineup source. A legacy
46
51
  `~/.agents/bearing-lite/default-role-lineup.md` is never read or created;
@@ -0,0 +1,122 @@
1
+ # Owner decisions and continuation
2
+
3
+ One integrated approval covers the approved work and its continuation grant.
4
+ Before asking, identify the unresolved decision, affected slices, evidence,
5
+ and why the current policy or authority does not answer it. Never invent an
6
+ extra role, review gate, approval checkpoint, or scope to resolve uncertainty.
7
+
8
+ | Class | Decision | Router behavior |
9
+ | --- | --- | --- |
10
+ | A | Existing policy | Apply the cited rule and record a dated execution receipt. A real policy conflict is a C boundary decision. |
11
+ | B | Approved fallback | Verify unavailability under the frozen fallback condition; activate the next eligible approved route and record evidence. Changing the lineup or fallback condition is C. |
12
+ | C | Bound, scope, or policy conflict | Ask for the smallest amendment when it blocks ready work. Never reset a spent bound. |
13
+ | D | Continue approved work | Use the effective continuation grant; never ask again while its conditions hold. Missing or expired authority is F. |
14
+ | E | Integrated plan approval | Present the package once for approval or change; execution waits for explicit approval. |
15
+ | F | Authority reserved to the owner | Ask only when the action is not already expressly authorized. Owner holds remain effective until explicitly lifted. |
16
+
17
+ A/B/D continuation requires verified authority, scope, budget, exclusions,
18
+ expiry, and owner-hold checks. Missing evidence is `NEEDS_MORE_EVIDENCE`, not
19
+ permission and not automatically an owner question. Recover discoverable
20
+ evidence first; escalate a real unresolved boundary as C or F. Fallback
21
+ activation is an execution receipt, not an authority amendment. Only the
22
+ owner changes the frozen lineup or grants authority.
23
+
24
+ ## Approval and authority
25
+
26
+ Map the Route includes `authority.json.continuation` in the integrated gate:
27
+ `granted`, `owner_decision_id`, named `exclusions`, and `expires_at` (UTC or
28
+ null for event-based expiry). The surrounding envelope retains allowed and
29
+ prohibited actions/paths, role grants, approval receipt, and nonempty
30
+ `expiry_conditions`. Record explicit approval against the exact package;
31
+ the continuation decision ID must occur in `granting_owner_decisions`.
32
+ No grant is inferred from silence, a lineup selection, or passing checks.
33
+
34
+ Always exclude scope change, budget exhaustion, owner holds, and owner-only
35
+ actions from the continuation grant. A separate explicit action grant can
36
+ authorize an otherwise owner-only action; continuation cannot create it.
37
+ Revocation, supersession, expiry, or changed authority invalidates the grant.
38
+ An older envelope without continuation remains valid historical data but
39
+ provides no standing continuation grant. Do not manufacture a retroactive one.
40
+
41
+ For first approval show outcome, scope/exclusions, role responsibilities,
42
+ route, bounds, risks, open decisions and proof coverage in a concise summary.
43
+ For revisions show changed requirements, design, proof cases, slices,
44
+ lineup, authority and bounds against the last owner-reviewed package, citing
45
+ both revisions/digests. Explain invalidated approvals and unresolved decisions.
46
+ Keep the complete frozen `review.html` accessible in both cases; a diff or
47
+ freeze PASS is neither approval nor proof of semantic completeness. An
48
+ unchanged package does not need reapproval. A changed package returns to the
49
+ same gate, never an extra gate.
50
+
51
+ ## Queue and continue
52
+
53
+ Keep pending questions in the existing `journey.json.open_decisions` array
54
+ as typed `record_type: owner_stop` records. Preserve stable IDs when moving
55
+ answered records to `decisions`; append owner answers to normal history.
56
+ Automatic resolutions and unsolicited owner directions remain normal dated
57
+ receipts, not fabricated questions. Required fields are defined by
58
+ `schemas/journey.schema.json` and checked by the metrics helper.
59
+
60
+ Queue nonblocking questions until wave end and present one batch with the
61
+ wave receipt. Queue nonblocking owner-only closeout actions until Journey
62
+ end. Required credentials, publication prerequisites, or other owner-only
63
+ dependencies surface immediately when they block ready work. Do not defer
64
+ safety/integrity intervention or an explicit owner stop. Each actual
65
+ presentation has a stable `round_trip_id`; questions in the same batch share
66
+ that ID and the exact same presentation timestamp.
67
+ Record `asked_at` when presented, not when drafted or queued; `answered_at`
68
+ when the owner answers. Cancellation uses `cancelled_at`, never a fabricated
69
+ answer. Times are UTC with a `Z` suffix and either whole seconds or exactly
70
+ three fractional digits (milliseconds); normalize host timestamps before
71
+ recording. Re-presenting an unanswered question keeps its ID and
72
+ original asked time; it is not another distinct question.
73
+
74
+ While waiting, walk the approved slice graph. Dispatch only READY slices
75
+ with satisfied dependencies, active authority, valid lease, available route,
76
+ and verified independence from the question and other running slices.
77
+ Disjoint writes alone do not prove independence: check shared runtime,
78
+ resources, integration order, and read/write dependencies. `parallel_safe`
79
+ is evidence to inspect, not permission. Respect host concurrency limits and
80
+ never dispatch completed or already-running work again. Keep dependent work
81
+ pending. Owner holds apply to their stated scope, including the whole Journey
82
+ when so directed; they cannot be bypassed by calling work independent.
83
+
84
+ ## Deterministic checks and measurement
85
+
86
+ Before a proposed stop, run `hooks/transition-order.cjs` with
87
+ `action_kind: owner_stop_check` and `owner_stop` containing `class`,
88
+ `blocking`, `evidence_ref`, `resolution_ref`, `authority`, `checked_at`,
89
+ and `checks`. Checks are explicit booleans: `scope`, `budget`, `exclusions`,
90
+ `expiry`, `owner_hold`, plus `policy` for A or `fallback` for B. Router must
91
+ verify them against the actual frozen inputs; the helper does not authenticate
92
+ receipts or interpret arbitrary path globs or expiry prose. C/E/F need the
93
+ class, evidence reference and blocking flag; return ASK or QUEUE. A/B/D
94
+ return CONTINUE only with the grant and all required checks. This is a
95
+ procedural transition adapter, not a new registered host event, permission
96
+ grant, scheduler, or security boundary. Skills-only hosts execute the same
97
+ checklist and disclose unavailable deterministic checks.
98
+
99
+ New Journeys set `owner_wait_tracking: true` and initialize
100
+ `owner_blocked_intervals: []`. Record intervals only while no authorized ready
101
+ work can progress specifically because of unanswered owner questions. Each
102
+ interval lists their `decision_ids`, `started_at`, and nullable `ended_at`.
103
+ End the interval as soon as work can progress, even if some questions remain
104
+ unanswered. Do not treat every dependency wait, off-hours gap, or commit gap as
105
+ owner-blocked time. Never backfill unknown times. Legacy Journeys without
106
+ tracking report `unavailable`, not zero waiting.
107
+
108
+ Run `node <plugin root>/hooks/owner-stops.cjs <journey.json> <as-of-UTC>` at
109
+ wave receipts and final closeout. It validates typed records, rejects duplicate
110
+ IDs or inconsistent times, counts `decisions_asked`, distinct approval
111
+ `round_trips`, pending/queued/cancelled questions and A–F counts, and reports
112
+ `response_ms` (sum of completed response intervals), `pending_response_ms`,
113
+ `response_window_ms` (union of asked-to-answer/cancellation/as-of intervals),
114
+ and `blocked_ms` (union of the explicitly recorded fully blocked intervals).
115
+ Overlapping questions are not additive wall-clock wait. Local records only:
116
+ no telemetry or network transmission. Closeout renders these metrics and
117
+ coverage limitations; it must not claim measured savings from legacy gaps.
118
+
119
+ Operational target after settled scope: zero unnecessary stops per wave;
120
+ one integrated approval batch and one closeout batch per unchanged Journey
121
+ when closeout needs owner authority. Exceptions are recorded, never suppressed
122
+ to hit a quota. Forecast savings only from verified classified observations.
@@ -35,3 +35,8 @@ confirmation or rereview returns `OWNER_AMENDMENT_REQUIRED`.
35
35
  This gate reviews planning artifacts before dispatch. It never invokes a
36
36
  reviewer. Implementation assurance remains governed separately by
37
37
  `max_assurance_rounds` and task `required_assurance` / `assurance_rounds`.
38
+
39
+ Owner presentation and continuation follow `owner-stops.md`: one integrated
40
+ gate includes the bounded grant, first-approval summary or revision diff, open
41
+ decisions, and access to the full frozen package. This adds no reviewer,
42
+ review round, or approval checkpoint and does not alter the bounds above.
@@ -13,7 +13,6 @@ description: >
13
13
  Wave authority. Coordinates more and implements less than Crewmate.
14
14
 
15
15
  ## Inputs and match
16
-
17
16
  - **Inputs:** approved baseline, wave objective, packet graph, dependencies,
18
17
  scope, authority, lineup from the recorded Journey snapshot, visible wave
19
18
  receipt, acceptance, and compact return schema.
@@ -21,7 +20,6 @@ Wave authority. Coordinates more and implements less than Crewmate.
21
20
  - **Non-match:** one bounded packet needs no orchestration, multiple waves conflict, or assurance alone is requested.
22
21
 
23
22
  ## Algorithm
24
-
25
23
  1. Continue this wave when identity, authority, route, and generation are
26
24
  unchanged; otherwise start fresh. Verify wave readiness, packet boundaries,
27
25
  dependencies, and approved identities from the recorded Journey snapshot,
@@ -42,6 +40,9 @@ Wave authority. Coordinates more and implements less than Crewmate.
42
40
  4. Inspect compact returns against write sets and acceptance; integrate
43
41
  evidence without implementing. Update `implementation.json` and `review.html`
44
42
  once per wave, plus owner-decision or blocker changes.
43
+ Apply `../bearing-lite/references/owner-stops.md` for queued questions,
44
+ blocking prerequisites, proven-independent progress and owner holds; never
45
+ add unapproved roles or gates.
45
46
  5. Dispatch declared assurance automatically at wave-end on this wave's
46
47
  integrated candidate. Deterministic checks always run. Honor
47
48
  `max_assurance_rounds` of 1 per declared phase or wave from visible
@@ -53,7 +54,6 @@ Wave authority. Coordinates more and implements less than Crewmate.
53
54
  `COMPLETE`, deployment checks do not reopen assurance.
54
55
 
55
56
  ## Return and recovery
56
-
57
57
  Return `READY`, `REROUTED`, `WAITING_ON`, or `OWNER_DECISION_REQUIRED` with
58
58
  verdict, candidate_ref, changed_paths, tests, findings, and blocker. Reroute only from new evidence; three attempts per packet.
59
59
 
@@ -46,7 +46,8 @@ Fresh planning node. The Router writes Journey state and owns owner conversation
46
46
  requirements. Two `review.html` states: `planning-review` and `final-closeout`.
47
47
  5. Give every slice stable requirement/design/SEIT IDs, dependencies, exact
48
48
  write set, authority, role, session rule, evidence, recovery, and stop rule.
49
- 6. Open and verify final HTML, then request exactly one integrated owner review
49
+ 6. Follow `../bearing-lite/references/owner-stops.md`.
50
+ Open and verify HTML, then request exactly one integrated owner review
50
51
  of outcome, design, route, lineup, cadence, and plan. Dispatch remains
51
52
  prohibited until approval. An owner change regenerates affected artifacts,
52
53
  then returns to this same gate; never insert a lineup or route-review pause.
@@ -22,6 +22,10 @@ Event side lane. Transcribes; cannot activate authority.
22
22
 
23
23
  1. Append owner decisions, authority events, ledger snapshots, lineup
24
24
  selections, and configuration digest to `journey.json` history.
25
+ Follow `../bearing-lite/references/owner-stops.md` for typed owner-stop
26
+ records: preserve question and round-trip IDs, record actual asked/answered
27
+ UTC times and explicit fully blocked intervals, and never infer missing
28
+ times. Automatic resolutions and owner steering are not invented questions.
25
29
  2. Do not activate authority or invent unresolved choices.
26
30
  3. Do not write Journey selections into `lineups.json` unless the owner
27
31
  asks to save a reusable profile.