@staix/agent-hub 0.23.1 → 0.25.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/CHANGELOG.md +24 -0
- package/README.md +32 -5
- package/docs/agent-notes/adapters.md +5 -2
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +16 -2
- package/docs/agent-notes/tasks.md +9 -2
- package/docs/agent-notes/tests.md +3 -0
- package/docs/dogfood.md +16 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +198 -5
- package/docs/profiles.md +2 -0
- package/docs/quickstart.md +19 -4
- package/docs/security.md +225 -8
- package/docs/specs/2026-09-19-agent-hub-design.md +54 -1
- package/docs/specs/2026-09-19-multi-project-design.md +12 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +351 -0
- package/docs/verification/2026-10-31-0.24.0.md +63 -0
- package/docs/verification/2026-10-31-claude-implementer-2.1.316.md +65 -0
- package/docs/verification/2026-11-01-0.25.0.md +91 -0
- package/docs/verification/claude-shell-wrapper-native.py +1 -1
- package/docs/verified.json +31 -15
- package/package.json +4 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +5120 -4710
- package/src/adapters/claude-channel.ts +29 -6
- package/src/adapters/compat.ts +3 -3
- package/src/adapters/definitions/native-cli-claude-setup.ts +4 -0
- package/src/adapters/definitions/native-cli-doctor.ts +30 -0
- package/src/adapters/jcode.ts +3 -0
- package/src/adapters/registry.ts +1 -1
- package/src/cli/acon-register.ts +29 -0
- package/src/cli/autonomy-homes.ts +33 -0
- package/src/cli/autonomy-offline.ts +75 -0
- package/src/cli/autonomy-projects.ts +103 -0
- package/src/cli/autonomy.ts +44 -0
- package/src/cli/claude-homes.ts +123 -0
- package/src/cli/claude-profile-hook.ts +130 -0
- package/src/cli/handoff-args.ts +27 -0
- package/src/cli/handoff-output.ts +25 -0
- package/src/cli/help.ts +8 -4
- package/src/cli/identity.ts +8 -1
- package/src/cli/main.ts +174 -33
- package/src/cli/output.ts +8 -5
- package/src/cli/schedule-output.ts +8 -4
- package/src/cli/setup-homes.ts +46 -0
- package/src/cli/setup.ts +1 -1
- package/src/cli/study-output.ts +71 -0
- package/src/cli/study.ts +58 -0
- package/src/cli/task-output.ts +1 -0
- package/src/hub/action-class.ts +203 -14
- package/src/hub/action-enforce.ts +8 -6
- package/src/hub/action-policy.ts +95 -9
- package/src/hub/autonomy-consent.ts +173 -0
- package/src/hub/autonomy-defaults.ts +58 -0
- package/src/hub/autonomy.ts +232 -0
- package/src/hub/board.ts +871 -2
- package/src/hub/channel-project.ts +61 -0
- package/src/hub/config-apply.ts +3 -2
- package/src/hub/confirmation-matrix.ts +202 -0
- package/src/hub/control-client.ts +10 -1
- package/src/hub/daemon.ts +546 -27
- package/src/hub/envelope.ts +2 -0
- package/src/hub/events.ts +4 -1
- package/src/hub/execution-budget.ts +12 -0
- package/src/hub/handoff.ts +126 -0
- package/src/hub/hub-tools.ts +12 -2
- package/src/hub/manager.ts +64 -57
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/path-policy.ts +12 -8
- package/src/hub/project.ts +14 -5
- package/src/hub/provenance.ts +30 -0
- package/src/hub/research-campaign.ts +265 -0
- package/src/hub/research-experiments.ts +445 -0
- package/src/hub/restart.ts +26 -0
- package/src/hub/schedules.ts +210 -9
- package/src/hub/session-identity.ts +49 -0
- package/src/hub/settings.ts +105 -8
- package/src/hub/task-repository.ts +11 -6
- package/src/hub/tasks.ts +894 -22
- package/src/hub/ui.ts +15 -6
- package/src/memory/capture.ts +2 -2
- package/src/ui/index.html +5 -3
- package/src/ui/office-space.ts +118 -0
- package/src/ui/plugin.ts +293 -0
- package/templates/AGENTS.block.md +1 -0
- package/templates/presets/autonomous.json +161 -0
- package/templates/presets/confirmation-matrix.json +943 -0
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +4 -4
- package/templates/profiles/writing.json +1 -1
- package/templates/roles/research/analyst.md +1 -0
- package/templates/roles/research/fact-checker.md +1 -0
- package/templates/roles/research/research-lead.md +1 -0
- package/templates/roles/research/researcher.md +1 -0
- package/templates/roles/research/statistician.md +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import presetJson from "../../templates/presets/autonomous.json";
|
|
2
|
+
import { checkSettingValue, settingDef } from "./settings.ts";
|
|
3
|
+
import { confirmationMatrix, MATRIX_ENVIRONMENTS, type ConfirmationMatrix, type ConfirmationRow, type MatrixEnvironment } from "./confirmation-matrix.ts";
|
|
4
|
+
import { PERMISSION_MODES, type PermissionMode } from "./permission-mode.ts";
|
|
5
|
+
|
|
6
|
+
export type AutonomyEnvironment = string;
|
|
7
|
+
export type AutonomyForm = "tui" | "headless";
|
|
8
|
+
export interface AutonomyOperation { key: string; value: unknown; store: "config" | "runtime" }
|
|
9
|
+
export interface AutonomySetting { store: "config" | "runtime"; values?: readonly string[]; locked?: string }
|
|
10
|
+
export type AutonomySettingLookup = (key: string) => AutonomySetting | undefined;
|
|
11
|
+
export interface AutonomyKindPreset {
|
|
12
|
+
permission: Record<AutonomyForm, PermissionMode | null>;
|
|
13
|
+
start: Record<MatrixEnvironment, AutonomyForm | null> & Record<string, AutonomyForm | null>;
|
|
14
|
+
}
|
|
15
|
+
export interface AutonomyEnvironmentPreset { matrixEnvironment: MatrixEnvironment; aliases: string[]; terminalOpen: string[] | null; instruction: string; detect: { priority: number; variable?: string; terminal?: boolean } }
|
|
16
|
+
export interface AutonomyPreset {
|
|
17
|
+
schema: 1;
|
|
18
|
+
issue: string;
|
|
19
|
+
about: string;
|
|
20
|
+
hub: Record<string, unknown>;
|
|
21
|
+
floorClasses: string[];
|
|
22
|
+
environments: Record<MatrixEnvironment, AutonomyEnvironmentPreset> & Record<string, AutonomyEnvironmentPreset>;
|
|
23
|
+
kinds: Record<string, AutonomyKindPreset>;
|
|
24
|
+
}
|
|
25
|
+
export interface AutonomyStructuralGuard { verified: boolean; reason: string }
|
|
26
|
+
export interface AutonomyPeer {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
form?: AutonomyForm;
|
|
30
|
+
/** Resolved definition is native-managed in every form, rather than only the current form. */
|
|
31
|
+
unmanaged?: boolean;
|
|
32
|
+
permissionMode?: string;
|
|
33
|
+
lockedMode?: string;
|
|
34
|
+
actionEnforcement?: { enforced: boolean; reason?: string };
|
|
35
|
+
structuralGuards?: Partial<Record<string, AutonomyStructuralGuard>>;
|
|
36
|
+
}
|
|
37
|
+
export interface AutonomyFloorStatus { class: string; status: "hub-enforced" | "structural-guard" | "not-enforced"; reason: string }
|
|
38
|
+
export interface AutonomyPeerStatus {
|
|
39
|
+
id: string;
|
|
40
|
+
kind: string;
|
|
41
|
+
form: AutonomyForm | null;
|
|
42
|
+
effectiveMode: string;
|
|
43
|
+
/** Preset intent, not proof that a config transaction has applied it. */
|
|
44
|
+
nextForm: AutonomyForm | null;
|
|
45
|
+
nextMode: string | null;
|
|
46
|
+
applies: "next peer start";
|
|
47
|
+
floor: AutonomyFloorStatus[];
|
|
48
|
+
missing: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface AutonomyStatusInput {
|
|
51
|
+
environment: AutonomyEnvironment;
|
|
52
|
+
peers: readonly AutonomyPeer[];
|
|
53
|
+
rows?: readonly ConfirmationRow[];
|
|
54
|
+
terminalOpener?: { available: boolean; reason?: string; via?: string };
|
|
55
|
+
/** Host floor verdicts per peer and class (`floorEnforcement` in action-policy.ts, from the running policy and where the hub sees the peer's calls); a class without a row is not enforced. */
|
|
56
|
+
floorEnforcement?: Readonly<Record<string, Readonly<Record<string, AutonomyFloorStatus>>>>;
|
|
57
|
+
preset?: AutonomyPreset;
|
|
58
|
+
matrix?: ConfirmationMatrix;
|
|
59
|
+
}
|
|
60
|
+
const FORMS: readonly AutonomyForm[] = ["tui", "headless"];
|
|
61
|
+
const object = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
62
|
+
const same = (left: readonly string[], right: readonly string[]) => left.length === right.length && [...left].sort().join("\0") === [...right].sort().join("\0");
|
|
63
|
+
const form = (value: unknown): value is AutonomyForm => value === "tui" || value === "headless";
|
|
64
|
+
const peerId = (value: string) => /^[a-z][a-z0-9_-]{0,63}$/.test(value);
|
|
65
|
+
export const autonomyPreset = (): AutonomyPreset => presetJson as unknown as AutonomyPreset;
|
|
66
|
+
|
|
67
|
+
export function autonomyEnvironment(env: NodeJS.ProcessEnv, terminal: boolean, preset: AutonomyPreset = autonomyPreset()): string {
|
|
68
|
+
if (validateAutonomyPreset(preset).length) throw new Error("autonomy preset is invalid");
|
|
69
|
+
const matching = Object.entries(preset.environments).filter(([, row]) => (!row.detect.variable || !!env[row.detect.variable]) && (row.detect.terminal === undefined || row.detect.terminal === terminal))
|
|
70
|
+
.sort((a, b) => a[1].detect.priority - b[1].detect.priority);
|
|
71
|
+
if (!matching[0] || matching[1]?.[1].detect.priority === matching[0][1].detect.priority) throw new Error("autonomy environment is missing or ambiguous");
|
|
72
|
+
return matching[0][0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pure, closed-data validation. A new catalog kind needs preset data, never a new kind branch. */
|
|
76
|
+
export function validateAutonomyPreset(value: unknown, matrix: ConfirmationMatrix = confirmationMatrix()): string[] {
|
|
77
|
+
if (!object(value)) return ["autonomy preset must be an object"];
|
|
78
|
+
const problems: string[] = [];
|
|
79
|
+
if (value.schema !== 1) problems.push("autonomy preset needs schema 1");
|
|
80
|
+
for (const field of ["issue", "about"] as const) if (typeof value[field] !== "string" || !value[field]) problems.push(`${field} must be non-empty text`);
|
|
81
|
+
if (!object(value.hub)) problems.push("hub must be an object");
|
|
82
|
+
else {
|
|
83
|
+
for (const key of ["tasks.auto_pull", "task_sweep.enabled", "task_sweep.auto_reassign"]) if (value.hub[key] !== true) problems.push(`hub.${key} must be true`);
|
|
84
|
+
for (const [key, raw] of Object.entries(value.hub)) {
|
|
85
|
+
const definition = settingDef(key);
|
|
86
|
+
if (typeof definition === "string" || definition.store !== "config") { problems.push(`hub.${key}: not a config setting`); continue; }
|
|
87
|
+
try { checkSettingValue(definition, raw); } catch { problems.push(`hub.${key}: invalid registered value`); }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(value.floorClasses) || !value.floorClasses.length || value.floorClasses.length > 32 || new Set(value.floorClasses).size !== value.floorClasses.length || value.floorClasses.some(item => typeof item !== "string" || !/^[a-z][a-z-]{0,63}$/.test(item))) problems.push("floorClasses must name distinct floor classes");
|
|
91
|
+
const environments = object(value.environments) ? Object.keys(value.environments) : [];
|
|
92
|
+
if (!object(value.environments) || !MATRIX_ENVIRONMENTS.every(environment => environments.includes(environment)) || environments.some(environment => !peerId(environment))) problems.push("environments must include terminal, orca and headless with plain ids");
|
|
93
|
+
else {
|
|
94
|
+
const aliases = new Set<string>(environments);
|
|
95
|
+
for (const environment of environments) {
|
|
96
|
+
const entry = value.environments[environment];
|
|
97
|
+
if (!object(entry) || !(MATRIX_ENVIRONMENTS as readonly unknown[]).includes(entry.matrixEnvironment) || !Array.isArray(entry.aliases) || entry.aliases.some(alias => typeof alias !== "string" || !/^[a-z][a-z0-9-]*$/.test(alias)) || typeof entry.instruction !== "string" || !entry.instruction.trim()) { problems.push(`environments.${environment}: aliases and instruction required`); continue; }
|
|
98
|
+
for (const alias of entry.aliases as string[]) { if (aliases.has(alias)) problems.push(`environment alias ${alias} is ambiguous`); aliases.add(alias); }
|
|
99
|
+
const detect = entry.detect;
|
|
100
|
+
if (!object(detect) || !Number.isSafeInteger(detect.priority) || (detect.priority as number) < 0 || (detect.priority as number) > 100 || Object.keys(detect).some(key => !["priority", "variable", "terminal"].includes(key)) || detect.variable === undefined && detect.terminal === undefined || detect.variable !== undefined && (typeof detect.variable !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(detect.variable)) || detect.terminal !== undefined && typeof detect.terminal !== "boolean") problems.push(`environments.${environment}.detect: bounded priority and environment-variable or terminal predicate required`);
|
|
101
|
+
if (entry.terminalOpen !== null && (!Array.isArray(entry.terminalOpen) || !entry.terminalOpen.length || entry.terminalOpen.some(word => typeof word !== "string") || entry.terminalOpen.filter(word => word === "{command}").length !== 1)) problems.push(`environments.${environment}.terminalOpen must be null or an argv template with one {command}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!object(value.kinds)) return [...problems, "kinds must be an object"];
|
|
105
|
+
for (const kind of Object.keys(matrix.kinds)) if (!Object.hasOwn(value.kinds, kind)) problems.push(`kinds.${kind}: missing`);
|
|
106
|
+
for (const [kind, raw] of Object.entries(value.kinds)) {
|
|
107
|
+
const facts = matrix.kinds[kind];
|
|
108
|
+
if (!facts) { problems.push(`kinds.${kind}: not in the confirmation matrix`); continue; }
|
|
109
|
+
if (!object(raw) || !object(raw.permission) || !object(raw.start)) { problems.push(`kinds.${kind}: permission and start required`); continue; }
|
|
110
|
+
if (!same(Object.keys(raw.permission), FORMS)) problems.push(`kinds.${kind}.permission must name tui and headless`);
|
|
111
|
+
if (!same(Object.keys(raw.start), environments)) problems.push(`kinds.${kind}.start must name every environment`);
|
|
112
|
+
for (const modeForm of FORMS) {
|
|
113
|
+
const mode = raw.permission[modeForm], supported = [...facts.forms, ...facts.extraForms].includes(modeForm);
|
|
114
|
+
if (mode !== null && (!(PERMISSION_MODES as readonly unknown[]).includes(mode) || !supported || facts.hubMode === "unmanaged" || facts.hubMode === "unmanaged-in-tui" && modeForm === "tui")) problems.push(`kinds.${kind}.permission.${modeForm}: unsupported hub mode`);
|
|
115
|
+
}
|
|
116
|
+
for (const environment of environments) {
|
|
117
|
+
const selected = raw.start[environment];
|
|
118
|
+
if (selected !== null && (!form(selected) || !facts.forms.includes(selected))) problems.push(`kinds.${kind}.start.${environment}: not a base start form`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return problems;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function environmentOf(value: AutonomyEnvironment, preset: AutonomyPreset): string {
|
|
125
|
+
if (Object.hasOwn(preset.environments, value)) return value;
|
|
126
|
+
const match = Object.keys(preset.environments).filter(environment => preset.environments[environment]!.aliases.includes(value));
|
|
127
|
+
if (match.length !== 1) throw new Error(`autonomy: unsupported environment ${value}`);
|
|
128
|
+
return match[0]!;
|
|
129
|
+
}
|
|
130
|
+
function checkedPreset(preset: AutonomyPreset, matrix: ConfirmationMatrix): void {
|
|
131
|
+
const problems = validateAutonomyPreset(preset, matrix);
|
|
132
|
+
if (problems.length) throw new Error(`autonomy: invalid preset: ${problems.join("; ")}`);
|
|
133
|
+
}
|
|
134
|
+
function nextForm(peer: Pick<AutonomyPeer, "id" | "kind" | "form" | "unmanaged">, environment: string, preset: AutonomyPreset, matrix: ConfirmationMatrix, lookup?: AutonomySettingLookup): AutonomyForm | null {
|
|
135
|
+
const entry = preset.kinds[peer.kind], facts = matrix.kinds[peer.kind];
|
|
136
|
+
if (!entry || !facts) return null;
|
|
137
|
+
// Declared variants are explicit; a runtime TUI observation must not defeat a requested headless default.
|
|
138
|
+
if (peer.form && (facts.extraForms.includes(peer.form) || peer.unmanaged && facts.forms.includes(peer.form))) return peer.form;
|
|
139
|
+
const values = lookup?.(`peers.${peer.id}.start_mode`)?.values;
|
|
140
|
+
if (values?.length === 1 && form(values[0]) && [...facts.forms, ...facts.extraForms].includes(values[0])) return values[0];
|
|
141
|
+
return entry.start[environment] ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Desired CONFIG leaf writes only. The daemon supplies its current setting definitions and owns the transaction/undo. */
|
|
145
|
+
export function planAutonomy(environment: AutonomyEnvironment, lookup?: AutonomySettingLookup, peers?: readonly Pick<AutonomyPeer, "id" | "kind" | "form" | "unmanaged">[], preset: AutonomyPreset = autonomyPreset(), matrix: ConfirmationMatrix = confirmationMatrix()): AutonomyOperation[] {
|
|
146
|
+
checkedPreset(preset, matrix);
|
|
147
|
+
const selected = environmentOf(environment, preset), operations: AutonomyOperation[] = [];
|
|
148
|
+
const add = (key: string, value: unknown, optionalLocked = false) => {
|
|
149
|
+
const definition = lookup?.(key);
|
|
150
|
+
if (lookup && !definition) throw new Error(`autonomy: no safe writer for ${key}`);
|
|
151
|
+
if (definition?.locked) { if (optionalLocked) return; throw new Error(`autonomy: locked required setting ${key}: ${definition.locked}`); }
|
|
152
|
+
if (definition && (definition.store !== "config" || definition.values && !definition.values.includes(String(value)))) throw new Error(`autonomy: unsupported preset value for ${key}`);
|
|
153
|
+
operations.push({ key, value: structuredClone(value), store: "config" });
|
|
154
|
+
};
|
|
155
|
+
for (const [key, value] of Object.entries(preset.hub)) add(key, value);
|
|
156
|
+
const terminal = preset.environments[selected]!.terminalOpen;
|
|
157
|
+
if (terminal !== null) add("terminal.open", terminal);
|
|
158
|
+
const targets: readonly AutonomyPeer[] = peers ?? Object.keys(preset.kinds).map(kind => ({ id: kind, kind }));
|
|
159
|
+
const seen = new Set<string>();
|
|
160
|
+
for (const peer of targets) {
|
|
161
|
+
if (!peerId(peer.id) || seen.has(peer.id)) throw new Error(`autonomy: invalid or duplicate peer id ${peer.id}`);
|
|
162
|
+
seen.add(peer.id);
|
|
163
|
+
const entry = preset.kinds[peer.kind], facts = matrix.kinds[peer.kind];
|
|
164
|
+
if (!entry || !facts) throw new Error(`autonomy: no preset for kind ${peer.kind}`);
|
|
165
|
+
const selectedForm = nextForm(peer, selected, preset, matrix, lookup);
|
|
166
|
+
if (!selectedForm) continue;
|
|
167
|
+
const startKey = `peers.${peer.id}.start_mode`;
|
|
168
|
+
if (matrix.keys["peers.<peer>.start_mode"]?.appliesTo?.includes(peer.kind) || lookup?.(startKey)) add(startKey, selectedForm, true);
|
|
169
|
+
const mode = peer.unmanaged ? null : entry.permission[selectedForm];
|
|
170
|
+
if (mode !== null) add(`permission_modes.${peer.id}`, mode, true);
|
|
171
|
+
}
|
|
172
|
+
return operations;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Fixed removal scope from shipped data and host peer ids, never ownership metadata or a selected environment. */
|
|
176
|
+
export function autonomyRemovalKeys(peers: readonly Pick<AutonomyPeer, "id" | "kind">[] = Object.keys(autonomyPreset().kinds).map(kind => ({ id: kind, kind })), preset: AutonomyPreset = autonomyPreset()): string[] {
|
|
177
|
+
const keys = new Set(Object.keys(preset.hub));
|
|
178
|
+
if (Object.values(preset.environments).some(row => row.terminalOpen !== null)) keys.add("terminal.open");
|
|
179
|
+
for (const peer of peers) {
|
|
180
|
+
if (!peerId(peer.id)) throw new Error(`autonomy: invalid peer id ${peer.id}`);
|
|
181
|
+
const entry = preset.kinds[peer.kind];
|
|
182
|
+
if (!entry) continue;
|
|
183
|
+
if (Object.values(entry.permission).some(mode => mode !== null)) {
|
|
184
|
+
keys.add(`permission_modes.${peer.id}`);
|
|
185
|
+
keys.add(`permission.${peer.id}`);
|
|
186
|
+
}
|
|
187
|
+
if (Object.values(entry.start).some(form => form !== null)) keys.add(`peers.${peer.id}.start_mode`);
|
|
188
|
+
}
|
|
189
|
+
return [...keys];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Current observations only: neither preset intent nor a kind name establishes a live hook, native sandbox or GitHub guard. */
|
|
193
|
+
export function autonomyStatus(input: AutonomyStatusInput): AutonomyPeerStatus[] {
|
|
194
|
+
const preset = input.preset ?? autonomyPreset(), matrix = input.matrix ?? confirmationMatrix();
|
|
195
|
+
checkedPreset(preset, matrix);
|
|
196
|
+
const environment = environmentOf(input.environment, preset), rows = input.rows ?? matrix.rows;
|
|
197
|
+
return input.peers.map(peer => {
|
|
198
|
+
const facts = matrix.kinds[peer.kind], entry = preset.kinds[peer.kind];
|
|
199
|
+
const desiredForm = nextForm(peer, environment, preset, matrix), currentForm = peer.form ?? desiredForm;
|
|
200
|
+
const mode = !peer.unmanaged && currentForm && entry ? entry.permission[currentForm] : null;
|
|
201
|
+
const desiredMode = peer.unmanaged ? null : peer.lockedMode ?? (desiredForm && entry ? entry.permission[desiredForm] : null);
|
|
202
|
+
const effectiveMode = peer.unmanaged ? "unmanaged" : peer.permissionMode ?? (!facts || !entry || !currentForm ? "unknown" : mode === null ? "unmanaged" : "unknown");
|
|
203
|
+
const missing: string[] = [];
|
|
204
|
+
if (peer.lockedMode) missing.push(`The ${peer.lockedMode} permission ceiling is locked; the preset does not change it.`);
|
|
205
|
+
if (!facts || !entry) missing.push(`No autonomy preset or confirmation inventory exists for kind ${peer.kind}.`);
|
|
206
|
+
if (!desiredForm) missing.push(`No ${preset.environments[environment]!.matrixEnvironment} base start is available for this peer; use an explicit supported instance or a person-owned terminal.`);
|
|
207
|
+
if (mode === null && facts && entry && currentForm) missing.push("Native permissions are not managed by the hub in this form; configure them in the native client.");
|
|
208
|
+
else if (peer.permissionMode === undefined || peer.permissionMode === "unknown") missing.push("The current runtime permission mode is unknown.");
|
|
209
|
+
if (!peer.lockedMode && desiredMode !== null && effectiveMode !== desiredMode) missing.push(`Current mode ${effectiveMode}; the preset requests ${desiredMode} for the next peer start. Apply the project defaults and restart the peer to use it.`);
|
|
210
|
+
if (currentForm === "tui" && input.terminalOpener?.available !== true) missing.push(`${input.terminalOpener?.reason ?? "No verified terminal opener is available."} ${preset.environments[environment]!.instruction}`);
|
|
211
|
+
const rowEnvironment = currentForm === "headless" ? "headless" : preset.environments[environment]!.matrixEnvironment;
|
|
212
|
+
const applicable = rows.filter(row => row.kinds.includes(peer.kind) && row.environments.includes(rowEnvironment) && (row.form === "any" || row.form === currentForm));
|
|
213
|
+
for (const row of applicable) for (const unknown of row.unknowns ?? []) missing.push(unknown);
|
|
214
|
+
const classed = applicable.filter(row => row.point === "classed-action");
|
|
215
|
+
const visible = classed.length > 0 && classed.every(row => row.hubSeesBeforeRun === "yes" || row.hubSeesBeforeRun === "partial");
|
|
216
|
+
if (classed.length && !visible) missing.push("The confirmation inventory does not establish pre-run visibility for every turn of this form.");
|
|
217
|
+
const floor = preset.floorClasses.map((action): AutonomyFloorStatus => {
|
|
218
|
+
const proof = input.floorEnforcement?.[peer.id]?.[action];
|
|
219
|
+
if (proof?.status === "hub-enforced" && proof.reason.trim() && peer.actionEnforcement?.enforced === true && visible) return { class: action, status: "hub-enforced", reason: proof.reason };
|
|
220
|
+
if (proof?.status === "not-enforced" && proof.reason.trim()) {
|
|
221
|
+
missing.push(`${action}: ${proof.reason}`);
|
|
222
|
+
return { class: action, status: "not-enforced", reason: proof.reason };
|
|
223
|
+
}
|
|
224
|
+
const guard = peer.structuralGuards?.[action];
|
|
225
|
+
if (guard?.verified === true && guard.reason.trim()) return { class: action, status: "structural-guard", reason: guard.reason };
|
|
226
|
+
const reason = `Hub floor enforcement is not established for this peer and form.${peer.actionEnforcement?.reason ? ` ${peer.actionEnforcement.reason}` : ""}`;
|
|
227
|
+
missing.push(`${action}: ${reason} No verified structural guard was supplied.`);
|
|
228
|
+
return { class: action, status: "not-enforced", reason };
|
|
229
|
+
});
|
|
230
|
+
return { id: peer.id, kind: peer.kind, form: currentForm, effectiveMode, nextForm: desiredForm, nextMode: desiredMode, applies: "next peer start", floor, missing: [...new Set(missing)] };
|
|
231
|
+
});
|
|
232
|
+
}
|