@alphazede/bearing-lite 0.2.0 → 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.
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ /** Deterministically apply evidence events to Journey state (#64). */
4
+ const fs = require("node:fs");
5
+
6
+ const REQUIRED = [
7
+ "schema_version", "event_id", "source", "journey", "repository", "unit",
8
+ "candidate_revision", "generation", "evidence", "transition", "occurred_at",
9
+ ];
10
+ const KINDS = new Set(["implementation", "verification", "assurance", "acceptance", "merge", "issue_close"]);
11
+ const FIELDS = {
12
+ implementation: "implementation",
13
+ verification: "verification",
14
+ assurance: "assurance",
15
+ acceptance: "acceptance",
16
+ merge: "observed_merge",
17
+ issue_close: "observed_issue_close",
18
+ };
19
+ const PREDECESSORS = { verification: "implementation", assurance: "verification", acceptance: "assurance" };
20
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
21
+ const nonempty = (value) => typeof value === "string" && value.length > 0;
22
+ const only = (value, names) => Object.keys(value).every((name) => names.includes(name));
23
+ const dateTime = (value) => nonempty(value) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
24
+
25
+ function receiptFor(event) {
26
+ return {
27
+ event_id: event?.event_id ?? null,
28
+ applied: false,
29
+ code: "invalid_event",
30
+ transition: event?.transition?.kind ?? null,
31
+ slice: event?.unit?.slice ?? null,
32
+ candidate_revision: event?.candidate_revision ?? null,
33
+ evidence_sha256: event?.evidence?.sha256 ?? null,
34
+ };
35
+ }
36
+
37
+ function structuralError(event) {
38
+ if (!object(event) || !REQUIRED.every((name) => Object.hasOwn(event, name)) || !only(event, REQUIRED)) return true;
39
+ if (event.schema_version !== 1 || !nonempty(event.event_id) || !nonempty(event.source) ||
40
+ !nonempty(event.journey) || !nonempty(event.repository) || !nonempty(event.candidate_revision) ||
41
+ !Number.isInteger(event.generation) || event.generation < 0 || !dateTime(event.occurred_at)) return true;
42
+ if (!object(event.unit) || !only(event.unit, ["wave", "slice"]) || !nonempty(event.unit.slice) ||
43
+ (Object.hasOwn(event.unit, "wave") && !nonempty(event.unit.wave))) return true;
44
+ if (!object(event.evidence) || !only(event.evidence, ["ref", "sha256"])) return true;
45
+ if (!object(event.transition) || !only(event.transition, ["kind", "verdict"]) ||
46
+ !KINDS.has(event.transition.kind) || !nonempty(event.transition.verdict)) return true;
47
+ return false;
48
+ }
49
+
50
+ function reject(state, receipt, code) {
51
+ return { state, receipt: { ...receipt, code, reason: code } };
52
+ }
53
+
54
+ function applyEvent(state, event, options = {}) {
55
+ const receipt = receiptFor(event);
56
+ if (structuralError(event)) return reject(state, receipt, "invalid_event");
57
+ if (options.expected_version !== undefined && options.expected_version !== state.version) {
58
+ return reject(state, receipt, "stale_write");
59
+ }
60
+ if (state.applied_event_ids.includes(event.event_id)) return reject(state, receipt, "duplicate_event");
61
+ if (event.journey !== state.journey || event.repository !== state.repository) {
62
+ return reject(state, receipt, "unrelated_journey");
63
+ }
64
+ if (event.generation < state.generation) return reject(state, receipt, "stale_generation");
65
+
66
+ const hasEvidence = nonempty(event.evidence.ref) && nonempty(event.evidence.sha256);
67
+ if (!hasEvidence && event.transition.verdict === "PASS") return reject(state, receipt, "bare_pass");
68
+ if (!hasEvidence) return reject(state, receipt, "missing_evidence");
69
+ if (!/^[0-9a-f]{64}$/.test(event.evidence.sha256)) return reject(state, receipt, "invalid_event");
70
+
71
+ const kind = event.transition.kind;
72
+ const slice = state.slices[event.unit.slice] || {};
73
+ const field = FIELDS[kind];
74
+ const prior = slice[field];
75
+ const history = prior ? {
76
+ ...prior.history,
77
+ [prior.candidate_revision]: { verdict: prior.verdict, sha256: prior.evidence.sha256 },
78
+ } : {};
79
+ const h = Object.hasOwn(history, event.candidate_revision) ? history[event.candidate_revision] : undefined;
80
+ if (h && (h.verdict !== event.transition.verdict || h.sha256 !== event.evidence.sha256)) {
81
+ return reject(state, receipt, "conflicting_receipt");
82
+ }
83
+ if (h) {
84
+ return reject(state, receipt, event.candidate_revision === prior.candidate_revision ? "duplicate_event" : "stale_candidate");
85
+ }
86
+
87
+ const implementation = slice.implementation;
88
+ if (["verification", "assurance", "acceptance"].includes(kind) &&
89
+ implementation?.candidate_revision !== event.candidate_revision) {
90
+ return reject(state, receipt, implementation ? "stale_candidate" : "predecessor_missing");
91
+ }
92
+ const predecessor = PREDECESSORS[kind];
93
+ if (predecessor && slice[predecessor]?.candidate_revision !== event.candidate_revision) {
94
+ return reject(state, receipt, "predecessor_missing");
95
+ }
96
+
97
+ const entry = {
98
+ candidate_revision: event.candidate_revision,
99
+ verdict: event.transition.verdict,
100
+ evidence: { ...event.evidence },
101
+ event_id: event.event_id,
102
+ history: { ...history, [event.candidate_revision]: { verdict: event.transition.verdict, sha256: event.evidence.sha256 } },
103
+ };
104
+ const next = {
105
+ ...state,
106
+ generation: Math.max(state.generation, event.generation),
107
+ version: state.version + 1,
108
+ applied_event_ids: [...state.applied_event_ids, event.event_id],
109
+ slices: { ...state.slices, [event.unit.slice]: { ...slice, [field]: entry } },
110
+ };
111
+ return { state: next, receipt: { ...receipt, applied: true, code: "applied" } };
112
+ }
113
+
114
+ function replay(state, events) {
115
+ const receipts = [];
116
+ let current = state;
117
+ for (const event of events) {
118
+ const result = applyEvent(current, event);
119
+ current = result.state;
120
+ receipts.push(result.receipt);
121
+ }
122
+ return { state: current, receipts };
123
+ }
124
+
125
+ function main(args = process.argv.slice(2)) {
126
+ const [stateFile, eventsFile] = args;
127
+ try {
128
+ if (!stateFile || !eventsFile) throw new Error("usage: node hooks/reconcile.cjs <state.json> <events.ndjson>");
129
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
130
+ const events = fs.readFileSync(eventsFile, "utf8").split(/\r?\n/).filter(Boolean).map(JSON.parse);
131
+ const result = replay(state, events);
132
+ const temporary = `${stateFile}.tmp-${process.pid}`;
133
+ fs.writeFileSync(temporary, JSON.stringify(result.state, null, 2) + "\n");
134
+ fs.renameSync(temporary, stateFile);
135
+ for (const receipt of result.receipts) process.stdout.write(JSON.stringify(receipt) + "\n");
136
+ return 0;
137
+ } catch (error) {
138
+ process.stderr.write(`${error.message}\n`);
139
+ return 1;
140
+ }
141
+ }
142
+
143
+ module.exports = { applyEvent, replay };
144
+
145
+ if (require.main === module) process.exitCode = main();
package/hooks/te-host.cjs CHANGED
@@ -584,6 +584,28 @@ function readStdinSync() {
584
584
  }
