@hasna/instructions 0.4.3 → 0.4.5

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/index.js CHANGED
@@ -14,11 +14,11 @@ var CONFIG_CATEGORIES = [
14
14
  var CONFIG_AGENTS = [
15
15
  "claude",
16
16
  "codex",
17
- "gemini",
18
17
  "opencode",
19
18
  "cursor",
20
19
  "codewith",
21
20
  "aicopilot",
21
+ "antigravity",
22
22
  "zsh",
23
23
  "git",
24
24
  "npm",
@@ -1067,1497 +1067,1637 @@ function resolveConfigStore(env = process.env) {
1067
1067
  return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
1068
1068
  }
1069
1069
  // src/status.ts
1070
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1070
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1071
1071
 
1072
1072
  // src/lib/apply.ts
1073
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, realpathSync, writeFileSync } from "fs";
1074
- import { basename as basename2, dirname, resolve } from "path";
1075
- import { homedir as homedir2 } from "os";
1073
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, writeFileSync } from "fs";
1074
+ import { basename as basename3, dirname as dirname2, resolve as resolve2 } from "path";
1075
+ import { homedir as homedir3 } from "os";
1076
1076
 
1077
- // src/lib/transforms.ts
1078
- import { basename, extname } from "path";
1077
+ // src/lib/config-agents.ts
1078
+ var DEPRECATED_CONFIG_AGENTS = ["gemini"];
1079
+ var ACTIVE_CONFIG_AGENT_SET = new Set(CONFIG_AGENTS);
1080
+ var DEPRECATED_CONFIG_AGENT_SET = new Set(DEPRECATED_CONFIG_AGENTS);
1081
+ function isDeprecatedConfigAgent(agent) {
1082
+ return !!agent && DEPRECATED_CONFIG_AGENT_SET.has(agent);
1083
+ }
1084
+ function isSupportedConfigAgent(agent) {
1085
+ return !!agent && ACTIVE_CONFIG_AGENT_SET.has(agent) && !isDeprecatedConfigAgent(agent);
1086
+ }
1087
+ function isRetiredOrUnsupportedConfigAgent(agent) {
1088
+ return !!agent && !isSupportedConfigAgent(agent);
1089
+ }
1090
+ function retiredOrUnsupportedAgentReason(agent) {
1091
+ if (!agent)
1092
+ return "missing agent";
1093
+ return isDeprecatedConfigAgent(agent) ? `deprecated agent: ${agent}` : `unsupported agent: ${agent}`;
1094
+ }
1095
+
1096
+ // src/lib/session-render.ts
1097
+ import { createHash } from "crypto";
1098
+ import { existsSync as existsSync3, readFileSync, realpathSync, statSync } from "fs";
1099
+ import { homedir as homedir2 } from "os";
1100
+ import { basename, dirname, extname, isAbsolute, join as join3, parse, posix, relative, resolve } from "path";
1101
+ var CODEWITH_NATIVE_IMPORTS_ENV = "HASNA_CONFIGS_CODEWITH_NATIVE_IMPORTS";
1102
+ var SESSION_RENDER_MANAGED_MARKER = "Managed by @hasna/configs session render";
1103
+ var SESSION_RENDER_SCHEMA = "hasna.configs.session-render/v1";
1104
+ var RAW_STORE_ROOT_ENV = "HASNA_CONFIGS_HOME";
1105
+ var ANTIGRAVITY_RULE_FILE_CHAR_LIMIT = 12000;
1106
+ var SESSION_RENDER_TOOLS = [
1107
+ "claude",
1108
+ "codex",
1109
+ "cursor",
1110
+ "opencode",
1111
+ "codewith",
1112
+ "aicopilot",
1113
+ "antigravity"
1114
+ ];
1115
+ var SESSION_INSTRUCTION_LAYERS = [
1116
+ "global",
1117
+ "tool",
1118
+ "account",
1119
+ "machine",
1120
+ "division",
1121
+ "workspace",
1122
+ "repo",
1123
+ "path",
1124
+ "agent",
1125
+ "session",
1126
+ "local"
1127
+ ];
1128
+ var CODEWITH_FLATTENED_ADAPTER = {
1129
+ tool: "codewith",
1130
+ mode: "flattened-markdown",
1131
+ indexFile: "CODEWITH.md",
1132
+ managedDir: ".hasna/instructions",
1133
+ envVar: "CODEWITH_HOME",
1134
+ nativeImports: false,
1135
+ description: "Codewith CODEWITH.md flattened until native @ imports are implemented in Codewith."
1136
+ };
1137
+ var CODEWITH_NATIVE_ADAPTER = {
1138
+ tool: "codewith",
1139
+ mode: "native-imports",
1140
+ indexFile: "CODEWITH.md",
1141
+ managedDir: ".hasna/instructions",
1142
+ envVar: "CODEWITH_HOME",
1143
+ nativeImports: true,
1144
+ description: "Codewith CODEWITH.md with gated @ imports into managed fragments."
1145
+ };
1146
+ var SESSION_TOOL_ADAPTERS = {
1147
+ claude: {
1148
+ tool: "claude",
1149
+ mode: "native-imports",
1150
+ indexFile: "CLAUDE.md",
1151
+ managedDir: ".hasna/instructions",
1152
+ envVar: "CLAUDE_CONFIG_DIR",
1153
+ nativeImports: true,
1154
+ description: "Claude Code CLAUDE.md with @ imports into managed fragments."
1155
+ },
1156
+ codex: {
1157
+ tool: "codex",
1158
+ mode: "flattened-markdown",
1159
+ indexFile: "AGENTS.md",
1160
+ managedDir: ".hasna/instructions",
1161
+ envVar: "CODEX_HOME",
1162
+ nativeImports: false,
1163
+ description: "Codex AGENTS.md flattened instruction file."
1164
+ },
1165
+ cursor: {
1166
+ tool: "cursor",
1167
+ mode: "cursor-mdc",
1168
+ managedDir: ".cursor/rules",
1169
+ nativeImports: false,
1170
+ description: "Cursor project rule files in .cursor/rules/*.mdc."
1171
+ },
1172
+ opencode: {
1173
+ tool: "opencode",
1174
+ mode: "opencode-instructions",
1175
+ indexFile: "AGENTS.md",
1176
+ configFile: "opencode.json",
1177
+ managedDir: ".hasna/instructions",
1178
+ envVar: "OPENCODE_CONFIG_DIR",
1179
+ nativeImports: false,
1180
+ description: "OpenCode AGENTS.md plus opencode.json instructions pointing at managed fragments."
1181
+ },
1182
+ aicopilot: {
1183
+ tool: "aicopilot",
1184
+ mode: "flattened-markdown",
1185
+ indexFile: "AICOPILOT.md",
1186
+ managedDir: ".hasna/instructions",
1187
+ envVar: "AICOPILOT_CONFIG_DIR",
1188
+ nativeImports: false,
1189
+ description: "AI Copilot AICOPILOT.md flattened instruction file."
1190
+ },
1191
+ antigravity: {
1192
+ tool: "antigravity",
1193
+ mode: "antigravity-rules",
1194
+ managedDir: ".agents/rules",
1195
+ nativeImports: false,
1196
+ description: "Google Antigravity project rules in .agents/rules/*.md."
1197
+ },
1198
+ codewith: CODEWITH_FLATTENED_ADAPTER
1199
+ };
1200
+ var SESSION_LAYER_RANK = {
1201
+ global: 10,
1202
+ tool: 20,
1203
+ account: 30,
1204
+ machine: 40,
1205
+ division: 50,
1206
+ workspace: 60,
1207
+ repo: 70,
1208
+ path: 80,
1209
+ agent: 90,
1210
+ session: 100,
1211
+ local: 110
1212
+ };
1213
+ function normalizeSessionInstructionLayer(value) {
1214
+ if (value === "provider")
1215
+ return "tool";
1216
+ if (value === "identity")
1217
+ return "agent";
1218
+ if (value === "project")
1219
+ return "repo";
1220
+ if (value === "global" || value === "tool" || value === "account" || value === "machine" || value === "division" || value === "workspace" || value === "repo" || value === "path" || value === "agent" || value === "session" || value === "local")
1221
+ return value;
1222
+ throw new Error(`Invalid session instruction layer: ${String(value)}`);
1223
+ }
1079
1224
  function ensureTrailingNewline(content) {
1080
1225
  return content.endsWith(`
1081
1226
  `) ? content : `${content}
1082
1227
  `;
1083
1228
  }
1229
+ function sha256(content) {
1230
+ return createHash("sha256").update(content).digest("hex");
1231
+ }
1232
+ function fingerprint(value) {
1233
+ return sha256(JSON.stringify(value));
1234
+ }
1235
+ function slug(value) {
1236
+ const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1237
+ return s || "instruction";
1238
+ }
1084
1239
  function yamlQuote(value) {
1085
1240
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
1086
1241
  }
