@hasna/instructions 0.7.2 → 0.7.5
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 +13 -0
- package/dist/chunks/{apply-wj7wh5km.js → apply-1cpkx082.js} +2 -2
- package/dist/chunks/{apply-ps6jjt6k.js → apply-xraar7p0.js} +2 -2
- package/dist/chunks/{index-pq18fdbj.js → index-16j8x10s.js} +64 -3
- package/dist/chunks/{index-n6nh6f5r.js → index-6qfy0542.js} +193 -15
- package/dist/chunks/{index-ek15201v.js → index-rvb4znxa.js} +2 -2
- package/dist/chunks/{index-ape24zc0.js → index-src5t3my.js} +1 -1
- package/dist/chunks/{index-y8410ve0.js → index-x0v4m19f.js} +1 -1
- package/dist/chunks/{index-18j7x3xw.js → index-ysnp8y58.js} +2 -2
- package/dist/chunks/{local-1c1qxzvx.js → local-66fck74w.js} +1 -1
- package/dist/chunks/{local-hc67bynj.js → local-jqfx3kbq.js} +1 -1
- package/dist/chunks/{sync-azd9t4p8.js → sync-8763n4sc.js} +3 -3
- package/dist/chunks/{sync-12m3vs6g.js → sync-8ca3ew84.js} +3 -3
- package/dist/cli/index.js +526 -554
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +727 -587
- package/dist/lib/asset-plan.d.ts +4 -1
- package/dist/lib/asset-plan.d.ts.map +1 -1
- package/dist/lib/harness-discovery.d.ts +52 -0
- package/dist/lib/harness-discovery.d.ts.map +1 -0
- package/dist/lib/machine.d.ts.map +1 -1
- package/dist/lib/managed-skill-runtimes.d.ts +12 -28
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/session-apply.d.ts +11 -0
- package/dist/lib/session-apply.d.ts.map +1 -1
- package/dist/lib/session-refresh.d.ts.map +1 -1
- package/dist/lib/session-render.d.ts +7 -0
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/mcp/index.js +7 -7
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +153 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +77 -0
- package/dist/server/openapi.d.ts +27 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
syncKnown,
|
|
10
10
|
syncProject,
|
|
11
11
|
syncToDisk
|
|
12
|
-
} from "../chunks/index-
|
|
12
|
+
} from "../chunks/index-ysnp8y58.js";
|
|
13
13
|
import {
|
|
14
14
|
applyConfigsWithReport,
|
|
15
15
|
compactPathForConfigHome,
|
|
16
16
|
expandPath,
|
|
17
17
|
normalizeTargetPath,
|
|
18
18
|
sessionRenderOwnsPath
|
|
19
|
-
} from "../chunks/index-
|
|
19
|
+
} from "../chunks/index-x0v4m19f.js";
|
|
20
20
|
import {
|
|
21
21
|
CODEWITH_NATIVE_IMPORTS_ENV,
|
|
22
22
|
CloudConfigStore,
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
truncateText,
|
|
70
70
|
withProjectContextSessionGuard,
|
|
71
71
|
writeProjectContextCoordinatedFile
|
|
72
|
-
} from "../chunks/index-
|
|
72
|
+
} from "../chunks/index-6qfy0542.js";
|
|
73
73
|
import {
|
|
74
74
|
ConfigNotFoundError,
|
|
75
75
|
INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
@@ -3795,9 +3795,162 @@ var {
|
|
|
3795
3795
|
|
|
3796
3796
|
// src/cli/index.tsx
|
|
3797
3797
|
import chalk from "chalk";
|
|
3798
|
-
import { existsSync as
|
|
3799
|
-
import { homedir as
|
|
3800
|
-
|
|
3798
|
+
import { existsSync as existsSync10, lstatSync as lstatSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readSync, renameSync, unlinkSync, writeFileSync as writeFileSync4, writeSync } from "fs";
|
|
3799
|
+
import { homedir as homedir5 } from "os";
|
|
3800
|
+
|
|
3801
|
+
// src/lib/harness-discovery.ts
|
|
3802
|
+
import { accessSync, constants, lstatSync, realpathSync, statSync } from "fs";
|
|
3803
|
+
import { homedir as homedir2 } from "os";
|
|
3804
|
+
import { delimiter, dirname, isAbsolute, join as join3, resolve as resolve2 } from "path";
|
|
3805
|
+
var HARNESS_DISCOVERY_SCHEMA = "hasna.instructions.harness-discovery/v1";
|
|
3806
|
+
var DISCOVERABLE_HARNESSES = ["claude", "codex", "opencode", "sumi"];
|
|
3807
|
+
function pathInput(value, home) {
|
|
3808
|
+
if (!value || value !== value.trim() || /[\x00-\x1f\x7f]/.test(value))
|
|
3809
|
+
throw new Error("Harness paths must be nonempty and contain no surrounding whitespace or control characters.");
|
|
3810
|
+
if (value.split("/").some((segment) => segment === "." || segment === ".."))
|
|
3811
|
+
throw new Error("Harness paths must not contain dot segments; normalizing them can change a symlink destination.");
|
|
3812
|
+
let path = value;
|
|
3813
|
+
if (home) {
|
|
3814
|
+
for (const prefix of ["~", "{{HOME}}", "{{HOME_DIR}}", "${HOME}"]) {
|
|
3815
|
+
if (path === prefix)
|
|
3816
|
+
path = home;
|
|
3817
|
+
else if (path.startsWith(`${prefix}/`))
|
|
3818
|
+
path = join3(home, path.slice(prefix.length + 1));
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (!isAbsolute(path))
|
|
3822
|
+
throw new Error("Harness paths must be absolute or explicitly relative to the owner home (~/ or {{HOME_DIR}}/).");
|
|
3823
|
+
return resolve2(path);
|
|
3824
|
+
}
|
|
3825
|
+
function observe(path) {
|
|
3826
|
+
let symlink = false;
|
|
3827
|
+
let viaSymlink = false;
|
|
3828
|
+
for (let parent = dirname(path);parent !== dirname(parent); parent = dirname(parent)) {
|
|
3829
|
+
try {
|
|
3830
|
+
if (lstatSync(parent).isSymbolicLink()) {
|
|
3831
|
+
viaSymlink = true;
|
|
3832
|
+
break;
|
|
3833
|
+
}
|
|
3834
|
+
} catch {}
|
|
3835
|
+
}
|
|
3836
|
+
try {
|
|
3837
|
+
symlink = lstatSync(path).isSymbolicLink();
|
|
3838
|
+
const stat = statSync(path);
|
|
3839
|
+
return { path, state: "present", realPath: realpathSync(path), symlink, viaSymlink: viaSymlink || symlink, type: stat.isFile() ? "file" : stat.isDirectory() ? "directory" : "other" };
|
|
3840
|
+
} catch (error) {
|
|
3841
|
+
const missing = error.code === "ENOENT";
|
|
3842
|
+
return { path, state: missing ? symlink ? "dangling-symlink" : "missing" : "unreadable", realPath: null, symlink, viaSymlink: viaSymlink || symlink, type: null };
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
function executableAt(path) {
|
|
3846
|
+
const observation = observe(path);
|
|
3847
|
+
if (observation.type !== "file")
|
|
3848
|
+
return null;
|
|
3849
|
+
try {
|
|
3850
|
+
accessSync(path, constants.X_OK);
|
|
3851
|
+
return observation;
|
|
3852
|
+
} catch {
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
function configSelection(tool, home, env, override) {
|
|
3857
|
+
if (override !== undefined)
|
|
3858
|
+
return { path: pathInput(override, home), source: "override" };
|
|
3859
|
+
const key = { claude: "CLAUDE_CONFIG_DIR", codex: "CODEX_HOME", opencode: "OPENCODE_CONFIG_DIR", sumi: "SUMI_CONFIG_DIR" }[tool];
|
|
3860
|
+
const nativePath = (value, selector) => {
|
|
3861
|
+
try {
|
|
3862
|
+
return pathInput(value);
|
|
3863
|
+
} catch {
|
|
3864
|
+
throw new Error(`${selector} is present but is not an unambiguous absolute native path; supply the reviewed runtime path explicitly.`);
|
|
3865
|
+
}
|
|
3866
|
+
};
|
|
3867
|
+
if (env[key] !== undefined && !(tool === "codex" && env[key] === ""))
|
|
3868
|
+
return { path: nativePath(env[key], key), source: key };
|
|
3869
|
+
if (tool === "claude" || tool === "codex")
|
|
3870
|
+
return { path: join3(home, `.${tool}`), source: "native-default" };
|
|
3871
|
+
if (env.XDG_CONFIG_HOME !== undefined)
|
|
3872
|
+
return { path: join3(nativePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), tool), source: "XDG_CONFIG_HOME" };
|
|
3873
|
+
if (tool === "sumi") {
|
|
3874
|
+
if (env.SUMI_HOME !== undefined)
|
|
3875
|
+
return { path: join3(nativePath(env.SUMI_HOME, "SUMI_HOME"), "config"), source: "SUMI_HOME" };
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
return { path: join3(home, ".config", tool), source: "native-default" };
|
|
3879
|
+
}
|
|
3880
|
+
function discoverHarnesses(options = {}) {
|
|
3881
|
+
const env = options.env ?? process.env;
|
|
3882
|
+
const home = pathInput(options.ownerHome ?? env.HOME ?? env.USERPROFILE ?? homedir2());
|
|
3883
|
+
const project = options.projectRoot === undefined ? null : observe(pathInput(options.projectRoot, home));
|
|
3884
|
+
const variables = { HOME_DIR: home };
|
|
3885
|
+
if (project)
|
|
3886
|
+
variables.PROJECT_ROOT = project.path;
|
|
3887
|
+
const tools = DISCOVERABLE_HARNESSES.map((tool) => {
|
|
3888
|
+
const diagnostics = [];
|
|
3889
|
+
const override = options.overrides?.[tool];
|
|
3890
|
+
let executable = null;
|
|
3891
|
+
let executableSource = null;
|
|
3892
|
+
if (override?.executable !== undefined) {
|
|
3893
|
+
executable = executableAt(pathInput(override.executable, home));
|
|
3894
|
+
executableSource = "override";
|
|
3895
|
+
if (!executable)
|
|
3896
|
+
diagnostics.push("Explicit executable is missing, unreadable, not a file, or not executable; PATH fallback is disabled.");
|
|
3897
|
+
} else {
|
|
3898
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
3899
|
+
if (!isAbsolute(directory)) {
|
|
3900
|
+
const warning = "Relative or empty PATH entries were ignored; this is an absolute-path candidate inventory, not proof of the launcher's selected executable.";
|
|
3901
|
+
if (!diagnostics.includes(warning))
|
|
3902
|
+
diagnostics.push(warning);
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
let validated;
|
|
3906
|
+
try {
|
|
3907
|
+
validated = pathInput(directory);
|
|
3908
|
+
} catch {
|
|
3909
|
+
diagnostics.push("An ambiguous PATH directory was ignored; use an explicit executable override after reviewing its native path.");
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
executable = executableAt(join3(validated, tool));
|
|
3913
|
+
if (executable) {
|
|
3914
|
+
executableSource = "PATH";
|
|
3915
|
+
break;
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
let selected = null;
|
|
3920
|
+
let configError = false;
|
|
3921
|
+
try {
|
|
3922
|
+
selected = configSelection(tool, home, env, override?.configDir);
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
if (override?.configDir !== undefined)
|
|
3925
|
+
throw error;
|
|
3926
|
+
configError = true;
|
|
3927
|
+
diagnostics.push(`Config root unresolved: ${error.message}`);
|
|
3928
|
+
}
|
|
3929
|
+
const config = selected ? { ...observe(selected.path), source: selected.source } : null;
|
|
3930
|
+
const filename = tool === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
3931
|
+
const globalPrompt = config ? observe(join3(config.path, filename)) : null;
|
|
3932
|
+
const promptOverrides = tool === "codex" ? [...new Set([config?.path, project?.path].filter((path) => !!path))].map((path) => observe(join3(path, "AGENTS.override.md"))) : [];
|
|
3933
|
+
if (!config && !configError)
|
|
3934
|
+
diagnostics.push("Config root unresolved: supply the actual `sumi debug paths config` result as a configDir override; no runtime or migration was invoked.");
|
|
3935
|
+
if (executable) {
|
|
3936
|
+
variables[`${tool.toUpperCase()}_EXECUTABLE`] = executable.path;
|
|
3937
|
+
if (config)
|
|
3938
|
+
variables[`${tool.toUpperCase()}_CONFIG_DIR`] = config.path;
|
|
3939
|
+
}
|
|
3940
|
+
const linkedGlobal = !!globalPrompt?.viaSymlink || !!config?.viaSymlink;
|
|
3941
|
+
const hasOverride = promptOverrides.some((path) => path.state !== "missing" || path.viaSymlink);
|
|
3942
|
+
const scopeReviewRequired = linkedGlobal || hasOverride;
|
|
3943
|
+
if (linkedGlobal)
|
|
3944
|
+
diagnostics.push("Global config or prompt resolves through a symlink; preserve its lexical path and review the destination scope before planning writes.");
|
|
3945
|
+
if (hasOverride)
|
|
3946
|
+
diagnostics.push("A Codex AGENTS.override.md candidate may take precedence; review its native loader behavior and scope before applying AGENTS.md.");
|
|
3947
|
+
return { tool, status: executable ? "executable-found" : "absent", executable, executableSource, versionEvidence: "not-probed", config, globalPrompt, projectPrompt: project ? observe(join3(project.path, filename)) : null, promptOverrides, scopeReviewRequired, diagnostics };
|
|
3948
|
+
});
|
|
3949
|
+
return { schema: HARNESS_DISCOVERY_SCHEMA, ownerHome: observe(home), projectRoot: project, tools, templateVariables: variables };
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
// src/cli/index.tsx
|
|
3953
|
+
import { basename as basename2, dirname as dirname7, join as join12, resolve as resolve8 } from "path";
|
|
3801
3954
|
|
|
3802
3955
|
// src/lib/config-target-identity.ts
|
|
3803
3956
|
function findConfigsByTargetPath(configs, targetPath) {
|
|
@@ -3838,7 +3991,7 @@ function findDuplicateTargetPathGroups(configs) {
|
|
|
3838
3991
|
// src/lib/export.ts
|
|
3839
3992
|
import { createHash } from "crypto";
|
|
3840
3993
|
import { existsSync as existsSync3, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3841
|
-
import { dirname, join as
|
|
3994
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
3842
3995
|
import { tmpdir } from "os";
|
|
3843
3996
|
var ARCHIVE_EXCLUSIONS = [
|
|
3844
3997
|
{
|
|
@@ -4174,13 +4327,13 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
4174
4327
|
integrity,
|
|
4175
4328
|
exclusions: ARCHIVE_EXCLUSIONS
|
|
4176
4329
|
};
|
|
4177
|
-
const absOutput =
|
|
4178
|
-
mkdirSync(
|
|
4179
|
-
const stagingDir = mkdtempSync(
|
|
4330
|
+
const absOutput = resolve3(outputPath);
|
|
4331
|
+
mkdirSync(dirname2(absOutput), { recursive: true });
|
|
4332
|
+
const stagingDir = mkdtempSync(join5(tmpdir(), "instructions-domain-export-"));
|
|
4180
4333
|
try {
|
|
4181
|
-
writeFileSync(
|
|
4334
|
+
writeFileSync(join5(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4182
4335
|
`, "utf-8");
|
|
4183
|
-
writeFileSync(
|
|
4336
|
+
writeFileSync(join5(stagingDir, "domain.json"), payload, "utf-8");
|
|
4184
4337
|
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
|
|
4185
4338
|
stdout: "pipe",
|
|
4186
4339
|
stderr: "pipe"
|
|
@@ -4199,7 +4352,7 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
4199
4352
|
|
|
4200
4353
|
// src/lib/import.ts
|
|
4201
4354
|
import { createHash as createHash2 } from "crypto";
|
|
4202
|
-
import { resolve as
|
|
4355
|
+
import { resolve as resolve4 } from "path";
|
|
4203
4356
|
function compareText2(left, right) {
|
|
4204
4357
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
4205
4358
|
}
|
|
@@ -4487,7 +4640,7 @@ async function importV1(reader, manifest, store, conflict) {
|
|
|
4487
4640
|
async function importConfigs(bundlePath, opts = {}) {
|
|
4488
4641
|
const store = opts.store ?? resolveConfigStore();
|
|
4489
4642
|
const conflict = opts.conflict ?? "skip";
|
|
4490
|
-
const reader = await openArchive(
|
|
4643
|
+
const reader = await openArchive(resolve4(bundlePath));
|
|
4491
4644
|
const manifestText = await reader.read("manifest.json");
|
|
4492
4645
|
let manifest;
|
|
4493
4646
|
try {
|
|
@@ -4513,13 +4666,13 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
4513
4666
|
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
4514
4667
|
import {
|
|
4515
4668
|
existsSync as existsSync5,
|
|
4516
|
-
lstatSync,
|
|
4669
|
+
lstatSync as lstatSync2,
|
|
4517
4670
|
mkdirSync as mkdirSync2,
|
|
4518
4671
|
readFileSync,
|
|
4519
4672
|
readdirSync,
|
|
4520
|
-
statSync
|
|
4673
|
+
statSync as statSync2
|
|
4521
4674
|
} from "fs";
|
|
4522
|
-
import { dirname as
|
|
4675
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, parse, posix, relative, resolve as resolve5 } from "path";
|
|
4523
4676
|
class SessionApplyError extends Error {
|
|
4524
4677
|
constructor(message) {
|
|
4525
4678
|
super(message);
|
|
@@ -4549,13 +4702,15 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4549
4702
|
const adoptedHashes = new Map(adoptions.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4550
4703
|
const reconciliations = validateFileReconciliations(plan, targetHome, previousManifest, options);
|
|
4551
4704
|
const reconciledHashes = new Map(reconciliations.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4552
|
-
const
|
|
4705
|
+
const retirements = validateFileRetirements(plan, targetHome, previousManifest, options);
|
|
4706
|
+
const retiredHashes = new Map(retirements.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4707
|
+
const manifestFile = manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations, retirements);
|
|
4553
4708
|
const files = [...payloadFiles, manifestFile];
|
|
4554
4709
|
const currentRelativePaths = new Set(files.map((file) => file.relativePath));
|
|
4555
4710
|
const drift = checkSessionRenderDrift(targetHome, manifestPath);
|
|
4556
4711
|
const results = [
|
|
4557
4712
|
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes)),
|
|
4558
|
-
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options)
|
|
4713
|
+
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options, retiredHashes)
|
|
4559
4714
|
];
|
|
4560
4715
|
assertNotSilentManagedWipeout(plan, results);
|
|
4561
4716
|
const conflicts = results.filter((result) => result.action === "conflict");
|
|
@@ -4579,7 +4734,8 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4579
4734
|
conflicts,
|
|
4580
4735
|
drift,
|
|
4581
4736
|
adoptions,
|
|
4582
|
-
reconciliations
|
|
4737
|
+
reconciliations,
|
|
4738
|
+
retirements
|
|
4583
4739
|
};
|
|
4584
4740
|
}
|
|
4585
4741
|
let snapshotPath = null;
|
|
@@ -4594,10 +4750,14 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4594
4750
|
const forcePortableFileOps = options.test_hooks?.force_portable_file_ops ?? false;
|
|
4595
4751
|
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4596
4752
|
ensureSessionTargetHome(targetHome);
|
|
4597
|
-
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations);
|
|
4753
|
+
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations, retirements);
|
|
4598
4754
|
snapshotPath = rollback.snapshotPath;
|
|
4599
4755
|
options.test_hooks?.before_apply_writes?.({ plan, results });
|
|
4600
4756
|
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4757
|
+
for (const result of results) {
|
|
4758
|
+
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
4759
|
+
coordination?.assert_held();
|
|
4760
|
+
}
|
|
4601
4761
|
const resultsByPath = new Map(results.map((result) => [result.path, result]));
|
|
4602
4762
|
for (const file of payloadFiles) {
|
|
4603
4763
|
applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
@@ -4633,7 +4793,8 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4633
4793
|
conflicts,
|
|
4634
4794
|
drift,
|
|
4635
4795
|
adoptions,
|
|
4636
|
-
reconciliations
|
|
4796
|
+
reconciliations,
|
|
4797
|
+
retirements
|
|
4637
4798
|
};
|
|
4638
4799
|
}
|
|
4639
4800
|
function assertManifestPrecondition(path, targetHome, expected) {
|
|
@@ -4662,7 +4823,7 @@ function validateFileAdoptions(plan, targetHome, previousHashes, options) {
|
|
|
4662
4823
|
throw new SessionApplyError(`Duplicate file adoption target: ${request.relativePath}`);
|
|
4663
4824
|
}
|
|
4664
4825
|
seen.add(request.relativePath);
|
|
4665
|
-
const candidates = plan.
|
|
4826
|
+
const candidates = exactPreimageCandidates(plan).filter((file2) => file2.relativePath === request.relativePath);
|
|
4666
4827
|
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4667
4828
|
throw new SessionApplyError(`File adoption target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4668
4829
|
}
|
|
@@ -4681,6 +4842,10 @@ function validateFileAdoptions(plan, targetHome, previousHashes, options) {
|
|
|
4681
4842
|
};
|
|
4682
4843
|
});
|
|
4683
4844
|
}
|
|
4845
|
+
function exactPreimageCandidates(plan) {
|
|
4846
|
+
const customAgentPaths = new Set((plan.assetPlan?.assets ?? []).filter((asset) => asset.kind === "custom-agent" && asset.support === "supported" && asset.action === "write" && asset.destination.strategy === "emit-file").map((asset) => posix.normalize(asset.destination.relativePath)));
|
|
4847
|
+
return [...plan.files, ...(plan.assetFiles ?? []).filter((file) => customAgentPaths.has(file.relativePath))];
|
|
4848
|
+
}
|
|
4684
4849
|
function readExactFilePreimage(path, targetHome, request, operation) {
|
|
4685
4850
|
if (currentSessionFileHash(path, targetHome) === null) {
|
|
4686
4851
|
throw new SessionApplyError(`File ${operation} target does not exist: ${request.relativePath}`);
|
|
@@ -4718,7 +4883,7 @@ function validateFileReconciliations(plan, targetHome, previousManifest, options
|
|
|
4718
4883
|
throw new SessionApplyError(`Duplicate file reconciliation target: ${request.relativePath}`);
|
|
4719
4884
|
}
|
|
4720
4885
|
seen.add(request.relativePath);
|
|
4721
|
-
const candidates = plan.
|
|
4886
|
+
const candidates = exactPreimageCandidates(plan).filter((file2) => file2.relativePath === request.relativePath);
|
|
4722
4887
|
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4723
4888
|
throw new SessionApplyError(`File reconciliation target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4724
4889
|
}
|
|
@@ -4745,8 +4910,47 @@ function validateFileReconciliations(plan, targetHome, previousManifest, options
|
|
|
4745
4910
|
};
|
|
4746
4911
|
});
|
|
4747
4912
|
}
|
|
4748
|
-
function
|
|
4749
|
-
|
|
4913
|
+
function validateFileRetirements(plan, targetHome, previousManifest, options) {
|
|
4914
|
+
const requests = options.retireFiles ?? [];
|
|
4915
|
+
if (!Array.isArray(requests))
|
|
4916
|
+
throw new SessionApplyError("Session file retirements must be an array.");
|
|
4917
|
+
if (requests.length === 0)
|
|
4918
|
+
return [];
|
|
4919
|
+
if (options.force)
|
|
4920
|
+
throw new SessionApplyError("Exact file retirement cannot be combined with force.");
|
|
4921
|
+
if (options.expectedManifestSha256 === undefined) {
|
|
4922
|
+
throw new SessionApplyError("Exact file retirement requires an expected manifest SHA-256 precondition.");
|
|
4923
|
+
}
|
|
4924
|
+
if (!previousManifest || previousManifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || previousManifest.tool !== plan.tool || previousManifest.targetHome !== targetHome) {
|
|
4925
|
+
throw new SessionApplyError("File retirement requires this renderer's manifest for the same tool and target home.");
|
|
4926
|
+
}
|
|
4927
|
+
const seen = new Set;
|
|
4928
|
+
return requests.map((request) => {
|
|
4929
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4930
|
+
throw new SessionApplyError("Each file retirement requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4931
|
+
}
|
|
4932
|
+
if (seen.has(request.relativePath))
|
|
4933
|
+
throw new SessionApplyError(`Duplicate file retirement target: ${request.relativePath}`);
|
|
4934
|
+
seen.add(request.relativePath);
|
|
4935
|
+
if (plan.allFiles.some((file) => file.relativePath === request.relativePath)) {
|
|
4936
|
+
throw new SessionApplyError(`File retirement target is retained by the new plan: ${request.relativePath}`);
|
|
4937
|
+
}
|
|
4938
|
+
const owned = previousManifest.files.filter((entry) => entry.relativePath === request.relativePath);
|
|
4939
|
+
const path = resolveManifestRelativePath(request.relativePath, targetHome);
|
|
4940
|
+
if (owned.length !== 1 || owned[0].path !== path || !/^[a-f0-9]{64}$/.test(owned[0].sha256) || !Array.isArray(owned[0].sourceIds) || owned[0].sourceIds.some((id) => typeof id !== "string") || !isPlanManagedFile(plan, request.relativePath, owned[0].role) && owned[0].role !== "asset") {
|
|
4941
|
+
throw new SessionApplyError(`File retirement target is not an obsolete managed fragment, rule, or asset: ${request.relativePath}`);
|
|
4942
|
+
}
|
|
4943
|
+
return {
|
|
4944
|
+
path,
|
|
4945
|
+
relativePath: request.relativePath,
|
|
4946
|
+
preimageSha256: readExactFilePreimage(path, targetHome, request, "retirement"),
|
|
4947
|
+
previousManagedSha256: owned[0].sha256,
|
|
4948
|
+
sourceIds: [...owned[0].sourceIds]
|
|
4949
|
+
};
|
|
4950
|
+
});
|
|
4951
|
+
}
|
|
4952
|
+
function manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations, retirements) {
|
|
4953
|
+
if (adoptions.length === 0 && previousManifest?.adoptions === undefined && reconciliations.length === 0 && previousManifest?.reconciliations === undefined && retirements.length === 0 && previousManifest?.retirements === undefined)
|
|
4750
4954
|
return plan.manifestFile;
|
|
4751
4955
|
if (previousManifest?.adoptions !== undefined && !Array.isArray(previousManifest.adoptions)) {
|
|
4752
4956
|
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
@@ -4787,10 +4991,28 @@ function manifestWithFileProvenance(plan, previousManifest, adoptions, reconcili
|
|
|
4787
4991
|
};
|
|
4788
4992
|
reconciledEntries.set(JSON.stringify(canonical), canonical);
|
|
4789
4993
|
}
|
|
4994
|
+
if (previousManifest?.retirements !== undefined && !Array.isArray(previousManifest.retirements)) {
|
|
4995
|
+
throw new SessionApplyError("Previous session manifest retirement provenance is invalid.");
|
|
4996
|
+
}
|
|
4997
|
+
const retiredEntries = new Map;
|
|
4998
|
+
for (const entry of [...previousManifest?.retirements ?? [], ...retirements.map(({ path: _path, ...receipt }) => receipt)]) {
|
|
4999
|
+
if (!entry || typeof entry.relativePath !== "string" || !/^[a-f0-9]{64}$/.test(entry.preimageSha256) || !/^[a-f0-9]{64}$/.test(entry.previousManagedSha256) || !Array.isArray(entry.sourceIds) || entry.sourceIds.some((id) => typeof id !== "string")) {
|
|
5000
|
+
throw new SessionApplyError("Previous session manifest retirement provenance is invalid.");
|
|
5001
|
+
}
|
|
5002
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
5003
|
+
const canonical = {
|
|
5004
|
+
relativePath: entry.relativePath,
|
|
5005
|
+
preimageSha256: entry.preimageSha256,
|
|
5006
|
+
previousManagedSha256: entry.previousManagedSha256,
|
|
5007
|
+
sourceIds: [...entry.sourceIds]
|
|
5008
|
+
};
|
|
5009
|
+
retiredEntries.set(JSON.stringify(canonical), canonical);
|
|
5010
|
+
}
|
|
4790
5011
|
const manifest = {
|
|
4791
5012
|
...plan.manifest,
|
|
4792
5013
|
...adoptions.length > 0 || previousManifest?.adoptions !== undefined ? { adoptions: [...entries.values()] } : {},
|
|
4793
|
-
...reconciliations.length > 0 || previousManifest?.reconciliations !== undefined ? { reconciliations: [...reconciledEntries.values()] } : {}
|
|
5014
|
+
...reconciliations.length > 0 || previousManifest?.reconciliations !== undefined ? { reconciliations: [...reconciledEntries.values()] } : {},
|
|
5015
|
+
...retirements.length > 0 || previousManifest?.retirements !== undefined ? { retirements: [...retiredEntries.values()] } : {}
|
|
4794
5016
|
};
|
|
4795
5017
|
const content = `${JSON.stringify(manifest, null, 2)}
|
|
4796
5018
|
`;
|
|
@@ -4828,7 +5050,7 @@ function ensureSessionTargetHome(targetHome) {
|
|
|
4828
5050
|
}
|
|
4829
5051
|
function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
4830
5052
|
const safeTargetHome = assertSafeTargetHome(targetHome);
|
|
4831
|
-
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative(safeTargetHome,
|
|
5053
|
+
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative(safeTargetHome, resolve5(manifestPath)), safeTargetHome) : resolve5(safeTargetHome, ".hasna", "session-render-manifest.json");
|
|
4832
5054
|
const checkedAt = new Date().toISOString();
|
|
4833
5055
|
const previousManifest = readPreviousManifest(resolvedManifestPath);
|
|
4834
5056
|
if (!previousManifest) {
|
|
@@ -4878,13 +5100,13 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
4878
5100
|
function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
|
|
4879
5101
|
const snapshot = readSessionRenderSnapshot(snapshotPath);
|
|
4880
5102
|
const targetHome = assertSafeTargetHome(snapshot.targetHome);
|
|
4881
|
-
const resolvedSnapshotPath =
|
|
5103
|
+
const resolvedSnapshotPath = resolve5(snapshotPath);
|
|
4882
5104
|
const snapshotDir = getSessionRenderSnapshotDir(targetHome);
|
|
4883
5105
|
const snapshotDirRelative = relative(snapshotDir, resolvedSnapshotPath);
|
|
4884
|
-
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !
|
|
5106
|
+
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute2(snapshotDirRelative);
|
|
4885
5107
|
if (!insideSnapshotDir) {
|
|
4886
5108
|
const snapshotRelativePath = relative(targetHome, resolvedSnapshotPath);
|
|
4887
|
-
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") ||
|
|
5109
|
+
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute2(snapshotRelativePath)) {
|
|
4888
5110
|
throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
|
|
4889
5111
|
}
|
|
4890
5112
|
}
|
|
@@ -5001,14 +5223,14 @@ function requiredRestoreHash(file) {
|
|
|
5001
5223
|
return file.previousSha256;
|
|
5002
5224
|
}
|
|
5003
5225
|
function readSessionRenderSnapshot(snapshotPath) {
|
|
5004
|
-
const resolved =
|
|
5226
|
+
const resolved = resolve5(snapshotPath);
|
|
5005
5227
|
if (!existsSync5(resolved))
|
|
5006
5228
|
throw new SessionApplyError(`Session snapshot not found: ${snapshotPath}`);
|
|
5007
|
-
const stat =
|
|
5229
|
+
const stat = lstatSync2(resolved);
|
|
5008
5230
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
5009
5231
|
throw new SessionApplyError(`Session snapshot is not a regular file: ${snapshotPath}`);
|
|
5010
5232
|
}
|
|
5011
|
-
if (
|
|
5233
|
+
if (statSync2(resolved).size > 32 * 1024 * 1024) {
|
|
5012
5234
|
throw new SessionApplyError(`Session snapshot exceeds the 32 MiB restore limit: ${snapshotPath}`);
|
|
5013
5235
|
}
|
|
5014
5236
|
let parsed;
|
|
@@ -5082,7 +5304,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
5082
5304
|
}
|
|
5083
5305
|
function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previousManifestFiles, targetHome, snapshotPath) {
|
|
5084
5306
|
assertNoNewerSessionSnapshot(snapshotPath, snapshot.createdAt, targetHome);
|
|
5085
|
-
const manifestPath =
|
|
5307
|
+
const manifestPath = resolve5(snapshot.manifestPath);
|
|
5086
5308
|
const manifestRelativePath = relative(targetHome, manifestPath).replaceAll("\\", "/");
|
|
5087
5309
|
resolveSnapshotFilePath(manifestRelativePath, snapshot.manifestPath, targetHome);
|
|
5088
5310
|
const manifestSha256 = currentSessionFileHash(manifestPath, targetHome);
|
|
@@ -5099,7 +5321,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
|
|
|
5099
5321
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is invalid: ${snapshotPath}`);
|
|
5100
5322
|
}
|
|
5101
5323
|
const appliedManifest = parsedManifest;
|
|
5102
|
-
if (appliedManifest.schema !== SESSION_RENDER_SCHEMA || appliedManifest.tool !== snapshot.tool || appliedManifest.profile !== snapshot.profile || typeof appliedManifest.targetHome !== "string" ||
|
|
5324
|
+
if (appliedManifest.schema !== SESSION_RENDER_SCHEMA || appliedManifest.tool !== snapshot.tool || appliedManifest.profile !== snapshot.profile || typeof appliedManifest.targetHome !== "string" || resolve5(appliedManifest.targetHome) !== targetHome || appliedManifest.targetKind !== "session-home" && appliedManifest.targetKind !== "project-root" || !Array.isArray(appliedManifest.files)) {
|
|
5103
5325
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest does not match its snapshot: ${snapshotPath}`);
|
|
5104
5326
|
}
|
|
5105
5327
|
const afterFiles = [];
|
|
@@ -5183,17 +5405,17 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
|
|
|
5183
5405
|
if (!Number.isFinite(createdAtMs)) {
|
|
5184
5406
|
throw new SessionApplyError(`Pre-rollback legacy v1 snapshot has an invalid creation time: ${snapshotPath}`);
|
|
5185
5407
|
}
|
|
5186
|
-
for (const entry of readdirSync(
|
|
5187
|
-
const candidatePath =
|
|
5188
|
-
if (candidatePath ===
|
|
5408
|
+
for (const entry of readdirSync(dirname3(snapshotPath))) {
|
|
5409
|
+
const candidatePath = resolve5(dirname3(snapshotPath), entry);
|
|
5410
|
+
if (candidatePath === resolve5(snapshotPath) || !entry.endsWith(".json"))
|
|
5189
5411
|
continue;
|
|
5190
|
-
const candidateStat =
|
|
5412
|
+
const candidateStat = lstatSync2(candidatePath);
|
|
5191
5413
|
if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
|
|
5192
5414
|
continue;
|
|
5193
5415
|
try {
|
|
5194
5416
|
const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));
|
|
5195
5417
|
const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
|
|
5196
|
-
if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" &&
|
|
5418
|
+
if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" && resolve5(candidate.targetHome) === targetHome && Number.isFinite(candidateCreatedAtMs) && candidateCreatedAtMs >= createdAtMs) {
|
|
5197
5419
|
throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
|
|
5198
5420
|
}
|
|
5199
5421
|
} catch (error) {
|
|
@@ -5262,7 +5484,7 @@ function inferLegacySnapshotAction(file, previousFiles, previousManifestFiles, p
|
|
|
5262
5484
|
}
|
|
5263
5485
|
function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
5264
5486
|
const path = resolveManifestRelativePath(relativePath, targetHome);
|
|
5265
|
-
if (
|
|
5487
|
+
if (resolve5(recordedPath) !== path) {
|
|
5266
5488
|
throw new SessionApplyError(`Session snapshot file path mismatch for ${relativePath}`);
|
|
5267
5489
|
}
|
|
5268
5490
|
return path;
|
|
@@ -5378,17 +5600,55 @@ function isPlanManagedFile(plan, relativePath, role) {
|
|
|
5378
5600
|
return true;
|
|
5379
5601
|
return plan.tool === "claude" && role === "rule" && relativePath.startsWith("rules/");
|
|
5380
5602
|
}
|
|
5381
|
-
function planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options) {
|
|
5603
|
+
function planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options, retiredHashes) {
|
|
5382
5604
|
if (!previousManifest)
|
|
5383
5605
|
return [];
|
|
5384
|
-
return previousManifest.files.filter((file) => !currentRelativePaths.has(file.relativePath)).filter((file) => isPlanManagedFile(plan, file.relativePath, file.role)).map((file) => planStaleFileResult(file, targetHome, options)).filter((result) => result !== null);
|
|
5606
|
+
return previousManifest.files.filter((file) => !currentRelativePaths.has(file.relativePath)).filter((file) => isPlanManagedFile(plan, file.relativePath, file.role) || file.role === "asset" || retiredHashes.has(file.relativePath)).map((file) => planStaleFileResult(file, targetHome, options, retiredHashes.get(file.relativePath))).filter((result) => result !== null);
|
|
5385
5607
|
}
|
|
5386
|
-
function planStaleFileResult(file, targetHome, options) {
|
|
5608
|
+
function planStaleFileResult(file, targetHome, options, retiredHash) {
|
|
5387
5609
|
const target = resolveManifestRelativePath(file.relativePath, targetHome);
|
|
5388
|
-
if (!existsSync5(target))
|
|
5389
|
-
|
|
5610
|
+
if (!existsSync5(target)) {
|
|
5611
|
+
if (file.role !== "asset")
|
|
5612
|
+
return null;
|
|
5613
|
+
return {
|
|
5614
|
+
path: target,
|
|
5615
|
+
relativePath: file.relativePath,
|
|
5616
|
+
role: file.role,
|
|
5617
|
+
action: "conflict",
|
|
5618
|
+
changed: true,
|
|
5619
|
+
previousSha256: null,
|
|
5620
|
+
newSha256: "",
|
|
5621
|
+
reason: "obsolete managed asset is missing; preserve manifest ownership until its reviewed preimage is restored and retired exactly"
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5390
5624
|
const previousContent = readFileSync(target, "utf-8");
|
|
5391
5625
|
const previousSha256 = sha2563(previousContent);
|
|
5626
|
+
if (retiredHash !== undefined) {
|
|
5627
|
+
if (previousSha256 !== retiredHash)
|
|
5628
|
+
throw new SessionApplyError(`File retirement preimage changed after validation: ${file.relativePath}`);
|
|
5629
|
+
return {
|
|
5630
|
+
path: target,
|
|
5631
|
+
relativePath: file.relativePath,
|
|
5632
|
+
role: file.role,
|
|
5633
|
+
action: "delete",
|
|
5634
|
+
changed: true,
|
|
5635
|
+
previousSha256,
|
|
5636
|
+
newSha256: "",
|
|
5637
|
+
reason: "exact reviewed obsolete managed file retired"
|
|
5638
|
+
};
|
|
5639
|
+
}
|
|
5640
|
+
if (file.role === "asset") {
|
|
5641
|
+
return {
|
|
5642
|
+
path: target,
|
|
5643
|
+
relativePath: file.relativePath,
|
|
5644
|
+
role: file.role,
|
|
5645
|
+
action: "conflict",
|
|
5646
|
+
changed: true,
|
|
5647
|
+
previousSha256,
|
|
5648
|
+
newSha256: "",
|
|
5649
|
+
reason: "obsolete managed asset requires exact retirement; preserve ownership until --retire-file and --expected-manifest-sha256 are supplied"
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5392
5652
|
if (!options.force && previousSha256 !== file.sha256) {
|
|
5393
5653
|
return {
|
|
5394
5654
|
path: target,
|
|
@@ -5432,21 +5692,21 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
|
|
|
5432
5692
|
return previousHashes.get(file.relativePath) === previousSha256;
|
|
5433
5693
|
}
|
|
5434
5694
|
function resolvePlannedFilePath(plan, file, targetHome) {
|
|
5435
|
-
const target =
|
|
5695
|
+
const target = resolve5(targetHome, ...file.relativePath.split("/"));
|
|
5436
5696
|
const rel = relative(targetHome, target);
|
|
5437
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
5697
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
5438
5698
|
throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
|
|
5439
5699
|
}
|
|
5440
|
-
if (
|
|
5700
|
+
if (resolve5(file.path) !== target) {
|
|
5441
5701
|
throw new SessionApplyError(`Session file path mismatch for ${file.relativePath}: ${file.path}`);
|
|
5442
5702
|
}
|
|
5443
5703
|
assertNoSymlinkSegments(targetHome, target);
|
|
5444
5704
|
return target;
|
|
5445
5705
|
}
|
|
5446
5706
|
function resolveManifestRelativePath(relativePath, targetHome) {
|
|
5447
|
-
const target =
|
|
5707
|
+
const target = resolve5(targetHome, ...relativePath.split(/[\\/]+/));
|
|
5448
5708
|
const rel = relative(targetHome, target);
|
|
5449
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
5709
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
5450
5710
|
throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
|
|
5451
5711
|
}
|
|
5452
5712
|
assertNoSymlinkSegments(targetHome, target);
|
|
@@ -5497,7 +5757,7 @@ function currentSessionFileHash(path, targetHome) {
|
|
|
5497
5757
|
assertNoSymlinkSegments(targetHome, path);
|
|
5498
5758
|
if (!existsSync5(path))
|
|
5499
5759
|
return null;
|
|
5500
|
-
const stat =
|
|
5760
|
+
const stat = lstatSync2(path);
|
|
5501
5761
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
5502
5762
|
throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
|
|
5503
5763
|
}
|
|
@@ -5509,7 +5769,7 @@ function requiredPreviousHash(result) {
|
|
|
5509
5769
|
}
|
|
5510
5770
|
return result.previousSha256;
|
|
5511
5771
|
}
|
|
5512
|
-
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations) {
|
|
5772
|
+
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations, retirements) {
|
|
5513
5773
|
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").map((result) => {
|
|
5514
5774
|
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
5515
5775
|
const content = readFileSync(result.path, "utf-8");
|
|
@@ -5533,7 +5793,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5533
5793
|
};
|
|
5534
5794
|
}
|
|
5535
5795
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
5536
|
-
const snapshotPath =
|
|
5796
|
+
const snapshotPath = join6(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
|
|
5537
5797
|
const afterFiles = results.map((result) => {
|
|
5538
5798
|
if (result.action === "conflict") {
|
|
5539
5799
|
throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
|
|
@@ -5558,11 +5818,12 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5558
5818
|
files: existingFiles,
|
|
5559
5819
|
afterFiles,
|
|
5560
5820
|
...adoptions.length > 0 ? { adoptions } : {},
|
|
5561
|
-
...reconciliations.length > 0 ? { reconciliations } : {}
|
|
5821
|
+
...reconciliations.length > 0 ? { reconciliations } : {},
|
|
5822
|
+
...retirements.length > 0 ? { retirements } : {}
|
|
5562
5823
|
};
|
|
5563
5824
|
const snapshotContent = `${JSON.stringify(snapshot, null, 2)}
|
|
5564
5825
|
`;
|
|
5565
|
-
if ((adoptions.length > 0 || reconciliations.length > 0) && Buffer.byteLength(snapshotContent, "utf8") > 32 * 1024 * 1024) {
|
|
5826
|
+
if ((adoptions.length > 0 || reconciliations.length > 0 || retirements.length > 0) && Buffer.byteLength(snapshotContent, "utf8") > 32 * 1024 * 1024) {
|
|
5566
5827
|
throw new SessionApplyError("Exact file preimage snapshot exceeds the 32 MiB restore limit.");
|
|
5567
5828
|
}
|
|
5568
5829
|
coordination?.assert_held();
|
|
@@ -5585,14 +5846,14 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5585
5846
|
};
|
|
5586
5847
|
}
|
|
5587
5848
|
function assertSafeTargetHome(targetHome) {
|
|
5588
|
-
if (!
|
|
5849
|
+
if (!isAbsolute2(targetHome))
|
|
5589
5850
|
throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
|
|
5590
|
-
const normalized =
|
|
5851
|
+
const normalized = resolve5(targetHome);
|
|
5591
5852
|
if (normalized === parse(normalized).root) {
|
|
5592
5853
|
throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
|
|
5593
5854
|
}
|
|
5594
5855
|
assertNoSymlinkAncestors(normalized);
|
|
5595
|
-
if (existsSync5(normalized) &&
|
|
5856
|
+
if (existsSync5(normalized) && lstatSync2(normalized).isSymbolicLink()) {
|
|
5596
5857
|
throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
|
|
5597
5858
|
}
|
|
5598
5859
|
return normalized;
|
|
@@ -5602,22 +5863,22 @@ function assertNoSymlinkSegments(root, target) {
|
|
|
5602
5863
|
const rel = relative(root, target);
|
|
5603
5864
|
let current = root;
|
|
5604
5865
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
5605
|
-
current =
|
|
5606
|
-
if (existsSync5(current) &&
|
|
5866
|
+
current = join6(current, segment);
|
|
5867
|
+
if (existsSync5(current) && lstatSync2(current).isSymbolicLink()) {
|
|
5607
5868
|
throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
|
|
5608
5869
|
}
|
|
5609
5870
|
}
|
|
5610
5871
|
}
|
|
5611
5872
|
function assertNoSymlinkAncestors(path) {
|
|
5612
|
-
const normalized =
|
|
5873
|
+
const normalized = resolve5(path);
|
|
5613
5874
|
const parsed = parse(normalized);
|
|
5614
5875
|
let current = parsed.root;
|
|
5615
5876
|
const rel = relative(parsed.root, normalized);
|
|
5616
5877
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
5617
|
-
current =
|
|
5878
|
+
current = join6(current, segment);
|
|
5618
5879
|
if (!existsSync5(current))
|
|
5619
5880
|
return;
|
|
5620
|
-
if (
|
|
5881
|
+
if (lstatSync2(current).isSymbolicLink()) {
|
|
5621
5882
|
throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
|
|
5622
5883
|
}
|
|
5623
5884
|
}
|
|
@@ -5628,8 +5889,8 @@ function sha2563(content) {
|
|
|
5628
5889
|
|
|
5629
5890
|
// src/lib/session-refresh.ts
|
|
5630
5891
|
import { createHash as createHash4 } from "crypto";
|
|
5631
|
-
import { closeSync, constants, fstatSync, lstatSync as
|
|
5632
|
-
import { dirname as
|
|
5892
|
+
import { closeSync, constants as constants2, fstatSync, lstatSync as lstatSync4, openSync, readFileSync as readFileSync3 } from "fs";
|
|
5893
|
+
import { basename, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, parse as parse2, resolve as resolve6 } from "path";
|
|
5633
5894
|
|
|
5634
5895
|
// src/lib/global-source-coverage.ts
|
|
5635
5896
|
var GLOBAL_SOURCE_SLUG_PREFIX = "global-";
|
|
@@ -5667,9 +5928,9 @@ function formatGlobalSourceCoverageWarnings(result) {
|
|
|
5667
5928
|
|
|
5668
5929
|
// src/lib/station-profile.ts
|
|
5669
5930
|
import { spawnSync } from "child_process";
|
|
5670
|
-
import { existsSync as existsSync6, lstatSync as
|
|
5671
|
-
import { arch as osArch, homedir as
|
|
5672
|
-
import { dirname as
|
|
5931
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5932
|
+
import { arch as osArch, homedir as homedir3, hostname as osHostname, platform as osPlatform, userInfo as osUserInfo } from "os";
|
|
5933
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
5673
5934
|
var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
|
|
5674
5935
|
var STATION_PROFILE_SOURCE_ID = "station-profile";
|
|
5675
5936
|
var STATION_PROFILE_LAYER = "machine";
|
|
@@ -5680,16 +5941,16 @@ var STATION_PROFILE_PRIMARY_SCOPE = "@hasna";
|
|
|
5680
5941
|
var MACHINES_MANIFEST_PATH_ENV = "HASNA_MACHINES_MANIFEST_PATH";
|
|
5681
5942
|
var BUN_INSTALL_ENV = "BUN_INSTALL";
|
|
5682
5943
|
function homeDir(env = process.env) {
|
|
5683
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
5944
|
+
return env["HOME"] || env["USERPROFILE"] || homedir3();
|
|
5684
5945
|
}
|
|
5685
5946
|
function getStationProfileCachePath(env = process.env) {
|
|
5686
|
-
return
|
|
5947
|
+
return join7(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
|
|
5687
5948
|
}
|
|
5688
5949
|
function getMachinesManifestPath(env = process.env) {
|
|
5689
|
-
return env[MACHINES_MANIFEST_PATH_ENV] ||
|
|
5950
|
+
return env[MACHINES_MANIFEST_PATH_ENV] || join7(homeDir(env), ".hasna", "machines", "machines.json");
|
|
5690
5951
|
}
|
|
5691
5952
|
function getBunGlobalModulesDir(env = process.env) {
|
|
5692
|
-
return
|
|
5953
|
+
return join7(env[BUN_INSTALL_ENV] || join7(homeDir(env), ".bun"), "install", "global", "node_modules");
|
|
5693
5954
|
}
|
|
5694
5955
|
function readMachinesManifest(path) {
|
|
5695
5956
|
try {
|
|
@@ -5751,7 +6012,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
5751
6012
|
const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname);
|
|
5752
6013
|
const home = homeDir(env);
|
|
5753
6014
|
const platform = stringField(record, "platform") ?? osPlatform();
|
|
5754
|
-
const workspacePath = stringField(record, "workspacePath")
|
|
6015
|
+
const workspacePath = stringField(record, "workspacePath");
|
|
5755
6016
|
const machine = {
|
|
5756
6017
|
id: stringField(record, "id") ?? hostname,
|
|
5757
6018
|
hostname: stringField(record, "hostname") ?? hostname,
|
|
@@ -5768,7 +6029,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
5768
6029
|
return machine;
|
|
5769
6030
|
}
|
|
5770
6031
|
function scopedPackageNames(modulesDir, scope) {
|
|
5771
|
-
const scopeDir =
|
|
6032
|
+
const scopeDir = join7(modulesDir, scope);
|
|
5772
6033
|
try {
|
|
5773
6034
|
if (!existsSync6(scopeDir))
|
|
5774
6035
|
return null;
|
|
@@ -5780,7 +6041,7 @@ function scopedPackageNames(modulesDir, scope) {
|
|
|
5780
6041
|
function readdirNames(dir) {
|
|
5781
6042
|
return readdirSync2(dir).filter((name) => {
|
|
5782
6043
|
try {
|
|
5783
|
-
return
|
|
6044
|
+
return lstatSync3(join7(dir, name)).isDirectory();
|
|
5784
6045
|
} catch {
|
|
5785
6046
|
return false;
|
|
5786
6047
|
}
|
|
@@ -5867,7 +6128,7 @@ function refreshStationProfile(options = {}) {
|
|
|
5867
6128
|
if (!options.dryRun) {
|
|
5868
6129
|
const existing = existsSync6(path) ? readFileSync2(path, "utf8") : null;
|
|
5869
6130
|
if (existing !== content) {
|
|
5870
|
-
mkdirSync3(
|
|
6131
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
5871
6132
|
writeFileSync2(path, content, "utf8");
|
|
5872
6133
|
}
|
|
5873
6134
|
}
|
|
@@ -5907,7 +6168,6 @@ function stationProfileSource(env = process.env) {
|
|
|
5907
6168
|
}
|
|
5908
6169
|
|
|
5909
6170
|
// src/lib/session-refresh.ts
|
|
5910
|
-
import { basename } from "path";
|
|
5911
6171
|
var nonempty = exports_external.string().min(1).max(4096);
|
|
5912
6172
|
var selectorSchema = exports_external.object({
|
|
5913
6173
|
schema: exports_external.literal("hasna.instructions.hosted-profile-selector/v1"),
|
|
@@ -5958,18 +6218,18 @@ function assertHostedProfileBindings(profileId, configs, bindings) {
|
|
|
5958
6218
|
}
|
|
5959
6219
|
}
|
|
5960
6220
|
function readManagedManifest(targetHome) {
|
|
5961
|
-
const path =
|
|
5962
|
-
let ancestor =
|
|
6221
|
+
const path = join8(targetHome, SESSION_RENDER_MANIFEST_RELATIVE_PATH);
|
|
6222
|
+
let ancestor = dirname5(path);
|
|
5963
6223
|
while (true) {
|
|
5964
|
-
const stat =
|
|
6224
|
+
const stat = lstatSync4(ancestor);
|
|
5965
6225
|
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
5966
6226
|
throw new Error("SESSION_REFRESH_PATH_INVALID: managed target uses a symlink or non-directory ancestor.");
|
|
5967
|
-
const parent =
|
|
6227
|
+
const parent = dirname5(ancestor);
|
|
5968
6228
|
if (parent === ancestor)
|
|
5969
6229
|
break;
|
|
5970
6230
|
ancestor = parent;
|
|
5971
6231
|
}
|
|
5972
|
-
const fd = openSync(path,
|
|
6232
|
+
const fd = openSync(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
5973
6233
|
let raw;
|
|
5974
6234
|
try {
|
|
5975
6235
|
const stat = fstatSync(fd);
|
|
@@ -5985,6 +6245,19 @@ function readManagedManifest(targetHome) {
|
|
|
5985
6245
|
if (manifest.schema !== SESSION_RENDER_SCHEMA || !SESSION_RENDER_TOOLS.includes(manifest.tool) || manifest.targetHome !== targetHome || !manifest.profile?.trim() || !["session-home", "project-root"].includes(manifest.targetKind) || !Array.isArray(manifest.files) || !manifest.refreshSelector || manifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || manifest.targetOwner?.targetHome !== targetHome) {
|
|
5986
6246
|
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: target is not an owned hosted profile render; apply a hosted --compile-profile first.");
|
|
5987
6247
|
}
|
|
6248
|
+
const recordedProjectRoot = manifest.targetOwner.projectRoot;
|
|
6249
|
+
if (recordedProjectRoot !== null && recordedProjectRoot !== undefined) {
|
|
6250
|
+
if (typeof recordedProjectRoot !== "string" || !isAbsolute3(recordedProjectRoot)) {
|
|
6251
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: recorded project root must be an absolute path.");
|
|
6252
|
+
}
|
|
6253
|
+
const normalizedProjectRoot = resolve6(recordedProjectRoot);
|
|
6254
|
+
if (normalizedProjectRoot === parse2(normalizedProjectRoot).root || manifest.targetKind === "project-root" && normalizedProjectRoot !== targetHome || manifest.targetKind === "session-home" && manifest.tool !== "opencode") {
|
|
6255
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: recorded project root does not match the managed target.");
|
|
6256
|
+
}
|
|
6257
|
+
manifest.targetOwner.projectRoot = normalizedProjectRoot;
|
|
6258
|
+
} else if (manifest.targetKind === "project-root") {
|
|
6259
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: project-scoped render is missing its recorded project root.");
|
|
6260
|
+
}
|
|
5988
6261
|
manifest.refreshSelector = normalizeSessionHostedProfileSelector(manifest.refreshSelector);
|
|
5989
6262
|
return { manifest, sha256: createHash4("sha256").update(raw).digest("hex") };
|
|
5990
6263
|
}
|
|
@@ -5992,7 +6265,7 @@ async function refreshSessionRender(input) {
|
|
|
5992
6265
|
const { store } = input;
|
|
5993
6266
|
if (store.mode !== "api" || !store.v1BaseUrl)
|
|
5994
6267
|
throw new Error("SESSION_REFRESH_HOSTED_REQUIRED: refresh requires the configured hosted Instructions API.");
|
|
5995
|
-
const targetHome =
|
|
6268
|
+
const targetHome = resolve6(input.targetHome);
|
|
5996
6269
|
if (targetHome === parse2(targetHome).root)
|
|
5997
6270
|
throw new Error("SESSION_REFRESH_PATH_INVALID: filesystem root is not a managed target.");
|
|
5998
6271
|
const previous = readManagedManifest(targetHome);
|
|
@@ -6027,7 +6300,7 @@ async function refreshSessionRender(input) {
|
|
|
6027
6300
|
profile_id: profile.id,
|
|
6028
6301
|
provider_version: selector.providerVersion,
|
|
6029
6302
|
targetHome,
|
|
6030
|
-
...previous.manifest.targetKind === "project-root" ? { projectRoot: targetHome } : {},
|
|
6303
|
+
...previous.manifest.targetKind === "project-root" ? { projectRoot: targetHome } : previous.manifest.targetOwner.projectRoot ? { projectRoot: previous.manifest.targetOwner.projectRoot } : {},
|
|
6031
6304
|
sessionId: previous.manifest.sessionId ?? undefined,
|
|
6032
6305
|
refreshSelector: selector,
|
|
6033
6306
|
codewithNativeImports: selector.codewithNativeImports,
|
|
@@ -6036,7 +6309,7 @@ async function refreshSessionRender(input) {
|
|
|
6036
6309
|
bindings,
|
|
6037
6310
|
asset_configs: assetConfigs,
|
|
6038
6311
|
asset_bindings: assetBindings,
|
|
6039
|
-
asset_plan_mode:
|
|
6312
|
+
asset_plan_mode: "apply",
|
|
6040
6313
|
asset_scope: selector.assetScope,
|
|
6041
6314
|
asset_surface: selector.assetSurface,
|
|
6042
6315
|
extra_sources: station ? [station] : undefined,
|
|
@@ -6392,19 +6665,9 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
|
|
|
6392
6665
|
}
|
|
6393
6666
|
|
|
6394
6667
|
// src/lib/managed-skill-runtimes.ts
|
|
6395
|
-
import {
|
|
6396
|
-
import {
|
|
6397
|
-
import {
|
|
6398
|
-
existsSync as existsSync7,
|
|
6399
|
-
lstatSync as lstatSync4,
|
|
6400
|
-
mkdirSync as mkdirSync4,
|
|
6401
|
-
readFileSync as readFileSync4,
|
|
6402
|
-
renameSync,
|
|
6403
|
-
rmSync as rmSync2,
|
|
6404
|
-
writeFileSync as writeFileSync3
|
|
6405
|
-
} from "fs";
|
|
6406
|
-
import { homedir as homedir3 } from "os";
|
|
6407
|
-
import { dirname as dirname5, join as join8, parse as parse3, relative as relative2, resolve as resolve6 } from "path";
|
|
6668
|
+
import { lstatSync as lstatSync5 } from "fs";
|
|
6669
|
+
import { homedir as homedir4 } from "os";
|
|
6670
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
6408
6671
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
6409
6672
|
var INBOX_SKILL_MARKERS = [
|
|
6410
6673
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -6413,378 +6676,76 @@ var INBOX_SKILL_MARKERS = [
|
|
|
6413
6676
|
[".config", "opencode", "skills", "inbox", "SKILL.md"],
|
|
6414
6677
|
[".cursor", "skills", "inbox", "SKILL.md"]
|
|
6415
6678
|
];
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
const normalized = resolve6(path);
|
|
6429
|
-
const parsed = parse3(normalized);
|
|
6430
|
-
let current = parsed.root;
|
|
6431
|
-
const rel = relative2(parsed.root, normalized);
|
|
6432
|
-
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
6433
|
-
current = join8(current, segment);
|
|
6434
|
-
if (!existsSync7(current))
|
|
6435
|
-
return null;
|
|
6436
|
-
if (lstatSync4(current).isSymbolicLink())
|
|
6437
|
-
return current;
|
|
6438
|
-
}
|
|
6439
|
-
return null;
|
|
6440
|
-
}
|
|
6441
|
-
function assertNoSymlinkAncestors2(path) {
|
|
6442
|
-
const found = findSymlinkedAncestor(path);
|
|
6443
|
-
if (found !== null) {
|
|
6444
|
-
throw new Error(`managed skill path uses a symlink ancestor: ${found}`);
|
|
6445
|
-
}
|
|
6446
|
-
}
|
|
6447
|
-
function packagedInboxSkillPath(explicitPath) {
|
|
6448
|
-
if (explicitPath)
|
|
6449
|
-
return explicitPath;
|
|
6450
|
-
throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
|
|
6451
|
-
}
|
|
6452
|
-
function readCanonicalSkill(explicitPath) {
|
|
6453
|
-
const assetPath = packagedInboxSkillPath(explicitPath);
|
|
6454
|
-
const stat = lstatOrNull(assetPath);
|
|
6455
|
-
if (!stat?.isFile()) {
|
|
6456
|
-
throw new Error("packaged inbox skill contract is not a regular file");
|
|
6457
|
-
}
|
|
6458
|
-
const content = readFileSync4(assetPath, "utf8");
|
|
6459
|
-
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
6460
|
-
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
6461
|
-
}
|
|
6462
|
-
if (!content.includes("There is no separate")) {
|
|
6463
|
-
throw new Error("packaged inbox skill contract does not retire the legacy executable");
|
|
6464
|
-
}
|
|
6465
|
-
return { content, sha256: sha2564(content) };
|
|
6466
|
-
}
|
|
6467
|
-
function runProbe(command, args) {
|
|
6468
|
-
const result = spawnSync2(command, args, {
|
|
6469
|
-
encoding: "utf8",
|
|
6470
|
-
timeout: 5000,
|
|
6471
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6472
|
-
});
|
|
6473
|
-
if (result.error || result.status !== 0) {
|
|
6474
|
-
return { ok: false, output: "" };
|
|
6475
|
-
}
|
|
6476
|
-
return {
|
|
6477
|
-
ok: true,
|
|
6478
|
-
output: `${result.stdout ?? ""}
|
|
6479
|
-
${result.stderr ?? ""}`.trim()
|
|
6480
|
-
};
|
|
6481
|
-
}
|
|
6482
|
-
function parseVersion(output) {
|
|
6483
|
-
return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
|
|
6484
|
-
}
|
|
6485
|
-
function compareVersions(left, right) {
|
|
6486
|
-
const a = left.split(".").map(Number);
|
|
6487
|
-
const b = right.split(".").map(Number);
|
|
6488
|
-
for (let i = 0;i < Math.max(a.length, b.length); i++) {
|
|
6489
|
-
const delta = (a[i] ?? 0) - (b[i] ?? 0);
|
|
6490
|
-
if (delta !== 0)
|
|
6491
|
-
return delta;
|
|
6492
|
-
}
|
|
6493
|
-
return 0;
|
|
6494
|
-
}
|
|
6495
|
-
function inspectSkillMarkers(homeDir2) {
|
|
6496
|
-
return INBOX_SKILL_MARKERS.map((parts) => join8(homeDir2, ...parts)).map((path) => {
|
|
6497
|
-
const stat = lstatOrNull(path);
|
|
6498
|
-
if (!stat)
|
|
6499
|
-
return null;
|
|
6500
|
-
if (!stat.isFile()) {
|
|
6501
|
-
return { path, content: null, mode: null, regular: false };
|
|
6679
|
+
function needsMigrationReview(homeDir2, parts) {
|
|
6680
|
+
let current = resolve7(homeDir2);
|
|
6681
|
+
const absolute = join9(current, ...parts);
|
|
6682
|
+
for (const segment of parts) {
|
|
6683
|
+
current = join9(current, segment);
|
|
6684
|
+
let stat;
|
|
6685
|
+
try {
|
|
6686
|
+
stat = lstatSync5(current);
|
|
6687
|
+
} catch (error) {
|
|
6688
|
+
if (error.code === "ENOENT")
|
|
6689
|
+
return false;
|
|
6690
|
+
throw error;
|
|
6502
6691
|
}
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
content: readFileSync4(path, "utf8"),
|
|
6506
|
-
mode: stat.mode & 511,
|
|
6507
|
-
regular: true
|
|
6508
|
-
};
|
|
6509
|
-
}).filter((snapshot) => snapshot !== null);
|
|
6510
|
-
}
|
|
6511
|
-
function inspectInbox(options) {
|
|
6512
|
-
const homeDir2 = options.homeDir ?? homedir3();
|
|
6513
|
-
const runtimeCommand = options.conversationsCommand ?? "conversations";
|
|
6514
|
-
const snapshots = inspectSkillMarkers(homeDir2);
|
|
6515
|
-
const skillPresent = snapshots.length > 0;
|
|
6516
|
-
let canonicalContent = null;
|
|
6517
|
-
let canonicalSha256 = null;
|
|
6518
|
-
let assetError = null;
|
|
6519
|
-
try {
|
|
6520
|
-
const canonical = readCanonicalSkill(options.assetPath);
|
|
6521
|
-
canonicalContent = canonical.content;
|
|
6522
|
-
canonicalSha256 = canonical.sha256;
|
|
6523
|
-
} catch (error) {
|
|
6524
|
-
assetError = error instanceof Error ? error.message : String(error);
|
|
6525
|
-
}
|
|
6526
|
-
const versionProbe = skillPresent ? runProbe(runtimeCommand, ["--version"]) : { ok: false, output: "" };
|
|
6527
|
-
const helpProbe = versionProbe.ok ? runProbe(runtimeCommand, ["watch", "--help"]) : { ok: false, output: "" };
|
|
6528
|
-
const runtimeVersion = versionProbe.ok ? parseVersion(versionProbe.output) : null;
|
|
6529
|
-
const supportsFrom = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[0]);
|
|
6530
|
-
const supportsAll = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[1]);
|
|
6531
|
-
const supportsFullContent = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[2]);
|
|
6532
|
-
const packageReady = versionProbe.ok && runtimeVersion !== null && compareVersions(runtimeVersion, INBOX_CONVERSATIONS_MINIMUM_VERSION) >= 0 && helpProbe.ok && supportsFrom && supportsAll && supportsFullContent;
|
|
6533
|
-
const heartbeatProbe = skillPresent && packageReady && options.agent ? runProbe(runtimeCommand, ["agents", "heartbeat", "--from", options.agent, "--json"]) : null;
|
|
6534
|
-
const hostedHeartbeat = heartbeatProbe === null ? "unverified" : heartbeatProbe.ok ? "passed" : "failed";
|
|
6535
|
-
const deliveryVerified = hostedHeartbeat === "passed" && options.deliveryVerified === true;
|
|
6536
|
-
const staleMarkers = canonicalContent === null ? snapshots.map((snapshot) => snapshot.path) : snapshots.filter((snapshot) => !snapshot.regular || snapshot.content !== canonicalContent).map((snapshot) => snapshot.path);
|
|
6537
|
-
let reason = "skill not installed";
|
|
6538
|
-
if (skillPresent) {
|
|
6539
|
-
const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
|
|
6540
|
-
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6541
|
-
if (nonRegular)
|
|
6542
|
-
reason = "managed skill target is not a regular file";
|
|
6543
|
-
else if (symlinkAncestor)
|
|
6544
|
-
reason = `managed skill path uses a symlink ancestor: ${symlinkAncestor}`;
|
|
6545
|
-
else if (assetError)
|
|
6546
|
-
reason = assetError;
|
|
6547
|
-
else if (!versionProbe.ok)
|
|
6548
|
-
reason = "conversations command unavailable";
|
|
6549
|
-
else if (!runtimeVersion)
|
|
6550
|
-
reason = "conversations version is unreadable";
|
|
6551
|
-
else if (compareVersions(runtimeVersion, INBOX_CONVERSATIONS_MINIMUM_VERSION) < 0) {
|
|
6552
|
-
reason = `conversations ${runtimeVersion} is older than ${INBOX_CONVERSATIONS_MINIMUM_VERSION}`;
|
|
6553
|
-
} else if (!helpProbe.ok)
|
|
6554
|
-
reason = "conversations watch help is unavailable";
|
|
6555
|
-
else if (!supportsFrom || !supportsAll || !supportsFullContent) {
|
|
6556
|
-
const missing = [
|
|
6557
|
-
!supportsFrom ? "--from" : null,
|
|
6558
|
-
!supportsAll ? "--all" : null,
|
|
6559
|
-
!supportsFullContent ? "--full-content" : null
|
|
6560
|
-
].filter((flag) => flag !== null);
|
|
6561
|
-
reason = `conversations watch is missing required flags: ${missing.join(", ")}`;
|
|
6562
|
-
} else if (staleMarkers.length > 0)
|
|
6563
|
-
reason = "skill contract stale";
|
|
6564
|
-
else if (hostedHeartbeat === "failed")
|
|
6565
|
-
reason = "hosted heartbeat failed; manual fallback required";
|
|
6566
|
-
else if (hostedHeartbeat === "unverified")
|
|
6567
|
-
reason = "hosted heartbeat unverified; manual fallback required";
|
|
6568
|
-
else if (!deliveryVerified)
|
|
6569
|
-
reason = "hosted heartbeat passed; channel and DM delivery verification required";
|
|
6570
|
-
else
|
|
6571
|
-
reason = "ready";
|
|
6692
|
+
if (stat.isSymbolicLink() || current !== absolute && !stat.isDirectory())
|
|
6693
|
+
return true;
|
|
6572
6694
|
}
|
|
6695
|
+
return true;
|
|
6696
|
+
}
|
|
6697
|
+
function inspectLegacyInbox(options) {
|
|
6698
|
+
const homeDir2 = options.homeDir ?? homedir4();
|
|
6699
|
+
const markers = INBOX_SKILL_MARKERS.filter((parts) => needsMigrationReview(homeDir2, parts)).map((parts) => join9(homeDir2, ...parts));
|
|
6700
|
+
const present = markers.length > 0;
|
|
6573
6701
|
return {
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
reason
|
|
6594
|
-
},
|
|
6595
|
-
canonicalContent,
|
|
6596
|
-
snapshots
|
|
6702
|
+
skill: "inbox",
|
|
6703
|
+
runtime: "conversations watch",
|
|
6704
|
+
minimum_version: INBOX_CONVERSATIONS_MINIMUM_VERSION,
|
|
6705
|
+
skill_present: present,
|
|
6706
|
+
skill_markers: markers,
|
|
6707
|
+
skill_contracts_current: 0,
|
|
6708
|
+
stale_skill_markers: markers,
|
|
6709
|
+
expected_skill_sha256: null,
|
|
6710
|
+
runtime_command: options.conversationsCommand ?? "conversations",
|
|
6711
|
+
runtime_present: false,
|
|
6712
|
+
runtime_version: null,
|
|
6713
|
+
watch_supports_from: false,
|
|
6714
|
+
watch_supports_all: false,
|
|
6715
|
+
watch_supports_full_content: false,
|
|
6716
|
+
hosted_heartbeat: "unverified",
|
|
6717
|
+
delivery_verified: false,
|
|
6718
|
+
manual_fallback_ready: false,
|
|
6719
|
+
healthy: !present,
|
|
6720
|
+
reason: present ? "Native Inbox skill management is retired; use the Skills CLI to review skills migrate native, then sync and load the selected skills" : "No legacy native Inbox skill found; skill availability is managed by the Skills CLI"
|
|
6597
6721
|
};
|
|
6598
6722
|
}
|
|
6599
6723
|
function inspectManagedSkillRuntimes(options = {}) {
|
|
6600
|
-
const runtime =
|
|
6601
|
-
const installed = runtime.skill_present ? [runtime] : [];
|
|
6724
|
+
const runtime = inspectLegacyInbox(options);
|
|
6602
6725
|
return {
|
|
6603
6726
|
runtimes: [runtime],
|
|
6604
|
-
skills_present:
|
|
6605
|
-
healthy:
|
|
6606
|
-
missing:
|
|
6727
|
+
skills_present: runtime.skill_present ? 1 : 0,
|
|
6728
|
+
healthy: 0,
|
|
6729
|
+
missing: runtime.skill_present ? 1 : 0
|
|
6607
6730
|
};
|
|
6608
6731
|
}
|
|
6609
|
-
function runtimeReadyForWrite(status) {
|
|
6610
|
-
return status.runtime_present && status.runtime_version !== null && compareVersions(status.runtime_version, INBOX_CONVERSATIONS_MINIMUM_VERSION) >= 0 && status.watch_supports_from && status.watch_supports_all && status.watch_supports_full_content;
|
|
6611
|
-
}
|
|
6612
|
-
function projectUpdatedStatus(status, contractCount) {
|
|
6613
|
-
const healthy = status.hosted_heartbeat === "passed" && status.delivery_verified;
|
|
6614
|
-
return {
|
|
6615
|
-
...status,
|
|
6616
|
-
skill_contracts_current: contractCount,
|
|
6617
|
-
stale_skill_markers: [],
|
|
6618
|
-
manual_fallback_ready: true,
|
|
6619
|
-
healthy,
|
|
6620
|
-
reason: healthy ? "ready" : status.hosted_heartbeat === "failed" ? "hosted heartbeat failed; manual fallback required" : status.hosted_heartbeat === "unverified" ? "hosted heartbeat unverified; manual fallback required" : "hosted heartbeat passed; channel and DM delivery verification required"
|
|
6621
|
-
};
|
|
6622
|
-
}
|
|
6623
|
-
function cleanup(path) {
|
|
6624
|
-
rmSync2(path, { force: true });
|
|
6625
|
-
}
|
|
6626
|
-
function writeAtomic(path, content, mode) {
|
|
6627
|
-
assertNoSymlinkAncestors2(dirname5(path));
|
|
6628
|
-
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
6629
|
-
try {
|
|
6630
|
-
mkdirSync4(dirname5(path), { recursive: true, mode: 493 });
|
|
6631
|
-
writeFileSync3(tempPath, content, { mode, flag: "wx" });
|
|
6632
|
-
renameSync(tempPath, path);
|
|
6633
|
-
} finally {
|
|
6634
|
-
cleanup(tempPath);
|
|
6635
|
-
}
|
|
6636
|
-
}
|
|
6637
|
-
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
6638
|
-
lstat: lstatOrNull,
|
|
6639
|
-
read: (path) => readFileSync4(path, "utf8"),
|
|
6640
|
-
write: writeAtomic
|
|
6641
|
-
};
|
|
6642
|
-
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
6643
|
-
const written = [];
|
|
6644
|
-
try {
|
|
6645
|
-
for (const snapshot of snapshots) {
|
|
6646
|
-
const currentStat = fileOperations.lstat(snapshot.path);
|
|
6647
|
-
if (!currentStat?.isFile() || fileOperations.read(snapshot.path) !== snapshot.content) {
|
|
6648
|
-
throw new Error("managed skill changed after inspection; refusing a stale write");
|
|
6649
|
-
}
|
|
6650
|
-
fileOperations.write(snapshot.path, canonicalContent, snapshot.mode);
|
|
6651
|
-
written.push(snapshot);
|
|
6652
|
-
}
|
|
6653
|
-
return { ok: true, error: null, rollback_conflicts: [] };
|
|
6654
|
-
} catch (error) {
|
|
6655
|
-
const rollbackConflicts = [];
|
|
6656
|
-
for (const snapshot of written.reverse()) {
|
|
6657
|
-
const currentStat = fileOperations.lstat(snapshot.path);
|
|
6658
|
-
if (!currentStat?.isFile()) {
|
|
6659
|
-
rollbackConflicts.push(`${snapshot.path}: no longer a regular file`);
|
|
6660
|
-
continue;
|
|
6661
|
-
}
|
|
6662
|
-
let currentContent;
|
|
6663
|
-
try {
|
|
6664
|
-
currentContent = fileOperations.read(snapshot.path);
|
|
6665
|
-
} catch {
|
|
6666
|
-
rollbackConflicts.push(`${snapshot.path}: could not read the current file`);
|
|
6667
|
-
continue;
|
|
6668
|
-
}
|
|
6669
|
-
if (currentContent !== canonicalContent) {
|
|
6670
|
-
rollbackConflicts.push(`${snapshot.path}: changed after this reconciliation wrote it`);
|
|
6671
|
-
continue;
|
|
6672
|
-
}
|
|
6673
|
-
try {
|
|
6674
|
-
fileOperations.write(snapshot.path, snapshot.content, snapshot.mode);
|
|
6675
|
-
} catch {
|
|
6676
|
-
rollbackConflicts.push(`${snapshot.path}: still owned but could not be restored`);
|
|
6677
|
-
}
|
|
6678
|
-
}
|
|
6679
|
-
return {
|
|
6680
|
-
ok: false,
|
|
6681
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6682
|
-
rollback_conflicts: rollbackConflicts
|
|
6683
|
-
};
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
6732
|
async function reconcileManagedSkillRuntimes(options = {}) {
|
|
6733
|
+
const runtime = inspectLegacyInbox(options);
|
|
6687
6734
|
const dryRun = options.dryRun ?? false;
|
|
6688
|
-
const before = inspectInbox(options);
|
|
6689
|
-
const status = before.status;
|
|
6690
|
-
if (!status.skill_present) {
|
|
6691
|
-
return {
|
|
6692
|
-
runtimes: [{ ...status, action: "skipped", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6693
|
-
changed: 0,
|
|
6694
|
-
failed: 0,
|
|
6695
|
-
dry_run: dryRun
|
|
6696
|
-
};
|
|
6697
|
-
}
|
|
6698
|
-
if (before.snapshots.some((snapshot) => !snapshot.regular)) {
|
|
6699
|
-
return {
|
|
6700
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6701
|
-
changed: 0,
|
|
6702
|
-
failed: 1,
|
|
6703
|
-
dry_run: dryRun
|
|
6704
|
-
};
|
|
6705
|
-
}
|
|
6706
|
-
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6707
|
-
if (symlinkedAncestor) {
|
|
6708
|
-
return {
|
|
6709
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6710
|
-
changed: 0,
|
|
6711
|
-
failed: 1,
|
|
6712
|
-
dry_run: dryRun
|
|
6713
|
-
};
|
|
6714
|
-
}
|
|
6715
|
-
if (!before.canonicalContent || !runtimeReadyForWrite(status)) {
|
|
6716
|
-
return {
|
|
6717
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6718
|
-
changed: 0,
|
|
6719
|
-
failed: 1,
|
|
6720
|
-
dry_run: dryRun
|
|
6721
|
-
};
|
|
6722
|
-
}
|
|
6723
|
-
const staleSnapshots = before.snapshots.filter((snapshot) => snapshot.content !== before.canonicalContent);
|
|
6724
|
-
if (staleSnapshots.length === 0) {
|
|
6725
|
-
return {
|
|
6726
|
-
runtimes: [{ ...status, action: "unchanged", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6727
|
-
changed: 0,
|
|
6728
|
-
failed: 0,
|
|
6729
|
-
dry_run: dryRun
|
|
6730
|
-
};
|
|
6731
|
-
}
|
|
6732
|
-
if (dryRun) {
|
|
6733
|
-
const projected = projectUpdatedStatus(status, before.snapshots.length);
|
|
6734
|
-
return {
|
|
6735
|
-
runtimes: [{
|
|
6736
|
-
...projected,
|
|
6737
|
-
action: "update",
|
|
6738
|
-
dry_run: true,
|
|
6739
|
-
skill_contracts_changed: staleSnapshots.length
|
|
6740
|
-
}],
|
|
6741
|
-
changed: 1,
|
|
6742
|
-
failed: 0,
|
|
6743
|
-
dry_run: true
|
|
6744
|
-
};
|
|
6745
|
-
}
|
|
6746
|
-
const transaction = writeSkillContractsTransactional(staleSnapshots.map((snapshot) => ({
|
|
6747
|
-
path: snapshot.path,
|
|
6748
|
-
content: snapshot.content,
|
|
6749
|
-
mode: snapshot.mode ?? 420
|
|
6750
|
-
})), before.canonicalContent);
|
|
6751
|
-
if (!transaction.ok) {
|
|
6752
|
-
const rollbackConflictReason = transaction.rollback_conflicts.length > 0 ? `; rollback conflicts: ${transaction.rollback_conflicts.join("; ")}` : "";
|
|
6753
|
-
const reason = `${transaction.error ?? "managed skill reconciliation failed"}${rollbackConflictReason}`;
|
|
6754
|
-
return {
|
|
6755
|
-
runtimes: [{
|
|
6756
|
-
...status,
|
|
6757
|
-
action: "failed",
|
|
6758
|
-
dry_run: false,
|
|
6759
|
-
skill_contracts_changed: 0,
|
|
6760
|
-
reason
|
|
6761
|
-
}],
|
|
6762
|
-
changed: 0,
|
|
6763
|
-
failed: 1,
|
|
6764
|
-
dry_run: false
|
|
6765
|
-
};
|
|
6766
|
-
}
|
|
6767
|
-
const after = inspectInbox(options).status;
|
|
6768
|
-
const accepted = after.healthy || after.manual_fallback_ready;
|
|
6769
6735
|
return {
|
|
6770
|
-
runtimes: [{
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
skill_contracts_changed: accepted ? staleSnapshots.length : 0
|
|
6775
|
-
}],
|
|
6776
|
-
changed: accepted ? 1 : 0,
|
|
6777
|
-
failed: accepted ? 0 : 1,
|
|
6778
|
-
dry_run: false
|
|
6736
|
+
runtimes: [{ ...runtime, action: runtime.skill_present ? "failed" : "skipped", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6737
|
+
changed: 0,
|
|
6738
|
+
failed: runtime.skill_present ? 1 : 0,
|
|
6739
|
+
dry_run: dryRun
|
|
6779
6740
|
};
|
|
6780
6741
|
}
|
|
6781
6742
|
|
|
6782
6743
|
// src/status.ts
|
|
6783
|
-
import { existsSync as
|
|
6744
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
6784
6745
|
|
|
6785
6746
|
// src/lib/package-version.ts
|
|
6786
|
-
import { existsSync as
|
|
6787
|
-
import { dirname as dirname6, join as
|
|
6747
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
6748
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6788
6749
|
import { fileURLToPath } from "url";
|
|
6789
6750
|
var cached = null;
|
|
6790
6751
|
function getPackageVersion() {
|
|
@@ -6793,9 +6754,9 @@ function getPackageVersion() {
|
|
|
6793
6754
|
try {
|
|
6794
6755
|
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
6795
6756
|
for (let i = 0;i < 8; i++) {
|
|
6796
|
-
const pkgPath =
|
|
6797
|
-
if (
|
|
6798
|
-
const pkg = JSON.parse(
|
|
6757
|
+
const pkgPath = join10(dir, "package.json");
|
|
6758
|
+
if (existsSync7(pkgPath)) {
|
|
6759
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
6799
6760
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
6800
6761
|
cached = pkg.version;
|
|
6801
6762
|
return cached;
|
|
@@ -6870,11 +6831,11 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6870
6831
|
continue;
|
|
6871
6832
|
knownTargets += 1;
|
|
6872
6833
|
const targetPath = expandPath(config.target_path);
|
|
6873
|
-
if (!
|
|
6834
|
+
if (!existsSync8(targetPath)) {
|
|
6874
6835
|
missingTargets += 1;
|
|
6875
6836
|
continue;
|
|
6876
6837
|
}
|
|
6877
|
-
const disk =
|
|
6838
|
+
const disk = readFileSync5(targetPath, "utf-8");
|
|
6878
6839
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
|
|
6879
6840
|
if (redactedDisk !== config.content) {
|
|
6880
6841
|
driftedTargets += 1;
|
|
@@ -6966,9 +6927,9 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6966
6927
|
}
|
|
6967
6928
|
|
|
6968
6929
|
// src/lib/provider-context.ts
|
|
6969
|
-
import { createHash as
|
|
6970
|
-
import { existsSync as
|
|
6971
|
-
import { join as
|
|
6930
|
+
import { createHash as createHash5 } from "crypto";
|
|
6931
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
6932
|
+
import { join as join11 } from "path";
|
|
6972
6933
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
6973
6934
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
6974
6935
|
var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
|
|
@@ -7103,8 +7064,8 @@ function renderPerEndpointFragment(entry) {
|
|
|
7103
7064
|
function renderProviderFragment(entry) {
|
|
7104
7065
|
return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
|
|
7105
7066
|
}
|
|
7106
|
-
function
|
|
7107
|
-
return
|
|
7067
|
+
function sha2564(content) {
|
|
7068
|
+
return createHash5("sha256").update(content).digest("hex");
|
|
7108
7069
|
}
|
|
7109
7070
|
function resolveAndRenderProviderContext(opts) {
|
|
7110
7071
|
const entry = matchProviderEndpoint(opts.origin);
|
|
@@ -7113,18 +7074,18 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
7113
7074
|
const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
|
|
7114
7075
|
const reason = entry === null && opts.rawEndpoint ? originAccepted ? `endpoint "${recordedEndpoint}" is not in the provider-context registry; using the invariant fragment` : "endpoint rejected (embedded credentials or unparseable); using the invariant fragment" : null;
|
|
7115
7076
|
const content = renderProviderFragment(entry);
|
|
7116
|
-
const dir =
|
|
7117
|
-
if (!
|
|
7118
|
-
|
|
7077
|
+
const dir = join11(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
7078
|
+
if (!existsSync9(dir))
|
|
7079
|
+
mkdirSync4(dir, { recursive: true });
|
|
7119
7080
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
7120
|
-
const fragmentPath =
|
|
7121
|
-
const fragmentSha256 =
|
|
7122
|
-
|
|
7123
|
-
const manifestPath =
|
|
7081
|
+
const fragmentPath = join11(dir, filename);
|
|
7082
|
+
const fragmentSha256 = sha2564(content);
|
|
7083
|
+
writeFileSync3(fragmentPath, content, "utf8");
|
|
7084
|
+
const manifestPath = join11(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
7124
7085
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
7125
7086
|
try {
|
|
7126
|
-
if (
|
|
7127
|
-
const parsed = JSON.parse(
|
|
7087
|
+
if (existsSync9(manifestPath)) {
|
|
7088
|
+
const parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
7128
7089
|
if (parsed && typeof parsed === "object")
|
|
7129
7090
|
manifest = parsed;
|
|
7130
7091
|
}
|
|
@@ -7139,7 +7100,7 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
7139
7100
|
rawModel: opts.rawModel || null
|
|
7140
7101
|
};
|
|
7141
7102
|
manifest.fragments = fragmentsObj;
|
|
7142
|
-
|
|
7103
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
7143
7104
|
return {
|
|
7144
7105
|
entry,
|
|
7145
7106
|
rawEndpoint: opts.rawEndpoint,
|
|
@@ -7270,7 +7231,7 @@ function parseSessionSource(value, order) {
|
|
|
7270
7231
|
if (!path)
|
|
7271
7232
|
throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
|
|
7272
7233
|
const absPath = resolveSessionPath(path);
|
|
7273
|
-
if (!
|
|
7234
|
+
if (!existsSync10(absPath))
|
|
7274
7235
|
throw new Error(`Instruction source file not found: ${absPath}`);
|
|
7275
7236
|
const content = readSessionInstructionSourceFile(absPath);
|
|
7276
7237
|
const source = sourceFromFilePath(absPath, content, order);
|
|
@@ -7314,7 +7275,7 @@ function sessionSourceReplacements(values) {
|
|
|
7314
7275
|
return replacements;
|
|
7315
7276
|
}
|
|
7316
7277
|
function readSessionInstructionSourceFile(path) {
|
|
7317
|
-
const stat =
|
|
7278
|
+
const stat = lstatSync6(path);
|
|
7318
7279
|
if (stat.isSymbolicLink()) {
|
|
7319
7280
|
throw new Error("SESSION_SOURCE_SYMLINK_REJECTED: instruction source file must be a regular non-symlink file");
|
|
7320
7281
|
}
|
|
@@ -7324,7 +7285,7 @@ function readSessionInstructionSourceFile(path) {
|
|
|
7324
7285
|
if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
|
|
7325
7286
|
throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
|
|
7326
7287
|
}
|
|
7327
|
-
return
|
|
7288
|
+
return readFileSync7(path, "utf-8");
|
|
7328
7289
|
}
|
|
7329
7290
|
function parseLayeredReference(value) {
|
|
7330
7291
|
const trimmed = value.trim();
|
|
@@ -7353,9 +7314,9 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
7353
7314
|
}
|
|
7354
7315
|
for (const value of opts.identityExport ?? []) {
|
|
7355
7316
|
const path = resolveSessionPath(value);
|
|
7356
|
-
if (!
|
|
7317
|
+
if (!existsSync10(path))
|
|
7357
7318
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
7358
|
-
const parsed = JSON.parse(
|
|
7319
|
+
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
7359
7320
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
7360
7321
|
}
|
|
7361
7322
|
return sources.map((source) => {
|
|
@@ -7507,19 +7468,19 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
7507
7468
|
if (value === "-")
|
|
7508
7469
|
return { json: readBoundedProjectContextStdin() };
|
|
7509
7470
|
const path = resolveSessionPath(value);
|
|
7510
|
-
if (!
|
|
7471
|
+
if (!existsSync10(path)) {
|
|
7511
7472
|
if (allowMissing)
|
|
7512
7473
|
return {};
|
|
7513
7474
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
|
|
7514
7475
|
}
|
|
7515
|
-
const stat =
|
|
7476
|
+
const stat = lstatSync6(path);
|
|
7516
7477
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
7517
7478
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", "bundle input must be a regular non-symlink file");
|
|
7518
7479
|
}
|
|
7519
7480
|
if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
|
|
7520
7481
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
|
|
7521
7482
|
}
|
|
7522
|
-
return { json:
|
|
7483
|
+
return { json: readFileSync7(path, "utf8"), sourcePath: path };
|
|
7523
7484
|
}
|
|
7524
7485
|
function readBoundedProjectContextStdin() {
|
|
7525
7486
|
const chunks = [];
|
|
@@ -7904,12 +7865,12 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7904
7865
|
if (expectedVersion !== undefined && !opts.update) {
|
|
7905
7866
|
throw new Error("--expected-version requires --update");
|
|
7906
7867
|
}
|
|
7907
|
-
const abs =
|
|
7908
|
-
if (!
|
|
7868
|
+
const abs = resolve8(filePath);
|
|
7869
|
+
if (!existsSync10(abs)) {
|
|
7909
7870
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
7910
7871
|
process.exit(1);
|
|
7911
7872
|
}
|
|
7912
|
-
const rawContent =
|
|
7873
|
+
const rawContent = readFileSync7(abs, "utf-8");
|
|
7913
7874
|
const storedFmt = detectFormat(abs);
|
|
7914
7875
|
const fmt = redactFormatForTarget(abs, storedFmt);
|
|
7915
7876
|
const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
|
|
@@ -8092,7 +8053,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
8092
8053
|
for (const entry of entries) {
|
|
8093
8054
|
if (!entry.isDirectory())
|
|
8094
8055
|
continue;
|
|
8095
|
-
const projDir =
|
|
8056
|
+
const projDir = join12(absDir, entry.name);
|
|
8096
8057
|
const hasAgentConfig = [
|
|
8097
8058
|
"CLAUDE.md",
|
|
8098
8059
|
".mcp.json",
|
|
@@ -8105,7 +8066,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
8105
8066
|
".aicopilot",
|
|
8106
8067
|
".cursor",
|
|
8107
8068
|
".agents"
|
|
8108
|
-
].some((marker) =>
|
|
8069
|
+
].some((marker) => existsSync10(join12(projDir, marker)));
|
|
8109
8070
|
if (!hasAgentConfig)
|
|
8110
8071
|
continue;
|
|
8111
8072
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
|
|
@@ -8153,7 +8114,7 @@ program.command("import <file>").description("Restore a v2 Instructions domain a
|
|
|
8153
8114
|
});
|
|
8154
8115
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
8155
8116
|
const store = resolveConfigStore();
|
|
8156
|
-
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
8117
|
+
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join12(getRawStoreRoot(), "instructions.db");
|
|
8157
8118
|
const stats = await store.getConfigStats();
|
|
8158
8119
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
8159
8120
|
console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
|
|
@@ -8376,7 +8337,7 @@ profileCmd.command("binding <profile> <config>").description("Set a config's sch
|
|
|
8376
8337
|
process.exit(1);
|
|
8377
8338
|
}
|
|
8378
8339
|
});
|
|
8379
|
-
profileCmd.command("apply [id]").description("Apply all configs in a profile to disk").option("--dry-run", "preview without writing").option("--from <agent>", "
|
|
8340
|
+
profileCmd.command("apply [id]").description("Apply all configs in a profile to disk").option("--dry-run", "preview without writing").option("--from <agent>", "deprecated compatibility option; no heartbeat is sent").option("--delivery-verified", "deprecated compatibility option; Skills delivery is not verified here").option("--auto", "resolve the matching profile for the current machine").option("--hostname <hostname>", "override detected hostname for auto resolution").option("--os <os>", "override detected OS for auto resolution").option("--arch <arch>", "override detected arch for auto resolution").action(async (id, opts) => {
|
|
8380
8341
|
try {
|
|
8381
8342
|
const store = resolveConfigStore();
|
|
8382
8343
|
const { machine, profile } = await getMachineProfileContext(opts, store);
|
|
@@ -8413,14 +8374,6 @@ profileCmd.command("apply [id]").description("Apply all configs in a profile to
|
|
|
8413
8374
|
}
|
|
8414
8375
|
if (report.failures.length > 0)
|
|
8415
8376
|
process.exitCode = 1;
|
|
8416
|
-
const runtimeReport = await reconcileManagedSkillRuntimes({
|
|
8417
|
-
dryRun: opts.dryRun,
|
|
8418
|
-
agent: opts.from,
|
|
8419
|
-
deliveryVerified: opts.deliveryVerified
|
|
8420
|
-
});
|
|
8421
|
-
printManagedSkillRuntimeReport(runtimeReport);
|
|
8422
|
-
if (runtimeReport.failed > 0)
|
|
8423
|
-
process.exitCode = 1;
|
|
8424
8377
|
console.log(chalk.dim(`
|
|
8425
8378
|
${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${machine.os_family}/${machine.arch})`));
|
|
8426
8379
|
} catch (e) {
|
|
@@ -8592,7 +8545,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
8592
8545
|
process.exit(1);
|
|
8593
8546
|
}
|
|
8594
8547
|
});
|
|
8595
|
-
sessionCmd.command("apply").description("Write a session render plan to its managed target home or explicit project root").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Sumi: resolved config from `sumi debug paths config`; Grok: GROK_HOME; Devin: user config directory (global AGENTS.md unless --project-root is set)").option("--project-root <path>", "repository root for project-scoped adapters such as Cursor").option("--session-id <id>", "session id to include in the manifest").option("--source <layer:id=path>", `instruction source file; layers: ${SESSION_SOURCE_LAYER_HELP}`, collectOption, []).option("--config <layer:id-or-slug>", "stored config source by id/slug; repeatable; layer aliases match --source", collectOption, []).option("--identity-export <path>", "OpenIdentities configs instruction export JSON; repeatable", collectOption, []).option("--replace-source <replacer-id>[=<target-source-id>]", "source id that broadly replaces earlier layers, or targets one earlier source", collectOption, []).option("--compile-profile <id-or-slug>", "compile persisted config bindings from this Instructions profile").option("--provider-version <semver>", "installed provider version used for capability matching").option("--provider-variant <variant>", "explicit provider capability variant (for example OpenCode v2-agents)").option("--model <model>", "active model used for model activation").option("--path <path>", "active path recorded in the graph context").option("--manual <config-id-or-slug>", "manually activate a binding; repeatable", collectOption, []).option("--asset-surface <surface>", "explicit provider asset surface (for example cline cli or ide)").option("--asset-scope <scope>", "asset scope (global|project|session)").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render").option("--check-global-coverage", "verify every registered, non-retired global-* source is in this render (expected read fresh from the registry, independent of this plan); on shortfall warn, exit non-zero, and for apply refuse to write (todos 102d6d0a, O15-00694)").option("--no-station-profile", "do not inject the cached station-profile source").option("--dry-run", "preview writes and conflicts without writing").option("--force", "overwrite existing unmanaged files").option("--adopt-file <relativepath=sha256>", "adopt one reviewed unmanaged generated target only at its exact observed SHA256; repeatable", collectOption, []).option("--reconcile-file <relativepath=sha256>", "reconcile one reviewed managed drifted target at its exact observed SHA256; requires --expected-manifest-sha256", collectOption, []).option("--expected-manifest-sha256 <sha256>", "require the previously reviewed manifest preimage to match exactly before applying").option("--json", "output apply JSON").action(async (opts) => {
|
|
8548
|
+
sessionCmd.command("apply").description("Write a session render plan to its managed target home or explicit project root").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Sumi: resolved config from `sumi debug paths config`; Grok: GROK_HOME; Devin: user config directory (global AGENTS.md unless --project-root is set)").option("--project-root <path>", "repository root for project-scoped adapters such as Cursor").option("--session-id <id>", "session id to include in the manifest").option("--source <layer:id=path>", `instruction source file; layers: ${SESSION_SOURCE_LAYER_HELP}`, collectOption, []).option("--config <layer:id-or-slug>", "stored config source by id/slug; repeatable; layer aliases match --source", collectOption, []).option("--identity-export <path>", "OpenIdentities configs instruction export JSON; repeatable", collectOption, []).option("--replace-source <replacer-id>[=<target-source-id>]", "source id that broadly replaces earlier layers, or targets one earlier source", collectOption, []).option("--compile-profile <id-or-slug>", "compile persisted config bindings from this Instructions profile").option("--provider-version <semver>", "installed provider version used for capability matching").option("--provider-variant <variant>", "explicit provider capability variant (for example OpenCode v2-agents)").option("--model <model>", "active model used for model activation").option("--path <path>", "active path recorded in the graph context").option("--manual <config-id-or-slug>", "manually activate a binding; repeatable", collectOption, []).option("--asset-surface <surface>", "explicit provider asset surface (for example cline cli or ide)").option("--asset-scope <scope>", "asset scope (global|project|session)").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render").option("--check-global-coverage", "verify every registered, non-retired global-* source is in this render (expected read fresh from the registry, independent of this plan); on shortfall warn, exit non-zero, and for apply refuse to write (todos 102d6d0a, O15-00694)").option("--no-station-profile", "do not inject the cached station-profile source").option("--dry-run", "preview writes and conflicts without writing").option("--force", "overwrite existing unmanaged files").option("--adopt-file <relativepath=sha256>", "adopt one reviewed unmanaged generated target only at its exact observed SHA256; repeatable", collectOption, []).option("--reconcile-file <relativepath=sha256>", "reconcile one reviewed managed drifted target at its exact observed SHA256; requires --expected-manifest-sha256", collectOption, []).option("--retire-file <relativepath=sha256>", "retire one reviewed obsolete managed fragment, rule, or asset at its exact SHA256; requires --expected-manifest-sha256", collectOption, []).option("--expected-manifest-sha256 <sha256>", "require the previously reviewed manifest preimage to match exactly before applying").option("--json", "output apply JSON").action(async (opts) => {
|
|
8596
8549
|
try {
|
|
8597
8550
|
const tool = opts.tool;
|
|
8598
8551
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -8626,7 +8579,8 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8626
8579
|
});
|
|
8627
8580
|
const adoptFiles = parsePreimages(opts.adoptFile, "--adopt-file");
|
|
8628
8581
|
const reconcileFiles = parsePreimages(opts.reconcileFile, "--reconcile-file");
|
|
8629
|
-
const
|
|
8582
|
+
const retireFiles = parsePreimages(opts.retireFile, "--retire-file");
|
|
8583
|
+
const result = applySessionRender(plan, { dryRun: opts.dryRun, force: opts.force, adoptFiles, reconcileFiles, retireFiles, expectedManifestSha256: opts.expectedManifestSha256, ownedClaudeAuthorities });
|
|
8630
8584
|
if (opts.json) {
|
|
8631
8585
|
printJson({
|
|
8632
8586
|
...result,
|
|
@@ -8726,6 +8680,36 @@ sessionCmd.command("restore <snapshot>").description("Restore a session render s
|
|
|
8726
8680
|
process.exit(1);
|
|
8727
8681
|
}
|
|
8728
8682
|
});
|
|
8683
|
+
program.command("harness").description("Inspect native harness installations and portable path inputs").command("discover").description("Read paths without running harnesses, reading prompt bodies, or changing configuration").option("--owner-home <path>", "explicit owner home; otherwise HOME, USERPROFILE, then OS home").option("--project-root <path>", "separate repository scope; never redirects global configuration").option("--executable <tool=path>", "explicit executable path; repeat per harness", (value, previous) => [...previous, value], []).option("--config-dir <tool=path>", "reviewed native config root; repeat per harness", (value, previous) => [...previous, value], []).option("--json", "output the versioned discovery receipt").action((opts) => {
|
|
8684
|
+
try {
|
|
8685
|
+
const overrides = {};
|
|
8686
|
+
for (const [key, inputs] of [["executable", opts.executable], ["configDir", opts.configDir]]) {
|
|
8687
|
+
for (const input of inputs) {
|
|
8688
|
+
const separator = input.indexOf("=");
|
|
8689
|
+
const tool = input.slice(0, separator);
|
|
8690
|
+
if (separator < 1 || !DISCOVERABLE_HARNESSES.includes(tool))
|
|
8691
|
+
throw new Error("Expected claude|codex|opencode|sumi=path.");
|
|
8692
|
+
const entry = overrides[tool] ??= {};
|
|
8693
|
+
if (entry[key] !== undefined)
|
|
8694
|
+
throw new Error(`Duplicate ${key} for ${tool}.`);
|
|
8695
|
+
entry[key] = input.slice(separator + 1);
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
const result = discoverHarnesses({ ownerHome: opts.ownerHome, projectRoot: opts.projectRoot, overrides });
|
|
8699
|
+
if (opts.json) {
|
|
8700
|
+
printJson(result);
|
|
8701
|
+
return;
|
|
8702
|
+
}
|
|
8703
|
+
for (const entry of result.tools) {
|
|
8704
|
+
printLine(`${entry.tool}: ${entry.status}; executable=${entry.executable?.path ?? "none"}; config=${entry.config?.path ?? "unresolved"}; version=not-probed`);
|
|
8705
|
+
for (const diagnostic of entry.diagnostics)
|
|
8706
|
+
printLine(` ${diagnostic}`);
|
|
8707
|
+
}
|
|
8708
|
+
} catch (error) {
|
|
8709
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
8710
|
+
process.exitCode = 1;
|
|
8711
|
+
}
|
|
8712
|
+
});
|
|
8729
8713
|
var stationProfileCmd = program.command("station-profile").description("Generate and inject the compact station profile block into every session render");
|
|
8730
8714
|
stationProfileCmd.command("refresh").description("Regenerate the cached station profile block (idempotent; writes only when changed)").option("--dry-run", "build and print the block without writing the cache").option("--json", "output JSON").action((opts) => {
|
|
8731
8715
|
try {
|
|
@@ -8955,7 +8939,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
|
|
|
8955
8939
|
} else if (opts.all) {
|
|
8956
8940
|
configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
|
|
8957
8941
|
} else {
|
|
8958
|
-
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-
|
|
8942
|
+
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-8763n4sc.js");
|
|
8959
8943
|
const slugs = [
|
|
8960
8944
|
...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
|
|
8961
8945
|
];
|
|
@@ -9242,14 +9226,14 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
|
|
|
9242
9226
|
} else if (target === "codex") {
|
|
9243
9227
|
const { appendFileSync, existsSync: ex, mkdirSync: md } = await import("fs");
|
|
9244
9228
|
const { dirname: dn, join: j } = await import("path");
|
|
9245
|
-
const configPath = j(
|
|
9229
|
+
const configPath = j(homedir5(), ".codex", "config.toml");
|
|
9246
9230
|
const block = `
|
|
9247
9231
|
[mcp_servers.configs]
|
|
9248
9232
|
command = "${mcpBinary}"
|
|
9249
9233
|
args = []
|
|
9250
9234
|
`;
|
|
9251
9235
|
if (ex(configPath)) {
|
|
9252
|
-
const content =
|
|
9236
|
+
const content = readFileSync7(configPath, "utf-8");
|
|
9253
9237
|
if (findTomlTableLines(content.split(`
|
|
9254
9238
|
`), "mcp_servers.configs")) {
|
|
9255
9239
|
console.log(chalk.dim("= Already installed in Codex"));
|
|
@@ -9262,7 +9246,7 @@ args = []
|
|
|
9262
9246
|
} else if (target === "antigravity") {
|
|
9263
9247
|
const { mkdirSync: md, readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
|
|
9264
9248
|
const { dirname: dn, join: j } = await import("path");
|
|
9265
|
-
const configPath = j(
|
|
9249
|
+
const configPath = j(homedir5(), ".gemini", "config", "mcp_config.json");
|
|
9266
9250
|
let settings = {};
|
|
9267
9251
|
if (ex(configPath)) {
|
|
9268
9252
|
try {
|
|
@@ -9306,12 +9290,12 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
9306
9290
|
} else if (target === "codex") {
|
|
9307
9291
|
const { existsSync: ex, writeFileSync: wf } = await import("fs");
|
|
9308
9292
|
const { join: j } = await import("path");
|
|
9309
|
-
const configPath = j(
|
|
9293
|
+
const configPath = j(homedir5(), ".codex", "config.toml");
|
|
9310
9294
|
if (!ex(configPath)) {
|
|
9311
9295
|
console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
|
|
9312
9296
|
continue;
|
|
9313
9297
|
}
|
|
9314
|
-
const content =
|
|
9298
|
+
const content = readFileSync7(configPath, "utf-8");
|
|
9315
9299
|
const lines = content.split(`
|
|
9316
9300
|
`);
|
|
9317
9301
|
const table = findTomlTableLines(lines, "mcp_servers.configs");
|
|
@@ -9330,7 +9314,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
9330
9314
|
console.log(chalk.green("\u2713") + " Removed from Codex");
|
|
9331
9315
|
} else if (target === "antigravity") {
|
|
9332
9316
|
const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
|
|
9333
|
-
const configPath =
|
|
9317
|
+
const configPath = join12(homedir5(), ".gemini", "config", "mcp_config.json");
|
|
9334
9318
|
if (!ex(configPath)) {
|
|
9335
9319
|
console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
|
|
9336
9320
|
continue;
|
|
@@ -9365,15 +9349,15 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
9365
9349
|
if (!opts.confirmLocal) {
|
|
9366
9350
|
throw new Error("migrate-legacy requires --confirm-local");
|
|
9367
9351
|
}
|
|
9368
|
-
const destinationPath =
|
|
9352
|
+
const destinationPath = resolve8(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join12(getRawStoreRoot(), "instructions.db"));
|
|
9369
9353
|
const database = await import("../chunks/database-wstmr56z.js");
|
|
9370
9354
|
database.getDatabase(destinationPath);
|
|
9371
9355
|
database.resetDatabase();
|
|
9372
9356
|
const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
|
|
9373
9357
|
const result = migrateLegacyInstructionsStore({
|
|
9374
|
-
...opts.source ? { sourcePath:
|
|
9358
|
+
...opts.source ? { sourcePath: resolve8(opts.source) } : {},
|
|
9375
9359
|
destinationPath,
|
|
9376
|
-
...opts.backup ? { backupPath:
|
|
9360
|
+
...opts.backup ? { backupPath: resolve8(opts.backup) } : {},
|
|
9377
9361
|
dryRun: !opts.apply,
|
|
9378
9362
|
...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
|
|
9379
9363
|
operatorConfirmed: true
|
|
@@ -9452,7 +9436,7 @@ DB stats:`));
|
|
|
9452
9436
|
if (count > 0)
|
|
9453
9437
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
9454
9438
|
}
|
|
9455
|
-
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
9439
|
+
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join12(getRawStoreRoot(), "instructions.db");
|
|
9456
9440
|
console.log(chalk.dim(`
|
|
9457
9441
|
${store.mode === "api" ? "API" : "DB"}: ${location}`));
|
|
9458
9442
|
});
|
|
@@ -9470,11 +9454,11 @@ program.command("status").description("Health check: total configs, drift from d
|
|
|
9470
9454
|
console.log(chalk.cyan("Missing:") + ` ${status.health.missingTargets === 0 ? chalk.green("0") : chalk.yellow(String(status.health.missingTargets))} (file not on disk)`);
|
|
9471
9455
|
console.log(chalk.cyan("Secrets:") + ` ${status.health.unredactedSecretFindings === 0 ? chalk.green("0 \u2713") : chalk.red(String(status.health.unredactedSecretFindings) + " \u26A0")} unredacted`);
|
|
9472
9456
|
console.log(chalk.cyan("Retired agents:") + ` ${status.health.retiredAgentRows === 0 ? chalk.green("0") : chalk.yellow(String(status.health.retiredAgentRows))} row(s)`);
|
|
9473
|
-
console.log(chalk.cyan("
|
|
9457
|
+
console.log(chalk.cyan("Legacy Inbox migration:") + ` ${status.health.missingManagedSkillRuntimes === 0 ? chalk.green("no legacy paths found") : chalk.yellow("native paths need Skills CLI review")} (Skills availability is not verified here)`);
|
|
9474
9458
|
console.log(chalk.cyan("Templates:") + ` ${status.counts.configs.templates} (with {{VAR}} placeholders)`);
|
|
9475
9459
|
});
|
|
9476
|
-
var managedSkillsCmd = program.command("managed-skills").description("
|
|
9477
|
-
managedSkillsCmd.command("status").option("--from <agent>", "
|
|
9460
|
+
var managedSkillsCmd = program.command("managed-skills").description("Report retired native Inbox skill paths; manage skills through the Skills CLI");
|
|
9461
|
+
managedSkillsCmd.command("status").option("--from <agent>", "deprecated compatibility option; no heartbeat is sent").option("--delivery-verified", "deprecated compatibility option; Skills delivery is not verified here").option("--json", "output the read-only legacy migration status as JSON").action((opts) => {
|
|
9478
9462
|
const report = inspectManagedSkillRuntimes({
|
|
9479
9463
|
agent: opts.from,
|
|
9480
9464
|
deliveryVerified: opts.deliveryVerified
|
|
@@ -9486,7 +9470,7 @@ managedSkillsCmd.command("status").option("--from <agent>", "verify the hosted C
|
|
|
9486
9470
|
return;
|
|
9487
9471
|
}
|
|
9488
9472
|
if (report.skills_present === 0) {
|
|
9489
|
-
console.log(chalk.dim("No
|
|
9473
|
+
console.log(chalk.dim("No legacy native Inbox skill found. Use the Skills CLI to inspect selected skills."));
|
|
9490
9474
|
return;
|
|
9491
9475
|
}
|
|
9492
9476
|
for (const runtime of report.runtimes) {
|
|
@@ -9498,7 +9482,7 @@ managedSkillsCmd.command("status").option("--from <agent>", "verify the hosted C
|
|
|
9498
9482
|
if (report.missing > 0)
|
|
9499
9483
|
process.exitCode = 1;
|
|
9500
9484
|
});
|
|
9501
|
-
managedSkillsCmd.command("apply").option("--dry-run", "preview without writing").option("--from <agent>", "
|
|
9485
|
+
managedSkillsCmd.command("apply").description("Deprecated: report migration requirements without repairing native skill payloads").option("--dry-run", "preview without writing").option("--from <agent>", "deprecated compatibility option; no heartbeat is sent").option("--delivery-verified", "deprecated compatibility option; Skills delivery is not verified here").option("--json", "output the read-only retirement report as JSON").action(async (opts) => {
|
|
9502
9486
|
const report = await reconcileManagedSkillRuntimes({
|
|
9503
9487
|
dryRun: opts.dryRun,
|
|
9504
9488
|
agent: opts.from,
|
|
@@ -9553,8 +9537,8 @@ storageCmd.command("status").description("Show redacted S3 configuration status
|
|
|
9553
9537
|
});
|
|
9554
9538
|
var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
|
|
9555
9539
|
storageBackupCmd.command("push <file>").description("Upload an immutable backup archive and integrity manifest").requiredOption("--id <backup-id>", "stable immutable backup identifier").option("--content-type <type>", "payload content type", "application/gzip").option("--dry-run", "plan keys and hashes without contacting S3").option("--json", "output JSON").action(async (file, opts) => {
|
|
9556
|
-
const abs =
|
|
9557
|
-
if (!
|
|
9540
|
+
const abs = resolve8(file);
|
|
9541
|
+
if (!existsSync10(abs) || !lstatSync6(abs).isFile())
|
|
9558
9542
|
throw new Error(`Backup file not found: ${abs}`);
|
|
9559
9543
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
9560
9544
|
import("../chunks/s3-config-hhght91k.js"),
|
|
@@ -9644,19 +9628,19 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9644
9628
|
backupId,
|
|
9645
9629
|
...versions
|
|
9646
9630
|
});
|
|
9647
|
-
const output =
|
|
9648
|
-
if (
|
|
9631
|
+
const output = resolve8(opts.output);
|
|
9632
|
+
if (existsSync10(output)) {
|
|
9649
9633
|
if (!opts.force)
|
|
9650
9634
|
throw new Error(`Refusing to overwrite existing backup file: ${output}`);
|
|
9651
|
-
if (!
|
|
9635
|
+
if (!lstatSync6(output).isFile() || lstatSync6(output).isSymbolicLink()) {
|
|
9652
9636
|
throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
|
|
9653
9637
|
}
|
|
9654
9638
|
}
|
|
9655
|
-
|
|
9656
|
-
const temp =
|
|
9639
|
+
mkdirSync5(dirname7(output), { recursive: true, mode: 448 });
|
|
9640
|
+
const temp = join12(dirname7(output), `.${basename2(output)}.tmp-${process.pid}-${Date.now()}`);
|
|
9657
9641
|
try {
|
|
9658
|
-
|
|
9659
|
-
|
|
9642
|
+
writeFileSync4(temp, pulled.bytes, { flag: "wx", mode: 384 });
|
|
9643
|
+
renameSync(temp, output);
|
|
9660
9644
|
} catch (error) {
|
|
9661
9645
|
try {
|
|
9662
9646
|
unlinkSync(temp);
|
|
@@ -9678,10 +9662,10 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9678
9662
|
});
|
|
9679
9663
|
program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
|
|
9680
9664
|
const { mkdirSync: mk } = await import("fs");
|
|
9681
|
-
const backupDir =
|
|
9665
|
+
const backupDir = join12(getRawStoreRoot(), "backups");
|
|
9682
9666
|
mk(backupDir, { recursive: true });
|
|
9683
9667
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
|
|
9684
|
-
const outPath =
|
|
9668
|
+
const outPath = join12(backupDir, `configs-${ts}.tar.gz`);
|
|
9685
9669
|
const result = await exportConfigs(outPath, { store: resolveConfigStore() });
|
|
9686
9670
|
const { statSync: st } = await import("fs");
|
|
9687
9671
|
const size = st(outPath).size;
|
|
@@ -9707,9 +9691,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
9707
9691
|
console.log(chalk.cyan("Known files on disk:"));
|
|
9708
9692
|
for (const k of KNOWN_CONFIGS) {
|
|
9709
9693
|
if (k.rulesDir) {
|
|
9710
|
-
|
|
9694
|
+
existsSync10(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
|
|
9711
9695
|
} else {
|
|
9712
|
-
|
|
9696
|
+
existsSync10(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
|
|
9713
9697
|
}
|
|
9714
9698
|
}
|
|
9715
9699
|
const allConfigs = await store.listConfigs();
|
|
@@ -9855,7 +9839,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9855
9839
|
const store = resolveConfigStore();
|
|
9856
9840
|
const interval = Number(opts.interval);
|
|
9857
9841
|
const { statSync: st } = await import("fs");
|
|
9858
|
-
const { expandPath: expandPath2 } = await import("../chunks/apply-
|
|
9842
|
+
const { expandPath: expandPath2 } = await import("../chunks/apply-1cpkx082.js");
|
|
9859
9843
|
console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
|
|
9860
9844
|
console.log(chalk.dim(`Watching known config files for changes\u2026
|
|
9861
9845
|
`));
|
|
@@ -9863,16 +9847,16 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9863
9847
|
for (const k of KNOWN_CONFIGS) {
|
|
9864
9848
|
if (k.rulesDir) {
|
|
9865
9849
|
const absDir = expandPath2(k.rulesDir);
|
|
9866
|
-
if (!
|
|
9850
|
+
if (!existsSync10(absDir))
|
|
9867
9851
|
continue;
|
|
9868
9852
|
const { readdirSync: readdirSync3 } = await import("fs");
|
|
9869
9853
|
for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9870
|
-
const abs =
|
|
9854
|
+
const abs = join12(absDir, f);
|
|
9871
9855
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9872
9856
|
}
|
|
9873
9857
|
} else {
|
|
9874
9858
|
const abs = expandPath2(k.path);
|
|
9875
|
-
if (
|
|
9859
|
+
if (existsSync10(abs))
|
|
9876
9860
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9877
9861
|
}
|
|
9878
9862
|
}
|
|
@@ -9880,7 +9864,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9880
9864
|
const tick = async () => {
|
|
9881
9865
|
let changed = 0;
|
|
9882
9866
|
for (const [abs, oldMtime] of mtimes) {
|
|
9883
|
-
if (!
|
|
9867
|
+
if (!existsSync10(abs))
|
|
9884
9868
|
continue;
|
|
9885
9869
|
const newMtime = st(abs).mtimeMs;
|
|
9886
9870
|
if (newMtime !== oldMtime) {
|
|
@@ -9892,10 +9876,10 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9892
9876
|
for (const k of KNOWN_CONFIGS) {
|
|
9893
9877
|
if (k.rulesDir) {
|
|
9894
9878
|
const absDir = expandPath2(k.rulesDir);
|
|
9895
|
-
if (!
|
|
9879
|
+
if (!existsSync10(absDir))
|
|
9896
9880
|
continue;
|
|
9897
9881
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9898
|
-
const abs =
|
|
9882
|
+
const abs = join12(absDir, f);
|
|
9899
9883
|
if (!mtimes.has(abs)) {
|
|
9900
9884
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9901
9885
|
changed++;
|
|
@@ -9903,7 +9887,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9903
9887
|
}
|
|
9904
9888
|
} else {
|
|
9905
9889
|
const abs = expandPath2(k.path);
|
|
9906
|
-
if (
|
|
9890
|
+
if (existsSync10(abs) && !mtimes.has(abs)) {
|
|
9907
9891
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9908
9892
|
changed++;
|
|
9909
9893
|
}
|
|
@@ -9931,11 +9915,11 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
9931
9915
|
if (!c.target_path)
|
|
9932
9916
|
continue;
|
|
9933
9917
|
const abs = expandPath(c.target_path);
|
|
9934
|
-
if (!
|
|
9918
|
+
if (!existsSync10(abs)) {
|
|
9935
9919
|
missing++;
|
|
9936
9920
|
continue;
|
|
9937
9921
|
}
|
|
9938
|
-
const disk =
|
|
9922
|
+
const disk = readFileSync7(abs, "utf-8");
|
|
9939
9923
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
|
|
9940
9924
|
if (redactedDisk !== c.content)
|
|
9941
9925
|
drifted++;
|
|
@@ -10002,7 +9986,7 @@ program.command("clean").description("Remove configs from DB whose target files
|
|
|
10002
9986
|
if (!c.target_path)
|
|
10003
9987
|
continue;
|
|
10004
9988
|
const abs = expandPath(c.target_path);
|
|
10005
|
-
if (!
|
|
9989
|
+
if (!existsSync10(abs)) {
|
|
10006
9990
|
if (printed < maxPrinted) {
|
|
10007
9991
|
if (opts.dryRun) {
|
|
10008
9992
|
console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
|
|
@@ -10026,7 +10010,7 @@ ${removed} orphaned config(s) ${opts.dryRun ? "found" : "removed"}${omitted > 0
|
|
|
10026
10010
|
console.log(chalk.dim(`Use --limit ${removed} to print every orphan row.`));
|
|
10027
10011
|
}
|
|
10028
10012
|
});
|
|
10029
|
-
program.command("bootstrap").description("Install the full @hasna ecosystem: CLI tools + MCP servers + configs").option("--dry-run", "show what would be installed without doing it").option("--from <agent>", "
|
|
10013
|
+
program.command("bootstrap").description("Install the full @hasna ecosystem: CLI tools + MCP servers + configs").option("--dry-run", "show what would be installed without doing it").option("--from <agent>", "deprecated compatibility option; no heartbeat is sent").option("--delivery-verified", "deprecated compatibility option; Skills delivery is not verified here").option("--skip-mcp", "skip MCP server registration").action(async (opts) => {
|
|
10030
10014
|
const store = resolveConfigStore();
|
|
10031
10015
|
const packages = [
|
|
10032
10016
|
{ name: "@hasna/todos", bin: "todos", mcp: "todos-mcp" },
|
|
@@ -10086,20 +10070,8 @@ Initializing configs:`));
|
|
|
10086
10070
|
} else {
|
|
10087
10071
|
console.log(chalk.dim(" would run: configs init"));
|
|
10088
10072
|
}
|
|
10089
|
-
console.log(chalk.
|
|
10090
|
-
|
|
10091
|
-
const runtimeReport = await reconcileManagedSkillRuntimes({
|
|
10092
|
-
dryRun: opts.dryRun,
|
|
10093
|
-
agent: opts.from,
|
|
10094
|
-
deliveryVerified: opts.deliveryVerified
|
|
10095
|
-
});
|
|
10096
|
-
printManagedSkillRuntimeReport(runtimeReport);
|
|
10097
|
-
if (runtimeReport.failed > 0) {
|
|
10098
|
-
console.error(chalk.red(`
|
|
10099
|
-
Bootstrap incomplete: a managed skill runtime could not be reconciled.`));
|
|
10100
|
-
process.exitCode = 1;
|
|
10101
|
-
return;
|
|
10102
|
-
}
|
|
10073
|
+
console.log(chalk.dim(`
|
|
10074
|
+
Manage skill publication, selection and agent hooks through the Skills CLI.`));
|
|
10103
10075
|
console.log(chalk.bold(`
|
|
10104
10076
|
\u2713 Bootstrap complete.`) + chalk.dim(" Restart agent sessions to load updated integrations."));
|
|
10105
10077
|
});
|
|
@@ -10145,7 +10117,7 @@ providerContextCmd.command("resolve").description("Resolve the endpoint to a pro
|
|
|
10145
10117
|
try {
|
|
10146
10118
|
const rawEndpoint = opts.endpoint ?? process.env["ANTHROPIC_BASE_URL"] ?? "";
|
|
10147
10119
|
const rawModel = opts.model ?? process.env["ANTHROPIC_MODEL"] ?? "";
|
|
10148
|
-
const homeDir2 = opts.home ??
|
|
10120
|
+
const homeDir2 = opts.home ?? homedir5();
|
|
10149
10121
|
const origin = normalizeEndpointOrigin(rawEndpoint);
|
|
10150
10122
|
const resolution = resolveAndRenderProviderContext({
|
|
10151
10123
|
origin,
|