hatch3r 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -14,7 +14,7 @@ var HATCH3R_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/version.ts"() {
16
16
  "use strict";
17
- HATCH3R_VERSION = "1.7.0";
17
+ HATCH3R_VERSION = "1.7.1";
18
18
  }
19
19
  });
20
20
 
@@ -307,7 +307,8 @@ ${MANAGED_BLOCK_END}`;
307
307
  }
308
308
  const before = existingContent.substring(0, startIdx);
309
309
  const after = existingContent.substring(endIdx + MANAGED_BLOCK_END.length);
310
- return `${before}${block}${after}`;
310
+ const result = `${before}${block}${after}`;
311
+ return result.endsWith("\n") ? result : result + "\n";
311
312
  }
312
313
  function extractManagedBlock(content) {
313
314
  const startIdx = content.indexOf(MANAGED_BLOCK_START);
@@ -330,7 +331,8 @@ function extractCustomContent(content) {
330
331
  function wrapInManagedBlock(content) {
331
332
  return `${MANAGED_BLOCK_START}
332
333
  ${content.trim()}
333
- ${MANAGED_BLOCK_END}`;
334
+ ${MANAGED_BLOCK_END}
335
+ `;
334
336
  }
335
337
  function hasManagedBlock(content) {
336
338
  return content.includes(MANAGED_BLOCK_START) && content.includes(MANAGED_BLOCK_END);
@@ -1307,7 +1309,8 @@ async function safeWriteFile(filePath, content, options = {}) {
1307
1309
  if (options.managedContent) {
1308
1310
  if (!hasManagedBlock(existingContent)) {
1309
1311
  if (options.appendIfNoBlock) {
1310
- const prepended = [content.trim(), "", existingContent.trimStart()].join("\n");
1312
+ let prepended = [content.trim(), "", existingContent.trimStart()].join("\n");
1313
+ if (!prepended.endsWith("\n")) prepended += "\n";
1311
1314
  if (skipIfUnchanged && prepended === existingContent) {
1312
1315
  return { path: filePath, action: "unchanged" };
1313
1316
  }
@@ -2319,7 +2322,9 @@ var init_archive = __esm({
2319
2322
  var hatchJson_exports = {};
2320
2323
  __export(hatchJson_exports, {
2321
2324
  addManagedFile: () => addManagedFile,
2325
+ applyPreservedManifestFields: () => applyPreservedManifestFields,
2322
2326
  createManifest: () => createManifest,
2327
+ extractPreservedManifestFields: () => extractPreservedManifestFields,
2323
2328
  isValidGitBranchName: () => isValidGitBranchName,
2324
2329
  migrateManifest: () => migrateManifest,
2325
2330
  readManifest: () => readManifest,
@@ -2590,6 +2595,64 @@ function addManagedFile(manifest, filePath) {
2590
2595
  function removeManagedFile(manifest, filePath) {
2591
2596
  manifest.managedFiles = manifest.managedFiles.filter((f) => f !== filePath);
2592
2597
  }
2598
+ function extractPreservedManifestFields(manifest) {
2599
+ const out = {};
2600
+ if (manifest.board) out.board = manifest.board;
2601
+ if (manifest.costTracking) out.costTracking = manifest.costTracking;
2602
+ if (manifest.specs) out.specs = manifest.specs;
2603
+ if (manifest.userContent) out.userContent = manifest.userContent;
2604
+ if (manifest.hooks) out.hooks = manifest.hooks;
2605
+ if (manifest.models) out.models = manifest.models;
2606
+ if (manifest.claude) out.claude = manifest.claude;
2607
+ if (manifest.repos) out.repos = manifest.repos;
2608
+ if (manifest.packages) out.packages = manifest.packages;
2609
+ if (manifest.workspace) out.workspace = manifest.workspace;
2610
+ if (manifest.worktree?.extraPatterns !== void 0 || manifest.worktree?.nodeModules !== void 0) {
2611
+ out.worktreeExtras = {};
2612
+ if (manifest.worktree.extraPatterns !== void 0) {
2613
+ out.worktreeExtras.extraPatterns = manifest.worktree.extraPatterns;
2614
+ }
2615
+ if (manifest.worktree.nodeModules !== void 0) {
2616
+ out.worktreeExtras.nodeModules = manifest.worktree.nodeModules;
2617
+ }
2618
+ }
2619
+ return out;
2620
+ }
2621
+ function applyPreservedManifestFields(manifest, preserved) {
2622
+ if (preserved.board) {
2623
+ if (manifest.board) {
2624
+ manifest.board = {
2625
+ ...preserved.board,
2626
+ owner: manifest.board.owner,
2627
+ repo: manifest.board.repo,
2628
+ defaultBranch: manifest.board.defaultBranch ?? preserved.board.defaultBranch
2629
+ };
2630
+ } else {
2631
+ manifest.board = {
2632
+ ...preserved.board,
2633
+ owner: manifest.owner || preserved.board.owner,
2634
+ repo: manifest.repo || preserved.board.repo
2635
+ };
2636
+ }
2637
+ }
2638
+ if (preserved.costTracking) manifest.costTracking = preserved.costTracking;
2639
+ if (preserved.specs) manifest.specs = preserved.specs;
2640
+ if (preserved.userContent) manifest.userContent = preserved.userContent;
2641
+ if (preserved.hooks) manifest.hooks = preserved.hooks;
2642
+ if (preserved.models) manifest.models = preserved.models;
2643
+ if (preserved.claude) manifest.claude = preserved.claude;
2644
+ if (preserved.repos) manifest.repos = preserved.repos;
2645
+ if (preserved.packages) manifest.packages = preserved.packages;
2646
+ if (preserved.workspace) manifest.workspace = preserved.workspace;
2647
+ if (preserved.worktreeExtras && manifest.worktree?.enabled) {
2648
+ if (preserved.worktreeExtras.extraPatterns !== void 0) {
2649
+ manifest.worktree.extraPatterns = preserved.worktreeExtras.extraPatterns;
2650
+ }
2651
+ if (preserved.worktreeExtras.nodeModules !== void 0) {
2652
+ manifest.worktree.nodeModules = preserved.worktreeExtras.nodeModules;
2653
+ }
2654
+ }
2655
+ }
2593
2656
  var init_hatchJson = __esm({
2594
2657
  "src/manifest/hatchJson.ts"() {
2595
2658
  "use strict";
@@ -2954,205 +3017,498 @@ var init_canonical = __esm({
2954
3017
  }
2955
3018
  });
2956
3019
 
2957
- // src/content/tags.ts
2958
- function isLanguageTag(tag) {
2959
- return tag.startsWith("lang:");
2960
- }
2961
- function resolveLanguageTags(projectLanguages) {
2962
- const result = /* @__PURE__ */ new Set();
2963
- for (const lang of projectLanguages) {
2964
- const tag = LANGUAGE_TO_TAG[lang];
2965
- if (tag) result.add(tag);
2966
- }
2967
- return result;
2968
- }
2969
- function filterByLanguages(items, projectLanguages) {
2970
- if (projectLanguages.length === 0) return [...items];
2971
- const relevant = resolveLanguageTags(projectLanguages);
2972
- return items.filter((item) => {
2973
- if (item.protected) return true;
2974
- const itemLangTags = item.tags.filter(isLanguageTag);
2975
- if (itemLangTags.length === 0) return true;
2976
- return itemLangTags.some((t) => relevant.has(t));
2977
- });
2978
- }
2979
- var TAG_CORE, TAG_PLANNING, TAG_IMPLEMENTATION, TAG_REVIEW, TAG_DEVOPS, TAG_MAINTENANCE, TAG_BOARD, TAG_SECURITY, TAG_A11Y, TAG_PERFORMANCE, TAG_CUSTOMIZE, TAG_LANG_TYPESCRIPT, TAG_LANG_PYTHON, TAG_LANG_GO, TAG_LANG_RUST, TAG_LANG_JAVA, TAG_LANG_RUBY, WORKFLOW_TAGS, DOMAIN_TAGS, LANGUAGE_TO_TAG;
2980
- var init_tags = __esm({
2981
- "src/content/tags.ts"() {
2982
- "use strict";
2983
- TAG_CORE = "core";
2984
- TAG_PLANNING = "planning";
2985
- TAG_IMPLEMENTATION = "implementation";
2986
- TAG_REVIEW = "review";
2987
- TAG_DEVOPS = "devops";
2988
- TAG_MAINTENANCE = "maintenance";
2989
- TAG_BOARD = "board";
2990
- TAG_SECURITY = "security";
2991
- TAG_A11Y = "a11y";
2992
- TAG_PERFORMANCE = "performance";
2993
- TAG_CUSTOMIZE = "customize";
2994
- TAG_LANG_TYPESCRIPT = "lang:typescript";
2995
- TAG_LANG_PYTHON = "lang:python";
2996
- TAG_LANG_GO = "lang:go";
2997
- TAG_LANG_RUST = "lang:rust";
2998
- TAG_LANG_JAVA = "lang:java";
2999
- TAG_LANG_RUBY = "lang:ruby";
3000
- WORKFLOW_TAGS = [
3001
- TAG_CORE,
3002
- TAG_PLANNING,
3003
- TAG_IMPLEMENTATION,
3004
- TAG_REVIEW,
3005
- TAG_DEVOPS,
3006
- TAG_MAINTENANCE
3007
- ];
3008
- DOMAIN_TAGS = [
3009
- TAG_BOARD,
3010
- TAG_SECURITY,
3011
- TAG_A11Y,
3012
- TAG_PERFORMANCE,
3013
- TAG_CUSTOMIZE
3014
- ];
3015
- LANGUAGE_TO_TAG = {
3016
- typescript: TAG_LANG_TYPESCRIPT,
3017
- javascript: TAG_LANG_TYPESCRIPT,
3018
- // JS projects also benefit from TS rules
3019
- python: TAG_LANG_PYTHON,
3020
- go: TAG_LANG_GO,
3021
- rust: TAG_LANG_RUST,
3022
- java: TAG_LANG_JAVA,
3023
- kotlin: TAG_LANG_JAVA,
3024
- // Kotlin shares Java ecosystem
3025
- ruby: TAG_LANG_RUBY
3026
- };
3027
- }
3028
- });
3029
-
3030
- // src/content/index.ts
3031
- import { readFile as readFile9, readdir as readdir6, cp as cp2, mkdir as mkdir4, rm as rm2, stat as stat5 } from "fs/promises";
3032
- import { createHash as createHash3 } from "crypto";
3033
- import { join as join12, dirname as dirname6, normalize, isAbsolute, posix as posix2 } from "path";
3034
- function assertSafePath(relativePath, label2) {
3035
- const sanitized = relativePath.replace(/\0/g, "");
3036
- const normalized = normalize(sanitized);
3037
- if (normalized.startsWith("..") || isAbsolute(normalized)) {
3038
- throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3039
- }
3040
- if (sanitized !== relativePath) {
3041
- throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3042
- }
3020
+ // src/pipeline/agentToolAllowlist.ts
3021
+ function getAgentToolPolicy(agentId) {
3022
+ return policyMap.get(agentId);
3043
3023
  }
3044
- function extractContentReferences(content) {
3045
- const refs = /* @__PURE__ */ new Set();
3046
- const pattern = /`((?:cmd-)?hatch3r-[a-z0-9-]+)`/g;
3047
- let match;
3048
- while ((match = pattern.exec(content)) !== null) {
3049
- refs.add(match[1]);
3024
+ function levenshtein(a, b) {
3025
+ if (a === b) return 0;
3026
+ if (a.length === 0) return b.length;
3027
+ if (b.length === 0) return a.length;
3028
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3029
+ let curr = new Array(b.length + 1);
3030
+ for (let i = 1; i <= a.length; i++) {
3031
+ curr[0] = i;
3032
+ for (let j = 1; j <= b.length; j++) {
3033
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3034
+ curr[j] = Math.min(
3035
+ curr[j - 1] + 1,
3036
+ // insertion
3037
+ prev[j] + 1,
3038
+ // deletion
3039
+ prev[j - 1] + cost
3040
+ // substitution
3041
+ );
3042
+ }
3043
+ [prev, curr] = [curr, prev];
3050
3044
  }
3051
- return [...refs];
3045
+ return prev[b.length];
3052
3046
  }
3053
- async function validateCrossReferences(contentRoot, index) {
3054
- const warnings = [];
3055
- const allIds = new Set(index.items.map((item) => item.id));
3056
- for (const item of index.items) {
3057
- let content;
3058
- try {
3059
- const filePath = item.type === "skill" ? join12(contentRoot, item.relativePath, "SKILL.md") : join12(contentRoot, `${item.relativePath}`);
3060
- content = await readFile9(filePath, "utf-8");
3061
- } catch {
3062
- continue;
3063
- }
3064
- const refs = extractContentReferences(content);
3065
- for (const ref of refs) {
3066
- if (ref === item.id) continue;
3067
- if (!allIds.has(ref) && !allIds.has(`${COMMAND_ID_PREFIX}${ref}`)) {
3068
- warnings.push(
3069
- `${item.type} "${item.id}" references "${ref}" which does not exist in the content index`
3070
- );
3071
- }
3047
+ function suggestNearestCategory(tool) {
3048
+ let bestMatch;
3049
+ let bestDistance = Infinity;
3050
+ for (const known of ALL_TOOL_CATEGORIES) {
3051
+ const dist = levenshtein(tool, known);
3052
+ if (dist < bestDistance) {
3053
+ bestDistance = dist;
3054
+ bestMatch = known;
3072
3055
  }
3073
3056
  }
3074
- return { warnings };
3057
+ return bestDistance <= 2 ? bestMatch : void 0;
3075
3058
  }
3076
- function validateOrchestrationDependencies(selection) {
3059
+ function validateToolPolicies(policies = AGENT_TOOL_POLICIES) {
3077
3060
  const warnings = [];
3078
- const selectedAgents = new Set(selection.items.agents);
3079
- const hasOrchestration = selection.items.rules.includes("hatch3r-agent-orchestration");
3080
- if (!hasOrchestration) return warnings;
3081
- for (const agentId of ORCHESTRATION_REQUIRED_AGENTS) {
3082
- if (!selectedAgents.has(agentId)) {
3061
+ const knownCategories = new Set(ALL_TOOL_CATEGORIES);
3062
+ for (const policy of policies) {
3063
+ if (policy.allowedTools.length === 0) {
3064
+ warnings.push(`Agent "${policy.agentId}" has an empty tool allowlist \u2014 it cannot invoke any tools.`);
3065
+ }
3066
+ const hasAll = ALL_TOOL_CATEGORIES.every(
3067
+ (cat) => policy.allowedTools.includes(cat)
3068
+ );
3069
+ if (hasAll) {
3083
3070
  warnings.push(
3084
- `Orchestration pipeline requires agent "${agentId}" but it is not in the content selection. The 4-phase pipeline (Research \u2192 Implement \u2192 Review \u2192 Quality) will be incomplete.`
3071
+ `Agent "${policy.agentId}" has access to all tool categories \u2014 consider restricting to least privilege.`
3085
3072
  );
3086
3073
  }
3074
+ for (const tool of policy.allowedTools) {
3075
+ if (!knownCategories.has(tool)) {
3076
+ const suggestion = suggestNearestCategory(tool);
3077
+ const didYouMean = suggestion ? ` Did you mean "${suggestion}"?` : "";
3078
+ throw new HatchError(
3079
+ `Invalid tool policy for agent "${policy.agentId}": unknown tool category "${tool}".${didYouMean} Valid categories: ${ALL_TOOL_CATEGORIES.join(", ")}.`,
3080
+ 1,
3081
+ "VALIDATION_ERROR"
3082
+ );
3083
+ }
3084
+ }
3087
3085
  }
3088
3086
  return warnings;
3089
3087
  }
3090
- function typeIdKey(type, id) {
3091
- return `${type}:${id}`;
3092
- }
3093
- function getAllItemsById(index, id) {
3094
- return index.items.filter((item) => item.id === id);
3095
- }
3096
- function applyCommandPrefix(id, type) {
3097
- return type === "command" ? `${COMMAND_ID_PREFIX}${id}` : id;
3098
- }
3099
- async function scanContentRoot(rootPath, source, items) {
3100
- for (const config of CONTENT_TYPE_CONFIGS) {
3101
- if (source === "user" && config.type !== "agent" && config.type !== "skill" && config.type !== "rule" && config.type !== "command" && config.type !== "hook") {
3102
- continue;
3103
- }
3104
- const dirPath = join12(rootPath, config.dir);
3105
- if (config.strategy === "subdirectory") {
3106
- let dirents;
3107
- try {
3108
- dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3109
- } catch (err) {
3110
- if (err.code === "ENOENT") continue;
3111
- throw err;
3112
- }
3113
- for (const dirent of dirents) {
3114
- if (!dirent.isDirectory()) continue;
3115
- const skillPath = join12(dirPath, dirent.name, "SKILL.md");
3116
- try {
3117
- const raw = await readFile9(skillPath, "utf-8");
3118
- const { metadata } = parseFrontmatter(raw);
3119
- const rawId = metadata.id || metadata.name || dirent.name;
3120
- const id = applyCommandPrefix(rawId, config.type);
3121
- const item = {
3122
- id,
3123
- type: config.type,
3124
- description: metadata.description ?? "",
3125
- tags: metadata.tags ?? [],
3126
- protected: metadata.protected,
3127
- relativePath: posix2.join(config.dir, dirent.name),
3128
- source
3129
- };
3130
- if (source === "user") {
3131
- const adapters = parseAdaptersFrontmatter(raw);
3132
- if (adapters) item.adapters = adapters;
3133
- }
3134
- items.push(item);
3135
- } catch (err) {
3136
- if (err.code !== "ENOENT") throw err;
3137
- }
3138
- }
3139
- } else {
3140
- let entries;
3141
- try {
3142
- const all = await readdir6(dirPath);
3143
- entries = all.filter((f) => f.endsWith(".md")).sort();
3144
- } catch (err) {
3145
- if (err.code === "ENOENT") continue;
3146
- throw err;
3147
- }
3148
- for (const file of entries) {
3149
- const filePath = join12(dirPath, file);
3150
- const raw = await readFile9(filePath, "utf-8");
3151
- const { metadata } = parseFrontmatter(raw);
3152
- const rawId = metadata.id || metadata.name || file.replace(/\.md$/, "");
3153
- const id = applyCommandPrefix(rawId, config.type);
3154
- const item = {
3155
- id,
3088
+ var AGENT_TOOL_POLICIES, policyMap, ALL_TOOL_CATEGORIES;
3089
+ var init_agentToolAllowlist = __esm({
3090
+ "src/pipeline/agentToolAllowlist.ts"() {
3091
+ "use strict";
3092
+ init_types();
3093
+ AGENT_TOOL_POLICIES = [
3094
+ {
3095
+ agentId: "hatch3r-researcher",
3096
+ allowedTools: ["read", "search", "web", "mcp"],
3097
+ description: "Read-only research: file reading, code search, web research, MCP queries. No write or execute."
3098
+ },
3099
+ {
3100
+ agentId: "hatch3r-implementer",
3101
+ allowedTools: ["read", "search", "write", "execute"],
3102
+ description: "Code implementation: file read/write, code search, command execution (tests, linters). No git, board, or web."
3103
+ },
3104
+ {
3105
+ agentId: "hatch3r-reviewer",
3106
+ allowedTools: ["read", "search"],
3107
+ description: "Code review: file reading and code search only. No write, execute, git, or board."
3108
+ },
3109
+ {
3110
+ agentId: "hatch3r-fixer",
3111
+ allowedTools: ["read", "search", "write", "execute"],
3112
+ description: "Fix application: file read/write, code search, command execution. No git, board, or web."
3113
+ },
3114
+ {
3115
+ agentId: "hatch3r-test-writer",
3116
+ allowedTools: ["read", "search", "write", "execute"],
3117
+ description: "Test writing: file read/write, code search, test execution. No git, board, or web."
3118
+ },
3119
+ {
3120
+ agentId: "hatch3r-security-auditor",
3121
+ allowedTools: ["read", "search", "execute"],
3122
+ description: "Security audit: file reading, code search, security tool execution. No write, git, board, or web."
3123
+ },
3124
+ {
3125
+ agentId: "hatch3r-docs-writer",
3126
+ allowedTools: ["read", "search", "write"],
3127
+ description: "Documentation: file read/write, code search. No execute, git, board, or web."
3128
+ },
3129
+ {
3130
+ agentId: "hatch3r-lint-fixer",
3131
+ allowedTools: ["read", "search", "write", "execute"],
3132
+ description: "Lint fixing: file read/write, code search, linter execution. No git, board, or web."
3133
+ },
3134
+ {
3135
+ agentId: "hatch3r-a11y-auditor",
3136
+ allowedTools: ["read", "search", "execute"],
3137
+ description: "Accessibility audit: file reading, code search, a11y tool execution. No write, git, board, or web."
3138
+ },
3139
+ {
3140
+ agentId: "hatch3r-perf-profiler",
3141
+ allowedTools: ["read", "search", "execute"],
3142
+ description: "Performance profiling: file reading, code search, profiler execution. No write, git, board, or web."
3143
+ },
3144
+ {
3145
+ agentId: "hatch3r-dependency-auditor",
3146
+ allowedTools: ["read", "search", "execute"],
3147
+ description: "Dependency audit: file reading, code search, audit tool execution. No write, git, board, or web."
3148
+ },
3149
+ {
3150
+ agentId: "hatch3r-architect",
3151
+ allowedTools: ["read", "search", "write"],
3152
+ description: "Architecture: file read/write (docs/ADRs), code search. No execute, git, board, or web."
3153
+ },
3154
+ {
3155
+ agentId: "hatch3r-devops",
3156
+ allowedTools: ["read", "search", "write", "execute"],
3157
+ description: "DevOps: file read/write, code search, CI/CD command execution. No git, board, or web."
3158
+ },
3159
+ {
3160
+ agentId: "hatch3r-ci-watcher",
3161
+ allowedTools: ["read", "search"],
3162
+ description: "CI monitoring: file reading, code search. No write, execute, git, board, or web."
3163
+ },
3164
+ {
3165
+ agentId: "hatch3r-context-rules",
3166
+ allowedTools: ["read", "search"],
3167
+ description: "Context loading: file reading and code search only. No write, execute, git, board, or web."
3168
+ },
3169
+ {
3170
+ agentId: "hatch3r-learnings-loader",
3171
+ allowedTools: ["read", "search"],
3172
+ description: "Learnings loading: file reading and code search only. No write, execute, git, board, or web."
3173
+ }
3174
+ ];
3175
+ policyMap = new Map(
3176
+ AGENT_TOOL_POLICIES.map((p) => [p.agentId, p])
3177
+ );
3178
+ ALL_TOOL_CATEGORIES = [
3179
+ "read",
3180
+ "search",
3181
+ "write",
3182
+ "execute",
3183
+ "web",
3184
+ "mcp",
3185
+ "git",
3186
+ "board"
3187
+ ];
3188
+ }
3189
+ });
3190
+
3191
+ // src/pipeline/adapterToolTranslator.ts
3192
+ function toClaudeToolsFrontmatter(agentId) {
3193
+ const policy = getAgentToolPolicy(agentId);
3194
+ if (!policy) return null;
3195
+ const tools = resolveNativeTools(policy.allowedTools, CLAUDE_CATEGORY_MAP);
3196
+ if (tools.length === 0) return null;
3197
+ return tools.join(", ");
3198
+ }
3199
+ function toCopilotToolsFrontmatter(agentId) {
3200
+ const policy = getAgentToolPolicy(agentId);
3201
+ if (!policy) return null;
3202
+ const tools = resolveNativeTools(policy.allowedTools, COPILOT_CATEGORY_MAP);
3203
+ return tools.length === 0 ? null : tools;
3204
+ }
3205
+ function toWindsurfToolsFrontmatter(agentId) {
3206
+ const policy = getAgentToolPolicy(agentId);
3207
+ if (!policy) return null;
3208
+ const tools = resolveNativeTools(policy.allowedTools, WINDSURF_CATEGORY_MAP);
3209
+ if (tools.length === 0) return null;
3210
+ return tools.join(", ");
3211
+ }
3212
+ function toCursorReadonlyFrontmatter(agentId) {
3213
+ const policy = getAgentToolPolicy(agentId);
3214
+ if (!policy) return null;
3215
+ const hasWrite = policy.allowedTools.includes("write");
3216
+ const hasExecute = policy.allowedTools.includes("execute");
3217
+ return !hasWrite && !hasExecute;
3218
+ }
3219
+ function getAskUserToolEntry(adapter) {
3220
+ return ASK_USER_TOOLS[adapter] ?? null;
3221
+ }
3222
+ function toAskUserPlatformNote(adapter) {
3223
+ const entry = getAskUserToolEntry(adapter);
3224
+ if (entry === null) {
3225
+ return [
3226
+ `**Platform:** No documented native question tool for \`${adapter}\`.`,
3227
+ "Use the Plain-Text Fallback Template below for every ASK checkpoint."
3228
+ ].join(" ");
3229
+ }
3230
+ const hint = entry.invocationHint ? ` ${entry.invocationHint}` : "";
3231
+ return [
3232
+ `**Platform:** Invoke the \`${entry.name}\` tool for every ASK checkpoint on \`${adapter}\`.${hint}`,
3233
+ "Use the Plain-Text Fallback Template only when the tool cannot represent the question (e.g., long free-text answers)."
3234
+ ].join(" ");
3235
+ }
3236
+ function buildAskUserPlatformTable() {
3237
+ const rows = Object.entries(ASK_USER_TOOLS).map(([adapter, entry]) => {
3238
+ if (entry === null) {
3239
+ return `| \`${adapter}\` | _No documented native tool \u2014 use the Plain-Text Fallback Template below._ |`;
3240
+ }
3241
+ return `| \`${adapter}\` | Invoke the \`${entry.name}\` tool for every ASK checkpoint. |`;
3242
+ });
3243
+ return [
3244
+ "| Adapter | Platform-Native Question Tool |",
3245
+ "|---------|-------------------------------|",
3246
+ ...rows
3247
+ ].join("\n");
3248
+ }
3249
+ function substituteCanonicalPlatformMarker(content) {
3250
+ if (!content.includes(PLATFORM_TOOL_MARKER)) return content;
3251
+ return content.split(PLATFORM_TOOL_MARKER).join(buildAskUserPlatformTable());
3252
+ }
3253
+ function resolveNativeTools(categories, map) {
3254
+ const out = /* @__PURE__ */ new Set();
3255
+ for (const cat of categories) {
3256
+ const native = map[cat];
3257
+ if (!native) continue;
3258
+ for (const t of native) out.add(t);
3259
+ }
3260
+ return [...out];
3261
+ }
3262
+ var CLAUDE_CATEGORY_MAP, COPILOT_CATEGORY_MAP, WINDSURF_CATEGORY_MAP, ASK_USER_TOOLS, PLATFORM_TOOL_MARKER;
3263
+ var init_adapterToolTranslator = __esm({
3264
+ "src/pipeline/adapterToolTranslator.ts"() {
3265
+ "use strict";
3266
+ init_agentToolAllowlist();
3267
+ CLAUDE_CATEGORY_MAP = {
3268
+ read: ["Read", "NotebookRead"],
3269
+ search: ["Grep", "Glob"],
3270
+ write: ["Edit", "MultiEdit", "Write", "NotebookEdit"],
3271
+ execute: ["Bash"],
3272
+ web: ["WebSearch", "WebFetch"],
3273
+ mcp: [],
3274
+ // MCP tools are scoped via the `mcpServers` frontmatter field, not `tools`.
3275
+ git: ["Bash"],
3276
+ // Git is driven via Bash; callers that grant git retain execute semantics.
3277
+ board: []
3278
+ // Project-board tooling is MCP-driven; see mcp mapping.
3279
+ };
3280
+ COPILOT_CATEGORY_MAP = {
3281
+ read: ["read"],
3282
+ search: ["search"],
3283
+ write: ["edit"],
3284
+ execute: ["execute"],
3285
+ web: ["web"],
3286
+ mcp: [],
3287
+ // MCP exposure is controlled via `mcp-servers`, not `tools`.
3288
+ git: ["execute"],
3289
+ board: []
3290
+ };
3291
+ WINDSURF_CATEGORY_MAP = CLAUDE_CATEGORY_MAP;
3292
+ ASK_USER_TOOLS = {
3293
+ claude: { name: "AskUserQuestion" },
3294
+ cursor: null,
3295
+ copilot: null,
3296
+ windsurf: null,
3297
+ codex: null,
3298
+ cline: null,
3299
+ opencode: null,
3300
+ amp: null,
3301
+ aider: null,
3302
+ kiro: null,
3303
+ goose: null,
3304
+ zed: null,
3305
+ "amazon-q": null,
3306
+ gemini: null,
3307
+ antigravity: null
3308
+ };
3309
+ PLATFORM_TOOL_MARKER = "<!-- HATCH3R:PLATFORM-TOOL -->";
3310
+ }
3311
+ });
3312
+
3313
+ // src/content/tags.ts
3314
+ function isLanguageTag(tag) {
3315
+ return tag.startsWith("lang:");
3316
+ }
3317
+ function resolveLanguageTags(projectLanguages) {
3318
+ const result = /* @__PURE__ */ new Set();
3319
+ for (const lang of projectLanguages) {
3320
+ const tag = LANGUAGE_TO_TAG[lang];
3321
+ if (tag) result.add(tag);
3322
+ }
3323
+ return result;
3324
+ }
3325
+ function filterByLanguages(items, projectLanguages) {
3326
+ if (projectLanguages.length === 0) return [...items];
3327
+ const relevant = resolveLanguageTags(projectLanguages);
3328
+ return items.filter((item) => {
3329
+ if (item.protected) return true;
3330
+ const itemLangTags = item.tags.filter(isLanguageTag);
3331
+ if (itemLangTags.length === 0) return true;
3332
+ return itemLangTags.some((t) => relevant.has(t));
3333
+ });
3334
+ }
3335
+ var TAG_CORE, TAG_PLANNING, TAG_IMPLEMENTATION, TAG_REVIEW, TAG_DEVOPS, TAG_MAINTENANCE, TAG_BOARD, TAG_SECURITY, TAG_A11Y, TAG_PERFORMANCE, TAG_CUSTOMIZE, TAG_LANG_TYPESCRIPT, TAG_LANG_PYTHON, TAG_LANG_GO, TAG_LANG_RUST, TAG_LANG_JAVA, TAG_LANG_RUBY, WORKFLOW_TAGS, DOMAIN_TAGS, LANGUAGE_TO_TAG;
3336
+ var init_tags = __esm({
3337
+ "src/content/tags.ts"() {
3338
+ "use strict";
3339
+ TAG_CORE = "core";
3340
+ TAG_PLANNING = "planning";
3341
+ TAG_IMPLEMENTATION = "implementation";
3342
+ TAG_REVIEW = "review";
3343
+ TAG_DEVOPS = "devops";
3344
+ TAG_MAINTENANCE = "maintenance";
3345
+ TAG_BOARD = "board";
3346
+ TAG_SECURITY = "security";
3347
+ TAG_A11Y = "a11y";
3348
+ TAG_PERFORMANCE = "performance";
3349
+ TAG_CUSTOMIZE = "customize";
3350
+ TAG_LANG_TYPESCRIPT = "lang:typescript";
3351
+ TAG_LANG_PYTHON = "lang:python";
3352
+ TAG_LANG_GO = "lang:go";
3353
+ TAG_LANG_RUST = "lang:rust";
3354
+ TAG_LANG_JAVA = "lang:java";
3355
+ TAG_LANG_RUBY = "lang:ruby";
3356
+ WORKFLOW_TAGS = [
3357
+ TAG_CORE,
3358
+ TAG_PLANNING,
3359
+ TAG_IMPLEMENTATION,
3360
+ TAG_REVIEW,
3361
+ TAG_DEVOPS,
3362
+ TAG_MAINTENANCE
3363
+ ];
3364
+ DOMAIN_TAGS = [
3365
+ TAG_BOARD,
3366
+ TAG_SECURITY,
3367
+ TAG_A11Y,
3368
+ TAG_PERFORMANCE,
3369
+ TAG_CUSTOMIZE
3370
+ ];
3371
+ LANGUAGE_TO_TAG = {
3372
+ typescript: TAG_LANG_TYPESCRIPT,
3373
+ javascript: TAG_LANG_TYPESCRIPT,
3374
+ // JS projects also benefit from TS rules
3375
+ python: TAG_LANG_PYTHON,
3376
+ go: TAG_LANG_GO,
3377
+ rust: TAG_LANG_RUST,
3378
+ java: TAG_LANG_JAVA,
3379
+ kotlin: TAG_LANG_JAVA,
3380
+ // Kotlin shares Java ecosystem
3381
+ ruby: TAG_LANG_RUBY
3382
+ };
3383
+ }
3384
+ });
3385
+
3386
+ // src/content/index.ts
3387
+ import { readFile as readFile9, readdir as readdir6, cp as cp2, mkdir as mkdir4, rm as rm2, stat as stat5 } from "fs/promises";
3388
+ import { createHash as createHash3 } from "crypto";
3389
+ import { join as join12, dirname as dirname6, normalize, isAbsolute, posix as posix2 } from "path";
3390
+ function assertSafePath(relativePath, label2) {
3391
+ const sanitized = relativePath.replace(/\0/g, "");
3392
+ const normalized = normalize(sanitized);
3393
+ if (normalized.startsWith("..") || isAbsolute(normalized)) {
3394
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3395
+ }
3396
+ if (sanitized !== relativePath) {
3397
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3398
+ }
3399
+ }
3400
+ function extractContentReferences(content) {
3401
+ const refs = /* @__PURE__ */ new Set();
3402
+ const pattern = /`((?:cmd-)?hatch3r-[a-z0-9-]+)`/g;
3403
+ let match;
3404
+ while ((match = pattern.exec(content)) !== null) {
3405
+ refs.add(match[1]);
3406
+ }
3407
+ return [...refs];
3408
+ }
3409
+ async function validateCrossReferences(contentRoot, index) {
3410
+ const warnings = [];
3411
+ const allIds = new Set(index.items.map((item) => item.id));
3412
+ for (const item of index.items) {
3413
+ let content;
3414
+ try {
3415
+ const filePath = item.type === "skill" ? join12(contentRoot, item.relativePath, "SKILL.md") : join12(contentRoot, `${item.relativePath}`);
3416
+ content = await readFile9(filePath, "utf-8");
3417
+ } catch {
3418
+ continue;
3419
+ }
3420
+ const refs = extractContentReferences(content);
3421
+ for (const ref of refs) {
3422
+ if (ref === item.id) continue;
3423
+ if (!allIds.has(ref) && !allIds.has(`${COMMAND_ID_PREFIX}${ref}`)) {
3424
+ warnings.push(
3425
+ `${item.type} "${item.id}" references "${ref}" which does not exist in the content index`
3426
+ );
3427
+ }
3428
+ }
3429
+ }
3430
+ return { warnings };
3431
+ }
3432
+ function validateOrchestrationDependencies(selection) {
3433
+ const warnings = [];
3434
+ const selectedAgents = new Set(selection.items.agents);
3435
+ const hasOrchestration = selection.items.rules.includes("hatch3r-agent-orchestration");
3436
+ if (!hasOrchestration) return warnings;
3437
+ for (const agentId of ORCHESTRATION_REQUIRED_AGENTS) {
3438
+ if (!selectedAgents.has(agentId)) {
3439
+ warnings.push(
3440
+ `Orchestration pipeline requires agent "${agentId}" but it is not in the content selection. The 4-phase pipeline (Research \u2192 Implement \u2192 Review \u2192 Quality) will be incomplete.`
3441
+ );
3442
+ }
3443
+ }
3444
+ return warnings;
3445
+ }
3446
+ function typeIdKey(type, id) {
3447
+ return `${type}:${id}`;
3448
+ }
3449
+ function getAllItemsById(index, id) {
3450
+ return index.items.filter((item) => item.id === id);
3451
+ }
3452
+ function applyCommandPrefix(id, type) {
3453
+ return type === "command" ? `${COMMAND_ID_PREFIX}${id}` : id;
3454
+ }
3455
+ async function scanContentRoot(rootPath, source, items) {
3456
+ for (const config of CONTENT_TYPE_CONFIGS) {
3457
+ if (source === "user" && config.type !== "agent" && config.type !== "skill" && config.type !== "rule" && config.type !== "command" && config.type !== "hook") {
3458
+ continue;
3459
+ }
3460
+ const dirPath = join12(rootPath, config.dir);
3461
+ if (config.strategy === "subdirectory") {
3462
+ let dirents;
3463
+ try {
3464
+ dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3465
+ } catch (err) {
3466
+ if (err.code === "ENOENT") continue;
3467
+ throw err;
3468
+ }
3469
+ for (const dirent of dirents) {
3470
+ if (!dirent.isDirectory()) continue;
3471
+ const skillPath = join12(dirPath, dirent.name, "SKILL.md");
3472
+ try {
3473
+ const raw = await readFile9(skillPath, "utf-8");
3474
+ const { metadata } = parseFrontmatter(raw);
3475
+ const rawId = metadata.id || metadata.name || dirent.name;
3476
+ const id = applyCommandPrefix(rawId, config.type);
3477
+ const item = {
3478
+ id,
3479
+ type: config.type,
3480
+ description: metadata.description ?? "",
3481
+ tags: metadata.tags ?? [],
3482
+ protected: metadata.protected,
3483
+ relativePath: posix2.join(config.dir, dirent.name),
3484
+ source
3485
+ };
3486
+ if (source === "user") {
3487
+ const adapters = parseAdaptersFrontmatter(raw);
3488
+ if (adapters) item.adapters = adapters;
3489
+ }
3490
+ items.push(item);
3491
+ } catch (err) {
3492
+ if (err.code !== "ENOENT") throw err;
3493
+ }
3494
+ }
3495
+ } else {
3496
+ let entries;
3497
+ try {
3498
+ const all = await readdir6(dirPath);
3499
+ entries = all.filter((f) => f.endsWith(".md")).sort();
3500
+ } catch (err) {
3501
+ if (err.code === "ENOENT") continue;
3502
+ throw err;
3503
+ }
3504
+ for (const file of entries) {
3505
+ const filePath = join12(dirPath, file);
3506
+ const raw = await readFile9(filePath, "utf-8");
3507
+ const { metadata } = parseFrontmatter(raw);
3508
+ const rawId = metadata.id || metadata.name || file.replace(/\.md$/, "");
3509
+ const id = applyCommandPrefix(rawId, config.type);
3510
+ const item = {
3511
+ id,
3156
3512
  type: config.type,
3157
3513
  description: metadata.description ?? "",
3158
3514
  tags: metadata.tags ?? [],
@@ -3470,8 +3826,26 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index, opt
3470
3826
  } catch (err) {
3471
3827
  if (err.code !== "ENOENT") throw err;
3472
3828
  }
3829
+ await substitutePlatformToolMarker(agentsDir);
3473
3830
  return copied;
3474
3831
  }
3832
+ async function substitutePlatformToolMarker(agentsDir) {
3833
+ const sharedDir = join12(agentsDir, "agents", "shared");
3834
+ let entries;
3835
+ try {
3836
+ entries = await readdir6(sharedDir, { withFileTypes: true });
3837
+ } catch (err) {
3838
+ if (err.code === "ENOENT") return;
3839
+ throw err;
3840
+ }
3841
+ for (const entry of entries) {
3842
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
3843
+ const filePath = join12(sharedDir, entry.name);
3844
+ const content = await readFile9(filePath, "utf-8");
3845
+ if (!content.includes(PLATFORM_TOOL_MARKER)) continue;
3846
+ await atomicWriteFile(filePath, substituteCanonicalPlatformMarker(content));
3847
+ }
3848
+ }
3475
3849
  async function buildSelectionsFromDisk(agentsDir) {
3476
3850
  const items = {
3477
3851
  agents: [],
@@ -3624,6 +3998,7 @@ var init_content = __esm({
3624
3998
  "use strict";
3625
3999
  init_canonical();
3626
4000
  init_safeWrite();
4001
+ init_adapterToolTranslator();
3627
4002
  init_types();
3628
4003
  init_tags();
3629
4004
  ORCHESTRATION_REQUIRED_AGENTS = [
@@ -3947,8 +4322,7 @@ async function generateRootAgentsMd(agentsDir) {
3947
4322
  return { full: AGENTS_MD_FULL, inner: AGENTS_MD_INNER };
3948
4323
  }
3949
4324
  const inner = sections.join("\n");
3950
- const full = `${wrapInManagedBlock(inner)}
3951
- `;
4325
+ const full = wrapInManagedBlock(inner);
3952
4326
  return { full, inner };
3953
4327
  }
3954
4328
  async function generateCanonicalAgentsMd(agentsDir) {
@@ -4172,8 +4546,7 @@ You are running the **minimal** content preset \u2014 only core agents and workf
4172
4546
  "- Skills: `/.agents/skills/`",
4173
4547
  "- Commands: `/.agents/commands/`"
4174
4548
  ].join("\n");
4175
- AGENTS_MD_FULL = `${wrapInManagedBlock(AGENTS_MD_INNER)}
4176
- `;
4549
+ AGENTS_MD_FULL = wrapInManagedBlock(AGENTS_MD_INNER);
4177
4550
  }
4178
4551
  });
4179
4552
 
@@ -4609,6 +4982,7 @@ var init_base = __esm({
4609
4982
  init_customization();
4610
4983
  init_mcp_utils();
4611
4984
  init_hooks();
4985
+ init_adapterToolTranslator();
4612
4986
  BaseAdapter = class {
4613
4987
  warnings = [];
4614
4988
  /**
@@ -4805,9 +5179,10 @@ var init_base = __esm({
4805
5179
  );
4806
5180
  const minimal = this.isMinimal(ctx);
4807
5181
  for (const rule of rules) {
4808
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
5182
+ const { content: raw, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
4809
5183
  this.warnings.push(...warnings);
4810
5184
  if (skip) continue;
5185
+ const content = this.substituteAskUserMarker(raw);
4811
5186
  const desc = overrides.description ?? rule.description;
4812
5187
  if (minimal) {
4813
5188
  lines.push(`## ${rule.id}`, "", this.stripMinimal(content), "");
@@ -4824,9 +5199,10 @@ var init_base = __esm({
4824
5199
  const agents = await this.readUserFacingCanonicalFiles(ctx.agentsDir, "agents");
4825
5200
  const minimal = this.isMinimal(ctx);
4826
5201
  for (const agent of agents) {
4827
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
5202
+ const { content: raw, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
4828
5203
  this.warnings.push(...warnings);
4829
5204
  if (skip) continue;
5205
+ const content = this.substituteAskUserMarker(raw);
4830
5206
  const model = resolveAgentModel(agent.id, agent, ctx.manifest, overrides);
4831
5207
  const desc = overrides.description ?? agent.description;
4832
5208
  const fmt = model ? (formatModel ?? defaultModelFormat)(model) : void 0;
@@ -4848,9 +5224,10 @@ var init_base = __esm({
4848
5224
  const results = [];
4849
5225
  const skills = await this.readTrackedCanonicalFiles(ctx.agentsDir, "skills");
4850
5226
  for (const skill of skills) {
4851
- const { content, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, skill);
5227
+ const { content: raw, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, skill);
4852
5228
  this.warnings.push(...warnings);
4853
5229
  if (skip) continue;
5230
+ const content = this.substituteAskUserMarker(raw);
4854
5231
  results.push(output(pathFn(skill.id), wrapInManagedBlock(content), content));
4855
5232
  }
4856
5233
  return results;
@@ -4861,9 +5238,10 @@ var init_base = __esm({
4861
5238
  const results = [];
4862
5239
  const skills = await this.readTrackedCanonicalFiles(ctx.agentsDir, "skills");
4863
5240
  for (const skill of skills) {
4864
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, skill);
5241
+ const { content: raw, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, skill);
4865
5242
  this.warnings.push(...warnings);
4866
5243
  if (skip) continue;
5244
+ const content = this.substituteAskUserMarker(raw);
4867
5245
  const desc = overrides.description ?? skill.description;
4868
5246
  const fm = `---
4869
5247
  name: ${skill.id}
@@ -4881,9 +5259,10 @@ ${wrapInManagedBlock(content)}`, content));
4881
5259
  const results = [];
4882
5260
  const commands = await this.readUserFacingCanonicalFiles(ctx.agentsDir, "commands");
4883
5261
  for (const cmd of commands) {
4884
- const { content, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, cmd);
5262
+ const { content: raw, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, cmd);
4885
5263
  this.warnings.push(...warnings);
4886
5264
  if (skip) continue;
5265
+ const content = this.substituteAskUserMarker(raw);
4887
5266
  results.push(output(pathFn(cmd.id), wrapInManagedBlock(content), content));
4888
5267
  }
4889
5268
  return results;
@@ -4937,469 +5316,240 @@ ${wrapInManagedBlock(content)}`, content));
4937
5316
  return ctx.generationMode === "minimal";
4938
5317
  }
4939
5318
  /**
4940
- * Strip verbose content for minimal generation mode.
4941
- * Removes markdown comments, collapses excessive blank lines,
4942
- * strips decorative formatting, and trims descriptions.
5319
+ * Replace the `<!-- HATCH3R:PLATFORM-TOOL -->` marker in canonical content
5320
+ * with the per-adapter platform-note paragraph. Idempotent and a no-op
5321
+ * when the marker is absent.
5322
+ *
5323
+ * See agents/shared/user-question-protocol.md and
5324
+ * src/pipeline/adapterToolTranslator.ts::toAskUserPlatformNote.
4943
5325
  */
4944
- stripMinimal(content) {
4945
- let result = content;
4946
- result = result.replace(/<!--[\s\S]*?-->/g, "");
4947
- result = result.replace(/^[-*_]{3,}\s*$/gm, "");
4948
- result = result.replace(/\n{3,}/g, "\n\n");
4949
- result = result.trim();
4950
- return result;
4951
- }
4952
- };
4953
- }
4954
- });
4955
-
4956
- // src/adapters/aider.ts
4957
- var AiderAdapter;
4958
- var init_aider = __esm({
4959
- "src/adapters/aider.ts"() {
4960
- "use strict";
4961
- init_types();
4962
- init_managedBlocks();
4963
- init_base();
4964
- AiderAdapter = class extends BaseAdapter {
4965
- name = "aider";
4966
- async doGenerate(ctx) {
4967
- const inner = [
4968
- ...await this.bridgeHeader(ctx),
4969
- ...await this.inlineRules(ctx),
4970
- ...await this.inlineAgents(ctx)
4971
- ].join("\n").trim();
4972
- const results = [
4973
- output("CONVENTIONS.md", wrapInManagedBlock(inner), inner)
4974
- ];
4975
- results.push(
4976
- ...await this.processSkillsRaw(ctx, (id) => `.aider/skills/${toPrefixedId(id)}/SKILL.md`)
4977
- );
4978
- results.push(output(".aider.conf.yml", [
4979
- "# Managed by hatch3r \u2014 do not edit manually",
4980
- "read:",
4981
- " - CONVENTIONS.md",
4982
- " - .agents/AGENTS.md",
4983
- "auto-lint: true",
4984
- ""
4985
- ].join("\n")));
4986
- return results;
4987
- }
4988
- };
4989
- }
4990
- });
4991
-
4992
- // src/adapters/amazonq.ts
4993
- function mapToAmazonQEvent(event) {
4994
- const mapping = {
4995
- "session-start": "agentSpawn",
4996
- "pre-commit": "preToolUse",
4997
- "file-save": "postToolUse",
4998
- "post-merge": "postToolUse",
4999
- "ci-failure": "stop"
5000
- };
5001
- return mapping[event] ?? null;
5002
- }
5003
- var AmazonQAdapter;
5004
- var init_amazonq = __esm({
5005
- "src/adapters/amazonq.ts"() {
5006
- "use strict";
5007
- init_types();
5008
- init_managedBlocks();
5009
- init_base();
5010
- init_customization();
5011
- AmazonQAdapter = class extends BaseAdapter {
5012
- name = "amazon-q";
5013
- async doGenerate(ctx) {
5014
- const results = [];
5015
- const inner = [
5016
- ...await this.bridgeHeader(ctx),
5017
- ...await this.inlineRules(ctx),
5018
- ...await this.inlineAgents(ctx)
5019
- ].join("\n").trim();
5020
- results.push(output(".amazonq/rules/hatch3r-agents.md", wrapInManagedBlock(inner), inner));
5021
- results.push(
5022
- ...await this.processSkillsRaw(ctx, (id) => `.amazonq/rules/hatch3r-skill-${id}.md`)
5023
- );
5024
- const mcp = await this.readFilteredMcp(ctx);
5025
- if (mcp && Object.keys(mcp).length > 0) {
5026
- const entries = this.buildStdMcpEntries(mcp, "shell");
5027
- if (Object.keys(entries).length > 0) {
5028
- results.push(output(".amazonq/mcp.json", JSON.stringify({ mcpServers: entries }, null, 2)));
5029
- }
5030
- }
5031
- const hooks = await this.readHooks(ctx);
5032
- const descriptorHooks = this.buildDescriptorHooks(hooks);
5033
- if (ctx.features.agents) {
5034
- const agents = await this.readUserFacingCanonicalFiles(ctx.agentsDir, "agents");
5035
- for (const agent of agents) {
5036
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
5037
- this.warnings.push(...warnings);
5038
- if (skip) continue;
5039
- const desc = overrides.description ?? agent.description;
5040
- const descriptor = {
5041
- name: toPrefixedId(agent.id),
5042
- description: desc,
5043
- instructions: content,
5044
- // C7.5-W2B2-H33: hatch3r writes .amazonq/mcp.json; setting this to
5045
- // true makes new agent descriptors inherit it, otherwise Q CLI
5046
- // ignores the legacy MCP file when a cli-agent is spawned.
5047
- useLegacyMcpJson: true
5048
- };
5049
- if (Object.keys(descriptorHooks).length > 0) {
5050
- descriptor.hooks = descriptorHooks;
5051
- }
5052
- results.push(output(
5053
- `.amazonq/cli-agents/${toPrefixedId(agent.id)}.json`,
5054
- JSON.stringify(descriptor, null, 2)
5055
- ));
5056
- }
5057
- }
5058
- if (hooks.length > 0) {
5059
- const hookLines = ["# Hatch3r Hooks", ""];
5060
- for (const hook of hooks) {
5061
- const amazonQEvent = mapToAmazonQEvent(hook.event);
5062
- if (!amazonQEvent) continue;
5063
- hookLines.push(`## ${hook.id}`, "");
5064
- hookLines.push(`**Event:** ${amazonQEvent} (${hook.event})`);
5065
- hookLines.push(`**Agent:** ${hook.agent}`);
5066
- hookLines.push(`**Description:** ${hook.description}`);
5067
- if (hook.condition?.globs) {
5068
- hookLines.push(`**Globs:** ${hook.condition.globs.join(", ")}`);
5069
- }
5070
- hookLines.push("");
5071
- hookLines.push(`HATCH3R_HOOK_ACTIVATED: When this hook's event (${hook.event}) is triggered, you MUST spawn the ${hook.agent} agent now. Read and follow the ${hook.agent} agent protocol in \`.agents/agents/${toPrefixedId(hook.agent)}.md\`.`);
5072
- hookLines.push("");
5073
- }
5074
- if (hookLines.length > 2) {
5075
- const hookContent = hookLines.join("\n");
5076
- results.push(output(".amazonq/rules/hatch3r-hooks.md", wrapInManagedBlock(hookContent), hookContent));
5077
- }
5078
- }
5079
- return results;
5326
+ substituteAskUserMarker(content) {
5327
+ if (!content.includes(PLATFORM_TOOL_MARKER)) return content;
5328
+ return content.split(PLATFORM_TOOL_MARKER).join(toAskUserPlatformNote(this.name));
5080
5329
  }
5081
- /**
5082
- * Build the `hooks` map for a cli-agent descriptor from hatch3r canonical
5083
- * hook definitions. Group multiple hatch3r hooks that map to the same AWS
5084
- * canonical event (e.g. `file-save` + `post-merge` both map to
5085
- * `postToolUse`) so each canonical event key holds an array of entries.
5086
- *
5087
- * Each entry emits an `echo` command that carries the HATCH3R_HOOK_ACTIVATED
5088
- * directive so the surrounding agent (which reads the rules-bridge file)
5089
- * knows which hatch3r hook fired and which agent to dispatch.
5330
+ /**
5331
+ * Strip verbose content for minimal generation mode.
5332
+ * Removes markdown comments, collapses excessive blank lines,
5333
+ * strips decorative formatting, and trims descriptions.
5090
5334
  */
5091
- buildDescriptorHooks(hooks) {
5092
- const grouped = {};
5093
- for (const hook of hooks) {
5094
- const amazonQEvent = mapToAmazonQEvent(hook.event);
5095
- if (!amazonQEvent) continue;
5096
- const marker = `HATCH3R_HOOK_ACTIVATED id=${hook.id} event=${hook.event} agent=${hook.agent}`;
5097
- const entry = {
5098
- command: `echo ${JSON.stringify(marker)}`
5099
- };
5100
- (grouped[amazonQEvent] ??= []).push(entry);
5101
- }
5102
- return grouped;
5335
+ stripMinimal(content) {
5336
+ let result = content;
5337
+ result = result.replace(/<!--[\s\S]*?-->/g, "");
5338
+ result = result.replace(/^[-*_]{3,}\s*$/gm, "");
5339
+ result = result.replace(/\n{3,}/g, "\n\n");
5340
+ result = result.trim();
5341
+ return result;
5103
5342
  }
5104
5343
  };
5105
5344
  }
5106
5345
  });
5107
5346
 
5108
- // src/adapters/amp.ts
5109
- var AmpAdapter;
5110
- var init_amp = __esm({
5111
- "src/adapters/amp.ts"() {
5347
+ // src/adapters/aider.ts
5348
+ var AiderAdapter;
5349
+ var init_aider = __esm({
5350
+ "src/adapters/aider.ts"() {
5112
5351
  "use strict";
5352
+ init_types();
5353
+ init_managedBlocks();
5113
5354
  init_base();
5114
- AmpAdapter = class extends BaseAdapter {
5115
- name = "amp";
5355
+ AiderAdapter = class extends BaseAdapter {
5356
+ name = "aider";
5116
5357
  async doGenerate(ctx) {
5117
- const results = [];
5118
- const mcp = await this.readFilteredMcp(ctx);
5119
- if (mcp && Object.keys(mcp).length > 0) {
5120
- const entries = this.buildStdMcpEntries(mcp, "shell");
5121
- if (Object.keys(entries).length > 0) {
5122
- results.push(output(".amp/settings.json", JSON.stringify({ "amp.mcpServers": entries }, null, 2)));
5123
- }
5124
- }
5358
+ const inner = [
5359
+ ...await this.bridgeHeader(ctx),
5360
+ ...await this.inlineRules(ctx),
5361
+ ...await this.inlineAgents(ctx)
5362
+ ].join("\n").trim();
5363
+ const results = [
5364
+ output("CONVENTIONS.md", wrapInManagedBlock(inner), inner)
5365
+ ];
5366
+ results.push(
5367
+ ...await this.processSkillsRaw(ctx, (id) => `.aider/skills/${toPrefixedId(id)}/SKILL.md`)
5368
+ );
5369
+ results.push(output(".aider.conf.yml", [
5370
+ "# Managed by hatch3r \u2014 do not edit manually",
5371
+ "read:",
5372
+ " - CONVENTIONS.md",
5373
+ " - .agents/AGENTS.md",
5374
+ "auto-lint: true",
5375
+ ""
5376
+ ].join("\n")));
5125
5377
  return results;
5126
5378
  }
5127
5379
  };
5128
5380
  }
5129
5381
  });
5130
5382
 
5131
- // src/adapters/antigravity.ts
5132
- var AntigravityAdapter;
5133
- var init_antigravity = __esm({
5134
- "src/adapters/antigravity.ts"() {
5383
+ // src/adapters/amazonq.ts
5384
+ function mapToAmazonQEvent(event) {
5385
+ const mapping = {
5386
+ "session-start": "agentSpawn",
5387
+ "pre-commit": "preToolUse",
5388
+ "file-save": "postToolUse",
5389
+ "post-merge": "postToolUse",
5390
+ "ci-failure": "stop"
5391
+ };
5392
+ return mapping[event] ?? null;
5393
+ }
5394
+ var AmazonQAdapter;
5395
+ var init_amazonq = __esm({
5396
+ "src/adapters/amazonq.ts"() {
5135
5397
  "use strict";
5136
5398
  init_types();
5137
5399
  init_managedBlocks();
5138
5400
  init_base();
5139
- AntigravityAdapter = class extends BaseAdapter {
5140
- name = "antigravity";
5401
+ init_customization();
5402
+ AmazonQAdapter = class extends BaseAdapter {
5403
+ name = "amazon-q";
5141
5404
  async doGenerate(ctx) {
5142
5405
  const results = [];
5143
5406
  const inner = [
5144
- ...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
5407
+ ...await this.bridgeHeader(ctx),
5145
5408
  ...await this.inlineRules(ctx),
5146
5409
  ...await this.inlineAgents(ctx)
5147
5410
  ].join("\n").trim();
5148
- results.push(output(".antigravity/rules.md", wrapInManagedBlock(inner), inner));
5411
+ results.push(output(".amazonq/rules/hatch3r-agents.md", wrapInManagedBlock(inner), inner));
5149
5412
  results.push(
5150
- ...await this.processSkillsRaw(ctx, (id) => `.agent/skills/${toPrefixedId(id)}/SKILL.md`)
5413
+ ...await this.processSkillsRaw(ctx, (id) => `.amazonq/rules/hatch3r-skill-${id}.md`)
5151
5414
  );
5152
5415
  const mcp = await this.readFilteredMcp(ctx);
5153
5416
  if (mcp && Object.keys(mcp).length > 0) {
5154
5417
  const entries = this.buildStdMcpEntries(mcp, "shell");
5155
- if (Object.keys(entries).length > 0) {
5156
- results.push(output(".antigravity/settings.json", JSON.stringify({ mcpServers: entries }, null, 2)));
5157
- }
5158
- }
5159
- return results;
5160
- }
5161
- };
5162
- }
5163
- });
5164
-
5165
- // src/pipeline/agentToolAllowlist.ts
5166
- function getAgentToolPolicy(agentId) {
5167
- return policyMap.get(agentId);
5168
- }
5169
- function levenshtein(a, b) {
5170
- if (a === b) return 0;
5171
- if (a.length === 0) return b.length;
5172
- if (b.length === 0) return a.length;
5173
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
5174
- let curr = new Array(b.length + 1);
5175
- for (let i = 1; i <= a.length; i++) {
5176
- curr[0] = i;
5177
- for (let j = 1; j <= b.length; j++) {
5178
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5179
- curr[j] = Math.min(
5180
- curr[j - 1] + 1,
5181
- // insertion
5182
- prev[j] + 1,
5183
- // deletion
5184
- prev[j - 1] + cost
5185
- // substitution
5186
- );
5187
- }
5188
- [prev, curr] = [curr, prev];
5189
- }
5190
- return prev[b.length];
5191
- }
5192
- function suggestNearestCategory(tool) {
5193
- let bestMatch;
5194
- let bestDistance = Infinity;
5195
- for (const known of ALL_TOOL_CATEGORIES) {
5196
- const dist = levenshtein(tool, known);
5197
- if (dist < bestDistance) {
5198
- bestDistance = dist;
5199
- bestMatch = known;
5200
- }
5201
- }
5202
- return bestDistance <= 2 ? bestMatch : void 0;
5203
- }
5204
- function validateToolPolicies(policies = AGENT_TOOL_POLICIES) {
5205
- const warnings = [];
5206
- const knownCategories = new Set(ALL_TOOL_CATEGORIES);
5207
- for (const policy of policies) {
5208
- if (policy.allowedTools.length === 0) {
5209
- warnings.push(`Agent "${policy.agentId}" has an empty tool allowlist \u2014 it cannot invoke any tools.`);
5210
- }
5211
- const hasAll = ALL_TOOL_CATEGORIES.every(
5212
- (cat) => policy.allowedTools.includes(cat)
5213
- );
5214
- if (hasAll) {
5215
- warnings.push(
5216
- `Agent "${policy.agentId}" has access to all tool categories \u2014 consider restricting to least privilege.`
5217
- );
5218
- }
5219
- for (const tool of policy.allowedTools) {
5220
- if (!knownCategories.has(tool)) {
5221
- const suggestion = suggestNearestCategory(tool);
5222
- const didYouMean = suggestion ? ` Did you mean "${suggestion}"?` : "";
5223
- throw new HatchError(
5224
- `Invalid tool policy for agent "${policy.agentId}": unknown tool category "${tool}".${didYouMean} Valid categories: ${ALL_TOOL_CATEGORIES.join(", ")}.`,
5225
- 1,
5226
- "VALIDATION_ERROR"
5227
- );
5228
- }
5229
- }
5230
- }
5231
- return warnings;
5232
- }
5233
- var AGENT_TOOL_POLICIES, policyMap, ALL_TOOL_CATEGORIES;
5234
- var init_agentToolAllowlist = __esm({
5235
- "src/pipeline/agentToolAllowlist.ts"() {
5236
- "use strict";
5237
- init_types();
5238
- AGENT_TOOL_POLICIES = [
5239
- {
5240
- agentId: "hatch3r-researcher",
5241
- allowedTools: ["read", "search", "web", "mcp"],
5242
- description: "Read-only research: file reading, code search, web research, MCP queries. No write or execute."
5243
- },
5244
- {
5245
- agentId: "hatch3r-implementer",
5246
- allowedTools: ["read", "search", "write", "execute"],
5247
- description: "Code implementation: file read/write, code search, command execution (tests, linters). No git, board, or web."
5248
- },
5249
- {
5250
- agentId: "hatch3r-reviewer",
5251
- allowedTools: ["read", "search"],
5252
- description: "Code review: file reading and code search only. No write, execute, git, or board."
5253
- },
5254
- {
5255
- agentId: "hatch3r-fixer",
5256
- allowedTools: ["read", "search", "write", "execute"],
5257
- description: "Fix application: file read/write, code search, command execution. No git, board, or web."
5258
- },
5259
- {
5260
- agentId: "hatch3r-test-writer",
5261
- allowedTools: ["read", "search", "write", "execute"],
5262
- description: "Test writing: file read/write, code search, test execution. No git, board, or web."
5263
- },
5264
- {
5265
- agentId: "hatch3r-security-auditor",
5266
- allowedTools: ["read", "search", "execute"],
5267
- description: "Security audit: file reading, code search, security tool execution. No write, git, board, or web."
5268
- },
5269
- {
5270
- agentId: "hatch3r-docs-writer",
5271
- allowedTools: ["read", "search", "write"],
5272
- description: "Documentation: file read/write, code search. No execute, git, board, or web."
5273
- },
5274
- {
5275
- agentId: "hatch3r-lint-fixer",
5276
- allowedTools: ["read", "search", "write", "execute"],
5277
- description: "Lint fixing: file read/write, code search, linter execution. No git, board, or web."
5278
- },
5279
- {
5280
- agentId: "hatch3r-a11y-auditor",
5281
- allowedTools: ["read", "search", "execute"],
5282
- description: "Accessibility audit: file reading, code search, a11y tool execution. No write, git, board, or web."
5283
- },
5284
- {
5285
- agentId: "hatch3r-perf-profiler",
5286
- allowedTools: ["read", "search", "execute"],
5287
- description: "Performance profiling: file reading, code search, profiler execution. No write, git, board, or web."
5288
- },
5289
- {
5290
- agentId: "hatch3r-dependency-auditor",
5291
- allowedTools: ["read", "search", "execute"],
5292
- description: "Dependency audit: file reading, code search, audit tool execution. No write, git, board, or web."
5293
- },
5294
- {
5295
- agentId: "hatch3r-architect",
5296
- allowedTools: ["read", "search", "write"],
5297
- description: "Architecture: file read/write (docs/ADRs), code search. No execute, git, board, or web."
5298
- },
5299
- {
5300
- agentId: "hatch3r-devops",
5301
- allowedTools: ["read", "search", "write", "execute"],
5302
- description: "DevOps: file read/write, code search, CI/CD command execution. No git, board, or web."
5303
- },
5304
- {
5305
- agentId: "hatch3r-ci-watcher",
5306
- allowedTools: ["read", "search"],
5307
- description: "CI monitoring: file reading, code search. No write, execute, git, board, or web."
5308
- },
5309
- {
5310
- agentId: "hatch3r-context-rules",
5311
- allowedTools: ["read", "search"],
5312
- description: "Context loading: file reading and code search only. No write, execute, git, board, or web."
5313
- },
5314
- {
5315
- agentId: "hatch3r-learnings-loader",
5316
- allowedTools: ["read", "search"],
5317
- description: "Learnings loading: file reading and code search only. No write, execute, git, board, or web."
5418
+ if (Object.keys(entries).length > 0) {
5419
+ results.push(output(".amazonq/mcp.json", JSON.stringify({ mcpServers: entries }, null, 2)));
5420
+ }
5421
+ }
5422
+ const hooks = await this.readHooks(ctx);
5423
+ const descriptorHooks = this.buildDescriptorHooks(hooks);
5424
+ if (ctx.features.agents) {
5425
+ const agents = await this.readUserFacingCanonicalFiles(ctx.agentsDir, "agents");
5426
+ for (const agent of agents) {
5427
+ const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
5428
+ this.warnings.push(...warnings);
5429
+ if (skip) continue;
5430
+ const desc = overrides.description ?? agent.description;
5431
+ const descriptor = {
5432
+ name: toPrefixedId(agent.id),
5433
+ description: desc,
5434
+ instructions: content,
5435
+ // C7.5-W2B2-H33: hatch3r writes .amazonq/mcp.json; setting this to
5436
+ // true makes new agent descriptors inherit it, otherwise Q CLI
5437
+ // ignores the legacy MCP file when a cli-agent is spawned.
5438
+ useLegacyMcpJson: true
5439
+ };
5440
+ if (Object.keys(descriptorHooks).length > 0) {
5441
+ descriptor.hooks = descriptorHooks;
5442
+ }
5443
+ results.push(output(
5444
+ `.amazonq/cli-agents/${toPrefixedId(agent.id)}.json`,
5445
+ JSON.stringify(descriptor, null, 2)
5446
+ ));
5447
+ }
5448
+ }
5449
+ if (hooks.length > 0) {
5450
+ const hookLines = ["# Hatch3r Hooks", ""];
5451
+ for (const hook of hooks) {
5452
+ const amazonQEvent = mapToAmazonQEvent(hook.event);
5453
+ if (!amazonQEvent) continue;
5454
+ hookLines.push(`## ${hook.id}`, "");
5455
+ hookLines.push(`**Event:** ${amazonQEvent} (${hook.event})`);
5456
+ hookLines.push(`**Agent:** ${hook.agent}`);
5457
+ hookLines.push(`**Description:** ${hook.description}`);
5458
+ if (hook.condition?.globs) {
5459
+ hookLines.push(`**Globs:** ${hook.condition.globs.join(", ")}`);
5460
+ }
5461
+ hookLines.push("");
5462
+ hookLines.push(`HATCH3R_HOOK_ACTIVATED: When this hook's event (${hook.event}) is triggered, you MUST spawn the ${hook.agent} agent now. Read and follow the ${hook.agent} agent protocol in \`.agents/agents/${toPrefixedId(hook.agent)}.md\`.`);
5463
+ hookLines.push("");
5464
+ }
5465
+ if (hookLines.length > 2) {
5466
+ const hookContent = hookLines.join("\n");
5467
+ results.push(output(".amazonq/rules/hatch3r-hooks.md", wrapInManagedBlock(hookContent), hookContent));
5468
+ }
5469
+ }
5470
+ return results;
5318
5471
  }
5319
- ];
5320
- policyMap = new Map(
5321
- AGENT_TOOL_POLICIES.map((p) => [p.agentId, p])
5322
- );
5323
- ALL_TOOL_CATEGORIES = [
5324
- "read",
5325
- "search",
5326
- "write",
5327
- "execute",
5328
- "web",
5329
- "mcp",
5330
- "git",
5331
- "board"
5332
- ];
5472
+ /**
5473
+ * Build the `hooks` map for a cli-agent descriptor from hatch3r canonical
5474
+ * hook definitions. Group multiple hatch3r hooks that map to the same AWS
5475
+ * canonical event (e.g. `file-save` + `post-merge` both map to
5476
+ * `postToolUse`) so each canonical event key holds an array of entries.
5477
+ *
5478
+ * Each entry emits an `echo` command that carries the HATCH3R_HOOK_ACTIVATED
5479
+ * directive so the surrounding agent (which reads the rules-bridge file)
5480
+ * knows which hatch3r hook fired and which agent to dispatch.
5481
+ */
5482
+ buildDescriptorHooks(hooks) {
5483
+ const grouped = {};
5484
+ for (const hook of hooks) {
5485
+ const amazonQEvent = mapToAmazonQEvent(hook.event);
5486
+ if (!amazonQEvent) continue;
5487
+ const marker = `HATCH3R_HOOK_ACTIVATED id=${hook.id} event=${hook.event} agent=${hook.agent}`;
5488
+ const entry = {
5489
+ command: `echo ${JSON.stringify(marker)}`
5490
+ };
5491
+ (grouped[amazonQEvent] ??= []).push(entry);
5492
+ }
5493
+ return grouped;
5494
+ }
5495
+ };
5333
5496
  }
5334
5497
  });
5335
5498
 
5336
- // src/pipeline/adapterToolTranslator.ts
5337
- function toClaudeToolsFrontmatter(agentId) {
5338
- const policy = getAgentToolPolicy(agentId);
5339
- if (!policy) return null;
5340
- const tools = resolveNativeTools(policy.allowedTools, CLAUDE_CATEGORY_MAP);
5341
- if (tools.length === 0) return null;
5342
- return tools.join(", ");
5343
- }
5344
- function toCopilotToolsFrontmatter(agentId) {
5345
- const policy = getAgentToolPolicy(agentId);
5346
- if (!policy) return null;
5347
- const tools = resolveNativeTools(policy.allowedTools, COPILOT_CATEGORY_MAP);
5348
- return tools.length === 0 ? null : tools;
5349
- }
5350
- function toWindsurfToolsFrontmatter(agentId) {
5351
- const policy = getAgentToolPolicy(agentId);
5352
- if (!policy) return null;
5353
- const tools = resolveNativeTools(policy.allowedTools, WINDSURF_CATEGORY_MAP);
5354
- if (tools.length === 0) return null;
5355
- return tools.join(", ");
5356
- }
5357
- function toCursorReadonlyFrontmatter(agentId) {
5358
- const policy = getAgentToolPolicy(agentId);
5359
- if (!policy) return null;
5360
- const hasWrite = policy.allowedTools.includes("write");
5361
- const hasExecute = policy.allowedTools.includes("execute");
5362
- return !hasWrite && !hasExecute;
5363
- }
5364
- function resolveNativeTools(categories, map) {
5365
- const out = /* @__PURE__ */ new Set();
5366
- for (const cat of categories) {
5367
- const native = map[cat];
5368
- if (!native) continue;
5369
- for (const t of native) out.add(t);
5370
- }
5371
- return [...out];
5372
- }
5373
- var CLAUDE_CATEGORY_MAP, COPILOT_CATEGORY_MAP, WINDSURF_CATEGORY_MAP;
5374
- var init_adapterToolTranslator = __esm({
5375
- "src/pipeline/adapterToolTranslator.ts"() {
5499
+ // src/adapters/amp.ts
5500
+ var AmpAdapter;
5501
+ var init_amp = __esm({
5502
+ "src/adapters/amp.ts"() {
5376
5503
  "use strict";
5377
- init_agentToolAllowlist();
5378
- CLAUDE_CATEGORY_MAP = {
5379
- read: ["Read", "NotebookRead"],
5380
- search: ["Grep", "Glob"],
5381
- write: ["Edit", "MultiEdit", "Write", "NotebookEdit"],
5382
- execute: ["Bash"],
5383
- web: ["WebSearch", "WebFetch"],
5384
- mcp: [],
5385
- // MCP tools are scoped via the `mcpServers` frontmatter field, not `tools`.
5386
- git: ["Bash"],
5387
- // Git is driven via Bash; callers that grant git retain execute semantics.
5388
- board: []
5389
- // Project-board tooling is MCP-driven; see mcp mapping.
5504
+ init_base();
5505
+ AmpAdapter = class extends BaseAdapter {
5506
+ name = "amp";
5507
+ async doGenerate(ctx) {
5508
+ const results = [];
5509
+ const mcp = await this.readFilteredMcp(ctx);
5510
+ if (mcp && Object.keys(mcp).length > 0) {
5511
+ const entries = this.buildStdMcpEntries(mcp, "shell");
5512
+ if (Object.keys(entries).length > 0) {
5513
+ results.push(output(".amp/settings.json", JSON.stringify({ "amp.mcpServers": entries }, null, 2)));
5514
+ }
5515
+ }
5516
+ return results;
5517
+ }
5390
5518
  };
5391
- COPILOT_CATEGORY_MAP = {
5392
- read: ["read"],
5393
- search: ["search"],
5394
- write: ["edit"],
5395
- execute: ["execute"],
5396
- web: ["web"],
5397
- mcp: [],
5398
- // MCP exposure is controlled via `mcp-servers`, not `tools`.
5399
- git: ["execute"],
5400
- board: []
5519
+ }
5520
+ });
5521
+
5522
+ // src/adapters/antigravity.ts
5523
+ var AntigravityAdapter;
5524
+ var init_antigravity = __esm({
5525
+ "src/adapters/antigravity.ts"() {
5526
+ "use strict";
5527
+ init_types();
5528
+ init_managedBlocks();
5529
+ init_base();
5530
+ AntigravityAdapter = class extends BaseAdapter {
5531
+ name = "antigravity";
5532
+ async doGenerate(ctx) {
5533
+ const results = [];
5534
+ const inner = [
5535
+ ...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
5536
+ ...await this.inlineRules(ctx),
5537
+ ...await this.inlineAgents(ctx)
5538
+ ].join("\n").trim();
5539
+ results.push(output(".antigravity/rules.md", wrapInManagedBlock(inner), inner));
5540
+ results.push(
5541
+ ...await this.processSkillsRaw(ctx, (id) => `.agent/skills/${toPrefixedId(id)}/SKILL.md`)
5542
+ );
5543
+ const mcp = await this.readFilteredMcp(ctx);
5544
+ if (mcp && Object.keys(mcp).length > 0) {
5545
+ const entries = this.buildStdMcpEntries(mcp, "shell");
5546
+ if (Object.keys(entries).length > 0) {
5547
+ results.push(output(".antigravity/settings.json", JSON.stringify({ mcpServers: entries }, null, 2)));
5548
+ }
5549
+ }
5550
+ return results;
5551
+ }
5401
5552
  };
5402
- WINDSURF_CATEGORY_MAP = CLAUDE_CATEGORY_MAP;
5403
5553
  }
5404
5554
  });
5405
5555
 
@@ -6120,7 +6270,7 @@ var init_packageManager = __esm({
6120
6270
  });
6121
6271
 
6122
6272
  // src/adapters/copilot.ts
6123
- var CopilotAdapter;
6273
+ var COPILOT_ENFORCEMENT_ADDENDUM, CopilotAdapter;
6124
6274
  var init_copilot = __esm({
6125
6275
  "src/adapters/copilot.ts"() {
6126
6276
  "use strict";
@@ -6132,6 +6282,30 @@ var init_copilot = __esm({
6132
6282
  init_customization();
6133
6283
  init_packageManager();
6134
6284
  init_adapterToolTranslator();
6285
+ COPILOT_ENFORCEMENT_ADDENDUM = `## Copilot Enforcement Model (no hook surface)
6286
+
6287
+ GitHub Copilot Chat does not expose a PreToolUse or pre-edit hook
6288
+ (see \`src/adapters/index.ts\` \u2014 \`copilot\` is the only adapter with
6289
+ \`hooks: false\` in \`ADAPTER_CAPABILITIES\`). Hatch3r cannot block
6290
+ code-writing tool calls server-side for Copilot. Enforcement is
6291
+ therefore trust-based \u2014 the directives in this file and in
6292
+ \`.github/instructions/\` are normative, not advisory.
6293
+
6294
+ Self-detectable drift indicators (halt the current turn if any appear):
6295
+
6296
+ - Missing pipeline-state header on a tracked Tier 2+ task (see
6297
+ \`hatch3r-agent-orchestration\` \u2192 Per-Turn Pipeline-State Header).
6298
+ - A call to \`replace_string_in_file\`, \`multi_replace_string_in_file\`,
6299
+ \`create_file\`, or any code-writing tool before the user has
6300
+ confirmed the Pre-Implementation Summary on a Tier 3 task (see
6301
+ \`hatch3r-deep-context\` \u2192 Tier 3 \u2014 Deep).
6302
+ - An \`Edit\` / \`Write\` invocation from the orchestrator turn that
6303
+ did not immediately follow a SUCCESS report from \`hatch3r-implementer\`
6304
+ via the \`Task\` tool.
6305
+
6306
+ On any drift, halt and re-delegate via \`hatch3r-implementer\` (Phase 2)
6307
+ or \`hatch3r-fixer\` (Phase 3). The only carve-out is \`hatch3r-quick-change\`
6308
+ Tier 1 trivial single-line edits per its declared scope.`;
6135
6309
  CopilotAdapter = class extends BaseAdapter {
6136
6310
  name = "copilot";
6137
6311
  async doGenerate(ctx) {
@@ -6142,9 +6316,10 @@ var init_copilot = __esm({
6142
6316
  const rules = await readCanonicalFiles(ctx.agentsDir, "rules", this.warnings);
6143
6317
  const sortedRules = sortByPrecedence(rules);
6144
6318
  for (const rule of sortedRules) {
6145
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
6319
+ const { content: rawContent, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
6146
6320
  this.warnings.push(...warnings);
6147
6321
  if (skip) continue;
6322
+ const content = this.substituteAskUserMarker(rawContent);
6148
6323
  const scope = overrides.scope ?? rule.scope;
6149
6324
  if (scope && scope !== "always") {
6150
6325
  scopedRules.push({ rule: { ...rule, description: overrides.description ?? rule.description }, content, scope });
@@ -6162,6 +6337,8 @@ var init_copilot = __esm({
6162
6337
  "",
6163
6338
  bridgeOrchestration,
6164
6339
  "",
6340
+ COPILOT_ENFORCEMENT_ADDENDUM,
6341
+ "",
6165
6342
  "## Hatch3r Rules",
6166
6343
  "",
6167
6344
  ...alwaysRules.map(
@@ -6199,7 +6376,7 @@ jobs:
6199
6376
  run: ${build}`;
6200
6377
  results.push(output(
6201
6378
  ".github/workflows/copilot-setup-steps.yml",
6202
- wrapInManagedBlock(copilotSetupStepsInner) + "\n",
6379
+ wrapInManagedBlock(copilotSetupStepsInner),
6203
6380
  copilotSetupStepsInner
6204
6381
  ));
6205
6382
  for (const { rule, content, scope } of scopedRules) {
@@ -6227,9 +6404,10 @@ ${wrapInManagedBlock(body)}`,
6227
6404
  if (ctx.features.agents) {
6228
6405
  const agents = await this.readUserFacingCanonicalFiles(ctx.agentsDir, "agents");
6229
6406
  for (const agent of agents) {
6230
- const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
6407
+ const { content: rawContent, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
6231
6408
  this.warnings.push(...warnings);
6232
6409
  if (skip) continue;
6410
+ const content = this.substituteAskUserMarker(rawContent);
6233
6411
  const model = resolveAgentModel(agent.id, agent, ctx.manifest, overrides);
6234
6412
  const desc = overrides.description ?? agent.description;
6235
6413
  const prefixedId = toPrefixedId(agent.id);
@@ -7166,28 +7344,28 @@ var init_adapters = __esm({
7166
7344
  };
7167
7345
  adapterCache = /* @__PURE__ */ new Map();
7168
7346
  ADAPTER_CAPABILITIES = {
7169
- cursor: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true },
7170
- claude: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true },
7171
- gemini: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true },
7172
- cline: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true },
7173
- codex: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7174
- "amazon-q": { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7175
- copilot: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: true, githubAgents: true, worktree: true, customization: true, modelOverride: true },
7176
- opencode: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7347
+ cursor: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7348
+ claude: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: true },
7349
+ gemini: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7350
+ cline: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7351
+ codex: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7352
+ "amazon-q": { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7353
+ copilot: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: true, githubAgents: true, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7354
+ opencode: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7177
7355
  // C7.5-W2B2-H31 (D9-SA9.7.1): Windsurf shipped Cascade Hooks in v1.13.12 (2026-01-25).
7178
7356
  // Hatch3r emits `.windsurf/hooks.json` per docs.windsurf.com/windsurf/cascade/hooks.md.
7179
- windsurf: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true },
7357
+ windsurf: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7180
7358
  // Amp reads AGENTS.md natively; the root file is written by generateRootAgentsMd()
7181
7359
  // in init/update, not by this adapter. Amp also reads skills natively from
7182
7360
  // `.agents/skills/` — populated by copyHatch3rFiles, not re-emitted by this
7183
7361
  // adapter (re-emission corrupts SKILL.md frontmatter via managed-block wrap).
7184
7362
  // doGenerate() emits MCP settings only.
7185
- amp: { agents: false, skills: false, rules: false, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7186
- kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7187
- aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7188
- goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true },
7189
- zed: { agents: true, skills: false, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: false, modelOverride: false },
7190
- antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true }
7363
+ amp: { agents: false, skills: false, rules: false, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7364
+ kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7365
+ aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7366
+ goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7367
+ zed: { agents: true, skills: false, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: false, modelOverride: false, nativeQuestionTool: false },
7368
+ antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false }
7191
7369
  };
7192
7370
  }
7193
7371
  });
@@ -11115,6 +11293,62 @@ tags: [<tag>, ...]
11115
11293
  The loader agent applies content-security and integrity checks to every
11116
11294
  entry; see \`hatch3r-learnings-loader\` for the full protocol.
11117
11295
 
11296
+ ## Recommended First Learning \u2014 Pipeline Drift
11297
+
11298
+ Copy the markdown block below into \`.agents/learnings/pipeline-drift-rule-73.md\`
11299
+ to prime your AI tool against the bypass pattern reported in hatch3r
11300
+ issue #73 (GitHub Copilot Chat skipping the four-phase sub-agent
11301
+ pipeline on Tier-3 epics). The \`hatch3r-learnings-loader\` agent will
11302
+ surface it on session start.
11303
+
11304
+ \`\`\`markdown
11305
+ ---
11306
+ id: pipeline-drift-rule-73
11307
+ category: pitfall
11308
+ area: orchestration
11309
+ recorded: 2026-05-12
11310
+ source: manual
11311
+ confidence: high
11312
+ author: human
11313
+ tags: [orchestration, copilot, drift]
11314
+ ---
11315
+
11316
+ ## Learning
11317
+
11318
+ The hatch3r four-phase sub-agent pipeline (Research -> Implement ->
11319
+ Review -> Quality) is trust-based on Copilot Chat \u2014 Copilot has
11320
+ \`hooks: false\` in \`src/adapters/index.ts\`, exposes no PreToolUse /
11321
+ pre-edit hook, and does not surface its chat transcript to external
11322
+ processes. Drift is invisible by default: Copilot can call
11323
+ \`multi_replace_string_in_file\` / \`create_file\` inline on a Tier-3
11324
+ task and the build can still pass.
11325
+
11326
+ Self-detectable signals:
11327
+
11328
+ - The orchestrator's reply does NOT start with the
11329
+ \`[hatch3r-pipeline: phase N | last: ... | next: ...]\` header on
11330
+ a tracked Tier 2+ task -> halt and re-ground.
11331
+ - A code-writing tool was called before the user confirmed the
11332
+ Pre-Implementation Summary on a Tier 3 task -> bypass mode.
11333
+ - An \`Edit\` / \`Write\` / equivalent fired from the orchestrator
11334
+ turn rather than from inside a \`hatch3r-implementer\` Task
11335
+ sub-agent -> bypass mode.
11336
+
11337
+ ## Evidence
11338
+
11339
+ - Issue: https://github.com/hatch3r-dev/hatch3r/issues/73
11340
+ - Rules: \`rules/hatch3r-agent-orchestration.md\` (Per-Turn
11341
+ Pipeline-State Header, Mandatory Delegation Directive);
11342
+ \`rules/hatch3r-deep-context.md\` (Tier 3 \u2014 Deep hard gate).
11343
+ - Adapter capability: \`src/adapters/index.ts\` \u2014 \`copilot\` is the
11344
+ only adapter with \`hooks: false\`.
11345
+ \`\`\`
11346
+
11347
+ Customize the \`recorded\` date and \`tags\` to match your setup.
11348
+ Adapters other than Copilot also benefit from this learning when
11349
+ the bypass pattern is plausible on their host (e.g., long-context
11350
+ sessions on any adapter).
11351
+
11118
11352
  Delete this README once you have authored real learnings.
11119
11353
  `;
11120
11354
  function selectionHasBoardContent(selection) {
@@ -11200,7 +11434,12 @@ async function runInitInner(options) {
11200
11434
  s1.succeed(step(1, totalSteps, `Canonical files created (${countSelectionItems(contentSelection)} items)`));
11201
11435
  const s2 = createSpinner(step(2, totalSteps, "Preparing manifest..."));
11202
11436
  s2.start();
11203
- const manifest = createManifest({ platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, content: contentSelection, languages: repoInfo.languages, worktreeEnabled, customization });
11437
+ const effectiveCustomization = customization ?? existingManifest?.customization;
11438
+ const manifest = createManifest({ platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, content: contentSelection, languages: repoInfo.languages, worktreeEnabled, customization: effectiveCustomization });
11439
+ const preservedFields = options.preservedManifestFields ?? (existingManifest ? extractPreservedManifestFields(existingManifest) : void 0);
11440
+ if (preservedFields) {
11441
+ applyPreservedManifestFields(manifest, preservedFields);
11442
+ }
11204
11443
  s2.succeed(step(2, totalSteps, "Manifest prepared"));
11205
11444
  const s3 = createSpinner(
11206
11445
  step(3, totalSteps, `Generating ${tools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")} output...`)
@@ -12069,6 +12308,7 @@ function resolveToolsFromOpts(toolsFlag, repoInfo) {
12069
12308
  }
12070
12309
 
12071
12310
  // src/cli/commands/clean.ts
12311
+ init_hatchJson();
12072
12312
  function captureConfig(manifest) {
12073
12313
  return {
12074
12314
  platform: manifest.platform ?? "github",
@@ -12087,7 +12327,8 @@ function captureConfig(manifest) {
12087
12327
  items: { agents: [], skills: [], rules: [], commands: [], prompts: [], hooks: [], githubAgents: [] }
12088
12328
  },
12089
12329
  worktreeEnabled: manifest.worktree?.enabled ?? false,
12090
- customization: manifest.customization
12330
+ customization: manifest.customization,
12331
+ preservedFields: extractPreservedManifestFields(manifest)
12091
12332
  };
12092
12333
  }
12093
12334
  function printInventory(inventory) {
@@ -12237,6 +12478,10 @@ async function cleanCommand(opts = {}) {
12237
12478
  // manifest preserves integration config and per-artifact overrides
12238
12479
  // across a clean -> reinit cycle.
12239
12480
  customization: config.customization,
12481
+ // 1.7.1: carry full platform/user manifest state (board IDs,
12482
+ // costTracking, specs, extension config, worktree extras) forward
12483
+ // so a clean -> reinit cycle no longer wipes them.
12484
+ preservedManifestFields: config.preservedFields,
12240
12485
  // Reinit-after-clean already prompted the user; suppress runInit's
12241
12486
  // own post-init create-prompt so we do not stack two confirmations.
12242
12487
  yes: true
@@ -13039,7 +13284,7 @@ function normalizeToRepoPath(absPath, rootDir) {
13039
13284
  const rel = relative6(rootDir, absPath);
13040
13285
  return sep5 === "/" ? rel : rel.split(sep5).join(posix3.sep);
13041
13286
  }
13042
- function buildProvenanceManifest(hatchVersion, rootDir, perAdapterOutputs) {
13287
+ function buildProvenanceManifest(hatchVersion, rootDir, perAdapterOutputs, previousManifest) {
13043
13288
  const entries = [];
13044
13289
  for (const { adapter, outputs } of perAdapterOutputs) {
13045
13290
  for (const out of outputs) {
@@ -13056,6 +13301,9 @@ function buildProvenanceManifest(hatchVersion, rootDir, perAdapterOutputs) {
13056
13301
  if (byAdapter !== 0) return byAdapter;
13057
13302
  return a.path.localeCompare(b.path);
13058
13303
  });
13304
+ if (previousManifest && previousManifest.hatchVersion === hatchVersion && provenanceEntriesEqual(previousManifest.entries, entries)) {
13305
+ return previousManifest;
13306
+ }
13059
13307
  return {
13060
13308
  version: 1,
13061
13309
  generated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -13063,10 +13311,62 @@ function buildProvenanceManifest(hatchVersion, rootDir, perAdapterOutputs) {
13063
13311
  entries
13064
13312
  };
13065
13313
  }
13314
+ function provenanceEntriesEqual(a, b) {
13315
+ if (a.length !== b.length) return false;
13316
+ for (let i = 0; i < a.length; i++) {
13317
+ const ea = a[i];
13318
+ const eb = b[i];
13319
+ if (ea.adapter !== eb.adapter) return false;
13320
+ if (ea.path !== eb.path) return false;
13321
+ if (ea.sourceFiles.length !== eb.sourceFiles.length) return false;
13322
+ for (let j = 0; j < ea.sourceFiles.length; j++) {
13323
+ if (ea.sourceFiles[j] !== eb.sourceFiles[j]) return false;
13324
+ }
13325
+ }
13326
+ return true;
13327
+ }
13066
13328
  async function writeProvenanceManifest(agentsDir, manifest) {
13067
13329
  const filePath = join31(agentsDir, PROVENANCE_FILE);
13068
13330
  await atomicWriteFile(filePath, JSON.stringify(manifest, null, 2) + "\n");
13069
13331
  }
13332
+ async function readProvenanceManifest(agentsDir) {
13333
+ const filePath = join31(agentsDir, PROVENANCE_FILE);
13334
+ let raw;
13335
+ try {
13336
+ raw = await readFile22(filePath, "utf-8");
13337
+ } catch (err) {
13338
+ if (err.code === "ENOENT") return null;
13339
+ throw err;
13340
+ }
13341
+ let parsed;
13342
+ try {
13343
+ parsed = JSON.parse(raw);
13344
+ } catch (err) {
13345
+ if (err instanceof SyntaxError) return null;
13346
+ throw err;
13347
+ }
13348
+ if (!isProvenanceManifest(parsed)) return null;
13349
+ return parsed;
13350
+ }
13351
+ function isProvenanceManifest(data) {
13352
+ if (typeof data !== "object" || data === null) return false;
13353
+ const obj = data;
13354
+ if (typeof obj.version !== "number") return false;
13355
+ if (typeof obj.generated !== "string") return false;
13356
+ if (typeof obj.hatchVersion !== "string") return false;
13357
+ if (!Array.isArray(obj.entries)) return false;
13358
+ for (const e of obj.entries) {
13359
+ if (typeof e !== "object" || e === null) return false;
13360
+ const entry = e;
13361
+ if (typeof entry.adapter !== "string") return false;
13362
+ if (typeof entry.path !== "string") return false;
13363
+ if (!Array.isArray(entry.sourceFiles)) return false;
13364
+ for (const s of entry.sourceFiles) {
13365
+ if (typeof s !== "string") return false;
13366
+ }
13367
+ }
13368
+ return true;
13369
+ }
13070
13370
 
13071
13371
  // src/cli/commands/sync.ts
13072
13372
  init_archive();
@@ -13442,12 +13742,16 @@ async function syncCommand(opts = {}) {
13442
13742
  `Integrity manifest regenerated with ${successfulAdapters.length}/${m.tools.length} adapters successful. Re-run sync after resolving errors to produce a complete manifest.`
13443
13743
  );
13444
13744
  }
13745
+ const previousProvenanceManifest = await readProvenanceManifest(agentsDir);
13445
13746
  const provenanceManifest = buildProvenanceManifest(
13446
13747
  HATCH3R_VERSION,
13447
13748
  rootDir,
13448
- perAdapterOutputs
13749
+ perAdapterOutputs,
13750
+ previousProvenanceManifest
13449
13751
  );
13450
- await writeProvenanceManifest(agentsDir, provenanceManifest);
13752
+ if (provenanceManifest !== previousProvenanceManifest) {
13753
+ await writeProvenanceManifest(agentsDir, provenanceManifest);
13754
+ }
13451
13755
  const orphanDiag = formatOrphanCleanupDiagnostic(orphanEntries);
13452
13756
  if (orphanDiag) warn(orphanDiag);
13453
13757
  const mergedByAdapter = { ...previousManagedByAdapter };