1087
- function headingLevel(line) {
1088
- const m = line.match(/^(#{1,6})\s+/);
1089
- return m ? m[1].length : null;
1242
+ function getRawStoreRoot() {
1243
+ return resolve(process.env[RAW_STORE_ROOT_ENV] || join3(process.env["HOME"] || homedir2(), ".hasna", "configs"));
1090
1244
  }
1091
- function stripClaudeOnlySections(content) {
1092
- const withoutMarkedBlocks = content.replace(/<!--\s*claude-only:start\s*-->[\s\S]*?<!--\s*claude-only:end\s*-->/gi, "");
1093
- const lines = withoutMarkedBlocks.split(`
1094
- `);
1095
- const kept = [];
1096
- let droppingUntilLevel = null;
1097
- for (const line of lines) {
1098
- const level = headingLevel(line);
1099
- if (level !== null && droppingUntilLevel !== null && level <= droppingUntilLevel) {
1100
- droppingUntilLevel = null;
1245
+ function defaultTargetHome(tool, profile, sessionId) {
1246
+ return join3(getRawStoreRoot(), "sessions", tool, slug(profile), slug(sessionId || "latest"));
1247
+ }
1248
+ function joinTarget(targetHome, relativePath) {
1249
+ const safeTargetHome = assertSafeTargetRoot(targetHome);
1250
+ const safeRelativePath = assertSafeRelativePath(relativePath);
1251
+ return join3(safeTargetHome, ...safeRelativePath.split("/"));
1252
+ }
1253
+ function makeFile(targetHome, relativePath, role, content, sourceIds) {
1254
+ const safeTargetHome = assertSafeTargetRoot(targetHome);
1255
+ const safeRelativePath = assertSafeRelativePath(relativePath);
1256
+ const normalizedContent = ensureTrailingNewline(content);
1257
+ return {
1258
+ path: joinTarget(safeTargetHome, safeRelativePath),
1259
+ relativePath: safeRelativePath,
1260
+ role,
1261
+ content: normalizedContent,
1262
+ sha256: sha256(normalizedContent),
1263
+ sourceIds
1264
+ };
1265
+ }
1266
+ function normalizeSources(sources, tool, allowEmptySources) {
1267
+ const ordered = sources.map((source, index) => {
1268
+ if (!source.id.trim())
1269
+ throw new Error("Session instruction source id is required.");
1270
+ const content = filterProviderOnlyBlocks(source.content ?? "", tool);
1271
+ const normalized = {
1272
+ ...source,
1273
+ content,
1274
+ normalizedId: slug(source.id),
1275
+ resolvedLabel: source.label ?? source.id,
1276
+ resolvedLayer: source.layer === undefined ? "agent" : normalizeSessionInstructionLayer(source.layer),
1277
+ resolvedMerge: source.merge ?? "append",
1278
+ resolvedOrder: source.order ?? index,
1279
+ resolvedRules: normalizeInstructionRules(source, tool)
1280
+ };
1281
+ const hasPathReferences = (normalized.sourcePaths ?? []).length > 0;
1282
+ if (!allowEmptySources && !normalized.content.trim() && normalized.resolvedRules.length === 0 && !hasPathReferences) {
1283
+ throw new Error(`Session instruction source "${source.id}" is empty. Pass --allow-empty-sources only for explicit empty renders.`);
1101
1284
  }
1102
- if (droppingUntilLevel !== null)
1285
+ return normalized;
1286
+ }).sort((a, b) => SESSION_LAYER_RANK[a.resolvedLayer] - SESSION_LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id));
1287
+ rejectDuplicateSourceSlugs(ordered);
1288
+ rejectDuplicateRulePaths(ordered);
1289
+ return ordered;
1290
+ }
1291
+ function filterProviderOnlyBlocks(content, tool) {
1292
+ const lines = content.split(/\r?\n/);
1293
+ const output = [];
1294
+ let activeProviders = null;
1295
+ for (const line of lines) {
1296
+ const start = line.match(/^\s*<!--\s*@hasna-provider:\s*([^>]+?)\s*-->\s*$/i);
1297
+ if (start) {
1298
+ if (activeProviders)
1299
+ throw new Error("Nested provider-only instruction blocks are not supported.");
1300
+ activeProviders = start[1].split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
1103
1301
  continue;
1104
- if (/^#{1,6}\s+.*claude(?:\s+code)?[-\s]+only\b/i.test(line)) {
1105
- droppingUntilLevel = level ?? 1;
1302
+ }
1303
+ if (/^\s*<!--\s*@hasna-end-provider\s*-->\s*$/i.test(line)) {
1304
+ if (!activeProviders)
1305
+ throw new Error("Provider-only instruction block end marker without start marker.");
1306
+ activeProviders = null;
1106
1307
  continue;
1107
1308
  }
1108
- kept.push(line);
1309
+ if (!activeProviders || activeProviders.includes(tool) || activeProviders.includes("all") || activeProviders.includes("generic")) {
1310
+ output.push(line);
1311
+ }
1109
1312
  }
1110
- return kept.join(`
1111
- `).replace(/\n{3,}/g, `
1112
-
1113
- `).trim();
1114
- }
1115
- function isClaudeRuleConfig(source, candidate) {
1116
- if (candidate.id === source.id)
1117
- return false;
1118
- if (candidate.agent !== "claude" || candidate.category !== "rules")
1119
- return false;
1120
- return !!candidate.target_path?.includes("/rules/");
1121
- }
1122
- function ruleLabel(config) {
1123
- const file = config.target_path ? basename(config.target_path) : config.name;
1124
- return file.replace(/\.(md|mdc|markdown)$/i, "");
1313
+ if (activeProviders)
1314
+ throw new Error("Provider-only instruction block was not closed.");
1315
+ return output.join(`
1316
+ `);
1125
1317
  }
1126
- function claudeRules(source, context) {
1127
- return (context.configs ?? []).filter((config) => isClaudeRuleConfig(source, config)).sort((a, b) => (a.target_path ?? a.name).localeCompare(b.target_path ?? b.name));
1318
+ function composeSources(sources) {
1319
+ let start = -1;
1320
+ for (let i = 0;i < sources.length; i++) {
1321
+ if (sources[i].resolvedMerge === "replace")
1322
+ start = i;
1323
+ }
1324
+ if (start < 0)
1325
+ return sources;
1326
+ const protectedSources = sources.slice(0, start).filter((source) => source.nonOverridable);
1327
+ return [...protectedSources, ...sources.slice(start)];
1128
1328
  }
1129
- function flattenWithRules(source, context) {
1130
- const parts = [stripClaudeOnlySections(source.content)];
1131
- const rules = claudeRules(source, context).map((rule) => ({ rule, content: stripClaudeOnlySections(rule.content) })).filter(({ content }) => Boolean(content)).map(({ rule, content }) => `### ${ruleLabel(rule)}
1132
-
1133
- ${content}`);
1134
- if (rules.length > 0) {
1135
- parts.push(`## Rules
1136
-
1137
- ${rules.join(`
1138
-
1139
- `)}`);
1329
+ function sectionForSource(source) {
1330
+ const parts = [
1331
+ `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1332
+ `# ${source.resolvedLabel}`
1333
+ ];
1334
+ if (source.path)
1335
+ parts.push(`Source: ${source.path}`);
1336
+ if (source.sourcePaths && source.sourcePaths.length > 0) {
1337
+ parts.push([
1338
+ "Source paths:",
1339
+ ...source.sourcePaths.map((sourcePath) => {
1340
+ const flags = [
1341
+ sourcePath.editable ? "editable" : null,
1342
+ sourcePath.required ? "required" : null,
1343
+ sourcePath.hash ? sourcePath.hash : null
1344
+ ].filter(Boolean);
1345
+ return `- ${sourcePath.path}${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
1346
+ })
1347
+ ].join(`
1348
+ `));
1140
1349
  }
1141
- return ensureTrailingNewline(parts.filter(Boolean).join(`
1350
+ if (source.owner)
1351
+ parts.push(`Owner: ${source.owner.kind}:${source.owner.id}`);
1352
+ const content = source.content.trim();
1353
+ if (content)
1354
+ parts.push(content);
1355
+ return parts.join(`
1142
1356
 
1143
- `));
1144
- }
1145
- function buildCodexAgentsMd(source, context = {}) {
1146
- return flattenWithRules(source, context);
1357
+ `);
1147
1358
  }
1148
- function buildOpenCodeAgentsMd(source, context = {}) {
1149
- return flattenWithRules(source, context);
1359
+ function sectionForRule(source, rule) {
1360
+ const parts = [
1361
+ `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1362
+ `# ${rule.resolvedLabel}`
1363
+ ];
1364
+ if (source.path)
1365
+ parts.push(`Source: ${source.path}`);
1366
+ if (rule.path)
1367
+ parts.push(`Rule path: ${rule.path}`);
1368
+ const content = rule.content.trim();
1369
+ if (content)
1370
+ parts.push(content);
1371
+ return parts.join(`
1372
+
1373
+ `);
1150
1374
  }
1151
- function buildCursorMdc(source) {
1152
- const stem = source.target_path ? basename(source.target_path, extname(source.target_path)) : source.slug;
1153
- const description = source.name || stem;
1154
- return ensureTrailingNewline([
1155
- "---",
1156
- `description: ${yamlQuote(description)}`,
1157
- 'globs: ["**/*"]',
1158
- "alwaysApply: true",
1159
- "---",
1375
+ function fragmentPath(adapter, index, source) {
1376
+ const n = String(index + 1).padStart(2, "0");
1377
+ return posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
1378
+ }
1379
+ function ruleFragmentPath(adapter, source, rule) {
1380
+ return posix.join(adapter.managedDir, "rules", source.normalizedId, rule.resolvedPath);
1381
+ }
1382
+ function importPath(indexRelativePath, fragmentRelativePath) {
1383
+ const relative2 = posix.relative(posix.dirname(indexRelativePath), fragmentRelativePath);
1384
+ if (relative2.startsWith("./") || relative2.startsWith("../"))
1385
+ return relative2;
1386
+ return `./${relative2}`;
1387
+ }
1388
+ function indexHeader(tool, profile) {
1389
+ return [
1390
+ `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1391
+ `# ${tool} session instructions`,
1160
1392
  "",
1161
- stripClaudeOnlySections(source.content)
1393
+ `Profile: ${profile}`
1162
1394
  ].join(`
1163
- `));
1395
+ `);
1164
1396
  }
1165
- function transformSkillContent(content) {
1166
- return ensureTrailingNewline(content.split(`
1167
- `).filter((line) => !/^\s*user_invocable\s*:/i.test(line)).join(`
1168
- `).replace(/\bAgent tool\b/g, "delegate through the available MCP or agent orchestration tools").replace(/\bTodoWrite\b/g, "track tasks with the agent's native task mechanism").replace(/\bRead\/Edit\b/g, "read and edit files with the available filesystem tools").replace(/\bRead\b/g, "read files with the available filesystem tools").replace(/\bEdit\b/g, "edit files with the available filesystem tools").replace(/\bBash\b/g, "run shell commands with the available terminal tool").trim());
1397
+ function buildNativeImportFiles(targetHome, adapter, profile, sources) {
1398
+ const indexFile = adapter.indexFile;
1399
+ const fragments = sources.flatMap((source, index2) => [
1400
+ makeFile(targetHome, fragmentPath(adapter, index2, source), "fragment", sectionForSource(source), [source.id]),
1401
+ ...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
1402
+ ]);
1403
+ const imports = fragments.map((file) => `@${importPath(indexFile, file.relativePath)}`);
1404
+ const index = makeFile(targetHome, indexFile, "index", [indexHeader(adapter.tool, profile), ...imports].join(`
1405
+ `), sources.map((source) => source.id));
1406
+ return [index, ...fragments];
1169
1407
  }
1170
- function applyTransform(source, output, context = {}) {
1171
- switch (output.transform) {
1172
- case "passthrough":
1173
- case "claude-passthrough":
1174
- return ensureTrailingNewline(source.content);
1175
- case "codex-flat":
1176
- return buildCodexAgentsMd(source, context);
1177
- case "opencode-flat":
1178
- return buildOpenCodeAgentsMd(source, context);
1408
+ function buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources) {
1409
+ const content = [
1410
+ indexHeader(adapter.tool, profile),
1411
+ ...sources.flatMap((source) => [
1412
+ sectionForSource(source),
1413
+ ...source.resolvedRules.map((rule) => sectionForRule(source, rule))
1414
+ ])
1415
+ ].join(`
1416
+
1417
+ `);
1418
+ return [
1419
+ makeFile(targetHome, adapter.indexFile, "index", content, [
1420
+ ...sources.map((source) => source.id),
1421
+ ...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
1422
+ ])
1423
+ ];
1424
+ }
1425
+ function buildCursorRuleFiles(targetHome, adapter, sources) {
1426
+ return sources.flatMap((source, index) => {
1427
+ const n = String(index + 1).padStart(2, "0");
1428
+ const stem = `${n}-${source.normalizedId}`;
1429
+ const relativePath = posix.join(adapter.managedDir, `${stem}.mdc`);
1430
+ const description = `${source.resolvedLabel} (${source.resolvedLayer})`;
1431
+ const content = [
1432
+ "---",
1433
+ `description: ${yamlQuote(description)}`,
1434
+ 'globs: ["**/*"]',
1435
+ "alwaysApply: true",
1436
+ "---",
1437
+ "",
1438
+ `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1439
+ "",
1440
+ source.content.trim()
1441
+ ].join(`
1442
+ `);
1443
+ const sourceFile = makeFile(targetHome, relativePath, "rule", content, [source.id]);
1444
+ const ruleFiles = source.resolvedRules.map((rule) => {
1445
+ const ruleStem = `${n}-${source.normalizedId}-${rule.normalizedId}`;
1446
+ const ruleRelativePath = posix.join(adapter.managedDir, `${ruleStem}.mdc`);
1447
+ const ruleDescription = `${rule.resolvedLabel} (${source.resolvedLayer})`;
1448
+ const ruleContent = [
1449
+ "---",
1450
+ `description: ${yamlQuote(ruleDescription)}`,
1451
+ `globs: ${JSON.stringify(rule.globs && rule.globs.length > 0 ? rule.globs : ["**/*"])}`,
1452
+ "alwaysApply: true",
1453
+ "---",
1454
+ "",
1455
+ `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1456
+ "",
1457
+ rule.content.trim()
1458
+ ].join(`
1459
+ `);
1460
+ return makeFile(targetHome, ruleRelativePath, "rule", ruleContent, [source.id, rule.id]);
1461
+ });
1462
+ return [sourceFile, ...ruleFiles];
1463
+ });
1464
+ }
1465
+ function buildOpenCodeFiles(targetHome, adapter, profile, sources) {
1466
+ const fragments = sources.flatMap((source, index) => [
1467
+ makeFile(targetHome, fragmentPath(adapter, index, source), "fragment", sectionForSource(source), [source.id]),
1468
+ ...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
1469
+ ]);
1470
+ const flattenedIndex = makeFile(targetHome, adapter.indexFile, "index", [
1471
+ indexHeader(adapter.tool, profile),
1472
+ ...sources.flatMap((source) => [
1473
+ sectionForSource(source),
1474
+ ...source.resolvedRules.map((rule) => sectionForRule(source, rule))
1475
+ ])
1476
+ ].join(`
1477
+
1478
+ `), [
1479
+ ...sources.map((source) => source.id),
1480
+ ...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
1481
+ ]);
1482
+ const config = {
1483
+ $schema: "https://opencode.ai/config.json",
1484
+ instructions: fragments.map((file) => file.relativePath)
1485
+ };
1486
+ return [
1487
+ flattenedIndex,
1488
+ makeFile(targetHome, adapter.configFile, "config", JSON.stringify(config, null, 2), sources.map((source) => source.id)),
1489
+ ...fragments
1490
+ ];
1491
+ }
1492
+ function buildAntigravityRuleFiles(targetHome, adapter, sources) {
1493
+ return sources.flatMap((source, index) => {
1494
+ const n = String(index + 1).padStart(2, "0");
1495
+ const sourcePath = posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
1496
+ const sourceFile = makeAntigravityRuleFile(targetHome, sourcePath, sectionForSource(source), [source.id]);
1497
+ const ruleFiles = source.resolvedRules.map((rule) => {
1498
+ const rulePath = posix.join(adapter.managedDir, `${n}-${source.normalizedId}-${rule.resolvedPath}`);
1499
+ return makeAntigravityRuleFile(targetHome, rulePath, sectionForRule(source, rule), [source.id, rule.id]);
1500
+ });
1501
+ return [sourceFile, ...ruleFiles];
1502
+ });
1503
+ }
1504
+ function makeAntigravityRuleFile(targetHome, relativePath, content, sourceIds) {
1505
+ const file = makeFile(targetHome, relativePath, "rule", content, sourceIds);
1506
+ if (file.content.length > ANTIGRAVITY_RULE_FILE_CHAR_LIMIT) {
1507
+ throw new Error(`Antigravity rule file ${file.relativePath} is ${file.content.length} characters; split it before rendering because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
1508
+ }
1509
+ return file;
1510
+ }
1511
+ function buildFiles(targetHome, adapter, profile, sources) {
1512
+ switch (adapter.mode) {
1513
+ case "native-imports":
1514
+ return buildNativeImportFiles(targetHome, adapter, profile, sources);
1515
+ case "flattened-markdown":
1516
+ return buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources);
1179
1517
  case "cursor-mdc":
1180
- return buildCursorMdc(source);
1181
- case "skill-neutral":
1182
- return transformSkillContent(source.content);
1518
+ return buildCursorRuleFiles(targetHome, adapter, sources);
1519
+ case "opencode-instructions":
1520
+ return buildOpenCodeFiles(targetHome, adapter, profile, sources);
1521
+ case "antigravity-rules":
1522
+ return buildAntigravityRuleFiles(targetHome, adapter, sources);
1183
1523
  }
1184
1524
  }
1185
-
1186
- // src/lib/apply.ts
1187
- function getConfigHome() {
1525
+ function adapterFor(input) {
1526
+ if (input.tool !== "codewith")
1527
+ return SESSION_TOOL_ADAPTERS[input.tool];
1528
+ const gatedNativeImports = input.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true";
1529
+ return gatedNativeImports ? CODEWITH_NATIVE_ADAPTER : CODEWITH_FLATTENED_ADAPTER;
1530
+ }
1531
+ function getHomeDir() {
1188
1532
  return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir2();
1189
1533
  }
1190
- function expandPath(p) {
1191
- if (p.startsWith("~/")) {
1192
- return resolve(getConfigHome(), p.slice(2));
1534
+ function cleanSessionPathInput(path) {
1535
+ const trimmed = path.trim();
1536
+ if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
1537
+ return trimmed.slice(1, -1);
1193
1538
  }
1194
- return resolve(p);
1539
+ return trimmed;
1195
1540
  }
1196
- function normalizeTargetPath(p) {
1197
- const expanded = expandPath(p);
1198
- try {
1199
- return realpathSync(expanded);
1200
- } catch {
1201
- let current = expanded;
1202
- const missingSegments = [];
1203
- while (true) {
1204
- if (existsSync3(current)) {
1205
- try {
1206
- return resolve(realpathSync(current), ...missingSegments);
1207
- } catch {
1208
- return expanded;
1209
- }
1210
- }
1211
- const parent = dirname(current);
1212
- const name = basename2(current);
1213
- if (parent === current)
1214
- return expanded;
1215
- missingSegments.unshift(name);
1216
- current = parent;
1217
- }
1541
+ function resolveSessionPath(path) {
1542
+ const cleaned = cleanSessionPathInput(path);
1543
+ if (!cleaned)
1544
+ throw new Error("Session render path cannot be empty.");
1545
+ const home = getHomeDir();
1546
+ if (cleaned === "~")
1547
+ return resolve(home);
1548
+ if (cleaned.startsWith("~/"))
1549
+ return resolve(home, cleaned.slice(2));
1550
+ if (cleaned === "{{HOME}}" || cleaned === "${HOME}")
1551
+ return resolve(home);
1552
+ if (cleaned.startsWith("{{HOME}}/"))
1553
+ return resolve(home, cleaned.slice("{{HOME}}/".length));
1554
+ if (cleaned.startsWith("${HOME}/"))
1555
+ return resolve(home, cleaned.slice("${HOME}/".length));
1556
+ return resolve(cleaned);
1557
+ }
1558
+ function assertSafeRelativePath(relativePath) {
1559
+ if (!relativePath.trim())
1560
+ throw new Error("Session render relative path cannot be empty.");
1561
+ if (relativePath.includes("\\"))
1562
+ throw new Error(`Session render relative path must use POSIX separators: ${relativePath}`);
1563
+ const normalized = posix.normalize(relativePath);
1564
+ if (normalized === "." || posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
1565
+ throw new Error(`Session render relative path escapes target root: ${relativePath}`);
1218
1566
  }
1567
+ return normalized;
1219
1568
  }
1220
- async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
1221
- const renderedTargetPath = opts.vars ? renderMachineAwareContent(targetPath, opts.vars) : targetPath;
1222
- const renderedContent = opts.vars ? renderMachineAwareContent(content, opts.vars) : content;
1223
- const path = expandPath(renderedTargetPath);
1224
- const previousContent = existsSync3(path) ? readFileSync(path, "utf-8") : null;
1225
- const changed = previousContent !== renderedContent;
1226
- if (!opts.dryRun) {
1227
- const dir = dirname(path);
1228
- if (!existsSync3(dir)) {
1229
- mkdirSync2(dir, { recursive: true });
1230
- }
1231
- if (previousContent !== null && changed) {
1232
- const store = opts.store ?? resolveConfigStore();
1233
- await store.createSnapshot(config.id, previousContent, config.version);
1569
+ function assertSafeTargetRoot(targetHome) {
1570
+ if (!isAbsolute(targetHome))
1571
+ throw new Error(`Session render target must be an absolute path: ${targetHome}`);
1572
+ const normalized = resolve(targetHome);
1573
+ if (normalized === parse(normalized).root) {
1574
+ throw new Error(`Session render target cannot be the filesystem root: ${targetHome}`);
1575
+ }
1576
+ return normalized;
1577
+ }
1578
+ function resolveRenderTarget(input) {
1579
+ if (input.tool === "cursor" || input.tool === "antigravity") {
1580
+ if (!input.projectRoot) {
1581
+ const label = input.tool === "cursor" ? "Cursor rules" : "Antigravity rules";
1582
+ const path = input.tool === "cursor" ? ".cursor/rules files" : ".agents/rules files";
1583
+ return {
1584
+ targetHome: defaultTargetHome(input.tool, input.profile, input.sessionId),
1585
+ targetKind: "blocked",
1586
+ blockers: [
1587
+ `${label} are project-scoped; pass --project-root (or projectRoot) before applying ${path}. --target-home is not treated as a repository root for ${input.tool}.`
1588
+ ]
1589
+ };
1234
1590
  }
1235
- writeFileSync(path, renderedContent, "utf-8");
1591
+ return {
1592
+ targetHome: resolveSessionPath(input.projectRoot),
1593
+ targetKind: "project-root",
1594
+ blockers: []
1595
+ };
1236
1596
  }
1237
1597
  return {
1238
- config_id: config.id,
1239
- path,
1240
- previous_content: previousContent,
1241
- new_content: renderedContent,
1242
- dry_run: opts.dryRun ?? false,
1243
- changed,
1244
- ...meta
1598
+ targetHome: input.targetHome ? resolveSessionPath(input.targetHome) : defaultTargetHome(input.tool, input.profile, input.sessionId),
1599
+ targetKind: "session-home",
1600
+ blockers: []
1245
1601
  };
1246
1602
  }
1247
- function isGeneratedOutputTarget(config, configs) {
1248
- if (!config.target_path)
1249
- return false;
1250
- const targetPath = normalizeTargetPath(config.target_path);
1251
- return configs.some((candidate) => candidate.id !== config.id && candidate.outputs.some((output) => normalizeTargetPath(output.target_path) === targetPath));
1252
- }
1253
- async function applyConfig(config, opts = {}) {
1254
- const selectedOutputs = opts.outputAgent ? config.outputs.filter((output) => output.agent === opts.outputAgent) : config.outputs;
1255
- const shouldApplyPrimary = !opts.outputAgent || config.agent === opts.outputAgent;
1256
- if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
1257
- throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
1258
- }
1259
- const store = opts.store ?? resolveConfigStore();
1260
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
1261
- if (isGeneratedOutputTarget(config, contextConfigs)) {
1262
- throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
1263
- }
1264
- const outputResults = [];
1265
- for (const output of selectedOutputs) {
1266
- outputResults.push(await applyOutput(config, output, contextConfigs, opts));
1267
- }
1268
- let result;
1269
- if (config.target_path && shouldApplyPrimary) {
1270
- result = await writeConfigResult(config, config.target_path, config.content, opts);
1271
- result.outputs = outputResults;
1272
- result.changed = result.changed || outputResults.some((output) => output.changed);
1273
- } else {
1274
- result = {
1275
- ...outputResults[0],
1276
- outputs: outputResults.slice(1),
1277
- changed: outputResults.some((output) => output.changed)
1603
+ function resolveSessionTargetOwnership(input, target) {
1604
+ if (target.targetKind === "blocked") {
1605
+ return {
1606
+ kind: "blocked",
1607
+ tool: input.tool,
1608
+ profile: input.profile,
1609
+ targetHome: target.targetHome,
1610
+ projectRoot: input.projectRoot ? resolveSessionPath(input.projectRoot) : null,
1611
+ ownedBy: "open-configs",
1612
+ reason: "target resolution blocked before provider files can be owned"
1278
1613
  };
1279
1614
  }
1280
- if (!opts.dryRun) {
1281
- await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
1282
- }
1283
- return result;
1284
- }
1285
- async function applyOutput(config, output, contextConfigs, opts) {
1286
- const content = applyTransform(config, output, { configs: contextConfigs });
1287
- return writeConfigResult(config, output.target_path, content, opts, {
1288
- agent: output.agent,
1289
- transform: output.transform
1290
- });
1291
- }
1292
- async function applyConfigs(configs, opts = {}) {
1293
- const results = [];
1294
- for (const config of configs) {
1295
- if (config.kind === "reference")
1296
- continue;
1297
- results.push(await applyConfig(config, opts));
1298
- }
1299
- return results;
1300
- }
1301
-
1302
- // src/lib/redact.ts
1303
- var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
1304
- var VALUE_PATTERNS = [
1305
- { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
1306
- { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
1307
- { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
1308
- { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
1309
- { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
1310
- { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
1311
- { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
1312
- { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
1313
- ];
1314
- var MIN_SECRET_VALUE_LEN = 8;
1315
- function redactShell(content) {
1316
- const redacted = [];
1317
- const lines = content.split(`
1318
- `);
1319
- const out = [];
1320
- for (let i = 0;i < lines.length; i++) {
1321
- const line = lines[i];
1322
- const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
1323
- if (m) {
1324
- const [, prefix, key, eq, quote, value] = m;
1325
- if (shouldRedactKeyValue(key, value)) {
1326
- const reason = reasonFor(key, value);
1327
- redacted.push({ varName: key, line: i + 1, reason });
1328
- out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
1329
- continue;
1330
- }
1331
- }
1332
- out.push(line);
1615
+ if (target.targetKind === "project-root") {
1616
+ return {
1617
+ kind: "project",
1618
+ tool: input.tool,
1619
+ profile: input.profile,
1620
+ targetHome: target.targetHome,
1621
+ projectRoot: target.targetHome,
1622
+ ownedBy: "open-configs",
1623
+ reason: "project-scoped provider files are generated in the explicit repository root"
1624
+ };
1333
1625
  }
1334
- return { content: out.join(`
1335
- `), redacted, isTemplate: redacted.length > 0 };
1626
+ return {
1627
+ kind: "provider-profile",
1628
+ tool: input.tool,
1629
+ profile: input.profile,
1630
+ targetHome: target.targetHome,
1631
+ projectRoot: null,
1632
+ ownedBy: "open-configs",
1633
+ reason: "profile-scoped provider home is generated by OpenConfigs from identity/config sources"
1634
+ };
1336
1635
  }
1337
- function redactJson(content) {
1338
- const redacted = [];
1339
- const lines = content.split(`
1340
- `);
1341
- const out = [];
1342
- for (let i = 0;i < lines.length; i++) {
1343
- const line = lines[i];
1344
- const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
1345
- if (m) {
1346
- const [, prefix, key, value, comma, trail] = m;
1347
- if (shouldRedactKeyValue(key, value)) {
1348
- const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1349
- redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
1350
- out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
1351
- continue;
1352
- }
1353
- }
1354
- let newLine = line;
1355
- for (const { re, reason } of VALUE_PATTERNS) {
1356
- newLine = newLine.replace(re, (match) => {
1357
- const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
1358
- redacted.push({ varName, line: i + 1, reason });
1359
- return `{{${varName}}}`;
1360
- });
1361
- }
1362
- out.push(newLine);
1636
+ function planSessionRender(input) {
1637
+ if (!SESSION_RENDER_TOOLS.includes(input.tool))
1638
+ throw new Error(`Unsupported session render tool: ${input.tool}`);
1639
+ if (!input.profile.trim())
1640
+ throw new Error("Session render profile is required.");
1641
+ const adapter = adapterFor(input);
1642
+ const { targetHome, targetKind, blockers } = resolveRenderTarget(input);
1643
+ const targetOwner = resolveSessionTargetOwnership(input, { targetHome, targetKind });
1644
+ const blocked = blockers.length > 0;
1645
+ const allowEmptySources = input.allowEmptySources === true;
1646
+ const orderedSources = composeSources(normalizeSources(input.sources, input.tool, allowEmptySources));
1647
+ if (orderedSources.length === 0 && !allowEmptySources) {
1648
+ throw new Error("Session render has no instruction sources. Pass --allow-empty-sources only for explicit empty renders.");
1363
1649
  }
1364
- return { content: out.join(`
1365
- `), redacted, isTemplate: redacted.length > 0 };
1650
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
1651
+ const env = adapter.envVar && !blocked ? { [adapter.envVar]: targetHome } : {};
1652
+ const warnings = [
1653
+ ...orderedSources.length === 0 ? ["No instruction sources were provided."] : [],
1654
+ ...blockers
1655
+ ];
1656
+ const files = blocked ? [] : buildFiles(targetHome, adapter, input.profile, orderedSources);
1657
+ rejectDuplicateRenderPaths(files);
1658
+ const manifest = {
1659
+ schema: SESSION_RENDER_SCHEMA,
1660
+ tool: input.tool,
1661
+ adapterMode: adapter.mode,
1662
+ profile: input.profile,
1663
+ sessionId: input.sessionId ?? null,
1664
+ targetHome,
1665
+ targetKind,
1666
+ targetOwner,
1667
+ writable: !blocked,
1668
+ blocked,
1669
+ blockers,
1670
+ generatedAt,
1671
+ env,
1672
+ sourceHash: fingerprint(orderedSources.map((source) => ({
1673
+ id: source.id,
1674
+ layer: source.resolvedLayer,
1675
+ order: source.resolvedOrder,
1676
+ merge: source.resolvedMerge,
1677
+ content: source.content,
1678
+ rules: source.resolvedRules.map((rule) => ({ id: rule.id, path: rule.resolvedPath, content: rule.content })),
1679
+ hash: source.hash ?? null
1680
+ }))),
1681
+ sources: orderedSources.map((source) => ({
1682
+ id: source.id,
1683
+ label: source.resolvedLabel,
1684
+ layer: source.resolvedLayer,
1685
+ merge: source.resolvedMerge,
1686
+ order: source.resolvedOrder,
1687
+ path: source.path ?? null,
1688
+ targetProviders: source.targetProviders ?? [],
1689
+ owner: source.owner ?? null,
1690
+ sourcePaths: source.sourcePaths ?? [],
1691
+ hash: source.hash ?? null,
1692
+ nonOverridable: source.nonOverridable === true,
1693
+ replacementScope: source.replacementScope ?? null,
1694
+ rules: source.resolvedRules.map((rule) => ({
1695
+ id: rule.id,
1696
+ label: rule.resolvedLabel,
1697
+ path: rule.resolvedPath,
1698
+ globs: rule.globs ?? [],
1699
+ hash: rule.hash ?? null
1700
+ })),
1701
+ provenance: source.provenance ?? null
1702
+ })),
1703
+ skippedSources: [],
1704
+ files: files.map((file) => ({
1705
+ path: file.path,
1706
+ relativePath: file.relativePath,
1707
+ role: file.role,
1708
+ sha256: file.sha256,
1709
+ sourceIds: file.sourceIds
1710
+ })),
1711
+ warnings
1712
+ };
1713
+ const manifestFile = makeFile(targetHome, posix.join(".hasna", "session-render-manifest.json"), "manifest", JSON.stringify(manifest, null, 2), orderedSources.map((source) => source.id));
1714
+ return {
1715
+ dryRun: true,
1716
+ tool: input.tool,
1717
+ adapter,
1718
+ profile: input.profile,
1719
+ sessionId: input.sessionId ?? null,
1720
+ targetHome,
1721
+ targetKind,
1722
+ targetOwner,
1723
+ writable: !blocked,
1724
+ blocked,
1725
+ blockers,
1726
+ env,
1727
+ files,
1728
+ manifest,
1729
+ manifestFile,
1730
+ allFiles: [...files, manifestFile],
1731
+ warnings
1732
+ };
1366
1733
  }
1367
- function redactToml(content) {
1368
- const redacted = [];
1369
- const lines = content.split(`
1370
- `);
1371
- const out = [];
1372
- for (let i = 0;i < lines.length; i++) {
1373
- const line = lines[i];
1374
- const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
1375
- if (m) {
1376
- const [, indent, key, eq, quote, value] = m;
1377
- if (shouldRedactKeyValue(key, value)) {
1378
- const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1379
- redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
1380
- out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
1381
- continue;
1382
- }
1383
- }
1384
- out.push(line);
1385
- }
1386
- return { content: out.join(`
1387
- `), redacted, isTemplate: redacted.length > 0 };
1388
- }
1389
- function redactIni(content) {
1390
- const redacted = [];
1391
- const lines = content.split(`
1392
- `);
1393
- const out = [];
1394
- for (let i = 0;i < lines.length; i++) {
1395
- const line = lines[i];
1396
- const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
1397
- if (authM && !isReferenceValue(authM[2].trim())) {
1398
- redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
1399
- out.push(`${authM[1]}\${NPM_TOKEN}`);
1400
- continue;
1401
- }
1402
- const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
1403
- if (m) {
1404
- const [, indent, key, eq, value] = m;
1405
- if (shouldRedactKeyValue(key, value)) {
1406
- const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1407
- redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
1408
- out.push(`${indent}${key}${eq}{{${varName}}}`);
1409
- continue;
1410
- }
1411
- }
1412
- out.push(line);
1413
- }
1414
- return { content: out.join(`
1415
- `), redacted, isTemplate: redacted.length > 0 };
1416
- }
1417
- function redactGeneric(content) {
1418
- const redacted = [];
1419
- const lines = content.split(`
1420
- `);
1421
- const out = [];
1422
- for (let i = 0;i < lines.length; i++) {
1423
- let line = lines[i];
1424
- for (const { re, reason } of VALUE_PATTERNS) {
1425
- line = line.replace(re, (match) => {
1426
- const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1427
- redacted.push({ varName, line: i + 1, reason });
1428
- return `{{${varName}}}`;
1429
- });
1430
- }
1431
- out.push(line);
1432
- }
1433
- return { content: out.join(`
1434
- `), redacted, isTemplate: redacted.length > 0 };
1435
- }
1436
- function shouldRedactKeyValue(key, value) {
1437
- if (!value || value.startsWith("{{"))
1438
- return false;
1439
- if (isReferenceValue(value.trim()))
1440
- return false;
1441
- if (value.length < MIN_SECRET_VALUE_LEN)
1442
- return false;
1443
- if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
1444
- return false;
1445
- if (SECRET_KEY_PATTERN.test(key))
1446
- return true;
1447
- for (const { re } of VALUE_PATTERNS) {
1448
- if (re.test(value))
1449
- return true;
1450
- }
1451
- return false;
1452
- }
1453
- function reasonFor(key, value) {
1454
- if (SECRET_KEY_PATTERN.test(key))
1455
- return `secret key name: ${key}`;
1456
- for (const { re, reason } of VALUE_PATTERNS) {
1457
- if (re.test(value))
1458
- return reason;
1459
- }
1460
- return "secret value pattern";
1461
- }
1462
- function isReferenceValue(value) {
1463
- return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
1464
- }
1465
- function redactContent(content, format) {
1466
- switch (format) {
1467
- case "shell":
1468
- return redactShell(content);
1469
- case "json":
1470
- return redactJson(content);
1471
- case "toml":
1472
- return redactToml(content);
1473
- case "ini":
1474
- return redactIni(content);
1475
- default:
1476
- return redactGeneric(content);
1477
- }
1478
- }
1479
- function scanSecrets(content, format) {
1480
- const r = redactContent(content, format);
1481
- return r.redacted;
1482
- }
1483
- function hasSecrets(content, format) {
1484
- return scanSecrets(content, format).length > 0;
1485
- }
1486
-
1487
- // src/status.ts
1488
- var PACKAGE_NAME = "@hasna/instructions";
1489
- var PACKAGE_VERSION = "0.3.0";
1490
- function activeDatabaseEnv() {
1491
- if (process.env["HASNA_INSTRUCTIONS_DB_PATH"])
1492
- return "HASNA_INSTRUCTIONS_DB_PATH";
1493
- return null;
1494
- }
1495
- function configuredDatabaseKind() {
1496
- const value = process.env["HASNA_INSTRUCTIONS_DB_PATH"] ?? "";
1497
- return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
1498
- }
1499
- function countBy(items, getValue) {
1500
- const counts = {};
1501
- for (const item of items) {
1502
- const value = getValue(item);
1503
- if (!value)
1504
- continue;
1505
- counts[value] = (counts[value] ?? 0) + 1;
1506
- }
1507
- return counts;
1508
- }
1509
- async function getConfigsStatus(store = resolveConfigStore()) {
1510
- let databaseReachable = true;
1511
- let configs = [];
1512
- let categoryStats = { total: 0 };
1513
- try {
1514
- configs = await store.listConfigs();
1515
- categoryStats = await store.getConfigStats();
1516
- } catch {
1517
- databaseReachable = false;
1518
- }
1519
- const fileConfigs = configs.filter((config) => config.kind === "file");
1520
- let driftedTargets = 0;
1521
- let missingTargets = 0;
1522
- let unredactedSecretFindings = 0;
1523
- let knownTargets = 0;
1524
- for (const config of fileConfigs) {
1525
- unredactedSecretFindings += scanSecrets(config.content, config.format).length;
1526
- if (!config.target_path)
1527
- continue;
1528
- knownTargets += 1;
1529
- const targetPath = expandPath(config.target_path);
1530
- if (!existsSync4(targetPath)) {
1531
- missingTargets += 1;
1532
- continue;
1533
- }
1534
- const disk = readFileSync2(targetPath, "utf-8");
1535
- const { content: redactedDisk } = redactContent(disk, config.format);
1536
- if (redactedDisk !== config.content) {
1537
- driftedTargets += 1;
1538
- }
1539
- }
1540
- let profiles = 0;
1541
- let machines = 0;
1542
- let profileLinks = 0;
1543
- let snapshots = 0;
1544
- if (databaseReachable) {
1545
- try {
1546
- const profileList = await store.listProfiles();
1547
- profiles = profileList.length;
1548
- machines = (await store.listMachines()).length;
1549
- for (const profile of profileList) {
1550
- profileLinks += (await store.getProfileConfigs(profile.id)).length;
1551
- }
1552
- for (const config of configs) {
1553
- snapshots += (await store.listSnapshots(config.id)).length;
1554
- }
1555
- } catch {
1556
- databaseReachable = false;
1557
- }
1558
- }
1559
- const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
1560
- const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
1561
- return {
1562
- service: "configs",
1563
- schemaVersion: "1.0",
1564
- package: {
1565
- name: PACKAGE_NAME,
1566
- version: PACKAGE_VERSION
1567
- },
1568
- env: {
1569
- database: {
1570
- primary: "HASNA_INSTRUCTIONS_DB_PATH",
1571
- active: activeDatabaseEnv(),
1572
- kind: configuredDatabaseKind()
1573
- }
1574
- },
1575
- counts: {
1576
- configs: {
1577
- total: configs.length,
1578
- file: fileConfigs.length,
1579
- reference: configs.filter((config) => config.kind === "reference").length,
1580
- templates: configs.filter((config) => config.is_template).length
1581
- },
1582
- byCategory,
1583
- byAgent: countBy(configs, (config) => config.agent),
1584
- byFormat: countBy(configs, (config) => config.format),
1585
- profiles,
1586
- profileLinks,
1587
- machines,
1588
- snapshots,
1589
- knownTargets
1590
- },
1591
- health: {
1592
- status,
1593
- databaseReachable,
1594
- driftedTargets,
1595
- missingTargets,
1596
- unredactedSecretFindings,
1597
- hasDrift: driftedTargets > 0,
1598
- hasMissingTargets: missingTargets > 0,
1599
- hasUnredactedSecrets: unredactedSecretFindings > 0
1600
- },
1601
- safety: {
1602
- includesConfigValues: false,
1603
- includesPrivatePaths: false,
1604
- includesHostnames: false,
1605
- includesSecretValues: false,
1606
- statusOutputIsMetadataOnly: true
1607
- }
1608
- };
1609
- }
1610
- // src/db/pg-migrations.ts
1611
- var PG_MIGRATIONS = [
1612
- `CREATE TABLE IF NOT EXISTS configs (
1613
- id TEXT PRIMARY KEY,
1614
- name TEXT NOT NULL,
1615
- slug TEXT NOT NULL UNIQUE,
1616
- kind TEXT NOT NULL DEFAULT 'file',
1617
- category TEXT NOT NULL,
1618
- agent TEXT NOT NULL DEFAULT 'global',
1619
- target_path TEXT,
1620
- outputs TEXT NOT NULL DEFAULT '[]',
1621
- format TEXT NOT NULL DEFAULT 'text',
1622
- content TEXT NOT NULL DEFAULT '',
1623
- description TEXT,
1624
- tags TEXT NOT NULL DEFAULT '[]',
1625
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
1626
- version INTEGER NOT NULL DEFAULT 1,
1627
- created_at TEXT NOT NULL,
1628
- updated_at TEXT NOT NULL,
1629
- synced_at TEXT
1630
- )`,
1631
- `CREATE TABLE IF NOT EXISTS config_snapshots (
1632
- id TEXT PRIMARY KEY,
1633
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1634
- content TEXT NOT NULL,
1635
- version INTEGER NOT NULL,
1636
- created_at TEXT NOT NULL
1637
- )`,
1638
- `CREATE TABLE IF NOT EXISTS profiles (
1639
- id TEXT PRIMARY KEY,
1640
- name TEXT NOT NULL,
1641
- slug TEXT NOT NULL UNIQUE,
1642
- description TEXT,
1643
- selectors TEXT NOT NULL DEFAULT '{}',
1644
- variables TEXT NOT NULL DEFAULT '{}',
1645
- created_at TEXT NOT NULL,
1646
- updated_at TEXT NOT NULL
1647
- )`,
1648
- `CREATE TABLE IF NOT EXISTS profile_configs (
1649
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
1650
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1651
- sort_order INTEGER NOT NULL DEFAULT 0,
1652
- PRIMARY KEY (profile_id, config_id)
1653
- )`,
1654
- `CREATE TABLE IF NOT EXISTS machines (
1655
- id TEXT PRIMARY KEY,
1656
- hostname TEXT NOT NULL UNIQUE,
1657
- os TEXT,
1658
- arch TEXT,
1659
- last_applied_at TEXT,
1660
- created_at TEXT NOT NULL
1661
- )`,
1662
- `CREATE TABLE IF NOT EXISTS feedback (
1663
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1664
- message TEXT NOT NULL,
1665
- email TEXT,
1666
- category TEXT DEFAULT 'general',
1667
- version TEXT,
1668
- machine_id TEXT,
1669
- created_at TEXT NOT NULL DEFAULT NOW()::text
1670
- )`,
1671
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
1672
- ];
1673
- // src/lib/session-render.ts
1674
- import { createHash } from "crypto";
1675
- import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync } from "fs";
1676
- import { homedir as homedir3 } from "os";
1677
- import { basename as basename3, dirname as dirname2, extname as extname2, isAbsolute, join as join3, parse, posix, relative, resolve as resolve2 } from "path";
1678
- var CODEWITH_NATIVE_IMPORTS_ENV = "HASNA_CONFIGS_CODEWITH_NATIVE_IMPORTS";
1679
- var SESSION_RENDER_MANAGED_MARKER = "Managed by @hasna/configs session render";
1680
- var SESSION_RENDER_SCHEMA = "hasna.configs.session-render/v1";
1681
- var RAW_STORE_ROOT_ENV = "HASNA_CONFIGS_HOME";
1682
- var SESSION_RENDER_TOOLS = [
1683
- "claude",
1684
- "codex",
1685
- "cursor",
1686
- "opencode",
1687
- "codewith"
1688
- ];
1689
- var CODEWITH_FLATTENED_ADAPTER = {
1690
- tool: "codewith",
1691
- mode: "flattened-markdown",
1692
- indexFile: "CODEWITH.md",
1693
- managedDir: ".hasna/instructions",
1694
- envVar: "CODEWITH_HOME",
1695
- nativeImports: false,
1696
- description: "Codewith CODEWITH.md flattened until native @ imports are implemented in Codewith."
1697
- };
1698
- var CODEWITH_NATIVE_ADAPTER = {
1699
- tool: "codewith",
1700
- mode: "native-imports",
1701
- indexFile: "CODEWITH.md",
1702
- managedDir: ".hasna/instructions",
1703
- envVar: "CODEWITH_HOME",
1704
- nativeImports: true,
1705
- description: "Codewith CODEWITH.md with gated @ imports into managed fragments."
1706
- };
1707
- var SESSION_TOOL_ADAPTERS = {
1708
- claude: {
1709
- tool: "claude",
1710
- mode: "native-imports",
1711
- indexFile: "CLAUDE.md",
1712
- managedDir: ".hasna/instructions",
1713
- envVar: "CLAUDE_CONFIG_DIR",
1714
- nativeImports: true,
1715
- description: "Claude Code CLAUDE.md with @ imports into managed fragments."
1716
- },
1717
- codex: {
1718
- tool: "codex",
1719
- mode: "flattened-markdown",
1720
- indexFile: "AGENTS.md",
1721
- managedDir: ".hasna/instructions",
1722
- envVar: "CODEX_HOME",
1723
- nativeImports: false,
1724
- description: "Codex AGENTS.md flattened instruction file."
1725
- },
1726
- cursor: {
1727
- tool: "cursor",
1728
- mode: "cursor-mdc",
1729
- managedDir: ".cursor/rules",
1730
- nativeImports: false,
1731
- description: "Cursor project rule files in .cursor/rules/*.mdc."
1732
- },
1733
- opencode: {
1734
- tool: "opencode",
1735
- mode: "opencode-instructions",
1736
- indexFile: "AGENTS.md",
1737
- configFile: "opencode.json",
1738
- managedDir: ".hasna/instructions",
1739
- envVar: "OPENCODE_CONFIG_DIR",
1740
- nativeImports: false,
1741
- description: "OpenCode AGENTS.md plus opencode.json instructions pointing at managed fragments."
1742
- },
1743
- codewith: CODEWITH_FLATTENED_ADAPTER
1744
- };
1745
- var LAYER_RANK = {
1746
- global: 10,
1747
- tool: 20,
1748
- account: 30,
1749
- agent: 40,
1750
- project: 50,
1751
- local: 60
1752
- };
1753
- function ensureTrailingNewline2(content) {
1754
- return content.endsWith(`
1755
- `) ? content : `${content}
1756
- `;
1757
- }
1758
- function sha256(content) {
1759
- return createHash("sha256").update(content).digest("hex");
1760
- }
1761
- function fingerprint(value) {
1762
- return sha256(JSON.stringify(value));
1763
- }
1764
- function slug(value) {
1765
- const s = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1766
- return s || "instruction";
1767
- }
1768
- function yamlQuote2(value) {
1769
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
1770
- }
1771
- function getRawStoreRoot() {
1772
- return resolve2(process.env[RAW_STORE_ROOT_ENV] || join3(process.env["HOME"] || homedir3(), ".hasna", "configs"));
1773
- }
1774
- function defaultTargetHome(tool, profile, sessionId) {
1775
- return join3(getRawStoreRoot(), "sessions", tool, slug(profile), slug(sessionId || "latest"));
1776
- }
1777
- function joinTarget(targetHome, relativePath) {
1778
- const safeTargetHome = assertSafeTargetRoot(targetHome);
1779
- const safeRelativePath = assertSafeRelativePath(relativePath);
1780
- return join3(safeTargetHome, ...safeRelativePath.split("/"));
1781
- }
1782
- function makeFile(targetHome, relativePath, role, content, sourceIds) {
1783
- const safeTargetHome = assertSafeTargetRoot(targetHome);
1784
- const safeRelativePath = assertSafeRelativePath(relativePath);
1785
- const normalizedContent = ensureTrailingNewline2(content);
1734
+ function sourceFromFilePath(path, content, order = 0) {
1735
+ const file = basename(path);
1786
1736
  return {
1787
- path: joinTarget(safeTargetHome, safeRelativePath),
1788
- relativePath: safeRelativePath,
1789
- role,
1790
- content: normalizedContent,
1791
- sha256: sha256(normalizedContent),
1792
- sourceIds
1793
- };
1794
- }
1795
- function normalizeSources(sources, tool, allowEmptySources) {
1796
- const ordered = sources.map((source, index) => {
1797
- if (!source.id.trim())
1798
- throw new Error("Session instruction source id is required.");
1799
- const content = filterProviderOnlyBlocks(source.content ?? "", tool);
1800
- const normalized = {
1801
- ...source,
1802
- content,
1803
- normalizedId: slug(source.id),
1804
- resolvedLabel: source.label ?? source.id,
1805
- resolvedLayer: source.layer ?? "agent",
1806
- resolvedMerge: source.merge ?? "append",
1807
- resolvedOrder: source.order ?? index,
1808
- resolvedRules: normalizeInstructionRules(source, tool)
1809
- };
1810
- const hasPathReferences = (normalized.sourcePaths ?? []).length > 0;
1811
- if (!allowEmptySources && !normalized.content.trim() && normalized.resolvedRules.length === 0 && !hasPathReferences) {
1812
- throw new Error(`Session instruction source "${source.id}" is empty. Pass --allow-empty-sources only for explicit empty renders.`);
1813
- }
1814
- return normalized;
1815
- }).sort((a, b) => LAYER_RANK[a.resolvedLayer] - LAYER_RANK[b.resolvedLayer] || a.resolvedOrder - b.resolvedOrder || a.id.localeCompare(b.id));
1816
- rejectDuplicateSourceSlugs(ordered);
1817
- rejectDuplicateRulePaths(ordered);
1818
- return ordered;
1819
- }
1820
- function filterProviderOnlyBlocks(content, tool) {
1821
- const lines = content.split(/\r?\n/);
1822
- const output = [];
1823
- let activeProviders = null;
1824
- for (const line of lines) {
1825
- const start = line.match(/^\s*<!--\s*@hasna-provider:\s*([^>]+?)\s*-->\s*$/i);
1826
- if (start) {
1827
- if (activeProviders)
1828
- throw new Error("Nested provider-only instruction blocks are not supported.");
1829
- activeProviders = start[1].split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
1830
- continue;
1831
- }
1832
- if (/^\s*<!--\s*@hasna-end-provider\s*-->\s*$/i.test(line)) {
1833
- if (!activeProviders)
1834
- throw new Error("Provider-only instruction block end marker without start marker.");
1835
- activeProviders = null;
1836
- continue;
1837
- }
1838
- if (!activeProviders || activeProviders.includes(tool) || activeProviders.includes("all") || activeProviders.includes("generic")) {
1839
- output.push(line);
1840
- }
1841
- }
1842
- if (activeProviders)
1843
- throw new Error("Provider-only instruction block was not closed.");
1844
- return output.join(`
1845
- `);
1846
- }
1847
- function composeSources(sources) {
1848
- let start = -1;
1849
- for (let i = 0;i < sources.length; i++) {
1850
- if (sources[i].resolvedMerge === "replace")
1851
- start = i;
1852
- }
1853
- if (start < 0)
1854
- return sources;
1855
- const protectedSources = sources.slice(0, start).filter((source) => source.nonOverridable);
1856
- return [...protectedSources, ...sources.slice(start)];
1857
- }
1858
- function sectionForSource(source) {
1859
- const parts = [
1860
- `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1861
- `# ${source.resolvedLabel}`
1862
- ];
1863
- if (source.path)
1864
- parts.push(`Source: ${source.path}`);
1865
- if (source.sourcePaths && source.sourcePaths.length > 0) {
1866
- parts.push([
1867
- "Source paths:",
1868
- ...source.sourcePaths.map((sourcePath) => {
1869
- const flags = [
1870
- sourcePath.editable ? "editable" : null,
1871
- sourcePath.required ? "required" : null,
1872
- sourcePath.hash ? sourcePath.hash : null
1873
- ].filter(Boolean);
1874
- return `- ${sourcePath.path}${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
1875
- })
1876
- ].join(`
1877
- `));
1878
- }
1879
- if (source.owner)
1880
- parts.push(`Owner: ${source.owner.kind}:${source.owner.id}`);
1881
- const content = source.content.trim();
1882
- if (content)
1883
- parts.push(content);
1884
- return parts.join(`
1885
-
1886
- `);
1887
- }
1888
- function sectionForRule(source, rule) {
1889
- const parts = [
1890
- `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1891
- `# ${rule.resolvedLabel}`
1892
- ];
1893
- if (source.path)
1894
- parts.push(`Source: ${source.path}`);
1895
- if (rule.path)
1896
- parts.push(`Rule path: ${rule.path}`);
1897
- const content = rule.content.trim();
1898
- if (content)
1899
- parts.push(content);
1900
- return parts.join(`
1901
-
1902
- `);
1903
- }
1904
- function fragmentPath(adapter, index, source) {
1905
- const n = String(index + 1).padStart(2, "0");
1906
- return posix.join(adapter.managedDir, `${n}-${source.normalizedId}.md`);
1907
- }
1908
- function ruleFragmentPath(adapter, source, rule) {
1909
- return posix.join(adapter.managedDir, "rules", source.normalizedId, rule.resolvedPath);
1910
- }
1911
- function importPath(indexRelativePath, fragmentRelativePath) {
1912
- const relative2 = posix.relative(posix.dirname(indexRelativePath), fragmentRelativePath);
1913
- if (relative2.startsWith("./") || relative2.startsWith("../"))
1914
- return relative2;
1915
- return `./${relative2}`;
1916
- }
1917
- function indexHeader(tool, profile) {
1918
- return [
1919
- `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1920
- `# ${tool} session instructions`,
1921
- "",
1922
- `Profile: ${profile}`
1923
- ].join(`
1924
- `);
1925
- }
1926
- function buildNativeImportFiles(targetHome, adapter, profile, sources) {
1927
- const indexFile = adapter.indexFile;
1928
- const fragments = sources.flatMap((source, index2) => [
1929
- makeFile(targetHome, fragmentPath(adapter, index2, source), "fragment", sectionForSource(source), [source.id]),
1930
- ...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
1931
- ]);
1932
- const imports = fragments.map((file) => `@${importPath(indexFile, file.relativePath)}`);
1933
- const index = makeFile(targetHome, indexFile, "index", [indexHeader(adapter.tool, profile), ...imports].join(`
1934
- `), sources.map((source) => source.id));
1935
- return [index, ...fragments];
1936
- }
1937
- function buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources) {
1938
- const content = [
1939
- indexHeader(adapter.tool, profile),
1940
- ...sources.flatMap((source) => [
1941
- sectionForSource(source),
1942
- ...source.resolvedRules.map((rule) => sectionForRule(source, rule))
1943
- ])
1944
- ].join(`
1945
-
1946
- `);
1947
- return [
1948
- makeFile(targetHome, adapter.indexFile, "index", content, [
1949
- ...sources.map((source) => source.id),
1950
- ...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
1951
- ])
1952
- ];
1953
- }
1954
- function buildCursorRuleFiles(targetHome, adapter, sources) {
1955
- return sources.flatMap((source, index) => {
1956
- const n = String(index + 1).padStart(2, "0");
1957
- const stem = `${n}-${source.normalizedId}`;
1958
- const relativePath = posix.join(adapter.managedDir, `${stem}.mdc`);
1959
- const description = `${source.resolvedLabel} (${source.resolvedLayer})`;
1960
- const content = [
1961
- "---",
1962
- `description: ${yamlQuote2(description)}`,
1963
- 'globs: ["**/*"]',
1964
- "alwaysApply: true",
1965
- "---",
1966
- "",
1967
- `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1968
- "",
1969
- source.content.trim()
1970
- ].join(`
1971
- `);
1972
- const sourceFile = makeFile(targetHome, relativePath, "rule", content, [source.id]);
1973
- const ruleFiles = source.resolvedRules.map((rule) => {
1974
- const ruleStem = `${n}-${source.normalizedId}-${rule.normalizedId}`;
1975
- const ruleRelativePath = posix.join(adapter.managedDir, `${ruleStem}.mdc`);
1976
- const ruleDescription = `${rule.resolvedLabel} (${source.resolvedLayer})`;
1977
- const ruleContent = [
1978
- "---",
1979
- `description: ${yamlQuote2(ruleDescription)}`,
1980
- `globs: ${JSON.stringify(rule.globs && rule.globs.length > 0 ? rule.globs : ["**/*"])}`,
1981
- "alwaysApply: true",
1982
- "---",
1983
- "",
1984
- `<!-- ${SESSION_RENDER_MANAGED_MARKER}. Do not edit this generated file directly. -->`,
1985
- "",
1986
- rule.content.trim()
1987
- ].join(`
1988
- `);
1989
- return makeFile(targetHome, ruleRelativePath, "rule", ruleContent, [source.id, rule.id]);
1990
- });
1991
- return [sourceFile, ...ruleFiles];
1992
- });
1993
- }
1994
- function buildOpenCodeFiles(targetHome, adapter, profile, sources) {
1995
- const fragments = sources.flatMap((source, index) => [
1996
- makeFile(targetHome, fragmentPath(adapter, index, source), "fragment", sectionForSource(source), [source.id]),
1997
- ...source.resolvedRules.map((rule) => makeFile(targetHome, ruleFragmentPath(adapter, source, rule), "rule", sectionForRule(source, rule), [source.id, rule.id]))
1998
- ]);
1999
- const flattenedIndex = makeFile(targetHome, adapter.indexFile, "index", [
2000
- indexHeader(adapter.tool, profile),
2001
- ...sources.flatMap((source) => [
2002
- sectionForSource(source),
2003
- ...source.resolvedRules.map((rule) => sectionForRule(source, rule))
2004
- ])
2005
- ].join(`
2006
-
2007
- `), [
2008
- ...sources.map((source) => source.id),
2009
- ...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
2010
- ]);
2011
- const config = {
2012
- $schema: "https://opencode.ai/config.json",
2013
- instructions: fragments.map((file) => file.relativePath)
1737
+ id: file.replace(extname(file), ""),
1738
+ label: file,
1739
+ content,
1740
+ layer: "agent",
1741
+ order,
1742
+ path
2014
1743
  };
2015
- return [
2016
- flattenedIndex,
2017
- makeFile(targetHome, adapter.configFile, "config", JSON.stringify(config, null, 2), sources.map((source) => source.id)),
2018
- ...fragments
2019
- ];
2020
1744
  }
2021
- function buildFiles(targetHome, adapter, profile, sources) {
2022
- switch (adapter.mode) {
2023
- case "native-imports":
2024
- return buildNativeImportFiles(targetHome, adapter, profile, sources);
2025
- case "flattened-markdown":
2026
- return buildFlattenedMarkdownFiles(targetHome, adapter, profile, sources);
2027
- case "cursor-mdc":
2028
- return buildCursorRuleFiles(targetHome, adapter, sources);
2029
- case "opencode-instructions":
2030
- return buildOpenCodeFiles(targetHome, adapter, profile, sources);
1745
+ function sourceFromConfig(config, order = 0, layer) {
1746
+ return {
1747
+ id: config.slug,
1748
+ label: config.name,
1749
+ content: config.content,
1750
+ layer: layer ?? (config.agent === "global" ? "global" : "agent"),
1751
+ order,
1752
+ path: config.target_path ?? undefined,
1753
+ provenance: {
1754
+ source: "open-configs",
1755
+ configSlug: config.slug,
1756
+ configAgent: config.agent
1757
+ }
1758
+ };
1759
+ }
1760
+ function sourcesFromIdentityExport(value, options = {}) {
1761
+ const record = asRecord(value, "identity instruction export");
1762
+ const shape = requireIdentityExportShape(record);
1763
+ const validation = asOptionalRecord(record["validation"]);
1764
+ if (validation && validation["valid"] === false) {
1765
+ const issues = Array.isArray(validation["issues"]) ? validation["issues"] : [];
1766
+ throw new Error(`Identity instruction export is invalid: ${JSON.stringify(issues)}`);
2031
1767
  }
1768
+ const sources = record["sources"];
1769
+ if (!Array.isArray(sources))
1770
+ throw new Error("Identity instruction export sources must be an array.");
1771
+ const offset = options.orderOffset ?? 0;
1772
+ return sources.map((item, index) => identitySourceToSessionSource(item, {
1773
+ path: options.path,
1774
+ tool: options.tool,
1775
+ orderFallback: offset + index,
1776
+ exportShape: shape
1777
+ })).filter((source) => source !== null);
2032
1778
  }
2033
- function adapterFor(input) {
2034
- if (input.tool !== "codewith")
2035
- return SESSION_TOOL_ADAPTERS[input.tool];
2036
- const gatedNativeImports = input.codewithNativeImports === true || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "1" || process.env[CODEWITH_NATIVE_IMPORTS_ENV] === "true";
2037
- return gatedNativeImports ? CODEWITH_NATIVE_ADAPTER : CODEWITH_FLATTENED_ADAPTER;
1779
+ function requireIdentityExportShape(record) {
1780
+ if (record["contract"] === "hasna.identities.configs-instructions/v1")
1781
+ return "configs-contract";
1782
+ if (record["version"] === 1 && record["package"] === "@hasna/identities")
1783
+ return "canonical-open-identities";
1784
+ throw new Error("Unsupported identity instruction export contract.");
2038
1785
  }
2039
- function getHomeDir() {
2040
- return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir3();
1786
+ function normalizeInstructionRules(source, tool) {
1787
+ const seen = new Set;
1788
+ return (source.rules ?? []).map((rule) => {
1789
+ if (!rule.id.trim())
1790
+ throw new Error(`Instruction rule id is required for source ${source.id}.`);
1791
+ const content = filterProviderOnlyBlocks(rule.content ?? "", tool);
1792
+ if (!content.trim() && !rule.path)
1793
+ throw new Error(`Instruction rule content or path is required for rule ${rule.id}.`);
1794
+ const resolvedPath = normalizeRulePath(rule.path ?? `${slug(rule.id)}.md`);
1795
+ const key = resolvedPath.toLowerCase();
1796
+ if (seen.has(key))
1797
+ throw new Error(`Duplicate rule path for source ${source.id}: ${resolvedPath}`);
1798
+ seen.add(key);
1799
+ return {
1800
+ ...rule,
1801
+ content,
1802
+ normalizedId: slug(rule.id),
1803
+ resolvedLabel: rule.label ?? rule.id,
1804
+ resolvedPath
1805
+ };
1806
+ });
2041
1807
  }
2042
- function cleanSessionPathInput(path) {
2043
- const trimmed = path.trim();
2044
- if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
2045
- return trimmed.slice(1, -1);
1808
+ function rejectDuplicateRenderPaths(files) {
1809
+ const seen = new Set;
1810
+ for (const file of files) {
1811
+ const key = file.relativePath.toLowerCase();
1812
+ if (seen.has(key))
1813
+ throw new Error(`Duplicate session render file path: ${file.relativePath}`);
1814
+ seen.add(key);
2046
1815
  }
2047
- return trimmed;
2048
1816
  }
2049
- function resolveSessionPath(path) {
2050
- const cleaned = cleanSessionPathInput(path);
2051
- if (!cleaned)
2052
- throw new Error("Session render path cannot be empty.");
2053
- const home = getHomeDir();
2054
- if (cleaned === "~")
2055
- return resolve2(home);
2056
- if (cleaned.startsWith("~/"))
2057
- return resolve2(home, cleaned.slice(2));
2058
- if (cleaned === "{{HOME}}" || cleaned === "${HOME}")
2059
- return resolve2(home);
2060
- if (cleaned.startsWith("{{HOME}}/"))
2061
- return resolve2(home, cleaned.slice("{{HOME}}/".length));
2062
- if (cleaned.startsWith("${HOME}/"))
2063
- return resolve2(home, cleaned.slice("${HOME}/".length));
2064
- return resolve2(cleaned);
1817
+ function rejectDuplicateSourceSlugs(sources) {
1818
+ const seen = new Map;
1819
+ for (const source of sources) {
1820
+ const existing = seen.get(source.normalizedId);
1821
+ if (existing)
1822
+ throw new Error(`Duplicate session instruction source slug: ${source.normalizedId} (${existing}, ${source.id})`);
1823
+ seen.set(source.normalizedId, source.id);
1824
+ }
2065
1825
  }
2066
- function assertSafeRelativePath(relativePath) {
2067
- if (!relativePath.trim())
2068
- throw new Error("Session render relative path cannot be empty.");
2069
- if (relativePath.includes("\\"))
2070
- throw new Error(`Session render relative path must use POSIX separators: ${relativePath}`);
2071
- const normalized = posix.normalize(relativePath);
1826
+ function rejectDuplicateRulePaths(sources) {
1827
+ const seen = new Map;
1828
+ for (const source of sources) {
1829
+ for (const rule of source.resolvedRules) {
1830
+ const key = rule.resolvedPath.toLowerCase();
1831
+ const existing = seen.get(key);
1832
+ if (existing)
1833
+ throw new Error(`Duplicate instruction rule path: ${rule.resolvedPath} (${existing}, ${rule.id})`);
1834
+ seen.set(key, rule.id);
1835
+ }
1836
+ }
1837
+ }
1838
+ function normalizeRulePath(path) {
1839
+ if (!path.trim())
1840
+ throw new Error("Instruction rule path cannot be empty.");
1841
+ if (path.includes("\\"))
1842
+ throw new Error(`Instruction rule path must use POSIX separators: ${path}`);
1843
+ const normalized = posix.normalize(path);
2072
1844
  if (normalized === "." || posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2073
- throw new Error(`Session render relative path escapes target root: ${relativePath}`);
1845
+ throw new Error(`Instruction rule path escapes managed rule directory: ${path}`);
2074
1846
  }
2075
1847
  return normalized;
2076
1848
  }
2077
- function assertSafeTargetRoot(targetHome) {
2078
- if (!isAbsolute(targetHome))
2079
- throw new Error(`Session render target must be an absolute path: ${targetHome}`);
2080
- const normalized = resolve2(targetHome);
2081
- if (normalized === parse(normalized).root) {
2082
- throw new Error(`Session render target cannot be the filesystem root: ${targetHome}`);
1849
+ function identitySourceToSessionSource(value, options) {
1850
+ const record = asRecord(value, "identity instruction source");
1851
+ const providers = asStringArray(record["targetProviders"]);
1852
+ if (options.tool && providers.length > 0 && !providerTargetsTool(providers, options.tool))
1853
+ return null;
1854
+ const sourcePaths = normalizeSourcePaths(record["sourcePaths"]);
1855
+ const kind = maybeString(record["kind"]);
1856
+ const layer = record["layer"] === undefined ? layerFromIdentityKind(kind, options.exportShape) : requireLayer(record["layer"]);
1857
+ const merge = requireMerge(record["merge"] ?? record["mergePolicy"] ?? "append");
1858
+ const id = requireString(record["id"], "identity instruction source id");
1859
+ const inlineContent = maybeString(record["content"]);
1860
+ const resolvedContent = inlineContent && inlineContent.trim() ? inlineContent : contentFromIdentitySourcePaths(sourcePaths, options.path, id) ?? inlineContent;
1861
+ return {
1862
+ id,
1863
+ label: maybeString(record["label"]) ?? maybeString(record["title"]) ?? id,
1864
+ layer,
1865
+ merge,
1866
+ order: typeof record["order"] === "number" ? record["order"] : typeof record["precedence"] === "number" ? record["precedence"] : options.orderFallback,
1867
+ content: resolvedContent ?? "",
1868
+ path: options.path,
1869
+ rules: normalizeIdentityRules(record["rules"]),
1870
+ provenance: asOptionalRecord(record["provenance"]) ?? null,
1871
+ targetProviders: providers,
1872
+ owner: normalizeIdentityOwner(record["owner"]),
1873
+ sourcePaths,
1874
+ globs: asStringArray(record["globs"]),
1875
+ hash: maybeString(record["hash"]),
1876
+ nonOverridable: record["nonOverridable"] === true,
1877
+ replacementScope: maybeString(record["replacementScope"]),
1878
+ metadata: asOptionalRecord(record["metadata"]) ?? null
1879
+ };
1880
+ }
1881
+ function layerFromIdentityKind(kind, exportShape) {
1882
+ if (!kind) {
1883
+ if (exportShape === "configs-contract")
1884
+ throw new Error("Invalid session instruction layer: undefined");
1885
+ return "agent";
1886
+ }
1887
+ switch (kind) {
1888
+ case "global-rules":
1889
+ case "global-system-prompt":
1890
+ return "global";
1891
+ case "provider-rules":
1892
+ case "provider-system-prompt":
1893
+ return "tool";
1894
+ case "identity-doc":
1895
+ case "persona-doc":
1896
+ return "agent";
1897
+ case "account-overlay":
1898
+ return "account";
1899
+ case "project-overlay":
1900
+ return "repo";
1901
+ case "machine-overlay":
1902
+ return "machine";
1903
+ case "session-overlay":
1904
+ return "session";
1905
+ default:
1906
+ throw new Error(`Invalid identity instruction source kind: ${kind}`);
2083
1907
  }
2084
- return normalized;
2085
1908
  }
2086
- function resolveRenderTarget(input) {
2087
- if (input.tool === "cursor") {
2088
- if (!input.projectRoot) {
2089
- return {
2090
- targetHome: defaultTargetHome(input.tool, input.profile, input.sessionId),
2091
- targetKind: "blocked",
2092
- blockers: [
2093
- "Cursor rules are project-scoped; pass --project-root (or projectRoot) before applying .cursor/rules files. --target-home is not treated as a repository root for Cursor."
2094
- ]
2095
- };
1909
+ function contentFromIdentitySourcePaths(sourcePaths, exportPath, sourceId) {
1910
+ if (sourcePaths.length === 0 || !exportPath)
1911
+ return;
1912
+ const baseDir = dirname(resolveSessionPath(exportPath));
1913
+ const contents = [];
1914
+ for (const sourcePath of sourcePaths) {
1915
+ const content = readIdentitySourcePath(sourcePath, baseDir, sourceId);
1916
+ if (content !== undefined)
1917
+ contents.push({ path: sourcePath.path, content });
1918
+ }
1919
+ if (contents.length === 0)
1920
+ return;
1921
+ if (contents.length === 1)
1922
+ return ensureTrailingNewline(contents[0].content);
1923
+ return ensureTrailingNewline(contents.map((item) => `<!-- Source path: ${item.path} -->
1924
+ ${item.content.trimEnd()}`).join(`
1925
+
1926
+ `));
1927
+ }
1928
+ function readIdentitySourcePath(sourcePath, baseDir, sourceId) {
1929
+ const resolvedPath = resolveIdentitySourcePath(sourcePath.path, baseDir, sourceId);
1930
+ if (!existsSync3(resolvedPath)) {
1931
+ if (sourcePath.required) {
1932
+ throw new Error(`Required identity instruction source path not found for ${sourceId}: ${sourcePath.path}`);
2096
1933
  }
1934
+ return;
1935
+ }
1936
+ const stat = statSync(resolvedPath);
1937
+ if (!stat.isFile()) {
1938
+ throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
1939
+ }
1940
+ const realBase = realpathSync(baseDir);
1941
+ const realPath = realpathSync(resolvedPath);
1942
+ if (!pathIsInside(realPath, realBase)) {
1943
+ throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
1944
+ }
1945
+ return readFileSync(realPath, "utf-8");
1946
+ }
1947
+ function resolveIdentitySourcePath(path, baseDir, sourceId) {
1948
+ const cleaned = cleanSessionPathInput(path);
1949
+ if (!cleaned)
1950
+ throw new Error(`Identity instruction source path cannot be empty for ${sourceId}.`);
1951
+ if (cleaned.includes("\\"))
1952
+ throw new Error(`Identity instruction source path must use POSIX separators for ${sourceId}: ${path}`);
1953
+ const resolvedPath = isAbsolute(cleaned) ? resolve(cleaned) : resolve(baseDir, cleaned);
1954
+ if (!pathIsInside(resolvedPath, resolve(baseDir))) {
1955
+ throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${path}`);
1956
+ }
1957
+ return resolvedPath;
1958
+ }
1959
+ function pathIsInside(path, baseDir) {
1960
+ const rel = relative(baseDir, path);
1961
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1962
+ }
1963
+ function providerTargetsTool(targets, tool) {
1964
+ return targets.map((target) => target.toLowerCase()).some((target) => target === tool || target === "all" || target === "generic");
1965
+ }
1966
+ function normalizeIdentityRules(value) {
1967
+ if (value === undefined || value === null)
1968
+ return [];
1969
+ if (!Array.isArray(value))
1970
+ throw new Error("Identity instruction source rules must be an array.");
1971
+ return value.map((item) => {
1972
+ const record = asRecord(item, "identity instruction rule");
1973
+ const id = requireString(record["id"], "identity instruction rule id");
2097
1974
  return {
2098
- targetHome: resolveSessionPath(input.projectRoot),
2099
- targetKind: "project-root",
2100
- blockers: []
1975
+ id,
1976
+ label: maybeString(record["label"]) ?? id,
1977
+ path: maybeString(record["path"]),
1978
+ content: maybeString(record["content"]) ?? "",
1979
+ globs: asStringArray(record["globs"]),
1980
+ hash: maybeString(record["hash"]),
1981
+ metadata: asOptionalRecord(record["metadata"]) ?? null
2101
1982
  };
2102
- }
1983
+ });
1984
+ }
1985
+ function normalizeIdentityOwner(value) {
1986
+ if (value === undefined || value === null)
1987
+ return null;
1988
+ const record = asRecord(value, "identity instruction owner");
2103
1989
  return {
2104
- targetHome: input.targetHome ? resolveSessionPath(input.targetHome) : defaultTargetHome(input.tool, input.profile, input.sessionId),
2105
- targetKind: "session-home",
2106
- blockers: []
1990
+ kind: requireString(record["kind"], "identity instruction owner kind"),
1991
+ id: requireString(record["id"], "identity instruction owner id")
2107
1992
  };
2108
1993
  }
2109
- function resolveSessionTargetOwnership(input, target) {
2110
- if (target.targetKind === "blocked") {
2111
- return {
2112
- kind: "blocked",
2113
- tool: input.tool,
2114
- profile: input.profile,
2115
- targetHome: target.targetHome,
2116
- projectRoot: input.projectRoot ? resolveSessionPath(input.projectRoot) : null,
2117
- ownedBy: "open-configs",
2118
- reason: "target resolution blocked before provider files can be owned"
2119
- };
2120
- }
2121
- if (target.targetKind === "project-root") {
1994
+ function normalizeSourcePaths(value) {
1995
+ if (value === undefined || value === null)
1996
+ return [];
1997
+ if (!Array.isArray(value))
1998
+ throw new Error("Identity instruction source paths must be an array.");
1999
+ return value.map((item) => {
2000
+ const record = asRecord(item, "identity instruction source path");
2122
2001
  return {
2123
- kind: "project",
2124
- tool: input.tool,
2125
- profile: input.profile,
2126
- targetHome: target.targetHome,
2127
- projectRoot: target.targetHome,
2128
- ownedBy: "open-configs",
2129
- reason: "project-scoped provider files are generated in the explicit repository root"
2002
+ path: requireString(record["path"], "identity instruction source path"),
2003
+ editable: record["editable"] === true,
2004
+ required: record["required"] === true,
2005
+ hash: maybeString(record["hash"])
2130
2006
  };
2131
- }
2132
- return {
2133
- kind: "provider-profile",
2134
- tool: input.tool,
2135
- profile: input.profile,
2136
- targetHome: target.targetHome,
2137
- projectRoot: null,
2138
- ownedBy: "open-configs",
2139
- reason: "profile-scoped provider home is generated by OpenConfigs from identity/config sources"
2140
- };
2007
+ });
2141
2008
  }
2142
- function planSessionRender(input) {
2143
- if (!SESSION_RENDER_TOOLS.includes(input.tool))
2144
- throw new Error(`Unsupported session render tool: ${input.tool}`);
2145
- if (!input.profile.trim())
2146
- throw new Error("Session render profile is required.");
2147
- const adapter = adapterFor(input);
2148
- const { targetHome, targetKind, blockers } = resolveRenderTarget(input);
2149
- const targetOwner = resolveSessionTargetOwnership(input, { targetHome, targetKind });
2150
- const blocked = blockers.length > 0;
2151
- const allowEmptySources = input.allowEmptySources === true;
2152
- const orderedSources = composeSources(normalizeSources(input.sources, input.tool, allowEmptySources));
2153
- if (orderedSources.length === 0 && !allowEmptySources) {
2154
- throw new Error("Session render has no instruction sources. Pass --allow-empty-sources only for explicit empty renders.");
2155
- }
2156
- const generatedAt = input.generatedAt ?? new Date().toISOString();
2157
- const env = adapter.envVar && !blocked ? { [adapter.envVar]: targetHome } : {};
2158
- const warnings = [
2159
- ...orderedSources.length === 0 ? ["No instruction sources were provided."] : [],
2160
- ...blockers
2161
- ];
2162
- const files = blocked ? [] : buildFiles(targetHome, adapter, input.profile, orderedSources);
2163
- rejectDuplicateRenderPaths(files);
2164
- const manifest = {
2165
- schema: SESSION_RENDER_SCHEMA,
2166
- tool: input.tool,
2167
- adapterMode: adapter.mode,
2168
- profile: input.profile,
2169
- sessionId: input.sessionId ?? null,
2170
- targetHome,
2171
- targetKind,
2172
- targetOwner,
2173
- writable: !blocked,
2174
- blocked,
2175
- blockers,
2176
- generatedAt,
2177
- env,
2178
- sourceHash: fingerprint(orderedSources.map((source) => ({
2179
- id: source.id,
2180
- layer: source.resolvedLayer,
2181
- order: source.resolvedOrder,
2182
- merge: source.resolvedMerge,
2183
- content: source.content,
2184
- rules: source.resolvedRules.map((rule) => ({ id: rule.id, path: rule.resolvedPath, content: rule.content })),
2185
- hash: source.hash ?? null
2186
- }))),
2187
- sources: orderedSources.map((source) => ({
2188
- id: source.id,
2189
- label: source.resolvedLabel,
2190
- layer: source.resolvedLayer,
2191
- merge: source.resolvedMerge,
2192
- order: source.resolvedOrder,
2193
- path: source.path ?? null,
2194
- targetProviders: source.targetProviders ?? [],
2195
- owner: source.owner ?? null,
2196
- sourcePaths: source.sourcePaths ?? [],
2197
- hash: source.hash ?? null,
2198
- nonOverridable: source.nonOverridable === true,
2199
- replacementScope: source.replacementScope ?? null,
2200
- rules: source.resolvedRules.map((rule) => ({
2201
- id: rule.id,
2202
- label: rule.resolvedLabel,
2203
- path: rule.resolvedPath,
2204
- globs: rule.globs ?? [],
2205
- hash: rule.hash ?? null
2206
- })),
2207
- provenance: source.provenance ?? null
2208
- })),
2209
- skippedSources: [],
2210
- files: files.map((file) => ({
2211
- path: file.path,
2212
- relativePath: file.relativePath,
2213
- role: file.role,
2214
- sha256: file.sha256,
2215
- sourceIds: file.sourceIds
2216
- })),
2217
- warnings
2218
- };
2219
- const manifestFile = makeFile(targetHome, posix.join(".hasna", "session-render-manifest.json"), "manifest", JSON.stringify(manifest, null, 2), orderedSources.map((source) => source.id));
2220
- return {
2221
- dryRun: true,
2222
- tool: input.tool,
2223
- adapter,
2224
- profile: input.profile,
2225
- sessionId: input.sessionId ?? null,
2226
- targetHome,
2227
- targetKind,
2228
- targetOwner,
2229
- writable: !blocked,
2230
- blocked,
2231
- blockers,
2232
- env,
2233
- files,
2234
- manifest,
2235
- manifestFile,
2236
- allFiles: [...files, manifestFile],
2237
- warnings
2238
- };
2009
+ function requireLayer(value) {
2010
+ return normalizeSessionInstructionLayer(value);
2239
2011
  }
2240
- function sourceFromFilePath(path, content, order = 0) {
2241
- const file = basename3(path);
2242
- return {
2243
- id: file.replace(extname2(file), ""),
2244
- label: file,
2245
- content,
2246
- layer: "agent",
2247
- order,
2248
- path
2249
- };
2012
+ function requireMerge(value) {
2013
+ if (value === "append" || value === "replace")
2014
+ return value;
2015
+ throw new Error(`Invalid session instruction merge policy: ${String(value)}`);
2250
2016
  }
2251
- function sourceFromConfig(config, order = 0, layer) {
2252
- return {
2253
- id: config.slug,
2254
- label: config.name,
2255
- content: config.content,
2256
- layer: layer ?? (config.agent === "global" ? "global" : "agent"),
2257
- order,
2258
- path: config.target_path ?? undefined,
2259
- provenance: {
2260
- source: "open-configs",
2261
- configSlug: config.slug,
2262
- configAgent: config.agent
2017
+ function asRecord(value, label) {
2018
+ if (!value || typeof value !== "object" || Array.isArray(value))
2019
+ throw new Error(`Invalid ${label}.`);
2020
+ return value;
2021
+ }
2022
+ function asOptionalRecord(value) {
2023
+ if (value === undefined || value === null)
2024
+ return null;
2025
+ return asRecord(value, "record");
2026
+ }
2027
+ function maybeString(value) {
2028
+ return typeof value === "string" ? value : undefined;
2029
+ }
2030
+ function requireString(value, label) {
2031
+ if (typeof value !== "string" || !value.trim())
2032
+ throw new Error(`Invalid ${label}.`);
2033
+ return value;
2034
+ }
2035
+ function asStringArray(value) {
2036
+ if (value === undefined || value === null)
2037
+ return [];
2038
+ if (!Array.isArray(value))
2039
+ return [];
2040
+ return value.filter((item) => typeof item === "string");
2041
+ }
2042
+
2043
+ // src/lib/transforms.ts
2044
+ import { basename as basename2, extname as extname2 } from "path";
2045
+ function ensureTrailingNewline2(content) {
2046
+ return content.endsWith(`
2047
+ `) ? content : `${content}
2048
+ `;
2049
+ }
2050
+ function yamlQuote2(value) {
2051
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
2052
+ }
2053
+ function headingLevel(line) {
2054
+ const m = line.match(/^(#{1,6})\s+/);
2055
+ return m ? m[1].length : null;
2056
+ }
2057
+ function stripClaudeOnlySections(content) {
2058
+ const withoutMarkedBlocks = content.replace(/<!--\s*claude-only:start\s*-->[\s\S]*?<!--\s*claude-only:end\s*-->/gi, "");
2059
+ const lines = withoutMarkedBlocks.split(`
2060
+ `);
2061
+ const kept = [];
2062
+ let droppingUntilLevel = null;
2063
+ for (const line of lines) {
2064
+ const level = headingLevel(line);
2065
+ if (level !== null && droppingUntilLevel !== null && level <= droppingUntilLevel) {
2066
+ droppingUntilLevel = null;
2263
2067
  }
2264
- };
2068
+ if (droppingUntilLevel !== null)
2069
+ continue;
2070
+ if (/^#{1,6}\s+.*claude(?:\s+code)?[-\s]+only\b/i.test(line)) {
2071
+ droppingUntilLevel = level ?? 1;
2072
+ continue;
2073
+ }
2074
+ kept.push(line);
2075
+ }
2076
+ return kept.join(`
2077
+ `).replace(/\n{3,}/g, `
2078
+
2079
+ `).trim();
2080
+ }
2081
+ function isClaudeRuleConfig(source, candidate) {
2082
+ if (candidate.id === source.id)
2083
+ return false;
2084
+ if (candidate.agent !== "claude" || candidate.category !== "rules")
2085
+ return false;
2086
+ return !!candidate.target_path?.includes("/rules/");
2265
2087
  }
2266
- function sourcesFromIdentityExport(value, options = {}) {
2267
- const record = asRecord(value, "identity instruction export");
2268
- const shape = requireIdentityExportShape(record);
2269
- const validation = asOptionalRecord(record["validation"]);
2270
- if (validation && validation["valid"] === false) {
2271
- const issues = Array.isArray(validation["issues"]) ? validation["issues"] : [];
2272
- throw new Error(`Identity instruction export is invalid: ${JSON.stringify(issues)}`);
2088
+ function ruleLabel(config) {
2089
+ const file = config.target_path ? basename2(config.target_path) : config.name;
2090
+ return file.replace(/\.(md|mdc|markdown)$/i, "");
2091
+ }
2092
+ function claudeRules(source, context) {
2093
+ return (context.configs ?? []).filter((config) => isClaudeRuleConfig(source, config)).sort((a, b) => (a.target_path ?? a.name).localeCompare(b.target_path ?? b.name));
2094
+ }
2095
+ function flattenWithRules(source, context) {
2096
+ const parts = [stripClaudeOnlySections(source.content)];
2097
+ const rules = claudeRules(source, context).map((rule) => ({ rule, content: stripClaudeOnlySections(rule.content) })).filter(({ content }) => Boolean(content)).map(({ rule, content }) => `### ${ruleLabel(rule)}
2098
+
2099
+ ${content}`);
2100
+ if (rules.length > 0) {
2101
+ parts.push(`## Rules
2102
+
2103
+ ${rules.join(`
2104
+
2105
+ `)}`);
2273
2106
  }
2274
- const sources = record["sources"];
2275
- if (!Array.isArray(sources))
2276
- throw new Error("Identity instruction export sources must be an array.");
2277
- const offset = options.orderOffset ?? 0;
2278
- return sources.map((item, index) => identitySourceToSessionSource(item, {
2279
- path: options.path,
2280
- tool: options.tool,
2281
- orderFallback: offset + index,
2282
- exportShape: shape
2283
- })).filter((source) => source !== null);
2107
+ return ensureTrailingNewline2(parts.filter(Boolean).join(`
2108
+
2109
+ `));
2284
2110
  }
2285
- function requireIdentityExportShape(record) {
2286
- if (record["contract"] === "hasna.identities.configs-instructions/v1")
2287
- return "configs-contract";
2288
- if (record["version"] === 1 && record["package"] === "@hasna/identities")
2289
- return "canonical-open-identities";
2290
- throw new Error("Unsupported identity instruction export contract.");
2111
+ function buildCodexAgentsMd(source, context = {}) {
2112
+ return flattenWithRules(source, context);
2291
2113
  }
2292
- function normalizeInstructionRules(source, tool) {
2293
- const seen = new Set;
2294
- return (source.rules ?? []).map((rule) => {
2295
- if (!rule.id.trim())
2296
- throw new Error(`Instruction rule id is required for source ${source.id}.`);
2297
- const content = filterProviderOnlyBlocks(rule.content ?? "", tool);
2298
- if (!content.trim() && !rule.path)
2299
- throw new Error(`Instruction rule content or path is required for rule ${rule.id}.`);
2300
- const resolvedPath = normalizeRulePath(rule.path ?? `${slug(rule.id)}.md`);
2301
- const key = resolvedPath.toLowerCase();
2302
- if (seen.has(key))
2303
- throw new Error(`Duplicate rule path for source ${source.id}: ${resolvedPath}`);
2304
- seen.add(key);
2305
- return {
2306
- ...rule,
2307
- content,
2308
- normalizedId: slug(rule.id),
2309
- resolvedLabel: rule.label ?? rule.id,
2310
- resolvedPath
2311
- };
2312
- });
2114
+ function buildOpenCodeAgentsMd(source, context = {}) {
2115
+ return flattenWithRules(source, context);
2313
2116
  }
2314
- function rejectDuplicateRenderPaths(files) {
2315
- const seen = new Set;
2316
- for (const file of files) {
2317
- const key = file.relativePath.toLowerCase();
2318
- if (seen.has(key))
2319
- throw new Error(`Duplicate session render file path: ${file.relativePath}`);
2320
- seen.add(key);
2117
+ function buildCursorMdc(source) {
2118
+ const stem = source.target_path ? basename2(source.target_path, extname2(source.target_path)) : source.slug;
2119
+ const description = source.name || stem;
2120
+ return ensureTrailingNewline2([
2121
+ "---",
2122
+ `description: ${yamlQuote2(description)}`,
2123
+ 'globs: ["**/*"]',
2124
+ "alwaysApply: true",
2125
+ "---",
2126
+ "",
2127
+ stripClaudeOnlySections(source.content)
2128
+ ].join(`
2129
+ `));
2130
+ }
2131
+ function transformSkillContent(content) {
2132
+ return ensureTrailingNewline2(content.split(`
2133
+ `).filter((line) => !/^\s*user_invocable\s*:/i.test(line)).join(`
2134
+ `).replace(/\bAgent tool\b/g, "delegate through the available MCP or agent orchestration tools").replace(/\bTodoWrite\b/g, "track tasks with the agent's native task mechanism").replace(/\bRead\/Edit\b/g, "read and edit files with the available filesystem tools").replace(/\bRead\b/g, "read files with the available filesystem tools").replace(/\bEdit\b/g, "edit files with the available filesystem tools").replace(/\bBash\b/g, "run shell commands with the available terminal tool").trim());
2135
+ }
2136
+ function applyTransform(source, output, context = {}) {
2137
+ switch (output.transform) {
2138
+ case "passthrough":
2139
+ case "claude-passthrough":
2140
+ return ensureTrailingNewline2(source.content);
2141
+ case "codex-flat":
2142
+ return buildCodexAgentsMd(source, context);
2143
+ case "opencode-flat":
2144
+ return buildOpenCodeAgentsMd(source, context);
2145
+ case "cursor-mdc":
2146
+ return buildCursorMdc(source);
2147
+ case "skill-neutral":
2148
+ return transformSkillContent(source.content);
2321
2149
  }
2322
2150
  }
2323
- function rejectDuplicateSourceSlugs(sources) {
2324
- const seen = new Map;
2325
- for (const source of sources) {
2326
- const existing = seen.get(source.normalizedId);
2327
- if (existing)
2328
- throw new Error(`Duplicate session instruction source slug: ${source.normalizedId} (${existing}, ${source.id})`);
2329
- seen.set(source.normalizedId, source.id);
2151
+
2152
+ // src/lib/apply.ts
2153
+ function getConfigHome() {
2154
+ return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir3();
2155
+ }
2156
+ function expandPath(p) {
2157
+ if (p.startsWith("~/")) {
2158
+ return resolve2(getConfigHome(), p.slice(2));
2330
2159
  }
2160
+ return resolve2(p);
2331
2161
  }
2332
- function rejectDuplicateRulePaths(sources) {
2333
- const seen = new Map;
2334
- for (const source of sources) {
2335
- for (const rule of source.resolvedRules) {
2336
- const key = rule.resolvedPath.toLowerCase();
2337
- const existing = seen.get(key);
2338
- if (existing)
2339
- throw new Error(`Duplicate instruction rule path: ${rule.resolvedPath} (${existing}, ${rule.id})`);
2340
- seen.set(key, rule.id);
2162
+ function normalizeTargetPath(p) {
2163
+ const expanded = expandPath(p);
2164
+ try {
2165
+ return realpathSync2(expanded);
2166
+ } catch {
2167
+ let current = expanded;
2168
+ const missingSegments = [];
2169
+ while (true) {
2170
+ if (existsSync4(current)) {
2171
+ try {
2172
+ return resolve2(realpathSync2(current), ...missingSegments);
2173
+ } catch {
2174
+ return expanded;
2175
+ }
2176
+ }
2177
+ const parent = dirname2(current);
2178
+ const name = basename3(current);
2179
+ if (parent === current)
2180
+ return expanded;
2181
+ missingSegments.unshift(name);
2182
+ current = parent;
2341
2183
  }
2342
2184
  }
2343
2185
  }
2344
- function normalizeRulePath(path) {
2345
- if (!path.trim())
2346
- throw new Error("Instruction rule path cannot be empty.");
2347
- if (path.includes("\\"))
2348
- throw new Error(`Instruction rule path must use POSIX separators: ${path}`);
2349
- const normalized = posix.normalize(path);
2350
- if (normalized === "." || posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2351
- throw new Error(`Instruction rule path escapes managed rule directory: ${path}`);
2186
+ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
2187
+ const renderedTargetPath = opts.vars ? renderMachineAwareContent(targetPath, opts.vars) : targetPath;
2188
+ const renderedContent = opts.vars ? renderMachineAwareContent(content, opts.vars) : content;
2189
+ const targetAgent = meta.agent ?? config.agent;
2190
+ if (isAntigravityRuleTarget(targetAgent, renderedTargetPath) && renderedContent.length > ANTIGRAVITY_RULE_FILE_CHAR_LIMIT) {
2191
+ throw new ConfigApplyError(`Antigravity rule file ${renderedTargetPath} is ${renderedContent.length} characters; split it before applying because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
2192
+ }
2193
+ const path = expandPath(renderedTargetPath);
2194
+ const previousContent = existsSync4(path) ? readFileSync2(path, "utf-8") : null;
2195
+ const changed = previousContent !== renderedContent;
2196
+ if (!opts.dryRun) {
2197
+ const dir = dirname2(path);
2198
+ if (!existsSync4(dir)) {
2199
+ mkdirSync2(dir, { recursive: true });
2200
+ }
2201
+ if (previousContent !== null && changed) {
2202
+ const store = opts.store ?? resolveConfigStore();
2203
+ await store.createSnapshot(config.id, previousContent, config.version);
2204
+ }
2205
+ writeFileSync(path, renderedContent, "utf-8");
2352
2206
  }
2353
- return normalized;
2354
- }
2355
- function identitySourceToSessionSource(value, options) {
2356
- const record = asRecord(value, "identity instruction source");
2357
- const providers = asStringArray(record["targetProviders"]);
2358
- if (options.tool && providers.length > 0 && !providerTargetsTool(providers, options.tool))
2359
- return null;
2360
- const sourcePaths = normalizeSourcePaths(record["sourcePaths"]);
2361
- const kind = maybeString(record["kind"]);
2362
- const layer = record["layer"] === undefined ? layerFromIdentityKind(kind, options.exportShape) : requireLayer(record["layer"]);
2363
- const merge = requireMerge(record["merge"] ?? record["mergePolicy"] ?? "append");
2364
- const id = requireString(record["id"], "identity instruction source id");
2365
- const inlineContent = maybeString(record["content"]);
2366
- const resolvedContent = inlineContent && inlineContent.trim() ? inlineContent : contentFromIdentitySourcePaths(sourcePaths, options.path, id) ?? inlineContent;
2367
2207
  return {
2368
- id,
2369
- label: maybeString(record["label"]) ?? maybeString(record["title"]) ?? id,
2370
- layer,
2371
- merge,
2372
- order: typeof record["order"] === "number" ? record["order"] : typeof record["precedence"] === "number" ? record["precedence"] : options.orderFallback,
2373
- content: resolvedContent ?? "",
2374
- path: options.path,
2375
- rules: normalizeIdentityRules(record["rules"]),
2376
- provenance: asOptionalRecord(record["provenance"]) ?? null,
2377
- targetProviders: providers,
2378
- owner: normalizeIdentityOwner(record["owner"]),
2379
- sourcePaths,
2380
- globs: asStringArray(record["globs"]),
2381
- hash: maybeString(record["hash"]),
2382
- nonOverridable: record["nonOverridable"] === true,
2383
- replacementScope: maybeString(record["replacementScope"]),
2384
- metadata: asOptionalRecord(record["metadata"]) ?? null
2208
+ config_id: config.id,
2209
+ path,
2210
+ previous_content: previousContent,
2211
+ new_content: renderedContent,
2212
+ dry_run: opts.dryRun ?? false,
2213
+ changed,
2214
+ ...meta
2385
2215
  };
2386
2216
  }
2387
- function layerFromIdentityKind(kind, exportShape) {
2388
- if (!kind) {
2389
- if (exportShape === "configs-contract")
2390
- throw new Error("Invalid session instruction layer: undefined");
2391
- return "agent";
2217
+ function isAntigravityRuleTarget(agent, targetPath) {
2218
+ return agent === "antigravity" && /\.(md|mdc|markdown)$/i.test(targetPath);
2219
+ }
2220
+ function isGeneratedOutputTarget(config, configs) {
2221
+ if (!config.target_path)
2222
+ return false;
2223
+ const targetPath = normalizeTargetPath(config.target_path);
2224
+ return configs.some((candidate) => candidate.id !== config.id && candidate.outputs.some((output) => normalizeTargetPath(output.target_path) === targetPath));
2225
+ }
2226
+ async function applyConfig(config, opts = {}) {
2227
+ if (opts.outputAgent && isRetiredOrUnsupportedConfigAgent(opts.outputAgent)) {
2228
+ throw new ConfigApplyError(`Config output agent "${opts.outputAgent}" is retired or unsupported \u2014 cannot apply to disk.`);
2392
2229
  }
2393
- switch (kind) {
2394
- case "global-rules":
2395
- case "global-system-prompt":
2396
- return "global";
2397
- case "provider-rules":
2398
- case "provider-system-prompt":
2399
- return "tool";
2400
- case "identity-doc":
2401
- case "persona-doc":
2402
- return "agent";
2403
- case "account-overlay":
2404
- return "account";
2405
- case "project-overlay":
2406
- return "project";
2407
- case "machine-overlay":
2408
- case "session-overlay":
2409
- return "local";
2410
- default:
2411
- throw new Error(`Invalid identity instruction source kind: ${kind}`);
2230
+ if (isRetiredOrUnsupportedConfigAgent(config.agent)) {
2231
+ throw new ConfigApplyError(`Config "${config.name}" uses retired or unsupported agent "${config.agent}" \u2014 cannot apply to disk.`);
2232
+ }
2233
+ const selectedOutputs = opts.outputAgent ? config.outputs.filter((output) => output.agent === opts.outputAgent) : config.outputs.filter((output) => !isRetiredOrUnsupportedConfigAgent(output.agent));
2234
+ const shouldApplyPrimary = !opts.outputAgent || config.agent === opts.outputAgent;
2235
+ if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
2236
+ throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
2237
+ }
2238
+ const store = opts.store ?? resolveConfigStore();
2239
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
2240
+ if (isGeneratedOutputTarget(config, contextConfigs)) {
2241
+ throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
2242
+ }
2243
+ const outputResults = [];
2244
+ for (const output of selectedOutputs) {
2245
+ outputResults.push(await applyOutput(config, output, contextConfigs, opts));
2246
+ }
2247
+ let result;
2248
+ if (config.target_path && shouldApplyPrimary) {
2249
+ result = await writeConfigResult(config, config.target_path, config.content, opts);
2250
+ result.outputs = outputResults;
2251
+ result.changed = result.changed || outputResults.some((output) => output.changed);
2252
+ } else {
2253
+ result = {
2254
+ ...outputResults[0],
2255
+ outputs: outputResults.slice(1),
2256
+ changed: outputResults.some((output) => output.changed)
2257
+ };
2258
+ }
2259
+ if (!opts.dryRun) {
2260
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
2412
2261
  }
2262
+ return result;
2413
2263
  }
2414
- function contentFromIdentitySourcePaths(sourcePaths, exportPath, sourceId) {
2415
- if (sourcePaths.length === 0 || !exportPath)
2416
- return;
2417
- const baseDir = dirname2(resolveSessionPath(exportPath));
2418
- const contents = [];
2419
- for (const sourcePath of sourcePaths) {
2420
- const content = readIdentitySourcePath(sourcePath, baseDir, sourceId);
2421
- if (content !== undefined)
2422
- contents.push({ path: sourcePath.path, content });
2264
+ async function applyOutput(config, output, contextConfigs, opts) {
2265
+ const content = applyTransform(config, output, { configs: contextConfigs });
2266
+ return writeConfigResult(config, output.target_path, content, opts, {
2267
+ agent: output.agent,
2268
+ transform: output.transform
2269
+ });
2270
+ }
2271
+ async function applyConfigs(configs, opts = {}) {
2272
+ const results = [];
2273
+ for (const config of configs) {
2274
+ if (config.kind === "reference")
2275
+ continue;
2276
+ if (isRetiredOrUnsupportedConfigAgent(config.agent))
2277
+ continue;
2278
+ results.push(await applyConfig(config, opts));
2423
2279
  }
2424
- if (contents.length === 0)
2425
- return;
2426
- if (contents.length === 1)
2427
- return ensureTrailingNewline2(contents[0].content);
2428
- return ensureTrailingNewline2(contents.map((item) => `<!-- Source path: ${item.path} -->
2429
- ${item.content.trimEnd()}`).join(`
2280
+ return results;
2281
+ }
2430
2282
 
2431
- `));
2283
+ // src/lib/package-version.ts
2284
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
2285
+ import { dirname as dirname3, join as join4 } from "path";
2286
+ import { fileURLToPath } from "url";
2287
+ var cached = null;
2288
+ function getPackageVersion() {
2289
+ if (cached)
2290
+ return cached;
2291
+ try {
2292
+ let dir = dirname3(fileURLToPath(import.meta.url));
2293
+ for (let i = 0;i < 8; i++) {
2294
+ const pkgPath = join4(dir, "package.json");
2295
+ if (existsSync5(pkgPath)) {
2296
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
2297
+ if (pkg.name === "@hasna/instructions" && pkg.version) {
2298
+ cached = pkg.version;
2299
+ return cached;
2300
+ }
2301
+ }
2302
+ const parent = dirname3(dir);
2303
+ if (parent === dir)
2304
+ break;
2305
+ dir = parent;
2306
+ }
2307
+ } catch {}
2308
+ cached = "0.0.0";
2309
+ return cached;
2432
2310
  }
2433
- function readIdentitySourcePath(sourcePath, baseDir, sourceId) {
2434
- const resolvedPath = resolveIdentitySourcePath(sourcePath.path, baseDir, sourceId);
2435
- if (!existsSync5(resolvedPath)) {
2436
- if (sourcePath.required) {
2437
- throw new Error(`Required identity instruction source path not found for ${sourceId}: ${sourcePath.path}`);
2311
+
2312
+ // src/lib/redact.ts
2313
+ var SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
2314
+ var VALUE_PATTERNS = [
2315
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
2316
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
2317
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
2318
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
2319
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
2320
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
2321
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
2322
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
2323
+ ];
2324
+ var MIN_SECRET_VALUE_LEN = 8;
2325
+ function redactShell(content) {
2326
+ const redacted = [];
2327
+ const lines = content.split(`
2328
+ `);
2329
+ const out = [];
2330
+ for (let i = 0;i < lines.length; i++) {
2331
+ const line = lines[i];
2332
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2333
+ if (m) {
2334
+ const [, prefix, key, eq, quote, value] = m;
2335
+ if (shouldRedactKeyValue(key, value)) {
2336
+ const reason = reasonFor(key, value);
2337
+ redacted.push({ varName: key, line: i + 1, reason });
2338
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
2339
+ continue;
2340
+ }
2438
2341
  }
2439
- return;
2440
- }
2441
- const stat = statSync(resolvedPath);
2442
- if (!stat.isFile()) {
2443
- throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
2444
- }
2445
- const realBase = realpathSync2(baseDir);
2446
- const realPath = realpathSync2(resolvedPath);
2447
- if (!pathIsInside(realPath, realBase)) {
2448
- throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
2342
+ out.push(line);
2449
2343
  }
2450
- return readFileSync3(realPath, "utf-8");
2344
+ return { content: out.join(`
2345
+ `), redacted, isTemplate: redacted.length > 0 };
2451
2346
  }
2452
- function resolveIdentitySourcePath(path, baseDir, sourceId) {
2453
- const cleaned = cleanSessionPathInput(path);
2454
- if (!cleaned)
2455
- throw new Error(`Identity instruction source path cannot be empty for ${sourceId}.`);
2456
- if (cleaned.includes("\\"))
2457
- throw new Error(`Identity instruction source path must use POSIX separators for ${sourceId}: ${path}`);
2458
- const resolvedPath = isAbsolute(cleaned) ? resolve2(cleaned) : resolve2(baseDir, cleaned);
2459
- if (!pathIsInside(resolvedPath, resolve2(baseDir))) {
2460
- throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${path}`);
2347
+ function redactJson(content) {
2348
+ const redacted = [];
2349
+ const lines = content.split(`
2350
+ `);
2351
+ const out = [];
2352
+ for (let i = 0;i < lines.length; i++) {
2353
+ const line = lines[i];
2354
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
2355
+ if (m) {
2356
+ const [, prefix, key, value, comma, trail] = m;
2357
+ if (shouldRedactKeyValue(key, value)) {
2358
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2359
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2360
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
2361
+ continue;
2362
+ }
2363
+ }
2364
+ let newLine = line;
2365
+ for (const { re, reason } of VALUE_PATTERNS) {
2366
+ newLine = newLine.replace(re, (match) => {
2367
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
2368
+ redacted.push({ varName, line: i + 1, reason });
2369
+ return `{{${varName}}}`;
2370
+ });
2371
+ }
2372
+ out.push(newLine);
2461
2373
  }
2462
- return resolvedPath;
2374
+ return { content: out.join(`
2375
+ `), redacted, isTemplate: redacted.length > 0 };
2463
2376
  }
2464
- function pathIsInside(path, baseDir) {
2465
- const rel = relative(baseDir, path);
2466
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
2377
+ function redactToml(content) {
2378
+ const redacted = [];
2379
+ const lines = content.split(`
2380
+ `);
2381
+ const out = [];
2382
+ for (let i = 0;i < lines.length; i++) {
2383
+ const line = lines[i];
2384
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
2385
+ if (m) {
2386
+ const [, indent, key, eq, quote, value] = m;
2387
+ if (shouldRedactKeyValue(key, value)) {
2388
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2389
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2390
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
2391
+ continue;
2392
+ }
2393
+ }
2394
+ out.push(line);
2395
+ }
2396
+ return { content: out.join(`
2397
+ `), redacted, isTemplate: redacted.length > 0 };
2467
2398
  }
2468
- function providerTargetsTool(targets, tool) {
2469
- return targets.map((target) => target.toLowerCase()).some((target) => target === tool || target === "all" || target === "generic");
2399
+ function redactIni(content) {
2400
+ const redacted = [];
2401
+ const lines = content.split(`
2402
+ `);
2403
+ const out = [];
2404
+ for (let i = 0;i < lines.length; i++) {
2405
+ const line = lines[i];
2406
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
2407
+ if (authM && !isReferenceValue(authM[2].trim())) {
2408
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
2409
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
2410
+ continue;
2411
+ }
2412
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
2413
+ if (m) {
2414
+ const [, indent, key, eq, value] = m;
2415
+ if (shouldRedactKeyValue(key, value)) {
2416
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2417
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
2418
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
2419
+ continue;
2420
+ }
2421
+ }
2422
+ out.push(line);
2423
+ }
2424
+ return { content: out.join(`
2425
+ `), redacted, isTemplate: redacted.length > 0 };
2470
2426
  }
2471
- function normalizeIdentityRules(value) {
2472
- if (value === undefined || value === null)
2473
- return [];
2474
- if (!Array.isArray(value))
2475
- throw new Error("Identity instruction source rules must be an array.");
2476
- return value.map((item) => {
2477
- const record = asRecord(item, "identity instruction rule");
2478
- const id = requireString(record["id"], "identity instruction rule id");
2479
- return {
2480
- id,
2481
- label: maybeString(record["label"]) ?? id,
2482
- path: maybeString(record["path"]),
2483
- content: maybeString(record["content"]) ?? "",
2484
- globs: asStringArray(record["globs"]),
2485
- hash: maybeString(record["hash"]),
2486
- metadata: asOptionalRecord(record["metadata"]) ?? null
2487
- };
2488
- });
2427
+ function redactGeneric(content) {
2428
+ const redacted = [];
2429
+ const lines = content.split(`
2430
+ `);
2431
+ const out = [];
2432
+ for (let i = 0;i < lines.length; i++) {
2433
+ let line = lines[i];
2434
+ for (const { re, reason } of VALUE_PATTERNS) {
2435
+ line = line.replace(re, (match) => {
2436
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2437
+ redacted.push({ varName, line: i + 1, reason });
2438
+ return `{{${varName}}}`;
2439
+ });
2440
+ }
2441
+ out.push(line);
2442
+ }
2443
+ return { content: out.join(`
2444
+ `), redacted, isTemplate: redacted.length > 0 };
2489
2445
  }
2490
- function normalizeIdentityOwner(value) {
2491
- if (value === undefined || value === null)
2492
- return null;
2493
- const record = asRecord(value, "identity instruction owner");
2494
- return {
2495
- kind: requireString(record["kind"], "identity instruction owner kind"),
2496
- id: requireString(record["id"], "identity instruction owner id")
2497
- };
2446
+ function shouldRedactKeyValue(key, value) {
2447
+ if (!value || value.startsWith("{{"))
2448
+ return false;
2449
+ if (isReferenceValue(value.trim()))
2450
+ return false;
2451
+ if (value.length < MIN_SECRET_VALUE_LEN)
2452
+ return false;
2453
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
2454
+ return false;
2455
+ if (SECRET_KEY_PATTERN.test(key))
2456
+ return true;
2457
+ for (const { re } of VALUE_PATTERNS) {
2458
+ if (re.test(value))
2459
+ return true;
2460
+ }
2461
+ return false;
2498
2462
  }
2499
- function normalizeSourcePaths(value) {
2500
- if (value === undefined || value === null)
2501
- return [];
2502
- if (!Array.isArray(value))
2503
- throw new Error("Identity instruction source paths must be an array.");
2504
- return value.map((item) => {
2505
- const record = asRecord(item, "identity instruction source path");
2506
- return {
2507
- path: requireString(record["path"], "identity instruction source path"),
2508
- editable: record["editable"] === true,
2509
- required: record["required"] === true,
2510
- hash: maybeString(record["hash"])
2511
- };
2512
- });
2463
+ function reasonFor(key, value) {
2464
+ if (SECRET_KEY_PATTERN.test(key))
2465
+ return `secret key name: ${key}`;
2466
+ for (const { re, reason } of VALUE_PATTERNS) {
2467
+ if (re.test(value))
2468
+ return reason;
2469
+ }
2470
+ return "secret value pattern";
2513
2471
  }
2514
- function requireLayer(value) {
2515
- if (value === "global" || value === "tool" || value === "account" || value === "agent" || value === "project" || value === "local")
2516
- return value;
2517
- throw new Error(`Invalid session instruction layer: ${String(value)}`);
2472
+ function isReferenceValue(value) {
2473
+ return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
2518
2474
  }
2519
- function requireMerge(value) {
2520
- if (value === "append" || value === "replace")
2521
- return value;
2522
- throw new Error(`Invalid session instruction merge policy: ${String(value)}`);
2475
+ function redactContent(content, format) {
2476
+ switch (format) {
2477
+ case "shell":
2478
+ return redactShell(content);
2479
+ case "json":
2480
+ return redactJson(content);
2481
+ case "toml":
2482
+ return redactToml(content);
2483
+ case "ini":
2484
+ return redactIni(content);
2485
+ default:
2486
+ return redactGeneric(content);
2487
+ }
2523
2488
  }
2524
- function asRecord(value, label) {
2525
- if (!value || typeof value !== "object" || Array.isArray(value))
2526
- throw new Error(`Invalid ${label}.`);
2527
- return value;
2489
+ function scanSecrets(content, format) {
2490
+ const r = redactContent(content, format);
2491
+ return r.redacted;
2528
2492
  }
2529
- function asOptionalRecord(value) {
2530
- if (value === undefined || value === null)
2531
- return null;
2532
- return asRecord(value, "record");
2493
+ function hasSecrets(content, format) {
2494
+ return scanSecrets(content, format).length > 0;
2533
2495
  }
2534
- function maybeString(value) {
2535
- return typeof value === "string" ? value : undefined;
2496
+
2497
+ // src/status.ts
2498
+ var PACKAGE_NAME = "@hasna/instructions";
2499
+ var PACKAGE_VERSION = getPackageVersion();
2500
+ function activeDatabaseEnv() {
2501
+ if (process.env["HASNA_INSTRUCTIONS_DB_PATH"])
2502
+ return "HASNA_INSTRUCTIONS_DB_PATH";
2503
+ return null;
2536
2504
  }
2537
- function requireString(value, label) {
2538
- if (typeof value !== "string" || !value.trim())
2539
- throw new Error(`Invalid ${label}.`);
2540
- return value;
2505
+ function configuredDatabaseKind() {
2506
+ const value = process.env["HASNA_INSTRUCTIONS_DB_PATH"] ?? "";
2507
+ return value === ":memory:" || value.startsWith("file::memory:") ? "memory" : "file";
2541
2508
  }
2542
- function asStringArray(value) {
2543
- if (value === undefined || value === null)
2544
- return [];
2545
- if (!Array.isArray(value))
2546
- return [];
2547
- return value.filter((item) => typeof item === "string");
2509
+ function countBy(items, getValue) {
2510
+ const counts = {};
2511
+ for (const item of items) {
2512
+ const value = getValue(item);
2513
+ if (!value)
2514
+ continue;
2515
+ counts[value] = (counts[value] ?? 0) + 1;
2516
+ }
2517
+ return counts;
2518
+ }
2519
+ async function getConfigsStatus(store = resolveConfigStore()) {
2520
+ let databaseReachable = true;
2521
+ let configs = [];
2522
+ let categoryStats = { total: 0 };
2523
+ try {
2524
+ configs = await store.listConfigs();
2525
+ categoryStats = await store.getConfigStats();
2526
+ } catch {
2527
+ databaseReachable = false;
2528
+ }
2529
+ const fileConfigs = configs.filter((config) => config.kind === "file");
2530
+ const retiredAgentRows = configs.filter((config) => isRetiredOrUnsupportedConfigAgent(config.agent)).length;
2531
+ let driftedTargets = 0;
2532
+ let missingTargets = 0;
2533
+ let unredactedSecretFindings = 0;
2534
+ let knownTargets = 0;
2535
+ for (const config of fileConfigs) {
2536
+ unredactedSecretFindings += scanSecrets(config.content, config.format).length;
2537
+ if (isRetiredOrUnsupportedConfigAgent(config.agent))
2538
+ continue;
2539
+ if (!config.target_path)
2540
+ continue;
2541
+ knownTargets += 1;
2542
+ const targetPath = expandPath(config.target_path);
2543
+ if (!existsSync6(targetPath)) {
2544
+ missingTargets += 1;
2545
+ continue;
2546
+ }
2547
+ const disk = readFileSync4(targetPath, "utf-8");
2548
+ const { content: redactedDisk } = redactContent(disk, config.format);
2549
+ if (redactedDisk !== config.content) {
2550
+ driftedTargets += 1;
2551
+ }
2552
+ }
2553
+ let profiles = 0;
2554
+ let machines = 0;
2555
+ let profileLinks = 0;
2556
+ let snapshots = 0;
2557
+ if (databaseReachable) {
2558
+ try {
2559
+ const profileList = await store.listProfiles();
2560
+ profiles = profileList.length;
2561
+ machines = (await store.listMachines()).length;
2562
+ for (const profile of profileList) {
2563
+ profileLinks += (await store.getProfileConfigs(profile.id)).length;
2564
+ }
2565
+ for (const config of configs) {
2566
+ snapshots += (await store.listSnapshots(config.id)).length;
2567
+ }
2568
+ } catch {
2569
+ databaseReachable = false;
2570
+ }
2571
+ }
2572
+ const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
2573
+ const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 && retiredAgentRows === 0 ? "ok" : "warn";
2574
+ return {
2575
+ service: "configs",
2576
+ schemaVersion: "1.0",
2577
+ package: {
2578
+ name: PACKAGE_NAME,
2579
+ version: PACKAGE_VERSION
2580
+ },
2581
+ env: {
2582
+ database: {
2583
+ primary: "HASNA_INSTRUCTIONS_DB_PATH",
2584
+ active: activeDatabaseEnv(),
2585
+ kind: configuredDatabaseKind()
2586
+ }
2587
+ },
2588
+ counts: {
2589
+ configs: {
2590
+ total: configs.length,
2591
+ file: fileConfigs.length,
2592
+ reference: configs.filter((config) => config.kind === "reference").length,
2593
+ templates: configs.filter((config) => config.is_template).length,
2594
+ retiredAgentRows
2595
+ },
2596
+ byCategory,
2597
+ byAgent: countBy(configs, (config) => config.agent),
2598
+ byFormat: countBy(configs, (config) => config.format),
2599
+ profiles,
2600
+ profileLinks,
2601
+ machines,
2602
+ snapshots,
2603
+ knownTargets
2604
+ },
2605
+ health: {
2606
+ status,
2607
+ databaseReachable,
2608
+ driftedTargets,
2609
+ missingTargets,
2610
+ unredactedSecretFindings,
2611
+ retiredAgentRows,
2612
+ hasDrift: driftedTargets > 0,
2613
+ hasMissingTargets: missingTargets > 0,
2614
+ hasUnredactedSecrets: unredactedSecretFindings > 0,
2615
+ hasRetiredAgentRows: retiredAgentRows > 0
2616
+ },
2617
+ safety: {
2618
+ includesConfigValues: false,
2619
+ includesPrivatePaths: false,
2620
+ includesHostnames: false,
2621
+ includesSecretValues: false,
2622
+ statusOutputIsMetadataOnly: true
2623
+ }
2624
+ };
2548
2625
  }
2626
+ // src/db/pg-migrations.ts
2627
+ var PG_MIGRATIONS = [
2628
+ `CREATE TABLE IF NOT EXISTS configs (
2629
+ id TEXT PRIMARY KEY,
2630
+ name TEXT NOT NULL,
2631
+ slug TEXT NOT NULL UNIQUE,
2632
+ kind TEXT NOT NULL DEFAULT 'file',
2633
+ category TEXT NOT NULL,
2634
+ agent TEXT NOT NULL DEFAULT 'global',
2635
+ target_path TEXT,
2636
+ outputs TEXT NOT NULL DEFAULT '[]',
2637
+ format TEXT NOT NULL DEFAULT 'text',
2638
+ content TEXT NOT NULL DEFAULT '',
2639
+ description TEXT,
2640
+ tags TEXT NOT NULL DEFAULT '[]',
2641
+ is_template BOOLEAN NOT NULL DEFAULT FALSE,
2642
+ version INTEGER NOT NULL DEFAULT 1,
2643
+ created_at TEXT NOT NULL,
2644
+ updated_at TEXT NOT NULL,
2645
+ synced_at TEXT
2646
+ )`,
2647
+ `CREATE TABLE IF NOT EXISTS config_snapshots (
2648
+ id TEXT PRIMARY KEY,
2649
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
2650
+ content TEXT NOT NULL,
2651
+ version INTEGER NOT NULL,
2652
+ created_at TEXT NOT NULL
2653
+ )`,
2654
+ `CREATE TABLE IF NOT EXISTS profiles (
2655
+ id TEXT PRIMARY KEY,
2656
+ name TEXT NOT NULL,
2657
+ slug TEXT NOT NULL UNIQUE,
2658
+ description TEXT,
2659
+ selectors TEXT NOT NULL DEFAULT '{}',
2660
+ variables TEXT NOT NULL DEFAULT '{}',
2661
+ created_at TEXT NOT NULL,
2662
+ updated_at TEXT NOT NULL
2663
+ )`,
2664
+ `CREATE TABLE IF NOT EXISTS profile_configs (
2665
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
2666
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
2667
+ sort_order INTEGER NOT NULL DEFAULT 0,
2668
+ PRIMARY KEY (profile_id, config_id)
2669
+ )`,
2670
+ `CREATE TABLE IF NOT EXISTS machines (
2671
+ id TEXT PRIMARY KEY,
2672
+ hostname TEXT NOT NULL UNIQUE,
2673
+ os TEXT,
2674
+ arch TEXT,
2675
+ last_applied_at TEXT,
2676
+ created_at TEXT NOT NULL
2677
+ )`,
2678
+ `CREATE TABLE IF NOT EXISTS feedback (
2679
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
2680
+ message TEXT NOT NULL,
2681
+ email TEXT,
2682
+ category TEXT DEFAULT 'general',
2683
+ version TEXT,
2684
+ machine_id TEXT,
2685
+ created_at TEXT NOT NULL DEFAULT NOW()::text
2686
+ )`,
2687
+ `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
2688
+ ];
2549
2689
  // src/lib/session-apply.ts
2550
2690
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2551
2691
  import {
2552
- existsSync as existsSync6,
2692
+ existsSync as existsSync7,
2553
2693
  lstatSync,
2554
2694
  mkdirSync as mkdirSync3,
2555
- readFileSync as readFileSync4,
2695
+ readFileSync as readFileSync5,
2556
2696
  renameSync,
2557
2697
  rmSync as rmSync2,
2558
2698
  writeFileSync as writeFileSync2
2559
2699
  } from "fs";
2560
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
2700
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
2561
2701
  class SessionApplyError extends Error {
2562
2702
  constructor(message) {
2563
2703
  super(message);
@@ -2598,7 +2738,7 @@ function applySessionRender(plan, options = {}) {
2598
2738
  snapshotPath = writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest);
2599
2739
  for (const file of files) {
2600
2740
  const target = resolvePlannedFilePath(plan, file, targetHome);
2601
- const existingContent = existsSync6(target) ? readFileSync4(target, "utf-8") : null;
2741
+ const existingContent = existsSync7(target) ? readFileSync5(target, "utf-8") : null;
2602
2742
  if (existingContent === file.content)
2603
2743
  continue;
2604
2744
  writePlannedFile(target, file.content, targetHome);
@@ -2607,7 +2747,7 @@ function applySessionRender(plan, options = {}) {
2607
2747
  if (result.action !== "delete")
2608
2748
  continue;
2609
2749
  assertNoSymlinkSegments(targetHome, result.path);
2610
- if (existsSync6(result.path))
2750
+ if (existsSync7(result.path))
2611
2751
  rmSync2(result.path);
2612
2752
  }
2613
2753
  }
@@ -2642,7 +2782,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
2642
2782
  const drifted = [];
2643
2783
  for (const file of previousManifest.files) {
2644
2784
  const target = resolveManifestRelativePath(file.relativePath, safeTargetHome);
2645
- if (!existsSync6(target)) {
2785
+ if (!existsSync7(target)) {
2646
2786
  missing.push({
2647
2787
  path: target,
2648
2788
  relativePath: file.relativePath,
@@ -2652,7 +2792,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
2652
2792
  });
2653
2793
  continue;
2654
2794
  }
2655
- const actualSha256 = sha2562(readFileSync4(target, "utf-8"));
2795
+ const actualSha256 = sha2562(readFileSync5(target, "utf-8"));
2656
2796
  if (actualSha256 !== file.sha256) {
2657
2797
  drifted.push({
2658
2798
  path: target,
@@ -2674,7 +2814,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
2674
2814
  }
2675
2815
  function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
2676
2816
  const target = resolvePlannedFilePath(plan, file, targetHome);
2677
- const previousContent = existsSync6(target) ? readFileSync4(target, "utf-8") : null;
2817
+ const previousContent = existsSync7(target) ? readFileSync5(target, "utf-8") : null;
2678
2818
  const previousSha256 = previousContent === null ? null : sha2562(previousContent);
2679
2819
  const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
2680
2820
  const changed = previousContent !== file.content;
@@ -2757,9 +2897,9 @@ function planStaleFileResults(plan, targetHome, previousManifest, currentRelativ
2757
2897
  }
2758
2898
  function planStaleFileResult(file, targetHome, options) {
2759
2899
  const target = resolveManifestRelativePath(file.relativePath, targetHome);
2760
- if (!existsSync6(target))
2900
+ if (!existsSync7(target))
2761
2901
  return null;
2762
- const previousContent = readFileSync4(target, "utf-8");
2902
+ const previousContent = readFileSync5(target, "utf-8");
2763
2903
  const previousSha256 = sha2562(previousContent);
2764
2904
  if (!options.force && previousSha256 !== file.sha256) {
2765
2905
  return {
@@ -2825,10 +2965,10 @@ function resolveManifestRelativePath(relativePath, targetHome) {
2825
2965
  return target;
2826
2966
  }
2827
2967
  function readPreviousManifest(path) {
2828
- if (!existsSync6(path))
2968
+ if (!existsSync7(path))
2829
2969
  return null;
2830
2970
  try {
2831
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
2971
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
2832
2972
  if (parsed.schema !== SESSION_RENDER_SCHEMA)
2833
2973
  return null;
2834
2974
  if (!Array.isArray(parsed.files))
@@ -2839,16 +2979,16 @@ function readPreviousManifest(path) {
2839
2979
  }
2840
2980
  }
2841
2981
  function writePlannedFile(path, content, targetHome) {
2842
- const dir = dirname3(path);
2982
+ const dir = dirname4(path);
2843
2983
  mkdirSync3(dir, { recursive: true });
2844
2984
  assertNoSymlinkSegments(targetHome, path);
2845
- const tmp = join4(dir, `.session-${randomUUID3()}.tmp`);
2985
+ const tmp = join5(dir, `.session-${randomUUID3()}.tmp`);
2846
2986
  writeFileSync2(tmp, content, "utf-8");
2847
2987
  renameSync(tmp, path);
2848
2988
  }
2849
2989
  function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest) {
2850
- const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync6(result.path)).map((result) => {
2851
- const content = readFileSync4(result.path, "utf-8");
2990
+ const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync7(result.path)).map((result) => {
2991
+ const content = readFileSync5(result.path, "utf-8");
2852
2992
  return {
2853
2993
  path: result.path,
2854
2994
  relativePath: result.relativePath,
@@ -2883,7 +3023,7 @@ function assertSafeTargetHome(targetHome) {
2883
3023
  throw new SessionApplyError(`Session target home cannot be the filesystem root: ${targetHome}`);
2884
3024
  }
2885
3025
  assertNoSymlinkAncestors(normalized);
2886
- if (existsSync6(normalized) && lstatSync(normalized).isSymbolicLink()) {
3026
+ if (existsSync7(normalized) && lstatSync(normalized).isSymbolicLink()) {
2887
3027
  throw new SessionApplyError(`Session target home cannot be a symlink: ${normalized}`);
2888
3028
  }
2889
3029
  return normalized;
@@ -2893,8 +3033,8 @@ function assertNoSymlinkSegments(root, target) {
2893
3033
  const rel = relative2(root, target);
2894
3034
  let current = root;
2895
3035
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
2896
- current = join4(current, segment);
2897
- if (existsSync6(current) && lstatSync(current).isSymbolicLink()) {
3036
+ current = join5(current, segment);
3037
+ if (existsSync7(current) && lstatSync(current).isSymbolicLink()) {
2898
3038
  throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
2899
3039
  }
2900
3040
  }
@@ -2905,8 +3045,8 @@ function assertNoSymlinkAncestors(path) {
2905
3045
  let current = parsed.root;
2906
3046
  const rel = relative2(parsed.root, normalized);
2907
3047
  for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
2908
- current = join4(current, segment);
2909
- if (!existsSync6(current))
3048
+ current = join5(current, segment);
3049
+ if (!existsSync7(current))
2910
3050
  return;
2911
3051
  if (lstatSync(current).isSymbolicLink()) {
2912
3052
  throw new SessionApplyError(`Session apply path uses a symlink ancestor: ${current}`);
@@ -3071,13 +3211,86 @@ async function ensurePlatformProfiles(store = resolveConfigStore()) {
3071
3211
  }
3072
3212
  return ensured;
3073
3213
  }
3214
+ // src/lib/global-agent-rules-standard.ts
3215
+ var GLOBAL_AGENT_RULES_STANDARD_SLUG = "global-agent-rules-standard";
3216
+ var GLOBAL_AGENT_RULES_STANDARD_CONTENT = `# Global Coding Agent Rules Standard
3217
+
3218
+ This standard is managed global/system prompt source content for Hasna coding
3219
+ agents. Rendered agents must receive these rules unless a newer authorized
3220
+ policy source supersedes them.
3221
+
3222
+ ## Session and Planning Defaults
3223
+
3224
+ 1. Use automatic session renaming when the agent supports it. Rename the
3225
+ session early to match the active task; if the task materially pivots, rename
3226
+ it again so operators can identify the run.
3227
+ 2. Never set Codewith goal/token budgets or goal-plan budgets unless the user
3228
+ explicitly asks for a budget. Durable goals and goal plans are unbudgeted by
3229
+ default.
3230
+
3231
+ ## Repository Mutation and Landing
3232
+
3233
+ 3. Repo mutation must happen in a task-scoped worktree. First inspect the canonical worktree root
3234
+ \`$HOME/.hasna/repos/worktrees\`; prefer Hasna repo/project worktree
3235
+ mechanisms when available; otherwise use \`git worktree\`. Never mutate shared checkouts.
3236
+ 4. PR-first landing: normal changes go through a branch/worktree and pull
3237
+ request before landing.
3238
+ 5. Never push directly to \`main\`, the default branch, or any protected branch
3239
+ unless the user explicitly instructs that exact repo and operation.
3240
+
3241
+ ## Autonomy and Source-of-Truth Tools
3242
+
3243
+ 6. Act autonomously. Diagnose, repair, validate, and iterate on the owning
3244
+ CLIs, packages, and workflows before asking the user. Ask only when blocked
3245
+ by destructive decisions, secret-bearing decisions, user-only authority, or
3246
+ external state the agent cannot safely obtain.
3247
+ 7. Use Hasna CLIs/packages as the source of truth: \`todos\`, \`conversations\`,
3248
+ \`mementos\`, \`knowledge\`, \`projects\`, \`repos\`, \`accounts\`,
3249
+ \`instructions\`, \`machines\`, \`secrets\`, and \`access\`.
3250
+ 8. Secrets safety is mandatory. Never expose secrets in prompts, tasks,
3251
+ memories, conversations, manifests, reports, logs, PR text, or any other
3252
+ agent-visible output. Reference vault item names, secret identifiers, and
3253
+ access grants only; never print credential values.
3254
+
3255
+ ## Conversation Surfaces
3256
+
3257
+ 9. Use default conversation surfaces correctly: \`announcements\` for policy,
3258
+ freeze, breaking, cutover, and release notices; \`incidents\` for outages,
3259
+ crash loops, data risk, or security exposure; \`git-publishing\` before and
3260
+ after package publishes; \`git-prs\`, \`git-commits\`, and \`git-releases\`
3261
+ for repository landing events; \`hq\` for broad coordination;
3262
+ \`agent-policy\` for agent operating-rule discussion; project/product
3263
+ channels for normal work; and \`conversations blockers\` for blocker
3264
+ discovery. Do not invent or refer to a literal blockers channel.
3265
+ `;
3266
+ async function ensureGlobalAgentRulesStandardConfig(store = resolveConfigStore()) {
3267
+ const input = {
3268
+ name: "Global Agent Rules Standard",
3269
+ category: "rules",
3270
+ agent: "global",
3271
+ format: "markdown",
3272
+ content: GLOBAL_AGENT_RULES_STANDARD_CONTENT,
3273
+ kind: "reference",
3274
+ description: "Managed global/system prompt rules for Hasna coding agents",
3275
+ tags: ["global-agent-rules", "system-prompt", "coding-agent-rules"]
3276
+ };
3277
+ try {
3278
+ const existing = await store.getConfig(GLOBAL_AGENT_RULES_STANDARD_SLUG);
3279
+ if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind || JSON.stringify(existing.tags) !== JSON.stringify(input.tags)) {
3280
+ return await store.updateConfig(existing.id, input);
3281
+ }
3282
+ return existing;
3283
+ } catch {
3284
+ return await store.createConfig(input);
3285
+ }
3286
+ }
3074
3287
  // src/lib/sync.ts
3075
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
3076
- import { basename as basename4, extname as extname3, join as join6 } from "path";
3288
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
3289
+ import { basename as basename4, extname as extname3, join as join7 } from "path";
3077
3290
 
3078
3291
  // src/lib/sync-dir.ts
3079
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
3080
- import { join as join5, relative as relative3 } from "path";
3292
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3293
+ import { join as join6, relative as relative3 } from "path";
3081
3294
  import { homedir as homedir4 } from "os";
3082
3295
  var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
3083
3296
  function shouldSkip(p) {
@@ -3086,9 +3299,9 @@ function shouldSkip(p) {
3086
3299
  async function syncFromDir(dir, opts = {}) {
3087
3300
  const store = opts.store ?? resolveConfigStore();
3088
3301
  const absDir = expandPath(dir);
3089
- if (!existsSync7(absDir))
3302
+ if (!existsSync8(absDir))
3090
3303
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
3091
- const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join5(absDir, f)).filter((f) => statSync2(f).isFile());
3304
+ const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join6(absDir, f)).filter((f) => statSync2(f).isFile());
3092
3305
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3093
3306
  const home = homedir4();
3094
3307
  const allConfigs = await store.listConfigs();
@@ -3098,7 +3311,7 @@ async function syncFromDir(dir, opts = {}) {
3098
3311
  continue;
3099
3312
  }
3100
3313
  try {
3101
- const content = readFileSync5(file, "utf-8");
3314
+ const content = readFileSync6(file, "utf-8");
3102
3315
  if (content.length > 500000) {
3103
3316
  result.skipped.push(file + " (too large)");
3104
3317
  continue;
@@ -3143,7 +3356,7 @@ async function syncToDir(dir, opts = {}) {
3143
3356
  }
3144
3357
  function walkDir(dir, files = []) {
3145
3358
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
3146
- const full = join5(dir, entry.name);
3359
+ const full = join6(dir, entry.name);
3147
3360
  if (shouldSkip(full))
3148
3361
  continue;
3149
3362
  if (entry.isDirectory())
@@ -3159,7 +3372,8 @@ var CLAUDE_PROMPT_OUTPUTS = [
3159
3372
  { agent: "codex", target_path: "~/.codex/AGENTS.md", transform: "codex-flat" },
3160
3373
  { agent: "codewith", target_path: "~/.codewith/CODEWITH.md", transform: "codex-flat" },
3161
3374
  { agent: "opencode", target_path: "~/.config/opencode/AGENTS.md", transform: "opencode-flat" },
3162
- { agent: "aicopilot", target_path: "~/.config/aicopilot/AGENTS.md", transform: "opencode-flat" },
3375
+ { agent: "aicopilot", target_path: "~/.config/aicopilot/AICOPILOT.md", transform: "codex-flat" },
3376
+ { agent: "antigravity", target_path: "~/.gemini/GEMINI.md", transform: "codex-flat" },
3163
3377
  { agent: "cursor", target_path: "~/.cursor/rules/claude.mdc", transform: "cursor-mdc" }
3164
3378
  ];
3165
3379
  function claudeRuleOutputs(fileName) {
@@ -3193,7 +3407,7 @@ function isGeneratedOutputTarget2(config, owners) {
3193
3407
  return !!ownerIds && !ownerIds.has(config.id);
3194
3408
  }
3195
3409
  function hasClaudePromptSource() {
3196
- return existsSync8(expandPath("~/.claude/CLAUDE.md"));
3410
+ return existsSync9(expandPath("~/.claude/CLAUDE.md"));
3197
3411
  }
3198
3412
  function hasClaudeRuleSourceForCursorTarget(targetPath) {
3199
3413
  const absoluteTargetPath = expandPath(targetPath);
@@ -3201,7 +3415,7 @@ function hasClaudeRuleSourceForCursorTarget(targetPath) {
3201
3415
  if (!absoluteTargetPath.startsWith(`${absolutePrefix}/`) || !absoluteTargetPath.endsWith(".mdc"))
3202
3416
  return false;
3203
3417
  const stem = basename4(absoluteTargetPath, ".mdc");
3204
- return existsSync8(expandPath(`~/.claude/rules/${stem}.md`)) || existsSync8(expandPath(`~/.claude/rules/${stem}.mdc`));
3418
+ return existsSync9(expandPath(`~/.claude/rules/${stem}.md`)) || existsSync9(expandPath(`~/.claude/rules/${stem}.mdc`));
3205
3419
  }
3206
3420
  function isKnownGeneratedTargetPath(targetPath) {
3207
3421
  const normalizedTargetPath = normalizeTargetPath(targetPath);
@@ -3224,10 +3438,10 @@ var KNOWN_CONFIGS = [
3224
3438
  { path: "~/.cursor/mcp.json", name: "cursor-mcp", category: "mcp", agent: "cursor", format: "json", optional: true, description: "Cursor MCP config (includes Skills MCP server entries)" },
3225
3439
  { path: "~/.codewith/CODEWITH.md", name: "codewith-codewith-md", category: "rules", agent: "codewith", format: "markdown", optional: true },
3226
3440
  { path: "~/.codewith/config.toml", name: "codewith-config", category: "mcp", agent: "codewith", format: "toml", optional: true, description: "codewith config (Codex fork, includes Skills MCP server entries)" },
3227
- { path: "~/.config/aicopilot/AGENTS.md", name: "aicopilot-agents-md", category: "rules", agent: "aicopilot", format: "markdown", optional: true },
3228
- { path: "~/.config/aicopilot/opencode.json", name: "aicopilot-config", category: "mcp", agent: "aicopilot", format: "json", optional: true, description: "AI Copilot config (OpenCode fork, includes Skills MCP server entries)" },
3229
- { path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
3230
- { path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown", optional: true },
3441
+ { path: "~/.config/aicopilot/AICOPILOT.md", name: "aicopilot-aicopilot-md", category: "rules", agent: "aicopilot", format: "markdown", optional: true },
3442
+ { path: "~/.config/aicopilot/aicopilot.json", name: "aicopilot-config", category: "mcp", agent: "aicopilot", format: "json", optional: true, description: "AI Copilot config (includes instructions and MCP server entries)" },
3443
+ { path: "~/.gemini/GEMINI.md", name: "antigravity-global-rules", category: "rules", agent: "antigravity", format: "markdown", optional: true, description: "Google Antigravity global rules file" },
3444
+ { path: "~/.gemini/config/mcp_config.json", name: "antigravity-global-mcp", category: "mcp", agent: "antigravity", format: "json", optional: true, description: "Google Antigravity global MCP server entries" },
3231
3445
  { path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
3232
3446
  { path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
3233
3447
  { path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh", optional: true },
@@ -3245,10 +3459,12 @@ var PROJECT_CONFIG_FILES = [
3245
3459
  { file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
3246
3460
  { file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
3247
3461
  { file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
3248
- { file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" },
3249
3462
  { file: ".opencode/AGENTS.md", category: "rules", agent: "opencode", format: "markdown" },
3250
3463
  { file: ".codewith/CODEWITH.md", category: "rules", agent: "codewith", format: "markdown" },
3251
- { file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
3464
+ { file: ".aicopilot/AICOPILOT.md", category: "rules", agent: "aicopilot", format: "markdown" },
3465
+ { file: "AICOPILOT.md", category: "rules", agent: "aicopilot", format: "markdown" },
3466
+ { file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" },
3467
+ { file: ".agents/mcp_config.json", category: "mcp", agent: "antigravity", format: "json" }
3252
3468
  ];
3253
3469
  async function syncProject(opts) {
3254
3470
  const store = opts.store ?? resolveConfigStore();
@@ -3258,11 +3474,11 @@ async function syncProject(opts) {
3258
3474
  const allConfigs = await store.listConfigs();
3259
3475
  const machine = detectMachineContext();
3260
3476
  for (const pf of PROJECT_CONFIG_FILES) {
3261
- const abs = join6(absDir, pf.file);
3262
- if (!existsSync8(abs))
3477
+ const abs = join7(absDir, pf.file);
3478
+ if (!existsSync9(abs))
3263
3479
  continue;
3264
3480
  try {
3265
- const rawContent = readFileSync6(abs, "utf-8");
3481
+ const rawContent = readFileSync7(abs, "utf-8");
3266
3482
  if (rawContent.length > 500000) {
3267
3483
  result.skipped.push(pf.file);
3268
3484
  continue;
@@ -3290,23 +3506,27 @@ async function syncProject(opts) {
3290
3506
  result.skipped.push(pf.file);
3291
3507
  }
3292
3508
  }
3293
- const rulesDir = join6(absDir, ".claude", "rules");
3294
- if (existsSync8(rulesDir)) {
3295
- const mdFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
3509
+ for (const ruleDir of [
3510
+ { dir: join7(absDir, ".claude", "rules"), agent: "claude", namePrefix: "rules" },
3511
+ { dir: join7(absDir, ".agents", "rules"), agent: "antigravity", namePrefix: "antigravity-rules" }
3512
+ ]) {
3513
+ if (!existsSync9(ruleDir.dir))
3514
+ continue;
3515
+ const mdFiles = readdirSync2(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
3296
3516
  for (const f of mdFiles) {
3297
- const abs = join6(rulesDir, f);
3298
- const raw = readFileSync6(abs, "utf-8");
3517
+ const abs = join7(ruleDir.dir, f);
3518
+ const raw = readFileSync7(abs, "utf-8");
3299
3519
  const redacted = redactContent(raw, "markdown");
3300
3520
  const machineAware = templateizeMachineContent(redacted.content, machine);
3301
3521
  const content = machineAware.content;
3302
3522
  const isTemplate2 = redacted.isTemplate || machineAware.changed;
3303
- const name = `${projectName}/rules/${f}`;
3523
+ const name = `${projectName}/${ruleDir.namePrefix}/${f}`;
3304
3524
  const targetPath = abs.startsWith(getConfigHome()) ? abs.replace(getConfigHome(), "~") : abs;
3305
3525
  const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
3306
3526
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
3307
3527
  if (!existing) {
3308
3528
  if (!opts.dryRun)
3309
- await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3529
+ await store.createConfig({ name, category: "rules", agent: ruleDir.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3310
3530
  result.added++;
3311
3531
  } else if (existing.content !== content) {
3312
3532
  if (!opts.dryRun)
@@ -3334,20 +3554,20 @@ async function syncKnown(opts = {}) {
3334
3554
  for (const known of targets) {
3335
3555
  if (known.rulesDir) {
3336
3556
  const absDir = expandPath(known.rulesDir);
3337
- if (!existsSync8(absDir)) {
3557
+ if (!existsSync9(absDir)) {
3338
3558
  result.skipped.push(known.rulesDir);
3339
3559
  continue;
3340
3560
  }
3341
3561
  const extensions = known.rulesExtensions ?? [".md", ".mdc"];
3342
3562
  const ruleFiles = readdirSync2(absDir).filter((f) => extensions.some((ext) => f.endsWith(ext)));
3343
3563
  for (const f of ruleFiles) {
3344
- const abs2 = join6(absDir, f);
3564
+ const abs2 = join7(absDir, f);
3345
3565
  const targetPath = abs2.replace(home, "~");
3346
3566
  if (existingOutputOwners.has(normalizeTargetPath(targetPath)) || isKnownGeneratedTargetPath(targetPath)) {
3347
3567
  result.skipped.push(`${targetPath} (generated output)`);
3348
3568
  continue;
3349
3569
  }
3350
- const raw = readFileSync6(abs2, "utf-8");
3570
+ const raw = readFileSync7(abs2, "utf-8");
3351
3571
  const redacted = redactContent(raw, "markdown");
3352
3572
  const machineAware = templateizeMachineContent(redacted.content, machine);
3353
3573
  const content = machineAware.content;
@@ -3375,12 +3595,12 @@ async function syncKnown(opts = {}) {
3375
3595
  continue;
3376
3596
  }
3377
3597
  const abs = expandPath(known.path);
3378
- if (!existsSync8(abs)) {
3598
+ if (!existsSync9(abs)) {
3379
3599
  result.skipped.push(known.path);
3380
3600
  continue;
3381
3601
  }
3382
3602
  try {
3383
- const rawContent = readFileSync6(abs, "utf-8");
3603
+ const rawContent = readFileSync7(abs, "utf-8");
3384
3604
  if (rawContent.length > 500000) {
3385
3605
  result.skipped.push(known.path + " (too large)");
3386
3606
  continue;
@@ -3444,6 +3664,10 @@ async function syncToDisk(opts = {}) {
3444
3664
  for (const config of configs) {
3445
3665
  if (!config.target_path && config.outputs.length === 0)
3446
3666
  continue;
3667
+ if (isRetiredOrUnsupportedConfigAgent(config.agent)) {
3668
+ result.skipped.push(`${config.slug} (${retiredOrUnsupportedAgentReason(config.agent)})`);
3669
+ continue;
3670
+ }
3447
3671
  try {
3448
3672
  const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
3449
3673
  r.changed ? result.updated++ : result.unchanged++;
@@ -3455,9 +3679,9 @@ async function syncToDisk(opts = {}) {
3455
3679
  }
3456
3680
  function buildDiff(expectedContent, targetPath) {
3457
3681
  const path = expandPath(targetPath);
3458
- if (!existsSync8(path))
3682
+ if (!existsSync9(path))
3459
3683
  return `(file not found on disk: ${path})`;
3460
- const diskContent = readFileSync6(path, "utf-8");
3684
+ const diskContent = readFileSync7(path, "utf-8");
3461
3685
  if (diskContent === expectedContent)
3462
3686
  return "(no diff \u2014 identical)";
3463
3687
  const stored = expectedContent.split(`
@@ -3510,12 +3734,12 @@ async function diffConfig(config, opts = {}) {
3510
3734
  }
3511
3735
  function detectCategory(filePath) {
3512
3736
  const p = filePath.toLowerCase().replace(getConfigHome(), "~");
3513
- if (p.includes("/.claude/rules/") || p.includes("/.cursor/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("codewith.md") || p.endsWith("gemini.md") || p.endsWith(".mdc"))
3737
+ if (p.includes("/.claude/rules/") || p.includes("/.cursor/rules/") || p.includes("/.agents/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("codewith.md") || p.endsWith("aicopilot.md") || p.endsWith("/.gemini/gemini.md") || p.endsWith(".mdc"))
3514
3738
  return "rules";
3515
- if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.gemini/") || p.includes("/.cursor/") || p.includes("/.config/opencode/") || p.includes("/.codewith/") || p.includes("/.config/aicopilot/"))
3516
- return "agent";
3517
3739
  if (p.includes(".mcp.json") || p.includes("mcp"))
3518
3740
  return "mcp";
3741
+ if (p.includes("/.claude/") || p.includes("/.codex/") || p.includes("/.antigravity/") || p.includes("/.agents/") || p.includes("/.cursor/") || p.includes("/.config/opencode/") || p.includes("/.codewith/") || p.includes("/.config/aicopilot/"))
3742
+ return "agent";
3519
3743
  if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc") || p.includes(".bash_profile"))
3520
3744
  return "shell";
3521
3745
  if (p.includes(".gitconfig") || p.includes(".gitignore"))
@@ -3528,6 +3752,10 @@ function detectCategory(filePath) {
3528
3752
  }
3529
3753
  function detectAgent(filePath) {
3530
3754
  const p = filePath.toLowerCase().replace(getConfigHome(), "~");
3755
+ if (p.endsWith("/.gemini/gemini.md") || p.endsWith("/.gemini/config/mcp_config.json"))
3756
+ return "antigravity";
3757
+ if (p.includes("/.agents/rules/") || p.endsWith("/.agents/mcp_config.json"))
3758
+ return "antigravity";
3531
3759
  if (p.includes("/.claude/") || p.endsWith("claude.md"))
3532
3760
  return "claude";
3533
3761
  if (p.includes("/.config/opencode/"))
@@ -3538,10 +3766,10 @@ function detectAgent(filePath) {
3538
3766
  return "codewith";
3539
3767
  if (p.includes("/.config/aicopilot/"))
3540
3768
  return "aicopilot";
3769
+ if (p.includes("/.antigravity/"))
3770
+ return "antigravity";
3541
3771
  if (p.includes("/.codex/") || p.endsWith("agents.md"))
3542
3772
  return "codex";
3543
- if (p.includes("/.gemini/") || p.endsWith("gemini.md"))
3544
- return "gemini";
3545
3773
  if (p.includes(".zshrc") || p.includes(".zprofile") || p.includes(".bashrc"))
3546
3774
  return "zsh";
3547
3775
  if (p.includes(".gitconfig") || p.includes(".gitignore"))
@@ -3565,15 +3793,15 @@ function detectFormat(filePath) {
3565
3793
  return "text";
3566
3794
  }
3567
3795
  // src/lib/export.ts
3568
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3569
- import { join as join7, resolve as resolve4 } from "path";
3796
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3797
+ import { join as join8, resolve as resolve4 } from "path";
3570
3798
  import { tmpdir } from "os";
3571
3799
  async function exportConfigs(outputPath, opts = {}) {
3572
3800
  const store = opts.store ?? resolveConfigStore();
3573
3801
  const configs = await store.listConfigs(opts.filter);
3574
3802
  const absOutput = resolve4(outputPath);
3575
- const tmpDir = join7(tmpdir(), `configs-export-${Date.now()}`);
3576
- const contentsDir = join7(tmpDir, "contents");
3803
+ const tmpDir = join8(tmpdir(), `configs-export-${Date.now()}`);
3804
+ const contentsDir = join8(tmpDir, "contents");
3577
3805
  try {
3578
3806
  mkdirSync4(contentsDir, { recursive: true });
3579
3807
  const manifest = {
@@ -3581,10 +3809,10 @@ async function exportConfigs(outputPath, opts = {}) {
3581
3809
  exported_at: new Date().toISOString(),
3582
3810
  configs: configs.map(({ content: _content, ...meta }) => meta)
3583
3811
  };
3584
- writeFileSync3(join7(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3812
+ writeFileSync3(join8(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3585
3813
  for (const config of configs) {
3586
3814
  const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
3587
- writeFileSync3(join7(contentsDir, fileName), config.content, "utf-8");
3815
+ writeFileSync3(join8(contentsDir, fileName), config.content, "utf-8");
3588
3816
  }
3589
3817
  const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
3590
3818
  stdout: "pipe",
@@ -3597,20 +3825,20 @@ async function exportConfigs(outputPath, opts = {}) {
3597
3825
  }
3598
3826
  return { path: absOutput, count: configs.length };
3599
3827
  } finally {
3600
- if (existsSync9(tmpDir)) {
3828
+ if (existsSync10(tmpDir)) {
3601
3829
  rmSync3(tmpDir, { recursive: true, force: true });
3602
3830
  }
3603
3831
  }
3604
3832
  }
3605
3833
  // src/lib/import.ts
3606
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
3607
- import { join as join8, resolve as resolve5 } from "path";
3834
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
3835
+ import { join as join9, resolve as resolve5 } from "path";
3608
3836
  import { tmpdir as tmpdir2 } from "os";
3609
3837
  async function importConfigs(bundlePath, opts = {}) {
3610
3838
  const store = opts.store ?? resolveConfigStore();
3611
3839
  const conflict = opts.conflict ?? "skip";
3612
3840
  const absPath = resolve5(bundlePath);
3613
- const tmpDir = join8(tmpdir2(), `configs-import-${Date.now()}`);
3841
+ const tmpDir = join9(tmpdir2(), `configs-import-${Date.now()}`);
3614
3842
  const result = { created: 0, updated: 0, skipped: 0, errors: [] };
3615
3843
  try {
3616
3844
  mkdirSync5(tmpDir, { recursive: true });
@@ -3623,15 +3851,15 @@ async function importConfigs(bundlePath, opts = {}) {
3623
3851
  const stderr = await new Response(proc.stderr).text();
3624
3852
  throw new Error(`tar extraction failed: ${stderr}`);
3625
3853
  }
3626
- const manifestPath = join8(tmpDir, "manifest.json");
3627
- if (!existsSync10(manifestPath))
3854
+ const manifestPath = join9(tmpDir, "manifest.json");
3855
+ if (!existsSync11(manifestPath))
3628
3856
  throw new Error("Invalid bundle: missing manifest.json");
3629
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
3857
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
3630
3858
  for (const meta of manifest.configs) {
3631
3859
  try {
3632
3860
  const ext = meta.format === "text" ? "txt" : meta.format;
3633
- const contentFile = join8(tmpDir, "contents", `${meta.slug}.${ext}`);
3634
- const content = existsSync10(contentFile) ? readFileSync7(contentFile, "utf-8") : "";
3861
+ const contentFile = join9(tmpDir, "contents", `${meta.slug}.${ext}`);
3862
+ const content = existsSync11(contentFile) ? readFileSync8(contentFile, "utf-8") : "";
3635
3863
  let existing = null;
3636
3864
  try {
3637
3865
  existing = await store.getConfig(meta.slug);
@@ -3665,16 +3893,16 @@ async function importConfigs(bundlePath, opts = {}) {
3665
3893
  }
3666
3894
  return result;
3667
3895
  } finally {
3668
- if (existsSync10(tmpDir)) {
3896
+ if (existsSync11(tmpDir)) {
3669
3897
  rmSync4(tmpDir, { recursive: true, force: true });
3670
3898
  }
3671
3899
  }
3672
3900
  }
3673
3901
  // src/lib/package-manager-guard.ts
3674
3902
  import { execFileSync } from "child_process";
3675
- import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
3903
+ import { existsSync as existsSync12, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
3676
3904
  import { homedir as homedir5 } from "os";
3677
- import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative as relative4, resolve as resolve6 } from "path";
3905
+ import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve6 } from "path";
3678
3906
  var SKIP_DIRS = new Set([
3679
3907
  ".git",
3680
3908
  "node_modules",
@@ -3716,7 +3944,7 @@ function scanPackageManagerSecrets(options = {}) {
3716
3944
  const findings = [];
3717
3945
  let scannedFiles = 0;
3718
3946
  for (const root of roots) {
3719
- if (!existsSync11(root))
3947
+ if (!existsSync12(root))
3720
3948
  continue;
3721
3949
  const stat = lstatSync2(root);
3722
3950
  if (stat.isFile()) {
@@ -3726,7 +3954,7 @@ function scanPackageManagerSecrets(options = {}) {
3726
3954
  if (text === null)
3727
3955
  continue;
3728
3956
  scannedFiles++;
3729
- findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
3957
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname5(root)));
3730
3958
  continue;
3731
3959
  }
3732
3960
  if (!stat.isDirectory())
@@ -3745,8 +3973,8 @@ function scanPackageManagerSecrets(options = {}) {
3745
3973
  if (options.includeHome) {
3746
3974
  const home = homedir5();
3747
3975
  for (const name of HOME_FILES) {
3748
- const file = join9(home, name);
3749
- if (!existsSync11(file))
3976
+ const file = join10(home, name);
3977
+ if (!existsSync12(file))
3750
3978
  continue;
3751
3979
  const text = readTextFile(file);
3752
3980
  if (text === null)
@@ -3770,12 +3998,12 @@ function collectRepoFiles(root) {
3770
3998
  if (entry.isDirectory()) {
3771
3999
  if (SKIP_DIRS.has(entry.name))
3772
4000
  continue;
3773
- visit(join9(dir, entry.name));
4001
+ visit(join10(dir, entry.name));
3774
4002
  continue;
3775
4003
  }
3776
4004
  if (!entry.isFile())
3777
4005
  continue;
3778
- const file = join9(dir, entry.name);
4006
+ const file = join10(dir, entry.name);
3779
4007
  if (shouldScanRepoFile(file))
3780
4008
  out.push(file);
3781
4009
  }
@@ -3813,7 +4041,7 @@ function readTextFile(file) {
3813
4041
  const stat = lstatSync2(file);
3814
4042
  if (!stat.isFile() || stat.size > 5000000)
3815
4043
  return null;
3816
- const buf = readFileSync8(file);
4044
+ const buf = readFileSync9(file);
3817
4045
  if (buf.includes(0))
3818
4046
  return null;
3819
4047
  return buf.toString("utf-8");
@@ -4013,7 +4241,7 @@ function trackedFiles(root) {
4013
4241
  }
4014
4242
  function isTrackedFile(file) {
4015
4243
  try {
4016
- const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
4244
+ const repoRoot = execFileSync("git", ["-C", dirname5(file), "rev-parse", "--show-toplevel"], {
4017
4245
  encoding: "utf-8",
4018
4246
  stdio: ["ignore", "pipe", "ignore"]
4019
4247
  }).trim();
@@ -4094,6 +4322,7 @@ export {
4094
4322
  expandPath,
4095
4323
  ensureProjectDashboardStandardConfig,
4096
4324
  ensurePlatformProfiles,
4325
+ ensureGlobalAgentRulesStandardConfig,
4097
4326
  diffConfig,
4098
4327
  detectMachineContext,
4099
4328
  detectFormat,
@@ -4117,6 +4346,8 @@ export {
4117
4346
  SESSION_RENDER_TOOLS,
4118
4347
  SESSION_RENDER_SCHEMA,
4119
4348
  SESSION_RENDER_MANAGED_MARKER,
4349
+ SESSION_LAYER_RANK,
4350
+ SESSION_INSTRUCTION_LAYERS,
4120
4351
  RAW_STORE_ROOT_ENV,
4121
4352
  ProfileNotFoundError,
4122
4353
  PROJECT_DASHBOARD_STANDARD_SLUG,
@@ -4127,6 +4358,8 @@ export {
4127
4358
  PG_MIGRATIONS,
4128
4359
  LocalConfigStore,
4129
4360
  KNOWN_CONFIGS,
4361
+ GLOBAL_AGENT_RULES_STANDARD_SLUG,
4362
+ GLOBAL_AGENT_RULES_STANDARD_CONTENT,
4130
4363
  ConfigNotFoundError,
4131
4364
  ConfigApplyError,
4132
4365
  CloudHttpError,