@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 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` contract emitted by Projects. It
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 !== PROJECT_CONTEXT_SCHEMA) {
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 the strict v1 schema", {
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: PROJECT_CONTEXT_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: PROJECT_CONTEXT_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: exports_external.literal(PROJECT_CONTEXT_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: exports_external.literal(PROJECT_CONTEXT_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 slug(value) {
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((a, b) => SESSION_LAYER_RANK[a.resolvedLayer] - SESSION_LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id));
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 composeSources(sources) {
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, replaceIds) {
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: replaceIds.has(resolvedId) ? "replace" : "append"
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 replaceIds = new Set(opts.replaceSource ?? []);
15976
- const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index, replaceIds));
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) => replaceIds.has(source.id) ? { ...source, merge: "replace" } : 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 instead of appending", 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) => {
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 instead of appending", 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) => {
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)`);