@alphazede/bearing-lite 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CONTRIBUTING.md +11 -6
  2. package/README.md +224 -121
  3. package/com.github.copilot/hooks/hooks.json +33 -0
  4. package/hooks/assurance-budget.cjs +33 -11
  5. package/hooks/closeout.cjs +17 -0
  6. package/hooks/com.anthropic.claude-code/mapping.md +35 -16
  7. package/hooks/owner-stops.cjs +154 -0
  8. package/hooks/plan-package.cjs +5 -4
  9. package/hooks/policy.cjs +10 -4
  10. package/hooks/profiles.cjs +119 -0
  11. package/hooks/te-host.cjs +40 -5
  12. package/hooks/transition-order.cjs +24 -4
  13. package/hooks/verification.cjs +358 -0
  14. package/package.json +6 -3
  15. package/plugin.json +2 -34
  16. package/profiles.json +1 -0
  17. package/schemas/authority.schema.json +55 -34
  18. package/schemas/implementation.schema.json +204 -5
  19. package/schemas/journey.schema.json +142 -48
  20. package/schemas/profiles.schema.json +359 -0
  21. package/schemas/verification.schema.json +126 -0
  22. package/skills/{set-bearings → architectural-alignment}/SKILL.md +17 -13
  23. package/skills/{set-bearings → architectural-alignment}/templates/workspace.md +1 -1
  24. package/skills/bearing-lite/SKILL.md +23 -23
  25. package/skills/bearing-lite/references/assurance-policy.md +32 -16
  26. package/skills/bearing-lite/references/lineups.md +7 -105
  27. package/skills/bearing-lite/references/owner-stops.md +122 -0
  28. package/skills/bearing-lite/references/peer-synthesis.md +11 -12
  29. package/skills/bearing-lite/references/profiles.md +149 -0
  30. package/skills/bearing-lite/references/resume.md +30 -0
  31. package/skills/bearing-lite/references/review-policy.md +8 -3
  32. package/skills/bearing-lite/references/role-routing.mmd +14 -14
  33. package/skills/bearing-lite/references/task-state.md +10 -10
  34. package/skills/bearing-lite/references/task-state.mmd +2 -2
  35. package/skills/bearing-lite/references/verification.md +52 -0
  36. package/skills/bearing-lite/templates/task.md +29 -28
  37. package/skills/{explorer → coordinator}/SKILL.md +19 -19
  38. package/skills/{crewmate → implementer}/SKILL.md +17 -15
  39. package/skills/{repository-fit → intake}/SKILL.md +10 -9
  40. package/skills/integration-engineer/SKILL.md +17 -12
  41. package/skills/light-implementer/SKILL.md +6 -5
  42. package/skills/onboard-bearing/SKILL.md +46 -0
  43. package/skills/plan-integrator/SKILL.md +14 -14
  44. package/skills/planning-and-design/SKILL.md +60 -0
  45. package/skills/{map-the-route → planning-and-design}/references/artifact-grammar.md +29 -27
  46. package/skills/prompt/SKILL.md +245 -0
  47. package/skills/requirements-engineer/SKILL.md +11 -11
  48. package/skills/{park-ranger → reviewer}/SKILL.md +16 -13
  49. package/skills/{gather-supplies → scope-definition}/SKILL.md +13 -13
  50. package/skills/scribe/SKILL.md +12 -8
  51. package/skills/systems-modeler/SKILL.md +5 -3
  52. package/skills/test-engineer/SKILL.md +16 -13
  53. package/templates/dod-manifest-v1.html +648 -0
  54. package/tools/render-dod-manifest.mjs +1743 -0
  55. package/lineups.json +0 -1
  56. package/schemas/lineups.schema.json +0 -145
  57. package/skills/map-the-route/SKILL.md +0 -59
  58. package/skills/navigator/SKILL.md +0 -36
  59. package/skills/surveyor/SKILL.md +0 -48
  60. package/skills/validator/SKILL.md +0 -37
  61. package/skills/validator/references/grading-rubric.md +0 -40
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Deterministic verification adapter (DES-BDL-008 / AC-BDL-009 / SEIT-BDL-004).
5
+ * Pure evaluator: no HOOK_CLASS, no host event, no download, no role dispatch.
6
+ * Reverify is one optional backend; availability never selects it for a task.
7
+ */
8
+
9
+ const STATUSES = Object.freeze(["VERIFIED", "REFUTED", "INCONCLUSIVE", "ERROR"]);
10
+ const AUTHORITIES = Object.freeze(["diagnostic", "assurance"]);
11
+ const STAGES = Object.freeze([
12
+ "implementation",
13
+ "integration",
14
+ "assurance",
15
+ "review",
16
+ "post_repair_closure",
17
+ ]);
18
+ const EXPECTED = Object.freeze(["VERIFIED", "REFUTED"]);
19
+ const SHA256 = /^[0-9a-f]{64}$/;
20
+ const CANDIDATE_FIELDS = Object.freeze([
21
+ "candidate_ref",
22
+ "candidate_revision",
23
+ "candidate_digest",
24
+ ]);
25
+
26
+ const isPlainObject = (value) =>
27
+ value !== null && typeof value === "object" && !Array.isArray(value);
28
+
29
+ const nonempty = (value) => typeof value === "string" && value.length > 0;
30
+
31
+ function deepEqual(a, b) {
32
+ if (a === b) return true;
33
+ if (typeof a !== typeof b) return false;
34
+ if (Array.isArray(a) && Array.isArray(b)) {
35
+ return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
36
+ }
37
+ if (isPlainObject(a) && isPlainObject(b)) {
38
+ const keys = Object.keys(a);
39
+ return keys.length === Object.keys(b).length && keys.every((key) => deepEqual(a[key], b[key]));
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function result(outcome, reason, extra = {}) {
45
+ return {
46
+ outcome,
47
+ reason,
48
+ status: extra.status ?? null,
49
+ authority: extra.authority ?? null,
50
+ gate_eligible: extra.gate_eligible === true,
51
+ closure_eligible: extra.closure_eligible === true,
52
+ rereview: false,
53
+ download_attempted: false,
54
+ backend_is_role: false,
55
+ };
56
+ }
57
+
58
+ function present(value) {
59
+ return !(value === undefined || value === null || value === "");
60
+ }
61
+
62
+ function sameCandidate(left, right) {
63
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
64
+ for (const field of CANDIDATE_FIELDS) {
65
+ const a = left[field];
66
+ const b = right[field];
67
+ if (field === "candidate_digest" && !present(a) && !present(b)) continue;
68
+ if (!present(a) || !present(b) || a !== b) return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ function requestErrors(request) {
74
+ if (!isPlainObject(request)) return "verification_request_missing";
75
+ if (request.schema_version !== "1" || request.kind !== "request") return "verification_request_invalid";
76
+ for (const field of [
77
+ "candidate_ref",
78
+ "candidate_revision",
79
+ "claim_id",
80
+ "claim_type",
81
+ "backend",
82
+ "stage",
83
+ "authority",
84
+ "expected_result",
85
+ ]) {
86
+ if (!nonempty(request[field])) return "verification_request_unbound";
87
+ }
88
+ if (!STAGES.includes(request.stage)) return "verification_request_invalid";
89
+ if (!AUTHORITIES.includes(request.authority)) return "verification_request_invalid";
90
+ if (!EXPECTED.includes(request.expected_result)) return "verification_request_invalid";
91
+ if (!isPlainObject(request.command_configuration) || Object.keys(request.command_configuration).length === 0) {
92
+ return "verification_request_unbound";
93
+ }
94
+ if (typeof request.selected !== "boolean" || typeof request.required !== "boolean") {
95
+ return "verification_request_unbound";
96
+ }
97
+ if (request.candidate_digest !== undefined && !SHA256.test(request.candidate_digest)) {
98
+ return "verification_request_invalid";
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function receiptErrors(receipt) {
104
+ if (!isPlainObject(receipt)) return "verification_receipt_missing";
105
+ if (receipt.schema_version !== "1" || receipt.kind !== "receipt") return "verification_receipt_invalid";
106
+ for (const field of [
107
+ "status",
108
+ "candidate_ref",
109
+ "candidate_revision",
110
+ "claim_id",
111
+ "backend",
112
+ "backend_version",
113
+ "evidence_digest",
114
+ "authority",
115
+ ]) {
116
+ if (!nonempty(receipt[field])) return "verification_receipt_unbound";
117
+ }
118
+ if (!STATUSES.includes(receipt.status)) return "verification_receipt_invalid";
119
+ if (!AUTHORITIES.includes(receipt.authority)) return "verification_receipt_invalid";
120
+ if (!SHA256.test(receipt.evidence_digest)) return "verification_receipt_unbound";
121
+ if (!isPlainObject(receipt.command_configuration) || Object.keys(receipt.command_configuration).length === 0) {
122
+ return "verification_receipt_unbound";
123
+ }
124
+ const who = receipt.produced_by;
125
+ if (!isPlainObject(who) || !nonempty(who.role) || !nonempty(who.identity) || !nonempty(who.session)) {
126
+ return "verification_receipt_unbound";
127
+ }
128
+ if (receipt.candidate_digest !== undefined && !SHA256.test(receipt.candidate_digest)) {
129
+ return "verification_receipt_invalid";
130
+ }
131
+ if (receipt.stage !== undefined && !STAGES.includes(receipt.stage)) return "verification_receipt_invalid";
132
+ return null;
133
+ }
134
+
135
+ function backendState(input, request) {
136
+ const backend = isPlainObject(input.backend) ? input.backend : {};
137
+ const available = backend.available === true;
138
+ const enabled = backend.enabled;
139
+ const name = nonempty(backend.name) ? backend.name : request.backend;
140
+ return {
141
+ name,
142
+ selected: request.selected === true,
143
+ required: request.required === true,
144
+ available,
145
+ enabled: enabled === false ? false : enabled === true ? true : available,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * @param {unknown} input verification evaluation request
151
+ * @returns {{
152
+ * outcome: string,
153
+ * reason: string,
154
+ * status: string|null,
155
+ * authority: string|null,
156
+ * gate_eligible: boolean,
157
+ * closure_eligible: boolean,
158
+ * rereview: false,
159
+ * download_attempted: false,
160
+ * backend_is_role: false
161
+ * }}
162
+ */
163
+ function evaluateVerification(input) {
164
+ try {
165
+ if (!isPlainObject(input)) {
166
+ return result("NEEDS_MORE_EVIDENCE", "verification_request_missing");
167
+ }
168
+ // Reverify/download is never performed here. A caller hint is ignored.
169
+ if (input.download === true || input.install === true || typeof input.download_fn === "function") {
170
+ // Fall through without invoking anything.
171
+ }
172
+
173
+ const requestErr = requestErrors(input.request);
174
+ if (requestErr) return result("NEEDS_MORE_EVIDENCE", requestErr);
175
+
176
+ const request = input.request;
177
+ const current = isPlainObject(input.candidate) ? input.candidate : request;
178
+ if (!sameCandidate(request, current)) {
179
+ return result("REJECT", "candidate_mismatch");
180
+ }
181
+
182
+ const backend = backendState(input, request);
183
+ const activated = backend.selected || backend.required;
184
+ if (!activated) {
185
+ if (!isPlainObject(input.receipt)) {
186
+ return result("INACTIVE", "backend_unselected_unrequired");
187
+ }
188
+ } else if (backend.enabled === false || backend.available !== true) {
189
+ return result("ERROR", "backend_unavailable", { status: "ERROR" });
190
+ }
191
+
192
+ if (!Object.hasOwn(input, "receipt") || input.receipt === undefined || input.receipt === null) {
193
+ return result(activated ? "NEEDS_MORE_EVIDENCE" : "INACTIVE", activated ? "verification_receipt_missing" : "backend_unselected_unrequired");
194
+ }
195
+
196
+ const receiptErr = receiptErrors(input.receipt);
197
+ if (receiptErr) return result("NEEDS_MORE_EVIDENCE", receiptErr);
198
+ const receipt = input.receipt;
199
+
200
+ if (
201
+ receipt.claim_id !== request.claim_id ||
202
+ receipt.backend !== request.backend ||
203
+ !deepEqual(receipt.command_configuration, request.command_configuration)
204
+ ) {
205
+ return result("REJECT", "claim_or_backend_mismatch", {
206
+ status: receipt.status,
207
+ authority: receipt.authority,
208
+ });
209
+ }
210
+ if (!sameCandidate(receipt, request) || !sameCandidate(receipt, current)) {
211
+ return result("REJECT", "candidate_mismatch", {
212
+ status: receipt.status,
213
+ authority: receipt.authority,
214
+ });
215
+ }
216
+
217
+ const repair = isPlainObject(input.repair) ? input.repair : {};
218
+ if (
219
+ nonempty(repair.prior_evidence_digest) &&
220
+ repair.prior_evidence_digest === receipt.evidence_digest &&
221
+ (repair.post_repair === true || nonempty(repair.prior_candidate_revision))
222
+ ) {
223
+ return result("REJECT", "stale_evidence", {
224
+ status: receipt.status,
225
+ authority: receipt.authority,
226
+ });
227
+ }
228
+ if (
229
+ nonempty(repair.prior_candidate_revision) &&
230
+ receipt.candidate_revision === repair.prior_candidate_revision &&
231
+ current.candidate_revision !== repair.prior_candidate_revision
232
+ ) {
233
+ return result("REJECT", "stale_evidence", {
234
+ status: receipt.status,
235
+ authority: receipt.authority,
236
+ });
237
+ }
238
+
239
+ if (receipt.status === "ERROR") {
240
+ return result("ERROR", "backend_error", {
241
+ status: "ERROR",
242
+ authority: receipt.authority,
243
+ });
244
+ }
245
+ if (receipt.status === "INCONCLUSIVE") {
246
+ return result("INCONCLUSIVE", "inconclusive_cannot_satisfy_gate", {
247
+ status: "INCONCLUSIVE",
248
+ authority: receipt.authority,
249
+ });
250
+ }
251
+
252
+ const author = isPlainObject(input.author) ? input.author : null;
253
+ const who = receipt.produced_by;
254
+ const sameIdentity = author && nonempty(author.identity) && who.identity === author.identity;
255
+ const sameSession = author && nonempty(author.session) && who.session === author.session;
256
+ const selfCert = sameIdentity || sameSession;
257
+ const gate = input.gate === "post_repair_closure" ? "post_repair_closure" : input.gate === "assurance" ? "assurance" : "none";
258
+
259
+ if (gate === "assurance") {
260
+ if (!author || !nonempty(author.identity) || !nonempty(author.session)) {
261
+ return result("NEEDS_MORE_EVIDENCE", "author_identity_missing", {
262
+ status: receipt.status,
263
+ authority: receipt.authority,
264
+ });
265
+ }
266
+ if (receipt.authority === "diagnostic") {
267
+ return result("REJECT", "diagnostic_cannot_satisfy_assurance_gate", {
268
+ status: receipt.status,
269
+ authority: "diagnostic",
270
+ });
271
+ }
272
+ if (selfCert) {
273
+ return result("REJECT", "self_certification", {
274
+ status: receipt.status,
275
+ authority: receipt.authority,
276
+ });
277
+ }
278
+ if (receipt.status !== request.expected_result) {
279
+ return result("REJECT", "expected_result_mismatch", {
280
+ status: receipt.status,
281
+ authority: receipt.authority,
282
+ });
283
+ }
284
+ return result("PASS", "independent_assurance_verified", {
285
+ status: receipt.status,
286
+ authority: "assurance",
287
+ gate_eligible: true,
288
+ });
289
+ }
290
+
291
+ if (gate === "post_repair_closure") {
292
+ if (input.review_after_repair === true || input.automatic_rereview_requested === true) {
293
+ return result("REJECT", "automatic_rereview_prohibited", {
294
+ status: receipt.status,
295
+ authority: receipt.authority,
296
+ });
297
+ }
298
+ if (!author || !nonempty(author.identity) || !nonempty(author.session)) {
299
+ return result("NEEDS_MORE_EVIDENCE", "author_identity_missing", {
300
+ status: receipt.status,
301
+ authority: receipt.authority,
302
+ });
303
+ }
304
+ if (selfCert) {
305
+ return result("REJECT", "self_certification", {
306
+ status: receipt.status,
307
+ authority: receipt.authority,
308
+ });
309
+ }
310
+ const closureStage =
311
+ request.stage === "post_repair_closure" || receipt.stage === "post_repair_closure";
312
+ if (receipt.authority === "diagnostic" && !closureStage) {
313
+ return result("REJECT", "diagnostic_cannot_satisfy_closure_gate", {
314
+ status: receipt.status,
315
+ authority: "diagnostic",
316
+ });
317
+ }
318
+ if (!closureStage) {
319
+ return result("REJECT", "closure_stage_required", {
320
+ status: receipt.status,
321
+ authority: receipt.authority,
322
+ });
323
+ }
324
+ if (receipt.status !== request.expected_result) {
325
+ return result("REJECT", "expected_result_mismatch", {
326
+ status: receipt.status,
327
+ authority: receipt.authority,
328
+ });
329
+ }
330
+ return result("PASS", "post_repair_deterministic_closure", {
331
+ status: receipt.status,
332
+ authority: receipt.authority,
333
+ closure_eligible: true,
334
+ });
335
+ }
336
+
337
+ if (receipt.authority === "diagnostic") {
338
+ return result("PASS", "diagnostic_receipt_recorded", {
339
+ status: receipt.status,
340
+ authority: "diagnostic",
341
+ });
342
+ }
343
+ return result("PASS", "receipt_recorded", {
344
+ status: receipt.status,
345
+ authority: receipt.authority,
346
+ });
347
+ } catch {
348
+ return result("NEEDS_MORE_EVIDENCE", "verification_adapter_unavailable");
349
+ }
350
+ }
351
+
352
+ module.exports = {
353
+ evaluateVerification,
354
+ STATUSES,
355
+ AUTHORITIES,
356
+ STAGES,
357
+ CANDIDATE_FIELDS,
358
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphazede/bearing-lite",
3
- "version": "0.2.1",
3
+ "version": "1.0.0",
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",
@@ -38,8 +38,11 @@
38
38
  "CONTRIBUTING.md",
39
39
  "SECURITY.md",
40
40
  "LICENSE-APACHE",
41
- "lineups.json",
42
- "schemas/"
41
+ "profiles.json",
42
+ "schemas/",
43
+ "templates/",
44
+ "tools/",
45
+ "com.github.copilot/"
43
46
  ],
44
47
  "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
45
48
  }
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": "1.0.0",
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",
@@ -18,37 +18,5 @@
18
18
  "routing",
19
19
  "developer-tools",
20
20
  "skills-first"
21
- ],
22
- "hooks": {
23
- "com.anthropic.claude-code.activation": {
24
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
25
- },
26
- "com.anthropic.claude-code.closeout": {
27
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
28
- },
29
- "com.openai.codex.activation": {
30
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
31
- },
32
- "com.openai.codex.closeout": {
33
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
34
- },
35
- "com.xai.grok-build.activation": {
36
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
37
- },
38
- "com.xai.grok-build.closeout": {
39
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
40
- },
41
- "com.cursor.ide.activation": {
42
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
43
- },
44
- "com.cursor.ide.closeout": {
45
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
46
- },
47
- "com.moonshotai.kimi-code.activation": {
48
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
49
- },
50
- "com.moonshotai.kimi-code.closeout": {
51
- "path": "./hooks/com.anthropic.claude-code/host.cjs"
52
- }
53
- }
21
+ ]
54
22
  }
package/profiles.json ADDED
@@ -0,0 +1 @@
1
+ {"schema_version":1,"profiles":{}}
@@ -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
  }