@hasna/instructions 0.4.25 → 0.4.27

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/dist/cli/index.js CHANGED
@@ -7930,6 +7930,9 @@ function computeProjectContextSourceHash(value) {
7930
7930
  return `sha256:${sha2562(stableStringify(normalized))}`;
7931
7931
  }
7932
7932
  function parseProjectContextBundle(input) {
7933
+ return parseProjectContextBundleInternal(input, true, true);
7934
+ }
7935
+ function parseProjectContextBundleInternal(input, allowLegacyHash, normalizeLegacyHash = false) {
7933
7936
  let encoded;
7934
7937
  try {
7935
7938
  const serialized = typeof input === "string" ? input : JSON.stringify(input);
@@ -7949,7 +7952,7 @@ function parseProjectContextBundle(input) {
7949
7952
  throw new ProjectContextError("PROJECT_CONTEXT_INVALID", "bundle is not valid JSON");
7950
7953
  }
7951
7954
  const candidateSchema = isRecord(value) ? value["schema"] : undefined;
7952
- if (typeof candidateSchema === "string" && candidateSchema !== PROJECT_CONTEXT_SCHEMA) {
7955
+ if (typeof candidateSchema === "string" && !PROJECT_CONTEXT_SUPPORTED_SCHEMAS.includes(candidateSchema)) {
7953
7956
  if (/^hasna\.projects\.project_context_bundle\.v[0-9]+$/.test(candidateSchema)) {
7954
7957
  throw new ProjectContextError("PROJECT_CONTEXT_UNSUPPORTED_VERSION", `unsupported bundle schema ${candidateSchema}`);
7955
7958
  }
@@ -7966,10 +7969,11 @@ function parseProjectContextBundle(input) {
7966
7969
  validateIdentityConsistency(bundle);
7967
7970
  rejectCredentialLikeBundle(bundle);
7968
7971
  const expected = computeProjectContextSourceHash(bundle);
7969
- if (bundle.hash !== expected) {
7972
+ const matchesLegacyHash = bundle.hash !== expected && allowLegacyHash && bundle.hash === computeLegacyProjectContextSourceHash(bundle);
7973
+ if (bundle.hash !== expected && !matchesLegacyHash) {
7970
7974
  throw new ProjectContextError("PROJECT_CONTEXT_HASH_MISMATCH", "bundle hash does not match its canonical allowlisted payload");
7971
7975
  }
7972
- return bundle;
7976
+ return matchesLegacyHash && normalizeLegacyHash ? { ...bundle, hash: expected } : bundle;
7973
7977
  }
7974
7978
  function planProjectContext(input) {
7975
7979
  const workspaceRoot = assertSafeWorkspaceRoot(input.workspace_root);
@@ -8217,6 +8221,8 @@ function applyProjectContext(options) {
8217
8221
  content: `${JSON.stringify(sessionManifest, null, 2)}
8218
8222
  `
8219
8223
  };
8224
+ const manifestContent = `${JSON.stringify(buildManifest(plan, now3), null, 2)}
8225
+ `;
8220
8226
  options.test_hooks?.before_compare?.({ attempt, plan });
8221
8227
  if (!hashesStillMatch(plan.expected_hashes, workspaceRoot)) {
8222
8228
  if (attempt === 0) {
@@ -8229,7 +8235,15 @@ function applyProjectContext(options) {
8229
8235
  return resultForPlan(plan, true, raceRetries, null);
8230
8236
  assertWorkspaceLockHeld(lockPath, lock, workspaceRoot);
8231
8237
  try {
8232
- snapshotPath = writeMetadataSnapshot(plan, now3);
8238
+ const metadataSnapshotPath = writeMetadataSnapshot(plan, now3);
8239
+ const rollbackSnapshotPath = writeProjectContextRollbackSnapshot(plan, now3, [
8240
+ { path: plan.fragment_path, content: plan.fragment, role: "fragment" },
8241
+ { path: plan.target_path, content: plan.target_content, role: "index" },
8242
+ { path: plan.cache_path, content: cacheContent, role: "config" },
8243
+ { path: sessionOutput.path, content: sessionOutput.content, role: "manifest" },
8244
+ { path: plan.manifest_path, content: manifestContent, role: "manifest" }
8245
+ ]);
8246
+ snapshotPath = rollbackSnapshotPath ?? metadataSnapshotPath;
8233
8247
  atomicWriteFile(plan.fragment_path, plan.fragment, workspaceRoot, 420, expectedPlanHash(plan, plan.fragment_path), undefined, options.test_hooks?.atomic_exchange_unavailable, undefined, options.test_hooks?.portable_create_only);
8234
8248
  options.test_hooks?.after_fragment?.({ attempt, plan });
8235
8249
  assertWorkspaceLockHeld(lockPath, lock, workspaceRoot);
@@ -8243,9 +8257,7 @@ function applyProjectContext(options) {
8243
8257
  options.test_hooks?.before_manifest?.({ attempt, plan });
8244
8258
  assertWorkspaceLockHeld(lockPath, lock, workspaceRoot);
8245
8259
  assertRenderedOutputsStable(plan, cacheContent, sessionOutput);
8246
- const manifest = buildManifest(plan, now3);
8247
- atomicWriteFile(plan.manifest_path, `${JSON.stringify(manifest, null, 2)}
8248
- `, workspaceRoot, 384, expectedPlanHash(plan, plan.manifest_path), undefined, options.test_hooks?.atomic_exchange_unavailable, undefined, options.test_hooks?.portable_create_only);
8260
+ atomicWriteFile(plan.manifest_path, manifestContent, workspaceRoot, 384, expectedPlanHash(plan, plan.manifest_path), undefined, options.test_hooks?.atomic_exchange_unavailable, undefined, options.test_hooks?.portable_create_only);
8249
8261
  return resultForPlan(plan, false, raceRetries, snapshotPath);
8250
8262
  } catch (error) {
8251
8263
  if (error instanceof ProjectContextHashRace && attempt === 0) {
@@ -8336,10 +8348,12 @@ function resolveBundleForApply(options, workspaceRoot, now3) {
8336
8348
  if (cache.project_id !== options.expected_project_id || cache.bundle.project.id !== options.expected_project_id) {
8337
8349
  throw new ProjectContextError("PROJECT_CONTEXT_CACHE_ID_MISMATCH", "cached project context belongs to a different project");
8338
8350
  }
8339
- const bundle = parseProjectContextBundle(cache.bundle);
8340
- if (bundle.revision !== cache.revision || bundle.hash !== cache.hash) {
8351
+ const cachedBundle = cache.bundle;
8352
+ if (cachedBundle.revision !== cache.revision || cachedBundle.hash !== cache.hash) {
8341
8353
  throw new ProjectContextError("PROJECT_CONTEXT_CACHE_INVALID", "cached revision or hash metadata is inconsistent");
8342
8354
  }
8355
+ const canonicalHash = computeProjectContextSourceHash(cachedBundle);
8356
+ const bundle = cachedBundle.hash === canonicalHash ? cachedBundle : { ...cachedBundle, hash: canonicalHash };
8343
8357
  const ageSeconds = Math.max(staleCacheAgeInSeconds(bundle.generated_at, now3, "bundle generated_at"), staleCacheAgeInSeconds(cache.cached_at, now3, "cache cached_at"));
8344
8358
  const maxAge = normalizeMaxStaleAge(options.max_stale_age_seconds);
8345
8359
  if (ageSeconds > maxAge) {
@@ -8436,11 +8450,13 @@ function parseManagedBlock(content, force) {
8436
8450
  if (structurallyInvalid) {
8437
8451
  if (!force)
8438
8452
  throw new ProjectContextError("MANAGED_BLOCK_INVALID", "managed project-context markers are duplicate, nested, malformed, or unbalanced");
8453
+ const firstMarkerRange = markerCommentRange(markerLines[0]);
8454
+ const lastMarkerRange = markerCommentRange(markerLines[markerLines.length - 1]);
8439
8455
  return {
8440
8456
  block: null,
8441
8457
  forceRange: {
8442
- start: markerLines[0].start,
8443
- end: lineContentEnd(markerLines[markerLines.length - 1])
8458
+ start: firstMarkerRange?.start ?? markerLines[0].start,
8459
+ end: lastMarkerRange?.end ?? lineContentEnd(markerLines[markerLines.length - 1])
8444
8460
  }
8445
8461
  };
8446
8462
  }
@@ -8471,6 +8487,21 @@ function parseManagedBlock(content, force) {
8471
8487
  forceRange: null
8472
8488
  };
8473
8489
  }
8490
+ function markerCommentRange(line) {
8491
+ const canonicalMarker = line.text.indexOf(PROJECT_CONTEXT_MANAGED_COMMENT);
8492
+ const legacyMarker = /@hasna\/configs project context/i.exec(line.text)?.index ?? -1;
8493
+ const marker = canonicalMarker >= 0 ? canonicalMarker : legacyMarker;
8494
+ if (marker < 0)
8495
+ return null;
8496
+ const commentStart = line.text.lastIndexOf("<!--", marker);
8497
+ const commentEnd = line.text.indexOf("-->", marker);
8498
+ if (commentStart < 0 || commentEnd < 0)
8499
+ return null;
8500
+ return {
8501
+ start: line.start + commentStart,
8502
+ end: line.start + commentEnd + "-->".length
8503
+ };
8504
+ }
8474
8505
  function parseMarkerLine(text) {
8475
8506
  const line = text.replace(/[\r\n]+$/, "");
8476
8507
  const canonical = line.match(/^<!-- Managed by @hasna\/configs project context (BEGIN|END) id=([A-Za-z0-9][A-Za-z0-9._:@+-]*) revision=([A-Za-z0-9%._~+-]+) hash=(sha256:[a-f0-9]{64}) -->$/);
@@ -8492,8 +8523,14 @@ function parseMarkerLine(text) {
8492
8523
  }
8493
8524
  function replaceOrAppendManagedBlock(content, block, parsed, legacy) {
8494
8525
  const range = parsed.block ?? parsed.forceRange ?? legacy;
8495
- if (range)
8496
- return `${content.slice(0, range.start)}${block}${content.slice(range.end)}`;
8526
+ if (range) {
8527
+ const before = content.slice(0, range.start);
8528
+ const after = content.slice(range.end);
8529
+ const eol2 = preferredEol(content);
8530
+ const beforeSeparator = before && !/[\r\n]$/.test(before) ? eol2 : "";
8531
+ const afterSeparator = after && !/^[\r\n]/.test(after) ? eol2 : "";
8532
+ return `${before}${beforeSeparator}${block}${afterSeparator}${after}`;
8533
+ }
8497
8534
  if (!content)
8498
8535
  return `${block}
8499
8536
  `;
@@ -8537,13 +8574,18 @@ function findLegacyCodewithWorkspaceSection(workspaceRoot, runtime, content, bun
8537
8574
  }
8538
8575
  function assertRevisionOrdering(plan, force) {
8539
8576
  const observations = [];
8577
+ const cache = readProjectContextCache(plan.cache_path, plan.workspace_root);
8578
+ const canonicalCacheHash = cache === null ? null : computeProjectContextSourceHash(cache.bundle);
8579
+ const normalizePersistedHash = (revision, hash) => cache !== null && canonicalCacheHash !== null && revision === cache.revision && hash === cache.hash ? canonicalCacheHash : hash;
8540
8580
  const manifest = readProjectContextManifest(plan.manifest_path, plan.workspace_root);
8541
8581
  if (manifest) {
8582
+ const manifestHashHasRecoveryProof = manifest.projectContext.hash === plan.bundle.hash || metadataSnapshotMatchesManifest(plan, manifest);
8583
+ const canonicalStateAlreadyInstalled = cache !== null && canonicalCacheHash === plan.bundle.hash && cache.project_id === plan.bundle.project.id && cache.revision === plan.bundle.revision && plan.marker !== null && plan.marker.id === plan.bundle.project.id && plan.marker.revision === plan.bundle.revision && plan.marker.hash === plan.bundle.hash && existsSync4(plan.fragment_path) && fragmentMatchesBundle(plan.fragment_path, plan.bundle, plan.workspace_root) && manifestHashHasRecoveryProof;
8542
8584
  observations.push({
8543
8585
  source: "manifest",
8544
8586
  id: manifest.projectContext.projectId,
8545
8587
  revision: manifest.projectContext.revision,
8546
- hash: manifest.projectContext.hash
8588
+ hash: canonicalStateAlreadyInstalled && manifest.projectContext.projectId === plan.bundle.project.id && manifest.projectContext.revision === plan.bundle.revision ? plan.bundle.hash : normalizePersistedHash(manifest.projectContext.revision, manifest.projectContext.hash)
8547
8589
  });
8548
8590
  const fragmentEntry = manifest.files.find((file) => file.relativePath === PROJECT_CONTEXT_FRAGMENT_PATH);
8549
8591
  if (fragmentEntry && existsSync4(plan.fragment_path)) {
@@ -8553,11 +8595,16 @@ function assertRevisionOrdering(plan, force) {
8553
8595
  }
8554
8596
  }
8555
8597
  }
8556
- const cache = readProjectContextCache(plan.cache_path, plan.workspace_root);
8557
8598
  if (cache)
8558
- observations.push({ source: "cache", id: cache.project_id, revision: cache.revision, hash: cache.hash });
8559
- if (plan.marker)
8560
- observations.push({ source: "marker", id: plan.marker.id, revision: plan.marker.revision, hash: plan.marker.hash });
8599
+ observations.push({ source: "cache", id: cache.project_id, revision: cache.revision, hash: canonicalCacheHash });
8600
+ if (plan.marker) {
8601
+ observations.push({
8602
+ source: "marker",
8603
+ id: plan.marker.id,
8604
+ revision: plan.marker.revision,
8605
+ hash: normalizePersistedHash(plan.marker.revision, plan.marker.hash)
8606
+ });
8607
+ }
8561
8608
  for (const observation of observations) {
8562
8609
  if (observation.id !== plan.bundle.project.id) {
8563
8610
  throw new ProjectContextError("PROJECT_CONTEXT_IDENTITY_CONFLICT", `${observation.source} belongs to another project`);
@@ -8677,6 +8724,10 @@ function buildSessionCompatibilityManifest(plan, now3) {
8677
8724
  };
8678
8725
  const targetOwner = isRecord(existing["targetOwner"]) ? existing["targetOwner"] : {};
8679
8726
  const adapterMode = plan.native_imports ? "native-imports" : "flattened-markdown";
8727
+ const existingProjectContext = isRecord(existing["projectContext"]) ? existing["projectContext"] : null;
8728
+ const existingProjectContextHash = existingProjectContext === null ? null : safeLegacyMetadataString(existingProjectContext["hash"], null);
8729
+ const existingSourceHash = typeof existing["sourceHash"] === "string" ? existing["sourceHash"] : null;
8730
+ const sourceHash = existingProjectContextHash === plan.bundle.hash && existingSourceHash !== null ? existingSourceHash : sha2562(stableStringify({ previous: existingSourceHash, projectContext: plan.bundle.hash }));
8680
8731
  return credentialSafeSessionManifest({
8681
8732
  schema: SESSION_RENDER_SCHEMA,
8682
8733
  tool,
@@ -8700,7 +8751,7 @@ function buildSessionCompatibilityManifest(plan, now3) {
8700
8751
  blockers: [],
8701
8752
  generatedAt: now3.toISOString(),
8702
8753
  env: sanitizeLegacyEnvironment(existing["env"]),
8703
- sourceHash: sha2562(stableStringify({ previous: typeof existing["sourceHash"] === "string" ? existing["sourceHash"] : null, projectContext: plan.bundle.hash })),
8754
+ sourceHash,
8704
8755
  sources,
8705
8756
  skippedSources: sanitizeLegacySkippedSources(existing["skippedSources"]),
8706
8757
  files: [...files.filter((file) => file["relativePath"] !== targetRelativePath), updatedTarget],
@@ -8905,7 +8956,7 @@ function projectContextManifestSource(cachePath, runtime, bundle) {
8905
8956
  rules: [],
8906
8957
  renderedPayloadSha256: sha2562(JSON.stringify(bundle)),
8907
8958
  provenance: {
8908
- schema: PROJECT_CONTEXT_SCHEMA,
8959
+ schema: bundle.schema,
8909
8960
  projectId: bundle.project.id,
8910
8961
  revision: bundle.revision,
8911
8962
  hash: bundle.hash
@@ -8914,7 +8965,7 @@ function projectContextManifestSource(cachePath, runtime, bundle) {
8914
8965
  }
8915
8966
  function manifestProjectContext(plan) {
8916
8967
  return {
8917
- schema: PROJECT_CONTEXT_SCHEMA,
8968
+ schema: plan.bundle.schema,
8918
8969
  projectId: plan.bundle.project.id,
8919
8970
  revision: plan.bundle.revision,
8920
8971
  hash: plan.bundle.hash,
@@ -8956,6 +9007,77 @@ function writeMetadataSnapshot(plan, now3) {
8956
9007
  `, plan.workspace_root, 384);
8957
9008
  return snapshotPath;
8958
9009
  }
9010
+ function metadataSnapshotMatchesManifest(plan, manifest) {
9011
+ const snapshotDir = resolve(plan.workspace_root, ...PROJECT_CONTEXT_SNAPSHOT_DIR.split("/"));
9012
+ const snapshotPath = resolve(snapshotDir, `${safeFilename(manifest.projectContext.revision)}-${manifest.projectContext.hash.slice(-12)}.json`);
9013
+ if (!existsSync4(snapshotPath))
9014
+ return false;
9015
+ const record = readJsonRecord(snapshotPath, plan.workspace_root);
9016
+ const result = projectContextMetadataSnapshotSchema.safeParse(record);
9017
+ if (!result.success)
9018
+ return false;
9019
+ if (result.data.projectId !== manifest.projectContext.projectId || result.data.revision !== manifest.projectContext.revision || result.data.hash !== manifest.projectContext.hash || result.data.status !== manifest.projectContext.status)
9020
+ return false;
9021
+ const sortFiles = (files) => [...files].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
9022
+ const snapshotFiles = sortFiles(result.data.files);
9023
+ const manifestFiles = sortFiles(manifest.files.map((file) => ({
9024
+ relativePath: file.relativePath,
9025
+ role: file.role,
9026
+ sha256: file.sha256
9027
+ })));
9028
+ return JSON.stringify(snapshotFiles) === JSON.stringify(manifestFiles);
9029
+ }
9030
+ function writeProjectContextRollbackSnapshot(plan, now3, outputs) {
9031
+ const targetExpectedHash = expectedPlanHash(plan, plan.target_path);
9032
+ if (targetExpectedHash === sha2562(plan.target_content))
9033
+ return null;
9034
+ const files = outputs.flatMap((output) => {
9035
+ const expectedHash = expectedPlanHash(plan, output.path);
9036
+ if (expectedHash === null)
9037
+ return [];
9038
+ const content = readUtf8RegularFile(output.path, plan.workspace_root, managedObservationMaxBytes(relativePosix(plan.workspace_root, output.path)));
9039
+ if (sha2562(content) !== expectedHash) {
9040
+ throw new ProjectContextHashRace(`managed path changed while creating rollback evidence: ${relativePosix(plan.workspace_root, output.path)}`);
9041
+ }
9042
+ return [{
9043
+ path: output.path,
9044
+ relativePath: relativePosix(plan.workspace_root, output.path),
9045
+ role: output.role,
9046
+ sha256: expectedHash,
9047
+ content
9048
+ }];
9049
+ });
9050
+ const afterFiles = outputs.map((output) => {
9051
+ const previousHash = expectedPlanHash(plan, output.path);
9052
+ const nextHash = sha2562(output.content);
9053
+ return {
9054
+ path: output.path,
9055
+ relativePath: relativePosix(plan.workspace_root, output.path),
9056
+ role: output.role,
9057
+ action: previousHash === null ? "create" : previousHash === nextHash ? "unchanged" : "update",
9058
+ sha256: nextHash
9059
+ };
9060
+ });
9061
+ const snapshotDir = resolve(plan.workspace_root, ...SESSION_RENDER_SNAPSHOT_RELATIVE_DIR.split("/"));
9062
+ ensureSafeDirectory(snapshotDir, plan.workspace_root, 448);
9063
+ const timestamp = now3.toISOString().replace(/[:.]/g, "-");
9064
+ const snapshotPath = resolve(snapshotDir, `${timestamp}-${randomUUID6()}.json`);
9065
+ const snapshot = {
9066
+ schema: "hasna.configs.session-render-snapshot/v2",
9067
+ createdAt: now3.toISOString(),
9068
+ tool: manifestTool(plan.runtime),
9069
+ profile: "project-context",
9070
+ targetHome: plan.workspace_root,
9071
+ targetKind: "project-root",
9072
+ manifestPath: plan.manifest_path,
9073
+ previousManifest: null,
9074
+ files,
9075
+ afterFiles
9076
+ };
9077
+ atomicWriteFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}
9078
+ `, plan.workspace_root, 384, null);
9079
+ return snapshotPath;
9080
+ }
8959
9081
  function readProjectContextManifest(path, workspaceRoot) {
8960
9082
  if (!existsSync4(path))
8961
9083
  return null;
@@ -8979,7 +9101,7 @@ function readProjectContextCache(path, workspaceRoot) {
8979
9101
  if (!result.success) {
8980
9102
  throw new ProjectContextError("PROJECT_CONTEXT_CACHE_INVALID", "cache is malformed or incompatible");
8981
9103
  }
8982
- const bundle = parseProjectContextBundle(result.data.bundle);
9104
+ const bundle = parseProjectContextBundleInternal(result.data.bundle, true);
8983
9105
  if (result.data.project_id !== bundle.project.id || result.data.revision !== bundle.revision || result.data.hash !== bundle.hash) {
8984
9106
  throw new ProjectContextError("PROJECT_CONTEXT_CACHE_INVALID", "cache metadata does not match its bundle");
8985
9107
  }
@@ -10292,23 +10414,36 @@ function removeHashForFingerprint(value) {
10292
10414
  return value;
10293
10415
  const copy = {};
10294
10416
  for (const [key, item] of Object.entries(value)) {
10295
- if (key === "hash")
10417
+ if (key === "generated_at" || key === "hash")
10296
10418
  continue;
10297
10419
  copy[key] = item;
10298
10420
  }
10299
10421
  return copy;
10300
10422
  }
10423
+ function computeLegacyProjectContextSourceHash(value) {
10424
+ if (!isRecord(value))
10425
+ return `sha256:${sha2562(stableStringify(value))}`;
10426
+ const copy = {};
10427
+ for (const [key, item] of Object.entries(value)) {
10428
+ if (key === "hash")
10429
+ continue;
10430
+ copy[key] = item;
10431
+ }
10432
+ return `sha256:${sha2562(stableStringify(copy))}`;
10433
+ }
10301
10434
  function sha2562(content) {
10302
10435
  return createHash2("sha256").update(content).digest("hex");
10303
10436
  }
10304
10437
  function isRecord(value) {
10305
10438
  return !!value && typeof value === "object" && !Array.isArray(value);
10306
10439
  }
10307
- 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, 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, commandSchema, projectContextBundleSchema, storedManifestProjectContextSchema, storedManifestFileSchema, storedManifestObservationSchema, projectContextMetadataSnapshotSchema, projectContextCacheSchema, ProjectContextError, ProjectContextHashRace, anchoredFsOps, atomicExchange, atomicExchangeLibraries;
10308
10441
  var init_project_context = __esm(() => {
10309
10442
  init_zod();
10310
10443
  init_redact();
10311
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);
10312
10447
  PROJECT_CONTEXT_MAX_INPUT_BYTES = 8 * 1024;
10313
10448
  PROJECT_CONTEXT_MAX_RENDERED_BYTES = 4 * 1024;
10314
10449
  SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES = 8 * 1024 * 1024;
@@ -10356,7 +10491,7 @@ var init_project_context = __esm(() => {
10356
10491
  argv: exports_external.array(commandArg).min(1).max(8)
10357
10492
  }).strict();
10358
10493
  projectContextBundleSchema = exports_external.object({
10359
- schema: exports_external.literal(PROJECT_CONTEXT_SCHEMA),
10494
+ schema: projectContextSchema,
10360
10495
  generated_at: isoTimestamp,
10361
10496
  hash: hashSchema,
10362
10497
  revision: revisionSchema,
@@ -10404,7 +10539,7 @@ var init_project_context = __esm(() => {
10404
10539
  commands: exports_external.array(commandSchema).max(PROJECT_CONTEXT_MAX_COMMANDS)
10405
10540
  }).strict();
10406
10541
  storedManifestProjectContextSchema = exports_external.object({
10407
- schema: exports_external.literal(PROJECT_CONTEXT_SCHEMA),
10542
+ schema: projectContextSchema,
10408
10543
  projectId: safeId,
10409
10544
  revision: revisionSchema,
10410
10545
  hash: hashSchema,
@@ -10444,6 +10579,25 @@ var init_project_context = __esm(() => {
10444
10579
  });
10445
10580
  }
10446
10581
  });
10582
+ projectContextMetadataSnapshotSchema = exports_external.object({
10583
+ schema: exports_external.literal("hasna.configs.session-render-snapshot/v1"),
10584
+ kind: exports_external.literal("project-context-metadata"),
10585
+ createdAt: isoTimestamp,
10586
+ projectId: safeId,
10587
+ revision: revisionSchema,
10588
+ hash: hashSchema,
10589
+ status: exports_external.enum(["fresh", "stale-source", "stale-cache"]),
10590
+ files: exports_external.array(exports_external.object({
10591
+ relativePath: exports_external.enum([
10592
+ PROJECT_CONTEXT_FRAGMENT_PATH,
10593
+ "CLAUDE.md",
10594
+ ".codewith/CODEWITH.md",
10595
+ "AGENTS.md"
10596
+ ]),
10597
+ role: exports_external.enum(["fragment", "index"]),
10598
+ sha256: exports_external.string().regex(/^[a-f0-9]{64}$/)
10599
+ }).strict()).min(1).max(2)
10600
+ }).strict();
10447
10601
  projectContextCacheSchema = exports_external.object({
10448
10602
  schema: exports_external.literal(PROJECT_CONTEXT_CACHE_SCHEMA),
10449
10603
  cached_at: isoTimestamp,
@@ -10656,10 +10810,13 @@ function sourceFingerprint(source) {
10656
10810
  metadata: canonicalFingerprintValue(source.metadata ?? null)
10657
10811
  };
10658
10812
  }
10659
- function slug(value) {
10813
+ function normalizeSessionInstructionSourceId(value) {
10660
10814
  const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10661
10815
  return s || "instruction";
10662
10816
  }
10817
+ function slug(value) {
10818
+ return normalizeSessionInstructionSourceId(value);
10819
+ }
10663
10820
  function yamlQuote2(value) {
10664
10821
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
10665
10822
  }
@@ -10778,12 +10935,17 @@ function normalizeSources(sources, tool, allowEmptySources) {
10778
10935
  }
10779
10936
  return normalized2;
10780
10937
  });
10938
+ const originalOrder = [...normalized].sort(compareSessionInstructionSources);
10781
10939
  const deduplicated = deduplicateSemanticPolicySources(normalized);
10782
- 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));
10940
+ const ordered = deduplicated.selected.sort(compareSessionInstructionSources);
10941
+ validateTargetedReplacementSources(originalOrder, ordered, deduplicated.skipped);
10783
10942
  rejectDuplicateSourceSlugs(ordered);
10784
10943
  rejectDuplicateRulePaths(ordered);
10785
10944
  return { sources: ordered, skipped: deduplicated.skipped };
10786
10945
  }
10946
+ function compareSessionInstructionSources(a, b) {
10947
+ return SESSION_LAYER_RANK[a.resolvedLayer] - SESSION_LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id);
10948
+ }
10787
10949
  function semanticPolicyIntegrity(body) {
10788
10950
  return sha2563(body) === AGENT_OPERATING_RULES_PAYLOAD_SHA256 ? "pinned-digest" : "unverified-self-declared";
10789
10951
  }
@@ -10910,7 +11072,52 @@ function filterProviderOnlyBlocks(content, tool) {
10910
11072
  return output.join(`
10911
11073
  `);
10912
11074
  }
10913
- function composeSources(sources) {
11075
+ function targetedReplacementTarget(source) {
11076
+ if (source.replacementScope == null)
11077
+ return null;
11078
+ if (source.resolvedMerge !== "replace") {
11079
+ throw new Error(`Session instruction source "${source.id}" uses replacement scope "${source.replacementScope}" ` + `with merge=${source.resolvedMerge}; replacement scopes require merge=replace, not append.`);
11080
+ }
11081
+ const scope = source.replacementScope.trim();
11082
+ if (!scope.startsWith("source:")) {
11083
+ throw new Error(`Invalid replacement scope "${source.replacementScope}" for source "${source.id}"; ` + 'expected "source:<normalized-source-id>".');
11084
+ }
11085
+ const target = scope.slice("source:".length);
11086
+ if (!target || target !== slug(target)) {
11087
+ throw new Error(`Invalid replacement scope "${source.replacementScope}" for source "${source.id}"; ` + "the target must be a non-empty normalized source id.");
11088
+ }
11089
+ return target;
11090
+ }
11091
+ function validateTargetedReplacementSources(originalSources, selectedSources, deduplicatedSources) {
11092
+ for (let replacerIndex = 0;replacerIndex < originalSources.length; replacerIndex++) {
11093
+ const replacer = originalSources[replacerIndex];
11094
+ const targetNormalizedId = targetedReplacementTarget(replacer);
11095
+ if (targetNormalizedId === null)
11096
+ continue;
11097
+ const matches = originalSources.filter((source) => source.normalizedId === targetNormalizedId);
11098
+ if (matches.length === 0) {
11099
+ throw new Error(`Targeted replacement source "${replacer.id}" names missing source "${targetNormalizedId}".`);
11100
+ }
11101
+ if (matches.length > 1) {
11102
+ throw new Error(`Targeted replacement source "${replacer.id}" is ambiguous after normalization: ` + `"${targetNormalizedId}" matches ${matches.map((source) => `"${source.id}"`).join(", ")}.`);
11103
+ }
11104
+ const target = matches[0];
11105
+ const targetIndex = originalSources.indexOf(target);
11106
+ if (targetIndex >= replacerIndex) {
11107
+ throw new Error(`Targeted replacement source "${replacer.id}" must name an earlier source; ` + `"${target.id}" is later than or identical to the replacer.`);
11108
+ }
11109
+ if (target.nonOverridable) {
11110
+ throw new Error(`Targeted replacement source "${replacer.id}" cannot replace non-overridable source "${target.id}".`);
11111
+ }
11112
+ if (!selectedSources.some((source) => source.id === replacer.id)) {
11113
+ throw new Error(`Targeted replacement source "${replacer.id}" was removed by semantic-policy deduplication before composition.`);
11114
+ }
11115
+ if (deduplicatedSources.some((source) => source.id === target.id)) {
11116
+ throw new Error(`Targeted replacement source "${replacer.id}" cannot claim success because target "${target.id}" ` + "was removed by semantic-policy deduplication before composition.");
11117
+ }
11118
+ }
11119
+ }
11120
+ function composeBroadReplaceSources(sources) {
10914
11121
  let start = -1;
10915
11122
  for (let i = 0;i < sources.length; i++) {
10916
11123
  if (sources[i].resolvedMerge === "replace")
@@ -10924,6 +11131,53 @@ function composeSources(sources) {
10924
11131
  const skipped = earlier.filter((source) => !source.nonOverridable).map((source) => skippedSource(source, `superseded by "${replacer.id}": a replace-merge source discards earlier overridable instruction layers`));
10925
11132
  return { sources: [...protectedSources, ...sources.slice(start)], skipped };
10926
11133
  }
11134
+ function composeSources(sources) {
11135
+ const hasTargetedReplacement = sources.some((source) => source.replacementScope !== undefined);
11136
+ if (!hasTargetedReplacement)
11137
+ return composeBroadReplaceSources(sources);
11138
+ const selected = [];
11139
+ const skipped = [];
11140
+ for (const source of sources) {
11141
+ const targetNormalizedId = targetedReplacementTarget(source);
11142
+ if (targetNormalizedId !== null) {
11143
+ const targetIndex = selected.findIndex((candidate) => candidate.normalizedId === targetNormalizedId);
11144
+ if (targetIndex < 0) {
11145
+ throw new Error(`Targeted replacement source "${source.id}" cannot replace "${targetNormalizedId}": ` + "the earlier target was already removed.");
11146
+ }
11147
+ const target = selected[targetIndex];
11148
+ if (target.nonOverridable) {
11149
+ throw new Error(`Targeted replacement source "${source.id}" cannot replace non-overridable source "${target.id}".`);
11150
+ }
11151
+ selected.splice(targetIndex, 1);
11152
+ skipped.push(skippedSource(target, `superseded by "${source.id}": targeted replacement ${source.replacementScope} ` + `removed exactly source "${target.id}"`));
11153
+ selected.push({
11154
+ ...source,
11155
+ provenance: {
11156
+ ...source.provenance ?? {},
11157
+ targetedReplacement: {
11158
+ scope: source.replacementScope,
11159
+ targetSourceId: target.id,
11160
+ targetNormalizedSourceId: target.normalizedId
11161
+ }
11162
+ }
11163
+ });
11164
+ continue;
11165
+ }
11166
+ if (source.resolvedMerge === "replace") {
11167
+ const retained = selected.filter((candidate) => candidate.nonOverridable);
11168
+ for (const candidate of selected) {
11169
+ if (candidate.nonOverridable)
11170
+ continue;
11171
+ skipped.push(skippedSource(candidate, `superseded by "${source.id}": a replace-merge source discards earlier overridable instruction layers`));
11172
+ }
11173
+ selected.length = 0;
11174
+ selected.push(...retained, source);
11175
+ continue;
11176
+ }
11177
+ selected.push(source);
11178
+ }
11179
+ return { sources: selected, skipped };
11180
+ }
10927
11181
  function sectionForSource(source) {
10928
11182
  const parts = [
10929
11183
  `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
@@ -15764,7 +16018,7 @@ function parseSessionLayer(value) {
15764
16018
  return value;
15765
16019
  throw new Error(`Invalid source layer "${value}"`);
15766
16020
  }
15767
- function parseSessionSource(value, order, replaceIds) {
16021
+ function parseSessionSource(value, order) {
15768
16022
  const idx = value.indexOf("=");
15769
16023
  let id = idx > 0 ? value.slice(0, idx).trim() : "";
15770
16024
  const path = idx > 0 ? value.slice(idx + 1).trim() : value.trim();
@@ -15787,9 +16041,39 @@ function parseSessionSource(value, order, replaceIds) {
15787
16041
  id: resolvedId,
15788
16042
  label: id ? resolvedId : source.label ?? resolvedId,
15789
16043
  layer,
15790
- merge: replaceIds.has(resolvedId) ? "replace" : "append"
16044
+ merge: "append"
15791
16045
  };
15792
16046
  }
16047
+ function parseSessionSourceReplacement(value) {
16048
+ const trimmed = value.trim();
16049
+ const separator = trimmed.indexOf("=");
16050
+ const replacerId = (separator >= 0 ? trimmed.slice(0, separator) : trimmed).trim();
16051
+ if (!replacerId) {
16052
+ throw new Error(`Invalid --replace-source "${value}" (expected replacer-id or replacer-id=target-source-id)`);
16053
+ }
16054
+ if (separator < 0)
16055
+ return { replacerId };
16056
+ const targetId = trimmed.slice(separator + 1).trim();
16057
+ if (!targetId) {
16058
+ throw new Error(`Invalid --replace-source "${value}" (target source id is required after "=")`);
16059
+ }
16060
+ return {
16061
+ replacerId,
16062
+ replacementScope: `source:${normalizeSessionInstructionSourceId(targetId)}`
16063
+ };
16064
+ }
16065
+ function sessionSourceReplacements(values) {
16066
+ const replacements = new Map;
16067
+ for (const value of values) {
16068
+ const replacement = parseSessionSourceReplacement(value);
16069
+ const existing = replacements.get(replacement.replacerId);
16070
+ if (existing && existing.replacementScope !== replacement.replacementScope) {
16071
+ throw new Error(`Conflicting --replace-source values for "${replacement.replacerId}": ` + `${existing.replacementScope ?? "broad"} and ${replacement.replacementScope ?? "broad"}.`);
16072
+ }
16073
+ replacements.set(replacement.replacerId, replacement);
16074
+ }
16075
+ return replacements;
16076
+ }
15793
16077
  function readSessionInstructionSourceFile(path) {
15794
16078
  const stat = lstatSync4(path);
15795
16079
  if (stat.isSymbolicLink()) {
@@ -15820,8 +16104,8 @@ function parseLayeredReference(value) {
15820
16104
  return { id: trimmed };
15821
16105
  }
15822
16106
  async function collectSessionSources(opts, tool, store) {
15823
- const replaceIds = new Set(opts.replaceSource ?? []);
15824
- const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index, replaceIds));
16107
+ const replacements = sessionSourceReplacements(opts.replaceSource ?? []);
16108
+ const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index));
15825
16109
  for (const value of opts.config ?? []) {
15826
16110
  const { layer, id } = parseLayeredReference(value);
15827
16111
  sources.push(sourceFromConfig(await store.getConfig(id), sources.length, layer));
@@ -15833,7 +16117,16 @@ async function collectSessionSources(opts, tool, store) {
15833
16117
  const parsed = JSON.parse(readFileSync12(path, "utf-8"));
15834
16118
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
15835
16119
  }
15836
- return sources.map((source) => replaceIds.has(source.id) ? { ...source, merge: "replace" } : source);
16120
+ return sources.map((source) => {
16121
+ const replacement = replacements.get(source.id);
16122
+ if (!replacement)
16123
+ return source;
16124
+ return {
16125
+ ...source,
16126
+ merge: "replace",
16127
+ replacementScope: replacement.replacementScope
16128
+ };
16129
+ });
15837
16130
  }
15838
16131
  async function checkGlobalSourceCoverage(plan, store) {
15839
16132
  const registryConfigs = await store.listConfigs({});
@@ -16576,7 +16869,7 @@ projectContextCmd.command("apply").description("Atomically write project context
16576
16869
  }
16577
16870
  });
16578
16871
  var sessionCmd = program.command("session").description("Plan and apply session-scoped agent instruction files");
16579
- 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) => {
16872
+ 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) => {
16580
16873
  try {
16581
16874
  const tool = opts.tool;
16582
16875
  if (!SESSION_RENDER_TOOLS.includes(tool)) {
@@ -16632,7 +16925,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
16632
16925
  process.exit(1);
16633
16926
  }
16634
16927
  });
16635
- 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) => {
16928
+ 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) => {
16636
16929
  try {
16637
16930
  const tool = opts.tool;
16638
16931
  if (!SESSION_RENDER_TOOLS.includes(tool)) {
@@ -17325,7 +17618,7 @@ program.command("watch").description("Watch known config files for changes and a
17325
17618
  setInterval(tick, interval);
17326
17619
  await new Promise(() => {});
17327
17620
  });
17328
- program.command("report").description("Summary of stored configs, drift, and ecosystem health").option("--json", "output as JSON").option("--markdown", "output as markdown").action(async () => {
17621
+ 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) => {
17329
17622
  const store = resolveConfigStore();
17330
17623
  const stats = await store.getConfigStats();
17331
17624
  const allConfigs = await store.listConfigs();
@@ -17351,6 +17644,32 @@ program.command("report").description("Summary of stored configs, drift, and eco
17351
17644
  for (const c of allConfigs)
17352
17645
  byAgent[c.agent] = (byAgent[c.agent] || 0) + 1;
17353
17646
  const projectConfigs = allConfigs.filter((c) => c.target_path && !c.target_path.startsWith("~/."));
17647
+ if (opts.json) {
17648
+ printJson({
17649
+ schema_version: 1,
17650
+ configs: {
17651
+ total: allConfigs.length,
17652
+ files: fileConfigs.length,
17653
+ references: refConfigs.length,
17654
+ templates: templates.length,
17655
+ project: projectConfigs.length
17656
+ },
17657
+ profiles: {
17658
+ total: profiles.length
17659
+ },
17660
+ drift: {
17661
+ drifted,
17662
+ missing
17663
+ },
17664
+ secrets: {
17665
+ findings: 0,
17666
+ policy: "redacted_on_ingest"
17667
+ },
17668
+ by_agent: byAgent,
17669
+ by_category: Object.fromEntries(Object.entries(stats).filter(([key]) => key !== "total").map(([key, value]) => [key, Number(value)]))
17670
+ });
17671
+ return;
17672
+ }
17354
17673
  console.log(chalk.bold(`configs report
17355
17674
  `));
17356
17675
  console.log(` Total: ${allConfigs.length} configs (${fileConfigs.length} files, ${refConfigs.length} references)`);