@bridge_gpt/mcp-server 0.2.67 → 0.2.69
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 +272 -51
- 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 +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- 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/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -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/http-client.js +43 -4
- 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 +1109 -695
- 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 +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- 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/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- 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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { validAssetDestination } from "./generated-assets-catalog.js";
|
|
3
|
+
export async function validateAssetRoot(raw, home, fs) {
|
|
4
|
+
if (!raw || raw.length > 4096 || /[\x00-\x1f\x7f]/.test(raw) || raw.split(/[\\/]/).includes(".."))
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
const canonical = await fs.realpath(path.resolve(raw));
|
|
8
|
+
const info = await fs.lstat(canonical);
|
|
9
|
+
if (!info.isDirectory() || info.isSymbolicLink() || canonical === path.parse(canonical).root
|
|
10
|
+
|| canonical === await fs.realpath(home))
|
|
11
|
+
return null;
|
|
12
|
+
// Validate canonical ancestry; a symlink in the caller's spelling is resolved
|
|
13
|
+
// once, while later operations use only the printed canonical root.
|
|
14
|
+
let current = path.parse(canonical).root;
|
|
15
|
+
for (const segment of path.relative(current, canonical).split(path.sep)) {
|
|
16
|
+
current = path.join(current, segment);
|
|
17
|
+
const parent = await fs.lstat(current);
|
|
18
|
+
if (!parent.isDirectory() || parent.isSymbolicLink())
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return canonical;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function filesystemErrorCode(error) {
|
|
28
|
+
return error && typeof error === "object" ? error.code : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Metadata/access only. No asset-body reads and no filesystem mutation. */
|
|
31
|
+
export async function inspectAssetPath(root, destination, fs) {
|
|
32
|
+
const api = fs.platform === "win32" ? path.win32 : path;
|
|
33
|
+
if (root.length > 4096 || !api.isAbsolute(root) || /[\x00-\x1f\x7f]/.test(root) || !validAssetDestination(destination)) {
|
|
34
|
+
return { status: "indeterminate", reason: "unsafe-path" };
|
|
35
|
+
}
|
|
36
|
+
let current = api.parse(root).root;
|
|
37
|
+
const rootSegments = api.relative(current, root).split(api.sep).filter(Boolean);
|
|
38
|
+
const segments = [...rootSegments, ...destination.split("/")];
|
|
39
|
+
if (segments.length > 256)
|
|
40
|
+
return { status: "indeterminate", reason: "path-limit" };
|
|
41
|
+
for (let i = 0; i < segments.length; i++) {
|
|
42
|
+
current = api.join(current, segments[i]);
|
|
43
|
+
try {
|
|
44
|
+
const info = await fs.lstat(current);
|
|
45
|
+
if (info.isSymbolicLink())
|
|
46
|
+
return { status: "indeterminate", reason: "unsafe-link" };
|
|
47
|
+
const final = i === segments.length - 1;
|
|
48
|
+
if (final ? !info.isFile() : !info.isDirectory())
|
|
49
|
+
return { status: "unreadable", reason: "non-file-or-parent" };
|
|
50
|
+
if (final)
|
|
51
|
+
await fs.access(current);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (filesystemErrorCode(error) === "ENOENT" && i >= rootSegments.length)
|
|
55
|
+
return { status: "missing", reason: "absent" };
|
|
56
|
+
return { status: "unreadable", reason: filesystemErrorCode(error) === "EACCES" ? "permission-denied" : "metadata-unavailable" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { status: "present", reason: "readable-regular-file" };
|
|
60
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open, mkdir, link, unlink, lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { validAssetDestination } from "./generated-assets-catalog.js";
|
|
6
|
+
/** Linux procfs provides descriptor-relative paths without adding a native
|
|
7
|
+
* dependency. Other platforms lack this qualified adapter and fail closed. */
|
|
8
|
+
export function createConfinedAssetPublisher(platform = process.platform) {
|
|
9
|
+
if (platform !== "linux")
|
|
10
|
+
return undefined;
|
|
11
|
+
return { async prepare(root, destination, content) {
|
|
12
|
+
if (!path.isAbsolute(root) || !validAssetDestination(destination) || path.normalize(root) !== root)
|
|
13
|
+
throw new Error("Unsafe asset path.");
|
|
14
|
+
const handles = [];
|
|
15
|
+
let temporary;
|
|
16
|
+
let parent;
|
|
17
|
+
let final;
|
|
18
|
+
let temporaryOwned = false;
|
|
19
|
+
const flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
|
|
20
|
+
const descriptorPath = (handle) => `/proc/self/fd/${handle.fd}`;
|
|
21
|
+
const assertParents = async () => {
|
|
22
|
+
for (const { handle, expected } of handles) {
|
|
23
|
+
const anchor = descriptorPath(handle);
|
|
24
|
+
if (await realpath(anchor) !== expected)
|
|
25
|
+
throw new Error("Parent confinement changed.");
|
|
26
|
+
const current = await lstat(expected);
|
|
27
|
+
const held = await handle.stat();
|
|
28
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== held.dev || current.ino !== held.ino)
|
|
29
|
+
throw new Error("Parent confinement changed.");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const closeHandles = async () => {
|
|
33
|
+
let failed = false;
|
|
34
|
+
for (const { handle } of handles.reverse()) {
|
|
35
|
+
try {
|
|
36
|
+
await handle.close();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
failed = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (failed)
|
|
43
|
+
throw new Error("Descriptor cleanup failed.");
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
// Open the root from / through O_NOFOLLOW directory handles. A substituted
|
|
47
|
+
// ancestor never redirects a subsequent lookup through the held descriptor.
|
|
48
|
+
let handle = await open("/", flags);
|
|
49
|
+
handles.push({ handle, expected: "/" });
|
|
50
|
+
let expected = "/";
|
|
51
|
+
const rootSegments = root.split("/").filter(Boolean);
|
|
52
|
+
const assetSegments = destination.split("/");
|
|
53
|
+
const parentSegments = assetSegments.slice(0, -1);
|
|
54
|
+
for (const [index, segment] of [...rootSegments, ...parentSegments].entries()) {
|
|
55
|
+
await assertParents();
|
|
56
|
+
const child = `${descriptorPath(handle)}/${segment}`;
|
|
57
|
+
if (index >= rootSegments.length) {
|
|
58
|
+
try {
|
|
59
|
+
await mkdir(child);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== "EEXIST")
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
handle = await open(child, flags);
|
|
67
|
+
expected = path.join(expected, segment);
|
|
68
|
+
handles.push({ handle, expected });
|
|
69
|
+
}
|
|
70
|
+
await assertParents();
|
|
71
|
+
parent = descriptorPath(handle);
|
|
72
|
+
temporary = `${parent}/.bridge-repair-${randomUUID()}.tmp`;
|
|
73
|
+
final = `${parent}/${assetSegments.at(-1)}`;
|
|
74
|
+
const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
|
|
75
|
+
temporaryOwned = true;
|
|
76
|
+
try {
|
|
77
|
+
await file.writeFile(content, "utf-8");
|
|
78
|
+
await file.sync();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await file.close();
|
|
82
|
+
}
|
|
83
|
+
await assertParents();
|
|
84
|
+
return {
|
|
85
|
+
async publish() {
|
|
86
|
+
await assertParents();
|
|
87
|
+
await link(temporary, final);
|
|
88
|
+
// Descriptor-relative publication cannot follow a replacement link.
|
|
89
|
+
// Report displaced ancestry as incomplete; never unlink a final path,
|
|
90
|
+
// which another creator may already have replaced.
|
|
91
|
+
await assertParents();
|
|
92
|
+
},
|
|
93
|
+
async cleanup() {
|
|
94
|
+
let failed = false;
|
|
95
|
+
try {
|
|
96
|
+
await unlink(temporary);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
failed = true;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await closeHandles();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
failed = true;
|
|
106
|
+
}
|
|
107
|
+
if (failed)
|
|
108
|
+
throw new Error("Owned temporary cleanup failed.");
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
let cleanupFailed = false;
|
|
114
|
+
if (temporary && temporaryOwned) {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(temporary);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
cleanupFailed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await closeHandles();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
cleanupFailed = true;
|
|
127
|
+
}
|
|
128
|
+
if (cleanupFailed)
|
|
129
|
+
throw Object.assign(new Error("Owned temporary cleanup failed."), { code: "ASSET_TEMP_CLEANUP_FAILED" });
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
2
|
+
import { VERSION } from "./version.generated.js";
|
|
3
|
+
import { isWorkflowHost } from "./generated-assets-host.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
6
|
+
export function packagedAssetRemedy(root, host, version = VERSION, platform = process.platform) {
|
|
7
|
+
if (!(platform === "win32" ? path.win32 : path).isAbsolute(root) || root.length > 4096 || /[\x00-\x1f\x7f]/.test(root) || !isWorkflowHost(host)
|
|
8
|
+
|| !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version))
|
|
9
|
+
throw new Error("Invalid repair context.");
|
|
10
|
+
if (platform !== "linux")
|
|
11
|
+
return { args: [], command: "In-place asset repair is not supported on this platform. Re-run start-tickets to provision worktree assets; for project scaffolds, re-run the existing Bridge --init/install workflow from the established project root.",
|
|
12
|
+
guidance: "Worktree provisioning preserves existing workflow files. Project init/install follows its existing refresh behavior. Refresh/reconnect the host, then run read-only doctor." };
|
|
13
|
+
const args = ["-y", `${MCP_PACKAGE_NAME}@${version}`, "repair-assets", "--project-root", root, "--host", host];
|
|
14
|
+
return { args, command: `npx -y ${MCP_PACKAGE_NAME}@${version} repair-assets --project-root ${quote(root)} --host ${host}`,
|
|
15
|
+
guidance: "Run from the established project root. Refresh/reconnect the host, then run read-only doctor. If discovery still disagrees with filesystem health, consult host support." };
|
|
16
|
+
}
|
|
17
|
+
export function assetFindingRemedy(finding, root, host, platform = process.platform) {
|
|
18
|
+
if (finding.status === "missing") {
|
|
19
|
+
const remedy = packagedAssetRemedy(root, host, VERSION, platform);
|
|
20
|
+
return `${remedy.command}\n${remedy.guidance}`;
|
|
21
|
+
}
|
|
22
|
+
if (finding.status === "present")
|
|
23
|
+
return "No filesystem repair is needed.";
|
|
24
|
+
return "Resolve permissions, non-file destinations, or unsafe links before retrying. Repair preserves existing files and refuses unsafe destinations.";
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildPackagedAssetCatalog, validAssetDestination } from "./generated-assets-catalog.js";
|
|
2
|
+
import { authorizeAssetRepair } from "./generated-assets-context.js";
|
|
3
|
+
import { inspectAssetPath, filesystemErrorCode } from "./generated-assets-paths.js";
|
|
4
|
+
export async function repairPackagedAssets(options) {
|
|
5
|
+
const catalog = options.catalog ?? buildPackagedAssetCatalog();
|
|
6
|
+
const result = { complete: false, findings: [], packageProblems: catalog.packageProblems.slice(0, 20), cleanupFailures: [] };
|
|
7
|
+
if (!authorizeAssetRepair(options.root, options.hosts, options.explicitPair, options.inventory)) {
|
|
8
|
+
result.packageProblems.push("repair-not-authorized");
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
const requested = new Map(catalog.assets.filter(a => a.hosts.some(h => options.hosts.includes(h))).map(a => [a.destination, a]));
|
|
12
|
+
if (!requested.size || requested.size > 1000) {
|
|
13
|
+
result.packageProblems.push("invalid-requested-catalog");
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
for (const record of options.inventory?.assets ?? []) {
|
|
17
|
+
if (options.hosts.includes(record.host) && ![...requested.values()].some(a => a.id === record.id && a.destination === record.destination)) {
|
|
18
|
+
result.packageProblems.push("recorded-asset-unavailable-in-bundle");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const asset of requested.values()) {
|
|
22
|
+
if (!validAssetDestination(asset.destination)) {
|
|
23
|
+
result.findings.push({ destination: "[invalid destination]", status: "failed", reason: "unsafe-path" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const finding = await inspectAssetPath(options.root, asset.destination, options.fs);
|
|
27
|
+
if (finding.status === "present") {
|
|
28
|
+
result.findings.push({ destination: asset.destination, status: "preserved", reason: "existing-readable-file" });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (finding.status !== "missing") {
|
|
32
|
+
result.findings.push({ destination: asset.destination, status: finding.status === "unreadable" ? "unreadable" : "failed", reason: finding.reason });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!options.publisher) {
|
|
36
|
+
result.findings.push({ destination: asset.destination, status: "failed", reason: "confined-publication-unsupported" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let prepared;
|
|
40
|
+
try {
|
|
41
|
+
prepared = await options.publisher.prepare(options.root, asset.destination, asset.content);
|
|
42
|
+
await prepared.publish();
|
|
43
|
+
result.findings.push({ destination: asset.destination, status: "restored", reason: "exclusive-publication" });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (filesystemErrorCode(error) === "ASSET_TEMP_CLEANUP_FAILED")
|
|
47
|
+
result.cleanupFailures.push(asset.destination);
|
|
48
|
+
if (filesystemErrorCode(error) === "EEXIST") {
|
|
49
|
+
const winner = await inspectAssetPath(options.root, asset.destination, options.fs);
|
|
50
|
+
result.findings.push({ destination: asset.destination, status: winner.status === "present" ? "preserved" : "failed",
|
|
51
|
+
reason: winner.status === "present" ? "concurrent-creation" : "concurrent-destination-unusable" });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
result.findings.push({ destination: asset.destination, status: "failed", reason: "publication-or-confinement-failed" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (prepared) {
|
|
59
|
+
try {
|
|
60
|
+
await prepared.cleanup();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
result.cleanupFailures.push(asset.destination);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
result.packageProblems = [...new Set(result.packageProblems)].slice(0, 20);
|
|
69
|
+
result.complete = !result.packageProblems.length && !result.cleanupFailures.length
|
|
70
|
+
&& result.findings.every(f => f.status === "restored" || f.status === "preserved");
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const ASSET_DISCOVERY_WAIT_MS = 1000;
|
|
2
|
+
export const ASSET_HEALTH_CACHE_MS = 15000;
|
|
3
|
+
export function createAssetHealthSession(options) {
|
|
4
|
+
const now = options.now ?? Date.now;
|
|
5
|
+
const setTimer = options.setTimer ?? setTimeout;
|
|
6
|
+
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
7
|
+
let snapshot = null;
|
|
8
|
+
let inspectedAt = -Infinity;
|
|
9
|
+
let flight;
|
|
10
|
+
let servedSignature;
|
|
11
|
+
let generation = 0;
|
|
12
|
+
let closed = false;
|
|
13
|
+
let timer;
|
|
14
|
+
const signature = (advice) => advice ? JSON.stringify([advice.host, advice.command, advice.missingCount, advice.paths, advice.capabilities]) : "";
|
|
15
|
+
const refresh = () => {
|
|
16
|
+
if (closed)
|
|
17
|
+
return Promise.resolve(null);
|
|
18
|
+
if (flight)
|
|
19
|
+
return flight;
|
|
20
|
+
if (now() - inspectedAt < ASSET_HEALTH_CACHE_MS)
|
|
21
|
+
return Promise.resolve(snapshot);
|
|
22
|
+
const epoch = generation;
|
|
23
|
+
const work = Promise.resolve().then(options.inspect).catch(() => null).then(advice => {
|
|
24
|
+
if (closed || generation !== epoch)
|
|
25
|
+
return null;
|
|
26
|
+
snapshot = advice;
|
|
27
|
+
inspectedAt = now();
|
|
28
|
+
const next = signature(advice);
|
|
29
|
+
if (servedSignature !== undefined && servedSignature !== next) {
|
|
30
|
+
servedSignature = next;
|
|
31
|
+
try {
|
|
32
|
+
void Promise.resolve(options.notify()).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
catch { /* fail open */ }
|
|
35
|
+
}
|
|
36
|
+
return advice;
|
|
37
|
+
}).finally(() => { if (flight === work)
|
|
38
|
+
flight = undefined; });
|
|
39
|
+
flight = work;
|
|
40
|
+
return work;
|
|
41
|
+
};
|
|
42
|
+
const schedule = () => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
timer = setTimer(() => { void refresh().finally(schedule); }, ASSET_HEALTH_CACHE_MS);
|
|
46
|
+
timer.unref?.();
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
start() { if (!timer && !closed) {
|
|
50
|
+
void refresh();
|
|
51
|
+
schedule();
|
|
52
|
+
} },
|
|
53
|
+
async getAdvisory() {
|
|
54
|
+
let timeout;
|
|
55
|
+
try {
|
|
56
|
+
const advice = await Promise.race([refresh(), new Promise(resolve => {
|
|
57
|
+
timeout = setTimer(() => resolve(null), ASSET_DISCOVERY_WAIT_MS);
|
|
58
|
+
})]);
|
|
59
|
+
servedSignature = signature(advice);
|
|
60
|
+
return advice;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (timeout)
|
|
64
|
+
clearTimer(timeout);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
invalidate() { generation++; snapshot = null; inspectedAt = -Infinity; flight = undefined; },
|
|
68
|
+
close() { closed = true; generation++; snapshot = null; if (timer)
|
|
69
|
+
clearTimer(timer); },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assetRootBinding } from "./generated-assets-context.js";
|
|
3
|
+
export const asset = (destination = ".claude/commands/implement-ticket.md") => ({
|
|
4
|
+
id: destination, destination, content: "complete packaged bytes", kind: "command", hosts: ["claude"],
|
|
5
|
+
workflows: ["implement-ticket"], classification: "entrypoint",
|
|
6
|
+
});
|
|
7
|
+
export const catalog = (assets = [asset()]) => ({ assets, packageProblems: [] });
|
|
8
|
+
export const context = (assets = [asset()]) => ({ root: "/repo", host: "claude",
|
|
9
|
+
initialized: true, corroborated: true, inventory: { version: 1, packageVersion: "0.1.0", rootBinding: assetRootBinding("/repo"),
|
|
10
|
+
assets: assets.map(a => ({ id: a.id, destination: a.destination, host: "claude" })) } });
|
|
11
|
+
export function metadataFixture(assets = [asset()]) {
|
|
12
|
+
const entries = new Map([["/repo", "dir"]]);
|
|
13
|
+
for (const a of assets) {
|
|
14
|
+
let parent = path.posix.dirname(`/repo/${a.destination}`);
|
|
15
|
+
while (parent !== "/") {
|
|
16
|
+
entries.set(parent, "dir");
|
|
17
|
+
parent = path.posix.dirname(parent);
|
|
18
|
+
}
|
|
19
|
+
entries.set(`/repo/${a.destination}`, "file");
|
|
20
|
+
}
|
|
21
|
+
const operations = [];
|
|
22
|
+
const fs = {
|
|
23
|
+
platform: "linux",
|
|
24
|
+
lstat: async (p) => {
|
|
25
|
+
operations.push(`lstat:${p}`);
|
|
26
|
+
const type = entries.get(p);
|
|
27
|
+
if (!type || type === "EACCES")
|
|
28
|
+
throw Object.assign(new Error("SENTINEL_SECRET_RAW_EXCEPTION"), { code: type ?? "ENOENT" });
|
|
29
|
+
return { isFile: () => type === "file", isDirectory: () => type === "dir", isSymbolicLink: () => type === "link" };
|
|
30
|
+
},
|
|
31
|
+
access: async (p) => { operations.push(`access:${p}`); if (!entries.has(p))
|
|
32
|
+
throw Object.assign(new Error(), { code: "ENOENT" }); },
|
|
33
|
+
};
|
|
34
|
+
return { entries, operations, fs };
|
|
35
|
+
}
|