@hasna/instructions 0.7.1 → 0.7.4
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 +47 -7
- package/dist/chunks/{apply-c3sd4agz.js → apply-1gyr8r0x.js} +3 -3
- package/dist/chunks/{apply-pnn7a3y7.js → apply-fyfzqc7s.js} +3 -3
- package/dist/chunks/{index-3djthcm6.js → index-27az1qb1.js} +28 -1
- package/dist/chunks/{index-r7nfef2g.js → index-6sbmg1vc.js} +3 -3
- package/dist/chunks/{index-pdpg8a44.js → index-axs3yftz.js} +2 -2
- package/dist/chunks/{index-aj66f6xr.js → index-dwagz53p.js} +92 -8
- package/dist/chunks/{index-6jncnnw7.js → index-enh4yv85.js} +274 -31
- package/dist/chunks/{index-ypcef3a2.js → index-n9arv47w.js} +2 -2
- package/dist/chunks/{index-n3fjspr0.js → index-preyvj8n.js} +28 -1
- package/dist/chunks/{index-jg8ptcwh.js → index-z27xp7fh.js} +2 -2
- package/dist/chunks/{local-0a9gja58.js → local-5dsm856v.js} +15 -4
- package/dist/chunks/{local-969ye538.js → local-yxsbs8x3.js} +15 -4
- package/dist/chunks/{sync-xtkbm5br.js → sync-6r1v8hd3.js} +4 -4
- package/dist/chunks/{sync-q5hvwq6e.js → sync-k3pab4s6.js} +4 -4
- package/dist/chunks/{template-wgnzdn7h.js → template-e4qrxs8p.js} +1 -1
- package/dist/chunks/{template-fsfxpe4p.js → template-nhn124zd.js} +1 -1
- package/dist/cli/index.js +1018 -584
- package/dist/data/config-store.d.ts +12 -4
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/configs.d.ts.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1264 -601
- package/dist/lib/asset-plan.d.ts +4 -1
- package/dist/lib/asset-plan.d.ts.map +1 -1
- package/dist/lib/codewith-shared-todos-storage-standard.d.ts.map +1 -1
- package/dist/lib/dangerous-operation-guard-standard.d.ts.map +1 -1
- package/dist/lib/global-agent-rules-standard.d.ts.map +1 -1
- package/dist/lib/global-source-coverage.d.ts +12 -7
- package/dist/lib/global-source-coverage.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/instruction-graph.d.ts +1 -1
- package/dist/lib/instruction-graph.d.ts.map +1 -1
- package/dist/lib/instruction-source-policy.d.ts +6 -0
- package/dist/lib/instruction-source-policy.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/platform-profiles.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/session-apply.d.ts +35 -0
- package/dist/lib/session-apply.d.ts.map +1 -1
- package/dist/lib/session-refresh.d.ts +30 -0
- package/dist/lib/session-refresh.d.ts.map +1 -0
- package/dist/lib/session-render.d.ts +41 -1
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/lib/transforms.d.ts +1 -0
- package/dist/lib/transforms.d.ts.map +1 -1
- package/dist/mcp/index.js +13 -12
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +172 -0
- package/dist/sdk/v1-client.d.ts +7 -1
- package/dist/sdk/v1-client.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +26 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +199 -8
- package/dist/server/openapi.d.ts +150 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/types/index.d.ts +24 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,31 +9,39 @@ import {
|
|
|
9
9
|
syncKnown,
|
|
10
10
|
syncProject,
|
|
11
11
|
syncToDisk
|
|
12
|
-
} from "../chunks/index-
|
|
12
|
+
} from "../chunks/index-axs3yftz.js";
|
|
13
13
|
import {
|
|
14
14
|
applyConfigsWithReport,
|
|
15
15
|
compactPathForConfigHome,
|
|
16
16
|
expandPath,
|
|
17
|
-
normalizeTargetPath
|
|
18
|
-
|
|
17
|
+
normalizeTargetPath,
|
|
18
|
+
sessionRenderOwnsPath
|
|
19
|
+
} from "../chunks/index-6sbmg1vc.js";
|
|
19
20
|
import {
|
|
21
|
+
CODEWITH_NATIVE_IMPORTS_ENV,
|
|
20
22
|
CloudConfigStore,
|
|
21
23
|
DEFAULT_LIST_LIMIT,
|
|
22
24
|
PROJECT_CONTEXT_MAX_INPUT_BYTES,
|
|
23
25
|
ProjectContextError,
|
|
24
26
|
SESSION_INSTRUCTION_LAYERS,
|
|
25
27
|
SESSION_MANAGED_INPUT_MAX_BYTES,
|
|
28
|
+
SESSION_RENDERER_OWNER_ID,
|
|
26
29
|
SESSION_RENDER_MANAGED_MARKER,
|
|
30
|
+
SESSION_RENDER_MANIFEST_RELATIVE_PATH,
|
|
27
31
|
SESSION_RENDER_SCHEMA,
|
|
28
32
|
SESSION_RENDER_TOOLS,
|
|
29
33
|
applyProjectContext,
|
|
34
|
+
assertInstructionSource,
|
|
30
35
|
detectClaudeAuthorityConflicts,
|
|
31
36
|
detectCursorAuthorityConflicts,
|
|
32
37
|
detectMachineContext,
|
|
33
38
|
ensureCodewithSharedTodosStorageStandardConfig,
|
|
34
39
|
ensureGlobalAgentRulesStandardConfig,
|
|
40
|
+
exports_external,
|
|
35
41
|
formatCliError,
|
|
36
42
|
getSessionRenderSnapshotDir,
|
|
43
|
+
instructionSourceRejection,
|
|
44
|
+
isRetiredInstructionSource,
|
|
37
45
|
isRetiredOrUnsupportedConfigAgent,
|
|
38
46
|
normalizeProfileAssetBinding,
|
|
39
47
|
normalizeProfileConfigBinding,
|
|
@@ -61,11 +69,14 @@ import {
|
|
|
61
69
|
truncateText,
|
|
62
70
|
withProjectContextSessionGuard,
|
|
63
71
|
writeProjectContextCoordinatedFile
|
|
64
|
-
} from "../chunks/index-
|
|
72
|
+
} from "../chunks/index-enh4yv85.js";
|
|
65
73
|
import {
|
|
74
|
+
ConfigNotFoundError,
|
|
66
75
|
INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA,
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
ProfileNotFoundError,
|
|
77
|
+
extractTemplateVars,
|
|
78
|
+
validateExpectedConfigVersion
|
|
79
|
+
} from "../chunks/index-preyvj8n.js";
|
|
69
80
|
import {
|
|
70
81
|
slugify
|
|
71
82
|
} from "../chunks/index-ma2xmp6c.js";
|
|
@@ -3784,9 +3795,162 @@ var {
|
|
|
3784
3795
|
|
|
3785
3796
|
// src/cli/index.tsx
|
|
3786
3797
|
import chalk from "chalk";
|
|
3787
|
-
import { existsSync as
|
|
3788
|
-
import { homedir as
|
|
3789
|
-
|
|
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";
|
|
3790
3954
|
|
|
3791
3955
|
// src/lib/config-target-identity.ts
|
|
3792
3956
|
function findConfigsByTargetPath(configs, targetPath) {
|
|
@@ -3827,7 +3991,7 @@ function findDuplicateTargetPathGroups(configs) {
|
|
|
3827
3991
|
// src/lib/export.ts
|
|
3828
3992
|
import { createHash } from "crypto";
|
|
3829
3993
|
import { existsSync as existsSync3, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3830
|
-
import { dirname, join as
|
|
3994
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
3831
3995
|
import { tmpdir } from "os";
|
|
3832
3996
|
var ARCHIVE_EXCLUSIONS = [
|
|
3833
3997
|
{
|
|
@@ -4163,13 +4327,13 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
4163
4327
|
integrity,
|
|
4164
4328
|
exclusions: ARCHIVE_EXCLUSIONS
|
|
4165
4329
|
};
|
|
4166
|
-
const absOutput =
|
|
4167
|
-
mkdirSync(
|
|
4168
|
-
const stagingDir = mkdtempSync(
|
|
4330
|
+
const absOutput = resolve3(outputPath);
|
|
4331
|
+
mkdirSync(dirname2(absOutput), { recursive: true });
|
|
4332
|
+
const stagingDir = mkdtempSync(join5(tmpdir(), "instructions-domain-export-"));
|
|
4169
4333
|
try {
|
|
4170
|
-
writeFileSync(
|
|
4334
|
+
writeFileSync(join5(stagingDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4171
4335
|
`, "utf-8");
|
|
4172
|
-
writeFileSync(
|
|
4336
|
+
writeFileSync(join5(stagingDir, "domain.json"), payload, "utf-8");
|
|
4173
4337
|
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", stagingDir, "manifest.json", "domain.json"], {
|
|
4174
4338
|
stdout: "pipe",
|
|
4175
4339
|
stderr: "pipe"
|
|
@@ -4188,7 +4352,7 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
4188
4352
|
|
|
4189
4353
|
// src/lib/import.ts
|
|
4190
4354
|
import { createHash as createHash2 } from "crypto";
|
|
4191
|
-
import { resolve as
|
|
4355
|
+
import { resolve as resolve4 } from "path";
|
|
4192
4356
|
function compareText2(left, right) {
|
|
4193
4357
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
4194
4358
|
}
|
|
@@ -4476,7 +4640,7 @@ async function importV1(reader, manifest, store, conflict) {
|
|
|
4476
4640
|
async function importConfigs(bundlePath, opts = {}) {
|
|
4477
4641
|
const store = opts.store ?? resolveConfigStore();
|
|
4478
4642
|
const conflict = opts.conflict ?? "skip";
|
|
4479
|
-
const reader = await openArchive(
|
|
4643
|
+
const reader = await openArchive(resolve4(bundlePath));
|
|
4480
4644
|
const manifestText = await reader.read("manifest.json");
|
|
4481
4645
|
let manifest;
|
|
4482
4646
|
try {
|
|
@@ -4502,13 +4666,13 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
4502
4666
|
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
4503
4667
|
import {
|
|
4504
4668
|
existsSync as existsSync5,
|
|
4505
|
-
lstatSync,
|
|
4669
|
+
lstatSync as lstatSync2,
|
|
4506
4670
|
mkdirSync as mkdirSync2,
|
|
4507
4671
|
readFileSync,
|
|
4508
4672
|
readdirSync,
|
|
4509
|
-
statSync
|
|
4673
|
+
statSync as statSync2
|
|
4510
4674
|
} from "fs";
|
|
4511
|
-
import { dirname as
|
|
4675
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, parse, posix, relative, resolve as resolve5 } from "path";
|
|
4512
4676
|
class SessionApplyError extends Error {
|
|
4513
4677
|
constructor(message) {
|
|
4514
4678
|
super(message);
|
|
@@ -4530,15 +4694,23 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4530
4694
|
const targetHome = assertSafeTargetHome(plan.targetHome);
|
|
4531
4695
|
assertClaudeAuthorityStillClear(plan, targetHome, options.ownedClaudeAuthorities);
|
|
4532
4696
|
const payloadFiles = [...plan.files, ...plan.assetFiles ?? []];
|
|
4533
|
-
const files = [...payloadFiles, plan.manifestFile];
|
|
4534
4697
|
const manifestPath = resolvePlannedFilePath(plan, plan.manifestFile, targetHome);
|
|
4698
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4535
4699
|
const previousManifest = readPreviousManifest(manifestPath);
|
|
4536
4700
|
const previousHashes = previousManifest ? new Map(previousManifest.files.map((file) => [file.relativePath, file.sha256])) : new Map;
|
|
4701
|
+
const adoptions = validateFileAdoptions(plan, targetHome, previousHashes, options);
|
|
4702
|
+
const adoptedHashes = new Map(adoptions.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4703
|
+
const reconciliations = validateFileReconciliations(plan, targetHome, previousManifest, options);
|
|
4704
|
+
const reconciledHashes = new Map(reconciliations.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
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);
|
|
4708
|
+
const files = [...payloadFiles, manifestFile];
|
|
4537
4709
|
const currentRelativePaths = new Set(files.map((file) => file.relativePath));
|
|
4538
4710
|
const drift = checkSessionRenderDrift(targetHome, manifestPath);
|
|
4539
4711
|
const results = [
|
|
4540
|
-
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options)),
|
|
4541
|
-
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options)
|
|
4712
|
+
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes)),
|
|
4713
|
+
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options, retiredHashes)
|
|
4542
4714
|
];
|
|
4543
4715
|
assertNotSilentManagedWipeout(plan, results);
|
|
4544
4716
|
const conflicts = results.filter((result) => result.action === "conflict");
|
|
@@ -4560,7 +4732,10 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4560
4732
|
skippedSources: plan.manifest.skippedSources,
|
|
4561
4733
|
files: results,
|
|
4562
4734
|
conflicts,
|
|
4563
|
-
drift
|
|
4735
|
+
drift,
|
|
4736
|
+
adoptions,
|
|
4737
|
+
reconciliations,
|
|
4738
|
+
retirements
|
|
4564
4739
|
};
|
|
4565
4740
|
}
|
|
4566
4741
|
let snapshotPath = null;
|
|
@@ -4573,10 +4748,16 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4573
4748
|
if (!options.dryRun) {
|
|
4574
4749
|
const allowPortableFallback = coordination === null;
|
|
4575
4750
|
const forcePortableFileOps = options.test_hooks?.force_portable_file_ops ?? false;
|
|
4751
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4576
4752
|
ensureSessionTargetHome(targetHome);
|
|
4577
|
-
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4753
|
+
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations, retirements);
|
|
4578
4754
|
snapshotPath = rollback.snapshotPath;
|
|
4579
4755
|
options.test_hooks?.before_apply_writes?.({ plan, results });
|
|
4756
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4757
|
+
for (const result of results) {
|
|
4758
|
+
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
4759
|
+
coordination?.assert_held();
|
|
4760
|
+
}
|
|
4580
4761
|
const resultsByPath = new Map(results.map((result) => [result.path, result]));
|
|
4581
4762
|
for (const file of payloadFiles) {
|
|
4582
4763
|
applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
@@ -4596,7 +4777,7 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4596
4777
|
});
|
|
4597
4778
|
coordination?.assert_held();
|
|
4598
4779
|
}
|
|
4599
|
-
applyPlannedFile(plan,
|
|
4780
|
+
applyPlannedFile(plan, manifestFile, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4600
4781
|
}
|
|
4601
4782
|
return {
|
|
4602
4783
|
dryRun: options.dryRun ?? false,
|
|
@@ -4610,9 +4791,233 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4610
4791
|
skippedSources: plan.manifest.skippedSources,
|
|
4611
4792
|
files: results,
|
|
4612
4793
|
conflicts,
|
|
4613
|
-
drift
|
|
4794
|
+
drift,
|
|
4795
|
+
adoptions,
|
|
4796
|
+
reconciliations,
|
|
4797
|
+
retirements
|
|
4614
4798
|
};
|
|
4615
4799
|
}
|
|
4800
|
+
function assertManifestPrecondition(path, targetHome, expected) {
|
|
4801
|
+
if (expected === undefined)
|
|
4802
|
+
return;
|
|
4803
|
+
if (!/^[a-f0-9]{64}$/.test(expected)) {
|
|
4804
|
+
throw new SessionApplyError("Expected session manifest SHA-256 must contain 64 lowercase hexadecimal characters.");
|
|
4805
|
+
}
|
|
4806
|
+
if (currentSessionFileHash(path, targetHome) !== expected) {
|
|
4807
|
+
throw new SessionApplyError("Session manifest SHA-256 precondition failed; reload the manifest and resolve a new plan.");
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
function validateFileAdoptions(plan, targetHome, previousHashes, options) {
|
|
4811
|
+
const requests = options.adoptFiles ?? [];
|
|
4812
|
+
if (!Array.isArray(requests))
|
|
4813
|
+
throw new SessionApplyError("Session file adoptions must be an array.");
|
|
4814
|
+
if (requests.length > 0 && options.force) {
|
|
4815
|
+
throw new SessionApplyError("Exact file adoption cannot be combined with force.");
|
|
4816
|
+
}
|
|
4817
|
+
const seen = new Set;
|
|
4818
|
+
return requests.map((request) => {
|
|
4819
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4820
|
+
throw new SessionApplyError("Each file adoption requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4821
|
+
}
|
|
4822
|
+
if (seen.has(request.relativePath)) {
|
|
4823
|
+
throw new SessionApplyError(`Duplicate file adoption target: ${request.relativePath}`);
|
|
4824
|
+
}
|
|
4825
|
+
seen.add(request.relativePath);
|
|
4826
|
+
const candidates = exactPreimageCandidates(plan).filter((file2) => file2.relativePath === request.relativePath);
|
|
4827
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4828
|
+
throw new SessionApplyError(`File adoption target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4829
|
+
}
|
|
4830
|
+
const file = candidates[0];
|
|
4831
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4832
|
+
if (previousHashes.has(file.relativePath) || sessionRenderOwnsPath(path)) {
|
|
4833
|
+
throw new SessionApplyError(`File adoption target is already managed: ${request.relativePath}`);
|
|
4834
|
+
}
|
|
4835
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "adoption");
|
|
4836
|
+
return {
|
|
4837
|
+
path,
|
|
4838
|
+
relativePath: file.relativePath,
|
|
4839
|
+
preimageSha256: observedSha256,
|
|
4840
|
+
renderedSha256: file.sha256,
|
|
4841
|
+
sourceIds: [...file.sourceIds]
|
|
4842
|
+
};
|
|
4843
|
+
});
|
|
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
|
+
}
|
|
4849
|
+
function readExactFilePreimage(path, targetHome, request, operation) {
|
|
4850
|
+
if (currentSessionFileHash(path, targetHome) === null) {
|
|
4851
|
+
throw new SessionApplyError(`File ${operation} target does not exist: ${request.relativePath}`);
|
|
4852
|
+
}
|
|
4853
|
+
const bytes = readFileSync(path);
|
|
4854
|
+
if (!bytes.equals(Buffer.from(bytes.toString("utf8"), "utf8"))) {
|
|
4855
|
+
throw new SessionApplyError(`File ${operation} target must contain losslessly restorable UTF-8: ${request.relativePath}`);
|
|
4856
|
+
}
|
|
4857
|
+
const observedSha256 = createHash3("sha256").update(bytes).digest("hex");
|
|
4858
|
+
if (observedSha256 !== request.sha256) {
|
|
4859
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${request.relativePath}`);
|
|
4860
|
+
}
|
|
4861
|
+
return observedSha256;
|
|
4862
|
+
}
|
|
4863
|
+
function validateFileReconciliations(plan, targetHome, previousManifest, options) {
|
|
4864
|
+
const requests = options.reconcileFiles ?? [];
|
|
4865
|
+
if (!Array.isArray(requests))
|
|
4866
|
+
throw new SessionApplyError("Session file reconciliations must be an array.");
|
|
4867
|
+
if (requests.length === 0)
|
|
4868
|
+
return [];
|
|
4869
|
+
if (options.force)
|
|
4870
|
+
throw new SessionApplyError("Exact file reconciliation cannot be combined with force.");
|
|
4871
|
+
if (options.expectedManifestSha256 === undefined) {
|
|
4872
|
+
throw new SessionApplyError("Exact file reconciliation requires an expected manifest SHA-256 precondition.");
|
|
4873
|
+
}
|
|
4874
|
+
if (!previousManifest || previousManifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || previousManifest.tool !== plan.tool || previousManifest.targetHome !== targetHome) {
|
|
4875
|
+
throw new SessionApplyError("File reconciliation requires this renderer's manifest for the same tool and target home.");
|
|
4876
|
+
}
|
|
4877
|
+
const seen = new Set;
|
|
4878
|
+
return requests.map((request) => {
|
|
4879
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4880
|
+
throw new SessionApplyError("Each file reconciliation requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4881
|
+
}
|
|
4882
|
+
if (seen.has(request.relativePath)) {
|
|
4883
|
+
throw new SessionApplyError(`Duplicate file reconciliation target: ${request.relativePath}`);
|
|
4884
|
+
}
|
|
4885
|
+
seen.add(request.relativePath);
|
|
4886
|
+
const candidates = exactPreimageCandidates(plan).filter((file2) => file2.relativePath === request.relativePath);
|
|
4887
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4888
|
+
throw new SessionApplyError(`File reconciliation target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4889
|
+
}
|
|
4890
|
+
const file = candidates[0];
|
|
4891
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4892
|
+
const owned = previousManifest.files.filter((entry) => entry.relativePath === file.relativePath);
|
|
4893
|
+
if (owned.length !== 1 || owned[0].path !== path || owned[0].role !== file.role || !/^[a-f0-9]{64}$/.test(owned[0].sha256)) {
|
|
4894
|
+
throw new SessionApplyError(`File reconciliation target is not owned by the observed manifest: ${request.relativePath}`);
|
|
4895
|
+
}
|
|
4896
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "reconciliation");
|
|
4897
|
+
if (observedSha256 === owned[0].sha256) {
|
|
4898
|
+
throw new SessionApplyError(`File reconciliation target has no managed drift: ${request.relativePath}`);
|
|
4899
|
+
}
|
|
4900
|
+
if (observedSha256 === file.sha256) {
|
|
4901
|
+
throw new SessionApplyError(`File reconciliation target already matches the planned output: ${request.relativePath}`);
|
|
4902
|
+
}
|
|
4903
|
+
return {
|
|
4904
|
+
path,
|
|
4905
|
+
relativePath: file.relativePath,
|
|
4906
|
+
preimageSha256: observedSha256,
|
|
4907
|
+
renderedSha256: file.sha256,
|
|
4908
|
+
previousManagedSha256: owned[0].sha256,
|
|
4909
|
+
sourceIds: [...file.sourceIds]
|
|
4910
|
+
};
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
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)
|
|
4954
|
+
return plan.manifestFile;
|
|
4955
|
+
if (previousManifest?.adoptions !== undefined && !Array.isArray(previousManifest.adoptions)) {
|
|
4956
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4957
|
+
}
|
|
4958
|
+
const entries = new Map;
|
|
4959
|
+
for (const entry of [
|
|
4960
|
+
...previousManifest?.adoptions ?? [],
|
|
4961
|
+
...adoptions.map(({ path: _path, ...receipt }) => receipt)
|
|
4962
|
+
]) {
|
|
4963
|
+
if (!entry || typeof entry.relativePath !== "string" || !/^[a-f0-9]{64}$/.test(entry.preimageSha256) || !/^[a-f0-9]{64}$/.test(entry.renderedSha256) || !Array.isArray(entry.sourceIds) || entry.sourceIds.some((id) => typeof id !== "string")) {
|
|
4964
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4965
|
+
}
|
|
4966
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4967
|
+
const previous = entries.get(entry.relativePath);
|
|
4968
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(entry)) {
|
|
4969
|
+
throw new SessionApplyError(`Conflicting session file adoption provenance: ${entry.relativePath}`);
|
|
4970
|
+
}
|
|
4971
|
+
entries.set(entry.relativePath, entry);
|
|
4972
|
+
}
|
|
4973
|
+
if (previousManifest?.reconciliations !== undefined && !Array.isArray(previousManifest.reconciliations)) {
|
|
4974
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4975
|
+
}
|
|
4976
|
+
const reconciledEntries = new Map;
|
|
4977
|
+
for (const entry of [
|
|
4978
|
+
...previousManifest?.reconciliations ?? [],
|
|
4979
|
+
...reconciliations.map(({ path: _path, ...receipt }) => receipt)
|
|
4980
|
+
]) {
|
|
4981
|
+
if (!entry || typeof entry.relativePath !== "string" || !/^[a-f0-9]{64}$/.test(entry.preimageSha256) || !/^[a-f0-9]{64}$/.test(entry.renderedSha256) || !/^[a-f0-9]{64}$/.test(entry.previousManagedSha256) || !Array.isArray(entry.sourceIds) || entry.sourceIds.some((id) => typeof id !== "string")) {
|
|
4982
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4983
|
+
}
|
|
4984
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4985
|
+
const canonical = {
|
|
4986
|
+
relativePath: entry.relativePath,
|
|
4987
|
+
preimageSha256: entry.preimageSha256,
|
|
4988
|
+
renderedSha256: entry.renderedSha256,
|
|
4989
|
+
previousManagedSha256: entry.previousManagedSha256,
|
|
4990
|
+
sourceIds: [...entry.sourceIds]
|
|
4991
|
+
};
|
|
4992
|
+
reconciledEntries.set(JSON.stringify(canonical), canonical);
|
|
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
|
+
}
|
|
5011
|
+
const manifest = {
|
|
5012
|
+
...plan.manifest,
|
|
5013
|
+
...adoptions.length > 0 || previousManifest?.adoptions !== undefined ? { adoptions: [...entries.values()] } : {},
|
|
5014
|
+
...reconciliations.length > 0 || previousManifest?.reconciliations !== undefined ? { reconciliations: [...reconciledEntries.values()] } : {},
|
|
5015
|
+
...retirements.length > 0 || previousManifest?.retirements !== undefined ? { retirements: [...retiredEntries.values()] } : {}
|
|
5016
|
+
};
|
|
5017
|
+
const content = `${JSON.stringify(manifest, null, 2)}
|
|
5018
|
+
`;
|
|
5019
|
+
return { ...plan.manifestFile, content, sha256: sha2563(content) };
|
|
5020
|
+
}
|
|
4616
5021
|
function assertCursorAuthorityUnchanged(plan) {
|
|
4617
5022
|
if (plan.tool !== "cursor" || plan.targetKind === "blocked")
|
|
4618
5023
|
return;
|
|
@@ -4645,7 +5050,7 @@ function ensureSessionTargetHome(targetHome) {
|
|
|
4645
5050
|
}
|
|
4646
5051
|
function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
4647
5052
|
const safeTargetHome = assertSafeTargetHome(targetHome);
|
|
4648
|
-
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative(safeTargetHome,
|
|
5053
|
+
const resolvedManifestPath = manifestPath ? resolveManifestRelativePath(relative(safeTargetHome, resolve5(manifestPath)), safeTargetHome) : resolve5(safeTargetHome, ".hasna", "session-render-manifest.json");
|
|
4649
5054
|
const checkedAt = new Date().toISOString();
|
|
4650
5055
|
const previousManifest = readPreviousManifest(resolvedManifestPath);
|
|
4651
5056
|
if (!previousManifest) {
|
|
@@ -4695,13 +5100,13 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
4695
5100
|
function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
|
|
4696
5101
|
const snapshot = readSessionRenderSnapshot(snapshotPath);
|
|
4697
5102
|
const targetHome = assertSafeTargetHome(snapshot.targetHome);
|
|
4698
|
-
const resolvedSnapshotPath =
|
|
5103
|
+
const resolvedSnapshotPath = resolve5(snapshotPath);
|
|
4699
5104
|
const snapshotDir = getSessionRenderSnapshotDir(targetHome);
|
|
4700
5105
|
const snapshotDirRelative = relative(snapshotDir, resolvedSnapshotPath);
|
|
4701
|
-
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !
|
|
5106
|
+
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute2(snapshotDirRelative);
|
|
4702
5107
|
if (!insideSnapshotDir) {
|
|
4703
5108
|
const snapshotRelativePath = relative(targetHome, resolvedSnapshotPath);
|
|
4704
|
-
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") ||
|
|
5109
|
+
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute2(snapshotRelativePath)) {
|
|
4705
5110
|
throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
|
|
4706
5111
|
}
|
|
4707
5112
|
}
|
|
@@ -4818,14 +5223,14 @@ function requiredRestoreHash(file) {
|
|
|
4818
5223
|
return file.previousSha256;
|
|
4819
5224
|
}
|
|
4820
5225
|
function readSessionRenderSnapshot(snapshotPath) {
|
|
4821
|
-
const resolved =
|
|
5226
|
+
const resolved = resolve5(snapshotPath);
|
|
4822
5227
|
if (!existsSync5(resolved))
|
|
4823
5228
|
throw new SessionApplyError(`Session snapshot not found: ${snapshotPath}`);
|
|
4824
|
-
const stat =
|
|
5229
|
+
const stat = lstatSync2(resolved);
|
|
4825
5230
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
4826
5231
|
throw new SessionApplyError(`Session snapshot is not a regular file: ${snapshotPath}`);
|
|
4827
5232
|
}
|
|
4828
|
-
if (
|
|
5233
|
+
if (statSync2(resolved).size > 32 * 1024 * 1024) {
|
|
4829
5234
|
throw new SessionApplyError(`Session snapshot exceeds the 32 MiB restore limit: ${snapshotPath}`);
|
|
4830
5235
|
}
|
|
4831
5236
|
let parsed;
|
|
@@ -4899,7 +5304,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
4899
5304
|
}
|
|
4900
5305
|
function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previousManifestFiles, targetHome, snapshotPath) {
|
|
4901
5306
|
assertNoNewerSessionSnapshot(snapshotPath, snapshot.createdAt, targetHome);
|
|
4902
|
-
const manifestPath =
|
|
5307
|
+
const manifestPath = resolve5(snapshot.manifestPath);
|
|
4903
5308
|
const manifestRelativePath = relative(targetHome, manifestPath).replaceAll("\\", "/");
|
|
4904
5309
|
resolveSnapshotFilePath(manifestRelativePath, snapshot.manifestPath, targetHome);
|
|
4905
5310
|
const manifestSha256 = currentSessionFileHash(manifestPath, targetHome);
|
|
@@ -4916,7 +5321,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
|
|
|
4916
5321
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is invalid: ${snapshotPath}`);
|
|
4917
5322
|
}
|
|
4918
5323
|
const appliedManifest = parsedManifest;
|
|
4919
|
-
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)) {
|
|
4920
5325
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest does not match its snapshot: ${snapshotPath}`);
|
|
4921
5326
|
}
|
|
4922
5327
|
const afterFiles = [];
|
|
@@ -5000,17 +5405,17 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
|
|
|
5000
5405
|
if (!Number.isFinite(createdAtMs)) {
|
|
5001
5406
|
throw new SessionApplyError(`Pre-rollback legacy v1 snapshot has an invalid creation time: ${snapshotPath}`);
|
|
5002
5407
|
}
|
|
5003
|
-
for (const entry of readdirSync(
|
|
5004
|
-
const candidatePath =
|
|
5005
|
-
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"))
|
|
5006
5411
|
continue;
|
|
5007
|
-
const candidateStat =
|
|
5412
|
+
const candidateStat = lstatSync2(candidatePath);
|
|
5008
5413
|
if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
|
|
5009
5414
|
continue;
|
|
5010
5415
|
try {
|
|
5011
5416
|
const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));
|
|
5012
5417
|
const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
|
|
5013
|
-
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) {
|
|
5014
5419
|
throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
|
|
5015
5420
|
}
|
|
5016
5421
|
} catch (error) {
|
|
@@ -5079,17 +5484,34 @@ function inferLegacySnapshotAction(file, previousFiles, previousManifestFiles, p
|
|
|
5079
5484
|
}
|
|
5080
5485
|
function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
5081
5486
|
const path = resolveManifestRelativePath(relativePath, targetHome);
|
|
5082
|
-
if (
|
|
5487
|
+
if (resolve5(recordedPath) !== path) {
|
|
5083
5488
|
throw new SessionApplyError(`Session snapshot file path mismatch for ${relativePath}`);
|
|
5084
5489
|
}
|
|
5085
5490
|
return path;
|
|
5086
5491
|
}
|
|
5087
|
-
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
5492
|
+
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes) {
|
|
5088
5493
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
5089
5494
|
const previousContent = existsSync5(target) ? readFileSync(target, "utf-8") : null;
|
|
5090
5495
|
const previousSha256 = previousContent === null ? null : sha2563(previousContent);
|
|
5091
5496
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
5092
5497
|
const changed = previousContent !== file.content;
|
|
5498
|
+
const exactSha256 = adoptedHashes.get(file.relativePath) ?? reconciledHashes.get(file.relativePath);
|
|
5499
|
+
if (exactSha256 !== undefined) {
|
|
5500
|
+
const operation = adoptedHashes.has(file.relativePath) ? "adoption" : "reconciliation";
|
|
5501
|
+
if (previousSha256 !== exactSha256) {
|
|
5502
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${file.relativePath}`);
|
|
5503
|
+
}
|
|
5504
|
+
return {
|
|
5505
|
+
path: target,
|
|
5506
|
+
relativePath: file.relativePath,
|
|
5507
|
+
role: file.role,
|
|
5508
|
+
action: "update",
|
|
5509
|
+
changed,
|
|
5510
|
+
previousSha256,
|
|
5511
|
+
newSha256: file.sha256,
|
|
5512
|
+
reason: `exact observed SHA-256 ${operation}`
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5093
5515
|
if (previousContent !== null && !options.force && !previouslyManaged) {
|
|
5094
5516
|
return {
|
|
5095
5517
|
path: target,
|
|
@@ -5178,17 +5600,55 @@ function isPlanManagedFile(plan, relativePath, role) {
|
|
|
5178
5600
|
return true;
|
|
5179
5601
|
return plan.tool === "claude" && role === "rule" && relativePath.startsWith("rules/");
|
|
5180
5602
|
}
|
|
5181
|
-
function planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options) {
|
|
5603
|
+
function planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options, retiredHashes) {
|
|
5182
5604
|
if (!previousManifest)
|
|
5183
5605
|
return [];
|
|
5184
|
-
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);
|
|
5185
5607
|
}
|
|
5186
|
-
function planStaleFileResult(file, targetHome, options) {
|
|
5608
|
+
function planStaleFileResult(file, targetHome, options, retiredHash) {
|
|
5187
5609
|
const target = resolveManifestRelativePath(file.relativePath, targetHome);
|
|
5188
|
-
if (!existsSync5(target))
|
|
5189
|
-
|
|
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
|
+
}
|
|
5190
5624
|
const previousContent = readFileSync(target, "utf-8");
|
|
5191
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
|
+
}
|
|
5192
5652
|
if (!options.force && previousSha256 !== file.sha256) {
|
|
5193
5653
|
return {
|
|
5194
5654
|
path: target,
|
|
@@ -5232,21 +5692,21 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
|
|
|
5232
5692
|
return previousHashes.get(file.relativePath) === previousSha256;
|
|
5233
5693
|
}
|
|
5234
5694
|
function resolvePlannedFilePath(plan, file, targetHome) {
|
|
5235
|
-
const target =
|
|
5695
|
+
const target = resolve5(targetHome, ...file.relativePath.split("/"));
|
|
5236
5696
|
const rel = relative(targetHome, target);
|
|
5237
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
5697
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
5238
5698
|
throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
|
|
5239
5699
|
}
|
|
5240
|
-
if (
|
|
5700
|
+
if (resolve5(file.path) !== target) {
|
|
5241
5701
|
throw new SessionApplyError(`Session file path mismatch for ${file.relativePath}: ${file.path}`);
|
|
5242
5702
|
}
|
|
5243
5703
|
assertNoSymlinkSegments(targetHome, target);
|
|
5244
5704
|
return target;
|
|
5245
5705
|
}
|
|
5246
5706
|
function resolveManifestRelativePath(relativePath, targetHome) {
|
|
5247
|
-
const target =
|
|
5707
|
+
const target = resolve5(targetHome, ...relativePath.split(/[\\/]+/));
|
|
5248
5708
|
const rel = relative(targetHome, target);
|
|
5249
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
5709
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
5250
5710
|
throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
|
|
5251
5711
|
}
|
|
5252
5712
|
assertNoSymlinkSegments(targetHome, target);
|
|
@@ -5297,7 +5757,7 @@ function currentSessionFileHash(path, targetHome) {
|
|
|
5297
5757
|
assertNoSymlinkSegments(targetHome, path);
|
|
5298
5758
|
if (!existsSync5(path))
|
|
5299
5759
|
return null;
|
|
5300
|
-
const stat =
|
|
5760
|
+
const stat = lstatSync2(path);
|
|
5301
5761
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
5302
5762
|
throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
|
|
5303
5763
|
}
|
|
@@ -5309,9 +5769,13 @@ function requiredPreviousHash(result) {
|
|
|
5309
5769
|
}
|
|
5310
5770
|
return result.previousSha256;
|
|
5311
5771
|
}
|
|
5312
|
-
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
|
|
5313
|
-
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").
|
|
5772
|
+
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations, retirements) {
|
|
5773
|
+
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").map((result) => {
|
|
5774
|
+
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
5314
5775
|
const content = readFileSync(result.path, "utf-8");
|
|
5776
|
+
if (sha2563(content) !== result.previousSha256) {
|
|
5777
|
+
throw new SessionApplyError(`Session snapshot preimage changed after planning: ${result.relativePath}`);
|
|
5778
|
+
}
|
|
5315
5779
|
return {
|
|
5316
5780
|
path: result.path,
|
|
5317
5781
|
relativePath: result.relativePath,
|
|
@@ -5329,7 +5793,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5329
5793
|
};
|
|
5330
5794
|
}
|
|
5331
5795
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
5332
|
-
const snapshotPath =
|
|
5796
|
+
const snapshotPath = join6(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID3()}.json`);
|
|
5333
5797
|
const afterFiles = results.map((result) => {
|
|
5334
5798
|
if (result.action === "conflict") {
|
|
5335
5799
|
throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
|
|
@@ -5352,13 +5816,20 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5352
5816
|
manifestPath,
|
|
5353
5817
|
previousManifest,
|
|
5354
5818
|
files: existingFiles,
|
|
5355
|
-
afterFiles
|
|
5819
|
+
afterFiles,
|
|
5820
|
+
...adoptions.length > 0 ? { adoptions } : {},
|
|
5821
|
+
...reconciliations.length > 0 ? { reconciliations } : {},
|
|
5822
|
+
...retirements.length > 0 ? { retirements } : {}
|
|
5356
5823
|
};
|
|
5824
|
+
const snapshotContent = `${JSON.stringify(snapshot, null, 2)}
|
|
5825
|
+
`;
|
|
5826
|
+
if ((adoptions.length > 0 || reconciliations.length > 0 || retirements.length > 0) && Buffer.byteLength(snapshotContent, "utf8") > 32 * 1024 * 1024) {
|
|
5827
|
+
throw new SessionApplyError("Exact file preimage snapshot exceeds the 32 MiB restore limit.");
|
|
5828
|
+
}
|
|
5357
5829
|
coordination?.assert_held();
|
|
5358
5830
|
writeProjectContextCoordinatedFile({
|
|
5359
5831
|
path: snapshotPath,
|
|
5360
|
-
content:
|
|
5361
|
-
`,
|
|
5832
|
+
content: snapshotContent,
|
|
5362
5833
|
workspace_root: sessionRenderSnapshotWorkspaceRoot(targetHome),
|
|
5363
5834
|
default_mode: 384,
|
|
5364
5835
|
expected_hash: null,
|
|
@@ -5375,14 +5846,14 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5375
5846
|
};
|
|
5376
5847
|
}
|
|
5377
5848
|
function assertSafeTargetHome(targetHome) {
|
|
5378
|
-
if (!
|
|
5849
|
+
if (!isAbsolute2(targetHome))
|
|
5379
5850
|
throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
|
|
5380
|
-
const normalized =
|
|
5851
|
+
const normalized = resolve5(targetHome);
|
|
5381
5852
|
if (normalized === parse(normalized).root) {
|
|
5382
5853
|
throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
|
|
5383
5854
|
}
|
|
5384
5855
|
assertNoSymlinkAncestors(normalized);
|
|
5385
|
-
if (existsSync5(normalized) &&
|
|
5856
|
+
if (existsSync5(normalized) && lstatSync2(normalized).isSymbolicLink()) {
|
|
5386
5857
|
throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
|
|
5387
5858
|
}
|
|
5388
5859
|
return normalized;
|
|
@@ -5392,22 +5863,22 @@ function assertNoSymlinkSegments(root, target) {
|
|
|
5392
5863
|
const rel = relative(root, target);
|
|
5393
5864
|
let current = root;
|
|
5394
5865
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
5395
|
-
current =
|
|
5396
|
-
if (existsSync5(current) &&
|
|
5866
|
+
current = join6(current, segment);
|
|
5867
|
+
if (existsSync5(current) && lstatSync2(current).isSymbolicLink()) {
|
|
5397
5868
|
throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
|
|
5398
5869
|
}
|
|
5399
5870
|
}
|
|
5400
5871
|
}
|
|
5401
5872
|
function assertNoSymlinkAncestors(path) {
|
|
5402
|
-
const normalized =
|
|
5873
|
+
const normalized = resolve5(path);
|
|
5403
5874
|
const parsed = parse(normalized);
|
|
5404
5875
|
let current = parsed.root;
|
|
5405
5876
|
const rel = relative(parsed.root, normalized);
|
|
5406
5877
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
5407
|
-
current =
|
|
5878
|
+
current = join6(current, segment);
|
|
5408
5879
|
if (!existsSync5(current))
|
|
5409
5880
|
return;
|
|
5410
|
-
if (
|
|
5881
|
+
if (lstatSync2(current).isSymbolicLink()) {
|
|
5411
5882
|
throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
|
|
5412
5883
|
}
|
|
5413
5884
|
}
|
|
@@ -5416,11 +5887,15 @@ function sha2563(content) {
|
|
|
5416
5887
|
return createHash3("sha256").update(content).digest("hex");
|
|
5417
5888
|
}
|
|
5418
5889
|
|
|
5890
|
+
// src/lib/session-refresh.ts
|
|
5891
|
+
import { createHash as createHash4 } from "crypto";
|
|
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";
|
|
5894
|
+
|
|
5419
5895
|
// src/lib/global-source-coverage.ts
|
|
5420
5896
|
var GLOBAL_SOURCE_SLUG_PREFIX = "global-";
|
|
5421
|
-
var RETIRED_GLOBAL_SOURCE_TAG = "retired-global-source";
|
|
5422
5897
|
function expectedGlobalSourceSlugs(registryConfigs) {
|
|
5423
|
-
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) =>
|
|
5898
|
+
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) => instructionSourceRejection(c) === null).map((c) => c.slug).sort();
|
|
5424
5899
|
}
|
|
5425
5900
|
function accountedGlobalSourceSlugs(manifest) {
|
|
5426
5901
|
return [...manifest.sources, ...manifest.skippedSources].map((source) => source.id).filter((id) => id.startsWith(GLOBAL_SOURCE_SLUG_PREFIX));
|
|
@@ -5433,6 +5908,10 @@ function computeGlobalSourceCoverage(registryConfigs, configuredSlugs) {
|
|
|
5433
5908
|
const unexpected = [...configuredSet].filter((slug) => slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX) && !expectedSet.has(slug)).sort();
|
|
5434
5909
|
return {
|
|
5435
5910
|
expectedSlugs: expected,
|
|
5911
|
+
excludedSources: registryConfigs.filter((config) => config.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).flatMap((config) => {
|
|
5912
|
+
const reason = instructionSourceRejection(config);
|
|
5913
|
+
return reason ? [{ slug: config.slug, reason }] : [];
|
|
5914
|
+
}).sort((left, right) => left.slug.localeCompare(right.slug)),
|
|
5436
5915
|
configuredSlugs: [...configuredSet].sort(),
|
|
5437
5916
|
missingSlugs: missing,
|
|
5438
5917
|
unexpectedSlugs: unexpected,
|
|
@@ -5449,9 +5928,9 @@ function formatGlobalSourceCoverageWarnings(result) {
|
|
|
5449
5928
|
|
|
5450
5929
|
// src/lib/station-profile.ts
|
|
5451
5930
|
import { spawnSync } from "child_process";
|
|
5452
|
-
import { existsSync as existsSync6, lstatSync as
|
|
5453
|
-
import { arch as osArch, homedir as
|
|
5454
|
-
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";
|
|
5455
5934
|
var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
|
|
5456
5935
|
var STATION_PROFILE_SOURCE_ID = "station-profile";
|
|
5457
5936
|
var STATION_PROFILE_LAYER = "machine";
|
|
@@ -5462,16 +5941,16 @@ var STATION_PROFILE_PRIMARY_SCOPE = "@hasna";
|
|
|
5462
5941
|
var MACHINES_MANIFEST_PATH_ENV = "HASNA_MACHINES_MANIFEST_PATH";
|
|
5463
5942
|
var BUN_INSTALL_ENV = "BUN_INSTALL";
|
|
5464
5943
|
function homeDir(env = process.env) {
|
|
5465
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
5944
|
+
return env["HOME"] || env["USERPROFILE"] || homedir3();
|
|
5466
5945
|
}
|
|
5467
5946
|
function getStationProfileCachePath(env = process.env) {
|
|
5468
|
-
return
|
|
5947
|
+
return join7(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
|
|
5469
5948
|
}
|
|
5470
5949
|
function getMachinesManifestPath(env = process.env) {
|
|
5471
|
-
return env[MACHINES_MANIFEST_PATH_ENV] ||
|
|
5950
|
+
return env[MACHINES_MANIFEST_PATH_ENV] || join7(homeDir(env), ".hasna", "machines", "machines.json");
|
|
5472
5951
|
}
|
|
5473
5952
|
function getBunGlobalModulesDir(env = process.env) {
|
|
5474
|
-
return
|
|
5953
|
+
return join7(env[BUN_INSTALL_ENV] || join7(homeDir(env), ".bun"), "install", "global", "node_modules");
|
|
5475
5954
|
}
|
|
5476
5955
|
function readMachinesManifest(path) {
|
|
5477
5956
|
try {
|
|
@@ -5533,7 +6012,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
5533
6012
|
const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname);
|
|
5534
6013
|
const home = homeDir(env);
|
|
5535
6014
|
const platform = stringField(record, "platform") ?? osPlatform();
|
|
5536
|
-
const workspacePath = stringField(record, "workspacePath")
|
|
6015
|
+
const workspacePath = stringField(record, "workspacePath");
|
|
5537
6016
|
const machine = {
|
|
5538
6017
|
id: stringField(record, "id") ?? hostname,
|
|
5539
6018
|
hostname: stringField(record, "hostname") ?? hostname,
|
|
@@ -5550,7 +6029,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
5550
6029
|
return machine;
|
|
5551
6030
|
}
|
|
5552
6031
|
function scopedPackageNames(modulesDir, scope) {
|
|
5553
|
-
const scopeDir =
|
|
6032
|
+
const scopeDir = join7(modulesDir, scope);
|
|
5554
6033
|
try {
|
|
5555
6034
|
if (!existsSync6(scopeDir))
|
|
5556
6035
|
return null;
|
|
@@ -5562,7 +6041,7 @@ function scopedPackageNames(modulesDir, scope) {
|
|
|
5562
6041
|
function readdirNames(dir) {
|
|
5563
6042
|
return readdirSync2(dir).filter((name) => {
|
|
5564
6043
|
try {
|
|
5565
|
-
return
|
|
6044
|
+
return lstatSync3(join7(dir, name)).isDirectory();
|
|
5566
6045
|
} catch {
|
|
5567
6046
|
return false;
|
|
5568
6047
|
}
|
|
@@ -5649,7 +6128,7 @@ function refreshStationProfile(options = {}) {
|
|
|
5649
6128
|
if (!options.dryRun) {
|
|
5650
6129
|
const existing = existsSync6(path) ? readFileSync2(path, "utf8") : null;
|
|
5651
6130
|
if (existing !== content) {
|
|
5652
|
-
mkdirSync3(
|
|
6131
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
5653
6132
|
writeFileSync2(path, content, "utf8");
|
|
5654
6133
|
}
|
|
5655
6134
|
}
|
|
@@ -5688,6 +6167,188 @@ function stationProfileSource(env = process.env) {
|
|
|
5688
6167
|
};
|
|
5689
6168
|
}
|
|
5690
6169
|
|
|
6170
|
+
// src/lib/session-refresh.ts
|
|
6171
|
+
var nonempty = exports_external.string().min(1).max(4096);
|
|
6172
|
+
var selectorSchema = exports_external.object({
|
|
6173
|
+
schema: exports_external.literal("hasna.instructions.hosted-profile-selector/v1"),
|
|
6174
|
+
authority: nonempty,
|
|
6175
|
+
profileId: nonempty,
|
|
6176
|
+
providerVersion: nonempty,
|
|
6177
|
+
providerVariant: nonempty.optional(),
|
|
6178
|
+
model: nonempty.optional(),
|
|
6179
|
+
path: nonempty.optional(),
|
|
6180
|
+
manual: exports_external.array(nonempty).max(256),
|
|
6181
|
+
codewithNativeImports: exports_external.boolean(),
|
|
6182
|
+
allowEmptySources: exports_external.boolean(),
|
|
6183
|
+
stationProfile: exports_external.boolean(),
|
|
6184
|
+
checkGlobalCoverage: exports_external.boolean(),
|
|
6185
|
+
assetSurface: nonempty.optional(),
|
|
6186
|
+
assetScope: exports_external.enum(["global", "project", "session"]).optional()
|
|
6187
|
+
}).strict();
|
|
6188
|
+
function authority(value) {
|
|
6189
|
+
const url = new URL(value);
|
|
6190
|
+
if (url.username || url.password || url.search || url.hash || !["https:", "http:"].includes(url.protocol)) {
|
|
6191
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_INVALID: expected a credential-free Instructions API authority.");
|
|
6192
|
+
}
|
|
6193
|
+
return url.toString().replace(/\/$/, "");
|
|
6194
|
+
}
|
|
6195
|
+
function normalizeSessionHostedProfileSelector(value) {
|
|
6196
|
+
const parsed = selectorSchema.parse(value);
|
|
6197
|
+
return { ...parsed, authority: authority(parsed.authority) };
|
|
6198
|
+
}
|
|
6199
|
+
function assertHostedProfileBindings(profileId, configs, bindings) {
|
|
6200
|
+
const configIds = new Set(configs.map((config) => config.id));
|
|
6201
|
+
const bindingIds = new Set(bindings.map((binding) => binding.config_id));
|
|
6202
|
+
if (configIds.size !== configs.length || bindingIds.size !== bindings.length || bindings.some((binding) => binding.profile_id !== profileId || !configIds.has(binding.config_id)) || [...configIds].some((id) => !bindingIds.has(id))) {
|
|
6203
|
+
throw new Error("HOSTED_PROFILE_BINDINGS_INCOMPLETE: exact one-to-one config membership and explicit profile bindings are required; refusing legacy activation fallback.");
|
|
6204
|
+
}
|
|
6205
|
+
for (const row of bindings) {
|
|
6206
|
+
const raw = row.binding;
|
|
6207
|
+
const invalid = () => new Error("HOSTED_PROFILE_BINDINGS_INVALID: each hosted binding must contain a complete explicit schema, activation, required flag and fallback; refusing legacy activation defaults.");
|
|
6208
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
6209
|
+
throw invalid();
|
|
6210
|
+
const record = raw;
|
|
6211
|
+
if (!["schema", "activation", "required", "fallback"].every((key) => Object.hasOwn(record, key)) || typeof record["required"] !== "boolean")
|
|
6212
|
+
throw invalid();
|
|
6213
|
+
try {
|
|
6214
|
+
normalizeProfileConfigBinding(record);
|
|
6215
|
+
} catch {
|
|
6216
|
+
throw invalid();
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
function readManagedManifest(targetHome) {
|
|
6221
|
+
const path = join8(targetHome, SESSION_RENDER_MANIFEST_RELATIVE_PATH);
|
|
6222
|
+
let ancestor = dirname5(path);
|
|
6223
|
+
while (true) {
|
|
6224
|
+
const stat = lstatSync4(ancestor);
|
|
6225
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
6226
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: managed target uses a symlink or non-directory ancestor.");
|
|
6227
|
+
const parent = dirname5(ancestor);
|
|
6228
|
+
if (parent === ancestor)
|
|
6229
|
+
break;
|
|
6230
|
+
ancestor = parent;
|
|
6231
|
+
}
|
|
6232
|
+
const fd = openSync(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
6233
|
+
let raw;
|
|
6234
|
+
try {
|
|
6235
|
+
const stat = fstatSync(fd);
|
|
6236
|
+
if (!stat.isFile() || stat.size > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6237
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: expected a bounded regular manifest file.");
|
|
6238
|
+
raw = readFileSync3(fd);
|
|
6239
|
+
if (raw.byteLength > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6240
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: manifest grew beyond the read limit.");
|
|
6241
|
+
} finally {
|
|
6242
|
+
closeSync(fd);
|
|
6243
|
+
}
|
|
6244
|
+
const manifest = JSON.parse(raw.toString("utf8"));
|
|
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) {
|
|
6246
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: target is not an owned hosted profile render; apply a hosted --compile-profile first.");
|
|
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
|
+
}
|
|
6261
|
+
manifest.refreshSelector = normalizeSessionHostedProfileSelector(manifest.refreshSelector);
|
|
6262
|
+
return { manifest, sha256: createHash4("sha256").update(raw).digest("hex") };
|
|
6263
|
+
}
|
|
6264
|
+
async function refreshSessionRender(input) {
|
|
6265
|
+
const { store } = input;
|
|
6266
|
+
if (store.mode !== "api" || !store.v1BaseUrl)
|
|
6267
|
+
throw new Error("SESSION_REFRESH_HOSTED_REQUIRED: refresh requires the configured hosted Instructions API.");
|
|
6268
|
+
const targetHome = resolve6(input.targetHome);
|
|
6269
|
+
if (targetHome === parse2(targetHome).root)
|
|
6270
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: filesystem root is not a managed target.");
|
|
6271
|
+
const previous = readManagedManifest(targetHome);
|
|
6272
|
+
const selector = previous.manifest.refreshSelector;
|
|
6273
|
+
if (authority(store.v1BaseUrl) !== selector.authority)
|
|
6274
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_MISMATCH: configured API differs from the recorded profile authority.");
|
|
6275
|
+
const profile = await store.getProfile(selector.profileId);
|
|
6276
|
+
if (profile.id !== selector.profileId)
|
|
6277
|
+
throw new Error("SESSION_REFRESH_PROFILE_MISMATCH: hosted profile identity changed.");
|
|
6278
|
+
const [configs, bindings, assetBindings] = await Promise.all([
|
|
6279
|
+
store.getProfileConfigs(profile.id),
|
|
6280
|
+
store.getProfileConfigBindings(profile.id, { requireExplicit: true }),
|
|
6281
|
+
store.getProfileAssetBindings(profile.id, { requireExplicit: true })
|
|
6282
|
+
]);
|
|
6283
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
6284
|
+
if (configs.length > 2048 || bindings.length > 8192 || assetBindings.length > 1024)
|
|
6285
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted profile exceeds refresh bounds.");
|
|
6286
|
+
const totalBytes = configs.reduce((sum, config) => sum + Buffer.byteLength(config.content), 0);
|
|
6287
|
+
if (totalBytes > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6288
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted instruction payload exceeds refresh byte bounds.");
|
|
6289
|
+
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
6290
|
+
if (assetConfigs.reduce((sum, config) => sum + Buffer.byteLength(config.content), totalBytes) > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6291
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted source and asset payload exceeds refresh byte bounds.");
|
|
6292
|
+
let ownedClaudeAuthorities;
|
|
6293
|
+
if (previous.manifest.tool === "claude") {
|
|
6294
|
+
ownedClaudeAuthorities = (await store.listConfigs({ category: "rules", agent: "claude", kind: "file" })).filter((config) => config.target_path && basename(normalizeTargetPath(config.target_path)) === "AGENTS.md").map((config) => ({ slug: config.slug, targetPath: config.target_path, content: config.content }));
|
|
6295
|
+
}
|
|
6296
|
+
const station = selector.stationProfile ? stationProfileSource() : null;
|
|
6297
|
+
const plan = planProfileSessionRender({
|
|
6298
|
+
tool: previous.manifest.tool,
|
|
6299
|
+
profile: previous.manifest.profile,
|
|
6300
|
+
profile_id: profile.id,
|
|
6301
|
+
provider_version: selector.providerVersion,
|
|
6302
|
+
targetHome,
|
|
6303
|
+
...previous.manifest.targetKind === "project-root" ? { projectRoot: targetHome } : previous.manifest.targetOwner.projectRoot ? { projectRoot: previous.manifest.targetOwner.projectRoot } : {},
|
|
6304
|
+
sessionId: previous.manifest.sessionId ?? undefined,
|
|
6305
|
+
refreshSelector: selector,
|
|
6306
|
+
codewithNativeImports: selector.codewithNativeImports,
|
|
6307
|
+
allowEmptySources: selector.allowEmptySources,
|
|
6308
|
+
configs,
|
|
6309
|
+
bindings,
|
|
6310
|
+
asset_configs: assetConfigs,
|
|
6311
|
+
asset_bindings: assetBindings,
|
|
6312
|
+
asset_plan_mode: "apply",
|
|
6313
|
+
asset_scope: selector.assetScope,
|
|
6314
|
+
asset_surface: selector.assetSurface,
|
|
6315
|
+
extra_sources: station ? [station] : undefined,
|
|
6316
|
+
ownedClaudeAuthorities,
|
|
6317
|
+
graph_context: {
|
|
6318
|
+
...selector.providerVariant ? { provider_variant: selector.providerVariant } : {},
|
|
6319
|
+
...selector.model ? { model: selector.model } : {},
|
|
6320
|
+
...selector.path ? { path: selector.path } : {},
|
|
6321
|
+
...selector.manual.length ? { manual: selector.manual } : {}
|
|
6322
|
+
}
|
|
6323
|
+
});
|
|
6324
|
+
if (selector.checkGlobalCoverage) {
|
|
6325
|
+
const coverage = computeGlobalSourceCoverage(await store.listConfigs({}), accountedGlobalSourceSlugs(plan.manifest));
|
|
6326
|
+
if (!coverage.complete)
|
|
6327
|
+
throw new Error(`SESSION_REFRESH_COVERAGE_INCOMPLETE: ${coverage.missingSlugs.length} registered global sources are absent.`);
|
|
6328
|
+
}
|
|
6329
|
+
const preview = applySessionRender(plan, { dryRun: true, expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6330
|
+
const common = { checkedAt: new Date().toISOString(), targetHome, selector, sourceHash: plan.manifest.sourceHash };
|
|
6331
|
+
if (preview.conflicts.length)
|
|
6332
|
+
return { ...common, status: "blocked", apply: preview };
|
|
6333
|
+
if (!preview.drift.clean)
|
|
6334
|
+
throw new Error("SESSION_REFRESH_DRIFT: generated files changed or disappeared since their last managed render.");
|
|
6335
|
+
const sameSources = previous.manifest.sourceHash === plan.manifest.sourceHash;
|
|
6336
|
+
const samePayloads = preview.files.filter((file) => file.role !== "manifest").every((file) => file.action === "unchanged");
|
|
6337
|
+
if (sameSources && samePayloads) {
|
|
6338
|
+
if (readManagedManifest(targetHome).sha256 !== previous.sha256 || !checkSessionRenderDrift(targetHome).clean)
|
|
6339
|
+
throw new Error("SESSION_REFRESH_DRIFT: managed target changed during refresh.");
|
|
6340
|
+
return { ...common, status: "unchanged", apply: { ...preview, files: preview.files.map((file) => file.role === "manifest" ? { ...file, action: "unchanged", changed: false, newSha256: previous.sha256, reason: "hosted sources and generated payloads unchanged" } : file) } };
|
|
6341
|
+
}
|
|
6342
|
+
if (input.dryRun)
|
|
6343
|
+
return { ...common, status: "dry-run", apply: preview };
|
|
6344
|
+
const applied = applySessionRender(plan, { expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6345
|
+
if (applied.conflicts.length)
|
|
6346
|
+
return { ...common, status: "blocked", apply: applied };
|
|
6347
|
+
if (!applied.applied || !checkSessionRenderDrift(targetHome).clean)
|
|
6348
|
+
throw new Error("SESSION_REFRESH_READBACK_FAILED: applied instructions did not verify against their manifest.");
|
|
6349
|
+
return { ...common, status: "updated", apply: applied };
|
|
6350
|
+
}
|
|
6351
|
+
|
|
5691
6352
|
// src/lib/project-dashboard-standard.ts
|
|
5692
6353
|
var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
|
|
5693
6354
|
var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
@@ -5780,11 +6441,15 @@ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()
|
|
|
5780
6441
|
};
|
|
5781
6442
|
try {
|
|
5782
6443
|
const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
|
|
6444
|
+
if (isRetiredInstructionSource(existing))
|
|
6445
|
+
return existing;
|
|
5783
6446
|
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind) {
|
|
5784
6447
|
return await store.updateConfig(existing.id, input);
|
|
5785
6448
|
}
|
|
5786
6449
|
return existing;
|
|
5787
|
-
} catch {
|
|
6450
|
+
} catch (error) {
|
|
6451
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6452
|
+
throw error;
|
|
5788
6453
|
return await store.createConfig(input);
|
|
5789
6454
|
}
|
|
5790
6455
|
}
|
|
@@ -5797,10 +6462,9 @@ function profileHasSelectors(profile) {
|
|
|
5797
6462
|
var PLATFORM_PROFILE_PRESETS = [
|
|
5798
6463
|
{
|
|
5799
6464
|
name: "linux-arm64",
|
|
5800
|
-
description: "Default
|
|
5801
|
-
selectors: { os: ["linux"], arch: ["arm64"]
|
|
6465
|
+
description: "Default reviewed instruction profile for Linux arm64",
|
|
6466
|
+
selectors: { os: ["linux"], arch: ["arm64"] },
|
|
5802
6467
|
variables: {
|
|
5803
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
|
|
5804
6468
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5805
6469
|
BUN_PATH: "{{BUN_BIN_DIR}}/bun",
|
|
5806
6470
|
PATH_PREFIX: "{{BUN_BIN_DIR}}",
|
|
@@ -5809,10 +6473,9 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5809
6473
|
},
|
|
5810
6474
|
{
|
|
5811
6475
|
name: "macos-arm64",
|
|
5812
|
-
description: "Default
|
|
5813
|
-
selectors: { os: ["macos"], arch: ["arm64"]
|
|
6476
|
+
description: "Default reviewed instruction profile for macOS arm64",
|
|
6477
|
+
selectors: { os: ["macos"], arch: ["arm64"] },
|
|
5814
6478
|
variables: {
|
|
5815
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
|
|
5816
6479
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5817
6480
|
BUN_PATH: "/opt/homebrew/bin/bun",
|
|
5818
6481
|
PATH_PREFIX: "/opt/homebrew/bin:{{BUN_BIN_DIR}}",
|
|
@@ -5821,12 +6484,21 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5821
6484
|
}
|
|
5822
6485
|
];
|
|
5823
6486
|
async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
5824
|
-
const configs = await store.listConfigs();
|
|
5825
6487
|
const ensured = [];
|
|
5826
|
-
|
|
5827
|
-
let profile;
|
|
6488
|
+
const existingProfiles = await Promise.all(PLATFORM_PROFILE_PRESETS.map(async (preset) => {
|
|
5828
6489
|
try {
|
|
5829
|
-
|
|
6490
|
+
return await store.getProfile(preset.name);
|
|
6491
|
+
} catch (error) {
|
|
6492
|
+
if (error instanceof ProfileNotFoundError)
|
|
6493
|
+
return null;
|
|
6494
|
+
throw error;
|
|
6495
|
+
}
|
|
6496
|
+
}));
|
|
6497
|
+
for (const [index, preset] of PLATFORM_PROFILE_PRESETS.entries()) {
|
|
6498
|
+
let profile;
|
|
6499
|
+
const existing = existingProfiles[index];
|
|
6500
|
+
if (existing) {
|
|
6501
|
+
profile = existing;
|
|
5830
6502
|
const selectors = mergeProfileSelectors(preset.selectors, profile.selectors);
|
|
5831
6503
|
const variables = mergeProfileVariables(preset.variables, profile.variables);
|
|
5832
6504
|
if (JSON.stringify(selectors) !== JSON.stringify(profile.selectors) || JSON.stringify(variables) !== JSON.stringify(profile.variables)) {
|
|
@@ -5836,12 +6508,9 @@ async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
|
5836
6508
|
variables
|
|
5837
6509
|
});
|
|
5838
6510
|
}
|
|
5839
|
-
}
|
|
6511
|
+
} else {
|
|
5840
6512
|
profile = await store.createProfile(preset);
|
|
5841
6513
|
}
|
|
5842
|
-
for (const config of configs) {
|
|
5843
|
-
await store.addConfigToProfile(profile.id, config.id);
|
|
5844
|
-
}
|
|
5845
6514
|
ensured.push(profile);
|
|
5846
6515
|
}
|
|
5847
6516
|
return ensured;
|
|
@@ -5982,29 +6651,23 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
|
|
|
5982
6651
|
};
|
|
5983
6652
|
try {
|
|
5984
6653
|
const existing = await store.getConfig(DANGEROUS_OPERATION_GUARD_STANDARD_SLUG);
|
|
6654
|
+
if (isRetiredInstructionSource(existing))
|
|
6655
|
+
return existing;
|
|
5985
6656
|
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind || JSON.stringify(existing.tags) !== JSON.stringify(input.tags)) {
|
|
5986
6657
|
return await store.updateConfig(existing.id, input);
|
|
5987
6658
|
}
|
|
5988
6659
|
return existing;
|
|
5989
|
-
} catch {
|
|
6660
|
+
} catch (error) {
|
|
6661
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6662
|
+
throw error;
|
|
5990
6663
|
return await store.createConfig(input);
|
|
5991
6664
|
}
|
|
5992
6665
|
}
|
|
5993
6666
|
|
|
5994
6667
|
// src/lib/managed-skill-runtimes.ts
|
|
5995
|
-
import {
|
|
5996
|
-
import {
|
|
5997
|
-
import {
|
|
5998
|
-
existsSync as existsSync7,
|
|
5999
|
-
lstatSync as lstatSync3,
|
|
6000
|
-
mkdirSync as mkdirSync4,
|
|
6001
|
-
readFileSync as readFileSync3,
|
|
6002
|
-
renameSync,
|
|
6003
|
-
rmSync as rmSync2,
|
|
6004
|
-
writeFileSync as writeFileSync3
|
|
6005
|
-
} from "fs";
|
|
6006
|
-
import { homedir as homedir3 } from "os";
|
|
6007
|
-
import { dirname as dirname4, join as join7, parse as parse2, relative as relative2, resolve as resolve5 } 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";
|
|
6008
6671
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
6009
6672
|
var INBOX_SKILL_MARKERS = [
|
|
6010
6673
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -6013,395 +6676,93 @@ var INBOX_SKILL_MARKERS = [
|
|
|
6013
6676
|
[".config", "opencode", "skills", "inbox", "SKILL.md"],
|
|
6014
6677
|
[".cursor", "skills", "inbox", "SKILL.md"]
|
|
6015
6678
|
];
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
const normalized = resolve5(path);
|
|
6029
|
-
const parsed = parse2(normalized);
|
|
6030
|
-
let current = parsed.root;
|
|
6031
|
-
const rel = relative2(parsed.root, normalized);
|
|
6032
|
-
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
6033
|
-
current = join7(current, segment);
|
|
6034
|
-
if (!existsSync7(current))
|
|
6035
|
-
return null;
|
|
6036
|
-
if (lstatSync3(current).isSymbolicLink())
|
|
6037
|
-
return current;
|
|
6038
|
-
}
|
|
6039
|
-
return null;
|
|
6040
|
-
}
|
|
6041
|
-
function assertNoSymlinkAncestors2(path) {
|
|
6042
|
-
const found = findSymlinkedAncestor(path);
|
|
6043
|
-
if (found !== null) {
|
|
6044
|
-
throw new Error(`managed skill path uses a symlink ancestor: ${found}`);
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6047
|
-
function packagedInboxSkillPath(explicitPath) {
|
|
6048
|
-
if (explicitPath)
|
|
6049
|
-
return explicitPath;
|
|
6050
|
-
throw new Error("Bundled skill contracts are retired; use the Skills CLI to manage private skills");
|
|
6051
|
-
}
|
|
6052
|
-
function readCanonicalSkill(explicitPath) {
|
|
6053
|
-
const assetPath = packagedInboxSkillPath(explicitPath);
|
|
6054
|
-
const stat = lstatOrNull(assetPath);
|
|
6055
|
-
if (!stat?.isFile()) {
|
|
6056
|
-
throw new Error("packaged inbox skill contract is not a regular file");
|
|
6057
|
-
}
|
|
6058
|
-
const content = readFileSync3(assetPath, "utf8");
|
|
6059
|
-
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
6060
|
-
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
6061
|
-
}
|
|
6062
|
-
if (!content.includes("There is no separate")) {
|
|
6063
|
-
throw new Error("packaged inbox skill contract does not retire the legacy executable");
|
|
6064
|
-
}
|
|
6065
|
-
return { content, sha256: sha2564(content) };
|
|
6066
|
-
}
|
|
6067
|
-
function runProbe(command, args) {
|
|
6068
|
-
const result = spawnSync2(command, args, {
|
|
6069
|
-
encoding: "utf8",
|
|
6070
|
-
timeout: 5000,
|
|
6071
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6072
|
-
});
|
|
6073
|
-
if (result.error || result.status !== 0) {
|
|
6074
|
-
return { ok: false, output: "" };
|
|
6075
|
-
}
|
|
6076
|
-
return {
|
|
6077
|
-
ok: true,
|
|
6078
|
-
output: `${result.stdout ?? ""}
|
|
6079
|
-
${result.stderr ?? ""}`.trim()
|
|
6080
|
-
};
|
|
6081
|
-
}
|
|
6082
|
-
function parseVersion(output) {
|
|
6083
|
-
return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
|
|
6084
|
-
}
|
|
6085
|
-
function compareVersions(left, right) {
|
|
6086
|
-
const a = left.split(".").map(Number);
|
|
6087
|
-
const b = right.split(".").map(Number);
|
|
6088
|
-
for (let i = 0;i < Math.max(a.length, b.length); i++) {
|
|
6089
|
-
const delta = (a[i] ?? 0) - (b[i] ?? 0);
|
|
6090
|
-
if (delta !== 0)
|
|
6091
|
-
return delta;
|
|
6092
|
-
}
|
|
6093
|
-
return 0;
|
|
6094
|
-
}
|
|
6095
|
-
function inspectSkillMarkers(homeDir2) {
|
|
6096
|
-
return INBOX_SKILL_MARKERS.map((parts) => join7(homeDir2, ...parts)).map((path) => {
|
|
6097
|
-
const stat = lstatOrNull(path);
|
|
6098
|
-
if (!stat)
|
|
6099
|
-
return null;
|
|
6100
|
-
if (!stat.isFile()) {
|
|
6101
|
-
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;
|
|
6102
6691
|
}
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
content: readFileSync3(path, "utf8"),
|
|
6106
|
-
mode: stat.mode & 511,
|
|
6107
|
-
regular: true
|
|
6108
|
-
};
|
|
6109
|
-
}).filter((snapshot) => snapshot !== null);
|
|
6110
|
-
}
|
|
6111
|
-
function inspectInbox(options) {
|
|
6112
|
-
const homeDir2 = options.homeDir ?? homedir3();
|
|
6113
|
-
const runtimeCommand = options.conversationsCommand ?? "conversations";
|
|
6114
|
-
const snapshots = inspectSkillMarkers(homeDir2);
|
|
6115
|
-
const skillPresent = snapshots.length > 0;
|
|
6116
|
-
let canonicalContent = null;
|
|
6117
|
-
let canonicalSha256 = null;
|
|
6118
|
-
let assetError = null;
|
|
6119
|
-
try {
|
|
6120
|
-
const canonical = readCanonicalSkill(options.assetPath);
|
|
6121
|
-
canonicalContent = canonical.content;
|
|
6122
|
-
canonicalSha256 = canonical.sha256;
|
|
6123
|
-
} catch (error) {
|
|
6124
|
-
assetError = error instanceof Error ? error.message : String(error);
|
|
6125
|
-
}
|
|
6126
|
-
const versionProbe = skillPresent ? runProbe(runtimeCommand, ["--version"]) : { ok: false, output: "" };
|
|
6127
|
-
const helpProbe = versionProbe.ok ? runProbe(runtimeCommand, ["watch", "--help"]) : { ok: false, output: "" };
|
|
6128
|
-
const runtimeVersion = versionProbe.ok ? parseVersion(versionProbe.output) : null;
|
|
6129
|
-
const supportsFrom = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[0]);
|
|
6130
|
-
const supportsAll = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[1]);
|
|
6131
|
-
const supportsFullContent = helpProbe.ok && helpProbe.output.includes(REQUIRED_WATCH_FLAGS[2]);
|
|
6132
|
-
const packageReady = versionProbe.ok && runtimeVersion !== null && compareVersions(runtimeVersion, INBOX_CONVERSATIONS_MINIMUM_VERSION) >= 0 && helpProbe.ok && supportsFrom && supportsAll && supportsFullContent;
|
|
6133
|
-
const heartbeatProbe = skillPresent && packageReady && options.agent ? runProbe(runtimeCommand, ["agents", "heartbeat", "--from", options.agent, "--json"]) : null;
|
|
6134
|
-
const hostedHeartbeat = heartbeatProbe === null ? "unverified" : heartbeatProbe.ok ? "passed" : "failed";
|
|
6135
|
-
const deliveryVerified = hostedHeartbeat === "passed" && options.deliveryVerified === true;
|
|
6136
|
-
const staleMarkers = canonicalContent === null ? snapshots.map((snapshot) => snapshot.path) : snapshots.filter((snapshot) => !snapshot.regular || snapshot.content !== canonicalContent).map((snapshot) => snapshot.path);
|
|
6137
|
-
let reason = "skill not installed";
|
|
6138
|
-
if (skillPresent) {
|
|
6139
|
-
const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
|
|
6140
|
-
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
|
|
6141
|
-
if (nonRegular)
|
|
6142
|
-
reason = "managed skill target is not a regular file";
|
|
6143
|
-
else if (symlinkAncestor)
|
|
6144
|
-
reason = `managed skill path uses a symlink ancestor: ${symlinkAncestor}`;
|
|
6145
|
-
else if (assetError)
|
|
6146
|
-
reason = assetError;
|
|
6147
|
-
else if (!versionProbe.ok)
|
|
6148
|
-
reason = "conversations command unavailable";
|
|
6149
|
-
else if (!runtimeVersion)
|
|
6150
|
-
reason = "conversations version is unreadable";
|
|
6151
|
-
else if (compareVersions(runtimeVersion, INBOX_CONVERSATIONS_MINIMUM_VERSION) < 0) {
|
|
6152
|
-
reason = `conversations ${runtimeVersion} is older than ${INBOX_CONVERSATIONS_MINIMUM_VERSION}`;
|
|
6153
|
-
} else if (!helpProbe.ok)
|
|
6154
|
-
reason = "conversations watch help is unavailable";
|
|
6155
|
-
else if (!supportsFrom || !supportsAll || !supportsFullContent) {
|
|
6156
|
-
const missing = [
|
|
6157
|
-
!supportsFrom ? "--from" : null,
|
|
6158
|
-
!supportsAll ? "--all" : null,
|
|
6159
|
-
!supportsFullContent ? "--full-content" : null
|
|
6160
|
-
].filter((flag) => flag !== null);
|
|
6161
|
-
reason = `conversations watch is missing required flags: ${missing.join(", ")}`;
|
|
6162
|
-
} else if (staleMarkers.length > 0)
|
|
6163
|
-
reason = "skill contract stale";
|
|
6164
|
-
else if (hostedHeartbeat === "failed")
|
|
6165
|
-
reason = "hosted heartbeat failed; manual fallback required";
|
|
6166
|
-
else if (hostedHeartbeat === "unverified")
|
|
6167
|
-
reason = "hosted heartbeat unverified; manual fallback required";
|
|
6168
|
-
else if (!deliveryVerified)
|
|
6169
|
-
reason = "hosted heartbeat passed; channel and DM delivery verification required";
|
|
6170
|
-
else
|
|
6171
|
-
reason = "ready";
|
|
6692
|
+
if (stat.isSymbolicLink() || current !== absolute && !stat.isDirectory())
|
|
6693
|
+
return true;
|
|
6172
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;
|
|
6173
6701
|
return {
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
reason
|
|
6194
|
-
},
|
|
6195
|
-
canonicalContent,
|
|
6196
|
-
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"
|
|
6197
6721
|
};
|
|
6198
6722
|
}
|
|
6199
6723
|
function inspectManagedSkillRuntimes(options = {}) {
|
|
6200
|
-
const runtime =
|
|
6201
|
-
const installed = runtime.skill_present ? [runtime] : [];
|
|
6724
|
+
const runtime = inspectLegacyInbox(options);
|
|
6202
6725
|
return {
|
|
6203
6726
|
runtimes: [runtime],
|
|
6204
|
-
skills_present:
|
|
6205
|
-
healthy:
|
|
6206
|
-
missing:
|
|
6727
|
+
skills_present: runtime.skill_present ? 1 : 0,
|
|
6728
|
+
healthy: 0,
|
|
6729
|
+
missing: runtime.skill_present ? 1 : 0
|
|
6207
6730
|
};
|
|
6208
6731
|
}
|
|
6209
|
-
function runtimeReadyForWrite(status) {
|
|
6210
|
-
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;
|
|
6211
|
-
}
|
|
6212
|
-
function projectUpdatedStatus(status, contractCount) {
|
|
6213
|
-
const healthy = status.hosted_heartbeat === "passed" && status.delivery_verified;
|
|
6214
|
-
return {
|
|
6215
|
-
...status,
|
|
6216
|
-
skill_contracts_current: contractCount,
|
|
6217
|
-
stale_skill_markers: [],
|
|
6218
|
-
manual_fallback_ready: true,
|
|
6219
|
-
healthy,
|
|
6220
|
-
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"
|
|
6221
|
-
};
|
|
6222
|
-
}
|
|
6223
|
-
function cleanup(path) {
|
|
6224
|
-
rmSync2(path, { force: true });
|
|
6225
|
-
}
|
|
6226
|
-
function writeAtomic(path, content, mode) {
|
|
6227
|
-
assertNoSymlinkAncestors2(dirname4(path));
|
|
6228
|
-
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
6229
|
-
try {
|
|
6230
|
-
mkdirSync4(dirname4(path), { recursive: true, mode: 493 });
|
|
6231
|
-
writeFileSync3(tempPath, content, { mode, flag: "wx" });
|
|
6232
|
-
renameSync(tempPath, path);
|
|
6233
|
-
} finally {
|
|
6234
|
-
cleanup(tempPath);
|
|
6235
|
-
}
|
|
6236
|
-
}
|
|
6237
|
-
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
6238
|
-
lstat: lstatOrNull,
|
|
6239
|
-
read: (path) => readFileSync3(path, "utf8"),
|
|
6240
|
-
write: writeAtomic
|
|
6241
|
-
};
|
|
6242
|
-
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
6243
|
-
const written = [];
|
|
6244
|
-
try {
|
|
6245
|
-
for (const snapshot of snapshots) {
|
|
6246
|
-
const currentStat = fileOperations.lstat(snapshot.path);
|
|
6247
|
-
if (!currentStat?.isFile() || fileOperations.read(snapshot.path) !== snapshot.content) {
|
|
6248
|
-
throw new Error("managed skill changed after inspection; refusing a stale write");
|
|
6249
|
-
}
|
|
6250
|
-
fileOperations.write(snapshot.path, canonicalContent, snapshot.mode);
|
|
6251
|
-
written.push(snapshot);
|
|
6252
|
-
}
|
|
6253
|
-
return { ok: true, error: null, rollback_conflicts: [] };
|
|
6254
|
-
} catch (error) {
|
|
6255
|
-
const rollbackConflicts = [];
|
|
6256
|
-
for (const snapshot of written.reverse()) {
|
|
6257
|
-
const currentStat = fileOperations.lstat(snapshot.path);
|
|
6258
|
-
if (!currentStat?.isFile()) {
|
|
6259
|
-
rollbackConflicts.push(`${snapshot.path}: no longer a regular file`);
|
|
6260
|
-
continue;
|
|
6261
|
-
}
|
|
6262
|
-
let currentContent;
|
|
6263
|
-
try {
|
|
6264
|
-
currentContent = fileOperations.read(snapshot.path);
|
|
6265
|
-
} catch {
|
|
6266
|
-
rollbackConflicts.push(`${snapshot.path}: could not read the current file`);
|
|
6267
|
-
continue;
|
|
6268
|
-
}
|
|
6269
|
-
if (currentContent !== canonicalContent) {
|
|
6270
|
-
rollbackConflicts.push(`${snapshot.path}: changed after this reconciliation wrote it`);
|
|
6271
|
-
continue;
|
|
6272
|
-
}
|
|
6273
|
-
try {
|
|
6274
|
-
fileOperations.write(snapshot.path, snapshot.content, snapshot.mode);
|
|
6275
|
-
} catch {
|
|
6276
|
-
rollbackConflicts.push(`${snapshot.path}: still owned but could not be restored`);
|
|
6277
|
-
}
|
|
6278
|
-
}
|
|
6279
|
-
return {
|
|
6280
|
-
ok: false,
|
|
6281
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6282
|
-
rollback_conflicts: rollbackConflicts
|
|
6283
|
-
};
|
|
6284
|
-
}
|
|
6285
|
-
}
|
|
6286
6732
|
async function reconcileManagedSkillRuntimes(options = {}) {
|
|
6733
|
+
const runtime = inspectLegacyInbox(options);
|
|
6287
6734
|
const dryRun = options.dryRun ?? false;
|
|
6288
|
-
const before = inspectInbox(options);
|
|
6289
|
-
const status = before.status;
|
|
6290
|
-
if (!status.skill_present) {
|
|
6291
|
-
return {
|
|
6292
|
-
runtimes: [{ ...status, action: "skipped", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6293
|
-
changed: 0,
|
|
6294
|
-
failed: 0,
|
|
6295
|
-
dry_run: dryRun
|
|
6296
|
-
};
|
|
6297
|
-
}
|
|
6298
|
-
if (before.snapshots.some((snapshot) => !snapshot.regular)) {
|
|
6299
|
-
return {
|
|
6300
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6301
|
-
changed: 0,
|
|
6302
|
-
failed: 1,
|
|
6303
|
-
dry_run: dryRun
|
|
6304
|
-
};
|
|
6305
|
-
}
|
|
6306
|
-
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname4(path))).find((found) => found !== null);
|
|
6307
|
-
if (symlinkedAncestor) {
|
|
6308
|
-
return {
|
|
6309
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6310
|
-
changed: 0,
|
|
6311
|
-
failed: 1,
|
|
6312
|
-
dry_run: dryRun
|
|
6313
|
-
};
|
|
6314
|
-
}
|
|
6315
|
-
if (!before.canonicalContent || !runtimeReadyForWrite(status)) {
|
|
6316
|
-
return {
|
|
6317
|
-
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6318
|
-
changed: 0,
|
|
6319
|
-
failed: 1,
|
|
6320
|
-
dry_run: dryRun
|
|
6321
|
-
};
|
|
6322
|
-
}
|
|
6323
|
-
const staleSnapshots = before.snapshots.filter((snapshot) => snapshot.content !== before.canonicalContent);
|
|
6324
|
-
if (staleSnapshots.length === 0) {
|
|
6325
|
-
return {
|
|
6326
|
-
runtimes: [{ ...status, action: "unchanged", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
6327
|
-
changed: 0,
|
|
6328
|
-
failed: 0,
|
|
6329
|
-
dry_run: dryRun
|
|
6330
|
-
};
|
|
6331
|
-
}
|
|
6332
|
-
if (dryRun) {
|
|
6333
|
-
const projected = projectUpdatedStatus(status, before.snapshots.length);
|
|
6334
|
-
return {
|
|
6335
|
-
runtimes: [{
|
|
6336
|
-
...projected,
|
|
6337
|
-
action: "update",
|
|
6338
|
-
dry_run: true,
|
|
6339
|
-
skill_contracts_changed: staleSnapshots.length
|
|
6340
|
-
}],
|
|
6341
|
-
changed: 1,
|
|
6342
|
-
failed: 0,
|
|
6343
|
-
dry_run: true
|
|
6344
|
-
};
|
|
6345
|
-
}
|
|
6346
|
-
const transaction = writeSkillContractsTransactional(staleSnapshots.map((snapshot) => ({
|
|
6347
|
-
path: snapshot.path,
|
|
6348
|
-
content: snapshot.content,
|
|
6349
|
-
mode: snapshot.mode ?? 420
|
|
6350
|
-
})), before.canonicalContent);
|
|
6351
|
-
if (!transaction.ok) {
|
|
6352
|
-
const rollbackConflictReason = transaction.rollback_conflicts.length > 0 ? `; rollback conflicts: ${transaction.rollback_conflicts.join("; ")}` : "";
|
|
6353
|
-
const reason = `${transaction.error ?? "managed skill reconciliation failed"}${rollbackConflictReason}`;
|
|
6354
|
-
return {
|
|
6355
|
-
runtimes: [{
|
|
6356
|
-
...status,
|
|
6357
|
-
action: "failed",
|
|
6358
|
-
dry_run: false,
|
|
6359
|
-
skill_contracts_changed: 0,
|
|
6360
|
-
reason
|
|
6361
|
-
}],
|
|
6362
|
-
changed: 0,
|
|
6363
|
-
failed: 1,
|
|
6364
|
-
dry_run: false
|
|
6365
|
-
};
|
|
6366
|
-
}
|
|
6367
|
-
const after = inspectInbox(options).status;
|
|
6368
|
-
const accepted = after.healthy || after.manual_fallback_ready;
|
|
6369
6735
|
return {
|
|
6370
|
-
runtimes: [{
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
skill_contracts_changed: accepted ? staleSnapshots.length : 0
|
|
6375
|
-
}],
|
|
6376
|
-
changed: accepted ? 1 : 0,
|
|
6377
|
-
failed: accepted ? 0 : 1,
|
|
6378
|
-
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
|
|
6379
6740
|
};
|
|
6380
6741
|
}
|
|
6381
6742
|
|
|
6382
6743
|
// src/status.ts
|
|
6383
|
-
import { existsSync as
|
|
6744
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
6384
6745
|
|
|
6385
6746
|
// src/lib/package-version.ts
|
|
6386
|
-
import { existsSync as
|
|
6387
|
-
import { dirname as
|
|
6747
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
6748
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6388
6749
|
import { fileURLToPath } from "url";
|
|
6389
6750
|
var cached = null;
|
|
6390
6751
|
function getPackageVersion() {
|
|
6391
6752
|
if (cached)
|
|
6392
6753
|
return cached;
|
|
6393
6754
|
try {
|
|
6394
|
-
let dir =
|
|
6755
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
6395
6756
|
for (let i = 0;i < 8; i++) {
|
|
6396
|
-
const pkgPath =
|
|
6397
|
-
if (
|
|
6757
|
+
const pkgPath = join10(dir, "package.json");
|
|
6758
|
+
if (existsSync7(pkgPath)) {
|
|
6398
6759
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
6399
6760
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
6400
6761
|
cached = pkg.version;
|
|
6401
6762
|
return cached;
|
|
6402
6763
|
}
|
|
6403
6764
|
}
|
|
6404
|
-
const parent =
|
|
6765
|
+
const parent = dirname6(dir);
|
|
6405
6766
|
if (parent === dir)
|
|
6406
6767
|
break;
|
|
6407
6768
|
dir = parent;
|
|
@@ -6470,7 +6831,7 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6470
6831
|
continue;
|
|
6471
6832
|
knownTargets += 1;
|
|
6472
6833
|
const targetPath = expandPath(config.target_path);
|
|
6473
|
-
if (!
|
|
6834
|
+
if (!existsSync8(targetPath)) {
|
|
6474
6835
|
missingTargets += 1;
|
|
6475
6836
|
continue;
|
|
6476
6837
|
}
|
|
@@ -6567,8 +6928,8 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6567
6928
|
|
|
6568
6929
|
// src/lib/provider-context.ts
|
|
6569
6930
|
import { createHash as createHash5 } from "crypto";
|
|
6570
|
-
import { existsSync as
|
|
6571
|
-
import { join as
|
|
6931
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
6932
|
+
import { join as join11 } from "path";
|
|
6572
6933
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
6573
6934
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
6574
6935
|
var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
|
|
@@ -6703,7 +7064,7 @@ function renderPerEndpointFragment(entry) {
|
|
|
6703
7064
|
function renderProviderFragment(entry) {
|
|
6704
7065
|
return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
|
|
6705
7066
|
}
|
|
6706
|
-
function
|
|
7067
|
+
function sha2564(content) {
|
|
6707
7068
|
return createHash5("sha256").update(content).digest("hex");
|
|
6708
7069
|
}
|
|
6709
7070
|
function resolveAndRenderProviderContext(opts) {
|
|
@@ -6713,17 +7074,17 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
6713
7074
|
const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
|
|
6714
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;
|
|
6715
7076
|
const content = renderProviderFragment(entry);
|
|
6716
|
-
const dir =
|
|
6717
|
-
if (!
|
|
6718
|
-
|
|
7077
|
+
const dir = join11(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
7078
|
+
if (!existsSync9(dir))
|
|
7079
|
+
mkdirSync4(dir, { recursive: true });
|
|
6719
7080
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
6720
|
-
const fragmentPath =
|
|
6721
|
-
const fragmentSha256 =
|
|
6722
|
-
|
|
6723
|
-
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);
|
|
6724
7085
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
6725
7086
|
try {
|
|
6726
|
-
if (
|
|
7087
|
+
if (existsSync9(manifestPath)) {
|
|
6727
7088
|
const parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
6728
7089
|
if (parsed && typeof parsed === "object")
|
|
6729
7090
|
manifest = parsed;
|
|
@@ -6739,7 +7100,7 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
6739
7100
|
rawModel: opts.rawModel || null
|
|
6740
7101
|
};
|
|
6741
7102
|
manifest.fragments = fragmentsObj;
|
|
6742
|
-
|
|
7103
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
6743
7104
|
return {
|
|
6744
7105
|
entry,
|
|
6745
7106
|
rawEndpoint: opts.rawEndpoint,
|
|
@@ -6870,11 +7231,11 @@ function parseSessionSource(value, order) {
|
|
|
6870
7231
|
if (!path)
|
|
6871
7232
|
throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
|
|
6872
7233
|
const absPath = resolveSessionPath(path);
|
|
6873
|
-
if (!
|
|
7234
|
+
if (!existsSync10(absPath))
|
|
6874
7235
|
throw new Error(`Instruction source file not found: ${absPath}`);
|
|
6875
7236
|
const content = readSessionInstructionSourceFile(absPath);
|
|
6876
7237
|
const source = sourceFromFilePath(absPath, content, order);
|
|
6877
|
-
const resolvedId = id || source.id ||
|
|
7238
|
+
const resolvedId = id || source.id || basename2(absPath);
|
|
6878
7239
|
return {
|
|
6879
7240
|
...source,
|
|
6880
7241
|
id: resolvedId,
|
|
@@ -6914,7 +7275,7 @@ function sessionSourceReplacements(values) {
|
|
|
6914
7275
|
return replacements;
|
|
6915
7276
|
}
|
|
6916
7277
|
function readSessionInstructionSourceFile(path) {
|
|
6917
|
-
const stat =
|
|
7278
|
+
const stat = lstatSync6(path);
|
|
6918
7279
|
if (stat.isSymbolicLink()) {
|
|
6919
7280
|
throw new Error("SESSION_SOURCE_SYMLINK_REJECTED: instruction source file must be a regular non-symlink file");
|
|
6920
7281
|
}
|
|
@@ -6947,11 +7308,13 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6947
7308
|
const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index));
|
|
6948
7309
|
for (const value of opts.config ?? []) {
|
|
6949
7310
|
const { layer, id } = parseLayeredReference(value);
|
|
6950
|
-
|
|
7311
|
+
const config = await store.getConfig(id);
|
|
7312
|
+
assertInstructionSource(config);
|
|
7313
|
+
sources.push(sourceFromConfig(config, sources.length, layer));
|
|
6951
7314
|
}
|
|
6952
7315
|
for (const value of opts.identityExport ?? []) {
|
|
6953
7316
|
const path = resolveSessionPath(value);
|
|
6954
|
-
if (!
|
|
7317
|
+
if (!existsSync10(path))
|
|
6955
7318
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
6956
7319
|
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
6957
7320
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
@@ -6969,7 +7332,7 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6969
7332
|
}
|
|
6970
7333
|
async function loadOwnedClaudeAuthorities(store) {
|
|
6971
7334
|
const configs = await store.listConfigs({ category: "rules", agent: "claude", kind: "file" });
|
|
6972
|
-
return configs.filter((config) => config.target_path &&
|
|
7335
|
+
return configs.filter((config) => config.target_path && basename2(normalizeTargetPath(config.target_path)) === "AGENTS.md").map((config) => ({ slug: config.slug, targetPath: config.target_path, content: config.content }));
|
|
6973
7336
|
}
|
|
6974
7337
|
async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-run") {
|
|
6975
7338
|
const station = opts.stationProfile === false ? null : stationProfileSource();
|
|
@@ -6999,8 +7362,10 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
6999
7362
|
}
|
|
7000
7363
|
const profile = await store.getProfile(opts.compileProfile);
|
|
7001
7364
|
const configs = await store.getProfileConfigs(profile.id);
|
|
7002
|
-
const bindings = await store.getProfileConfigBindings(profile.id);
|
|
7003
|
-
|
|
7365
|
+
const bindings = await store.getProfileConfigBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7366
|
+
if (store.mode === "api")
|
|
7367
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
7368
|
+
const assetBindings = await store.getProfileAssetBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7004
7369
|
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
7005
7370
|
return planProfileSessionRender({
|
|
7006
7371
|
profile_id: profile.id,
|
|
@@ -7010,6 +7375,24 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
7010
7375
|
targetHome: opts.targetHome,
|
|
7011
7376
|
projectRoot: opts.projectRoot,
|
|
7012
7377
|
sessionId: opts.sessionId,
|
|
7378
|
+
...store.mode === "api" && store.v1BaseUrl ? {
|
|
7379
|
+
refreshSelector: normalizeSessionHostedProfileSelector({
|
|
7380
|
+
schema: "hasna.instructions.hosted-profile-selector/v1",
|
|
7381
|
+
authority: store.v1BaseUrl,
|
|
7382
|
+
profileId: profile.id,
|
|
7383
|
+
providerVersion: opts.providerVersion,
|
|
7384
|
+
...opts.providerVariant ? { providerVariant: opts.providerVariant } : {},
|
|
7385
|
+
...opts.model ? { model: opts.model } : {},
|
|
7386
|
+
...opts.path ? { path: opts.path } : {},
|
|
7387
|
+
manual: opts.manual ?? [],
|
|
7388
|
+
codewithNativeImports: opts.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true",
|
|
7389
|
+
allowEmptySources: opts.allowEmptySources === true,
|
|
7390
|
+
stationProfile: opts.stationProfile !== false,
|
|
7391
|
+
checkGlobalCoverage: opts.checkGlobalCoverage === true,
|
|
7392
|
+
...opts.assetSurface ? { assetSurface: opts.assetSurface } : {},
|
|
7393
|
+
...opts.assetScope ? { assetScope: opts.assetScope } : {}
|
|
7394
|
+
})
|
|
7395
|
+
} : {},
|
|
7013
7396
|
codewithNativeImports: opts.codewithNativeImports,
|
|
7014
7397
|
allowEmptySources: opts.allowEmptySources,
|
|
7015
7398
|
configs,
|
|
@@ -7085,12 +7468,12 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
7085
7468
|
if (value === "-")
|
|
7086
7469
|
return { json: readBoundedProjectContextStdin() };
|
|
7087
7470
|
const path = resolveSessionPath(value);
|
|
7088
|
-
if (!
|
|
7471
|
+
if (!existsSync10(path)) {
|
|
7089
7472
|
if (allowMissing)
|
|
7090
7473
|
return {};
|
|
7091
7474
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
|
|
7092
7475
|
}
|
|
7093
|
-
const stat =
|
|
7476
|
+
const stat = lstatSync6(path);
|
|
7094
7477
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
7095
7478
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", "bundle input must be a regular non-symlink file");
|
|
7096
7479
|
}
|
|
@@ -7476,9 +7859,14 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
|
|
|
7476
7859
|
}
|
|
7477
7860
|
console.log(chalk.green("\u2713") + ` Tags on ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}: ${nextTags.join(", ") || chalk.dim("(none)")}`);
|
|
7478
7861
|
});
|
|
7479
|
-
program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").option("--update", "if a config already owns this path, update that row in place instead of refusing").action(async (filePath, opts) => {
|
|
7480
|
-
const
|
|
7481
|
-
|
|
7862
|
+
program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").option("--update", "if a config already owns this path, update that row in place instead of refusing").option("--expected-version <version>", "with --update, require the stored version to match atomically").action(async (filePath, opts) => {
|
|
7863
|
+
const expectedVersion = opts.expectedVersion === undefined ? undefined : Number(opts.expectedVersion);
|
|
7864
|
+
validateExpectedConfigVersion(expectedVersion);
|
|
7865
|
+
if (expectedVersion !== undefined && !opts.update) {
|
|
7866
|
+
throw new Error("--expected-version requires --update");
|
|
7867
|
+
}
|
|
7868
|
+
const abs = resolve8(filePath);
|
|
7869
|
+
if (!existsSync10(abs)) {
|
|
7482
7870
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
7483
7871
|
process.exit(1);
|
|
7484
7872
|
}
|
|
@@ -7513,10 +7901,11 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7513
7901
|
const exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
|
|
7514
7902
|
const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
|
|
7515
7903
|
const rest = existingOwners.filter((owner) => owner.id !== target.id);
|
|
7516
|
-
if (content !== target.content) {
|
|
7904
|
+
if (content !== target.content && expectedVersion === undefined) {
|
|
7517
7905
|
await store.createSnapshot(target.id, target.content, target.version);
|
|
7518
7906
|
}
|
|
7519
7907
|
config = await store.updateConfig(target.id, {
|
|
7908
|
+
expected_version: expectedVersion,
|
|
7520
7909
|
content,
|
|
7521
7910
|
format: storedFmt,
|
|
7522
7911
|
is_template: (opts.template ?? false) || isTemplate,
|
|
@@ -7533,6 +7922,9 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7533
7922
|
}
|
|
7534
7923
|
return;
|
|
7535
7924
|
}
|
|
7925
|
+
if (expectedVersion !== undefined) {
|
|
7926
|
+
throw new Error("--expected-version requires an existing config; no config was created");
|
|
7927
|
+
}
|
|
7536
7928
|
config = await store.createConfig({
|
|
7537
7929
|
name,
|
|
7538
7930
|
kind: opts.kind ?? "file",
|
|
@@ -7661,7 +8053,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7661
8053
|
for (const entry of entries) {
|
|
7662
8054
|
if (!entry.isDirectory())
|
|
7663
8055
|
continue;
|
|
7664
|
-
const projDir =
|
|
8056
|
+
const projDir = join12(absDir, entry.name);
|
|
7665
8057
|
const hasAgentConfig = [
|
|
7666
8058
|
"CLAUDE.md",
|
|
7667
8059
|
".mcp.json",
|
|
@@ -7674,7 +8066,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7674
8066
|
".aicopilot",
|
|
7675
8067
|
".cursor",
|
|
7676
8068
|
".agents"
|
|
7677
|
-
].some((marker) =>
|
|
8069
|
+
].some((marker) => existsSync10(join12(projDir, marker)));
|
|
7678
8070
|
if (!hasAgentConfig)
|
|
7679
8071
|
continue;
|
|
7680
8072
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
|
|
@@ -7722,7 +8114,7 @@ program.command("import <file>").description("Restore a v2 Instructions domain a
|
|
|
7722
8114
|
});
|
|
7723
8115
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
7724
8116
|
const store = resolveConfigStore();
|
|
7725
|
-
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");
|
|
7726
8118
|
const stats = await store.getConfigStats();
|
|
7727
8119
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
7728
8120
|
console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
|
|
@@ -7945,7 +8337,7 @@ profileCmd.command("binding <profile> <config>").description("Set a config's sch
|
|
|
7945
8337
|
process.exit(1);
|
|
7946
8338
|
}
|
|
7947
8339
|
});
|
|
7948
|
-
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) => {
|
|
7949
8341
|
try {
|
|
7950
8342
|
const store = resolveConfigStore();
|
|
7951
8343
|
const { machine, profile } = await getMachineProfileContext(opts, store);
|
|
@@ -7982,14 +8374,6 @@ profileCmd.command("apply [id]").description("Apply all configs in a profile to
|
|
|
7982
8374
|
}
|
|
7983
8375
|
if (report.failures.length > 0)
|
|
7984
8376
|
process.exitCode = 1;
|
|
7985
|
-
const runtimeReport = await reconcileManagedSkillRuntimes({
|
|
7986
|
-
dryRun: opts.dryRun,
|
|
7987
|
-
agent: opts.from,
|
|
7988
|
-
deliveryVerified: opts.deliveryVerified
|
|
7989
|
-
});
|
|
7990
|
-
printManagedSkillRuntimeReport(runtimeReport);
|
|
7991
|
-
if (runtimeReport.failed > 0)
|
|
7992
|
-
process.exitCode = 1;
|
|
7993
8377
|
console.log(chalk.dim(`
|
|
7994
8378
|
${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${machine.os_family}/${machine.arch})`));
|
|
7995
8379
|
} catch (e) {
|
|
@@ -8106,7 +8490,7 @@ projectContextCmd.command("apply").description("Atomically write project context
|
|
|
8106
8490
|
}
|
|
8107
8491
|
});
|
|
8108
8492
|
var sessionCmd = program.command("session").description("Plan and apply session-scoped agent instruction files");
|
|
8109
|
-
sessionCmd.command("plan").description("Produce a dry-run render plan for profile-scoped instruction injection").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; Grok: GROK_HOME
|
|
8493
|
+
sessionCmd.command("plan").description("Produce a dry-run render plan for profile-scoped instruction injection").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("--allow-asset-installers", "opt in to planned provider installers; planning never invokes them").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render plan").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("--json", "output dry-run JSON").action(async (opts) => {
|
|
8110
8494
|
try {
|
|
8111
8495
|
const tool = opts.tool;
|
|
8112
8496
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -8161,7 +8545,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
8161
8545
|
process.exit(1);
|
|
8162
8546
|
}
|
|
8163
8547
|
});
|
|
8164
|
-
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; Grok: GROK_HOME
|
|
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) => {
|
|
8165
8549
|
try {
|
|
8166
8550
|
const tool = opts.tool;
|
|
8167
8551
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -8187,7 +8571,16 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8187
8571
|
return;
|
|
8188
8572
|
}
|
|
8189
8573
|
const ownedClaudeAuthorities = tool === "claude" ? await loadOwnedClaudeAuthorities(store) : undefined;
|
|
8190
|
-
const
|
|
8574
|
+
const parsePreimages = (values, option) => values.map((value) => {
|
|
8575
|
+
const separator = value.lastIndexOf("=");
|
|
8576
|
+
if (separator <= 0)
|
|
8577
|
+
throw new Error(`${option} requires relativepath=sha256.`);
|
|
8578
|
+
return { relativePath: value.slice(0, separator), sha256: value.slice(separator + 1) };
|
|
8579
|
+
});
|
|
8580
|
+
const adoptFiles = parsePreimages(opts.adoptFile, "--adopt-file");
|
|
8581
|
+
const reconcileFiles = parsePreimages(opts.reconcileFile, "--reconcile-file");
|
|
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 });
|
|
8191
8584
|
if (opts.json) {
|
|
8192
8585
|
printJson({
|
|
8193
8586
|
...result,
|
|
@@ -8228,7 +8621,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8228
8621
|
console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
|
|
8229
8622
|
}
|
|
8230
8623
|
if (result.conflicts.length > 0) {
|
|
8231
|
-
console.error(chalk.red(`Conflicts: ${result.conflicts.length}.
|
|
8624
|
+
console.error(chalk.red(`Conflicts: ${result.conflicts.length}. Review and import existing instructions before adopting each exact preimage with --adopt-file.`));
|
|
8232
8625
|
process.exitCode = 1;
|
|
8233
8626
|
}
|
|
8234
8627
|
} catch (e) {
|
|
@@ -8236,6 +8629,29 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8236
8629
|
process.exit(1);
|
|
8237
8630
|
}
|
|
8238
8631
|
});
|
|
8632
|
+
sessionCmd.command("refresh").description("Fetch the recorded hosted profile, recompile and safely update a managed target; unchanged output is not rewritten").requiredOption("--target-home <path>", "existing managed global or project target containing its hosted profile selector").option("--dry-run", "fetch authoritative sources and preview without writing").option("--json", "output refresh receipt without instruction content").action(async (opts) => {
|
|
8633
|
+
try {
|
|
8634
|
+
const result = await refreshSessionRender({ targetHome: resolveSessionPath(opts.targetHome), store: resolveConfigStore(), dryRun: opts.dryRun });
|
|
8635
|
+
if (opts.json)
|
|
8636
|
+
printJson(result);
|
|
8637
|
+
else {
|
|
8638
|
+
console.log(`${result.status}: ${result.targetHome}`);
|
|
8639
|
+
console.log(`hosted profile: ${result.selector.profileId}`);
|
|
8640
|
+
console.log(`source hash: ${result.sourceHash}`);
|
|
8641
|
+
if (result.apply.snapshotPath)
|
|
8642
|
+
console.log(`snapshot: ${result.apply.snapshotPath}`);
|
|
8643
|
+
}
|
|
8644
|
+
if (result.status === "blocked")
|
|
8645
|
+
process.exitCode = 1;
|
|
8646
|
+
} catch (error) {
|
|
8647
|
+
const message = formatCliError(error);
|
|
8648
|
+
if (opts.json)
|
|
8649
|
+
printJson({ status: "failed", error: message });
|
|
8650
|
+
else
|
|
8651
|
+
console.error(chalk.red(message));
|
|
8652
|
+
process.exitCode = 1;
|
|
8653
|
+
}
|
|
8654
|
+
});
|
|
8239
8655
|
sessionCmd.command("restore <snapshot>").description("Restore a session render snapshot only when applied files have not drifted").option("--dry-run", "preview restore actions and conflicts without writing").option("--json", "output restore JSON").action(async (snapshot, opts) => {
|
|
8240
8656
|
try {
|
|
8241
8657
|
const result = restoreSessionRenderSnapshot(resolveSessionPath(snapshot), {
|
|
@@ -8264,6 +8680,36 @@ sessionCmd.command("restore <snapshot>").description("Restore a session render s
|
|
|
8264
8680
|
process.exit(1);
|
|
8265
8681
|
}
|
|
8266
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
|
+
});
|
|
8267
8713
|
var stationProfileCmd = program.command("station-profile").description("Generate and inject the compact station profile block into every session render");
|
|
8268
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) => {
|
|
8269
8715
|
try {
|
|
@@ -8430,7 +8876,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
|
|
|
8430
8876
|
});
|
|
8431
8877
|
templateCmd.command("render <id>").description("Render a template config with variables and optionally apply to disk").option("--var <vars...>", "set variables as KEY=VALUE pairs").option("--env", "use environment variables to fill template vars").option("--apply", "write rendered output to target_path").option("--dry-run", "preview rendered output without writing").action(async (id, opts) => {
|
|
8432
8878
|
try {
|
|
8433
|
-
const { renderTemplate } = await import("../chunks/template-
|
|
8879
|
+
const { renderTemplate } = await import("../chunks/template-e4qrxs8p.js");
|
|
8434
8880
|
const c = await resolveConfigStore().getConfig(id);
|
|
8435
8881
|
const vars = {};
|
|
8436
8882
|
if (opts.var) {
|
|
@@ -8444,7 +8890,7 @@ templateCmd.command("render <id>").description("Render a template config with va
|
|
|
8444
8890
|
}
|
|
8445
8891
|
}
|
|
8446
8892
|
if (opts.env) {
|
|
8447
|
-
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-
|
|
8893
|
+
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-e4qrxs8p.js");
|
|
8448
8894
|
for (const v of extractTemplateVars2(c.content)) {
|
|
8449
8895
|
if (!(v.name in vars) && process.env[v.name]) {
|
|
8450
8896
|
vars[v.name] = process.env[v.name];
|
|
@@ -8493,7 +8939,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
|
|
|
8493
8939
|
} else if (opts.all) {
|
|
8494
8940
|
configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
|
|
8495
8941
|
} else {
|
|
8496
|
-
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-
|
|
8942
|
+
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-6r1v8hd3.js");
|
|
8497
8943
|
const slugs = [
|
|
8498
8944
|
...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
|
|
8499
8945
|
];
|
|
@@ -8780,7 +9226,7 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
|
|
|
8780
9226
|
} else if (target === "codex") {
|
|
8781
9227
|
const { appendFileSync, existsSync: ex, mkdirSync: md } = await import("fs");
|
|
8782
9228
|
const { dirname: dn, join: j } = await import("path");
|
|
8783
|
-
const configPath = j(
|
|
9229
|
+
const configPath = j(homedir5(), ".codex", "config.toml");
|
|
8784
9230
|
const block = `
|
|
8785
9231
|
[mcp_servers.configs]
|
|
8786
9232
|
command = "${mcpBinary}"
|
|
@@ -8800,7 +9246,7 @@ args = []
|
|
|
8800
9246
|
} else if (target === "antigravity") {
|
|
8801
9247
|
const { mkdirSync: md, readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
|
|
8802
9248
|
const { dirname: dn, join: j } = await import("path");
|
|
8803
|
-
const configPath = j(
|
|
9249
|
+
const configPath = j(homedir5(), ".gemini", "config", "mcp_config.json");
|
|
8804
9250
|
let settings = {};
|
|
8805
9251
|
if (ex(configPath)) {
|
|
8806
9252
|
try {
|
|
@@ -8844,7 +9290,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8844
9290
|
} else if (target === "codex") {
|
|
8845
9291
|
const { existsSync: ex, writeFileSync: wf } = await import("fs");
|
|
8846
9292
|
const { join: j } = await import("path");
|
|
8847
|
-
const configPath = j(
|
|
9293
|
+
const configPath = j(homedir5(), ".codex", "config.toml");
|
|
8848
9294
|
if (!ex(configPath)) {
|
|
8849
9295
|
console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
|
|
8850
9296
|
continue;
|
|
@@ -8868,7 +9314,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8868
9314
|
console.log(chalk.green("\u2713") + " Removed from Codex");
|
|
8869
9315
|
} else if (target === "antigravity") {
|
|
8870
9316
|
const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
|
|
8871
|
-
const configPath =
|
|
9317
|
+
const configPath = join12(homedir5(), ".gemini", "config", "mcp_config.json");
|
|
8872
9318
|
if (!ex(configPath)) {
|
|
8873
9319
|
console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
|
|
8874
9320
|
continue;
|
|
@@ -8903,15 +9349,15 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8903
9349
|
if (!opts.confirmLocal) {
|
|
8904
9350
|
throw new Error("migrate-legacy requires --confirm-local");
|
|
8905
9351
|
}
|
|
8906
|
-
const destinationPath =
|
|
9352
|
+
const destinationPath = resolve8(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join12(getRawStoreRoot(), "instructions.db"));
|
|
8907
9353
|
const database = await import("../chunks/database-wstmr56z.js");
|
|
8908
9354
|
database.getDatabase(destinationPath);
|
|
8909
9355
|
database.resetDatabase();
|
|
8910
9356
|
const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
|
|
8911
9357
|
const result = migrateLegacyInstructionsStore({
|
|
8912
|
-
...opts.source ? { sourcePath:
|
|
9358
|
+
...opts.source ? { sourcePath: resolve8(opts.source) } : {},
|
|
8913
9359
|
destinationPath,
|
|
8914
|
-
...opts.backup ? { backupPath:
|
|
9360
|
+
...opts.backup ? { backupPath: resolve8(opts.backup) } : {},
|
|
8915
9361
|
dryRun: !opts.apply,
|
|
8916
9362
|
...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
|
|
8917
9363
|
operatorConfirmed: true
|
|
@@ -8930,7 +9376,7 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8930
9376
|
if (result.backupCreated)
|
|
8931
9377
|
console.log(chalk.dim(" destination backup created before mutation"));
|
|
8932
9378
|
});
|
|
8933
|
-
program.command("init").description("First-time setup:
|
|
9379
|
+
program.command("init").description("First-time setup: create default profiles; local file import into a hosted authority is explicit").option("--force", "delete existing DB and start fresh").option("--import-local", "explicitly import known local config files into the configured hosted authority").action(async (opts) => {
|
|
8934
9380
|
const store = resolveConfigStore();
|
|
8935
9381
|
if (opts.force) {
|
|
8936
9382
|
try {
|
|
@@ -8944,26 +9390,28 @@ program.command("init").description("First-time setup: sync all known configs, c
|
|
|
8944
9390
|
}
|
|
8945
9391
|
console.log(chalk.bold(`@hasna/instructions \u2014 initializing
|
|
8946
9392
|
`));
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
9393
|
+
if (store.mode === "local" || opts.importLocal) {
|
|
9394
|
+
const result = await syncKnown({ store });
|
|
9395
|
+
console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
|
|
9396
|
+
if (result.skipped.length > 0)
|
|
9397
|
+
console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
|
|
9398
|
+
} else {
|
|
9399
|
+
console.log(chalk.dim("Hosted authority retained; use --import-local for an intentional local-file migration."));
|
|
8951
9400
|
}
|
|
8952
9401
|
const refs = [
|
|
8953
9402
|
{ slug: "workspace-structure", name: "Workspace Structure", category: "workspace", content: `# Workspace Structure
|
|
8954
9403
|
|
|
8955
|
-
|
|
8956
|
-
{ slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `#
|
|
8957
|
-
|
|
8958
|
-
Location: ~/.secrets (sourced by ~/.zshrc)
|
|
8959
|
-
Format: export KEY_NAME="value"
|
|
9404
|
+
Resolve repositories and task worktrees through the configured repository manager. Use the configured scratch workspace for temporary execution artifacts. Keep durable project records in their owning application and follow the current project conventions.`, desc: "Repository, worktree, and execution scratch responsibilities" },
|
|
9405
|
+
{ slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `# Credential References
|
|
8960
9406
|
|
|
8961
|
-
|
|
9407
|
+
Store credential values in the configured Secrets authority. Application configuration records contain credential names and supported resolver references only. Resolve values through the approved consuming application at execution time; never embed values in instruction content, generated prompts, or shell profiles.`, desc: "Secret reference and runtime resolution conventions; no credential values" }
|
|
8962
9408
|
];
|
|
8963
9409
|
for (const ref of refs) {
|
|
8964
9410
|
try {
|
|
8965
9411
|
await store.getConfig(ref.slug);
|
|
8966
|
-
} catch {
|
|
9412
|
+
} catch (error) {
|
|
9413
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
9414
|
+
throw error;
|
|
8967
9415
|
await store.createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
|
|
8968
9416
|
}
|
|
8969
9417
|
}
|
|
@@ -8973,12 +9421,11 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
|
|
|
8973
9421
|
await ensureProjectDashboardStandardConfig(store);
|
|
8974
9422
|
try {
|
|
8975
9423
|
await store.getProfile("my-setup");
|
|
8976
|
-
} catch {
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
|
|
9424
|
+
} catch (error) {
|
|
9425
|
+
if (!(error instanceof ProfileNotFoundError))
|
|
9426
|
+
throw error;
|
|
9427
|
+
await store.createProfile({ name: "my-setup", description: "Instruction profile; add explicitly reviewed and scoped source bindings" });
|
|
9428
|
+
console.log(chalk.green("\u2713") + ' Created empty profile "my-setup"; bind reviewed instruction sources explicitly.');
|
|
8982
9429
|
}
|
|
8983
9430
|
const machineProfiles = await ensurePlatformProfiles(store);
|
|
8984
9431
|
console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
|
|
@@ -8989,7 +9436,7 @@ DB stats:`));
|
|
|
8989
9436
|
if (count > 0)
|
|
8990
9437
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
8991
9438
|
}
|
|
8992
|
-
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");
|
|
8993
9440
|
console.log(chalk.dim(`
|
|
8994
9441
|
${store.mode === "api" ? "API" : "DB"}: ${location}`));
|
|
8995
9442
|
});
|
|
@@ -9007,11 +9454,11 @@ program.command("status").description("Health check: total configs, drift from d
|
|
|
9007
9454
|
console.log(chalk.cyan("Missing:") + ` ${status.health.missingTargets === 0 ? chalk.green("0") : chalk.yellow(String(status.health.missingTargets))} (file not on disk)`);
|
|
9008
9455
|
console.log(chalk.cyan("Secrets:") + ` ${status.health.unredactedSecretFindings === 0 ? chalk.green("0 \u2713") : chalk.red(String(status.health.unredactedSecretFindings) + " \u26A0")} unredacted`);
|
|
9009
9456
|
console.log(chalk.cyan("Retired agents:") + ` ${status.health.retiredAgentRows === 0 ? chalk.green("0") : chalk.yellow(String(status.health.retiredAgentRows))} row(s)`);
|
|
9010
|
-
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)`);
|
|
9011
9458
|
console.log(chalk.cyan("Templates:") + ` ${status.counts.configs.templates} (with {{VAR}} placeholders)`);
|
|
9012
9459
|
});
|
|
9013
|
-
var managedSkillsCmd = program.command("managed-skills").description("
|
|
9014
|
-
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) => {
|
|
9015
9462
|
const report = inspectManagedSkillRuntimes({
|
|
9016
9463
|
agent: opts.from,
|
|
9017
9464
|
deliveryVerified: opts.deliveryVerified
|
|
@@ -9023,7 +9470,7 @@ managedSkillsCmd.command("status").option("--from <agent>", "verify the hosted C
|
|
|
9023
9470
|
return;
|
|
9024
9471
|
}
|
|
9025
9472
|
if (report.skills_present === 0) {
|
|
9026
|
-
console.log(chalk.dim("No
|
|
9473
|
+
console.log(chalk.dim("No legacy native Inbox skill found. Use the Skills CLI to inspect selected skills."));
|
|
9027
9474
|
return;
|
|
9028
9475
|
}
|
|
9029
9476
|
for (const runtime of report.runtimes) {
|
|
@@ -9035,7 +9482,7 @@ managedSkillsCmd.command("status").option("--from <agent>", "verify the hosted C
|
|
|
9035
9482
|
if (report.missing > 0)
|
|
9036
9483
|
process.exitCode = 1;
|
|
9037
9484
|
});
|
|
9038
|
-
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) => {
|
|
9039
9486
|
const report = await reconcileManagedSkillRuntimes({
|
|
9040
9487
|
dryRun: opts.dryRun,
|
|
9041
9488
|
agent: opts.from,
|
|
@@ -9090,8 +9537,8 @@ storageCmd.command("status").description("Show redacted S3 configuration status
|
|
|
9090
9537
|
});
|
|
9091
9538
|
var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
|
|
9092
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) => {
|
|
9093
|
-
const abs =
|
|
9094
|
-
if (!
|
|
9540
|
+
const abs = resolve8(file);
|
|
9541
|
+
if (!existsSync10(abs) || !lstatSync6(abs).isFile())
|
|
9095
9542
|
throw new Error(`Backup file not found: ${abs}`);
|
|
9096
9543
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
9097
9544
|
import("../chunks/s3-config-hhght91k.js"),
|
|
@@ -9181,19 +9628,19 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9181
9628
|
backupId,
|
|
9182
9629
|
...versions
|
|
9183
9630
|
});
|
|
9184
|
-
const output =
|
|
9185
|
-
if (
|
|
9631
|
+
const output = resolve8(opts.output);
|
|
9632
|
+
if (existsSync10(output)) {
|
|
9186
9633
|
if (!opts.force)
|
|
9187
9634
|
throw new Error(`Refusing to overwrite existing backup file: ${output}`);
|
|
9188
|
-
if (!
|
|
9635
|
+
if (!lstatSync6(output).isFile() || lstatSync6(output).isSymbolicLink()) {
|
|
9189
9636
|
throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
|
|
9190
9637
|
}
|
|
9191
9638
|
}
|
|
9192
|
-
|
|
9193
|
-
const temp =
|
|
9639
|
+
mkdirSync5(dirname7(output), { recursive: true, mode: 448 });
|
|
9640
|
+
const temp = join12(dirname7(output), `.${basename2(output)}.tmp-${process.pid}-${Date.now()}`);
|
|
9194
9641
|
try {
|
|
9195
|
-
|
|
9196
|
-
|
|
9642
|
+
writeFileSync4(temp, pulled.bytes, { flag: "wx", mode: 384 });
|
|
9643
|
+
renameSync(temp, output);
|
|
9197
9644
|
} catch (error) {
|
|
9198
9645
|
try {
|
|
9199
9646
|
unlinkSync(temp);
|
|
@@ -9215,10 +9662,10 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9215
9662
|
});
|
|
9216
9663
|
program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
|
|
9217
9664
|
const { mkdirSync: mk } = await import("fs");
|
|
9218
|
-
const backupDir =
|
|
9665
|
+
const backupDir = join12(getRawStoreRoot(), "backups");
|
|
9219
9666
|
mk(backupDir, { recursive: true });
|
|
9220
9667
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
|
|
9221
|
-
const outPath =
|
|
9668
|
+
const outPath = join12(backupDir, `configs-${ts}.tar.gz`);
|
|
9222
9669
|
const result = await exportConfigs(outPath, { store: resolveConfigStore() });
|
|
9223
9670
|
const { statSync: st } = await import("fs");
|
|
9224
9671
|
const size = st(outPath).size;
|
|
@@ -9244,9 +9691,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
9244
9691
|
console.log(chalk.cyan("Known files on disk:"));
|
|
9245
9692
|
for (const k of KNOWN_CONFIGS) {
|
|
9246
9693
|
if (k.rulesDir) {
|
|
9247
|
-
|
|
9694
|
+
existsSync10(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
|
|
9248
9695
|
} else {
|
|
9249
|
-
|
|
9696
|
+
existsSync10(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
|
|
9250
9697
|
}
|
|
9251
9698
|
}
|
|
9252
9699
|
const allConfigs = await store.listConfigs();
|
|
@@ -9392,7 +9839,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9392
9839
|
const store = resolveConfigStore();
|
|
9393
9840
|
const interval = Number(opts.interval);
|
|
9394
9841
|
const { statSync: st } = await import("fs");
|
|
9395
|
-
const { expandPath: expandPath2 } = await import("../chunks/apply-
|
|
9842
|
+
const { expandPath: expandPath2 } = await import("../chunks/apply-fyfzqc7s.js");
|
|
9396
9843
|
console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
|
|
9397
9844
|
console.log(chalk.dim(`Watching known config files for changes\u2026
|
|
9398
9845
|
`));
|
|
@@ -9400,16 +9847,16 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9400
9847
|
for (const k of KNOWN_CONFIGS) {
|
|
9401
9848
|
if (k.rulesDir) {
|
|
9402
9849
|
const absDir = expandPath2(k.rulesDir);
|
|
9403
|
-
if (!
|
|
9850
|
+
if (!existsSync10(absDir))
|
|
9404
9851
|
continue;
|
|
9405
9852
|
const { readdirSync: readdirSync3 } = await import("fs");
|
|
9406
9853
|
for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9407
|
-
const abs =
|
|
9854
|
+
const abs = join12(absDir, f);
|
|
9408
9855
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9409
9856
|
}
|
|
9410
9857
|
} else {
|
|
9411
9858
|
const abs = expandPath2(k.path);
|
|
9412
|
-
if (
|
|
9859
|
+
if (existsSync10(abs))
|
|
9413
9860
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9414
9861
|
}
|
|
9415
9862
|
}
|
|
@@ -9417,7 +9864,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9417
9864
|
const tick = async () => {
|
|
9418
9865
|
let changed = 0;
|
|
9419
9866
|
for (const [abs, oldMtime] of mtimes) {
|
|
9420
|
-
if (!
|
|
9867
|
+
if (!existsSync10(abs))
|
|
9421
9868
|
continue;
|
|
9422
9869
|
const newMtime = st(abs).mtimeMs;
|
|
9423
9870
|
if (newMtime !== oldMtime) {
|
|
@@ -9429,10 +9876,10 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9429
9876
|
for (const k of KNOWN_CONFIGS) {
|
|
9430
9877
|
if (k.rulesDir) {
|
|
9431
9878
|
const absDir = expandPath2(k.rulesDir);
|
|
9432
|
-
if (!
|
|
9879
|
+
if (!existsSync10(absDir))
|
|
9433
9880
|
continue;
|
|
9434
9881
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9435
|
-
const abs =
|
|
9882
|
+
const abs = join12(absDir, f);
|
|
9436
9883
|
if (!mtimes.has(abs)) {
|
|
9437
9884
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9438
9885
|
changed++;
|
|
@@ -9440,7 +9887,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9440
9887
|
}
|
|
9441
9888
|
} else {
|
|
9442
9889
|
const abs = expandPath2(k.path);
|
|
9443
|
-
if (
|
|
9890
|
+
if (existsSync10(abs) && !mtimes.has(abs)) {
|
|
9444
9891
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9445
9892
|
changed++;
|
|
9446
9893
|
}
|
|
@@ -9468,7 +9915,7 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
9468
9915
|
if (!c.target_path)
|
|
9469
9916
|
continue;
|
|
9470
9917
|
const abs = expandPath(c.target_path);
|
|
9471
|
-
if (!
|
|
9918
|
+
if (!existsSync10(abs)) {
|
|
9472
9919
|
missing++;
|
|
9473
9920
|
continue;
|
|
9474
9921
|
}
|
|
@@ -9539,7 +9986,7 @@ program.command("clean").description("Remove configs from DB whose target files
|
|
|
9539
9986
|
if (!c.target_path)
|
|
9540
9987
|
continue;
|
|
9541
9988
|
const abs = expandPath(c.target_path);
|
|
9542
|
-
if (!
|
|
9989
|
+
if (!existsSync10(abs)) {
|
|
9543
9990
|
if (printed < maxPrinted) {
|
|
9544
9991
|
if (opts.dryRun) {
|
|
9545
9992
|
console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
|
|
@@ -9563,7 +10010,7 @@ ${removed} orphaned config(s) ${opts.dryRun ? "found" : "removed"}${omitted > 0
|
|
|
9563
10010
|
console.log(chalk.dim(`Use --limit ${removed} to print every orphan row.`));
|
|
9564
10011
|
}
|
|
9565
10012
|
});
|
|
9566
|
-
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) => {
|
|
9567
10014
|
const store = resolveConfigStore();
|
|
9568
10015
|
const packages = [
|
|
9569
10016
|
{ name: "@hasna/todos", bin: "todos", mcp: "todos-mcp" },
|
|
@@ -9571,7 +10018,6 @@ program.command("bootstrap").description("Install the full @hasna ecosystem: CLI
|
|
|
9571
10018
|
{ name: "@hasna/conversations", bin: "conversations", mcp: "conversations-mcp" },
|
|
9572
10019
|
{ name: "@hasna/skills", bin: "skills", mcp: "skills-mcp" },
|
|
9573
10020
|
{ name: "@hasna/economy", bin: "economy", mcp: "economy-mcp" },
|
|
9574
|
-
{ name: "@hasna/attachments", bin: "attachments", mcp: "attachments-mcp" },
|
|
9575
10021
|
{ name: "@hasna/emails", bin: "emails", mcp: "emails-mcp" },
|
|
9576
10022
|
{ name: "@hasna/recordings", bin: "recordings", mcp: "recordings-mcp" },
|
|
9577
10023
|
{ name: "@hasna/assistants", bin: "assistants", mcp: "assistants-mcp" }
|
|
@@ -9623,20 +10069,8 @@ Initializing configs:`));
|
|
|
9623
10069
|
} else {
|
|
9624
10070
|
console.log(chalk.dim(" would run: configs init"));
|
|
9625
10071
|
}
|
|
9626
|
-
console.log(chalk.
|
|
9627
|
-
|
|
9628
|
-
const runtimeReport = await reconcileManagedSkillRuntimes({
|
|
9629
|
-
dryRun: opts.dryRun,
|
|
9630
|
-
agent: opts.from,
|
|
9631
|
-
deliveryVerified: opts.deliveryVerified
|
|
9632
|
-
});
|
|
9633
|
-
printManagedSkillRuntimeReport(runtimeReport);
|
|
9634
|
-
if (runtimeReport.failed > 0) {
|
|
9635
|
-
console.error(chalk.red(`
|
|
9636
|
-
Bootstrap incomplete: a managed skill runtime could not be reconciled.`));
|
|
9637
|
-
process.exitCode = 1;
|
|
9638
|
-
return;
|
|
9639
|
-
}
|
|
10072
|
+
console.log(chalk.dim(`
|
|
10073
|
+
Manage skill publication, selection and agent hooks through the Skills CLI.`));
|
|
9640
10074
|
console.log(chalk.bold(`
|
|
9641
10075
|
\u2713 Bootstrap complete.`) + chalk.dim(" Restart agent sessions to load updated integrations."));
|
|
9642
10076
|
});
|
|
@@ -9682,7 +10116,7 @@ providerContextCmd.command("resolve").description("Resolve the endpoint to a pro
|
|
|
9682
10116
|
try {
|
|
9683
10117
|
const rawEndpoint = opts.endpoint ?? process.env["ANTHROPIC_BASE_URL"] ?? "";
|
|
9684
10118
|
const rawModel = opts.model ?? process.env["ANTHROPIC_MODEL"] ?? "";
|
|
9685
|
-
const homeDir2 = opts.home ??
|
|
10119
|
+
const homeDir2 = opts.home ?? homedir5();
|
|
9686
10120
|
const origin = normalizeEndpointOrigin(rawEndpoint);
|
|
9687
10121
|
const resolution = resolveAndRenderProviderContext({
|
|
9688
10122
|
origin,
|