@bridge_gpt/mcp-server 0.2.67 → 0.2.68
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/README.md +178 -15
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1374 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +18 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +964 -768
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +544 -37
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +29 -5
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { guardedFastForwardPush, isSafePublicationBranch } from "./guarded-fast-forward-push.js";
|
|
1
2
|
/**
|
|
2
3
|
* Executor worktree ensure (BAPI-534, TDD §7, §12).
|
|
3
4
|
*
|
|
@@ -30,6 +31,45 @@ export function resolveExecutorBranch(job) {
|
|
|
30
31
|
return { ok: true, branch: `feature/${ticketKey}` };
|
|
31
32
|
return { ok: false, error: "no branch could be resolved (no expected_branch and no ticket_key)" };
|
|
32
33
|
}
|
|
34
|
+
/** Configure only this branch; inherited routing must already be safe. */
|
|
35
|
+
export async function configureExecutorBranch(worktreePath, branch, deps) {
|
|
36
|
+
const failure = "executor push routing could not be confirmed";
|
|
37
|
+
if (!isSafePublicationBranch(branch))
|
|
38
|
+
return failure;
|
|
39
|
+
try {
|
|
40
|
+
const read = async (key) => {
|
|
41
|
+
const result = await deps.runCommand("git", ["config", "--get-all", key], { cwd: worktreePath });
|
|
42
|
+
if (result.exitCode === 1)
|
|
43
|
+
return [];
|
|
44
|
+
if (result.exitCode !== 0)
|
|
45
|
+
return null;
|
|
46
|
+
return result.stdout.trim().split("\n");
|
|
47
|
+
};
|
|
48
|
+
const allowed = async (key, values) => {
|
|
49
|
+
const actual = await read(key);
|
|
50
|
+
return actual !== null && actual.every(value => values.includes(value));
|
|
51
|
+
};
|
|
52
|
+
if (!await allowed("push.default", ["simple", "current", "upstream"]) ||
|
|
53
|
+
!await allowed("remote.pushDefault", ["origin"]) ||
|
|
54
|
+
!await allowed(`branch.${branch}.pushRemote`, ["origin"]) ||
|
|
55
|
+
!await allowed("remote.origin.push", []) ||
|
|
56
|
+
!await allowed("remote.origin.mirror", ["false"]))
|
|
57
|
+
return failure;
|
|
58
|
+
const urls = await read("remote.origin.url");
|
|
59
|
+
const pushUrls = await read("remote.origin.pushurl");
|
|
60
|
+
if (!urls || urls.length !== 1 || !pushUrls || pushUrls.length > 1 ||
|
|
61
|
+
(pushUrls.length === 1 && pushUrls[0] !== urls[0]))
|
|
62
|
+
return failure;
|
|
63
|
+
for (const [key, value] of [[`branch.${branch}.remote`, "origin"], [`branch.${branch}.merge`, `refs/heads/${branch}`]]) {
|
|
64
|
+
if ((await deps.runCommand("git", ["config", "--local", key, value], { cwd: worktreePath })).exitCode !== 0)
|
|
65
|
+
return failure;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return failure;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
33
73
|
/**
|
|
34
74
|
* BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory for an
|
|
35
75
|
* executor worker. Returns `void` and swallows everything — a diagnostic that
|
|
@@ -71,6 +111,7 @@ function toWorktreeCoreDeps(deps) {
|
|
|
71
111
|
/** Build the command-provisioning boundary from the executor deps (BAPI-664). */
|
|
72
112
|
function toCommandProvisioningDeps(deps) {
|
|
73
113
|
return {
|
|
114
|
+
recordAssets: deps.recordPackagedAssets,
|
|
74
115
|
readFile: deps.readFile,
|
|
75
116
|
writeFile: deps.writeFile,
|
|
76
117
|
mkdir: deps.mkdir,
|
|
@@ -89,12 +130,12 @@ function toCommandProvisioningDeps(deps) {
|
|
|
89
130
|
* preserved unchanged. Errors stay secret-free (no raw filesystem exception, Git
|
|
90
131
|
* output, or command contents).
|
|
91
132
|
*/
|
|
92
|
-
async function finalizeExecutorWorktree(worktreePath, branch, expectedHeadSha, deps) {
|
|
133
|
+
async function finalizeExecutorWorktree(worktreePath, branch, expectedHeadSha, deps, publication) {
|
|
93
134
|
const provisioned = await provisionCommandsForWorktree(worktreePath, toCommandProvisioningDeps(deps));
|
|
94
135
|
if (!provisioned.ok) {
|
|
95
|
-
return { ok: false, error: provisioned.error };
|
|
136
|
+
return { ok: false, error: provisioned.error, ...(publication ? { publication } : {}) };
|
|
96
137
|
}
|
|
97
|
-
return { ok: true, worktreePath, branch, expectedHeadSha };
|
|
138
|
+
return { ok: true, worktreePath, branch, expectedHeadSha, ...(publication ? { publication } : {}) };
|
|
98
139
|
}
|
|
99
140
|
/**
|
|
100
141
|
* BAPI-1300 — finish a RETAINED continuation worktree the same way a created one
|
|
@@ -157,6 +198,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
157
198
|
if (!branchResult.ok)
|
|
158
199
|
return { ok: false, error: branchResult.error };
|
|
159
200
|
const branch = branchResult.branch;
|
|
201
|
+
const postSwitch = policy.preparation ? (worktreePath) => configureExecutorBranch(worktreePath, branch, deps) : undefined;
|
|
160
202
|
// The shared primitive resolves the branch from `key` via a per-key override,
|
|
161
203
|
// so we pin the exact resolved branch through the override map.
|
|
162
204
|
const key = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
|
|
@@ -188,7 +230,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
188
230
|
const originRef = `origin/${branch}`;
|
|
189
231
|
const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
|
|
190
232
|
if (!commandSucceeded(fetch)) {
|
|
191
|
-
const reason =
|
|
233
|
+
const reason = "";
|
|
192
234
|
return {
|
|
193
235
|
ok: false,
|
|
194
236
|
error: `git fetch origin ${branch} failed${reason ? `: ${reason}` : ""}`,
|
|
@@ -206,15 +248,49 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
206
248
|
error: `failed to resolve '${originRef}' to a commit after fetching branch '${branch}'`,
|
|
207
249
|
};
|
|
208
250
|
}
|
|
251
|
+
let publication;
|
|
252
|
+
let safeHead = expectedHeadSha;
|
|
253
|
+
const preReset = policy.preparation ? async (worktreePath, actualBranch) => {
|
|
254
|
+
const context = policy.preparation;
|
|
255
|
+
const refuse = (reason) => ({ ok: false, error: `retained work preserved: ${reason}` });
|
|
256
|
+
if (!context.mayMutate() || context.branch !== actualBranch)
|
|
257
|
+
return refuse("preparation unavailable");
|
|
258
|
+
const head = await resolveCommitSha(deps, worktreePath, "HEAD");
|
|
259
|
+
const checked = await deps.runCommand("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: worktreePath });
|
|
260
|
+
if (!head || checked.exitCode !== 0 || checked.stdout.trim() !== branch)
|
|
261
|
+
return refuse("branch or head unknown");
|
|
262
|
+
if (head === expectedHeadSha)
|
|
263
|
+
return { ok: true, target: expectedHeadSha };
|
|
264
|
+
const ancestry = await deps.runCommand("git", ["merge-base", "--is-ancestor", expectedHeadSha, "HEAD"], { cwd: worktreePath });
|
|
265
|
+
if (ancestry.exitCode === 1) {
|
|
266
|
+
try {
|
|
267
|
+
deps.errorLog?.(`[executor] job ${context.jobId} ${new Date(deps.now()).toISOString()}: branch ${branch}: non-descending retained head ${head.slice(0, 12)}; resetting to ${expectedHeadSha.slice(0, 12)}`);
|
|
268
|
+
}
|
|
269
|
+
catch { /* advisory */ }
|
|
270
|
+
return { ok: true, target: expectedHeadSha };
|
|
271
|
+
}
|
|
272
|
+
if (ancestry.exitCode !== 0)
|
|
273
|
+
return refuse("ancestry unknown");
|
|
274
|
+
const pushed = await guardedFastForwardPush({ branch, expectedOriginSha: expectedHeadSha,
|
|
275
|
+
candidateSha: head, worktreePath, runCommand: deps.runCommand, env: deps.env,
|
|
276
|
+
remainingMs: context.remainingMs, mayMutate: context.mayMutate });
|
|
277
|
+
if (pushed.status !== "published")
|
|
278
|
+
return refuse(pushed.reason);
|
|
279
|
+
publication = { jobId: context.jobId, preparationId: context.preparationId, branch,
|
|
280
|
+
from_sha: pushed.from_sha, to_sha: pushed.to_sha };
|
|
281
|
+
context.recordPublication(publication);
|
|
282
|
+
safeHead = pushed.to_sha;
|
|
283
|
+
return { ok: true, target: safeHead };
|
|
284
|
+
} : undefined;
|
|
209
285
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseStartPoint, guardStaleWorktree,
|
|
210
286
|
// Hard-reset a reused existing local branch onto the freshly fetched
|
|
211
287
|
// pushed head; a missing local branch is instead cut from `baseStartPoint`
|
|
212
288
|
// (= `origin/<branch>`) inside the primitive, so both land at origin.
|
|
213
|
-
{ freshenFromOrigin: originRef });
|
|
289
|
+
{ freshenFromOrigin: policy.preparation ? expectedHeadSha : originRef, ...(preReset ? { preReset } : {}), ...(postSwitch ? { postSwitch } : {}) });
|
|
214
290
|
if (row.status === "created" && typeof row.path === "string") {
|
|
215
|
-
return finalizeExecutorWorktree(row.path, branch,
|
|
291
|
+
return finalizeExecutorWorktree(row.path, branch, safeHead, deps, publication);
|
|
216
292
|
}
|
|
217
|
-
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'
|
|
293
|
+
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'`, ...(publication ? { publication } : {}) };
|
|
218
294
|
}
|
|
219
295
|
// BAPI-586 (fresh dispatch): a fresh implementation-style job must start from
|
|
220
296
|
// the CURRENT remote base — never a stale local `main`, another local base
|
|
@@ -258,9 +334,10 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
258
334
|
// gate lives HERE, not in the shared primitive, which learns no job types.
|
|
259
335
|
const priorHeadSha = eligiblePriorHead(job, policy);
|
|
260
336
|
if (priorHeadSha !== null) {
|
|
261
|
-
return ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps);
|
|
337
|
+
return ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps, postSwitch);
|
|
262
338
|
}
|
|
263
339
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
340
|
+
...(postSwitch ? { postSwitch } : {}),
|
|
264
341
|
alignExistingBranchTo: baseSha,
|
|
265
342
|
verifyHeadMatches: baseSha,
|
|
266
343
|
// BAPI-862: the guard is ON for this fresh-dispatch path, and this is the
|
|
@@ -306,7 +383,7 @@ function eligiblePriorHead(job, policy) {
|
|
|
306
383
|
* without cleanup. The result says plainly whether prior uncommitted files could
|
|
307
384
|
* survive.
|
|
308
385
|
*/
|
|
309
|
-
async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps) {
|
|
386
|
+
async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps, postSwitch) {
|
|
310
387
|
const entries = await listGitWorktrees(deps.runCommand, deps.cwd);
|
|
311
388
|
const retained = findWorktreeByBranch(entries, branch) !== undefined;
|
|
312
389
|
const coreDeps = toWorktreeCoreDeps(deps);
|
|
@@ -315,7 +392,7 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
|
|
|
315
392
|
// Recreation reads origin/<branch>; make sure that ref is current and proven.
|
|
316
393
|
const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
|
|
317
394
|
if (!commandSucceeded(fetch)) {
|
|
318
|
-
const reason =
|
|
395
|
+
const reason = "";
|
|
319
396
|
return {
|
|
320
397
|
ok: false,
|
|
321
398
|
error: `cannot resume prior-attempt work on '${branch}': git fetch origin ${branch} failed` +
|
|
@@ -330,7 +407,7 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
|
|
|
330
407
|
};
|
|
331
408
|
}
|
|
332
409
|
}
|
|
333
|
-
const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy });
|
|
410
|
+
const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy, ...(postSwitch ? { postSwitch } : {}) });
|
|
334
411
|
if (row.status !== "created" || typeof row.path !== "string") {
|
|
335
412
|
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
336
413
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Import-safe read-only installer guidance; never launcher admission. */
|
|
2
|
+
import { resolveAgentSpec } from "./agent-registry.js";
|
|
3
|
+
import { getPreflightPrereqDescriptors, getDoctorOnlyPrereqDescriptors, probePrerequisite, formatGitCompatibilityDiagnostic, START_TICKETS_DOCTOR_COMMAND, } from "./start-tickets-prereqs.js";
|
|
4
|
+
/** One ten-second budget across all tool probes, including unavailable runners. */
|
|
5
|
+
export async function collectFirstBuildAdvisory(options, deps) {
|
|
6
|
+
const preflight = getPreflightPrereqDescriptors(deps.platform, deps.env);
|
|
7
|
+
const base = { platform: deps.platform, configuredHosts: options.configuredHosts, required: [], recommended: [] };
|
|
8
|
+
if (!preflight.ok)
|
|
9
|
+
return { ...base, status: "unsupported" };
|
|
10
|
+
if (options.decision.kind !== "spawn")
|
|
11
|
+
return { ...base, status: "manual" };
|
|
12
|
+
const agent = resolveAgentSpec(options.decision.agent);
|
|
13
|
+
if (!agent)
|
|
14
|
+
return { ...base, status: "manual" };
|
|
15
|
+
const now = deps.now ?? Date.now;
|
|
16
|
+
const deadline = now() + 10000;
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const allowed = new Set(["uv", agent.command, "ast-grep", "rg", "lizard", "git-compatibility"]);
|
|
19
|
+
const required = preflight.descriptors.filter((d) => d.id !== "git-work-tree");
|
|
20
|
+
const recommended = getDoctorOnlyPrereqDescriptors(deps.platform, deps.env, agent).filter((d) => allowed.has(d.id));
|
|
21
|
+
async function probe(descriptor) {
|
|
22
|
+
let uncertain = false;
|
|
23
|
+
const remaining = Math.max(0, deadline - now());
|
|
24
|
+
const scoped = { ...deps, runCommand: async (file, args, commandOptions) => {
|
|
25
|
+
const budget = Math.max(0, deadline - now());
|
|
26
|
+
if (controller.signal.aborted || budget === 0) {
|
|
27
|
+
uncertain = true;
|
|
28
|
+
throw new Error("probe unavailable");
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await deps.runCommand(file, args, {
|
|
32
|
+
...commandOptions, timeoutMs: Math.min(commandOptions?.timeoutMs ?? budget, budget), signal: controller.signal,
|
|
33
|
+
});
|
|
34
|
+
if (result.unavailable || controller.signal.aborted || now() >= deadline || result.exitCode > 1) {
|
|
35
|
+
uncertain = true;
|
|
36
|
+
throw new Error("probe unavailable");
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
uncertain = true;
|
|
42
|
+
throw new Error("probe unavailable");
|
|
43
|
+
}
|
|
44
|
+
} };
|
|
45
|
+
let timer;
|
|
46
|
+
const unavailable = async () => {
|
|
47
|
+
const result = await probePrerequisite({ ...scoped, runCommand: async () => { throw new Error("probe unavailable"); } }, descriptor);
|
|
48
|
+
return { ...result, detail: undefined, availability: "unknown" };
|
|
49
|
+
};
|
|
50
|
+
if (remaining === 0 || controller.signal.aborted)
|
|
51
|
+
return unavailable();
|
|
52
|
+
try {
|
|
53
|
+
const result = await Promise.race([
|
|
54
|
+
probePrerequisite(scoped, descriptor),
|
|
55
|
+
new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve(null); }, remaining); }),
|
|
56
|
+
]);
|
|
57
|
+
if (!result)
|
|
58
|
+
return unavailable();
|
|
59
|
+
return { ...result, detail: undefined, availability: uncertain ? "unknown" : result.found ? "found" : "missing" };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (timer)
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const result = { ...base, status: "supported", agent: options.decision.agent, required: [], recommended: [] };
|
|
67
|
+
try {
|
|
68
|
+
for (const descriptor of required)
|
|
69
|
+
result.required.push(await probe(descriptor));
|
|
70
|
+
for (const descriptor of recommended)
|
|
71
|
+
result.recommended.push(await probe(descriptor));
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
controller.abort();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function formatFirstBuildAdvisory(result) {
|
|
79
|
+
const lines = [`First-build prerequisites (${result.platform}${result.agent ? `; ${result.agent}` : ""})`];
|
|
80
|
+
if (result.status !== "supported")
|
|
81
|
+
lines.push(`${result.status}: continue manually in your configured host; launcher readiness was not checked.`);
|
|
82
|
+
else {
|
|
83
|
+
for (const [label, tools] of [["Required for launcher", result.required], ["Recommended diagnostics", result.recommended]]) {
|
|
84
|
+
lines.push(`${label}:`);
|
|
85
|
+
for (const tool of tools.filter((t) => t.id !== "git-compatibility")) {
|
|
86
|
+
lines.push(` ${tool.label}: ${tool.availability}${tool.availability === "missing" ? ` — ${tool.installHint}` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const git = result.recommended.find((t) => t.id === "git-compatibility");
|
|
90
|
+
lines.push(git?.gitCompatibility ? formatGitCompatibilityDiagnostic(git.gitCompatibility) : "Git compatibility advisory: unknown");
|
|
91
|
+
lines.push("Git work-tree context is checked by the launcher at launch time. Recommendations and Git compatibility add no admission gate.");
|
|
92
|
+
}
|
|
93
|
+
if (result.configuredHosts.length > 1)
|
|
94
|
+
lines.push(`Other configured hosts: ${result.configuredHosts.join(", ")}; tool checks above apply only to the selected agent.`);
|
|
95
|
+
lines.push(`Read-only follow-up: ${START_TICKETS_DOCTOR_COMMAND}${result.agent ? ` --agent ${result.agent}` : ""}`);
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { packagedAssetRemedy } from "./generated-assets-remedy.js";
|
|
2
|
+
export const RECOVERY_ADVISORY_META_KEY = "bridge-api/asset-recovery";
|
|
3
|
+
export function assetRecoveryAdvisory(result, root, host, platform = process.platform) {
|
|
4
|
+
if (!result.confirmed || !host)
|
|
5
|
+
return null;
|
|
6
|
+
const missing = result.findings.filter(f => f.status === "missing");
|
|
7
|
+
if (!missing.length)
|
|
8
|
+
return null;
|
|
9
|
+
const paths = missing.slice(0, 5).map(f => f.destination);
|
|
10
|
+
const capabilities = [...new Set(missing.flatMap(f => f.workflows))].slice(0, 5);
|
|
11
|
+
const remedy = packagedAssetRemedy(root, host, undefined, platform);
|
|
12
|
+
return { host, missingCount: missing.length, paths, capabilities, command: remedy.command,
|
|
13
|
+
text: `Bridge ${host} workflow files are missing (${missing.length}; ${paths.join(", ")}${missing.length > 5 ? ", …" : ""}). Affected capabilities: ${capabilities.join(", ") || "packaged workflow documentation"}. ${remedy.command}\n${remedy.guidance}` };
|
|
14
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { COMMANDS } from "./commands.generated.js";
|
|
2
|
+
import { AGENTS } from "./agents.generated.js";
|
|
3
|
+
import { DOCS, DOC_METADATA } from "./docs.generated.js";
|
|
4
|
+
import { renderCommandForHost } from "./command-host.js";
|
|
5
|
+
import { codexSkillAssets } from "./codex-skill-adapter.js";
|
|
6
|
+
import { reconstructAgentMarkdown, translateAgentToCopilot, translateCommandToCopilot } from "./agent-utils.js";
|
|
7
|
+
export const WORKFLOW_HOSTS = ["claude", "cursor", "codex", "copilot"];
|
|
8
|
+
/** Destination and rendered bytes have one owner, shared by all consumers. */
|
|
9
|
+
export function buildPackagedAssetCatalog() {
|
|
10
|
+
const assets = [];
|
|
11
|
+
const packageProblems = [];
|
|
12
|
+
const destinations = new Set();
|
|
13
|
+
const add = (destination, content, kind, hosts, workflows, classification = "entrypoint") => {
|
|
14
|
+
if (destinations.has(destination) || !validAssetDestination(destination) || !content) {
|
|
15
|
+
packageProblems.push("invalid-or-conflicting-packaged-asset");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
destinations.add(destination);
|
|
19
|
+
assets.push({ id: destination, destination, content, kind, hosts, workflows, classification });
|
|
20
|
+
};
|
|
21
|
+
for (const [filename, content] of Object.entries(COMMANDS)) {
|
|
22
|
+
const workflow = filename.replace(/\.md$/, "");
|
|
23
|
+
try {
|
|
24
|
+
add(`.claude/commands/${filename}`, renderCommandForHost(filename, content, "claude-code"), "command", ["claude"], [workflow]);
|
|
25
|
+
add(`.cursor/commands/${filename}`, renderCommandForHost(filename, content, "cursor"), "command", ["cursor"], [workflow]);
|
|
26
|
+
const copilot = translateCommandToCopilot(filename, content);
|
|
27
|
+
add(`.github/prompts/${workflow}.prompt.md`, copilot.prompt, "prompt", ["copilot"], [workflow]);
|
|
28
|
+
add(`.github/skills/${workflow}/SKILL.md`, copilot.skill, "skill", ["copilot"], [workflow]);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
packageProblems.push("command-render-failed");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
for (const asset of codexSkillAssets(COMMANDS)) {
|
|
36
|
+
add(asset.relativePath, asset.content, "skill", ["codex"], [asset.relativePath.split("/")[2]]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
packageProblems.push("skill-render-failed");
|
|
41
|
+
}
|
|
42
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
43
|
+
try {
|
|
44
|
+
add(`.claude/agents/${key}.md`, reconstructAgentMarkdown(agent.frontmatter, agent.body), "agent", ["claude"], [key]);
|
|
45
|
+
add(`.github/agents/${key}.agent.md`, translateAgentToCopilot(agent.frontmatter, agent.body), "agent", ["copilot"], [key]);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
packageProblems.push("agent-render-failed");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [destination, content] of Object.entries(DOCS)) {
|
|
52
|
+
const metadata = DOC_METADATA[destination];
|
|
53
|
+
add(destination, content, "document", WORKFLOW_HOSTS, metadata?.workflows ?? [], metadata?.classification ?? "reference");
|
|
54
|
+
}
|
|
55
|
+
if (!Object.keys(COMMANDS).length || !Object.keys(AGENTS).length || !Object.keys(DOCS).length) {
|
|
56
|
+
packageProblems.push("empty-required-bundle");
|
|
57
|
+
}
|
|
58
|
+
return { assets, packageProblems: [...new Set(packageProblems)].slice(0, 20) };
|
|
59
|
+
}
|
|
60
|
+
/** A bounded POSIX-relative destination, valid on every supported platform. */
|
|
61
|
+
export function validAssetDestination(value) {
|
|
62
|
+
if (typeof value !== "string" || !value || value.length > 512 || /[\x00-\x1f\x7f\\:<>"|?*]/.test(value))
|
|
63
|
+
return false;
|
|
64
|
+
return value.split("/").every(s => !!s && s !== "." && s !== ".." && !/[. ]$/.test(s)
|
|
65
|
+
&& !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(s));
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validAssetDestination, WORKFLOW_HOSTS } from "./generated-assets-catalog.js";
|
|
3
|
+
/** Secret-free association token; no absolute customer path is persisted. */
|
|
4
|
+
export function assetRootBinding(canonicalRoot) {
|
|
5
|
+
return createHash("sha256").update(canonicalRoot).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
export function validPackagedAssetInventory(value) {
|
|
8
|
+
if (!value || typeof value !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
const v = value;
|
|
11
|
+
if (v.version !== 1 || typeof v.rootBinding !== "string" || !/^[0-9a-f]{64}$/.test(v.rootBinding)
|
|
12
|
+
|| typeof v.packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(v.packageVersion)
|
|
13
|
+
|| !Array.isArray(v.assets) || v.assets.length > 1000)
|
|
14
|
+
return false;
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (const a of v.assets) {
|
|
17
|
+
if (!a || !validAssetDestination(a.id) || !validAssetDestination(a.destination)
|
|
18
|
+
|| !WORKFLOW_HOSTS.includes(a.host) || seen.has(`${a.host}:${a.id}`))
|
|
19
|
+
return false;
|
|
20
|
+
seen.add(`${a.host}:${a.id}`);
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
export function assetContextConfidence(context) {
|
|
25
|
+
if (!context.initialized || !context.host)
|
|
26
|
+
return { confirmed: false, reason: "unknown-initialized-host" };
|
|
27
|
+
if (!context.corroborated)
|
|
28
|
+
return { confirmed: false, reason: "uncorroborated-project" };
|
|
29
|
+
if (!validPackagedAssetInventory(context.inventory))
|
|
30
|
+
return { confirmed: false, reason: "missing-or-invalid-inventory" };
|
|
31
|
+
if (context.inventory.rootBinding !== assetRootBinding(context.root))
|
|
32
|
+
return { confirmed: false, reason: "root-binding-mismatch" };
|
|
33
|
+
if (!context.inventory.assets.some(a => a.host === context.host))
|
|
34
|
+
return { confirmed: false, reason: "unprovisioned-host" };
|
|
35
|
+
return { confirmed: true, reason: "root-bound-initialized-workflow" };
|
|
36
|
+
}
|
|
37
|
+
/** Explicit root+host selection authorizes current-catalog provisioning. An
|
|
38
|
+
* implicit selection must instead be bound to an exact-cwd inventory. */
|
|
39
|
+
export function authorizeAssetRepair(root, hosts, explicitPair, inventory) {
|
|
40
|
+
if (!hosts.length || hosts.some(h => !WORKFLOW_HOSTS.includes(h)))
|
|
41
|
+
return false;
|
|
42
|
+
if (explicitPair)
|
|
43
|
+
return true;
|
|
44
|
+
return validPackagedAssetInventory(inventory) && inventory.rootBinding === assetRootBinding(root)
|
|
45
|
+
&& hosts.every(host => inventory.assets.some(a => a.host === host));
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
2
|
+
import { inspectPackagedDependency } from "./generated-assets-health.js";
|
|
3
|
+
import { assetFindingRemedy } from "./generated-assets-remedy.js";
|
|
4
|
+
/** Used only by actual local-file consumers. Backend generation tools consume
|
|
5
|
+
* their bundled instructions, not customer slash-command files; they have no
|
|
6
|
+
* declared local packaged dependencies. Agent direct reads are covered in the
|
|
7
|
+
* narrowly scoped canonical command failure branches instead. */
|
|
8
|
+
export async function checkPackagedAssetDependency(root, dependency, fs) {
|
|
9
|
+
const asset = buildPackagedAssetCatalog().assets.find(a => a.id === dependency.assetId && a.hosts.includes(dependency.host));
|
|
10
|
+
if (!asset)
|
|
11
|
+
return { error: "PACKAGED_DEPENDENCY_UNAVAILABLE", status: 409,
|
|
12
|
+
message: "Required dependency is unavailable in the invoking bundle.", dependency: "[unavailable catalog asset]",
|
|
13
|
+
operation: dependency.operation, remedy: "Rebuild or reinstall the package; no consumer file can be reconstructed from this bundle." };
|
|
14
|
+
const finding = await inspectPackagedDependency(root, asset, fs);
|
|
15
|
+
if (finding.status === "present")
|
|
16
|
+
return null;
|
|
17
|
+
return { error: finding.status === "missing" ? "WORKFLOW_ASSET_MISSING" : "WORKFLOW_ASSET_UNUSABLE", status: 409,
|
|
18
|
+
asset_status: finding.status, reason: finding.reason, message: "The requested operation requires an unavailable local workflow asset.",
|
|
19
|
+
dependency: asset.destination, operation: dependency.operation, remedy: assetFindingRemedy(finding, root, dependency.host, fs.platform) };
|
|
20
|
+
}
|
|
21
|
+
export function reviewedRewriteFailure(dependency, reason) {
|
|
22
|
+
return { error: "BAD_REQUEST", status: 400, code: reason === "missing" ? "REVIEWED_FILE_MISSING" : reason === "unsafe" ? "REVIEWED_FILE_UNSAFE" : "REVIEWED_FILE_UNREADABLE",
|
|
23
|
+
asset_status: reason, dependency, operation: "publish_reviewed_attachment",
|
|
24
|
+
message: "The agent-reviewed rewrite is unavailable for publication.",
|
|
25
|
+
remedy: "Recreate the rewrite from eligible source material, review it, and save the reviewed Markdown file before publishing. Canonical retrieval is source material only; it cannot reconstruct a reviewed rewrite. Do not rerun paid generation automatically." };
|
|
26
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
2
|
+
import { inspectPackagedAssets, inspectPackagedDependency } from "./generated-assets-health.js";
|
|
3
|
+
import { assetRootBinding } from "./generated-assets-context.js";
|
|
4
|
+
import { readMcpInstallState } from "./mcp-install-state.js";
|
|
5
|
+
import { assetFindingRemedy } from "./generated-assets-remedy.js";
|
|
6
|
+
export async function collectPackagedAssetDoctor(deps) {
|
|
7
|
+
const lines = ["", "Packaged workflow assets (read-only advisory)"];
|
|
8
|
+
try {
|
|
9
|
+
const root = await deps.realpath(deps.cwd);
|
|
10
|
+
const state = await readMcpInstallState(root, deps);
|
|
11
|
+
const inventory = state.status === "valid" ? state.state.packagedAssets : undefined;
|
|
12
|
+
const catalog = buildPackagedAssetCatalog();
|
|
13
|
+
const health = await inspectPackagedAssets({ catalog, context: { root, host: null,
|
|
14
|
+
initialized: false, corroborated: false, inventory }, fs: deps });
|
|
15
|
+
lines.push(`Historical loss: indeterminate (${state.status === "valid" ? state.state.packagedAssetsStatus ?? health.reason : state.status}). Standalone doctor has no initialized host identity.`);
|
|
16
|
+
if (!inventory || inventory.rootBinding !== assetRootBinding(root)) {
|
|
17
|
+
lines.push(deps.platform === "linux" || (!deps.platform && process.platform === "linux")
|
|
18
|
+
? "For explicit provisioning, select both --project-root and --host (claude, cursor, codex, copilot) with repair-assets; directory presence does not establish an installation."
|
|
19
|
+
: "In-place asset repair is not supported on this platform. Re-run start-tickets for worktrees or the existing Bridge --init/install workflow for project scaffolds; directory presence does not establish an installation.");
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
lines.push(`Invoking package ${health.invokingVersion}; recorded package ${inventory.packageVersion}.`);
|
|
23
|
+
const records = inventory.assets.slice(0, 1000);
|
|
24
|
+
const findings = [];
|
|
25
|
+
const deadline = Date.now() + 1000;
|
|
26
|
+
for (const record of records) {
|
|
27
|
+
if (Date.now() >= deadline) {
|
|
28
|
+
lines.push("Remaining inspection: indeterminate (time budget).");
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const asset = catalog.assets.find(a => a.id === record.id && a.destination === record.destination && a.hosts.includes(record.host));
|
|
32
|
+
if (!asset) {
|
|
33
|
+
lines.push("Package problem: recorded asset unavailable from invoking bundle.");
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Exact physical checks are observations, not claims about deletion history.
|
|
37
|
+
const result = await inspectPackagedDependency(root, asset, deps, deadline - Date.now());
|
|
38
|
+
findings.push(result);
|
|
39
|
+
if (result.status !== "present" && lines.length < 20) {
|
|
40
|
+
lines.push(`${record.host}: ${result.status} (${result.reason}) ${asset.destination}`);
|
|
41
|
+
lines.push(assetFindingRemedy(result, root, record.host, deps.platform));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push(`Observed readable files: ${findings.filter(f => f.status === "present").length}; absent: ${findings.filter(f => f.status === "missing").length}; other unusable: ${findings.filter(f => f.status !== "present" && f.status !== "missing").length}.`);
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [...lines, "Inspection indeterminate: filesystem metadata or inventory unavailable."].join("\n");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VERSION } from "./version.generated.js";
|
|
2
|
+
import { assetContextConfidence } from "./generated-assets-context.js";
|
|
3
|
+
import { inspectAssetPath } from "./generated-assets-paths.js";
|
|
4
|
+
export const MAX_ASSET_INSPECTIONS = 1000;
|
|
5
|
+
/** Inspect only historically provisioned applicable assets; newer bundle entries
|
|
6
|
+
* are reported separately and never diagnosed as consumer loss. */
|
|
7
|
+
export async function inspectPackagedAssets(options) {
|
|
8
|
+
const { catalog, context, fs } = options;
|
|
9
|
+
const confidence = assetContextConfidence(context);
|
|
10
|
+
const result = { ...confidence, findings: [],
|
|
11
|
+
packageProblems: catalog.packageProblems.slice(0, 20), newlyIntroduced: [], invokingVersion: VERSION,
|
|
12
|
+
recordedVersion: context.inventory?.packageVersion };
|
|
13
|
+
if (!confidence.confirmed || !context.inventory)
|
|
14
|
+
return result;
|
|
15
|
+
const applicable = catalog.assets.filter(a => a.hosts.includes(context.host));
|
|
16
|
+
const byId = new Map(applicable.map(a => [a.id, a]));
|
|
17
|
+
const recorded = context.inventory.assets.filter(a => a.host === context.host);
|
|
18
|
+
if (recorded.length > MAX_ASSET_INSPECTIONS)
|
|
19
|
+
return { ...result, confirmed: false, reason: "inspection-limit" };
|
|
20
|
+
const recordedIds = new Set(recorded.map(a => a.id));
|
|
21
|
+
result.newlyIntroduced = applicable.filter(a => !recordedIds.has(a.id)).map(a => a.destination).slice(0, MAX_ASSET_INSPECTIONS);
|
|
22
|
+
const deadline = Date.now() + Math.min(1000, Math.max(1, options.budgetMs ?? 1000));
|
|
23
|
+
// Bounded serial probing; no unbounded Promise fan-out.
|
|
24
|
+
for (const record of recorded) {
|
|
25
|
+
const asset = byId.get(record.id);
|
|
26
|
+
if (!asset || record.destination !== asset.destination) {
|
|
27
|
+
result.packageProblems.push("recorded-asset-unavailable-in-bundle");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const remaining = deadline - Date.now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
return { ...result, confirmed: false, reason: "inspection-timeout" };
|
|
33
|
+
let timer;
|
|
34
|
+
try {
|
|
35
|
+
const finding = await Promise.race([
|
|
36
|
+
inspectPackagedDependency(context.root, asset, fs),
|
|
37
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), remaining); }),
|
|
38
|
+
]);
|
|
39
|
+
if (!finding)
|
|
40
|
+
return { ...result, confirmed: false, reason: "inspection-timeout" };
|
|
41
|
+
result.findings.push(finding);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (timer)
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
result.packageProblems = [...new Set(result.packageProblems)].slice(0, 20);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/** Fresh exact dependency inspection is independent of historical confidence. */
|
|
52
|
+
export async function inspectPackagedDependency(root, asset, fs, budgetMs = 1000) {
|
|
53
|
+
let timer;
|
|
54
|
+
try {
|
|
55
|
+
const finding = await Promise.race([inspectAssetPath(root, asset.destination, fs),
|
|
56
|
+
new Promise(resolve => {
|
|
57
|
+
timer = setTimeout(() => resolve({ status: "indeterminate", reason: "metadata-timeout" }), Math.max(1, Math.min(1000, budgetMs)));
|
|
58
|
+
})]);
|
|
59
|
+
return { id: asset.id, destination: asset.destination, workflows: asset.workflows, ...finding };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (timer)
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WORKFLOW_HOSTS } from "./generated-assets-catalog.js";
|
|
2
|
+
/** Exact identities with repository-owned handshake evidence. Cursor and
|
|
3
|
+
* Copilot VS Code remain unknown until an observed handshake is available.
|
|
4
|
+
* Copilot CLI evidence: learn-tool-gating.ts, CLI 1.0.71 probe (2026-08-26). */
|
|
5
|
+
const IDENTITIES = {
|
|
6
|
+
"claude-code": "claude", codex: "codex", "github-copilot-developer": "copilot",
|
|
7
|
+
};
|
|
8
|
+
export function isWorkflowHost(value) {
|
|
9
|
+
return typeof value === "string" && WORKFLOW_HOSTS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
export function resolveWorkflowHost(clientName, marker) {
|
|
12
|
+
const host = typeof clientName === "string" && Object.hasOwn(IDENTITIES, clientName) ? IDENTITIES[clientName] : null;
|
|
13
|
+
const marked = marker === "claude_code" ? "claude" : marker;
|
|
14
|
+
if (marked != null && (!isWorkflowHost(marked) || (host && host !== marked)))
|
|
15
|
+
return { host: null, reason: "conflicting-or-invalid-identity" };
|
|
16
|
+
return { host, reason: host ? "initialized-client" : "unknown-client" };
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rename, unlink, realpath } from "node:fs/promises";
|
|
2
|
+
import { assetRootBinding } from "./generated-assets-context.js";
|
|
3
|
+
import { recordPackagedAssetInventory } from "./mcp-install-state.js";
|
|
4
|
+
import { VERSION } from "./version.generated.js";
|
|
5
|
+
/** Production scaffold seam. Repair deliberately never calls this writer. */
|
|
6
|
+
export async function recordProvisionedAssets(root, assets, hosts) {
|
|
7
|
+
const canonicalRoot = await realpath(root);
|
|
8
|
+
const result = await recordPackagedAssetInventory(canonicalRoot, {
|
|
9
|
+
version: 1, packageVersion: VERSION, rootBinding: assetRootBinding(canonicalRoot),
|
|
10
|
+
assets: assets.flatMap(a => a.hosts.filter(h => hosts.includes(h)).map(host => ({ id: a.id, destination: a.destination, host }))),
|
|
11
|
+
}, {
|
|
12
|
+
readFile: p => readFile(p, "utf-8"), writeFile: (p, data) => writeFile(p, data, "utf-8"),
|
|
13
|
+
mkdir: async (p, options) => { await mkdir(p, options); }, rename, unlink,
|
|
14
|
+
});
|
|
15
|
+
if (!result.ok)
|
|
16
|
+
throw new Error("Packaged-asset inventory could not be safely recorded.");
|
|
17
|
+
}
|