@hasna/instructions 0.4.26 → 0.4.28
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 +14 -1
- package/dist/cli/index.js +219 -21
- package/dist/index.js +146 -12
- package/dist/lib/project-context.d.ts +244 -4
- package/dist/lib/project-context.d.ts.map +1 -1
- package/dist/lib/session-render.d.ts +1 -0
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/mcp/index.js +39 -6
- package/dist/server/index.js +70 -0
- package/dist/server/openapi.d.ts +97 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,12 @@ small. Human output is capped at 20 rows unless you pass `--limit`; use
|
|
|
52
52
|
- `--json` preserves full machine-readable records for automation.
|
|
53
53
|
- `show`/`inspect` and `snapshot show` print full config or snapshot content.
|
|
54
54
|
|
|
55
|
+
`instructions report --json` emits the stable `schema_version: 1` report
|
|
56
|
+
envelope. Its top-level fields are `configs`, `profiles`, `drift`, `secrets`,
|
|
57
|
+
`by_agent`, and `by_category`. The nested count fields are numeric, and
|
|
58
|
+
`secrets.policy` is `redacted_on_ingest`. Run `instructions report` without
|
|
59
|
+
`--json` for the human-readable report.
|
|
60
|
+
|
|
55
61
|
## Package-Manager Secret Guard
|
|
56
62
|
|
|
57
63
|
`instructions package-manager-scan` blocks package-manager credential ingress without
|
|
@@ -243,7 +249,8 @@ never silently become "overwrite renderer-owned instruction files".
|
|
|
243
249
|
### Managed project context
|
|
244
250
|
|
|
245
251
|
`instructions project-context plan|apply` is the sole writer for the strict
|
|
246
|
-
`hasna.projects.project_context_bundle.v1`
|
|
252
|
+
`hasna.projects.project_context_bundle.v1` and schema-compatible
|
|
253
|
+
`hasna.projects.project_context_bundle.v2` contracts emitted by Projects. It
|
|
247
254
|
accepts bounded structured JSON from a regular file or stdin and never invokes
|
|
248
255
|
Projects, Todos, Conversations, or Mementos while rendering:
|
|
249
256
|
|
|
@@ -286,6 +293,12 @@ compatible last-known-good cache can be selected explicitly with
|
|
|
286
293
|
`--allow-stale-cache --expected-project-id <id>`; its bounded age/status is
|
|
287
294
|
visible in the rendered context.
|
|
288
295
|
|
|
296
|
+
Projects v2 bundles may carry the strict optional
|
|
297
|
+
`hasna.projects.finance_project_metadata.v1` object. Instructions validates
|
|
298
|
+
that object and preserves every accepted finance field through the bundle
|
|
299
|
+
cache and session-manifest provenance path; malformed finance metadata and
|
|
300
|
+
finance attached to a legacy v1 bundle fail closed.
|
|
301
|
+
|
|
289
302
|
Compatibility remains additive: project-context manifests keep
|
|
290
303
|
`hasna.configs.session-render/v1`, `Managed by @hasna/configs`, and
|
|
291
304
|
`ownedBy: open-configs`, while recording `canonicalOwner: instructions`. The
|
package/dist/cli/index.js
CHANGED
|
@@ -7952,14 +7952,14 @@ function parseProjectContextBundleInternal(input, allowLegacyHash, normalizeLega
|
|
|
7952
7952
|
throw new ProjectContextError("PROJECT_CONTEXT_INVALID", "bundle is not valid JSON");
|
|
7953
7953
|
}
|
|
7954
7954
|
const candidateSchema = isRecord(value) ? value["schema"] : undefined;
|
|
7955
|
-
if (typeof candidateSchema === "string" && candidateSchema
|
|
7955
|
+
if (typeof candidateSchema === "string" && !PROJECT_CONTEXT_SUPPORTED_SCHEMAS.includes(candidateSchema)) {
|
|
7956
7956
|
if (/^hasna\.projects\.project_context_bundle\.v[0-9]+$/.test(candidateSchema)) {
|
|
7957
7957
|
throw new ProjectContextError("PROJECT_CONTEXT_UNSUPPORTED_VERSION", `unsupported bundle schema ${candidateSchema}`);
|
|
7958
7958
|
}
|
|
7959
7959
|
}
|
|
7960
7960
|
const result = projectContextBundleSchema.safeParse(value);
|
|
7961
7961
|
if (!result.success) {
|
|
7962
|
-
throw new ProjectContextError("PROJECT_CONTEXT_INVALID", "bundle does not match
|
|
7962
|
+
throw new ProjectContextError("PROJECT_CONTEXT_INVALID", "bundle does not match a strict supported schema", {
|
|
7963
7963
|
issues: result.error.issues.map((issue) => ({ path: issue.path.join("."), code: issue.code, message: issue.message }))
|
|
7964
7964
|
});
|
|
7965
7965
|
}
|
|
@@ -7969,7 +7969,7 @@ function parseProjectContextBundleInternal(input, allowLegacyHash, normalizeLega
|
|
|
7969
7969
|
validateIdentityConsistency(bundle);
|
|
7970
7970
|
rejectCredentialLikeBundle(bundle);
|
|
7971
7971
|
const expected = computeProjectContextSourceHash(bundle);
|
|
7972
|
-
const matchesLegacyHash = bundle.hash !== expected && allowLegacyHash && bundle.hash === computeLegacyProjectContextSourceHash(bundle);
|
|
7972
|
+
const matchesLegacyHash = bundle.hash !== expected && allowLegacyHash && bundle.schema === PROJECT_CONTEXT_SCHEMA && bundle.hash === computeLegacyProjectContextSourceHash(bundle);
|
|
7973
7973
|
if (bundle.hash !== expected && !matchesLegacyHash) {
|
|
7974
7974
|
throw new ProjectContextError("PROJECT_CONTEXT_HASH_MISMATCH", "bundle hash does not match its canonical allowlisted payload");
|
|
7975
7975
|
}
|
|
@@ -8956,7 +8956,7 @@ function projectContextManifestSource(cachePath, runtime, bundle) {
|
|
|
8956
8956
|
rules: [],
|
|
8957
8957
|
renderedPayloadSha256: sha2562(JSON.stringify(bundle)),
|
|
8958
8958
|
provenance: {
|
|
8959
|
-
schema:
|
|
8959
|
+
schema: bundle.schema,
|
|
8960
8960
|
projectId: bundle.project.id,
|
|
8961
8961
|
revision: bundle.revision,
|
|
8962
8962
|
hash: bundle.hash
|
|
@@ -8965,7 +8965,7 @@ function projectContextManifestSource(cachePath, runtime, bundle) {
|
|
|
8965
8965
|
}
|
|
8966
8966
|
function manifestProjectContext(plan) {
|
|
8967
8967
|
return {
|
|
8968
|
-
schema:
|
|
8968
|
+
schema: plan.bundle.schema,
|
|
8969
8969
|
projectId: plan.bundle.project.id,
|
|
8970
8970
|
revision: plan.bundle.revision,
|
|
8971
8971
|
hash: plan.bundle.hash,
|
|
@@ -10437,11 +10437,13 @@ function sha2562(content) {
|
|
|
10437
10437
|
function isRecord(value) {
|
|
10438
10438
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10439
10439
|
}
|
|
10440
|
-
var PROJECT_CONTEXT_SCHEMA = "hasna.projects.project_context_bundle.v1", PROJECT_CONTEXT_MAX_INPUT_BYTES, PROJECT_CONTEXT_MAX_RENDERED_BYTES, PROJECT_CONTEXT_MAX_APPROX_TOKENS = 1000, PROJECT_CONTEXT_MAX_COMMANDS = 6, PROJECT_CONTEXT_MAX_WARNINGS = 3, PROJECT_CONTEXT_FRAGMENT_PATH = ".hasna/instructions/project-context.md", PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/project-context-manifest.json", PROJECT_CONTEXT_CACHE_PATH = ".hasna/project-context-cache.json", PROJECT_CONTEXT_LOCK_PATH = ".hasna/project-context.lock", PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/project-context-snapshots", PROJECT_CONTEXT_CACHE_SCHEMA = "hasna.instructions.project-context-cache/v1", PROJECT_CONTEXT_MANAGED_COMMENT = "Managed by @hasna/configs project context", SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES, FOREIGN_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_MAX_BYTES, SESSION_MANAGED_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_PATHS, SESSION_MANAGED_OUTPUT_WARN_BYTES, PROJECT_CONTEXT_LOCK_STALE_MS, LEGACY_CONFIGS_PACKAGE = "@hasna/configs", LEGACY_CONFIGS_COMPAT_VERSION = "0.2.45", LEGACY_CONFIGS_EXECUTABLE = "configs", PROJECT_KINDS, PROJECT_STATUSES, LINK_STATES, RESOLUTION_SOURCES, safeId, nullableId, producerSlug, producerName, safeOptionalDisplay, isoTimestamp, revisionSchema, hashSchema, absolutePath, commandArg, commandSchema, projectContextBundleSchema, storedManifestProjectContextSchema, storedManifestFileSchema, storedManifestObservationSchema, projectContextMetadataSnapshotSchema, projectContextCacheSchema, ProjectContextError, ProjectContextHashRace, anchoredFsOps, atomicExchange, atomicExchangeLibraries;
|
|
10440
|
+
var PROJECT_CONTEXT_SCHEMA = "hasna.projects.project_context_bundle.v1", PROJECT_CONTEXT_SCHEMA_V2 = "hasna.projects.project_context_bundle.v2", PROJECT_CONTEXT_SUPPORTED_SCHEMAS, projectContextSchema, PROJECT_CONTEXT_MAX_INPUT_BYTES, PROJECT_CONTEXT_MAX_RENDERED_BYTES, PROJECT_CONTEXT_MAX_APPROX_TOKENS = 1000, PROJECT_CONTEXT_MAX_COMMANDS = 6, PROJECT_CONTEXT_MAX_WARNINGS = 3, PROJECT_CONTEXT_FRAGMENT_PATH = ".hasna/instructions/project-context.md", PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/project-context-manifest.json", PROJECT_CONTEXT_CACHE_PATH = ".hasna/project-context-cache.json", PROJECT_CONTEXT_LOCK_PATH = ".hasna/project-context.lock", PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/project-context-snapshots", PROJECT_CONTEXT_CACHE_SCHEMA = "hasna.instructions.project-context-cache/v1", PROJECT_CONTEXT_MANAGED_COMMENT = "Managed by @hasna/configs project context", SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES, FOREIGN_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_MAX_BYTES, SESSION_MANAGED_INPUT_MAX_BYTES, SESSION_MANAGED_OUTPUT_PATHS, SESSION_MANAGED_OUTPUT_WARN_BYTES, PROJECT_CONTEXT_LOCK_STALE_MS, LEGACY_CONFIGS_PACKAGE = "@hasna/configs", LEGACY_CONFIGS_COMPAT_VERSION = "0.2.45", LEGACY_CONFIGS_EXECUTABLE = "configs", PROJECT_KINDS, PROJECT_STATUSES, LINK_STATES, RESOLUTION_SOURCES, safeId, nullableId, producerSlug, producerName, safeOptionalDisplay, isoTimestamp, revisionSchema, hashSchema, absolutePath, commandArg, financeText, financeLegalEntity, financeProjectMetadataSchema, commandSchema, projectContextBundleSchema, storedManifestProjectContextSchema, storedManifestFileSchema, storedManifestObservationSchema, projectContextMetadataSnapshotSchema, projectContextCacheSchema, ProjectContextError, ProjectContextHashRace, anchoredFsOps, atomicExchange, atomicExchangeLibraries;
|
|
10441
10441
|
var init_project_context = __esm(() => {
|
|
10442
10442
|
init_zod();
|
|
10443
10443
|
init_redact();
|
|
10444
10444
|
init_session_render_contract();
|
|
10445
|
+
PROJECT_CONTEXT_SUPPORTED_SCHEMAS = [PROJECT_CONTEXT_SCHEMA, PROJECT_CONTEXT_SCHEMA_V2];
|
|
10446
|
+
projectContextSchema = exports_external.enum(PROJECT_CONTEXT_SUPPORTED_SCHEMAS);
|
|
10445
10447
|
PROJECT_CONTEXT_MAX_INPUT_BYTES = 8 * 1024;
|
|
10446
10448
|
PROJECT_CONTEXT_MAX_RENDERED_BYTES = 4 * 1024;
|
|
10447
10449
|
SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -10484,12 +10486,34 @@ var init_project_context = __esm(() => {
|
|
|
10484
10486
|
hashSchema = exports_external.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
10485
10487
|
absolutePath = exports_external.string().min(1).max(4096).refine((value) => isAbsolute(value), "must be absolute").refine(isSafeSingleLine, "must be safe").nullable();
|
|
10486
10488
|
commandArg = exports_external.string().min(1).max(1024).refine((value) => isSafeCommandArgument(value), "unsafe argv item");
|
|
10489
|
+
financeText = exports_external.string().min(1).max(512).refine((value) => value === value.trim(), "must be normalized");
|
|
10490
|
+
financeLegalEntity = exports_external.string().min(1).max(256).refine((value) => value === value.trim(), "must be normalized");
|
|
10491
|
+
financeProjectMetadataSchema = exports_external.object({
|
|
10492
|
+
schema: exports_external.literal("hasna.projects.finance_project_metadata.v1"),
|
|
10493
|
+
business_area: exports_external.literal("finance"),
|
|
10494
|
+
jurisdiction: exports_external.string().min(2).max(64).regex(/^[A-Z0-9][A-Z0-9._:-]{1,63}$/),
|
|
10495
|
+
legal_entities: exports_external.array(financeLegalEntity).min(1).max(100).refine((values) => new Set(values).size === values.length, "must not contain duplicate legal entities"),
|
|
10496
|
+
fiscal_cycle: exports_external.enum(["monthly", "quarterly", "annual", "event-driven"]),
|
|
10497
|
+
data_classification: exports_external.enum(["public", "internal", "confidential", "restricted"]),
|
|
10498
|
+
retention_policy: financeText,
|
|
10499
|
+
ledger_authority: financeText,
|
|
10500
|
+
evidence_store: financeText,
|
|
10501
|
+
approver: financeText,
|
|
10502
|
+
external_recipient_policy: financeText
|
|
10503
|
+
}).strict().superRefine((value, context) => {
|
|
10504
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 4 * 1024) {
|
|
10505
|
+
context.addIssue({
|
|
10506
|
+
code: exports_external.ZodIssueCode.custom,
|
|
10507
|
+
message: "finance project metadata exceeds the 4 KiB producer context budget"
|
|
10508
|
+
});
|
|
10509
|
+
}
|
|
10510
|
+
});
|
|
10487
10511
|
commandSchema = exports_external.object({
|
|
10488
10512
|
name: exports_external.enum(["show", "context", "why", "context-bundle"]),
|
|
10489
10513
|
argv: exports_external.array(commandArg).min(1).max(8)
|
|
10490
10514
|
}).strict();
|
|
10491
10515
|
projectContextBundleSchema = exports_external.object({
|
|
10492
|
-
schema:
|
|
10516
|
+
schema: projectContextSchema,
|
|
10493
10517
|
generated_at: isoTimestamp,
|
|
10494
10518
|
hash: hashSchema,
|
|
10495
10519
|
revision: revisionSchema,
|
|
@@ -10512,7 +10536,8 @@ var init_project_context = __esm(() => {
|
|
|
10512
10536
|
kind: exports_external.enum(PROJECT_KINDS),
|
|
10513
10537
|
status: exports_external.enum(PROJECT_STATUSES),
|
|
10514
10538
|
path: absolutePath,
|
|
10515
|
-
updated_at: isoTimestamp
|
|
10539
|
+
updated_at: isoTimestamp,
|
|
10540
|
+
finance: financeProjectMetadataSchema.optional()
|
|
10516
10541
|
}).strict(),
|
|
10517
10542
|
links: exports_external.object({
|
|
10518
10543
|
todos: exports_external.object({
|
|
@@ -10535,9 +10560,17 @@ var init_project_context = __esm(() => {
|
|
|
10535
10560
|
machine_id: nullableId
|
|
10536
10561
|
}).strict().nullable(),
|
|
10537
10562
|
commands: exports_external.array(commandSchema).max(PROJECT_CONTEXT_MAX_COMMANDS)
|
|
10538
|
-
}).strict()
|
|
10563
|
+
}).strict().superRefine((bundle, context) => {
|
|
10564
|
+
if (bundle.schema === PROJECT_CONTEXT_SCHEMA && bundle.project.finance !== undefined) {
|
|
10565
|
+
context.addIssue({
|
|
10566
|
+
code: exports_external.ZodIssueCode.custom,
|
|
10567
|
+
path: ["project", "finance"],
|
|
10568
|
+
message: "finance project metadata requires project-context bundle v2"
|
|
10569
|
+
});
|
|
10570
|
+
}
|
|
10571
|
+
});
|
|
10539
10572
|
storedManifestProjectContextSchema = exports_external.object({
|
|
10540
|
-
schema:
|
|
10573
|
+
schema: projectContextSchema,
|
|
10541
10574
|
projectId: safeId,
|
|
10542
10575
|
revision: revisionSchema,
|
|
10543
10576
|
hash: hashSchema,
|
|
@@ -10808,10 +10841,13 @@ function sourceFingerprint(source) {
|
|
|
10808
10841
|
metadata: canonicalFingerprintValue(source.metadata ?? null)
|
|
10809
10842
|
};
|
|
10810
10843
|
}
|
|
10811
|
-
function
|
|
10844
|
+
function normalizeSessionInstructionSourceId(value) {
|
|
10812
10845
|
const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10813
10846
|
return s || "instruction";
|
|
10814
10847
|
}
|
|
10848
|
+
function slug(value) {
|
|
10849
|
+
return normalizeSessionInstructionSourceId(value);
|
|
10850
|
+
}
|
|
10815
10851
|
function yamlQuote2(value) {
|
|
10816
10852
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
10817
10853
|
}
|
|
@@ -10930,12 +10966,17 @@ function normalizeSources(sources, tool, allowEmptySources) {
|
|
|
10930
10966
|
}
|
|
10931
10967
|
return normalized2;
|
|
10932
10968
|
});
|
|
10969
|
+
const originalOrder = [...normalized].sort(compareSessionInstructionSources);
|
|
10933
10970
|
const deduplicated = deduplicateSemanticPolicySources(normalized);
|
|
10934
|
-
const ordered = deduplicated.selected.sort(
|
|
10971
|
+
const ordered = deduplicated.selected.sort(compareSessionInstructionSources);
|
|
10972
|
+
validateTargetedReplacementSources(originalOrder, ordered, deduplicated.skipped);
|
|
10935
10973
|
rejectDuplicateSourceSlugs(ordered);
|
|
10936
10974
|
rejectDuplicateRulePaths(ordered);
|
|
10937
10975
|
return { sources: ordered, skipped: deduplicated.skipped };
|
|
10938
10976
|
}
|
|
10977
|
+
function compareSessionInstructionSources(a, b) {
|
|
10978
|
+
return SESSION_LAYER_RANK[a.resolvedLayer] - SESSION_LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id);
|
|
10979
|
+
}
|
|
10939
10980
|
function semanticPolicyIntegrity(body) {
|
|
10940
10981
|
return sha2563(body) === AGENT_OPERATING_RULES_PAYLOAD_SHA256 ? "pinned-digest" : "unverified-self-declared";
|
|
10941
10982
|
}
|
|
@@ -11062,7 +11103,52 @@ function filterProviderOnlyBlocks(content, tool) {
|
|
|
11062
11103
|
return output.join(`
|
|
11063
11104
|
`);
|
|
11064
11105
|
}
|
|
11065
|
-
function
|
|
11106
|
+
function targetedReplacementTarget(source) {
|
|
11107
|
+
if (source.replacementScope == null)
|
|
11108
|
+
return null;
|
|
11109
|
+
if (source.resolvedMerge !== "replace") {
|
|
11110
|
+
throw new Error(`Session instruction source "${source.id}" uses replacement scope "${source.replacementScope}" ` + `with merge=${source.resolvedMerge}; replacement scopes require merge=replace, not append.`);
|
|
11111
|
+
}
|
|
11112
|
+
const scope = source.replacementScope.trim();
|
|
11113
|
+
if (!scope.startsWith("source:")) {
|
|
11114
|
+
throw new Error(`Invalid replacement scope "${source.replacementScope}" for source "${source.id}"; ` + 'expected "source:<normalized-source-id>".');
|
|
11115
|
+
}
|
|
11116
|
+
const target = scope.slice("source:".length);
|
|
11117
|
+
if (!target || target !== slug(target)) {
|
|
11118
|
+
throw new Error(`Invalid replacement scope "${source.replacementScope}" for source "${source.id}"; ` + "the target must be a non-empty normalized source id.");
|
|
11119
|
+
}
|
|
11120
|
+
return target;
|
|
11121
|
+
}
|
|
11122
|
+
function validateTargetedReplacementSources(originalSources, selectedSources, deduplicatedSources) {
|
|
11123
|
+
for (let replacerIndex = 0;replacerIndex < originalSources.length; replacerIndex++) {
|
|
11124
|
+
const replacer = originalSources[replacerIndex];
|
|
11125
|
+
const targetNormalizedId = targetedReplacementTarget(replacer);
|
|
11126
|
+
if (targetNormalizedId === null)
|
|
11127
|
+
continue;
|
|
11128
|
+
const matches = originalSources.filter((source) => source.normalizedId === targetNormalizedId);
|
|
11129
|
+
if (matches.length === 0) {
|
|
11130
|
+
throw new Error(`Targeted replacement source "${replacer.id}" names missing source "${targetNormalizedId}".`);
|
|
11131
|
+
}
|
|
11132
|
+
if (matches.length > 1) {
|
|
11133
|
+
throw new Error(`Targeted replacement source "${replacer.id}" is ambiguous after normalization: ` + `"${targetNormalizedId}" matches ${matches.map((source) => `"${source.id}"`).join(", ")}.`);
|
|
11134
|
+
}
|
|
11135
|
+
const target = matches[0];
|
|
11136
|
+
const targetIndex = originalSources.indexOf(target);
|
|
11137
|
+
if (targetIndex >= replacerIndex) {
|
|
11138
|
+
throw new Error(`Targeted replacement source "${replacer.id}" must name an earlier source; ` + `"${target.id}" is later than or identical to the replacer.`);
|
|
11139
|
+
}
|
|
11140
|
+
if (target.nonOverridable) {
|
|
11141
|
+
throw new Error(`Targeted replacement source "${replacer.id}" cannot replace non-overridable source "${target.id}".`);
|
|
11142
|
+
}
|
|
11143
|
+
if (!selectedSources.some((source) => source.id === replacer.id)) {
|
|
11144
|
+
throw new Error(`Targeted replacement source "${replacer.id}" was removed by semantic-policy deduplication before composition.`);
|
|
11145
|
+
}
|
|
11146
|
+
if (deduplicatedSources.some((source) => source.id === target.id)) {
|
|
11147
|
+
throw new Error(`Targeted replacement source "${replacer.id}" cannot claim success because target "${target.id}" ` + "was removed by semantic-policy deduplication before composition.");
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
function composeBroadReplaceSources(sources) {
|
|
11066
11152
|
let start = -1;
|
|
11067
11153
|
for (let i = 0;i < sources.length; i++) {
|
|
11068
11154
|
if (sources[i].resolvedMerge === "replace")
|
|
@@ -11076,6 +11162,53 @@ function composeSources(sources) {
|
|
|
11076
11162
|
const skipped = earlier.filter((source) => !source.nonOverridable).map((source) => skippedSource(source, `superseded by "${replacer.id}": a replace-merge source discards earlier overridable instruction layers`));
|
|
11077
11163
|
return { sources: [...protectedSources, ...sources.slice(start)], skipped };
|
|
11078
11164
|
}
|
|
11165
|
+
function composeSources(sources) {
|
|
11166
|
+
const hasTargetedReplacement = sources.some((source) => source.replacementScope !== undefined);
|
|
11167
|
+
if (!hasTargetedReplacement)
|
|
11168
|
+
return composeBroadReplaceSources(sources);
|
|
11169
|
+
const selected = [];
|
|
11170
|
+
const skipped = [];
|
|
11171
|
+
for (const source of sources) {
|
|
11172
|
+
const targetNormalizedId = targetedReplacementTarget(source);
|
|
11173
|
+
if (targetNormalizedId !== null) {
|
|
11174
|
+
const targetIndex = selected.findIndex((candidate) => candidate.normalizedId === targetNormalizedId);
|
|
11175
|
+
if (targetIndex < 0) {
|
|
11176
|
+
throw new Error(`Targeted replacement source "${source.id}" cannot replace "${targetNormalizedId}": ` + "the earlier target was already removed.");
|
|
11177
|
+
}
|
|
11178
|
+
const target = selected[targetIndex];
|
|
11179
|
+
if (target.nonOverridable) {
|
|
11180
|
+
throw new Error(`Targeted replacement source "${source.id}" cannot replace non-overridable source "${target.id}".`);
|
|
11181
|
+
}
|
|
11182
|
+
selected.splice(targetIndex, 1);
|
|
11183
|
+
skipped.push(skippedSource(target, `superseded by "${source.id}": targeted replacement ${source.replacementScope} ` + `removed exactly source "${target.id}"`));
|
|
11184
|
+
selected.push({
|
|
11185
|
+
...source,
|
|
11186
|
+
provenance: {
|
|
11187
|
+
...source.provenance ?? {},
|
|
11188
|
+
targetedReplacement: {
|
|
11189
|
+
scope: source.replacementScope,
|
|
11190
|
+
targetSourceId: target.id,
|
|
11191
|
+
targetNormalizedSourceId: target.normalizedId
|
|
11192
|
+
}
|
|
11193
|
+
}
|
|
11194
|
+
});
|
|
11195
|
+
continue;
|
|
11196
|
+
}
|
|
11197
|
+
if (source.resolvedMerge === "replace") {
|
|
11198
|
+
const retained = selected.filter((candidate) => candidate.nonOverridable);
|
|
11199
|
+
for (const candidate of selected) {
|
|
11200
|
+
if (candidate.nonOverridable)
|
|
11201
|
+
continue;
|
|
11202
|
+
skipped.push(skippedSource(candidate, `superseded by "${source.id}": a replace-merge source discards earlier overridable instruction layers`));
|
|
11203
|
+
}
|
|
11204
|
+
selected.length = 0;
|
|
11205
|
+
selected.push(...retained, source);
|
|
11206
|
+
continue;
|
|
11207
|
+
}
|
|
11208
|
+
selected.push(source);
|
|
11209
|
+
}
|
|
11210
|
+
return { sources: selected, skipped };
|
|
11211
|
+
}
|
|
11079
11212
|
function sectionForSource(source) {
|
|
11080
11213
|
const parts = [
|
|
11081
11214
|
`<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
|
|
@@ -15916,7 +16049,7 @@ function parseSessionLayer(value) {
|
|
|
15916
16049
|
return value;
|
|
15917
16050
|
throw new Error(`Invalid source layer "${value}"`);
|
|
15918
16051
|
}
|
|
15919
|
-
function parseSessionSource(value, order
|
|
16052
|
+
function parseSessionSource(value, order) {
|
|
15920
16053
|
const idx = value.indexOf("=");
|
|
15921
16054
|
let id = idx > 0 ? value.slice(0, idx).trim() : "";
|
|
15922
16055
|
const path = idx > 0 ? value.slice(idx + 1).trim() : value.trim();
|
|
@@ -15939,9 +16072,39 @@ function parseSessionSource(value, order, replaceIds) {
|
|
|
15939
16072
|
id: resolvedId,
|
|
15940
16073
|
label: id ? resolvedId : source.label ?? resolvedId,
|
|
15941
16074
|
layer,
|
|
15942
|
-
merge:
|
|
16075
|
+
merge: "append"
|
|
15943
16076
|
};
|
|
15944
16077
|
}
|
|
16078
|
+
function parseSessionSourceReplacement(value) {
|
|
16079
|
+
const trimmed = value.trim();
|
|
16080
|
+
const separator = trimmed.indexOf("=");
|
|
16081
|
+
const replacerId = (separator >= 0 ? trimmed.slice(0, separator) : trimmed).trim();
|
|
16082
|
+
if (!replacerId) {
|
|
16083
|
+
throw new Error(`Invalid --replace-source "${value}" (expected replacer-id or replacer-id=target-source-id)`);
|
|
16084
|
+
}
|
|
16085
|
+
if (separator < 0)
|
|
16086
|
+
return { replacerId };
|
|
16087
|
+
const targetId = trimmed.slice(separator + 1).trim();
|
|
16088
|
+
if (!targetId) {
|
|
16089
|
+
throw new Error(`Invalid --replace-source "${value}" (target source id is required after "=")`);
|
|
16090
|
+
}
|
|
16091
|
+
return {
|
|
16092
|
+
replacerId,
|
|
16093
|
+
replacementScope: `source:${normalizeSessionInstructionSourceId(targetId)}`
|
|
16094
|
+
};
|
|
16095
|
+
}
|
|
16096
|
+
function sessionSourceReplacements(values) {
|
|
16097
|
+
const replacements = new Map;
|
|
16098
|
+
for (const value of values) {
|
|
16099
|
+
const replacement = parseSessionSourceReplacement(value);
|
|
16100
|
+
const existing = replacements.get(replacement.replacerId);
|
|
16101
|
+
if (existing && existing.replacementScope !== replacement.replacementScope) {
|
|
16102
|
+
throw new Error(`Conflicting --replace-source values for "${replacement.replacerId}": ` + `${existing.replacementScope ?? "broad"} and ${replacement.replacementScope ?? "broad"}.`);
|
|
16103
|
+
}
|
|
16104
|
+
replacements.set(replacement.replacerId, replacement);
|
|
16105
|
+
}
|
|
16106
|
+
return replacements;
|
|
16107
|
+
}
|
|
15945
16108
|
function readSessionInstructionSourceFile(path) {
|
|
15946
16109
|
const stat = lstatSync4(path);
|
|
15947
16110
|
if (stat.isSymbolicLink()) {
|
|
@@ -15972,8 +16135,8 @@ function parseLayeredReference(value) {
|
|
|
15972
16135
|
return { id: trimmed };
|
|
15973
16136
|
}
|
|
15974
16137
|
async function collectSessionSources(opts, tool, store) {
|
|
15975
|
-
const
|
|
15976
|
-
const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index
|
|
16138
|
+
const replacements = sessionSourceReplacements(opts.replaceSource ?? []);
|
|
16139
|
+
const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index));
|
|
15977
16140
|
for (const value of opts.config ?? []) {
|
|
15978
16141
|
const { layer, id } = parseLayeredReference(value);
|
|
15979
16142
|
sources.push(sourceFromConfig(await store.getConfig(id), sources.length, layer));
|
|
@@ -15985,7 +16148,16 @@ async function collectSessionSources(opts, tool, store) {
|
|
|
15985
16148
|
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
15986
16149
|
sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
|
|
15987
16150
|
}
|
|
15988
|
-
return sources.map((source) =>
|
|
16151
|
+
return sources.map((source) => {
|
|
16152
|
+
const replacement = replacements.get(source.id);
|
|
16153
|
+
if (!replacement)
|
|
16154
|
+
return source;
|
|
16155
|
+
return {
|
|
16156
|
+
...source,
|
|
16157
|
+
merge: "replace",
|
|
16158
|
+
replacementScope: replacement.replacementScope
|
|
16159
|
+
};
|
|
16160
|
+
});
|
|
15989
16161
|
}
|
|
15990
16162
|
async function checkGlobalSourceCoverage(plan, store) {
|
|
15991
16163
|
const registryConfigs = await store.listConfigs({});
|
|
@@ -16728,7 +16900,7 @@ projectContextCmd.command("apply").description("Atomically write project context
|
|
|
16728
16900
|
}
|
|
16729
16901
|
});
|
|
16730
16902
|
var sessionCmd = program.command("session").description("Plan and apply session-scoped agent instruction files");
|
|
16731
|
-
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>", "override generated profile-scoped target home").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 <id>", "source id that replaces earlier layers
|
|
16903
|
+
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>", "override generated profile-scoped target home").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("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render plan").option("--check-global-coverage", "warn (non-fatal) when a registered, non-retired global-* source is absent from this render's --config list; expected is read fresh from the registry, independent of this plan (todos 102d6d0a)").option("--json", "output dry-run JSON").action(async (opts) => {
|
|
16732
16904
|
try {
|
|
16733
16905
|
const tool = opts.tool;
|
|
16734
16906
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -16784,7 +16956,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
16784
16956
|
process.exit(1);
|
|
16785
16957
|
}
|
|
16786
16958
|
});
|
|
16787
|
-
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>", "override generated profile-scoped target home").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 <id>", "source id that replaces earlier layers
|
|
16959
|
+
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>", "override generated profile-scoped target home").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("--codewith-native-imports", "select the gated Codewith native @ import adapter").option("--allow-empty-sources", "allow an explicit empty render").option("--check-global-coverage", "warn (non-fatal) when a registered, non-retired global-* source is absent from this render's --config list; expected is read fresh from the registry, independent of this plan (todos 102d6d0a)").option("--dry-run", "preview writes and conflicts without writing").option("--force", "overwrite existing unmanaged files").option("--json", "output apply JSON").action(async (opts) => {
|
|
16788
16960
|
try {
|
|
16789
16961
|
const tool = opts.tool;
|
|
16790
16962
|
if (!SESSION_RENDER_TOOLS.includes(tool)) {
|
|
@@ -17477,7 +17649,7 @@ program.command("watch").description("Watch known config files for changes and a
|
|
|
17477
17649
|
setInterval(tick, interval);
|
|
17478
17650
|
await new Promise(() => {});
|
|
17479
17651
|
});
|
|
17480
|
-
program.command("report").description("Summary of stored configs, drift, and ecosystem health").option("--json", "output as JSON").option("--markdown", "output as markdown").action(async () => {
|
|
17652
|
+
program.command("report").description("Summary of stored configs, drift, and ecosystem health").option("--json", "output as JSON").option("--markdown", "output as markdown").action(async (opts) => {
|
|
17481
17653
|
const store = resolveConfigStore();
|
|
17482
17654
|
const stats = await store.getConfigStats();
|
|
17483
17655
|
const allConfigs = await store.listConfigs();
|
|
@@ -17503,6 +17675,32 @@ program.command("report").description("Summary of stored configs, drift, and eco
|
|
|
17503
17675
|
for (const c of allConfigs)
|
|
17504
17676
|
byAgent[c.agent] = (byAgent[c.agent] || 0) + 1;
|
|
17505
17677
|
const projectConfigs = allConfigs.filter((c) => c.target_path && !c.target_path.startsWith("~/."));
|
|
17678
|
+
if (opts.json) {
|
|
17679
|
+
printJson({
|
|
17680
|
+
schema_version: 1,
|
|
17681
|
+
configs: {
|
|
17682
|
+
total: allConfigs.length,
|
|
17683
|
+
files: fileConfigs.length,
|
|
17684
|
+
references: refConfigs.length,
|
|
17685
|
+
templates: templates.length,
|
|
17686
|
+
project: projectConfigs.length
|
|
17687
|
+
},
|
|
17688
|
+
profiles: {
|
|
17689
|
+
total: profiles.length
|
|
17690
|
+
},
|
|
17691
|
+
drift: {
|
|
17692
|
+
drifted,
|
|
17693
|
+
missing
|
|
17694
|
+
},
|
|
17695
|
+
secrets: {
|
|
17696
|
+
findings: 0,
|
|
17697
|
+
policy: "redacted_on_ingest"
|
|
17698
|
+
},
|
|
17699
|
+
by_agent: byAgent,
|
|
17700
|
+
by_category: Object.fromEntries(Object.entries(stats).filter(([key]) => key !== "total").map(([key, value]) => [key, Number(value)]))
|
|
17701
|
+
});
|
|
17702
|
+
return;
|
|
17703
|
+
}
|
|
17506
17704
|
console.log(chalk.bold(`configs report
|
|
17507
17705
|
`));
|
|
17508
17706
|
console.log(` Total: ${allConfigs.length} configs (${fileConfigs.length} files, ${refConfigs.length} references)`);
|