585
585
  }
586
586
 
587
+ /**
588
+ * Project the internal response onto the host wire (#71). Codex validates
589
+ * hookSpecificOutput with additionalProperties:false per event, so only host
590
+ * fields go to stdout: a PreToolUse deny, a Stop/SubagentStop block, or an
591
+ * empty object (allow / fail-open). The full internal verdict goes to stderr
592
+ * for audit; the exit stays 0 so no host reads stderr as a blocking reason.
593
+ */
594
+ function toWire(response) {
595
+ const inner = isPlainObject(response.hookSpecificOutput) ? response.hookSpecificOutput : {};
596
+ if (inner.permissionDecision === "deny") {
597
+ return {
598
+ hookSpecificOutput: {
599
+ hookEventName: "PreToolUse",
600
+ permissionDecision: "deny",
601
+ permissionDecisionReason: inner.permissionDecisionReason,
602
+ },
603
+ };
604
+ }
605
+ if (response.decision === "block") return { decision: "block", reason: response.reason };
606
+ return {};
607
+ }
608
+
587
609
  function main() {
588
610
  let raw = "";
589
611
  let response;
@@ -600,7 +622,8 @@ function main() {
600
622
  "adapter_failure: the test-engineering adapter could not run; unavailable"
601
623
  );
602
624
  }
