@hasna/instructions 0.7.1 → 0.7.2
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 +34 -7
- package/dist/chunks/{apply-c3sd4agz.js → apply-ps6jjt6k.js} +3 -3
- package/dist/chunks/{apply-pnn7a3y7.js → apply-wj7wh5km.js} +3 -3
- package/dist/chunks/{index-pdpg8a44.js → index-18j7x3xw.js} +2 -2
- package/dist/chunks/{index-3djthcm6.js → index-27az1qb1.js} +28 -1
- package/dist/chunks/{index-ypcef3a2.js → index-ape24zc0.js} +2 -2
- package/dist/chunks/{index-jg8ptcwh.js → index-ek15201v.js} +2 -2
- package/dist/chunks/{index-6jncnnw7.js → index-n6nh6f5r.js} +119 -17
- package/dist/chunks/{index-aj66f6xr.js → index-pq18fdbj.js} +29 -6
- package/dist/chunks/{index-n3fjspr0.js → index-preyvj8n.js} +28 -1
- package/dist/chunks/{index-r7nfef2g.js → index-y8410ve0.js} +3 -3
- package/dist/chunks/{local-0a9gja58.js → local-1c1qxzvx.js} +15 -4
- package/dist/chunks/{local-969ye538.js → local-hc67bynj.js} +15 -4
- package/dist/chunks/{sync-q5hvwq6e.js → sync-12m3vs6g.js} +4 -4
- package/dist/chunks/{sync-xtkbm5br.js → sync-azd9t4p8.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 +596 -133
- 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 +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +658 -98
- 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/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/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 +24 -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 +34 -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.js +19 -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 +20 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +122 -8
- package/dist/server/openapi.d.ts +123 -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 +16 -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-18j7x3xw.js";
|
|
13
13
|
import {
|
|
14
14
|
applyConfigsWithReport,
|
|
15
15
|
compactPathForConfigHome,
|
|
16
16
|
expandPath,
|
|
17
|
-
normalizeTargetPath
|
|
18
|
-
|
|
17
|
+
normalizeTargetPath,
|
|
18
|
+
sessionRenderOwnsPath
|
|
19
|
+
} from "../chunks/index-y8410ve0.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-n6nh6f5r.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,9 @@ var {
|
|
|
3784
3795
|
|
|
3785
3796
|
// src/cli/index.tsx
|
|
3786
3797
|
import chalk from "chalk";
|
|
3787
|
-
import { existsSync as existsSync11, lstatSync as
|
|
3798
|
+
import { existsSync as existsSync11, lstatSync as lstatSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readSync, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync5, writeSync } from "fs";
|
|
3788
3799
|
import { homedir as homedir4 } from "os";
|
|
3789
|
-
import { basename, dirname as
|
|
3800
|
+
import { basename as basename2, dirname as dirname7, join as join11, resolve as resolve7 } from "path";
|
|
3790
3801
|
|
|
3791
3802
|
// src/lib/config-target-identity.ts
|
|
3792
3803
|
function findConfigsByTargetPath(configs, targetPath) {
|
|
@@ -4530,14 +4541,20 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4530
4541
|
const targetHome = assertSafeTargetHome(plan.targetHome);
|
|
4531
4542
|
assertClaudeAuthorityStillClear(plan, targetHome, options.ownedClaudeAuthorities);
|
|
4532
4543
|
const payloadFiles = [...plan.files, ...plan.assetFiles ?? []];
|
|
4533
|
-
const files = [...payloadFiles, plan.manifestFile];
|
|
4534
4544
|
const manifestPath = resolvePlannedFilePath(plan, plan.manifestFile, targetHome);
|
|
4545
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4535
4546
|
const previousManifest = readPreviousManifest(manifestPath);
|
|
4536
4547
|
const previousHashes = previousManifest ? new Map(previousManifest.files.map((file) => [file.relativePath, file.sha256])) : new Map;
|
|
4548
|
+
const adoptions = validateFileAdoptions(plan, targetHome, previousHashes, options);
|
|
4549
|
+
const adoptedHashes = new Map(adoptions.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4550
|
+
const reconciliations = validateFileReconciliations(plan, targetHome, previousManifest, options);
|
|
4551
|
+
const reconciledHashes = new Map(reconciliations.map((entry) => [entry.relativePath, entry.preimageSha256]));
|
|
4552
|
+
const manifestFile = manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations);
|
|
4553
|
+
const files = [...payloadFiles, manifestFile];
|
|
4537
4554
|
const currentRelativePaths = new Set(files.map((file) => file.relativePath));
|
|
4538
4555
|
const drift = checkSessionRenderDrift(targetHome, manifestPath);
|
|
4539
4556
|
const results = [
|
|
4540
|
-
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options)),
|
|
4557
|
+
...files.map((file) => planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes)),
|
|
4541
4558
|
...planStaleFileResults(plan, targetHome, previousManifest, currentRelativePaths, options)
|
|
4542
4559
|
];
|
|
4543
4560
|
assertNotSilentManagedWipeout(plan, results);
|
|
@@ -4560,7 +4577,9 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4560
4577
|
skippedSources: plan.manifest.skippedSources,
|
|
4561
4578
|
files: results,
|
|
4562
4579
|
conflicts,
|
|
4563
|
-
drift
|
|
4580
|
+
drift,
|
|
4581
|
+
adoptions,
|
|
4582
|
+
reconciliations
|
|
4564
4583
|
};
|
|
4565
4584
|
}
|
|
4566
4585
|
let snapshotPath = null;
|
|
@@ -4573,10 +4592,12 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4573
4592
|
if (!options.dryRun) {
|
|
4574
4593
|
const allowPortableFallback = coordination === null;
|
|
4575
4594
|
const forcePortableFileOps = options.test_hooks?.force_portable_file_ops ?? false;
|
|
4595
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4576
4596
|
ensureSessionTargetHome(targetHome);
|
|
4577
|
-
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4597
|
+
rollback = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations);
|
|
4578
4598
|
snapshotPath = rollback.snapshotPath;
|
|
4579
4599
|
options.test_hooks?.before_apply_writes?.({ plan, results });
|
|
4600
|
+
assertManifestPrecondition(manifestPath, targetHome, options.expectedManifestSha256);
|
|
4580
4601
|
const resultsByPath = new Map(results.map((result) => [result.path, result]));
|
|
4581
4602
|
for (const file of payloadFiles) {
|
|
4582
4603
|
applyPlannedFile(plan, file, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
@@ -4596,7 +4617,7 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4596
4617
|
});
|
|
4597
4618
|
coordination?.assert_held();
|
|
4598
4619
|
}
|
|
4599
|
-
applyPlannedFile(plan,
|
|
4620
|
+
applyPlannedFile(plan, manifestFile, targetHome, resultsByPath, coordination, allowPortableFallback, forcePortableFileOps);
|
|
4600
4621
|
}
|
|
4601
4622
|
return {
|
|
4602
4623
|
dryRun: options.dryRun ?? false,
|
|
@@ -4610,9 +4631,171 @@ function applySessionRenderUnlocked(plan, options, coordination) {
|
|
|
4610
4631
|
skippedSources: plan.manifest.skippedSources,
|
|
4611
4632
|
files: results,
|
|
4612
4633
|
conflicts,
|
|
4613
|
-
drift
|
|
4634
|
+
drift,
|
|
4635
|
+
adoptions,
|
|
4636
|
+
reconciliations
|
|
4614
4637
|
};
|
|
4615
4638
|
}
|
|
4639
|
+
function assertManifestPrecondition(path, targetHome, expected) {
|
|
4640
|
+
if (expected === undefined)
|
|
4641
|
+
return;
|
|
4642
|
+
if (!/^[a-f0-9]{64}$/.test(expected)) {
|
|
4643
|
+
throw new SessionApplyError("Expected session manifest SHA-256 must contain 64 lowercase hexadecimal characters.");
|
|
4644
|
+
}
|
|
4645
|
+
if (currentSessionFileHash(path, targetHome) !== expected) {
|
|
4646
|
+
throw new SessionApplyError("Session manifest SHA-256 precondition failed; reload the manifest and resolve a new plan.");
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
function validateFileAdoptions(plan, targetHome, previousHashes, options) {
|
|
4650
|
+
const requests = options.adoptFiles ?? [];
|
|
4651
|
+
if (!Array.isArray(requests))
|
|
4652
|
+
throw new SessionApplyError("Session file adoptions must be an array.");
|
|
4653
|
+
if (requests.length > 0 && options.force) {
|
|
4654
|
+
throw new SessionApplyError("Exact file adoption cannot be combined with force.");
|
|
4655
|
+
}
|
|
4656
|
+
const seen = new Set;
|
|
4657
|
+
return requests.map((request) => {
|
|
4658
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4659
|
+
throw new SessionApplyError("Each file adoption requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4660
|
+
}
|
|
4661
|
+
if (seen.has(request.relativePath)) {
|
|
4662
|
+
throw new SessionApplyError(`Duplicate file adoption target: ${request.relativePath}`);
|
|
4663
|
+
}
|
|
4664
|
+
seen.add(request.relativePath);
|
|
4665
|
+
const candidates = plan.files.filter((file2) => file2.relativePath === request.relativePath);
|
|
4666
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4667
|
+
throw new SessionApplyError(`File adoption target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4668
|
+
}
|
|
4669
|
+
const file = candidates[0];
|
|
4670
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4671
|
+
if (previousHashes.has(file.relativePath) || sessionRenderOwnsPath(path)) {
|
|
4672
|
+
throw new SessionApplyError(`File adoption target is already managed: ${request.relativePath}`);
|
|
4673
|
+
}
|
|
4674
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "adoption");
|
|
4675
|
+
return {
|
|
4676
|
+
path,
|
|
4677
|
+
relativePath: file.relativePath,
|
|
4678
|
+
preimageSha256: observedSha256,
|
|
4679
|
+
renderedSha256: file.sha256,
|
|
4680
|
+
sourceIds: [...file.sourceIds]
|
|
4681
|
+
};
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
function readExactFilePreimage(path, targetHome, request, operation) {
|
|
4685
|
+
if (currentSessionFileHash(path, targetHome) === null) {
|
|
4686
|
+
throw new SessionApplyError(`File ${operation} target does not exist: ${request.relativePath}`);
|
|
4687
|
+
}
|
|
4688
|
+
const bytes = readFileSync(path);
|
|
4689
|
+
if (!bytes.equals(Buffer.from(bytes.toString("utf8"), "utf8"))) {
|
|
4690
|
+
throw new SessionApplyError(`File ${operation} target must contain losslessly restorable UTF-8: ${request.relativePath}`);
|
|
4691
|
+
}
|
|
4692
|
+
const observedSha256 = createHash3("sha256").update(bytes).digest("hex");
|
|
4693
|
+
if (observedSha256 !== request.sha256) {
|
|
4694
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${request.relativePath}`);
|
|
4695
|
+
}
|
|
4696
|
+
return observedSha256;
|
|
4697
|
+
}
|
|
4698
|
+
function validateFileReconciliations(plan, targetHome, previousManifest, options) {
|
|
4699
|
+
const requests = options.reconcileFiles ?? [];
|
|
4700
|
+
if (!Array.isArray(requests))
|
|
4701
|
+
throw new SessionApplyError("Session file reconciliations must be an array.");
|
|
4702
|
+
if (requests.length === 0)
|
|
4703
|
+
return [];
|
|
4704
|
+
if (options.force)
|
|
4705
|
+
throw new SessionApplyError("Exact file reconciliation cannot be combined with force.");
|
|
4706
|
+
if (options.expectedManifestSha256 === undefined) {
|
|
4707
|
+
throw new SessionApplyError("Exact file reconciliation requires an expected manifest SHA-256 precondition.");
|
|
4708
|
+
}
|
|
4709
|
+
if (!previousManifest || previousManifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || previousManifest.tool !== plan.tool || previousManifest.targetHome !== targetHome) {
|
|
4710
|
+
throw new SessionApplyError("File reconciliation requires this renderer's manifest for the same tool and target home.");
|
|
4711
|
+
}
|
|
4712
|
+
const seen = new Set;
|
|
4713
|
+
return requests.map((request) => {
|
|
4714
|
+
if (!request || typeof request.relativePath !== "string" || typeof request.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(request.sha256)) {
|
|
4715
|
+
throw new SessionApplyError("Each file reconciliation requires a plan-relative path and a 64-character lowercase SHA-256.");
|
|
4716
|
+
}
|
|
4717
|
+
if (seen.has(request.relativePath)) {
|
|
4718
|
+
throw new SessionApplyError(`Duplicate file reconciliation target: ${request.relativePath}`);
|
|
4719
|
+
}
|
|
4720
|
+
seen.add(request.relativePath);
|
|
4721
|
+
const candidates = plan.files.filter((file2) => file2.relativePath === request.relativePath);
|
|
4722
|
+
if (candidates.length !== 1 || candidates[0].role === "manifest") {
|
|
4723
|
+
throw new SessionApplyError(`File reconciliation target is not a unique planned instruction output: ${request.relativePath}`);
|
|
4724
|
+
}
|
|
4725
|
+
const file = candidates[0];
|
|
4726
|
+
const path = resolvePlannedFilePath(plan, file, targetHome);
|
|
4727
|
+
const owned = previousManifest.files.filter((entry) => entry.relativePath === file.relativePath);
|
|
4728
|
+
if (owned.length !== 1 || owned[0].path !== path || owned[0].role !== file.role || !/^[a-f0-9]{64}$/.test(owned[0].sha256)) {
|
|
4729
|
+
throw new SessionApplyError(`File reconciliation target is not owned by the observed manifest: ${request.relativePath}`);
|
|
4730
|
+
}
|
|
4731
|
+
const observedSha256 = readExactFilePreimage(path, targetHome, request, "reconciliation");
|
|
4732
|
+
if (observedSha256 === owned[0].sha256) {
|
|
4733
|
+
throw new SessionApplyError(`File reconciliation target has no managed drift: ${request.relativePath}`);
|
|
4734
|
+
}
|
|
4735
|
+
if (observedSha256 === file.sha256) {
|
|
4736
|
+
throw new SessionApplyError(`File reconciliation target already matches the planned output: ${request.relativePath}`);
|
|
4737
|
+
}
|
|
4738
|
+
return {
|
|
4739
|
+
path,
|
|
4740
|
+
relativePath: file.relativePath,
|
|
4741
|
+
preimageSha256: observedSha256,
|
|
4742
|
+
renderedSha256: file.sha256,
|
|
4743
|
+
previousManagedSha256: owned[0].sha256,
|
|
4744
|
+
sourceIds: [...file.sourceIds]
|
|
4745
|
+
};
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
function manifestWithFileProvenance(plan, previousManifest, adoptions, reconciliations) {
|
|
4749
|
+
if (adoptions.length === 0 && previousManifest?.adoptions === undefined && reconciliations.length === 0 && previousManifest?.reconciliations === undefined)
|
|
4750
|
+
return plan.manifestFile;
|
|
4751
|
+
if (previousManifest?.adoptions !== undefined && !Array.isArray(previousManifest.adoptions)) {
|
|
4752
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4753
|
+
}
|
|
4754
|
+
const entries = new Map;
|
|
4755
|
+
for (const entry of [
|
|
4756
|
+
...previousManifest?.adoptions ?? [],
|
|
4757
|
+
...adoptions.map(({ path: _path, ...receipt }) => receipt)
|
|
4758
|
+
]) {
|
|
4759
|
+
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")) {
|
|
4760
|
+
throw new SessionApplyError("Previous session manifest adoption provenance is invalid.");
|
|
4761
|
+
}
|
|
4762
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4763
|
+
const previous = entries.get(entry.relativePath);
|
|
4764
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(entry)) {
|
|
4765
|
+
throw new SessionApplyError(`Conflicting session file adoption provenance: ${entry.relativePath}`);
|
|
4766
|
+
}
|
|
4767
|
+
entries.set(entry.relativePath, entry);
|
|
4768
|
+
}
|
|
4769
|
+
if (previousManifest?.reconciliations !== undefined && !Array.isArray(previousManifest.reconciliations)) {
|
|
4770
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4771
|
+
}
|
|
4772
|
+
const reconciledEntries = new Map;
|
|
4773
|
+
for (const entry of [
|
|
4774
|
+
...previousManifest?.reconciliations ?? [],
|
|
4775
|
+
...reconciliations.map(({ path: _path, ...receipt }) => receipt)
|
|
4776
|
+
]) {
|
|
4777
|
+
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")) {
|
|
4778
|
+
throw new SessionApplyError("Previous session manifest reconciliation provenance is invalid.");
|
|
4779
|
+
}
|
|
4780
|
+
resolveManifestRelativePath(entry.relativePath, plan.targetHome);
|
|
4781
|
+
const canonical = {
|
|
4782
|
+
relativePath: entry.relativePath,
|
|
4783
|
+
preimageSha256: entry.preimageSha256,
|
|
4784
|
+
renderedSha256: entry.renderedSha256,
|
|
4785
|
+
previousManagedSha256: entry.previousManagedSha256,
|
|
4786
|
+
sourceIds: [...entry.sourceIds]
|
|
4787
|
+
};
|
|
4788
|
+
reconciledEntries.set(JSON.stringify(canonical), canonical);
|
|
4789
|
+
}
|
|
4790
|
+
const manifest = {
|
|
4791
|
+
...plan.manifest,
|
|
4792
|
+
...adoptions.length > 0 || previousManifest?.adoptions !== undefined ? { adoptions: [...entries.values()] } : {},
|
|
4793
|
+
...reconciliations.length > 0 || previousManifest?.reconciliations !== undefined ? { reconciliations: [...reconciledEntries.values()] } : {}
|
|
4794
|
+
};
|
|
4795
|
+
const content = `${JSON.stringify(manifest, null, 2)}
|
|
4796
|
+
`;
|
|
4797
|
+
return { ...plan.manifestFile, content, sha256: sha2563(content) };
|
|
4798
|
+
}
|
|
4616
4799
|
function assertCursorAuthorityUnchanged(plan) {
|
|
4617
4800
|
if (plan.tool !== "cursor" || plan.targetKind === "blocked")
|
|
4618
4801
|
return;
|
|
@@ -5084,12 +5267,29 @@ function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
|
5084
5267
|
}
|
|
5085
5268
|
return path;
|
|
5086
5269
|
}
|
|
5087
|
-
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
5270
|
+
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options, adoptedHashes, reconciledHashes) {
|
|
5088
5271
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
5089
5272
|
const previousContent = existsSync5(target) ? readFileSync(target, "utf-8") : null;
|
|
5090
5273
|
const previousSha256 = previousContent === null ? null : sha2563(previousContent);
|
|
5091
5274
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
5092
5275
|
const changed = previousContent !== file.content;
|
|
5276
|
+
const exactSha256 = adoptedHashes.get(file.relativePath) ?? reconciledHashes.get(file.relativePath);
|
|
5277
|
+
if (exactSha256 !== undefined) {
|
|
5278
|
+
const operation = adoptedHashes.has(file.relativePath) ? "adoption" : "reconciliation";
|
|
5279
|
+
if (previousSha256 !== exactSha256) {
|
|
5280
|
+
throw new SessionApplyError(`File ${operation} SHA-256 precondition failed: ${file.relativePath}`);
|
|
5281
|
+
}
|
|
5282
|
+
return {
|
|
5283
|
+
path: target,
|
|
5284
|
+
relativePath: file.relativePath,
|
|
5285
|
+
role: file.role,
|
|
5286
|
+
action: "update",
|
|
5287
|
+
changed,
|
|
5288
|
+
previousSha256,
|
|
5289
|
+
newSha256: file.sha256,
|
|
5290
|
+
reason: `exact observed SHA-256 ${operation}`
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5093
5293
|
if (previousContent !== null && !options.force && !previouslyManaged) {
|
|
5094
5294
|
return {
|
|
5095
5295
|
path: target,
|
|
@@ -5309,9 +5509,13 @@ function requiredPreviousHash(result) {
|
|
|
5309
5509
|
}
|
|
5310
5510
|
return result.previousSha256;
|
|
5311
5511
|
}
|
|
5312
|
-
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
|
|
5313
|
-
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").
|
|
5512
|
+
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps, adoptions, reconciliations) {
|
|
5513
|
+
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").map((result) => {
|
|
5514
|
+
assertExpectedSessionFileHash(result.path, targetHome, result.previousSha256);
|
|
5314
5515
|
const content = readFileSync(result.path, "utf-8");
|
|
5516
|
+
if (sha2563(content) !== result.previousSha256) {
|
|
5517
|
+
throw new SessionApplyError(`Session snapshot preimage changed after planning: ${result.relativePath}`);
|
|
5518
|
+
}
|
|
5315
5519
|
return {
|
|
5316
5520
|
path: result.path,
|
|
5317
5521
|
relativePath: result.relativePath,
|
|
@@ -5352,13 +5556,19 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
5352
5556
|
manifestPath,
|
|
5353
5557
|
previousManifest,
|
|
5354
5558
|
files: existingFiles,
|
|
5355
|
-
afterFiles
|
|
5559
|
+
afterFiles,
|
|
5560
|
+
...adoptions.length > 0 ? { adoptions } : {},
|
|
5561
|
+
...reconciliations.length > 0 ? { reconciliations } : {}
|
|
5356
5562
|
};
|
|
5563
|
+
const snapshotContent = `${JSON.stringify(snapshot, null, 2)}
|
|
5564
|
+
`;
|
|
5565
|
+
if ((adoptions.length > 0 || reconciliations.length > 0) && Buffer.byteLength(snapshotContent, "utf8") > 32 * 1024 * 1024) {
|
|
5566
|
+
throw new SessionApplyError("Exact file preimage snapshot exceeds the 32 MiB restore limit.");
|
|
5567
|
+
}
|
|
5357
5568
|
coordination?.assert_held();
|
|
5358
5569
|
writeProjectContextCoordinatedFile({
|
|
5359
5570
|
path: snapshotPath,
|
|
5360
|
-
content:
|
|
5361
|
-
`,
|
|
5571
|
+
content: snapshotContent,
|
|
5362
5572
|
workspace_root: sessionRenderSnapshotWorkspaceRoot(targetHome),
|
|
5363
5573
|
default_mode: 384,
|
|
5364
5574
|
expected_hash: null,
|
|
@@ -5416,11 +5626,15 @@ function sha2563(content) {
|
|
|
5416
5626
|
return createHash3("sha256").update(content).digest("hex");
|
|
5417
5627
|
}
|
|
5418
5628
|
|
|
5629
|
+
// src/lib/session-refresh.ts
|
|
5630
|
+
import { createHash as createHash4 } from "crypto";
|
|
5631
|
+
import { closeSync, constants, fstatSync, lstatSync as lstatSync3, openSync, readFileSync as readFileSync3 } from "fs";
|
|
5632
|
+
import { dirname as dirname4, join as join7, parse as parse2, resolve as resolve5 } from "path";
|
|
5633
|
+
|
|
5419
5634
|
// src/lib/global-source-coverage.ts
|
|
5420
5635
|
var GLOBAL_SOURCE_SLUG_PREFIX = "global-";
|
|
5421
|
-
var RETIRED_GLOBAL_SOURCE_TAG = "retired-global-source";
|
|
5422
5636
|
function expectedGlobalSourceSlugs(registryConfigs) {
|
|
5423
|
-
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) =>
|
|
5637
|
+
return registryConfigs.filter((c) => c.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).filter((c) => instructionSourceRejection(c) === null).map((c) => c.slug).sort();
|
|
5424
5638
|
}
|
|
5425
5639
|
function accountedGlobalSourceSlugs(manifest) {
|
|
5426
5640
|
return [...manifest.sources, ...manifest.skippedSources].map((source) => source.id).filter((id) => id.startsWith(GLOBAL_SOURCE_SLUG_PREFIX));
|
|
@@ -5433,6 +5647,10 @@ function computeGlobalSourceCoverage(registryConfigs, configuredSlugs) {
|
|
|
5433
5647
|
const unexpected = [...configuredSet].filter((slug) => slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX) && !expectedSet.has(slug)).sort();
|
|
5434
5648
|
return {
|
|
5435
5649
|
expectedSlugs: expected,
|
|
5650
|
+
excludedSources: registryConfigs.filter((config) => config.slug.startsWith(GLOBAL_SOURCE_SLUG_PREFIX)).flatMap((config) => {
|
|
5651
|
+
const reason = instructionSourceRejection(config);
|
|
5652
|
+
return reason ? [{ slug: config.slug, reason }] : [];
|
|
5653
|
+
}).sort((left, right) => left.slug.localeCompare(right.slug)),
|
|
5436
5654
|
configuredSlugs: [...configuredSet].sort(),
|
|
5437
5655
|
missingSlugs: missing,
|
|
5438
5656
|
unexpectedSlugs: unexpected,
|
|
@@ -5688,6 +5906,176 @@ function stationProfileSource(env = process.env) {
|
|
|
5688
5906
|
};
|
|
5689
5907
|
}
|
|
5690
5908
|
|
|
5909
|
+
// src/lib/session-refresh.ts
|
|
5910
|
+
import { basename } from "path";
|
|
5911
|
+
var nonempty = exports_external.string().min(1).max(4096);
|
|
5912
|
+
var selectorSchema = exports_external.object({
|
|
5913
|
+
schema: exports_external.literal("hasna.instructions.hosted-profile-selector/v1"),
|
|
5914
|
+
authority: nonempty,
|
|
5915
|
+
profileId: nonempty,
|
|
5916
|
+
providerVersion: nonempty,
|
|
5917
|
+
providerVariant: nonempty.optional(),
|
|
5918
|
+
model: nonempty.optional(),
|
|
5919
|
+
path: nonempty.optional(),
|
|
5920
|
+
manual: exports_external.array(nonempty).max(256),
|
|
5921
|
+
codewithNativeImports: exports_external.boolean(),
|
|
5922
|
+
allowEmptySources: exports_external.boolean(),
|
|
5923
|
+
stationProfile: exports_external.boolean(),
|
|
5924
|
+
checkGlobalCoverage: exports_external.boolean(),
|
|
5925
|
+
assetSurface: nonempty.optional(),
|
|
5926
|
+
assetScope: exports_external.enum(["global", "project", "session"]).optional()
|
|
5927
|
+
}).strict();
|
|
5928
|
+
function authority(value) {
|
|
5929
|
+
const url = new URL(value);
|
|
5930
|
+
if (url.username || url.password || url.search || url.hash || !["https:", "http:"].includes(url.protocol)) {
|
|
5931
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_INVALID: expected a credential-free Instructions API authority.");
|
|
5932
|
+
}
|
|
5933
|
+
return url.toString().replace(/\/$/, "");
|
|
5934
|
+
}
|
|
5935
|
+
function normalizeSessionHostedProfileSelector(value) {
|
|
5936
|
+
const parsed = selectorSchema.parse(value);
|
|
5937
|
+
return { ...parsed, authority: authority(parsed.authority) };
|
|
5938
|
+
}
|
|
5939
|
+
function assertHostedProfileBindings(profileId, configs, bindings) {
|
|
5940
|
+
const configIds = new Set(configs.map((config) => config.id));
|
|
5941
|
+
const bindingIds = new Set(bindings.map((binding) => binding.config_id));
|
|
5942
|
+
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))) {
|
|
5943
|
+
throw new Error("HOSTED_PROFILE_BINDINGS_INCOMPLETE: exact one-to-one config membership and explicit profile bindings are required; refusing legacy activation fallback.");
|
|
5944
|
+
}
|
|
5945
|
+
for (const row of bindings) {
|
|
5946
|
+
const raw = row.binding;
|
|
5947
|
+
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.");
|
|
5948
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
5949
|
+
throw invalid();
|
|
5950
|
+
const record = raw;
|
|
5951
|
+
if (!["schema", "activation", "required", "fallback"].every((key) => Object.hasOwn(record, key)) || typeof record["required"] !== "boolean")
|
|
5952
|
+
throw invalid();
|
|
5953
|
+
try {
|
|
5954
|
+
normalizeProfileConfigBinding(record);
|
|
5955
|
+
} catch {
|
|
5956
|
+
throw invalid();
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
function readManagedManifest(targetHome) {
|
|
5961
|
+
const path = join7(targetHome, SESSION_RENDER_MANIFEST_RELATIVE_PATH);
|
|
5962
|
+
let ancestor = dirname4(path);
|
|
5963
|
+
while (true) {
|
|
5964
|
+
const stat = lstatSync3(ancestor);
|
|
5965
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
5966
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: managed target uses a symlink or non-directory ancestor.");
|
|
5967
|
+
const parent = dirname4(ancestor);
|
|
5968
|
+
if (parent === ancestor)
|
|
5969
|
+
break;
|
|
5970
|
+
ancestor = parent;
|
|
5971
|
+
}
|
|
5972
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
5973
|
+
let raw;
|
|
5974
|
+
try {
|
|
5975
|
+
const stat = fstatSync(fd);
|
|
5976
|
+
if (!stat.isFile() || stat.size > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
5977
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: expected a bounded regular manifest file.");
|
|
5978
|
+
raw = readFileSync3(fd);
|
|
5979
|
+
if (raw.byteLength > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
5980
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: manifest grew beyond the read limit.");
|
|
5981
|
+
} finally {
|
|
5982
|
+
closeSync(fd);
|
|
5983
|
+
}
|
|
5984
|
+
const manifest = JSON.parse(raw.toString("utf8"));
|
|
5985
|
+
if (manifest.schema !== SESSION_RENDER_SCHEMA || !SESSION_RENDER_TOOLS.includes(manifest.tool) || manifest.targetHome !== targetHome || !manifest.profile?.trim() || !["session-home", "project-root"].includes(manifest.targetKind) || !Array.isArray(manifest.files) || !manifest.refreshSelector || manifest.targetOwner?.writer?.id !== SESSION_RENDERER_OWNER_ID || manifest.targetOwner?.targetHome !== targetHome) {
|
|
5986
|
+
throw new Error("SESSION_REFRESH_MANIFEST_INVALID: target is not an owned hosted profile render; apply a hosted --compile-profile first.");
|
|
5987
|
+
}
|
|
5988
|
+
manifest.refreshSelector = normalizeSessionHostedProfileSelector(manifest.refreshSelector);
|
|
5989
|
+
return { manifest, sha256: createHash4("sha256").update(raw).digest("hex") };
|
|
5990
|
+
}
|
|
5991
|
+
async function refreshSessionRender(input) {
|
|
5992
|
+
const { store } = input;
|
|
5993
|
+
if (store.mode !== "api" || !store.v1BaseUrl)
|
|
5994
|
+
throw new Error("SESSION_REFRESH_HOSTED_REQUIRED: refresh requires the configured hosted Instructions API.");
|
|
5995
|
+
const targetHome = resolve5(input.targetHome);
|
|
5996
|
+
if (targetHome === parse2(targetHome).root)
|
|
5997
|
+
throw new Error("SESSION_REFRESH_PATH_INVALID: filesystem root is not a managed target.");
|
|
5998
|
+
const previous = readManagedManifest(targetHome);
|
|
5999
|
+
const selector = previous.manifest.refreshSelector;
|
|
6000
|
+
if (authority(store.v1BaseUrl) !== selector.authority)
|
|
6001
|
+
throw new Error("SESSION_REFRESH_AUTHORITY_MISMATCH: configured API differs from the recorded profile authority.");
|
|
6002
|
+
const profile = await store.getProfile(selector.profileId);
|
|
6003
|
+
if (profile.id !== selector.profileId)
|
|
6004
|
+
throw new Error("SESSION_REFRESH_PROFILE_MISMATCH: hosted profile identity changed.");
|
|
6005
|
+
const [configs, bindings, assetBindings] = await Promise.all([
|
|
6006
|
+
store.getProfileConfigs(profile.id),
|
|
6007
|
+
store.getProfileConfigBindings(profile.id, { requireExplicit: true }),
|
|
6008
|
+
store.getProfileAssetBindings(profile.id, { requireExplicit: true })
|
|
6009
|
+
]);
|
|
6010
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
6011
|
+
if (configs.length > 2048 || bindings.length > 8192 || assetBindings.length > 1024)
|
|
6012
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted profile exceeds refresh bounds.");
|
|
6013
|
+
const totalBytes = configs.reduce((sum, config) => sum + Buffer.byteLength(config.content), 0);
|
|
6014
|
+
if (totalBytes > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6015
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted instruction payload exceeds refresh byte bounds.");
|
|
6016
|
+
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
6017
|
+
if (assetConfigs.reduce((sum, config) => sum + Buffer.byteLength(config.content), totalBytes) > SESSION_MANAGED_INPUT_MAX_BYTES)
|
|
6018
|
+
throw new Error("SESSION_REFRESH_INPUT_LIMIT: hosted source and asset payload exceeds refresh byte bounds.");
|
|
6019
|
+
let ownedClaudeAuthorities;
|
|
6020
|
+
if (previous.manifest.tool === "claude") {
|
|
6021
|
+
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 }));
|
|
6022
|
+
}
|
|
6023
|
+
const station = selector.stationProfile ? stationProfileSource() : null;
|
|
6024
|
+
const plan = planProfileSessionRender({
|
|
6025
|
+
tool: previous.manifest.tool,
|
|
6026
|
+
profile: previous.manifest.profile,
|
|
6027
|
+
profile_id: profile.id,
|
|
6028
|
+
provider_version: selector.providerVersion,
|
|
6029
|
+
targetHome,
|
|
6030
|
+
...previous.manifest.targetKind === "project-root" ? { projectRoot: targetHome } : {},
|
|
6031
|
+
sessionId: previous.manifest.sessionId ?? undefined,
|
|
6032
|
+
refreshSelector: selector,
|
|
6033
|
+
codewithNativeImports: selector.codewithNativeImports,
|
|
6034
|
+
allowEmptySources: selector.allowEmptySources,
|
|
6035
|
+
configs,
|
|
6036
|
+
bindings,
|
|
6037
|
+
asset_configs: assetConfigs,
|
|
6038
|
+
asset_bindings: assetBindings,
|
|
6039
|
+
asset_plan_mode: input.dryRun ? "dry-run" : "apply",
|
|
6040
|
+
asset_scope: selector.assetScope,
|
|
6041
|
+
asset_surface: selector.assetSurface,
|
|
6042
|
+
extra_sources: station ? [station] : undefined,
|
|
6043
|
+
ownedClaudeAuthorities,
|
|
6044
|
+
graph_context: {
|
|
6045
|
+
...selector.providerVariant ? { provider_variant: selector.providerVariant } : {},
|
|
6046
|
+
...selector.model ? { model: selector.model } : {},
|
|
6047
|
+
...selector.path ? { path: selector.path } : {},
|
|
6048
|
+
...selector.manual.length ? { manual: selector.manual } : {}
|
|
6049
|
+
}
|
|
6050
|
+
});
|
|
6051
|
+
if (selector.checkGlobalCoverage) {
|
|
6052
|
+
const coverage = computeGlobalSourceCoverage(await store.listConfigs({}), accountedGlobalSourceSlugs(plan.manifest));
|
|
6053
|
+
if (!coverage.complete)
|
|
6054
|
+
throw new Error(`SESSION_REFRESH_COVERAGE_INCOMPLETE: ${coverage.missingSlugs.length} registered global sources are absent.`);
|
|
6055
|
+
}
|
|
6056
|
+
const preview = applySessionRender(plan, { dryRun: true, expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6057
|
+
const common = { checkedAt: new Date().toISOString(), targetHome, selector, sourceHash: plan.manifest.sourceHash };
|
|
6058
|
+
if (preview.conflicts.length)
|
|
6059
|
+
return { ...common, status: "blocked", apply: preview };
|
|
6060
|
+
if (!preview.drift.clean)
|
|
6061
|
+
throw new Error("SESSION_REFRESH_DRIFT: generated files changed or disappeared since their last managed render.");
|
|
6062
|
+
const sameSources = previous.manifest.sourceHash === plan.manifest.sourceHash;
|
|
6063
|
+
const samePayloads = preview.files.filter((file) => file.role !== "manifest").every((file) => file.action === "unchanged");
|
|
6064
|
+
if (sameSources && samePayloads) {
|
|
6065
|
+
if (readManagedManifest(targetHome).sha256 !== previous.sha256 || !checkSessionRenderDrift(targetHome).clean)
|
|
6066
|
+
throw new Error("SESSION_REFRESH_DRIFT: managed target changed during refresh.");
|
|
6067
|
+
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) } };
|
|
6068
|
+
}
|
|
6069
|
+
if (input.dryRun)
|
|
6070
|
+
return { ...common, status: "dry-run", apply: preview };
|
|
6071
|
+
const applied = applySessionRender(plan, { expectedManifestSha256: previous.sha256, ownedClaudeAuthorities });
|
|
6072
|
+
if (applied.conflicts.length)
|
|
6073
|
+
return { ...common, status: "blocked", apply: applied };
|
|
6074
|
+
if (!applied.applied || !checkSessionRenderDrift(targetHome).clean)
|
|
6075
|
+
throw new Error("SESSION_REFRESH_READBACK_FAILED: applied instructions did not verify against their manifest.");
|
|
6076
|
+
return { ...common, status: "updated", apply: applied };
|
|
6077
|
+
}
|
|
6078
|
+
|
|
5691
6079
|
// src/lib/project-dashboard-standard.ts
|
|
5692
6080
|
var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
|
|
5693
6081
|
var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
@@ -5780,11 +6168,15 @@ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()
|
|
|
5780
6168
|
};
|
|
5781
6169
|
try {
|
|
5782
6170
|
const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
|
|
6171
|
+
if (isRetiredInstructionSource(existing))
|
|
6172
|
+
return existing;
|
|
5783
6173
|
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
6174
|
return await store.updateConfig(existing.id, input);
|
|
5785
6175
|
}
|
|
5786
6176
|
return existing;
|
|
5787
|
-
} catch {
|
|
6177
|
+
} catch (error) {
|
|
6178
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6179
|
+
throw error;
|
|
5788
6180
|
return await store.createConfig(input);
|
|
5789
6181
|
}
|
|
5790
6182
|
}
|
|
@@ -5797,10 +6189,9 @@ function profileHasSelectors(profile) {
|
|
|
5797
6189
|
var PLATFORM_PROFILE_PRESETS = [
|
|
5798
6190
|
{
|
|
5799
6191
|
name: "linux-arm64",
|
|
5800
|
-
description: "Default
|
|
5801
|
-
selectors: { os: ["linux"], arch: ["arm64"]
|
|
6192
|
+
description: "Default reviewed instruction profile for Linux arm64",
|
|
6193
|
+
selectors: { os: ["linux"], arch: ["arm64"] },
|
|
5802
6194
|
variables: {
|
|
5803
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/workspace",
|
|
5804
6195
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5805
6196
|
BUN_PATH: "{{BUN_BIN_DIR}}/bun",
|
|
5806
6197
|
PATH_PREFIX: "{{BUN_BIN_DIR}}",
|
|
@@ -5809,10 +6200,9 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5809
6200
|
},
|
|
5810
6201
|
{
|
|
5811
6202
|
name: "macos-arm64",
|
|
5812
|
-
description: "Default
|
|
5813
|
-
selectors: { os: ["macos"], arch: ["arm64"]
|
|
6203
|
+
description: "Default reviewed instruction profile for macOS arm64",
|
|
6204
|
+
selectors: { os: ["macos"], arch: ["arm64"] },
|
|
5814
6205
|
variables: {
|
|
5815
|
-
WORKSPACE_ROOT: "{{HOME_DIR}}/Workspace",
|
|
5816
6206
|
BUN_BIN_DIR: "{{HOME_DIR}}/.bun/bin",
|
|
5817
6207
|
BUN_PATH: "/opt/homebrew/bin/bun",
|
|
5818
6208
|
PATH_PREFIX: "/opt/homebrew/bin:{{BUN_BIN_DIR}}",
|
|
@@ -5821,12 +6211,21 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
5821
6211
|
}
|
|
5822
6212
|
];
|
|
5823
6213
|
async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
5824
|
-
const configs = await store.listConfigs();
|
|
5825
6214
|
const ensured = [];
|
|
5826
|
-
|
|
5827
|
-
let profile;
|
|
6215
|
+
const existingProfiles = await Promise.all(PLATFORM_PROFILE_PRESETS.map(async (preset) => {
|
|
5828
6216
|
try {
|
|
5829
|
-
|
|
6217
|
+
return await store.getProfile(preset.name);
|
|
6218
|
+
} catch (error) {
|
|
6219
|
+
if (error instanceof ProfileNotFoundError)
|
|
6220
|
+
return null;
|
|
6221
|
+
throw error;
|
|
6222
|
+
}
|
|
6223
|
+
}));
|
|
6224
|
+
for (const [index, preset] of PLATFORM_PROFILE_PRESETS.entries()) {
|
|
6225
|
+
let profile;
|
|
6226
|
+
const existing = existingProfiles[index];
|
|
6227
|
+
if (existing) {
|
|
6228
|
+
profile = existing;
|
|
5830
6229
|
const selectors = mergeProfileSelectors(preset.selectors, profile.selectors);
|
|
5831
6230
|
const variables = mergeProfileVariables(preset.variables, profile.variables);
|
|
5832
6231
|
if (JSON.stringify(selectors) !== JSON.stringify(profile.selectors) || JSON.stringify(variables) !== JSON.stringify(profile.variables)) {
|
|
@@ -5836,12 +6235,9 @@ async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
|
5836
6235
|
variables
|
|
5837
6236
|
});
|
|
5838
6237
|
}
|
|
5839
|
-
}
|
|
6238
|
+
} else {
|
|
5840
6239
|
profile = await store.createProfile(preset);
|
|
5841
6240
|
}
|
|
5842
|
-
for (const config of configs) {
|
|
5843
|
-
await store.addConfigToProfile(profile.id, config.id);
|
|
5844
|
-
}
|
|
5845
6241
|
ensured.push(profile);
|
|
5846
6242
|
}
|
|
5847
6243
|
return ensured;
|
|
@@ -5982,29 +6378,33 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
|
|
|
5982
6378
|
};
|
|
5983
6379
|
try {
|
|
5984
6380
|
const existing = await store.getConfig(DANGEROUS_OPERATION_GUARD_STANDARD_SLUG);
|
|
6381
|
+
if (isRetiredInstructionSource(existing))
|
|
6382
|
+
return existing;
|
|
5985
6383
|
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
6384
|
return await store.updateConfig(existing.id, input);
|
|
5987
6385
|
}
|
|
5988
6386
|
return existing;
|
|
5989
|
-
} catch {
|
|
6387
|
+
} catch (error) {
|
|
6388
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
6389
|
+
throw error;
|
|
5990
6390
|
return await store.createConfig(input);
|
|
5991
6391
|
}
|
|
5992
6392
|
}
|
|
5993
6393
|
|
|
5994
6394
|
// src/lib/managed-skill-runtimes.ts
|
|
5995
|
-
import { createHash as
|
|
6395
|
+
import { createHash as createHash5 } from "crypto";
|
|
5996
6396
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5997
6397
|
import {
|
|
5998
6398
|
existsSync as existsSync7,
|
|
5999
|
-
lstatSync as
|
|
6399
|
+
lstatSync as lstatSync4,
|
|
6000
6400
|
mkdirSync as mkdirSync4,
|
|
6001
|
-
readFileSync as
|
|
6401
|
+
readFileSync as readFileSync4,
|
|
6002
6402
|
renameSync,
|
|
6003
6403
|
rmSync as rmSync2,
|
|
6004
6404
|
writeFileSync as writeFileSync3
|
|
6005
6405
|
} from "fs";
|
|
6006
6406
|
import { homedir as homedir3 } from "os";
|
|
6007
|
-
import { dirname as
|
|
6407
|
+
import { dirname as dirname5, join as join8, parse as parse3, relative as relative2, resolve as resolve6 } from "path";
|
|
6008
6408
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
6009
6409
|
var INBOX_SKILL_MARKERS = [
|
|
6010
6410
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -6015,25 +6415,25 @@ var INBOX_SKILL_MARKERS = [
|
|
|
6015
6415
|
];
|
|
6016
6416
|
var REQUIRED_WATCH_FLAGS = ["--from <agent>", "--all", "--full-content"];
|
|
6017
6417
|
function sha2564(content) {
|
|
6018
|
-
return
|
|
6418
|
+
return createHash5("sha256").update(content).digest("hex");
|
|
6019
6419
|
}
|
|
6020
6420
|
function lstatOrNull(path) {
|
|
6021
6421
|
try {
|
|
6022
|
-
return
|
|
6422
|
+
return lstatSync4(path);
|
|
6023
6423
|
} catch {
|
|
6024
6424
|
return null;
|
|
6025
6425
|
}
|
|
6026
6426
|
}
|
|
6027
6427
|
function findSymlinkedAncestor(path) {
|
|
6028
|
-
const normalized =
|
|
6029
|
-
const parsed =
|
|
6428
|
+
const normalized = resolve6(path);
|
|
6429
|
+
const parsed = parse3(normalized);
|
|
6030
6430
|
let current = parsed.root;
|
|
6031
6431
|
const rel = relative2(parsed.root, normalized);
|
|
6032
6432
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
6033
|
-
current =
|
|
6433
|
+
current = join8(current, segment);
|
|
6034
6434
|
if (!existsSync7(current))
|
|
6035
6435
|
return null;
|
|
6036
|
-
if (
|
|
6436
|
+
if (lstatSync4(current).isSymbolicLink())
|
|
6037
6437
|
return current;
|
|
6038
6438
|
}
|
|
6039
6439
|
return null;
|
|
@@ -6055,7 +6455,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
6055
6455
|
if (!stat?.isFile()) {
|
|
6056
6456
|
throw new Error("packaged inbox skill contract is not a regular file");
|
|
6057
6457
|
}
|
|
6058
|
-
const content =
|
|
6458
|
+
const content = readFileSync4(assetPath, "utf8");
|
|
6059
6459
|
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
6060
6460
|
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
6061
6461
|
}
|
|
@@ -6093,7 +6493,7 @@ function compareVersions(left, right) {
|
|
|
6093
6493
|
return 0;
|
|
6094
6494
|
}
|
|
6095
6495
|
function inspectSkillMarkers(homeDir2) {
|
|
6096
|
-
return INBOX_SKILL_MARKERS.map((parts) =>
|
|
6496
|
+
return INBOX_SKILL_MARKERS.map((parts) => join8(homeDir2, ...parts)).map((path) => {
|
|
6097
6497
|
const stat = lstatOrNull(path);
|
|
6098
6498
|
if (!stat)
|
|
6099
6499
|
return null;
|
|
@@ -6102,7 +6502,7 @@ function inspectSkillMarkers(homeDir2) {
|
|
|
6102
6502
|
}
|
|
6103
6503
|
return {
|
|
6104
6504
|
path,
|
|
6105
|
-
content:
|
|
6505
|
+
content: readFileSync4(path, "utf8"),
|
|
6106
6506
|
mode: stat.mode & 511,
|
|
6107
6507
|
regular: true
|
|
6108
6508
|
};
|
|
@@ -6137,7 +6537,7 @@ function inspectInbox(options) {
|
|
|
6137
6537
|
let reason = "skill not installed";
|
|
6138
6538
|
if (skillPresent) {
|
|
6139
6539
|
const nonRegular = snapshots.some((snapshot) => !snapshot.regular);
|
|
6140
|
-
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
6540
|
+
const symlinkAncestor = snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6141
6541
|
if (nonRegular)
|
|
6142
6542
|
reason = "managed skill target is not a regular file";
|
|
6143
6543
|
else if (symlinkAncestor)
|
|
@@ -6224,10 +6624,10 @@ function cleanup(path) {
|
|
|
6224
6624
|
rmSync2(path, { force: true });
|
|
6225
6625
|
}
|
|
6226
6626
|
function writeAtomic(path, content, mode) {
|
|
6227
|
-
assertNoSymlinkAncestors2(
|
|
6627
|
+
assertNoSymlinkAncestors2(dirname5(path));
|
|
6228
6628
|
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
6229
6629
|
try {
|
|
6230
|
-
mkdirSync4(
|
|
6630
|
+
mkdirSync4(dirname5(path), { recursive: true, mode: 493 });
|
|
6231
6631
|
writeFileSync3(tempPath, content, { mode, flag: "wx" });
|
|
6232
6632
|
renameSync(tempPath, path);
|
|
6233
6633
|
} finally {
|
|
@@ -6236,7 +6636,7 @@ function writeAtomic(path, content, mode) {
|
|
|
6236
6636
|
}
|
|
6237
6637
|
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
6238
6638
|
lstat: lstatOrNull,
|
|
6239
|
-
read: (path) =>
|
|
6639
|
+
read: (path) => readFileSync4(path, "utf8"),
|
|
6240
6640
|
write: writeAtomic
|
|
6241
6641
|
};
|
|
6242
6642
|
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
@@ -6303,7 +6703,7 @@ async function reconcileManagedSkillRuntimes(options = {}) {
|
|
|
6303
6703
|
dry_run: dryRun
|
|
6304
6704
|
};
|
|
6305
6705
|
}
|
|
6306
|
-
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(
|
|
6706
|
+
const symlinkedAncestor = before.snapshots.map((snapshot) => snapshot.path).map((path) => findSymlinkedAncestor(dirname5(path))).find((found) => found !== null);
|
|
6307
6707
|
if (symlinkedAncestor) {
|
|
6308
6708
|
return {
|
|
6309
6709
|
runtimes: [{ ...status, action: "failed", dry_run: dryRun, skill_contracts_changed: 0 }],
|
|
@@ -6380,28 +6780,28 @@ async function reconcileManagedSkillRuntimes(options = {}) {
|
|
|
6380
6780
|
}
|
|
6381
6781
|
|
|
6382
6782
|
// src/status.ts
|
|
6383
|
-
import { existsSync as existsSync9, readFileSync as
|
|
6783
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
6384
6784
|
|
|
6385
6785
|
// src/lib/package-version.ts
|
|
6386
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6387
|
-
import { dirname as
|
|
6786
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
6787
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
6388
6788
|
import { fileURLToPath } from "url";
|
|
6389
6789
|
var cached = null;
|
|
6390
6790
|
function getPackageVersion() {
|
|
6391
6791
|
if (cached)
|
|
6392
6792
|
return cached;
|
|
6393
6793
|
try {
|
|
6394
|
-
let dir =
|
|
6794
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
6395
6795
|
for (let i = 0;i < 8; i++) {
|
|
6396
|
-
const pkgPath =
|
|
6796
|
+
const pkgPath = join9(dir, "package.json");
|
|
6397
6797
|
if (existsSync8(pkgPath)) {
|
|
6398
|
-
const pkg = JSON.parse(
|
|
6798
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
6399
6799
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
6400
6800
|
cached = pkg.version;
|
|
6401
6801
|
return cached;
|
|
6402
6802
|
}
|
|
6403
6803
|
}
|
|
6404
|
-
const parent =
|
|
6804
|
+
const parent = dirname6(dir);
|
|
6405
6805
|
if (parent === dir)
|
|
6406
6806
|
break;
|
|
6407
6807
|
dir = parent;
|
|
@@ -6474,7 +6874,7 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6474
6874
|
missingTargets += 1;
|
|
6475
6875
|
continue;
|
|
6476
6876
|
}
|
|
6477
|
-
const disk =
|
|
6877
|
+
const disk = readFileSync6(targetPath, "utf-8");
|
|
6478
6878
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
|
|
6479
6879
|
if (redactedDisk !== config.content) {
|
|
6480
6880
|
driftedTargets += 1;
|
|
@@ -6566,9 +6966,9 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
6566
6966
|
}
|
|
6567
6967
|
|
|
6568
6968
|
// src/lib/provider-context.ts
|
|
6569
|
-
import { createHash as
|
|
6570
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as
|
|
6571
|
-
import { join as
|
|
6969
|
+
import { createHash as createHash6 } from "crypto";
|
|
6970
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
6971
|
+
import { join as join10 } from "path";
|
|
6572
6972
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
6573
6973
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
6574
6974
|
var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
|
|
@@ -6704,7 +7104,7 @@ function renderProviderFragment(entry) {
|
|
|
6704
7104
|
return entry ? renderPerEndpointFragment(entry) : INVARIANT_FRAGMENT;
|
|
6705
7105
|
}
|
|
6706
7106
|
function sha2565(content) {
|
|
6707
|
-
return
|
|
7107
|
+
return createHash6("sha256").update(content).digest("hex");
|
|
6708
7108
|
}
|
|
6709
7109
|
function resolveAndRenderProviderContext(opts) {
|
|
6710
7110
|
const entry = matchProviderEndpoint(opts.origin);
|
|
@@ -6713,18 +7113,18 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
6713
7113
|
const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
|
|
6714
7114
|
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
7115
|
const content = renderProviderFragment(entry);
|
|
6716
|
-
const dir =
|
|
7116
|
+
const dir = join10(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
6717
7117
|
if (!existsSync10(dir))
|
|
6718
7118
|
mkdirSync5(dir, { recursive: true });
|
|
6719
7119
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
6720
|
-
const fragmentPath =
|
|
7120
|
+
const fragmentPath = join10(dir, filename);
|
|
6721
7121
|
const fragmentSha256 = sha2565(content);
|
|
6722
7122
|
writeFileSync4(fragmentPath, content, "utf8");
|
|
6723
|
-
const manifestPath =
|
|
7123
|
+
const manifestPath = join10(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
6724
7124
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
6725
7125
|
try {
|
|
6726
7126
|
if (existsSync10(manifestPath)) {
|
|
6727
|
-
const parsed = JSON.parse(
|
|
7127
|
+
const parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
6728
7128
|
if (parsed && typeof parsed === "object")
|
|
6729
7129
|
manifest = parsed;
|
|
6730
7130
|
}
|
|
@@ -6874,7 +7274,7 @@ function parseSessionSource(value, order) {
|
|
|
6874
7274
|
throw new Error(`Instruction source file not found: ${absPath}`);
|
|
6875
7275
|
const content = readSessionInstructionSourceFile(absPath);
|
|
6876
7276
|
const source = sourceFromFilePath(absPath, content, order);
|
|
6877
|
-
const resolvedId = id || source.id ||
|
|
7277
|
+
const resolvedId = id || source.id || basename2(absPath);
|
|
6878
7278
|
return {
|
|
6879
7279
|
...source,
|
|
6880
7280
|
id: resolvedId,
|
|
@@ -6914,7 +7314,7 @@ function sessionSourceReplacements(values) {
|
|
|
6914
7314
|
return replacements;
|
|
6915
7315
|
}
|
|
6916
7316
|
function readSessionInstructionSourceFile(path) {
|
|
6917
|
-
const stat =
|
|
7317
|
+
const stat = lstatSync5(path);
|
|
6918
7318
|
if (stat.isSymbolicLink()) {
|
|
6919
7319
|
throw new Error("SESSION_SOURCE_SYMLINK_REJECTED: instruction source file must be a regular non-symlink file");
|
|
6920
7320
|
}
|
|
@@ -6924,7 +7324,7 @@ function readSessionInstructionSourceFile(path) {
|
|
|
6924
7324
|
if (stat.size > SESSION_MANAGED_INPUT_MAX_BYTES) {
|
|
6925
7325
|
throw new Error(`SESSION_SOURCE_INPUT_TOO_LARGE: instruction source file exceeds ${SESSION_MANAGED_INPUT_MAX_BYTES} bytes`);
|
|
6926
7326
|
}
|
|
6927
|
-
return
|
|
7327
|
+
return readFileSync8(path, "utf-8");
|
|
6928
7328
|
}
|
|
6929
7329
|
function parseLayeredReference(value) {
|
|
6930
7330
|
const trimmed = value.trim();
|
|
@@ -6947,13 +7347,15 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6947
7347
|
const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index));
|
|
6948
7348
|
for (const value of opts.config ?? []) {
|
|
6949
7349
|
const { layer, id } = parseLayeredReference(value);
|
|
6950
|
-
|
|
7350
|
+
const config = await store.getConfig(id);
|
|
7351
|
+
assertInstructionSource(config);
|
|
7352
|
+
sources.push(sourceFromConfig(config, sources.length, layer));
|
|
6951
7353
|
}
|
|
6952
7354
|
for (const value of opts.identityExport ?? []) {
|
|
6953
7355
|
const path = resolveSessionPath(value);
|
|
6954
7356
|
if (!existsSync11(path))
|
|
6955
7357
|
throw new Error(`Identity instruction export not found: ${path}`);
|
|
6956
|
-
const parsed = JSON.parse(
|
|
7358
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
6957
7359
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
6958
7360
|
}
|
|
6959
7361
|
return sources.map((source) => {
|
|
@@ -6969,7 +7371,7 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
6969
7371
|
}
|
|
6970
7372
|
async function loadOwnedClaudeAuthorities(store) {
|
|
6971
7373
|
const configs = await store.listConfigs({ category: "rules", agent: "claude", kind: "file" });
|
|
6972
|
-
return configs.filter((config) => config.target_path &&
|
|
7374
|
+
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
7375
|
}
|
|
6974
7376
|
async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-run") {
|
|
6975
7377
|
const station = opts.stationProfile === false ? null : stationProfileSource();
|
|
@@ -6999,8 +7401,10 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
6999
7401
|
}
|
|
7000
7402
|
const profile = await store.getProfile(opts.compileProfile);
|
|
7001
7403
|
const configs = await store.getProfileConfigs(profile.id);
|
|
7002
|
-
const bindings = await store.getProfileConfigBindings(profile.id);
|
|
7003
|
-
|
|
7404
|
+
const bindings = await store.getProfileConfigBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7405
|
+
if (store.mode === "api")
|
|
7406
|
+
assertHostedProfileBindings(profile.id, configs, bindings);
|
|
7407
|
+
const assetBindings = await store.getProfileAssetBindings(profile.id, { requireExplicit: store.mode === "api" });
|
|
7004
7408
|
const assetConfigs = await Promise.all([...new Set(assetBindings.map((binding) => binding.source_config_id))].map((id) => store.getConfigById(id)));
|
|
7005
7409
|
return planProfileSessionRender({
|
|
7006
7410
|
profile_id: profile.id,
|
|
@@ -7010,6 +7414,24 @@ async function buildSessionRenderPlan(opts, tool, store, assetPlanMode = "dry-ru
|
|
|
7010
7414
|
targetHome: opts.targetHome,
|
|
7011
7415
|
projectRoot: opts.projectRoot,
|
|
7012
7416
|
sessionId: opts.sessionId,
|
|
7417
|
+
...store.mode === "api" && store.v1BaseUrl ? {
|
|
7418
|
+
refreshSelector: normalizeSessionHostedProfileSelector({
|
|
7419
|
+
schema: "hasna.instructions.hosted-profile-selector/v1",
|
|
7420
|
+
authority: store.v1BaseUrl,
|
|
7421
|
+
profileId: profile.id,
|
|
7422
|
+
providerVersion: opts.providerVersion,
|
|
7423
|
+
...opts.providerVariant ? { providerVariant: opts.providerVariant } : {},
|
|
7424
|
+
...opts.model ? { model: opts.model } : {},
|
|
7425
|
+
...opts.path ? { path: opts.path } : {},
|
|
7426
|
+
manual: opts.manual ?? [],
|
|
7427
|
+
codewithNativeImports: opts.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true",
|
|
7428
|
+
allowEmptySources: opts.allowEmptySources === true,
|
|
7429
|
+
stationProfile: opts.stationProfile !== false,
|
|
7430
|
+
checkGlobalCoverage: opts.checkGlobalCoverage === true,
|
|
7431
|
+
...opts.assetSurface ? { assetSurface: opts.assetSurface } : {},
|
|
7432
|
+
...opts.assetScope ? { assetScope: opts.assetScope } : {}
|
|
7433
|
+
})
|
|
7434
|
+
} : {},
|
|
7013
7435
|
codewithNativeImports: opts.codewithNativeImports,
|
|
7014
7436
|
allowEmptySources: opts.allowEmptySources,
|
|
7015
7437
|
configs,
|
|
@@ -7090,14 +7512,14 @@ function readProjectContextBundleOption(value, allowMissing = false) {
|
|
|
7090
7512
|
return {};
|
|
7091
7513
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_MISSING", `bundle file not found: ${path}`);
|
|
7092
7514
|
}
|
|
7093
|
-
const stat =
|
|
7515
|
+
const stat = lstatSync5(path);
|
|
7094
7516
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
7095
7517
|
throw new ProjectContextError("PROJECT_CONTEXT_SYMLINK_REJECTED", "bundle input must be a regular non-symlink file");
|
|
7096
7518
|
}
|
|
7097
7519
|
if (stat.size > PROJECT_CONTEXT_MAX_INPUT_BYTES) {
|
|
7098
7520
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `bundle exceeds ${PROJECT_CONTEXT_MAX_INPUT_BYTES} bytes`);
|
|
7099
7521
|
}
|
|
7100
|
-
return { json:
|
|
7522
|
+
return { json: readFileSync8(path, "utf8"), sourcePath: path };
|
|
7101
7523
|
}
|
|
7102
7524
|
function readBoundedProjectContextStdin() {
|
|
7103
7525
|
const chunks = [];
|
|
@@ -7476,13 +7898,18 @@ program.command("tag <id>").description("Add or remove tags on a stored config (
|
|
|
7476
7898
|
}
|
|
7477
7899
|
console.log(chalk.green("\u2713") + ` Tags on ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}: ${nextTags.join(", ") || chalk.dim("(none)")}`);
|
|
7478
7900
|
});
|
|
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
|
|
7901
|
+
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) => {
|
|
7902
|
+
const expectedVersion = opts.expectedVersion === undefined ? undefined : Number(opts.expectedVersion);
|
|
7903
|
+
validateExpectedConfigVersion(expectedVersion);
|
|
7904
|
+
if (expectedVersion !== undefined && !opts.update) {
|
|
7905
|
+
throw new Error("--expected-version requires --update");
|
|
7906
|
+
}
|
|
7907
|
+
const abs = resolve7(filePath);
|
|
7481
7908
|
if (!existsSync11(abs)) {
|
|
7482
7909
|
console.error(chalk.red(`File not found: ${abs}`));
|
|
7483
7910
|
process.exit(1);
|
|
7484
7911
|
}
|
|
7485
|
-
const rawContent =
|
|
7912
|
+
const rawContent = readFileSync8(abs, "utf-8");
|
|
7486
7913
|
const storedFmt = detectFormat(abs);
|
|
7487
7914
|
const fmt = redactFormatForTarget(abs, storedFmt);
|
|
7488
7915
|
const { content, redacted, isTemplate } = redactContent(rawContent, fmt);
|
|
@@ -7513,10 +7940,11 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7513
7940
|
const exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
|
|
7514
7941
|
const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
|
|
7515
7942
|
const rest = existingOwners.filter((owner) => owner.id !== target.id);
|
|
7516
|
-
if (content !== target.content) {
|
|
7943
|
+
if (content !== target.content && expectedVersion === undefined) {
|
|
7517
7944
|
await store.createSnapshot(target.id, target.content, target.version);
|
|
7518
7945
|
}
|
|
7519
7946
|
config = await store.updateConfig(target.id, {
|
|
7947
|
+
expected_version: expectedVersion,
|
|
7520
7948
|
content,
|
|
7521
7949
|
format: storedFmt,
|
|
7522
7950
|
is_template: (opts.template ?? false) || isTemplate,
|
|
@@ -7533,6 +7961,9 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
7533
7961
|
}
|
|
7534
7962
|
return;
|
|
7535
7963
|
}
|
|
7964
|
+
if (expectedVersion !== undefined) {
|
|
7965
|
+
throw new Error("--expected-version requires an existing config; no config was created");
|
|
7966
|
+
}
|
|
7536
7967
|
config = await store.createConfig({
|
|
7537
7968
|
name,
|
|
7538
7969
|
kind: opts.kind ?? "file",
|
|
@@ -7661,7 +8092,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7661
8092
|
for (const entry of entries) {
|
|
7662
8093
|
if (!entry.isDirectory())
|
|
7663
8094
|
continue;
|
|
7664
|
-
const projDir =
|
|
8095
|
+
const projDir = join11(absDir, entry.name);
|
|
7665
8096
|
const hasAgentConfig = [
|
|
7666
8097
|
"CLAUDE.md",
|
|
7667
8098
|
".mcp.json",
|
|
@@ -7674,7 +8105,7 @@ program.command("sync").description("Sync known AI coding configs from disk into
|
|
|
7674
8105
|
".aicopilot",
|
|
7675
8106
|
".cursor",
|
|
7676
8107
|
".agents"
|
|
7677
|
-
].some((marker) => existsSync11(
|
|
8108
|
+
].some((marker) => existsSync11(join11(projDir, marker)));
|
|
7678
8109
|
if (!hasAgentConfig)
|
|
7679
8110
|
continue;
|
|
7680
8111
|
const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
|
|
@@ -7722,7 +8153,7 @@ program.command("import <file>").description("Restore a v2 Instructions domain a
|
|
|
7722
8153
|
});
|
|
7723
8154
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
7724
8155
|
const store = resolveConfigStore();
|
|
7725
|
-
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
8156
|
+
const dbPath = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
|
|
7726
8157
|
const stats = await store.getConfigStats();
|
|
7727
8158
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
7728
8159
|
console.log(chalk.cyan(store.mode === "api" ? "API:" : "DB:") + " " + dbPath);
|
|
@@ -8106,7 +8537,7 @@ projectContextCmd.command("apply").description("Atomically write project context
|
|
|
8106
8537
|
}
|
|
8107
8538
|
});
|
|
8108
8539
|
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
|
|
8540
|
+
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
8541
|
try {
|
|
8111
8542
|
const tool = opts.tool;
|
|
8112
8543
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -8161,7 +8592,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
8161
8592
|
process.exit(1);
|
|
8162
8593
|
}
|
|
8163
8594
|
});
|
|
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
|
|
8595
|
+
sessionCmd.command("apply").description("Write a session render plan to its managed target home or explicit project root").requiredOption("--tool <tool>", `target tool (${SESSION_RENDER_TOOLS.join("|")})`).requiredOption("--profile <profile>", "account/profile name that owns the rendered instruction home").option("--target-home <path>", "native instruction home; Sumi: resolved config from `sumi debug paths config`; Grok: GROK_HOME; Devin: user config directory (global AGENTS.md unless --project-root is set)").option("--project-root <path>", "repository root for project-scoped adapters such as Cursor").option("--session-id <id>", "session id to include in the manifest").option("--source <layer:id=path>", `instruction source file; layers: ${SESSION_SOURCE_LAYER_HELP}`, collectOption, []).option("--config <layer:id-or-slug>", "stored config source by id/slug; repeatable; layer aliases match --source", collectOption, []).option("--identity-export <path>", "OpenIdentities configs instruction export JSON; repeatable", collectOption, []).option("--replace-source <replacer-id>[=<target-source-id>]", "source id that broadly replaces earlier layers, or targets one earlier source", collectOption, []).option("--compile-profile <id-or-slug>", "compile persisted config bindings from this Instructions profile").option("--provider-version <semver>", "installed provider version used for capability matching").option("--provider-variant <variant>", "explicit provider capability variant (for example OpenCode v2-agents)").option("--model <model>", "active model used for model activation").option("--path <path>", "active path recorded in the graph context").option("--manual <config-id-or-slug>", "manually activate a binding; repeatable", collectOption, []).option("--asset-surface <surface>", "explicit provider asset surface (for example cline cli or ide)").option("--asset-scope <scope>", "asset scope (global|project|session)").option("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render").option("--check-global-coverage", "verify every registered, non-retired global-* source is in this render (expected read fresh from the registry, independent of this plan); on shortfall warn, exit non-zero, and for apply refuse to write (todos 102d6d0a, O15-00694)").option("--no-station-profile", "do not inject the cached station-profile source").option("--dry-run", "preview writes and conflicts without writing").option("--force", "overwrite existing unmanaged files").option("--adopt-file <relativepath=sha256>", "adopt one reviewed unmanaged generated target only at its exact observed SHA256; repeatable", collectOption, []).option("--reconcile-file <relativepath=sha256>", "reconcile one reviewed managed drifted target at its exact observed SHA256; requires --expected-manifest-sha256", collectOption, []).option("--expected-manifest-sha256 <sha256>", "require the previously reviewed manifest preimage to match exactly before applying").option("--json", "output apply JSON").action(async (opts) => {
|
|
8165
8596
|
try {
|
|
8166
8597
|
const tool = opts.tool;
|
|
8167
8598
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -8187,7 +8618,15 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8187
8618
|
return;
|
|
8188
8619
|
}
|
|
8189
8620
|
const ownedClaudeAuthorities = tool === "claude" ? await loadOwnedClaudeAuthorities(store) : undefined;
|
|
8190
|
-
const
|
|
8621
|
+
const parsePreimages = (values, option) => values.map((value) => {
|
|
8622
|
+
const separator = value.lastIndexOf("=");
|
|
8623
|
+
if (separator <= 0)
|
|
8624
|
+
throw new Error(`${option} requires relativepath=sha256.`);
|
|
8625
|
+
return { relativePath: value.slice(0, separator), sha256: value.slice(separator + 1) };
|
|
8626
|
+
});
|
|
8627
|
+
const adoptFiles = parsePreimages(opts.adoptFile, "--adopt-file");
|
|
8628
|
+
const reconcileFiles = parsePreimages(opts.reconcileFile, "--reconcile-file");
|
|
8629
|
+
const result = applySessionRender(plan, { dryRun: opts.dryRun, force: opts.force, adoptFiles, reconcileFiles, expectedManifestSha256: opts.expectedManifestSha256, ownedClaudeAuthorities });
|
|
8191
8630
|
if (opts.json) {
|
|
8192
8631
|
printJson({
|
|
8193
8632
|
...result,
|
|
@@ -8228,7 +8667,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8228
8667
|
console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
|
|
8229
8668
|
}
|
|
8230
8669
|
if (result.conflicts.length > 0) {
|
|
8231
|
-
console.error(chalk.red(`Conflicts: ${result.conflicts.length}.
|
|
8670
|
+
console.error(chalk.red(`Conflicts: ${result.conflicts.length}. Review and import existing instructions before adopting each exact preimage with --adopt-file.`));
|
|
8232
8671
|
process.exitCode = 1;
|
|
8233
8672
|
}
|
|
8234
8673
|
} catch (e) {
|
|
@@ -8236,6 +8675,29 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
|
|
|
8236
8675
|
process.exit(1);
|
|
8237
8676
|
}
|
|
8238
8677
|
});
|
|
8678
|
+
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) => {
|
|
8679
|
+
try {
|
|
8680
|
+
const result = await refreshSessionRender({ targetHome: resolveSessionPath(opts.targetHome), store: resolveConfigStore(), dryRun: opts.dryRun });
|
|
8681
|
+
if (opts.json)
|
|
8682
|
+
printJson(result);
|
|
8683
|
+
else {
|
|
8684
|
+
console.log(`${result.status}: ${result.targetHome}`);
|
|
8685
|
+
console.log(`hosted profile: ${result.selector.profileId}`);
|
|
8686
|
+
console.log(`source hash: ${result.sourceHash}`);
|
|
8687
|
+
if (result.apply.snapshotPath)
|
|
8688
|
+
console.log(`snapshot: ${result.apply.snapshotPath}`);
|
|
8689
|
+
}
|
|
8690
|
+
if (result.status === "blocked")
|
|
8691
|
+
process.exitCode = 1;
|
|
8692
|
+
} catch (error) {
|
|
8693
|
+
const message = formatCliError(error);
|
|
8694
|
+
if (opts.json)
|
|
8695
|
+
printJson({ status: "failed", error: message });
|
|
8696
|
+
else
|
|
8697
|
+
console.error(chalk.red(message));
|
|
8698
|
+
process.exitCode = 1;
|
|
8699
|
+
}
|
|
8700
|
+
});
|
|
8239
8701
|
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
8702
|
try {
|
|
8241
8703
|
const result = restoreSessionRenderSnapshot(resolveSessionPath(snapshot), {
|
|
@@ -8430,7 +8892,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
|
|
|
8430
8892
|
});
|
|
8431
8893
|
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
8894
|
try {
|
|
8433
|
-
const { renderTemplate } = await import("../chunks/template-
|
|
8895
|
+
const { renderTemplate } = await import("../chunks/template-e4qrxs8p.js");
|
|
8434
8896
|
const c = await resolveConfigStore().getConfig(id);
|
|
8435
8897
|
const vars = {};
|
|
8436
8898
|
if (opts.var) {
|
|
@@ -8444,7 +8906,7 @@ templateCmd.command("render <id>").description("Render a template config with va
|
|
|
8444
8906
|
}
|
|
8445
8907
|
}
|
|
8446
8908
|
if (opts.env) {
|
|
8447
|
-
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-
|
|
8909
|
+
const { extractTemplateVars: extractTemplateVars2 } = await import("../chunks/template-e4qrxs8p.js");
|
|
8448
8910
|
for (const v of extractTemplateVars2(c.content)) {
|
|
8449
8911
|
if (!(v.name in vars) && process.env[v.name]) {
|
|
8450
8912
|
vars[v.name] = process.env[v.name];
|
|
@@ -8493,7 +8955,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
|
|
|
8493
8955
|
} else if (opts.all) {
|
|
8494
8956
|
configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
|
|
8495
8957
|
} else {
|
|
8496
|
-
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-
|
|
8958
|
+
const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await import("../chunks/sync-azd9t4p8.js");
|
|
8497
8959
|
const slugs = [
|
|
8498
8960
|
...KNOWN_CONFIGS2.filter((k) => !k.rulesDir).map((k) => k.name)
|
|
8499
8961
|
];
|
|
@@ -8787,7 +9249,7 @@ command = "${mcpBinary}"
|
|
|
8787
9249
|
args = []
|
|
8788
9250
|
`;
|
|
8789
9251
|
if (ex(configPath)) {
|
|
8790
|
-
const content =
|
|
9252
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
8791
9253
|
if (findTomlTableLines(content.split(`
|
|
8792
9254
|
`), "mcp_servers.configs")) {
|
|
8793
9255
|
console.log(chalk.dim("= Already installed in Codex"));
|
|
@@ -8849,7 +9311,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8849
9311
|
console.log(chalk.dim("= Not installed in Codex (no config.toml)"));
|
|
8850
9312
|
continue;
|
|
8851
9313
|
}
|
|
8852
|
-
const content =
|
|
9314
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
8853
9315
|
const lines = content.split(`
|
|
8854
9316
|
`);
|
|
8855
9317
|
const table = findTomlTableLines(lines, "mcp_servers.configs");
|
|
@@ -8868,7 +9330,7 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
|
|
|
8868
9330
|
console.log(chalk.green("\u2713") + " Removed from Codex");
|
|
8869
9331
|
} else if (target === "antigravity") {
|
|
8870
9332
|
const { existsSync: ex, readFileSync: rf, writeFileSync: wf } = await import("fs");
|
|
8871
|
-
const configPath =
|
|
9333
|
+
const configPath = join11(homedir4(), ".gemini", "config", "mcp_config.json");
|
|
8872
9334
|
if (!ex(configPath)) {
|
|
8873
9335
|
console.log(chalk.dim("= Not installed in Antigravity (no mcp_config.json)"));
|
|
8874
9336
|
continue;
|
|
@@ -8903,15 +9365,15 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8903
9365
|
if (!opts.confirmLocal) {
|
|
8904
9366
|
throw new Error("migrate-legacy requires --confirm-local");
|
|
8905
9367
|
}
|
|
8906
|
-
const destinationPath =
|
|
9368
|
+
const destinationPath = resolve7(opts.destination || process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db"));
|
|
8907
9369
|
const database = await import("../chunks/database-wstmr56z.js");
|
|
8908
9370
|
database.getDatabase(destinationPath);
|
|
8909
9371
|
database.resetDatabase();
|
|
8910
9372
|
const { migrateLegacyInstructionsStore } = await import("../chunks/legacy-store-migration-z2m5yzyk.js");
|
|
8911
9373
|
const result = migrateLegacyInstructionsStore({
|
|
8912
|
-
...opts.source ? { sourcePath:
|
|
9374
|
+
...opts.source ? { sourcePath: resolve7(opts.source) } : {},
|
|
8913
9375
|
destinationPath,
|
|
8914
|
-
...opts.backup ? { backupPath:
|
|
9376
|
+
...opts.backup ? { backupPath: resolve7(opts.backup) } : {},
|
|
8915
9377
|
dryRun: !opts.apply,
|
|
8916
9378
|
...opts.mergePreserveDestination ? { mergePolicy: "preserve-destination" } : {},
|
|
8917
9379
|
operatorConfirmed: true
|
|
@@ -8930,7 +9392,7 @@ program.command("migrate-legacy").description("Migrate the historical ~/.hasna/c
|
|
|
8930
9392
|
if (result.backupCreated)
|
|
8931
9393
|
console.log(chalk.dim(" destination backup created before mutation"));
|
|
8932
9394
|
});
|
|
8933
|
-
program.command("init").description("First-time setup:
|
|
9395
|
+
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
9396
|
const store = resolveConfigStore();
|
|
8935
9397
|
if (opts.force) {
|
|
8936
9398
|
try {
|
|
@@ -8944,26 +9406,28 @@ program.command("init").description("First-time setup: sync all known configs, c
|
|
|
8944
9406
|
}
|
|
8945
9407
|
console.log(chalk.bold(`@hasna/instructions \u2014 initializing
|
|
8946
9408
|
`));
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
9409
|
+
if (store.mode === "local" || opts.importLocal) {
|
|
9410
|
+
const result = await syncKnown({ store });
|
|
9411
|
+
console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
|
|
9412
|
+
if (result.skipped.length > 0)
|
|
9413
|
+
console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
|
|
9414
|
+
} else {
|
|
9415
|
+
console.log(chalk.dim("Hosted authority retained; use --import-local for an intentional local-file migration."));
|
|
8951
9416
|
}
|
|
8952
9417
|
const refs = [
|
|
8953
9418
|
{ slug: "workspace-structure", name: "Workspace Structure", category: "workspace", content: `# Workspace Structure
|
|
8954
9419
|
|
|
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"
|
|
9420
|
+
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" },
|
|
9421
|
+
{ slug: "secrets-schema", name: "Secrets Schema", category: "secrets_schema", content: `# Credential References
|
|
8960
9422
|
|
|
8961
|
-
|
|
9423
|
+
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
9424
|
];
|
|
8963
9425
|
for (const ref of refs) {
|
|
8964
9426
|
try {
|
|
8965
9427
|
await store.getConfig(ref.slug);
|
|
8966
|
-
} catch {
|
|
9428
|
+
} catch (error) {
|
|
9429
|
+
if (!(error instanceof ConfigNotFoundError))
|
|
9430
|
+
throw error;
|
|
8967
9431
|
await store.createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
|
|
8968
9432
|
}
|
|
8969
9433
|
}
|
|
@@ -8973,12 +9437,11 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
|
|
|
8973
9437
|
await ensureProjectDashboardStandardConfig(store);
|
|
8974
9438
|
try {
|
|
8975
9439
|
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`);
|
|
9440
|
+
} catch (error) {
|
|
9441
|
+
if (!(error instanceof ProfileNotFoundError))
|
|
9442
|
+
throw error;
|
|
9443
|
+
await store.createProfile({ name: "my-setup", description: "Instruction profile; add explicitly reviewed and scoped source bindings" });
|
|
9444
|
+
console.log(chalk.green("\u2713") + ' Created empty profile "my-setup"; bind reviewed instruction sources explicitly.');
|
|
8982
9445
|
}
|
|
8983
9446
|
const machineProfiles = await ensurePlatformProfiles(store);
|
|
8984
9447
|
console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
|
|
@@ -8989,7 +9452,7 @@ DB stats:`));
|
|
|
8989
9452
|
if (count > 0)
|
|
8990
9453
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
8991
9454
|
}
|
|
8992
|
-
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] ||
|
|
9455
|
+
const location = store.mode === "api" ? store.v1BaseUrl : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join11(getRawStoreRoot(), "instructions.db");
|
|
8993
9456
|
console.log(chalk.dim(`
|
|
8994
9457
|
${store.mode === "api" ? "API" : "DB"}: ${location}`));
|
|
8995
9458
|
});
|
|
@@ -9090,8 +9553,8 @@ storageCmd.command("status").description("Show redacted S3 configuration status
|
|
|
9090
9553
|
});
|
|
9091
9554
|
var storageBackupCmd = storageCmd.command("backup").description("Push, pull, and verify immutable Instructions backup objects");
|
|
9092
9555
|
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 (!existsSync11(abs) || !
|
|
9556
|
+
const abs = resolve7(file);
|
|
9557
|
+
if (!existsSync11(abs) || !lstatSync5(abs).isFile())
|
|
9095
9558
|
throw new Error(`Backup file not found: ${abs}`);
|
|
9096
9559
|
const [{ loadInstructionsS3Config }, backup, objectStore] = await Promise.all([
|
|
9097
9560
|
import("../chunks/s3-config-hhght91k.js"),
|
|
@@ -9181,16 +9644,16 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9181
9644
|
backupId,
|
|
9182
9645
|
...versions
|
|
9183
9646
|
});
|
|
9184
|
-
const output =
|
|
9647
|
+
const output = resolve7(opts.output);
|
|
9185
9648
|
if (existsSync11(output)) {
|
|
9186
9649
|
if (!opts.force)
|
|
9187
9650
|
throw new Error(`Refusing to overwrite existing backup file: ${output}`);
|
|
9188
|
-
if (!
|
|
9651
|
+
if (!lstatSync5(output).isFile() || lstatSync5(output).isSymbolicLink()) {
|
|
9189
9652
|
throw new Error(`Refusing to replace a non-regular backup destination: ${output}`);
|
|
9190
9653
|
}
|
|
9191
9654
|
}
|
|
9192
|
-
mkdirSync6(
|
|
9193
|
-
const temp =
|
|
9655
|
+
mkdirSync6(dirname7(output), { recursive: true, mode: 448 });
|
|
9656
|
+
const temp = join11(dirname7(output), `.${basename2(output)}.tmp-${process.pid}-${Date.now()}`);
|
|
9194
9657
|
try {
|
|
9195
9658
|
writeFileSync5(temp, pulled.bytes, { flag: "wx", mode: 384 });
|
|
9196
9659
|
renameSync2(temp, output);
|
|
@@ -9215,10 +9678,10 @@ storageBackupCmd.command("pull <backup-id>").description("Download and verify an
|
|
|
9215
9678
|
});
|
|
9216
9679
|
program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
|
|
9217
9680
|
const { mkdirSync: mk } = await import("fs");
|
|
9218
|
-
const backupDir =
|
|
9681
|
+
const backupDir = join11(getRawStoreRoot(), "backups");
|
|
9219
9682
|
mk(backupDir, { recursive: true });
|
|
9220
9683
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
|
|
9221
|
-
const outPath =
|
|
9684
|
+
const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
|
|
9222
9685
|
const result = await exportConfigs(outPath, { store: resolveConfigStore() });
|
|
9223
9686
|
const { statSync: st } = await import("fs");
|
|
9224
9687
|
const size = st(outPath).size;
|
|
@@ -9392,7 +9855,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9392
9855
|
const store = resolveConfigStore();
|
|
9393
9856
|
const interval = Number(opts.interval);
|
|
9394
9857
|
const { statSync: st } = await import("fs");
|
|
9395
|
-
const { expandPath: expandPath2 } = await import("../chunks/apply-
|
|
9858
|
+
const { expandPath: expandPath2 } = await import("../chunks/apply-wj7wh5km.js");
|
|
9396
9859
|
console.log(chalk.bold("@hasna/instructions watch") + chalk.dim(` \u2014 polling every ${interval}ms`));
|
|
9397
9860
|
console.log(chalk.dim(`Watching known config files for changes\u2026
|
|
9398
9861
|
`));
|
|
@@ -9404,7 +9867,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9404
9867
|
continue;
|
|
9405
9868
|
const { readdirSync: readdirSync3 } = await import("fs");
|
|
9406
9869
|
for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9407
|
-
const abs =
|
|
9870
|
+
const abs = join11(absDir, f);
|
|
9408
9871
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9409
9872
|
}
|
|
9410
9873
|
} else {
|
|
@@ -9432,7 +9895,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
9432
9895
|
if (!existsSync11(absDir))
|
|
9433
9896
|
continue;
|
|
9434
9897
|
for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
|
|
9435
|
-
const abs =
|
|
9898
|
+
const abs = join11(absDir, f);
|
|
9436
9899
|
if (!mtimes.has(abs)) {
|
|
9437
9900
|
mtimes.set(abs, st(abs).mtimeMs);
|
|
9438
9901
|
changed++;
|
|
@@ -9472,7 +9935,7 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
9472
9935
|
missing++;
|
|
9473
9936
|
continue;
|
|
9474
9937
|
}
|
|
9475
|
-
const disk =
|
|
9938
|
+
const disk = readFileSync8(abs, "utf-8");
|
|
9476
9939
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(c.target_path, c.format));
|
|
9477
9940
|
if (redactedDisk !== c.content)
|
|
9478
9941
|
drifted++;
|