@alphazede/bearing-lite 0.1.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.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE-APACHE +202 -0
- package/README.md +213 -0
- package/SECURITY.md +89 -0
- package/guide/migration.md +168 -0
- package/hooks/activation.cjs +163 -0
- package/hooks/closeout.cjs +242 -0
- package/hooks/protected-action.cjs +229 -0
- package/hooks/transition-order.cjs +290 -0
- package/package.json +37 -0
- package/plugin.json +22 -0
- package/skills/bearing-lite/SKILL.md +40 -0
- package/skills/bearing-lite/assets/role-routing.png +0 -0
- package/skills/bearing-lite/assets/task-state.png +0 -0
- package/skills/bearing-lite/references/role-routing.mmd +34 -0
- package/skills/bearing-lite/references/task-state.md +42 -0
- package/skills/bearing-lite/references/task-state.mmd +29 -0
- package/skills/bearing-lite/templates/task.md +55 -0
- package/skills/crewmate/SKILL.md +37 -0
- package/skills/delegate-authority/SKILL.md +38 -0
- package/skills/explorer/SKILL.md +38 -0
- package/skills/gather-supplies/SKILL.md +33 -0
- package/skills/map-the-route/SKILL.md +33 -0
- package/skills/map-the-route/references/artifact-grammar.md +83 -0
- package/skills/navigator/SKILL.md +44 -0
- package/skills/park-ranger/SKILL.md +38 -0
- package/skills/repository-fit/SKILL.md +33 -0
- package/skills/set-bearings/SKILL.md +33 -0
- package/skills/sub-explorer/SKILL.md +37 -0
- package/skills/surveyor/SKILL.md +38 -0
- package/skills/trail-boss/SKILL.md +38 -0
- package/skills/trail-boss/agents/openai.yaml +4 -0
- package/skills/validator/SKILL.md +41 -0
- package/skills/validator/references/grading-rubric.md +39 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Existing-run migration and distribution checkpoints
|
|
2
|
+
|
|
3
|
+
How to retire a deep Bearing run without importing hidden runtime state, hand
|
|
4
|
+
work off in a project-local readable record, and resume with Bearing Lite.
|
|
5
|
+
Repository rename and npm deprecation remain separate owner-authorized release
|
|
6
|
+
checkpoints; this guide documents them only and does not perform remote actions.
|
|
7
|
+
|
|
8
|
+
Bearing Lite does **not** import `.bearing` data, Focus session state, MCP
|
|
9
|
+
runtime state, or CLI journey ledgers. Resume uses a verified human-readable
|
|
10
|
+
handoff, the existing approved plan, Bearing Lite, and native agent tools.
|
|
11
|
+
|
|
12
|
+
## Existing-run migration
|
|
13
|
+
|
|
14
|
+
For each project that still has deep Bearing run data, complete these steps in
|
|
15
|
+
order:
|
|
16
|
+
|
|
17
|
+
1. **Retire the old run read-only.** Leave historical `.bearing/`,
|
|
18
|
+
`bearing-<plan>/`, Focus, MCP, and CLI runtime data in place. Do not mutate,
|
|
19
|
+
import, or rewrite it as Lite input.
|
|
20
|
+
2. **Write a project-local human-readable handoff** in the affected repository
|
|
21
|
+
(for example under that project's planning docs). Fill every field in the
|
|
22
|
+
checklist below from visible plan text, Git history, and recorded evidence —
|
|
23
|
+
not from hidden runtime stores.
|
|
24
|
+
3. **Verify the handoff** against the project tree and the current candidate
|
|
25
|
+
revision: every field is present, paths exist or are intentionally listed as
|
|
26
|
+
deleted, blockers and dependencies match the plan, and the next action is
|
|
27
|
+
actionable without deep runtime state.
|
|
28
|
+
4. **Resume through Bearing Lite** using the existing approved plan plus native
|
|
29
|
+
agent tools. Do not require `.bearing` import, Focus resume, MCP session
|
|
30
|
+
restore, or CLI ledger continuity to continue work.
|
|
31
|
+
5. **Keep legacy runtime data read-only** until the handoff is verified. Archive
|
|
32
|
+
or remove retired run data only as a later, separate owner-authorized action.
|
|
33
|
+
|
|
34
|
+
Stop if migration would require importing hidden runtime state as Lite resume
|
|
35
|
+
input. That is out of scope for Bearing Lite; produce a fuller handoff from
|
|
36
|
+
visible sources instead.
|
|
37
|
+
|
|
38
|
+
## Project-local handoff checklist
|
|
39
|
+
|
|
40
|
+
Use one handoff record per existing run (or per coherent plan-bound workspace).
|
|
41
|
+
Every field is required for a complete handoff:
|
|
42
|
+
|
|
43
|
+
| Field | What to record |
|
|
44
|
+
|---|---|
|
|
45
|
+
| **Approved plan** | Path and identity of the owner-approved plan the run was executing (title, date, or plan directory). |
|
|
46
|
+
| **Completed work** | Slices, waves, or deliverables already finished and accepted, with enough detail that a new agent does not re-implement them. |
|
|
47
|
+
| **Changed paths** | Exact repository paths modified by completed work (added, edited, or intentionally removed). |
|
|
48
|
+
| **Commands / evidence** | Commands run, test or validation results, review verdicts, and other durable proof that completed work actually landed. |
|
|
49
|
+
| **Blockers** | Open blockers, refusals, or owner decisions still required; write `none` only when none remain. |
|
|
50
|
+
| **Dependencies** | Incomplete prerequisites, external packages, environment needs, or cross-slice dependencies that still gate progress. |
|
|
51
|
+
| **Next action** | The single concrete next step for the receiving agent or owner, scoped so Lite can resume without deep runtime state. |
|
|
52
|
+
|
|
53
|
+
### Representative handoff template
|
|
54
|
+
|
|
55
|
+
Copy into the affected project and fill every section:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
# Migration handoff — <project or plan name>
|
|
59
|
+
|
|
60
|
+
## Approved plan
|
|
61
|
+
- Plan path:
|
|
62
|
+
- Plan identity / date:
|
|
63
|
+
|
|
64
|
+
## Completed work
|
|
65
|
+
- [ ] <slice or deliverable>: summary
|
|
66
|
+
|
|
67
|
+
## Changed paths
|
|
68
|
+
- `path/to/file` — added | edited | removed
|
|
69
|
+
|
|
70
|
+
## Commands / evidence
|
|
71
|
+
- Command or review: result / artifact path
|
|
72
|
+
|
|
73
|
+
## Blockers
|
|
74
|
+
- none | <blocker and owner>
|
|
75
|
+
|
|
76
|
+
## Dependencies
|
|
77
|
+
- none | <dependency and why it still gates work>
|
|
78
|
+
|
|
79
|
+
## Next action
|
|
80
|
+
- <one concrete next step for Bearing Lite + native tools>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Handoff review (PROC-MIGRATION-01)
|
|
84
|
+
|
|
85
|
+
Before treating the handoff as verified, confirm:
|
|
86
|
+
|
|
87
|
+
- [ ] Approved plan is named and locatable in the project.
|
|
88
|
+
- [ ] Completed work is enumerated without relying on `.bearing` or other hidden state.
|
|
89
|
+
- [ ] Changed paths are explicit and match the tree or intentional removals.
|
|
90
|
+
- [ ] Commands and evidence are durable and inspectable.
|
|
91
|
+
- [ ] Blockers are stated (or explicitly `none`).
|
|
92
|
+
- [ ] Dependencies are stated (or explicitly `none`).
|
|
93
|
+
- [ ] Next action is sufficient to resume with Bearing Lite and native tools only.
|
|
94
|
+
- [ ] No step requires importing `.bearing`, Focus, MCP, or CLI runtime state.
|
|
95
|
+
|
|
96
|
+
A handoff that omits blockers, dependencies, or next action is incomplete. A
|
|
97
|
+
migration that imports hidden runtime state as Lite input is rejected.
|
|
98
|
+
|
|
99
|
+
## Read-only legacy retirement
|
|
100
|
+
|
|
101
|
+
Until the handoff is verified:
|
|
102
|
+
|
|
103
|
+
- Treat `.bearing/`, plan-bound `bearing-<plan>/` workspaces, Focus contexts,
|
|
104
|
+
MCP session artifacts, and CLI journey ledgers as **read-only historical
|
|
105
|
+
evidence**.
|
|
106
|
+
- Do not delete, rewrite, or “upgrade” those stores into Lite state.
|
|
107
|
+
- Do not point Bearing Lite at them as an import or resume source.
|
|
108
|
+
|
|
109
|
+
After the handoff is verified, archive or remove retired run data only under
|
|
110
|
+
explicit repository-owner authorization as a separate action. Permanent deletion
|
|
111
|
+
is never implied by writing the handoff.
|
|
112
|
+
|
|
113
|
+
## Distribution checkpoints (documentation only)
|
|
114
|
+
|
|
115
|
+
These steps are **owner-authorized release checkpoints**. Document them here so
|
|
116
|
+
operators know the approved sequence. This guide does **not** rename a
|
|
117
|
+
repository, publish or deprecate an npm package, mutate remotes, or perform any
|
|
118
|
+
other registry or network publish action.
|
|
119
|
+
|
|
120
|
+
Approved sequence (PROC-DISTRIBUTION-01 — verify before any remote action):
|
|
121
|
+
|
|
122
|
+
1. **Repository rename (in place).** Rename the existing public repository from
|
|
123
|
+
`alphazede/bearing` to `alphazede/bearing-lite` so public Git history remains
|
|
124
|
+
intact. Do not create a replacement repository that discards history.
|
|
125
|
+
2. **Package identity.** Publish future Lite releases only as
|
|
126
|
+
`@alphazede/bearing-lite`, and only after exact package and native-client
|
|
127
|
+
validation plus explicit owner approval.
|
|
128
|
+
3. **Deprecate the old package.** Deprecate `@alphazede/bearing` with a message
|
|
129
|
+
that directs users to Bearing Lite. Do not republish Lite under the old
|
|
130
|
+
identity.
|
|
131
|
+
4. **Link and evidence hygiene.** Update public repository, issue, security,
|
|
132
|
+
marketplace, package, and documentation links to the Lite identity. Keep
|
|
133
|
+
historical public evidence clearly historical rather than rewriting it as
|
|
134
|
+
current Lite behavior.
|
|
135
|
+
5. **Protected settings and authorization.** Confirm protected branch or
|
|
136
|
+
repository settings, registry credentials, and owner authorization before any
|
|
137
|
+
remote mutation. Remote action without that checkpoint is blocked.
|
|
138
|
+
|
|
139
|
+
### Pre-remote owner checklist
|
|
140
|
+
|
|
141
|
+
Before any rename, publish, or deprecation command:
|
|
142
|
+
|
|
143
|
+
- [ ] Owner has explicitly approved the repository rename
|
|
144
|
+
`alphazede/bearing` → `alphazede/bearing-lite`.
|
|
145
|
+
- [ ] Owner has explicitly approved publishing `@alphazede/bearing-lite` (not the
|
|
146
|
+
old package name).
|
|
147
|
+
- [ ] Owner has explicitly approved deprecating `@alphazede/bearing` with a
|
|
148
|
+
migration message pointing at Lite.
|
|
149
|
+
- [ ] Package identity on the candidate is `@alphazede/bearing-lite`.
|
|
150
|
+
- [ ] Protected settings and release authority are confirmed.
|
|
151
|
+
- [ ] No remote or registry action is performed from documentation-only work.
|
|
152
|
+
|
|
153
|
+
Repository rename, npm deprecation, and publication remain separate protected
|
|
154
|
+
owner actions. Completing this migration guide does not authorize them.
|
|
155
|
+
|
|
156
|
+
## What Bearing Lite does not do
|
|
157
|
+
|
|
158
|
+
- Import or migrate `.bearing` / Focus / MCP / CLI runtime state as resume input.
|
|
159
|
+
- Automatically rewrite deep run data into Lite task records.
|
|
160
|
+
- Perform repository rename, npm publish, npm deprecation, or other remote
|
|
161
|
+
release actions from ordinary product use.
|
|
162
|
+
- Depend on another Bearing product's runtime, state store, or release gate to
|
|
163
|
+
resume work.
|
|
164
|
+
|
|
165
|
+
Resume authority is the verified project-local handoff, the approved plan, and
|
|
166
|
+
native tools under Bearing Lite.
|
|
167
|
+
|
|
168
|
+
[Back to the README](../README.md)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bearing Lite activation/context advisory adapter (CONTRACT-HOOK-01).
|
|
6
|
+
* Advisory only: outcomes are ADVISE or UNAVAILABLE. Never fabricates BLOCK.
|
|
7
|
+
* Skills-only clients: enforcement remains procedural until a verified host maps it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HOOK_CLASS = "activation";
|
|
11
|
+
const OUTCOMES = Object.freeze(["ADVISE", "REROUTE", "BLOCK", "UNAVAILABLE"]);
|
|
12
|
+
const ENFORCEMENT = "procedural";
|
|
13
|
+
|
|
14
|
+
const RECOVERY_UNAVAILABLE =
|
|
15
|
+
"Report UNAVAILABLE, invoke the bearing-lite router manually, and continue after context is loaded";
|
|
16
|
+
const RECOVERY_ROUTER =
|
|
17
|
+
"Invoke skills/bearing-lite (router), load the assigned role skill, and record the next action in the project plan";
|
|
18
|
+
const RECOVERY_CONTINUE =
|
|
19
|
+
"Continue with the assigned role; keep the project plan as the only task record";
|
|
20
|
+
|
|
21
|
+
function isPlainObject(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function result(outcome, reason, recovery, extra) {
|
|
26
|
+
const body = {
|
|
27
|
+
hook_class: HOOK_CLASS,
|
|
28
|
+
outcome,
|
|
29
|
+
reason,
|
|
30
|
+
recovery,
|
|
31
|
+
enforcement: ENFORCEMENT,
|
|
32
|
+
};
|
|
33
|
+
if (extra && typeof extra.protected_action === "string" && extra.protected_action) {
|
|
34
|
+
body.protected_action = extra.protected_action;
|
|
35
|
+
}
|
|
36
|
+
return body;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unavailable(reason) {
|
|
40
|
+
return result("UNAVAILABLE", reason, RECOVERY_UNAVAILABLE);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} input
|
|
45
|
+
* @returns {{hook_class:string,outcome:string,reason:string,recovery:string,enforcement:string,protected_action?:string}}
|
|
46
|
+
*/
|
|
47
|
+
function evaluate(input) {
|
|
48
|
+
try {
|
|
49
|
+
if (input === undefined || input === null) {
|
|
50
|
+
return unavailable("missing_input");
|
|
51
|
+
}
|
|
52
|
+
if (typeof input === "string") {
|
|
53
|
+
try {
|
|
54
|
+
input = JSON.parse(input);
|
|
55
|
+
} catch {
|
|
56
|
+
return unavailable("malformed_input");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!isPlainObject(input)) {
|
|
60
|
+
return unavailable("malformed_input");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input.infrastructure_failure) {
|
|
64
|
+
const kind = String(input.infrastructure_failure);
|
|
65
|
+
return unavailable(kind || "infrastructure_failure");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Safe channels must remain reachable; advisory never blocks them.
|
|
69
|
+
const channel = input.channel;
|
|
70
|
+
if (
|
|
71
|
+
channel === "repair" ||
|
|
72
|
+
channel === "status" ||
|
|
73
|
+
channel === "owner_communication" ||
|
|
74
|
+
channel === "safe_rollback"
|
|
75
|
+
) {
|
|
76
|
+
return result(
|
|
77
|
+
"ADVISE",
|
|
78
|
+
"channel_open",
|
|
79
|
+
"Keep repair, status, owner communication, and safe rollback available"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const planPresent = input.plan_present === true;
|
|
84
|
+
const nextActionKnown =
|
|
85
|
+
input.next_action_known === true ||
|
|
86
|
+
(typeof input.next_action === "string" && input.next_action.trim().length > 0);
|
|
87
|
+
const role =
|
|
88
|
+
typeof input.assigned_role === "string" && input.assigned_role.trim()
|
|
89
|
+
? input.assigned_role.trim()
|
|
90
|
+
: null;
|
|
91
|
+
const routerInvoked = input.router_invoked === true;
|
|
92
|
+
const missingStages = Array.isArray(input.missing_planning_stages)
|
|
93
|
+
? input.missing_planning_stages.filter((s) => typeof s === "string" && s.trim())
|
|
94
|
+
: [];
|
|
95
|
+
|
|
96
|
+
if (!planPresent || !nextActionKnown || !role || !routerInvoked || missingStages.length > 0) {
|
|
97
|
+
const gaps = [];
|
|
98
|
+
if (!planPresent) gaps.push("plan");
|
|
99
|
+
if (!nextActionKnown) gaps.push("next_action");
|
|
100
|
+
if (!role) gaps.push("assigned_role");
|
|
101
|
+
if (!routerInvoked) gaps.push("router");
|
|
102
|
+
if (missingStages.length > 0) gaps.push("planning_stages:" + missingStages.join(","));
|
|
103
|
+
return result("ADVISE", "context_incomplete:" + gaps.join("+"), RECOVERY_ROUTER);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result("ADVISE", "context_ready", RECOVERY_CONTINUE);
|
|
107
|
+
} catch {
|
|
108
|
+
return unavailable("adapter_exception");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readStdinSync() {
|
|
113
|
+
try {
|
|
114
|
+
return require("node:fs").readFileSync(0, "utf8");
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const code = err && err.code;
|
|
117
|
+
if (code === "EAGAIN" || code === "EOF") return "";
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function main() {
|
|
123
|
+
let raw = "";
|
|
124
|
+
try {
|
|
125
|
+
raw = readStdinSync();
|
|
126
|
+
} catch {
|
|
127
|
+
process.stdout.write(JSON.stringify(unavailable("stdin_read_failure")) + "\n");
|
|
128
|
+
process.exit(0);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const trimmed = raw.trim();
|
|
133
|
+
if (!trimmed) {
|
|
134
|
+
process.stdout.write(JSON.stringify(unavailable("missing_input")) + "\n");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(trimmed);
|
|
142
|
+
} catch {
|
|
143
|
+
process.stdout.write(JSON.stringify(unavailable("malformed_input")) + "\n");
|
|
144
|
+
process.exit(0);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const out = evaluate(parsed);
|
|
149
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
150
|
+
// Always exit 0: never map policy or infrastructure to process status (RISK-12 / RISK-20).
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
HOOK_CLASS,
|
|
156
|
+
OUTCOMES,
|
|
157
|
+
ENFORCEMENT,
|
|
158
|
+
evaluate,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (require.main === module) {
|
|
162
|
+
main();
|
|
163
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bearing Lite return/closeout adapter (CONTRACT-HOOK-01).
|
|
6
|
+
* Advisory for ordinary handoff reminders; may BLOCK only protected completion.
|
|
7
|
+
* Infrastructure failure fails open as UNAVAILABLE and never fabricates BLOCK.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HOOK_CLASS = "closeout";
|
|
11
|
+
const OUTCOMES = Object.freeze(["ADVISE", "REROUTE", "BLOCK", "UNAVAILABLE"]);
|
|
12
|
+
const ENFORCEMENT = "procedural";
|
|
13
|
+
|
|
14
|
+
const HANDOFF_FIELDS = Object.freeze([
|
|
15
|
+
"plan_ref",
|
|
16
|
+
"role",
|
|
17
|
+
"subject",
|
|
18
|
+
"depends_on",
|
|
19
|
+
"scope",
|
|
20
|
+
"authority",
|
|
21
|
+
"outcome",
|
|
22
|
+
"evidence",
|
|
23
|
+
"blocker",
|
|
24
|
+
"next_action",
|
|
25
|
+
"receiving_role",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const RECOVERY_UNAVAILABLE =
|
|
29
|
+
"Report UNAVAILABLE, complete the handoff checklist manually, and do not request protected completion until required fields and assurance are present";
|
|
30
|
+
const RECOVERY_HANDOFF =
|
|
31
|
+
"Complete the role return envelope (plan_ref, role, subject, depends_on, scope, authority, outcome, evidence, blocker, next_action, receiving_role) in the project plan";
|
|
32
|
+
const RECOVERY_COMPLETE =
|
|
33
|
+
"Handoff is complete; parent coordinator may advance using the project plan only";
|
|
34
|
+
const RECOVERY_BLOCK_COMPLETION =
|
|
35
|
+
"Do not mark COMPLETE; restore missing assurance, candidate match, or handoff fields first. Repair and owner channels remain open";
|
|
36
|
+
const RECOVERY_CHANNEL =
|
|
37
|
+
"Keep repair, status, owner communication, and safe rollback available; closeout does not block them";
|
|
38
|
+
|
|
39
|
+
const SAFE_CHANNELS = new Set([
|
|
40
|
+
"repair",
|
|
41
|
+
"status",
|
|
42
|
+
"owner_communication",
|
|
43
|
+
"safe_rollback",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function isPlainObject(value) {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function result(outcome, reason, recovery, extra) {
|
|
51
|
+
const body = {
|
|
52
|
+
hook_class: HOOK_CLASS,
|
|
53
|
+
outcome,
|
|
54
|
+
reason,
|
|
55
|
+
recovery,
|
|
56
|
+
enforcement: ENFORCEMENT,
|
|
57
|
+
};
|
|
58
|
+
if (extra && typeof extra.protected_action === "string" && extra.protected_action) {
|
|
59
|
+
body.protected_action = extra.protected_action;
|
|
60
|
+
}
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unavailable(reason) {
|
|
65
|
+
return result("UNAVAILABLE", reason, RECOVERY_UNAVAILABLE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeAssurance(value) {
|
|
69
|
+
if (value === undefined || value === null || value === "" || value === "none") {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.map(String).filter((s) => s && s !== "none");
|
|
74
|
+
}
|
|
75
|
+
return [String(value)].filter((s) => s && s !== "none");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function missingHandoffFields(input) {
|
|
79
|
+
const handoff = isPlainObject(input.handoff) ? input.handoff : input;
|
|
80
|
+
const missing = [];
|
|
81
|
+
for (const field of HANDOFF_FIELDS) {
|
|
82
|
+
const value = handoff[field];
|
|
83
|
+
if (value === undefined || value === null) {
|
|
84
|
+
missing.push(field);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
88
|
+
missing.push(field);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// blocker may be the string "none"; that is present and valid.
|
|
92
|
+
return missing;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {unknown} input
|
|
97
|
+
*/
|
|
98
|
+
function evaluate(input) {
|
|
99
|
+
try {
|
|
100
|
+
if (input === undefined || input === null) {
|
|
101
|
+
return unavailable("missing_input");
|
|
102
|
+
}
|
|
103
|
+
if (typeof input === "string") {
|
|
104
|
+
try {
|
|
105
|
+
input = JSON.parse(input);
|
|
106
|
+
} catch {
|
|
107
|
+
return unavailable("malformed_input");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!isPlainObject(input)) {
|
|
111
|
+
return unavailable("malformed_input");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (input.infrastructure_failure) {
|
|
115
|
+
return unavailable(String(input.infrastructure_failure) || "infrastructure_failure");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (SAFE_CHANNELS.has(input.channel) || SAFE_CHANNELS.has(input.action_kind)) {
|
|
119
|
+
return result("ADVISE", "channel_open", RECOVERY_CHANNEL);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const mode =
|
|
123
|
+
input.mode === "protected_completion" || input.protected_completion === true
|
|
124
|
+
? "protected_completion"
|
|
125
|
+
: "advisory";
|
|
126
|
+
|
|
127
|
+
const missing = missingHandoffFields(input);
|
|
128
|
+
const required = normalizeAssurance(input.required_assurance);
|
|
129
|
+
const accepted = Array.isArray(input.assurance_accepted)
|
|
130
|
+
? input.assurance_accepted.map(String)
|
|
131
|
+
: [];
|
|
132
|
+
const missingAssurance = required.filter((role) => !accepted.includes(role));
|
|
133
|
+
// Protected completion requires explicit same-candidate proof; omit/false fail closed.
|
|
134
|
+
// Advisory closeout never uses this flag for outcome selection.
|
|
135
|
+
const candidateMatched = input.candidate_matched === true;
|
|
136
|
+
const unresolvedBlocker =
|
|
137
|
+
input.unresolved_blocker === true ||
|
|
138
|
+
(typeof input.blocker === "string" &&
|
|
139
|
+
input.blocker.trim() &&
|
|
140
|
+
input.blocker.trim() !== "none");
|
|
141
|
+
|
|
142
|
+
if (mode === "protected_completion") {
|
|
143
|
+
const blockers = [];
|
|
144
|
+
if (missing.length > 0) blockers.push("handoff:" + missing.join(","));
|
|
145
|
+
if (missingAssurance.length > 0) {
|
|
146
|
+
blockers.push("assurance:" + missingAssurance.join(","));
|
|
147
|
+
}
|
|
148
|
+
if (!candidateMatched) blockers.push("candidate_mismatch");
|
|
149
|
+
if (unresolvedBlocker) blockers.push("unresolved_blocker");
|
|
150
|
+
|
|
151
|
+
if (blockers.length > 0) {
|
|
152
|
+
return result(
|
|
153
|
+
"BLOCK",
|
|
154
|
+
"protected_completion_invalid:" + blockers.join("+"),
|
|
155
|
+
RECOVERY_BLOCK_COMPLETION,
|
|
156
|
+
{ protected_action: "completion" }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result(
|
|
161
|
+
"ADVISE",
|
|
162
|
+
"protected_completion_ready",
|
|
163
|
+
RECOVERY_COMPLETE,
|
|
164
|
+
{ protected_action: "completion" }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Advisory closeout: never BLOCK.
|
|
169
|
+
if (missing.length > 0) {
|
|
170
|
+
return result(
|
|
171
|
+
"ADVISE",
|
|
172
|
+
"handoff_incomplete:" + missing.join(","),
|
|
173
|
+
RECOVERY_HANDOFF
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (missingAssurance.length > 0) {
|
|
178
|
+
return result(
|
|
179
|
+
"ADVISE",
|
|
180
|
+
"assurance_pending:" + missingAssurance.join(","),
|
|
181
|
+
"Do not request protected completion until assurance roles accept the same candidate: " +
|
|
182
|
+
missingAssurance.join(", ")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result("ADVISE", "handoff_complete", RECOVERY_COMPLETE);
|
|
187
|
+
} catch {
|
|
188
|
+
return unavailable("adapter_exception");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readStdinSync() {
|
|
193
|
+
try {
|
|
194
|
+
return require("node:fs").readFileSync(0, "utf8");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const code = err && err.code;
|
|
197
|
+
if (code === "EAGAIN" || code === "EOF") return "";
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function main() {
|
|
203
|
+
let raw = "";
|
|
204
|
+
try {
|
|
205
|
+
raw = readStdinSync();
|
|
206
|
+
} catch {
|
|
207
|
+
process.stdout.write(JSON.stringify(unavailable("stdin_read_failure")) + "\n");
|
|
208
|
+
process.exit(0);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const trimmed = raw.trim();
|
|
213
|
+
if (!trimmed) {
|
|
214
|
+
process.stdout.write(JSON.stringify(unavailable("missing_input")) + "\n");
|
|
215
|
+
process.exit(0);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(trimmed);
|
|
222
|
+
} catch {
|
|
223
|
+
process.stdout.write(JSON.stringify(unavailable("malformed_input")) + "\n");
|
|
224
|
+
process.exit(0);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
process.stdout.write(JSON.stringify(evaluate(parsed)) + "\n");
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
HOOK_CLASS,
|
|
234
|
+
OUTCOMES,
|
|
235
|
+
ENFORCEMENT,
|
|
236
|
+
HANDOFF_FIELDS,
|
|
237
|
+
evaluate,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (require.main === module) {
|
|
241
|
+
main();
|
|
242
|
+
}
|