603
- process.stdout.write(JSON.stringify(response) + "\n");
625
+ process.stderr.write(JSON.stringify(response) + "\n");
626
+ process.stdout.write(JSON.stringify(toWire(response)) + "\n");
604
627
  process.exit(0);
605
628
  }
606
629
 
@@ -613,6 +636,7 @@ module.exports = {
613
636
  supportChannel,
614
637
  handle,
615
638
  toHostResponse,
639
+ toWire,
616
640
  };
617
641
 
618
642
  if (require.main === module) {
@@ -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.0",
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.0",
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
  }
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/alphazede/bearing-lite/schemas/event.schema.json",
4
+ "title": "Bearing Lite evidence event",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "event_id",
10
+ "source",
11
+ "journey",
12
+ "repository",
13
+ "unit",
14
+ "candidate_revision",
15
+ "generation",
16
+ "evidence",
17
+ "transition",
18
+ "occurred_at"
19
+ ],
20
+ "properties": {
21
+ "schema_version": { "const": 1 },
22
+ "event_id": { "type": "string", "minLength": 1 },
23
+ "source": { "type": "string", "minLength": 1 },
24
+ "journey": { "type": "string", "minLength": 1 },
25
+ "repository": { "type": "string", "minLength": 1 },
26
+ "unit": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["slice"],
30
+ "properties": {
31
+ "wave": { "type": "string", "minLength": 1 },
32
+ "slice": { "type": "string", "minLength": 1 }
33
+ }
34
+ },
35
+ "candidate_revision": { "type": "string", "minLength": 1 },
36
+ "generation": { "type": "integer", "minimum": 0 },
37
+ "evidence": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["ref", "sha256"],
41
+ "properties": {
42
+ "ref": { "type": "string", "minLength": 1 },
43
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
44
+ }
45
+ },
46
+ "transition": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["kind", "verdict"],
50
+ "properties": {
51
+ "kind": {
52
+ "enum": ["implementation", "verification", "assurance", "acceptance", "merge", "issue_close"]
53
+ },
54
+ "verdict": { "type": "string", "minLength": 1 }
55
+ }
56
+ },
57
+ "occurred_at": { "type": "string", "format": "date-time" }
58
+ }
59
+ }
@@ -64,27 +64,50 @@
64
64
  "human_decision": { "type": ["string", "object", "null"] },
65
65
  "authority_id": { "type": ["string", "null"] },
66
66
  "dispatchable": { "type": "boolean" },
67
- "slice_status": { "type": "string" }
67
+ "slice_status": { "type": "string" },
68
+ "work_class": {
69
+ "type": "string",
70
+ "enum": ["light", "judgement"],
71
+ "description": "light: inputs fully determine the output and the packet's command verifies it (skills/light-implementer). Default judgement."
72
+ },
73
+ "work_class_reason": { "type": "string", "minLength": 1 }
68
74
  },
69
- "if": {
70
- "not": {
71
- "anyOf": [
72
- {
73
- "properties": { "dispatchable": { "const": false } },
74
- "required": ["dispatchable"]
75
- },
76
- {
77
- "properties": { "slice_status": { "const": "publication-blocked" } },
78
- "required": ["slice_status"]
75
+ "allOf": [
76
+ {
77
+ "if": {
78
+ "not": {
79
+ "anyOf": [
80
+ {
81
+ "properties": { "dispatchable": { "const": false } },
82
+ "required": ["dispatchable"]
83
+ },
84
+ {
85
+ "properties": { "slice_status": { "const": "publication-blocked" } },
86
+ "required": ["slice_status"]
87
+ }
88
+ ]
79
89
  }
80
- ]
81
- }
82
- },
83
- "then": {
84
- "properties": {
85
- "authority_id": { "type": "string", "minLength": 1 }
90
+ },
91
+ "then": {
92
+ "properties": {
93
+ "authority_id": { "type": "string", "minLength": 1 }
94
+ }
95
+ }
96
+ },
97
+ {
98
+ "if": {
99
+ "properties": { "work_class": { "const": "light" } },
100
+ "required": ["work_class"]
101
+ },
102
+ "then": {
103
+ "required": ["work_class_reason"],
104
+ "properties": {
105
+ "role": { "const": "Light Implementer" },
106
+ "command_ids": { "minItems": 1 }
107
+ }
108
+ }
86
109
  }
87
- }
110
+ ]
88
111
  }
89
112
  },
90
113
  "properties": {