@skanl/brambo-environment 0.1.1

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,109 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CLAUDE_MCP_TRAITS, CODEX_CONFIG_TRAITS, OPENCODE_CONFIG_TRAITS, readNativeMcpEntries, CLAUDE_MCP_TARGET_ID, CLAUDE_SKILLS_TARGET_ID, CODEX_CONFIG_TARGET_ID, CODEX_SKILLS_TARGET_ID, OPENCODE_CONFIG_TARGET_ID, OPENCODE_SKILLS_TARGET_ID, createClaudeMcpTarget, createClaudeSkillsTarget, createCodexConfigTarget, createCodexSkillsTarget, createOpenCodeConfigTarget, createOpenCodeSkillsTarget, } from '@skanl/brambo-projection';
4
+ export const EXECUTOR_PROFILES = [
5
+ {
6
+ executorId: 'claude-code',
7
+ targetId: CLAUDE_MCP_TARGET_ID,
8
+ // `~/.claude` is the directory Claude Code creates for itself; `~/.claude.json`
9
+ // is the file it reads MCP servers from. Either one is evidence it has run
10
+ // here. The home directory itself is deliberately not evidence — it exists on
11
+ // every machine and would make detection answer "yes" unconditionally.
12
+ evidencePaths: (homeDir) => [join(homeDir, '.claude.json'), join(homeDir, '.claude')],
13
+ machineConfig: (homeDir) => join(homeDir, '.claude.json'),
14
+ projectConfig: (projectDir) => join(projectDir, '.mcp.json'),
15
+ createTarget: (filePath) => createClaudeMcpTarget({ filePath }),
16
+ readMcpEntries: async (filePath) => await readNativeMcpEntries(CLAUDE_MCP_TRAITS, { filePath }),
17
+ machineSkills: (homeDir) => join(homeDir, '.claude', 'skills'),
18
+ skillsTargetId: CLAUDE_SKILLS_TARGET_ID,
19
+ createSkillsTarget: (rootPath) => createClaudeSkillsTarget({ rootPath }),
20
+ // `settings.json`, NOT `~/.claude.json`: correction-01 measured that the
21
+ // previous build wrote `$.brambo.{tools,mcpServers,skills,hooks}` there, into
22
+ // a file whose schema has none of those keys. Brambo's corrected target does
23
+ // not touch this file at all, which is exactly why nothing would ever clean
24
+ // it without this entry.
25
+ legacyConfig: (homeDir) => ({
26
+ filePath: join(homeDir, '.claude', 'settings.json'),
27
+ fileFormat: 'jsonc',
28
+ }),
29
+ },
30
+ {
31
+ executorId: 'codex',
32
+ targetId: CODEX_CONFIG_TARGET_ID,
33
+ // ponytail: no `projectConfig`, because Codex reads MCP servers from
34
+ // `~/.codex/config.toml` alone — correction-01 verified no project-scope
35
+ // location, and brambo does not invent one. Consequence, reported per run
36
+ // rather than silent: `brambo project init` cannot bind Codex to a project.
37
+ // Upgrade path: a verified per-project Codex config, if Codex grows one.
38
+ evidencePaths: (homeDir) => [join(homeDir, '.codex', 'config.toml'), join(homeDir, '.codex')],
39
+ machineConfig: (homeDir) => join(homeDir, '.codex', 'config.toml'),
40
+ projectConfig: undefined,
41
+ createTarget: (filePath) => createCodexConfigTarget({ filePath }),
42
+ readMcpEntries: async (filePath) => await readNativeMcpEntries(CODEX_CONFIG_TRAITS, { filePath }),
43
+ machineSkills: (homeDir) => join(homeDir, '.codex', 'skills'),
44
+ skillsTargetId: CODEX_SKILLS_TARGET_ID,
45
+ createSkillsTarget: (rootPath) => createCodexSkillsTarget({ rootPath }),
46
+ // The harmful one. A `# BEGIN brambo-managed` block here carries foreign
47
+ // sub-keys inside `[tools]` and `[skills]`, which are real fixed structs, so
48
+ // a documented `--strict-config` run fails to load the ENTIRE file.
49
+ legacyConfig: (homeDir) => ({
50
+ filePath: join(homeDir, '.codex', 'config.toml'),
51
+ fileFormat: 'toml',
52
+ }),
53
+ },
54
+ {
55
+ executorId: 'opencode',
56
+ targetId: OPENCODE_CONFIG_TARGET_ID,
57
+ evidencePaths: (homeDir) => [
58
+ join(homeDir, '.config', 'opencode', 'opencode.json'),
59
+ join(homeDir, '.config', 'opencode'),
60
+ ],
61
+ machineConfig: (homeDir) => join(homeDir, '.config', 'opencode', 'opencode.json'),
62
+ projectConfig: (projectDir) => join(projectDir, 'opencode.json'),
63
+ createTarget: (filePath) => createOpenCodeConfigTarget({ filePath }),
64
+ readMcpEntries: async (filePath) => await readNativeMcpEntries(OPENCODE_CONFIG_TRAITS, { filePath }),
65
+ machineSkills: (homeDir) => join(homeDir, '.config', 'opencode', 'skills'),
66
+ skillsTargetId: OPENCODE_SKILLS_TARGET_ID,
67
+ createSkillsTarget: (rootPath) => createOpenCodeSkillsTarget({ rootPath }),
68
+ // The same file brambo's corrected target merges into, so the reserved key
69
+ // sits beside the `mcp` entries brambo writes today; it is dropped at decode
70
+ // (`onExcessProperty: 'ignore'`) and read by nothing.
71
+ legacyConfig: (homeDir) => ({
72
+ filePath: join(homeDir, '.config', 'opencode', 'opencode.json'),
73
+ fileFormat: 'jsonc',
74
+ }),
75
+ },
76
+ ];
77
+ async function evidenceFor(path) {
78
+ try {
79
+ await stat(path);
80
+ return { path, exists: true };
81
+ }
82
+ catch (error) {
83
+ const code = error?.code;
84
+ // ENOENT and ENOTDIR are the two definitive absences: the path is not there,
85
+ // or a component of it is a file. Every other errno — EACCES, EPERM, ELOOP,
86
+ // an unreadable home — is brambo unable to LOOK, which is a different fact
87
+ // and is reported as one.
88
+ if (code === 'ENOENT' || code === 'ENOTDIR')
89
+ return { path, exists: false };
90
+ return { path, exists: undefined, error: code ?? String(error) };
91
+ }
92
+ }
93
+ /**
94
+ * Every executor brambo knows about, whether it was found, and the exact paths
95
+ * consulted for each. Returns the FULL catalogue on purpose: a run that detects
96
+ * nothing has to be able to tell the user what was looked for and where, and a
97
+ * list that omitted the misses could not.
98
+ */
99
+ export async function detectExecutors(homeDir) {
100
+ return await Promise.all(EXECUTOR_PROFILES.map(async (profile) => {
101
+ const evidence = await Promise.all(profile.evidencePaths(homeDir).map(evidenceFor));
102
+ return {
103
+ executorId: profile.executorId,
104
+ targetId: profile.targetId,
105
+ present: evidence.some((candidate) => candidate.exists === true),
106
+ evidence,
107
+ };
108
+ }));
109
+ }
@@ -0,0 +1,16 @@
1
+ export { PROJECTION_ACTION_ID, initMachine, initProject, noExecutorsDetected, deliveryFor, scopeDirectory, storeFor, type EntryDelivery, type InitMachineOptions, type InitProjectOptions, type InitResult, type LegacyBlock, type SkippedExecutor, type TargetFailure, type TargetProjection, type UnprojectableEntry, } from './init.ts';
2
+ export { DIAGNOSIS_FINDING_KINDS, FINDING_EXITS, diagnose, findingKindsFor, hasProblem, type Diagnosis, type DiagnoseOptions, type DiagnosisFinding, type DiagnosisFindingKind, type DiagnosisFindingSeverity, type DiagnosisTarget, type FindingExit, type WorktreeLeftover, } from './doctor.ts';
3
+ export { remediate, type RemediateOptions, type RemediationReport } from './remediate.ts';
4
+ export { EXECUTOR_PROFILES, detectExecutors, type EvidencePath, type ExecutorDetection, type ExecutorProfile } from './executors.ts';
5
+ export { RegistryStore } from '@skanl/brambo-registry';
6
+ export type { RegistryStoreOptions } from '@skanl/brambo-registry';
7
+ export { BUNDLE_KIND, BUNDLE_VERSION, OMITTED_FIELDS, createBundle, isCredential, parseBundle, readBundle, serializeBundle, writeBundle } from '@skanl/brambo-registry';
8
+ export type { OmittedEntry, OmittedField, RegistryBundle } from '@skanl/brambo-registry';
9
+ export { DRIFT_KINDS, BRAMBO_VERSION, REGISTRY_ENTRY_TYPES, REMEDIATION_KINDS, REMOVABLE_ENTRY_TYPES, RETIRED_ENTRY_TYPES, expandRegistryEntryPaths, isRetiredEntryType, } from '@skanl/brambo-contracts';
10
+ export type { DriftEntry, DriftKind, BramboErrorCode, ProjectionWarning, RegistryEntry, RegistryEntryType, RegistryScope, RemediationChange, RemediationKind, RemediationOutcome, RemediationRefusal, RetiredEntryType, StoredEntryType, } from '@skanl/brambo-contracts';
11
+ export { createMemoryLogSink } from '@skanl/brambo-kernel';
12
+ export type { LogRecord, LogSink, MemoryLogSink } from '@skanl/brambo-kernel';
13
+ export { WRITABLE_CONFIG_KEYS, configPathFor, setConfigValue, type ConfigWriteOptions, type ConfigWriteResult, type WritableConfigKey, } from '@skanl/brambo-projection';
14
+ export { ingestMachine } from './ingest.ts';
15
+ export type { IngestMachineOptions, MachineIngest, MachineMcpIngest, MachineMcpSkip, MachineSkillsSkip, OwnedMcpEntry, } from './ingest.ts';
16
+ export type { IngestOutcome, IngestWarning } from '@skanl/brambo-contracts';
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ export { PROJECTION_ACTION_ID, initMachine, initProject, noExecutorsDetected,
2
+ // The seams `brambo add` / `remove` / `list` bind to. Exported rather than
3
+ // re-derived in the binding: `storeFor` is the ONE mapping from a scope to a
4
+ // store, `scopeDirectory` is the trust boundary that keeps a project verb from
5
+ // building a tree brambo was asked to bind rather than create, and
6
+ // `deliveryFor` is what `add` reports its next step FROM — so the binding
7
+ // holds no idea of which entry type has a location at which scope.
8
+ deliveryFor, scopeDirectory, storeFor, } from './init.js';
9
+ export { DIAGNOSIS_FINDING_KINDS, FINDING_EXITS, diagnose, findingKindsFor, hasProblem, } from './doctor.js';
10
+ export { remediate } from './remediate.js';
11
+ export { EXECUTOR_PROFILES, detectExecutors } from './executors.js';
12
+ // Re-exported, not merely referenced. Under pnpm's strict layout a consumer that
13
+ // installed `@skanl/brambo-environment` cannot resolve `@skanl/brambo-contracts`,
14
+ // `@skanl/brambo-kernel` or `@skanl/brambo-registry` unless it declares them too — so a
15
+ // surface whose result carries a `DriftEntry` and whose precondition is "put
16
+ // entries in the registry first" has to hand back both, or the SDK promise is
17
+ // only true inside this monorepo. The list is exactly that: what you need to
18
+ // POPULATE the registry this projects from, READ a result, and OBSERVE the run.
19
+ //
20
+ // The closure is NOT total, and saying so is cheaper than a claim that rots:
21
+ // `RegistryStoreOptions.onStaleLockBreak` takes a `StaleLockBreak`,
22
+ // `ExecutorProfile.createTarget` returns a `ProjectionConfigTarget` and
23
+ // `ExecutorProfile.createSkillsTarget` a `ProjectionMaterialiseTarget`, none of
24
+ // which is re-exported. All three are reachable only by a consumer implementing
25
+ // one of those callbacks; the ordinary path needs none of them. Recorded in
26
+ // deferred-work.md rather than fixed by widening the surface on speculation.
27
+ export { RegistryStore } from '@skanl/brambo-registry';
28
+ // The bundle surface, for the same reason: a consumer holding a `RegistryStore`
29
+ // can build the artifact and read what did not travel without resolving
30
+ // `@skanl/brambo-registry` itself. `writeBundle` is here and this package still writes
31
+ // no file — it names a capability its own guard test forbids it to PERFORM,
32
+ // which is the whole point of a facade.
33
+ export { BUNDLE_KIND, BUNDLE_VERSION, OMITTED_FIELDS, createBundle, isCredential, parseBundle, readBundle, serializeBundle, writeBundle } from '@skanl/brambo-registry';
34
+ export { DRIFT_KINDS,
35
+ // `brambo --version`'s value, re-exported for the same reason every other
36
+ // constant on this list is: `@skanl/brambo-cli` is a THIN BINDING on the
37
+ // consumer tier, pinned by `packages/cli/test/run.test.ts` to
38
+ // `@skanl/brambo-environment` and `@skanl/brambo-session` and nothing else.
39
+ // M37.B put `import { BRAMBO_VERSION } from '@skanl/brambo-contracts'` at the top
40
+ // of `run.ts` and the pin's own comment records the premise that broke:
41
+ // "contracts moved to devDependencies once `describe()` stopped needing
42
+ // `instanceof BramboError`: the shipped CLI imports only consumer-tier
43
+ // packages". It did not any more, and the manifest and the import disagreed
44
+ // for a whole milestone -- the published binary importing a package it did not
45
+ // declare, which starts only because npm hoists the tree flat.
46
+ BRAMBO_VERSION, REGISTRY_ENTRY_TYPES, REMEDIATION_KINDS, REMOVABLE_ENTRY_TYPES, RETIRED_ENTRY_TYPES,
47
+ // The read-time inverse of the store's write-time normalization. A caller
48
+ // holding an entry in its PORTABLE form — the one a bundle carries — needs it
49
+ // to get back to the real paths the store's surface takes, and re-normalizing
50
+ // an already-normalized value corrupts it rather than being a no-op.
51
+ expandRegistryEntryPaths, isRetiredEntryType, } from '@skanl/brambo-contracts';
52
+ export { createMemoryLogSink } from '@skanl/brambo-kernel';
53
+ // Re-exported, not reimplemented. This package may not touch the filesystem at
54
+ // all (see test/guard.test.ts): the ledger is the sole authority for what brambo
55
+ // writes, and the clause is blunt on purpose. The WRITER is forwarded so the CLI
56
+ // reaches it through this facade -- the same shape as createMemoryLogSink above
57
+ // -- while the atomic primitive it uses stays inside @skanl/brambo-projection, where a
58
+ // previous story deliberately un-exported it.
59
+ export { WRITABLE_CONFIG_KEYS, configPathFor, setConfigValue, } from '@skanl/brambo-projection';
60
+ export { ingestMachine } from './ingest.js';
@@ -0,0 +1,60 @@
1
+ import type { IngestOutcome } from '@skanl/brambo-contracts';
2
+ import type { McpSourceDropped, McpSourceExclusion, McpSourceWarning, SkillsSourceWarning } from '@skanl/brambo-registry';
3
+ /** Which skill candidate the ingest looked at and did not contribute, and why. */
4
+ export type MachineSkillsSkip = SkillsSourceWarning;
5
+ /** Which server candidate the ingest looked at and did not contribute, and why. */
6
+ export type MachineMcpSkip = McpSourceWarning;
7
+ export interface IngestMachineOptions {
8
+ /** Defaults to the OS home directory, like every other machine-scope command. */
9
+ readonly homeDir?: string;
10
+ /**
11
+ * Decide everything a writing run decides and write nothing.
12
+ *
13
+ * Forwarded to the ONE `ingestProviders` call rather than answered by a second
14
+ * pass here: a preview computed by different code than the write is a preview
15
+ * that can lie.
16
+ */
17
+ readonly dryRun?: boolean;
18
+ }
19
+ /** The mcp-server half of one run, beside the skills half it shares a call with. */
20
+ export interface MachineMcpIngest {
21
+ /** The verified vendor config locations consulted, in profile order. */
22
+ readonly configPaths: readonly string[];
23
+ /** Candidates skipped: unreadable, an unusable id, or an ambiguous one. */
24
+ readonly skipped: readonly MachineMcpSkip[];
25
+ /**
26
+ * Servers left alone because brambo's own ledger claims them (D3), each paired
27
+ * with the `nativeLocation` that ledger record renders. The location is
28
+ * REPORTED and is deliberately not the match key: it is a rendering of the
29
+ * `targetId` and `entryId` that are, and matching on a rendering is how two
30
+ * answers come to differ.
31
+ */
32
+ readonly ownedByBrambo: readonly OwnedMcpEntry[];
33
+ /** Vendor keys the registry envelope cannot carry, per ingested server (D10). */
34
+ readonly dropped: readonly McpSourceDropped[];
35
+ }
36
+ export type OwnedMcpEntry = McpSourceExclusion;
37
+ export interface MachineIngest {
38
+ readonly homeDir: string;
39
+ /** The machine registry document the run wrote to, or would have. */
40
+ readonly registryPath: string;
41
+ /** The verified skills roots consulted, in the order the profiles declare. */
42
+ readonly roots: readonly string[];
43
+ readonly dryRun: boolean;
44
+ readonly outcome: IngestOutcome;
45
+ /** Skill candidates skipped: not a skill, an unusable id, or an ambiguous one. */
46
+ readonly skipped: readonly MachineSkillsSkip[];
47
+ /** Skill directories left alone because brambo's own ledger claims them (D3). */
48
+ readonly ownedByBrambo: readonly string[];
49
+ /** The other half of the same run, reported so neither can go silent. */
50
+ readonly mcpServers: MachineMcpIngest;
51
+ }
52
+ /**
53
+ * Puts the skills and the MCP servers already on this machine into the machine
54
+ * registry, in ONE two-phase run over both origins.
55
+ *
56
+ * Additive, exactly as both ports document: an entry an origin stops listing is
57
+ * left in the registry untouched, and nothing is ever removed. Pruning is a
58
+ * separate decision and is deliberately not made here.
59
+ */
60
+ export declare function ingestMachine(options?: IngestMachineOptions): Promise<MachineIngest>;
package/dist/ingest.js ADDED
@@ -0,0 +1,85 @@
1
+ import { homedir } from 'node:os';
2
+ import { BRAMBO_ERROR_CODES, BramboError } from '@skanl/brambo-contracts';
3
+ import { ProjectionLedger, SKILL_ENTRY_FILE } from '@skanl/brambo-projection';
4
+ import { createMachineMcpSource, createMachineSkillsSource, ingestProviders } from '@skanl/brambo-registry';
5
+ import { EXECUTOR_PROFILES } from './executors.js';
6
+ import { scopeDirectory, storeFor } from './init.js';
7
+ /**
8
+ * Puts the skills and the MCP servers already on this machine into the machine
9
+ * registry, in ONE two-phase run over both origins.
10
+ *
11
+ * Additive, exactly as both ports document: an entry an origin stops listing is
12
+ * left in the registry untouched, and nothing is ever removed. Pruning is a
13
+ * separate decision and is deliberately not made here.
14
+ */
15
+ export async function ingestMachine(options = {}) {
16
+ const dryRun = options.dryRun === true;
17
+ // The same trust boundary every other machine-scope command applies, and the
18
+ // same `homedir()` call: a second spelling of "the home directory" is how two
19
+ // commands come to disagree about which registry they are talking about.
20
+ const home = await scopeDirectory('the home directory', options.homeDir ?? homedir());
21
+ const ledger = new ProjectionLedger({ homeDir: home });
22
+ const read = await ledger.read();
23
+ if (read.state === 'unreadable') {
24
+ // BEFORE the roots or the vendor documents are even listed, let alone
25
+ // written. Without the ledger brambo cannot tell its own projections apart
26
+ // from your skills and your servers, and ingesting brambo's own output is
27
+ // worse than not ingesting at all — so this is a refusal rather than a run
28
+ // that proceeds with a weaker guarantee.
29
+ throw new BramboError(BRAMBO_ERROR_CODES.projectionLedgerUnavailable,
30
+ // Deliberately not opened with the word `brambo`: `test/printed-commands.ts`
31
+ // treats a backtick-quoted string that starts that way as a COMMAND, and
32
+ // this is a sentence.
33
+ `refusing to ingest without the ownership ledger, because without it brambo cannot tell its own projections from your skills and servers: ${read.warnings.map((warning) => warning.detail).join('; ')}`);
34
+ }
35
+ const ownedPaths = read.records.flatMap((record) => (record.ownedPaths ?? []).map((owned) => owned.path));
36
+ // ONE ledger read serves both origins. `targetId` + `entryId` is the match
37
+ // key; the `nativeLocation` beside it is carried into the report only.
38
+ const ownedEntries = read.records.map((record) => ({
39
+ targetId: record.targetId,
40
+ entryId: record.entryId,
41
+ nativeLocation: record.nativeLocation,
42
+ }));
43
+ // `machineSkills` and `machineConfig`, and nothing else. Every one of these was
44
+ // verified by running the real binary under an injected home; an executor whose
45
+ // skills location brambo has NOT proven carries `undefined` and contributes no
46
+ // root, which is the honest answer rather than a location brambo invented.
47
+ const roots = EXECUTOR_PROFILES.flatMap((profile) => profile.machineSkills === undefined ? [] : [profile.machineSkills(home)]);
48
+ const locations = EXECUTOR_PROFILES.map((profile) => {
49
+ const filePath = profile.machineConfig(home);
50
+ return { targetId: profile.targetId, filePath, read: async () => await profile.readMcpEntries(filePath) };
51
+ });
52
+ const skills = createMachineSkillsSource({ roots, entryFileName: SKILL_ENTRY_FILE, ownedPaths });
53
+ const servers = createMachineMcpSource({ locations, ownedEntries });
54
+ const store = storeFor('machine', home, home);
55
+ try {
56
+ // ONE call, both origins, one `dryRun`. Splitting it would give the two
57
+ // halves two chances to disagree about a store they both write to.
58
+ const outcome = await ingestProviders(store, {
59
+ toolProviders: [servers],
60
+ skillSources: [skills],
61
+ dryRun,
62
+ });
63
+ return {
64
+ homeDir: home,
65
+ registryPath: store.storePath('global'),
66
+ roots,
67
+ dryRun,
68
+ outcome,
69
+ skipped: [...skills.warnings],
70
+ ownedByBrambo: [...skills.excluded],
71
+ mcpServers: {
72
+ configPaths: locations.map((location) => location.filePath),
73
+ skipped: [...servers.warnings],
74
+ // Already whole: the source echoes back the ledger record it matched,
75
+ // so the location reported beside an exclusion is by construction the
76
+ // one that caused it, with no second lookup that could miss.
77
+ ownedByBrambo: [...servers.excluded],
78
+ dropped: [...servers.dropped],
79
+ },
80
+ };
81
+ }
82
+ finally {
83
+ await store.dispose();
84
+ }
85
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1,325 @@
1
+ import type { DriftEntry, BramboErrorCode, ProjectionTarget, ProjectionWarning, RegistryEntry, RegistryScope } from '@skanl/brambo-contracts';
2
+ import type { LogSink } from '@skanl/brambo-kernel';
3
+ import type { ProjectionMode } from '@skanl/brambo-projection';
4
+ import { RegistryStore } from '@skanl/brambo-registry';
5
+ import type { ExecutorDetection, ExecutorProfile } from './executors.ts';
6
+ /**
7
+ * Subject PREFIX every projection record is written under. The subject is
8
+ * `${PROJECTION_ACTION_ID}#${targetId}` — bounded by brambo's own constants, so
9
+ * it can never be rejected by the sink's identifier rules the way a file path
10
+ * (unbounded length, arbitrary characters) could be.
11
+ *
12
+ * Exported because a reader of the record stream needs the same string brambo
13
+ * wrote; match with `subject.startsWith(PROJECTION_ACTION_ID + '#')`.
14
+ */
15
+ export declare const PROJECTION_ACTION_ID = "environment.projection";
16
+ /** A registry entry a target could not express natively, and why (correction-01 C5). */
17
+ export interface UnprojectableEntry {
18
+ readonly entryId: string;
19
+ readonly reason: string;
20
+ }
21
+ /** A detected executor that was not projected into, and why. */
22
+ export interface SkippedExecutor {
23
+ readonly executorId: string;
24
+ readonly reason: string;
25
+ }
26
+ /**
27
+ * A per-target failure, flattened to code and message. A live `Error` would
28
+ * serialise to `{}` for every caller that prints the result, and the code is the
29
+ * part a caller acts on.
30
+ */
31
+ export interface TargetFailure {
32
+ readonly code: BramboErrorCode;
33
+ readonly message: string;
34
+ }
35
+ /**
36
+ * What happened to ONE executor's configuration. The four facts the caller has
37
+ * to be able to tell apart are separate fields rather than one status word,
38
+ * because they are not mutually exclusive: a single run can write one entry,
39
+ * report a second as drifted, and report a third as unprojectable.
40
+ *
41
+ * written — `written === true`: the file on disk changed.
42
+ * unchanged — `written === false` with empty `drift` and no `error`.
43
+ * drifted — `drift` is non-empty; nothing in it was overwritten.
44
+ * unprojectable — `unprojectable` is non-empty; nothing was written for those.
45
+ * failed — `error` is set; this target alone is affected.
46
+ */
47
+ export interface TargetProjection {
48
+ readonly executorId: string;
49
+ readonly targetId: string;
50
+ /**
51
+ * The vendor's own location, as that vendor reads it: a configuration FILE in
52
+ * `targets`, a skills ROOT DIRECTORY in `skills`.
53
+ */
54
+ readonly filePath: string;
55
+ readonly written: boolean;
56
+ readonly drift: readonly DriftEntry[];
57
+ readonly unprojectable: readonly UnprojectableEntry[];
58
+ readonly error?: TargetFailure;
59
+ }
60
+ export interface InitResult {
61
+ readonly scope: 'machine' | 'project';
62
+ /** Brambo's own state directory for this scope; it exists once init returns. */
63
+ readonly bramboDir: string;
64
+ /** The registry store this run read from; it exists once init returns. */
65
+ readonly registryPath: string;
66
+ readonly ledgerPath: string;
67
+ /** Registry entries this run projected from, across every scope it can see. */
68
+ readonly entryCount: number;
69
+ /** EVERY executor brambo knows, found or not, with the paths consulted. */
70
+ readonly detected: readonly ExecutorDetection[];
71
+ readonly targets: readonly TargetProjection[];
72
+ /**
73
+ * The skills root of every detected executor whose location brambo has
74
+ * VERIFIED, one row each — a separate array rather than more `targets` rows
75
+ * because the two surfaces are not the same thing: one names a file brambo
76
+ * merges text into, the other a directory tree brambo materialises and, when a
77
+ * skill leaves the registry, removes. Empty where no verified location
78
+ * applies, which is every executor at project scope.
79
+ */
80
+ readonly skills: readonly TargetProjection[];
81
+ readonly skipped: readonly SkippedExecutor[];
82
+ readonly warnings: readonly ProjectionWarning[];
83
+ }
84
+ export interface InitMachineOptions {
85
+ /** Defaults to the OS home directory. */
86
+ readonly homeDir?: string;
87
+ /**
88
+ * Where the projection records go. Omitted, a memory sink is built and then
89
+ * dropped — the records are still produced, simply unread.
90
+ *
91
+ * ponytail: the caller owns the sink they pass, draining included. Read
92
+ * `sink.records` only after `await sink.drain()`.
93
+ */
94
+ readonly log?: LogSink;
95
+ }
96
+ export interface InitProjectOptions extends InitMachineOptions {
97
+ /** Defaults to `process.cwd()`. */
98
+ readonly projectDir?: string;
99
+ }
100
+ /**
101
+ * True when no executor was found — the caller's non-zero-exit condition.
102
+ *
103
+ * Takes the DETECTION, not an `InitResult`: `brambo doctor` has to answer the
104
+ * same question about the same evidence, and two spellings of "did brambo find
105
+ * anything" is how the two commands come to disagree about one machine.
106
+ */
107
+ export declare function noExecutorsDetected(result: {
108
+ readonly detected: readonly ExecutorDetection[];
109
+ }): boolean;
110
+ /**
111
+ * The trust boundary. `homeDir` and `projectDir` are caller-supplied paths that
112
+ * decide where brambo creates directories and which vendor files it writes, so
113
+ * every one of them is resolved ONCE here and rejected unless it already names a
114
+ * directory.
115
+ *
116
+ * Three failures this closes, all of them observed: `homeDir: ''` — which is
117
+ * exactly `process.env.HOME ?? ''` in a consumer — resolves to the CWD and
118
+ * relocates the machine scope into whatever directory the process happens to be
119
+ * in; `brambo project init ~/typo` built the whole missing tree and wrote a
120
+ * vendor config into it; and `brambo project init ~/repo/.git` would have done
121
+ * the same inside a git directory. Brambo BINDS a project, it does not create one.
122
+ */
123
+ export declare function scopeDirectory(label: string, value: string): Promise<string>;
124
+ interface PlannedTarget {
125
+ readonly profile: ExecutorProfile;
126
+ readonly target: ProjectionTarget;
127
+ }
128
+ /**
129
+ * Brambo's OWN prior output still sitting in a vendor file (correction-01 C6).
130
+ *
131
+ * Every field of every row is produced by `runRemediation` under INSPECTION —
132
+ * the same call, in the same file, that `discard` performs. So the sentence
133
+ * `brambo doctor` prints about a legacy block is the sentence the remediation
134
+ * will act on, and the two cannot describe different regions.
135
+ */
136
+ export interface LegacyBlock {
137
+ readonly executorId: string;
138
+ readonly targetId: string;
139
+ readonly filePath: string;
140
+ /** What brambo found and would remove, or why it will not touch it. */
141
+ readonly detail: string;
142
+ /** Bytes the file would lose; 0 when brambo refuses. */
143
+ readonly byteDelta: number;
144
+ readonly refusal?: TargetFailure;
145
+ }
146
+ export declare function targetsFor(scope: 'machine' | 'project', detected: readonly ExecutorDetection[], homeDir: string, projectDir: string): {
147
+ readonly planned: readonly PlannedTarget[];
148
+ /**
149
+ * The skills roots, planned only where the executor has a location brambo
150
+ * VERIFIED. Machine scope only: no executor has a project-scope skills
151
+ * location brambo has proven, so `brambo project init` materialises none and
152
+ * the skills stay reported as unprojectable — the same refusal that keeps
153
+ * Codex out of a project's MCP configuration.
154
+ */
155
+ readonly skills: readonly PlannedTarget[];
156
+ readonly skipped: readonly SkippedExecutor[];
157
+ };
158
+ /**
159
+ * A per-target row in BOTH modes. `changed` is the one fact whose SENTENCE the
160
+ * mode decides — the merged text differs from the bytes on disk — so it is named
161
+ * for the fact and never for the write: `initMachine`/`initProject` map it to
162
+ * `written`, `diagnose` maps it to `wouldWrite`, and neither reading can be
163
+ * mistaken for the other by a caller holding the wrong one.
164
+ */
165
+ export interface ScopeTarget {
166
+ readonly executorId: string;
167
+ readonly targetId: string;
168
+ readonly filePath: string;
169
+ readonly changed: boolean;
170
+ readonly drift: readonly DriftEntry[];
171
+ readonly unprojectable: readonly UnprojectableEntry[];
172
+ readonly error?: TargetFailure;
173
+ }
174
+ /** Everything one scope's engine run produced, before either caller phrases it. */
175
+ export interface ScopeReport {
176
+ readonly bramboDir: string;
177
+ /**
178
+ * This scope's registry document. Under `'apply'` it exists by the time the
179
+ * report is built; under `'inspect'` it is only a PATH, and whether anything
180
+ * is there is the answer to "has brambo been initialised here".
181
+ */
182
+ readonly registryPath: string;
183
+ readonly ledgerPath: string;
184
+ readonly entryCount: number;
185
+ readonly detected: readonly ExecutorDetection[];
186
+ readonly targets: readonly ScopeTarget[];
187
+ /** One row per VERIFIED skills root; see `InitResult.skills`. */
188
+ readonly skills: readonly ScopeTarget[];
189
+ readonly skipped: readonly SkippedExecutor[];
190
+ /**
191
+ * Brambo's own prior output found in a vendor file. Computed under `'inspect'`
192
+ * ONLY: `brambo init` neither reports nor removes it, because removing it is a
193
+ * decision and this story's rule is that a decision is a user's.
194
+ */
195
+ readonly legacy: readonly LegacyBlock[];
196
+ readonly warnings: readonly ProjectionWarning[];
197
+ /**
198
+ * Set ONLY under `'inspect'`, and only when brambo's own registry document
199
+ * could not be read. `'apply'` rethrows instead: projecting against a registry
200
+ * brambo cannot read would delete every entry it holds from every vendor file.
201
+ * When this is set, `targets` is EMPTY — with no registry there is nothing
202
+ * brambo can honestly say projecting would do.
203
+ */
204
+ readonly registryError?: TargetFailure;
205
+ /**
206
+ * Entries whose type brambo has RETIRED — readable, listed and removable, and
207
+ * never handed to a target. Reported by `brambo doctor` because a word brambo no
208
+ * longer has is a state a user has to be told about AND given an exit from;
209
+ * `brambo init` neither projects nor removes them, because removing an entry is
210
+ * a decision and a decision is a user's.
211
+ */
212
+ readonly retired: readonly RetiredEntry[];
213
+ }
214
+ /**
215
+ * One retired entry AND the document that actually holds it.
216
+ *
217
+ * The scope is carried rather than inferred, and that is the whole point of this
218
+ * type. `RegistryEntry` has no scope field and `store.list()` is the MERGED
219
+ * view, so a caller holding only the entry has to guess — and the only guess
220
+ * available is the scope being diagnosed. `brambo project doctor` reads the
221
+ * GLOBAL registry too, so that guess attributed a global entry to the (possibly
222
+ * empty) project document and told the user to run `brambo project remove`, which
223
+ * exits 1 for an entry that is not there. A finding that names a file must name
224
+ * the file the entry is actually in.
225
+ */
226
+ export interface RetiredEntry {
227
+ readonly entry: RegistryEntry;
228
+ readonly scope: Exclude<RegistryScope, 'agent'>;
229
+ /** The document holding it — NOT necessarily the one being diagnosed. */
230
+ readonly registryPath: string;
231
+ }
232
+ export declare function storeFor(scope: 'machine' | 'project', homeDir: string, projectDir: string): RegistryStore;
233
+ /**
234
+ * What would actually DELIVER one entry at one scope, and — when nothing at
235
+ * that scope would — which scope does.
236
+ *
237
+ * DERIVED, never asserted. `brambo add` used to end with a sentence written
238
+ * beside the command ("`brambo project init` puts it into every detected
239
+ * executor"), and for a project-scope SKILL that sentence was false: no
240
+ * executor has a project-scope skills root brambo has verified, machine-scope
241
+ * projection cannot see a project-scope entry, and the entry was inert forever
242
+ * while the command it named exited 0. A promise can be kept syntactically and
243
+ * broken in substance, which is exactly what the printed-command invariant
244
+ * cannot catch.
245
+ *
246
+ * So nothing here knows which entry TYPE has a location at which scope. It runs
247
+ * `targetsFor` — the same planner `brambo init` runs — and then asks each target
248
+ * it planned whether it would take THIS entry, in the target's own words:
249
+ *
250
+ * - a config target is asked to merge into an EMPTY document. That call is
251
+ * pure text (`nativeText: ''` is the contract's "the file does not exist
252
+ * yet"), so no vendor file is read and none is written; an entry the target
253
+ * cannot express comes back in `skippedEntryIds`.
254
+ * - a materialise target is asked to PLAN. It describes what it would place
255
+ * and never touches the destination, and where it refuses it says why.
256
+ *
257
+ * Consequence, and it is the point: giving any `ExecutorProfile` a project-scope
258
+ * skills root changes what `brambo add` prints with no edit to the CLI and none
259
+ * to this function, and removing every skills root changes it the other way.
260
+ */
261
+ export interface EntryDelivery {
262
+ readonly scope: 'machine' | 'project';
263
+ /** The command that projects this scope. Always one the binary dispatches. */
264
+ readonly command: string;
265
+ /** Detected executors whose target for this scope would take the entry. */
266
+ readonly executorIds: readonly string[];
267
+ /** Where a target refused, in the target's own words. */
268
+ readonly reasons: readonly string[];
269
+ /**
270
+ * Set ONLY when nothing at this scope takes the entry and another scope
271
+ * would. Naming it is the difference between a dead end and a next step.
272
+ */
273
+ readonly elsewhere?: EntryDelivery;
274
+ /**
275
+ * Set when brambo could not work the answer out at all. The entry is already
276
+ * registered by the time this runs, so a failure here reports itself and
277
+ * never turns a completed registration into a failed command.
278
+ */
279
+ readonly undetermined?: string;
280
+ }
281
+ /**
282
+ * The command that projects one scope. One spelling, and `doctor.ts` is now a
283
+ * caller too: it named `brambo init` as the exit from a PROJECT's
284
+ * `not-initialised` and `out-of-date`, and that command exits 0 leaving the
285
+ * project exactly as it was.
286
+ */
287
+ export declare function projectCommandFor(scope: 'machine' | 'project'): string;
288
+ /**
289
+ * {@link EntryDelivery} for one scope, with the OTHER scope answered too
290
+ * whenever this one takes the entry nowhere.
291
+ *
292
+ * Contained: a target that throws while being asked leaves `undetermined` set
293
+ * rather than propagating, because the caller has already registered the entry
294
+ * and a message is not worth losing a completed write over.
295
+ */
296
+ export declare function deliveryFor(entry: RegistryEntry, scope: 'machine' | 'project', homeDir: string, projectDir: string): Promise<EntryDelivery>;
297
+ /**
298
+ * Registry -> detection -> projection engine, for one scope. `brambo init` and
299
+ * `brambo doctor` are THIS function under the two projection modes and nothing
300
+ * else: same entries, same detection, same targets, same engine call, same drift
301
+ * classification. Two code paths could disagree about what applying would do,
302
+ * and they would disagree exactly when a user is trying to fix something.
303
+ *
304
+ * Every line below either reads, or writes through the projection engine — which
305
+ * under `'inspect'` writes nothing at all.
306
+ */
307
+ export declare function runScope(scope: 'machine' | 'project', homeDir: string, projectDir: string, log: LogSink | undefined, mode: ProjectionMode): Promise<ScopeReport>;
308
+ /**
309
+ * Prepares this machine: brambo's own directory and registry store exist
310
+ * afterwards, and the global registry is projected into every detected
311
+ * executor's own machine-scope configuration.
312
+ *
313
+ * Idempotent. A second run over an unchanged registry writes no vendor byte and
314
+ * reports every target as unchanged.
315
+ */
316
+ export declare function initMachine(options?: InitMachineOptions): Promise<InitResult>;
317
+ /**
318
+ * Binds a project: brambo's own directory and project registry store exist under
319
+ * it afterwards, and the registry it can see — the project's entries over the
320
+ * machine's — is projected into every detected executor that has a project-scope
321
+ * configuration. An executor without one is reported as skipped, never written
322
+ * to somewhere it does not read.
323
+ */
324
+ export declare function initProject(options?: InitProjectOptions): Promise<InitResult>;
325
+ export {};