hatch3r 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/agents/hatch3r-a11y-auditor.md +7 -14
- package/agents/hatch3r-architect.md +7 -14
- package/agents/hatch3r-ci-watcher.md +7 -13
- package/agents/hatch3r-context-rules.md +5 -10
- package/agents/hatch3r-dependency-auditor.md +10 -19
- package/agents/hatch3r-devops.md +7 -16
- package/agents/hatch3r-docs-writer.md +7 -14
- package/agents/hatch3r-fixer.md +2 -8
- package/agents/hatch3r-implementer.md +2 -8
- package/agents/hatch3r-learnings-loader.md +150 -21
- package/agents/hatch3r-lint-fixer.md +7 -12
- package/agents/hatch3r-perf-profiler.md +7 -14
- package/agents/hatch3r-researcher.md +7 -14
- package/agents/hatch3r-reviewer.md +7 -13
- package/agents/hatch3r-security-auditor.md +7 -15
- package/agents/hatch3r-test-writer.md +7 -14
- package/agents/modes/architecture.md +44 -0
- package/agents/modes/boundary-analysis.md +45 -0
- package/agents/modes/codebase-impact.md +81 -0
- package/agents/modes/complexity-risk.md +40 -0
- package/agents/modes/coverage-analysis.md +44 -0
- package/agents/modes/current-state.md +52 -0
- package/agents/modes/feature-design.md +39 -0
- package/agents/modes/impact-analysis.md +45 -0
- package/agents/modes/library-docs.md +31 -0
- package/agents/modes/migration-path.md +55 -0
- package/agents/modes/prior-art.md +31 -0
- package/agents/modes/refactoring-strategy.md +55 -0
- package/agents/modes/regression.md +45 -0
- package/agents/modes/requirements-elicitation.md +68 -0
- package/agents/modes/risk-assessment.md +41 -0
- package/agents/modes/risk-prioritization.md +43 -0
- package/agents/modes/root-cause.md +39 -0
- package/agents/modes/similar-implementation.md +70 -0
- package/agents/modes/symptom-trace.md +39 -0
- package/agents/modes/test-pattern.md +61 -0
- package/agents/shared/external-knowledge.md +32 -0
- package/agents/shared/quality-charter.md +78 -0
- package/commands/board/pickup-azure-devops.md +4 -0
- package/commands/board/pickup-delegation-multi.md +3 -0
- package/commands/board/pickup-delegation.md +3 -0
- package/commands/board/pickup-github.md +4 -0
- package/commands/board/pickup-gitlab.md +4 -0
- package/commands/board/pickup-post-impl.md +8 -1
- package/commands/board/shared-azure-devops.md +13 -3
- package/commands/board/shared-github.md +1 -0
- package/commands/board/shared-gitlab.md +9 -2
- package/commands/hatch3r-agent-customize.md +5 -1
- package/commands/hatch3r-board-groom.md +55 -2
- package/commands/hatch3r-board-init.md +5 -2
- package/commands/hatch3r-board-shared.md +62 -2
- package/commands/hatch3r-command-customize.md +4 -0
- package/commands/hatch3r-context-health.md +22 -2
- package/commands/hatch3r-cost-tracking.md +14 -0
- package/commands/hatch3r-hooks.md +1 -1
- package/commands/hatch3r-learn.md +68 -2
- package/commands/hatch3r-quick-change.md +29 -3
- package/commands/hatch3r-revision.md +136 -16
- package/commands/hatch3r-rule-customize.md +4 -0
- package/commands/hatch3r-skill-customize.md +4 -0
- package/commands/hatch3r-workflow.md +10 -1
- package/dist/cli/index.js +2528 -640
- package/dist/cli/index.js.map +1 -1
- package/package.json +12 -9
- package/rules/hatch3r-agent-orchestration-detail.md +159 -0
- package/rules/hatch3r-agent-orchestration-detail.mdc +156 -0
- package/rules/hatch3r-agent-orchestration.md +91 -318
- package/rules/hatch3r-agent-orchestration.mdc +127 -149
- package/rules/hatch3r-code-standards.mdc +10 -2
- package/rules/hatch3r-component-conventions.mdc +0 -1
- package/rules/hatch3r-deep-context.mdc +30 -8
- package/rules/hatch3r-dependency-management.mdc +17 -5
- package/rules/hatch3r-i18n.mdc +0 -1
- package/rules/hatch3r-migrations.mdc +12 -1
- package/rules/hatch3r-observability.mdc +289 -0
- package/rules/hatch3r-security-patterns.mdc +11 -0
- package/rules/hatch3r-testing.mdc +1 -1
- package/rules/hatch3r-theming.mdc +0 -1
- package/rules/hatch3r-tooling-hierarchy.mdc +18 -4
- package/skills/hatch3r-agent-customize/SKILL.md +4 -72
- package/skills/hatch3r-command-customize/SKILL.md +4 -62
- package/skills/hatch3r-customize/SKILL.md +117 -0
- package/skills/hatch3r-dep-audit/SKILL.md +1 -1
- package/skills/hatch3r-rule-customize/SKILL.md +4 -65
- package/skills/hatch3r-skill-customize/SKILL.md +4 -62
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import ora from "ora";
|
|
|
12
12
|
import boxen from "boxen";
|
|
13
13
|
|
|
14
14
|
// src/version.ts
|
|
15
|
-
var HATCH3R_VERSION = "1.
|
|
15
|
+
var HATCH3R_VERSION = "1.4.0";
|
|
16
16
|
|
|
17
17
|
// src/cli/shared/ui.ts
|
|
18
18
|
var CYAN = chalk.hex("#06b6d4");
|
|
@@ -132,7 +132,7 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
132
132
|
import chalk3 from "chalk";
|
|
133
133
|
|
|
134
134
|
// src/types.ts
|
|
135
|
-
var TOOLS = ["cursor", "copilot", "claude", "opencode", "windsurf", "amp", "codex", "gemini", "cline", "aider", "kiro", "goose", "zed", "amazon-q"];
|
|
135
|
+
var TOOLS = ["cursor", "copilot", "claude", "opencode", "windsurf", "amp", "codex", "gemini", "cline", "aider", "kiro", "goose", "zed", "amazon-q", "antigravity"];
|
|
136
136
|
var VALID_TOOLS = new Set(TOOLS);
|
|
137
137
|
var TOOL_CHOICES = TOOLS.join(", ");
|
|
138
138
|
var MANAGED_BLOCK_START = "<!-- HATCH3R:BEGIN -->";
|
|
@@ -140,9 +140,10 @@ var MANAGED_BLOCK_END = "<!-- HATCH3R:END -->";
|
|
|
140
140
|
var HATCH3R_PREFIX = "hatch3r-";
|
|
141
141
|
var AGENTS_DIR = ".agents";
|
|
142
142
|
var HatchError = class extends Error {
|
|
143
|
-
constructor(message, exitCode = 1) {
|
|
143
|
+
constructor(message, exitCode = 1, errorCode = "UNKNOWN_ERROR") {
|
|
144
144
|
super(message);
|
|
145
145
|
this.exitCode = exitCode;
|
|
146
|
+
this.errorCode = errorCode;
|
|
146
147
|
this.name = "HatchError";
|
|
147
148
|
}
|
|
148
149
|
};
|
|
@@ -275,8 +276,8 @@ async function resolvePatterns(rootDir, patterns) {
|
|
|
275
276
|
}
|
|
276
277
|
function isInsideWorktree(dir) {
|
|
277
278
|
try {
|
|
278
|
-
const
|
|
279
|
-
return
|
|
279
|
+
const stat6 = statSync(join(dir, ".git"));
|
|
280
|
+
return stat6.isFile();
|
|
280
281
|
} catch {
|
|
281
282
|
return false;
|
|
282
283
|
}
|
|
@@ -356,6 +357,9 @@ var ADAPTER_WORKTREE_PATTERNS = {
|
|
|
356
357
|
],
|
|
357
358
|
"amazon-q": [
|
|
358
359
|
{ pattern: ".amazonq/", strategy: "copy", reason: "Amazon Q adapter output (rules, settings)" }
|
|
360
|
+
],
|
|
361
|
+
antigravity: [
|
|
362
|
+
{ pattern: ".antigravity/", strategy: "copy", reason: "Antigravity adapter output (rules, skills, settings)" }
|
|
359
363
|
]
|
|
360
364
|
};
|
|
361
365
|
async function generateWorktreeInclude(manifest, rootDir) {
|
|
@@ -535,7 +539,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
|
|
|
535
539
|
error("Worktree path is required when running from the main repo.");
|
|
536
540
|
console.log(chalk3.dim(" Usage: hatch3r worktree-setup <worktree-path>"));
|
|
537
541
|
console.log(chalk3.dim(" Or run this command from inside a worktree.\n"));
|
|
538
|
-
throw new HatchError("Missing worktree path", 1);
|
|
542
|
+
throw new HatchError("Missing worktree path", 1, "VALIDATION_ERROR");
|
|
539
543
|
}
|
|
540
544
|
targetRoot = join3(cwd, worktreePath);
|
|
541
545
|
}
|
|
@@ -547,7 +551,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
|
|
|
547
551
|
if (err.code === "ENOENT") {
|
|
548
552
|
error(`No ${WORKTREE_INCLUDE_FILE} found in ${mainRoot}`);
|
|
549
553
|
console.log(chalk3.dim(" Run `hatch3r init` or `hatch3r sync` to generate it.\n"));
|
|
550
|
-
throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1);
|
|
554
|
+
throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1, "FS_ERROR");
|
|
551
555
|
}
|
|
552
556
|
throw err;
|
|
553
557
|
}
|
|
@@ -610,8 +614,9 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
|
|
|
610
614
|
}
|
|
611
615
|
|
|
612
616
|
// src/cli/commands/config.ts
|
|
613
|
-
import { fileURLToPath as
|
|
614
|
-
import {
|
|
617
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
618
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
619
|
+
import { dirname as dirname11, join as join21 } from "path";
|
|
615
620
|
import chalk5 from "chalk";
|
|
616
621
|
import inquirer2 from "inquirer";
|
|
617
622
|
|
|
@@ -627,7 +632,8 @@ import {
|
|
|
627
632
|
access,
|
|
628
633
|
rename,
|
|
629
634
|
unlink as unlink2,
|
|
630
|
-
open
|
|
635
|
+
open,
|
|
636
|
+
copyFile as copyFile2
|
|
631
637
|
} from "fs/promises";
|
|
632
638
|
import { dirname as dirname3, basename } from "path";
|
|
633
639
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -774,7 +780,16 @@ var DENY_PATTERNS = [
|
|
|
774
780
|
/override\s+(all\s+)?security/i,
|
|
775
781
|
/(?:atob|Buffer\.from)\s*\([^)]*(?:eval|exec|require)/i,
|
|
776
782
|
/(?:chmod|chown)\s+[0-7]{3,4}/i,
|
|
777
|
-
/(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i
|
|
783
|
+
/(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i,
|
|
784
|
+
// Prompt injection indicators
|
|
785
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
786
|
+
/disregard\s+(all\s+)?(previous|prior|above)/i,
|
|
787
|
+
/you\s+are\s+now\s+(?:a|an|the)\s/i,
|
|
788
|
+
/new\s+instructions\s*:/i,
|
|
789
|
+
/system\s+prompt\s*:/i,
|
|
790
|
+
/forget\s+(all\s+)?(previous|prior|above)\s+(instructions|rules|context)/i,
|
|
791
|
+
/act\s+as\s+(?:a|an)\s+(?:unrestricted|unfiltered|jailbroken)/i,
|
|
792
|
+
/do\s+not\s+follow\s+(?:any|the|your)\s+(?:previous|prior|above|original)\s/i
|
|
778
793
|
];
|
|
779
794
|
var ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\uFEFF\u00AD]/g;
|
|
780
795
|
var MAX_CUSTOMIZE_MD_BYTES = 10240;
|
|
@@ -966,6 +981,14 @@ async function atomicWriteFile(filePath, content) {
|
|
|
966
981
|
throw err;
|
|
967
982
|
}
|
|
968
983
|
}
|
|
984
|
+
} catch (err) {
|
|
985
|
+
const code = err.code;
|
|
986
|
+
if (code === "ENOSPC") {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`Not enough disk space to write ${filePath}. Free up space and re-run the command.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
throw err;
|
|
969
992
|
} finally {
|
|
970
993
|
try {
|
|
971
994
|
await unlink2(tmpPath);
|
|
@@ -991,7 +1014,7 @@ async function safeWriteFile(filePath, content, options = {}) {
|
|
|
991
1014
|
return {
|
|
992
1015
|
path: filePath,
|
|
993
1016
|
action: "skipped",
|
|
994
|
-
warning: `Skipped ${filePath}:
|
|
1017
|
+
warning: `Skipped ${filePath}: managed block markers (HATCH3R:BEGIN/END) missing. To fix: restore the markers around hatch3r content, or move your custom content and re-run hatch3r update.`
|
|
995
1018
|
};
|
|
996
1019
|
}
|
|
997
1020
|
const customContent = extractCustomContent(existingContent);
|
|
@@ -1000,11 +1023,13 @@ async function safeWriteFile(filePath, content, options = {}) {
|
|
|
1000
1023
|
try {
|
|
1001
1024
|
merged = insertManagedBlock(existingContent, options.managedContent);
|
|
1002
1025
|
} catch {
|
|
1026
|
+
const bakPath = filePath + ".bak";
|
|
1027
|
+
await copyFile2(filePath, bakPath);
|
|
1003
1028
|
await atomicWriteFile(filePath, content);
|
|
1004
1029
|
return {
|
|
1005
1030
|
path: filePath,
|
|
1006
1031
|
action: "updated",
|
|
1007
|
-
warning: `Auto-repaired corrupted managed block in ${filePath}`
|
|
1032
|
+
warning: `Auto-repaired corrupted managed block in ${filePath} (backup saved to ${bakPath})`
|
|
1008
1033
|
};
|
|
1009
1034
|
}
|
|
1010
1035
|
await atomicWriteFile(filePath, merged);
|
|
@@ -1023,7 +1048,7 @@ async function safeWriteFile(filePath, content, options = {}) {
|
|
|
1023
1048
|
return {
|
|
1024
1049
|
path: filePath,
|
|
1025
1050
|
action: "skipped",
|
|
1026
|
-
warning: `Skipped ${filePath}:
|
|
1051
|
+
warning: `Skipped ${filePath}: managed block markers (HATCH3R:BEGIN/END) missing. To fix: restore the markers around hatch3r content, or move your custom content and re-run hatch3r update.`
|
|
1027
1052
|
};
|
|
1028
1053
|
}
|
|
1029
1054
|
|
|
@@ -1137,6 +1162,22 @@ function validateManifest(data) {
|
|
|
1137
1162
|
if (!specs.paths.every((v) => typeof v === "string")) return false;
|
|
1138
1163
|
if (specs.lastGenerated !== void 0 && typeof specs.lastGenerated !== "string") return false;
|
|
1139
1164
|
}
|
|
1165
|
+
if (obj.workspace !== void 0) {
|
|
1166
|
+
if (typeof obj.workspace !== "object" || obj.workspace === null) return false;
|
|
1167
|
+
const ws = obj.workspace;
|
|
1168
|
+
if (typeof ws.rootPath !== "string") return false;
|
|
1169
|
+
if (typeof ws.lastSync !== "string") return false;
|
|
1170
|
+
if (typeof ws.syncVersion !== "string") return false;
|
|
1171
|
+
if (typeof ws.workspaceChecksum !== "string") return false;
|
|
1172
|
+
if (ws.excludedContent !== void 0) {
|
|
1173
|
+
if (!Array.isArray(ws.excludedContent)) return false;
|
|
1174
|
+
if (!ws.excludedContent.every((v) => typeof v === "string")) return false;
|
|
1175
|
+
}
|
|
1176
|
+
if (ws.localContent !== void 0) {
|
|
1177
|
+
if (!Array.isArray(ws.localContent)) return false;
|
|
1178
|
+
if (!ws.localContent.every((v) => typeof v === "string")) return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1140
1181
|
return true;
|
|
1141
1182
|
}
|
|
1142
1183
|
async function readManifest(rootDir) {
|
|
@@ -1294,10 +1335,10 @@ async function ensureEnvMcp(rootDir, servers) {
|
|
|
1294
1335
|
}
|
|
1295
1336
|
|
|
1296
1337
|
// src/cli/commands/update.ts
|
|
1297
|
-
import { cp as
|
|
1338
|
+
import { cp as cp3, mkdir as mkdir5, readdir as readdir7, stat as stat2 } from "fs/promises";
|
|
1298
1339
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1299
1340
|
import { fileURLToPath } from "url";
|
|
1300
|
-
import { dirname as
|
|
1341
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
1301
1342
|
import chalk4 from "chalk";
|
|
1302
1343
|
import inquirer from "inquirer";
|
|
1303
1344
|
|
|
@@ -1397,7 +1438,7 @@ async function readGlobMd(baseDir, fileType) {
|
|
|
1397
1438
|
let entries;
|
|
1398
1439
|
try {
|
|
1399
1440
|
const all = await readdir(baseDir, { recursive: true });
|
|
1400
|
-
entries = all.filter((f) => f.endsWith(".md"));
|
|
1441
|
+
entries = all.filter((f) => f.endsWith(".md")).sort();
|
|
1401
1442
|
} catch (err) {
|
|
1402
1443
|
if (err.code !== "ENOENT") throw err;
|
|
1403
1444
|
return [];
|
|
@@ -1433,7 +1474,7 @@ async function readGlobMd(baseDir, fileType) {
|
|
|
1433
1474
|
async function readSkillSubdirs(baseDir) {
|
|
1434
1475
|
let dirents;
|
|
1435
1476
|
try {
|
|
1436
|
-
dirents = await readdir(baseDir, { withFileTypes: true });
|
|
1477
|
+
dirents = (await readdir(baseDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1437
1478
|
} catch (err) {
|
|
1438
1479
|
if (err.code !== "ENOENT") throw err;
|
|
1439
1480
|
return [];
|
|
@@ -1517,7 +1558,16 @@ Full protocol: \`hatch3r-agent-orchestration\` rule in \`/.agents/rules/\`.
|
|
|
1517
1558
|
- Rules: \`/.agents/rules/\` \u2014 Agents: \`/.agents/agents/\` \u2014 Skills: \`/.agents/skills/\`
|
|
1518
1559
|
- Commands: \`/.agents/commands/\` \u2014 MCP: \`/.agents/mcp/mcp.json\` \u2014 Policy: \`/.agents/policy/\`
|
|
1519
1560
|
|
|
1520
|
-
Do not edit \`hatch3r-\` prefixed files \u2014 managed by hatch3r, overwritten on update
|
|
1561
|
+
Do not edit \`hatch3r-\` prefixed files \u2014 managed by hatch3r, overwritten on update.
|
|
1562
|
+
|
|
1563
|
+
## Getting Started (staged introduction)
|
|
1564
|
+
|
|
1565
|
+
New to hatch3r? Start here and expand as you go:
|
|
1566
|
+
|
|
1567
|
+
**Day 1 \u2014 Core workflow:** Use the 4-phase pipeline above for any task. Start by invoking \`hatch3r-researcher\` for context, then \`hatch3r-implementer\` for changes.
|
|
1568
|
+
**Week 1 \u2014 Skills & commands:** Load skills from \`/.agents/skills/\` matching your task type. Try \`/hatch3r-feature\` or \`/hatch3r-bug-fix\` commands.
|
|
1569
|
+
**Week 2 \u2014 Board & team:** If using project management, run \`/hatch3r-board-init\` to set up your board. Use \`/hatch3r-board-pickup\` for structured delivery.
|
|
1570
|
+
**Ongoing \u2014 Customization:** Override agent behavior via \`.hatch3r/{type}/{id}.customize.yaml\`. Add project learnings to \`/.agents/learnings/\`.`;
|
|
1521
1571
|
async function generateBridgeOrchestration(agentsDir) {
|
|
1522
1572
|
const skills = await readSkillDirs(join8(agentsDir, "skills"));
|
|
1523
1573
|
if (skills.length === 0) return BRIDGE_ORCHESTRATION;
|
|
@@ -1897,13 +1947,14 @@ var BaseAdapter = class {
|
|
|
1897
1947
|
* Adapters that violate these invariants will produce broken output files or
|
|
1898
1948
|
* corrupt user content during the merge phase.
|
|
1899
1949
|
*/
|
|
1900
|
-
async generate(agentsDir, manifest) {
|
|
1950
|
+
async generate(agentsDir, manifest, generationMode = "standard") {
|
|
1901
1951
|
this.warnings = [];
|
|
1902
1952
|
return this.doGenerate({
|
|
1903
1953
|
agentsDir,
|
|
1904
1954
|
manifest,
|
|
1905
1955
|
features: manifest.features,
|
|
1906
|
-
projectRoot: dirname4(agentsDir)
|
|
1956
|
+
projectRoot: dirname4(agentsDir),
|
|
1957
|
+
generationMode
|
|
1907
1958
|
});
|
|
1908
1959
|
}
|
|
1909
1960
|
/**
|
|
@@ -1915,8 +1966,19 @@ var BaseAdapter = class {
|
|
|
1915
1966
|
const outputs = await this.generate(agentsDir, manifest);
|
|
1916
1967
|
return outputs.map((o) => o.path);
|
|
1917
1968
|
}
|
|
1918
|
-
async bridgeHeader(
|
|
1919
|
-
const orchestration = await generateBridgeOrchestration(agentsDir);
|
|
1969
|
+
async bridgeHeader(ctx, agentsPath = "/.agents/AGENTS.md") {
|
|
1970
|
+
const orchestration = await generateBridgeOrchestration(ctx.agentsDir);
|
|
1971
|
+
if (this.isMinimal(ctx)) {
|
|
1972
|
+
return [
|
|
1973
|
+
"",
|
|
1974
|
+
"# Hatch3r Agent Instructions",
|
|
1975
|
+
"",
|
|
1976
|
+
`Instructions: \`${agentsPath}\``,
|
|
1977
|
+
"",
|
|
1978
|
+
this.stripMinimal(orchestration),
|
|
1979
|
+
""
|
|
1980
|
+
];
|
|
1981
|
+
}
|
|
1920
1982
|
return [
|
|
1921
1983
|
"",
|
|
1922
1984
|
"# Hatch3r Agent Instructions",
|
|
@@ -1931,12 +1993,17 @@ var BaseAdapter = class {
|
|
|
1931
1993
|
if (!ctx.features.rules) return [];
|
|
1932
1994
|
const lines = [];
|
|
1933
1995
|
const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
|
|
1996
|
+
const minimal = this.isMinimal(ctx);
|
|
1934
1997
|
for (const rule of rules) {
|
|
1935
1998
|
const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
|
|
1936
1999
|
this.warnings.push(...warnings);
|
|
1937
2000
|
if (skip) continue;
|
|
1938
2001
|
const desc = overrides.description ?? rule.description;
|
|
1939
|
-
|
|
2002
|
+
if (minimal) {
|
|
2003
|
+
lines.push(`## ${rule.id}`, "", this.stripMinimal(content), "");
|
|
2004
|
+
} else {
|
|
2005
|
+
lines.push(`## ${rule.id}`, "", desc, "", content, "");
|
|
2006
|
+
}
|
|
1940
2007
|
}
|
|
1941
2008
|
return lines;
|
|
1942
2009
|
}
|
|
@@ -1944,6 +2011,7 @@ var BaseAdapter = class {
|
|
|
1944
2011
|
if (!ctx.features.agents) return [];
|
|
1945
2012
|
const lines = [];
|
|
1946
2013
|
const agents = await readCanonicalFiles(ctx.agentsDir, "agents");
|
|
2014
|
+
const minimal = this.isMinimal(ctx);
|
|
1947
2015
|
for (const agent of agents) {
|
|
1948
2016
|
const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
|
|
1949
2017
|
this.warnings.push(...warnings);
|
|
@@ -1953,7 +2021,11 @@ var BaseAdapter = class {
|
|
|
1953
2021
|
const fmt = model ? (formatModel ?? defaultModelFormat)(model) : void 0;
|
|
1954
2022
|
lines.push(`## Agent: ${agent.id}`);
|
|
1955
2023
|
if (fmt && !fmt.after) lines.push(fmt.text);
|
|
1956
|
-
|
|
2024
|
+
if (minimal) {
|
|
2025
|
+
lines.push("", this.stripMinimal(content));
|
|
2026
|
+
} else {
|
|
2027
|
+
lines.push("", desc, "", content);
|
|
2028
|
+
}
|
|
1957
2029
|
if (fmt?.after) lines.push("", fmt.text);
|
|
1958
2030
|
lines.push("");
|
|
1959
2031
|
}
|
|
@@ -2036,6 +2108,23 @@ ${wrapInManagedBlock(content)}`, content));
|
|
|
2036
2108
|
if (!ctx.features.hooks) return [];
|
|
2037
2109
|
return readHookDefinitions(ctx.agentsDir);
|
|
2038
2110
|
}
|
|
2111
|
+
/** Returns true when the adapter is running in minimal generation mode. */
|
|
2112
|
+
isMinimal(ctx) {
|
|
2113
|
+
return ctx.generationMode === "minimal";
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Strip verbose content for minimal generation mode.
|
|
2117
|
+
* Removes markdown comments, collapses excessive blank lines,
|
|
2118
|
+
* strips decorative formatting, and trims descriptions.
|
|
2119
|
+
*/
|
|
2120
|
+
stripMinimal(content) {
|
|
2121
|
+
let result = content;
|
|
2122
|
+
result = result.replace(/<!--[\s\S]*?-->/g, "");
|
|
2123
|
+
result = result.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
2124
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
2125
|
+
result = result.trim();
|
|
2126
|
+
return result;
|
|
2127
|
+
}
|
|
2039
2128
|
};
|
|
2040
2129
|
|
|
2041
2130
|
// src/adapters/aider.ts
|
|
@@ -2043,7 +2132,7 @@ var AiderAdapter = class extends BaseAdapter {
|
|
|
2043
2132
|
name = "aider";
|
|
2044
2133
|
async doGenerate(ctx) {
|
|
2045
2134
|
const inner = [
|
|
2046
|
-
...await this.bridgeHeader(ctx
|
|
2135
|
+
...await this.bridgeHeader(ctx),
|
|
2047
2136
|
...await this.inlineRules(ctx),
|
|
2048
2137
|
...await this.inlineAgents(ctx)
|
|
2049
2138
|
].join("\n");
|
|
@@ -2071,7 +2160,7 @@ var AmazonQAdapter = class extends BaseAdapter {
|
|
|
2071
2160
|
async doGenerate(ctx) {
|
|
2072
2161
|
const results = [];
|
|
2073
2162
|
const inner = [
|
|
2074
|
-
...await this.bridgeHeader(ctx
|
|
2163
|
+
...await this.bridgeHeader(ctx),
|
|
2075
2164
|
...await this.inlineRules(ctx),
|
|
2076
2165
|
...await this.inlineAgents(ctx)
|
|
2077
2166
|
].join("\n");
|
|
@@ -2083,7 +2172,7 @@ var AmazonQAdapter = class extends BaseAdapter {
|
|
|
2083
2172
|
if (mcp && Object.keys(mcp).length > 0) {
|
|
2084
2173
|
const entries = this.buildStdMcpEntries(mcp);
|
|
2085
2174
|
if (Object.keys(entries).length > 0) {
|
|
2086
|
-
results.push(output(".amazonq/
|
|
2175
|
+
results.push(output(".amazonq/mcp.json", JSON.stringify({ mcpServers: entries }, null, 2)));
|
|
2087
2176
|
}
|
|
2088
2177
|
}
|
|
2089
2178
|
return results;
|
|
@@ -2096,15 +2185,15 @@ var AmpAdapter = class extends BaseAdapter {
|
|
|
2096
2185
|
async doGenerate(ctx) {
|
|
2097
2186
|
const results = [];
|
|
2098
2187
|
const inner = [
|
|
2099
|
-
...await this.bridgeHeader(ctx
|
|
2188
|
+
...await this.bridgeHeader(ctx),
|
|
2100
2189
|
...await this.inlineRules(ctx),
|
|
2101
2190
|
...await this.inlineAgents(ctx, (m) => ({
|
|
2102
2191
|
text: `**Recommended model:** \`${m}\`. Use Smart mode for Opus, Rush for Haiku, Deep for Codex.`
|
|
2103
2192
|
}))
|
|
2104
2193
|
].join("\n");
|
|
2105
|
-
results.push(output("
|
|
2194
|
+
results.push(output("AGENTS.md", wrapInManagedBlock(inner), inner));
|
|
2106
2195
|
results.push(
|
|
2107
|
-
...await this.processSkillsRaw(ctx, (id) => `.
|
|
2196
|
+
...await this.processSkillsRaw(ctx, (id) => `.agents/skills/${toPrefixedId(id)}/SKILL.md`)
|
|
2108
2197
|
);
|
|
2109
2198
|
const mcp = await this.readFilteredMcp(ctx);
|
|
2110
2199
|
if (mcp && Object.keys(mcp).length > 0) {
|
|
@@ -2117,22 +2206,55 @@ var AmpAdapter = class extends BaseAdapter {
|
|
|
2117
2206
|
}
|
|
2118
2207
|
};
|
|
2119
2208
|
|
|
2209
|
+
// src/adapters/antigravity.ts
|
|
2210
|
+
var AntigravityAdapter = class extends BaseAdapter {
|
|
2211
|
+
name = "antigravity";
|
|
2212
|
+
async doGenerate(ctx) {
|
|
2213
|
+
const results = [];
|
|
2214
|
+
const inner = [
|
|
2215
|
+
...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
|
|
2216
|
+
...await this.inlineRules(ctx),
|
|
2217
|
+
...await this.inlineAgents(ctx)
|
|
2218
|
+
].join("\n");
|
|
2219
|
+
results.push(output(".antigravity/rules.md", wrapInManagedBlock(inner), inner));
|
|
2220
|
+
results.push(
|
|
2221
|
+
...await this.processSkillsRaw(ctx, (id) => `.antigravity/skills/${toPrefixedId(id)}/SKILL.md`)
|
|
2222
|
+
);
|
|
2223
|
+
const mcp = await this.readFilteredMcp(ctx);
|
|
2224
|
+
if (mcp && Object.keys(mcp).length > 0) {
|
|
2225
|
+
const entries = this.buildStdMcpEntries(mcp);
|
|
2226
|
+
if (Object.keys(entries).length > 0) {
|
|
2227
|
+
results.push(output(".antigravity/settings.json", JSON.stringify({ mcpServers: entries }, null, 2)));
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return results;
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
|
|
2120
2234
|
// src/adapters/claude.ts
|
|
2121
2235
|
var AGENT_TEAMS_SECTION = [
|
|
2122
|
-
"## Agent Teams
|
|
2236
|
+
"## Agent Teams",
|
|
2123
2237
|
"",
|
|
2124
|
-
"This project uses hatch3r's 4-phase sub-agent pipeline (Research
|
|
2238
|
+
"This project uses hatch3r's 4-phase sub-agent pipeline (Research -> Implement -> Review -> Quality)",
|
|
2125
2239
|
"which maps directly to Claude Code Agent Teams. Each phase becomes a teammate role.",
|
|
2126
2240
|
"",
|
|
2127
2241
|
"### Enabling Agent Teams",
|
|
2128
2242
|
"",
|
|
2129
|
-
"Agent Teams is
|
|
2130
|
-
"
|
|
2243
|
+
"Agent Teams is enabled via `.claude/settings.json`. The env var `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`",
|
|
2244
|
+
"is set automatically by hatch3r. Once enabled, request a team in the prompt:",
|
|
2131
2245
|
"",
|
|
2132
2246
|
"```",
|
|
2133
2247
|
"Create an agent team for this task. Use the hatch3r 4-phase pipeline.",
|
|
2134
2248
|
"```",
|
|
2135
2249
|
"",
|
|
2250
|
+
"### Teammate Display Modes",
|
|
2251
|
+
"",
|
|
2252
|
+
"Agent Teams supports two display modes configured via `teammateMode` in `.claude/settings.json`:",
|
|
2253
|
+
"",
|
|
2254
|
+
'- `"auto"` (default): uses split panes if inside tmux, in-process otherwise.',
|
|
2255
|
+
'- `"in-process"`: all teammates run inside your main terminal. Use Shift+Down to cycle.',
|
|
2256
|
+
'- `"tmux"`: each teammate gets its own pane. Requires tmux or iTerm2.',
|
|
2257
|
+
"",
|
|
2136
2258
|
"### Pipeline-to-Team Mapping",
|
|
2137
2259
|
"",
|
|
2138
2260
|
"| Phase | Teammate Role | hatch3r Agents | Delegation Notes |",
|
|
@@ -2183,6 +2305,20 @@ var AGENT_TEAMS_SECTION = [
|
|
|
2183
2305
|
"- Assign explicit file boundaries to avoid edit conflicts between teammates.",
|
|
2184
2306
|
"- Use the `hatch3r-agent-team` command (`/hatch3r-agent-team`) for guided team creation."
|
|
2185
2307
|
];
|
|
2308
|
+
var AGENT_TEAMS_SECTION_MINIMAL = [
|
|
2309
|
+
"## Agent Teams",
|
|
2310
|
+
"",
|
|
2311
|
+
"Pipeline maps to Claude Code Agent Teams. Enable via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`.",
|
|
2312
|
+
"",
|
|
2313
|
+
"| Phase | Role | Agents |",
|
|
2314
|
+
"|-------|------|--------|",
|
|
2315
|
+
"| Research | `researcher` | `hatch3r-researcher` |",
|
|
2316
|
+
"| Implement | `implementer` | `hatch3r-implementer` |",
|
|
2317
|
+
"| Review | `reviewer` | `hatch3r-reviewer`, `hatch3r-fixer` |",
|
|
2318
|
+
"| Quality | `quality-*` | `hatch3r-test-writer`, `hatch3r-security-auditor`, + conditional |",
|
|
2319
|
+
"",
|
|
2320
|
+
"Use `/hatch3r-agent-team` for guided team creation."
|
|
2321
|
+
];
|
|
2186
2322
|
var AGENT_TEAM_COMMAND = `# hatch3r Agent Team
|
|
2187
2323
|
|
|
2188
2324
|
Create a Claude Code Agent Team that follows the hatch3r 4-phase pipeline.
|
|
@@ -2237,7 +2373,8 @@ Each quality teammate owns distinct files to avoid conflicts.
|
|
|
2237
2373
|
|
|
2238
2374
|
## Important
|
|
2239
2375
|
|
|
2240
|
-
-
|
|
2376
|
+
- Teammate display mode is configured in \`.claude/settings.json\` via \`teammateMode\` (\`auto\`, \`in-process\`, or \`tmux\`)
|
|
2377
|
+
- Override for a single session: \`claude --teammate-mode in-process\`
|
|
2241
2378
|
- Each teammate reads CLAUDE.md and inherits project rules automatically
|
|
2242
2379
|
- Assign explicit file/directory boundaries to each teammate
|
|
2243
2380
|
- The lead coordinates; it should NOT implement code itself (use delegate mode)
|
|
@@ -2288,8 +2425,20 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
2288
2425
|
name = "claude";
|
|
2289
2426
|
async doGenerate(ctx) {
|
|
2290
2427
|
const results = [];
|
|
2428
|
+
const minimal = this.isMinimal(ctx);
|
|
2291
2429
|
const bridgeOrchestration = await generateBridgeOrchestration(ctx.agentsDir);
|
|
2292
|
-
const
|
|
2430
|
+
const teamsSection = minimal ? AGENT_TEAMS_SECTION_MINIMAL : AGENT_TEAMS_SECTION;
|
|
2431
|
+
const innerParts = minimal ? [
|
|
2432
|
+
"",
|
|
2433
|
+
"# Hatch3r Project Instructions",
|
|
2434
|
+
"",
|
|
2435
|
+
"Instructions: `.agents/AGENTS.md`. Rules: `.claude/rules/`. Agents: `.claude/agents/`.",
|
|
2436
|
+
"",
|
|
2437
|
+
this.stripMinimal(bridgeOrchestration),
|
|
2438
|
+
"",
|
|
2439
|
+
...teamsSection,
|
|
2440
|
+
""
|
|
2441
|
+
] : [
|
|
2293
2442
|
"",
|
|
2294
2443
|
"# Hatch3r Project Instructions",
|
|
2295
2444
|
"",
|
|
@@ -2298,14 +2447,24 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
2298
2447
|
"",
|
|
2299
2448
|
bridgeOrchestration,
|
|
2300
2449
|
"",
|
|
2301
|
-
...
|
|
2450
|
+
...teamsSection,
|
|
2302
2451
|
"",
|
|
2303
2452
|
"## Personal Settings",
|
|
2304
2453
|
"",
|
|
2305
2454
|
"Create `CLAUDE.local.md` for personal settings (not committed to git).",
|
|
2306
2455
|
"Claude Code reads this file for user-specific preferences.",
|
|
2456
|
+
"",
|
|
2457
|
+
"## Getting Started with Claude Code",
|
|
2458
|
+
"",
|
|
2459
|
+
"New to this project's agent setup? Progress through these stages:",
|
|
2460
|
+
"",
|
|
2461
|
+
"**Start here:** Rules in `.claude/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
|
|
2462
|
+
"**Next:** Use `/hatch3r-feature` or `/hatch3r-bug-fix` commands for guided workflows.",
|
|
2463
|
+
"**Then:** Delegate to agents in `.claude/agents/` \u2014 use Agent Teams for parallel execution.",
|
|
2464
|
+
"**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
|
|
2307
2465
|
""
|
|
2308
|
-
]
|
|
2466
|
+
];
|
|
2467
|
+
const innerContent = innerParts.join("\n");
|
|
2309
2468
|
results.push(output("CLAUDE.md", wrapInManagedBlock(innerContent), innerContent));
|
|
2310
2469
|
if (ctx.features.rules) {
|
|
2311
2470
|
const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
|
|
@@ -2314,7 +2473,9 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
2314
2473
|
this.warnings.push(...warnings);
|
|
2315
2474
|
if (skip) continue;
|
|
2316
2475
|
const desc = overrides.description ?? rule.description;
|
|
2317
|
-
const body = `# ${rule.id}
|
|
2476
|
+
const body = minimal ? `# ${rule.id}
|
|
2477
|
+
|
|
2478
|
+
${this.stripMinimal(content)}` : `# ${rule.id}
|
|
2318
2479
|
|
|
2319
2480
|
${desc}
|
|
2320
2481
|
|
|
@@ -2330,23 +2491,33 @@ ${content}`;
|
|
|
2330
2491
|
if (skip) continue;
|
|
2331
2492
|
const agentId = toPrefixedId(agent.id);
|
|
2332
2493
|
const model = resolveAgentModel(agent.id, agent, ctx.manifest, overrides);
|
|
2333
|
-
const modelGuidance = model ? `
|
|
2334
|
-
|
|
2335
|
-
## Recommended Model
|
|
2336
|
-
|
|
2337
|
-
Preferred: \`${model}\`. Set via \`/model ${model}\` or env \`CLAUDE_CODE_SUBAGENT_MODEL=${model}\`.` : "";
|
|
2338
2494
|
const desc = overrides.description ?? agent.description;
|
|
2339
2495
|
const fm = `---
|
|
2340
2496
|
description: ${desc}
|
|
2341
2497
|
---`;
|
|
2342
|
-
|
|
2343
|
-
|
|
2498
|
+
if (minimal) {
|
|
2499
|
+
const modelNote = model ? `
|
|
2500
|
+
Model: \`${model}\`` : "";
|
|
2501
|
+
const body = `${this.stripMinimal(content)}${modelNote}`;
|
|
2502
|
+
results.push(output(`.claude/agents/${agentId}.md`, `${fm}
|
|
2503
|
+
|
|
2504
|
+
${wrapInManagedBlock(body)}`, body));
|
|
2505
|
+
} else {
|
|
2506
|
+
const modelGuidance = model ? `
|
|
2507
|
+
|
|
2508
|
+
## Recommended Model
|
|
2509
|
+
|
|
2510
|
+
Preferred: \`${model}\`. Set via \`/model ${model}\` or env \`CLAUDE_CODE_SUBAGENT_MODEL=${model}\`.` : "";
|
|
2511
|
+
const body = `${content}${modelGuidance}`;
|
|
2512
|
+
results.push(output(`.claude/agents/${agentId}.md`, `${fm}
|
|
2344
2513
|
|
|
2345
2514
|
${wrapInManagedBlock(body)}`, body));
|
|
2515
|
+
}
|
|
2346
2516
|
}
|
|
2347
2517
|
}
|
|
2348
2518
|
const defaultAllow = ["Read", "Edit", "MultiEdit", "Write", "Grep", "Glob", "LS", "TodoRead", "TodoWrite"];
|
|
2349
2519
|
const claudeConfig = ctx.manifest.claude;
|
|
2520
|
+
const teammateMode = claudeConfig?.teammateMode ?? "auto";
|
|
2350
2521
|
const settingsObj = {
|
|
2351
2522
|
_hatch3r: {
|
|
2352
2523
|
version: HATCH3R_VERSION,
|
|
@@ -2356,7 +2527,7 @@ ${wrapInManagedBlock(body)}`, body));
|
|
|
2356
2527
|
allow: claudeConfig?.permissions?.allow ?? defaultAllow,
|
|
2357
2528
|
deny: claudeConfig?.permissions?.deny ?? []
|
|
2358
2529
|
},
|
|
2359
|
-
teammateMode
|
|
2530
|
+
teammateMode
|
|
2360
2531
|
};
|
|
2361
2532
|
const hooksConfig = {};
|
|
2362
2533
|
const hooks = await this.readHooks(ctx);
|
|
@@ -2387,7 +2558,9 @@ ${wrapInManagedBlock(body)}`, body));
|
|
|
2387
2558
|
});
|
|
2388
2559
|
}
|
|
2389
2560
|
settingsObj.hooks = hooksConfig;
|
|
2390
|
-
|
|
2561
|
+
const agentTeamsSetting = ctx.manifest.claude?.agentTeams;
|
|
2562
|
+
if (agentTeamsSetting === "ga") {
|
|
2563
|
+
} else if (agentTeamsSetting !== false) {
|
|
2391
2564
|
settingsObj.env = { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
|
|
2392
2565
|
}
|
|
2393
2566
|
results.push(output(".claude/settings.json", JSON.stringify(settingsObj, null, 2)));
|
|
@@ -2509,7 +2682,16 @@ ${content}`;
|
|
|
2509
2682
|
"Canonical agent instructions live at `/.agents/AGENTS.md`.",
|
|
2510
2683
|
"Rules and skills are managed in `.roo/rules/` and `.cline/skills/`.",
|
|
2511
2684
|
"",
|
|
2512
|
-
bridgeOrchestration
|
|
2685
|
+
bridgeOrchestration,
|
|
2686
|
+
"",
|
|
2687
|
+
"## Getting Started with Roo Code",
|
|
2688
|
+
"",
|
|
2689
|
+
"New to this project's agent setup? Progress through these stages:",
|
|
2690
|
+
"",
|
|
2691
|
+
"**Start here:** Rules in `.roo/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
|
|
2692
|
+
"**Next:** Use workflow commands in `.clinerules/workflows/` for guided task execution.",
|
|
2693
|
+
"**Then:** Switch to custom modes (defined in `.roomodes`) for specialized agent behaviors.",
|
|
2694
|
+
"**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files."
|
|
2513
2695
|
].join("\n");
|
|
2514
2696
|
results.push(output(".roo/rules/hatch3r-bridge.md", wrapInManagedBlock(bridgeBody), bridgeBody));
|
|
2515
2697
|
return results;
|
|
@@ -2660,6 +2842,15 @@ ${r.rule.description}
|
|
|
2660
2842
|
|
|
2661
2843
|
${r.content}`
|
|
2662
2844
|
),
|
|
2845
|
+
"",
|
|
2846
|
+
"## Getting Started with Copilot",
|
|
2847
|
+
"",
|
|
2848
|
+
"New to this project's agent setup? Progress through these stages:",
|
|
2849
|
+
"",
|
|
2850
|
+
"**Start here:** Instructions in `.github/instructions/` scope rules to specific file patterns. The orchestration bridge above guides your workflow.",
|
|
2851
|
+
"**Next:** Use prompts in `.github/prompts/` and commands in `.github/copilot/commands/` for guided workflows.",
|
|
2852
|
+
"**Then:** Delegate to agents in `.github/agents/` for specialized tasks.",
|
|
2853
|
+
"**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
|
|
2663
2854
|
""
|
|
2664
2855
|
].join("\n");
|
|
2665
2856
|
results.push(output(".github/copilot-instructions.md", wrapInManagedBlock(innerContent), innerContent));
|
|
@@ -2806,7 +2997,7 @@ var CursorAdapter = class extends BaseAdapter {
|
|
|
2806
2997
|
const lines = [`name: ${agent.id}`, `description: ${desc}`];
|
|
2807
2998
|
if (model) lines.push(`model: ${model}`);
|
|
2808
2999
|
if (agent.readonly) lines.push("readonly: true");
|
|
2809
|
-
if (agent.background) lines.push("
|
|
3000
|
+
if (agent.background) lines.push("is_background: true");
|
|
2810
3001
|
const fm = `---
|
|
2811
3002
|
${lines.join("\n")}
|
|
2812
3003
|
---`;
|
|
@@ -2867,7 +3058,16 @@ Background subagents write output to \`~/.cursor/subagents/\` for later inspecti
|
|
|
2867
3058
|
|
|
2868
3059
|
Cursor v2.6 added MCP Apps (interactive UIs in agent chats) and Team Marketplaces for plugins.
|
|
2869
3060
|
If this project includes MCP servers that expose UI components, they will render inline as MCP Apps.
|
|
2870
|
-
Plugin configurations in \`.cursor/mcp.json\` are compatible with Team Marketplace distribution
|
|
3061
|
+
Plugin configurations in \`.cursor/mcp.json\` are compatible with Team Marketplace distribution.
|
|
3062
|
+
|
|
3063
|
+
## Getting Started with Cursor
|
|
3064
|
+
|
|
3065
|
+
New to this project's agent setup? Progress through these stages:
|
|
3066
|
+
|
|
3067
|
+
**Start here:** Rules in \`.cursor/rules/\` are loaded automatically. The orchestration bridge above guides your workflow.
|
|
3068
|
+
**Next:** Use \`/hatch3r-feature\` or \`/hatch3r-bug-fix\` commands in Cursor chat for guided workflows.
|
|
3069
|
+
**Then:** Delegate to agents in \`.cursor/agents/\` \u2014 Cursor supports up to 4 subagents in parallel.
|
|
3070
|
+
**Later:** Customize agent behavior via \`.hatch3r/{type}/{id}.customize.yaml\` without editing managed files.`;
|
|
2871
3071
|
results.push(mdcOutput(".cursor/rules/hatch3r-bridge.mdc", bridgeFm, bridgeBody));
|
|
2872
3072
|
if (ctx.manifest.tools.includes("cursor")) {
|
|
2873
3073
|
const envConfig = {
|
|
@@ -2899,7 +3099,7 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
2899
3099
|
async doGenerate(ctx) {
|
|
2900
3100
|
const results = [];
|
|
2901
3101
|
const inner = [
|
|
2902
|
-
...await this.bridgeHeader(ctx
|
|
3102
|
+
...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
|
|
2903
3103
|
...await this.inlineRules(ctx),
|
|
2904
3104
|
...await this.inlineAgents(ctx, (m) => ({
|
|
2905
3105
|
text: `**Recommended model:** \`${m}\`. Set via \`gemini --model ${m}\` or select in Google AI Studio.`,
|
|
@@ -2958,11 +3158,12 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
2958
3158
|
};
|
|
2959
3159
|
|
|
2960
3160
|
// src/adapters/goose.ts
|
|
3161
|
+
import { stringify as yamlStringify } from "yaml";
|
|
2961
3162
|
var GooseAdapter = class extends BaseAdapter {
|
|
2962
3163
|
name = "goose";
|
|
2963
3164
|
async doGenerate(ctx) {
|
|
2964
3165
|
const lines = [
|
|
2965
|
-
...await this.bridgeHeader(ctx
|
|
3166
|
+
...await this.bridgeHeader(ctx),
|
|
2966
3167
|
...await this.inlineRules(ctx),
|
|
2967
3168
|
...await this.inlineAgents(ctx)
|
|
2968
3169
|
];
|
|
@@ -2988,8 +3189,108 @@ var GooseAdapter = class extends BaseAdapter {
|
|
|
2988
3189
|
results.push(output(".goose/mcp.json", JSON.stringify(gooseMcp, null, 2)));
|
|
2989
3190
|
}
|
|
2990
3191
|
}
|
|
3192
|
+
const agents = ctx.features.agents ? await readCanonicalFiles(ctx.agentsDir, "agents") : [];
|
|
3193
|
+
const profile = await this.buildProfile(ctx, agents, mcp);
|
|
3194
|
+
const profileYaml = yamlStringify(profile);
|
|
3195
|
+
results.push(output(".goose/profiles/hatch3r.yaml", profileYaml));
|
|
2991
3196
|
return results;
|
|
2992
3197
|
}
|
|
3198
|
+
/** Build a Goose profile that maps hatch3r content to Goose's recipe system. */
|
|
3199
|
+
async buildProfile(ctx, agents, mcp) {
|
|
3200
|
+
const extensions = this.buildExtensions(mcp);
|
|
3201
|
+
const recipe = await this.buildRecipe(ctx, agents);
|
|
3202
|
+
const capabilities = this.deriveAcpCapabilities(ctx, agents);
|
|
3203
|
+
return {
|
|
3204
|
+
name: "hatch3r",
|
|
3205
|
+
description: `hatch3r-managed Goose profile for ${ctx.manifest.project || ctx.manifest.repo}. Provides agent pipeline, recipe interoperability, and ACP compatibility.`,
|
|
3206
|
+
instructions: `Follow the canonical agent instructions at .agents/AGENTS.md. Use the hatch3r 4-phase pipeline: Research, Implement, Review, Quality.`,
|
|
3207
|
+
...extensions.length > 0 ? { extensions } : {},
|
|
3208
|
+
recipes: [recipe],
|
|
3209
|
+
acp: {
|
|
3210
|
+
enabled: true,
|
|
3211
|
+
version: "0.2",
|
|
3212
|
+
capabilities
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
/** Map MCP servers to Goose extensions. */
|
|
3217
|
+
buildExtensions(mcp) {
|
|
3218
|
+
if (!mcp) return [];
|
|
3219
|
+
const extensions = [];
|
|
3220
|
+
for (const [name, entry] of Object.entries(mcp)) {
|
|
3221
|
+
if (entry.command) {
|
|
3222
|
+
extensions.push({
|
|
3223
|
+
name,
|
|
3224
|
+
type: "mcp",
|
|
3225
|
+
config: {
|
|
3226
|
+
command: entry.command,
|
|
3227
|
+
args: entry.args || [],
|
|
3228
|
+
...entry.env && Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
|
|
3229
|
+
}
|
|
3230
|
+
});
|
|
3231
|
+
} else if (entry.url) {
|
|
3232
|
+
extensions.push({
|
|
3233
|
+
name,
|
|
3234
|
+
type: "mcp",
|
|
3235
|
+
config: { url: entry.url }
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
return extensions;
|
|
3240
|
+
}
|
|
3241
|
+
/** Build a Goose recipe from hatch3r's agent pipeline. */
|
|
3242
|
+
async buildRecipe(ctx, agents) {
|
|
3243
|
+
const steps = [];
|
|
3244
|
+
const phaseMap = [
|
|
3245
|
+
{ phase: "Research", agentPattern: "researcher", fallback: "Gather context from the codebase. Identify affected files, patterns, and conventions. Do not modify any files." },
|
|
3246
|
+
{ phase: "Implement", agentPattern: "implementer", fallback: "Implement the requested changes following project conventions. Require plan approval before making changes." },
|
|
3247
|
+
{ phase: "Review", agentPattern: "reviewer", fallback: "Review all changes for correctness, style, security, and adherence to project rules. Report findings as Critical/Warning/Info." },
|
|
3248
|
+
{ phase: "Quality", agentPattern: "test-writer", fallback: "Write or update tests for the implemented changes. Run the test suite and verify all tests pass." }
|
|
3249
|
+
];
|
|
3250
|
+
for (const { phase, agentPattern, fallback } of phaseMap) {
|
|
3251
|
+
const matchingAgent = agents.find(
|
|
3252
|
+
(a) => a.id.includes(agentPattern)
|
|
3253
|
+
);
|
|
3254
|
+
if (matchingAgent) {
|
|
3255
|
+
const { skip, warnings } = await applyCustomization(ctx.projectRoot, matchingAgent);
|
|
3256
|
+
this.warnings.push(...warnings);
|
|
3257
|
+
if (!skip) {
|
|
3258
|
+
const instruction = matchingAgent.description || fallback;
|
|
3259
|
+
steps.push({
|
|
3260
|
+
instruction: `[${phase}] ${instruction}`,
|
|
3261
|
+
agent: toPrefixedId(matchingAgent.id)
|
|
3262
|
+
});
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
steps.push({ instruction: `[${phase}] ${fallback}` });
|
|
3267
|
+
}
|
|
3268
|
+
return {
|
|
3269
|
+
name: "hatch3r-pipeline",
|
|
3270
|
+
description: "hatch3r 4-phase development pipeline: Research, Implement, Review, Quality.",
|
|
3271
|
+
steps
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
/** Derive ACP capability advertisements from project configuration. */
|
|
3275
|
+
deriveAcpCapabilities(ctx, agents) {
|
|
3276
|
+
const capabilities = [
|
|
3277
|
+
"code-generation",
|
|
3278
|
+
"code-review"
|
|
3279
|
+
];
|
|
3280
|
+
if (agents.some((a) => a.id.includes("test"))) {
|
|
3281
|
+
capabilities.push("test-generation");
|
|
3282
|
+
}
|
|
3283
|
+
if (agents.some((a) => a.id.includes("security"))) {
|
|
3284
|
+
capabilities.push("security-audit");
|
|
3285
|
+
}
|
|
3286
|
+
if (agents.some((a) => a.id.includes("docs"))) {
|
|
3287
|
+
capabilities.push("documentation");
|
|
3288
|
+
}
|
|
3289
|
+
if (ctx.features.mcp) {
|
|
3290
|
+
capabilities.push("tool-use");
|
|
3291
|
+
}
|
|
3292
|
+
return capabilities;
|
|
3293
|
+
}
|
|
2993
3294
|
};
|
|
2994
3295
|
|
|
2995
3296
|
// src/adapters/kiro.ts
|
|
@@ -3006,7 +3307,7 @@ var KiroAdapter = class extends BaseAdapter {
|
|
|
3006
3307
|
name = "kiro";
|
|
3007
3308
|
async doGenerate(ctx) {
|
|
3008
3309
|
const results = [];
|
|
3009
|
-
const lines = [...await this.bridgeHeader(ctx
|
|
3310
|
+
const lines = [...await this.bridgeHeader(ctx)];
|
|
3010
3311
|
if (ctx.features.rules) {
|
|
3011
3312
|
const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
|
|
3012
3313
|
for (const rule of rules) {
|
|
@@ -3139,7 +3440,7 @@ function isGlobPattern(scope) {
|
|
|
3139
3440
|
function ruleTrigger(scope) {
|
|
3140
3441
|
if (!scope) return "model_decision";
|
|
3141
3442
|
if (scope === "always") return "always_on";
|
|
3142
|
-
return "
|
|
3443
|
+
return "glob";
|
|
3143
3444
|
}
|
|
3144
3445
|
var WindsurfAdapter = class extends BaseAdapter {
|
|
3145
3446
|
name = "windsurf";
|
|
@@ -3155,7 +3456,17 @@ var WindsurfAdapter = class extends BaseAdapter {
|
|
|
3155
3456
|
"",
|
|
3156
3457
|
bridgeOrchestration,
|
|
3157
3458
|
"",
|
|
3158
|
-
...await this.inlineAgents(ctx)
|
|
3459
|
+
...await this.inlineAgents(ctx),
|
|
3460
|
+
"",
|
|
3461
|
+
"## Getting Started with Windsurf",
|
|
3462
|
+
"",
|
|
3463
|
+
"New to this project's agent setup? Progress through these stages:",
|
|
3464
|
+
"",
|
|
3465
|
+
"**Start here:** Rules in `.windsurf/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
|
|
3466
|
+
"**Next:** Use commands in `.windsurf/workflows/` for guided workflows (e.g., feature development, bug fixes).",
|
|
3467
|
+
"**Then:** Use parallel Cascade sessions for independent tasks to maximize throughput.",
|
|
3468
|
+
"**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
|
|
3469
|
+
""
|
|
3159
3470
|
].join("\n");
|
|
3160
3471
|
results.push(output(".windsurfrules", wrapInManagedBlock(windsurfInner), windsurfInner));
|
|
3161
3472
|
if (ctx.features.rules) {
|
|
@@ -3166,7 +3477,7 @@ var WindsurfAdapter = class extends BaseAdapter {
|
|
|
3166
3477
|
if (skip) continue;
|
|
3167
3478
|
const scope = overrides.scope ?? rule.scope;
|
|
3168
3479
|
const trigger = ruleTrigger(scope);
|
|
3169
|
-
const globScope = trigger === "
|
|
3480
|
+
const globScope = trigger === "glob" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
|
|
3170
3481
|
const fm = `---
|
|
3171
3482
|
trigger: ${trigger}${globScope ? `
|
|
3172
3483
|
globs: "${globScope}"` : ""}
|
|
@@ -3204,7 +3515,7 @@ var ZedAdapter = class extends BaseAdapter {
|
|
|
3204
3515
|
name = "zed";
|
|
3205
3516
|
async doGenerate(ctx) {
|
|
3206
3517
|
const inner = [
|
|
3207
|
-
...await this.bridgeHeader(ctx
|
|
3518
|
+
...await this.bridgeHeader(ctx),
|
|
3208
3519
|
...await this.inlineRules(ctx),
|
|
3209
3520
|
...await this.inlineAgents(ctx)
|
|
3210
3521
|
].join("\n");
|
|
@@ -3227,7 +3538,8 @@ var adapters = {
|
|
|
3227
3538
|
kiro: new KiroAdapter(),
|
|
3228
3539
|
goose: new GooseAdapter(),
|
|
3229
3540
|
zed: new ZedAdapter(),
|
|
3230
|
-
"amazon-q": new AmazonQAdapter()
|
|
3541
|
+
"amazon-q": new AmazonQAdapter(),
|
|
3542
|
+
antigravity: new AntigravityAdapter()
|
|
3231
3543
|
};
|
|
3232
3544
|
function getAdapter(tool) {
|
|
3233
3545
|
const adapter = adapters[tool];
|
|
@@ -3250,7 +3562,8 @@ var ADAPTER_CAPABILITIES = {
|
|
|
3250
3562
|
kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false },
|
|
3251
3563
|
aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false },
|
|
3252
3564
|
goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false },
|
|
3253
|
-
zed: { agents: true, skills: false, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false }
|
|
3565
|
+
zed: { agents: true, skills: false, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false },
|
|
3566
|
+
antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false }
|
|
3254
3567
|
};
|
|
3255
3568
|
function getUnsupportedFeatureWarnings(tool, manifest) {
|
|
3256
3569
|
const caps = ADAPTER_CAPABILITIES[tool];
|
|
@@ -3349,7 +3662,7 @@ function validateIntegrityManifest(data) {
|
|
|
3349
3662
|
for (const val of Object.values(obj.files)) {
|
|
3350
3663
|
if (typeof val !== "string") return false;
|
|
3351
3664
|
}
|
|
3352
|
-
if (
|
|
3665
|
+
if (typeof obj.checksum !== "string") return false;
|
|
3353
3666
|
return true;
|
|
3354
3667
|
}
|
|
3355
3668
|
async function readIntegrityManifest(agentsDir) {
|
|
@@ -3370,12 +3683,10 @@ async function verifyIntegrity(agentsDir) {
|
|
|
3370
3683
|
return [];
|
|
3371
3684
|
}
|
|
3372
3685
|
const results = [];
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
return results;
|
|
3378
|
-
}
|
|
3686
|
+
const expected = createHash("sha256").update(JSON.stringify(manifest.files)).digest("hex");
|
|
3687
|
+
if (manifest.checksum !== expected) {
|
|
3688
|
+
results.push({ file: INTEGRITY_FILE, status: "tampered" });
|
|
3689
|
+
return results;
|
|
3379
3690
|
}
|
|
3380
3691
|
const manifestFiles = new Set(Object.keys(manifest.files));
|
|
3381
3692
|
for (const [filePath, expectedHash] of Object.entries(manifest.files)) {
|
|
@@ -3423,52 +3734,248 @@ async function verifyIntegrity(agentsDir) {
|
|
|
3423
3734
|
return results;
|
|
3424
3735
|
}
|
|
3425
3736
|
|
|
3426
|
-
// src/
|
|
3427
|
-
import { readFile as readFile12, readdir as readdir5,
|
|
3428
|
-
import {
|
|
3429
|
-
function
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3737
|
+
// src/archive/index.ts
|
|
3738
|
+
import { access as access3, cp, mkdir as mkdir3, readFile as readFile12, readdir as readdir5, rm, stat, writeFile as writeFile3 } from "fs/promises";
|
|
3739
|
+
import { dirname as dirname6, join as join14, sep } from "path";
|
|
3740
|
+
function toPosixPath(p) {
|
|
3741
|
+
return sep === "\\" ? p.replaceAll("\\", "/") : p;
|
|
3742
|
+
}
|
|
3743
|
+
var ARCHIVE_DIR = ".hatch3r-archive";
|
|
3744
|
+
var TOOL_PATH_PREFIXES = {
|
|
3745
|
+
cursor: [".cursor/"],
|
|
3746
|
+
claude: [".claude/", "CLAUDE.md", ".mcp.json"],
|
|
3747
|
+
copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
|
|
3748
|
+
windsurf: [".windsurf/", ".windsurfrules"],
|
|
3749
|
+
amp: [".amp/"],
|
|
3750
|
+
codex: [".codex/"],
|
|
3751
|
+
gemini: [".gemini/", "GEMINI.md"],
|
|
3752
|
+
cline: [".roo/", ".roomodes"],
|
|
3753
|
+
aider: ["CONVENTIONS.md", ".aider.conf.yml"],
|
|
3754
|
+
kiro: [".kiro/"],
|
|
3755
|
+
opencode: ["opencode.json"],
|
|
3756
|
+
goose: [".goosehints"],
|
|
3757
|
+
zed: [".rules"],
|
|
3758
|
+
"amazon-q": [".amazonq/"],
|
|
3759
|
+
antigravity: [".antigravity/"]
|
|
3760
|
+
};
|
|
3761
|
+
var PATH_PATTERNS = [
|
|
3762
|
+
{ pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
|
|
3763
|
+
{ pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
|
|
3764
|
+
{ pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
|
|
3765
|
+
{ pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
|
|
3766
|
+
];
|
|
3767
|
+
function parseOutputPath(filePath) {
|
|
3768
|
+
for (const { pattern, type } of PATH_PATTERNS) {
|
|
3769
|
+
const match = filePath.match(pattern);
|
|
3770
|
+
if (match) {
|
|
3771
|
+
let id = match[1];
|
|
3772
|
+
if (id.startsWith(HATCH3R_PREFIX)) {
|
|
3773
|
+
id = id.slice(HATCH3R_PREFIX.length);
|
|
3774
|
+
}
|
|
3775
|
+
id = sanitizeId(id);
|
|
3776
|
+
if (id.length > 0) return { type, id };
|
|
3777
|
+
}
|
|
3433
3778
|
}
|
|
3779
|
+
return null;
|
|
3434
3780
|
}
|
|
3435
|
-
function
|
|
3436
|
-
const
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3781
|
+
function stripFrontmatter(content) {
|
|
3782
|
+
const trimmed = content.trimStart();
|
|
3783
|
+
if (trimmed.startsWith("---")) {
|
|
3784
|
+
const endIdx = trimmed.indexOf("\n---", 3);
|
|
3785
|
+
if (endIdx !== -1) {
|
|
3786
|
+
return trimmed.slice(endIdx + 4).trim();
|
|
3787
|
+
}
|
|
3441
3788
|
}
|
|
3442
|
-
return
|
|
3789
|
+
return content.trim();
|
|
3443
3790
|
}
|
|
3444
|
-
async function
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3791
|
+
async function fileExists2(path) {
|
|
3792
|
+
try {
|
|
3793
|
+
await access3(path);
|
|
3794
|
+
return true;
|
|
3795
|
+
} catch {
|
|
3796
|
+
return false;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
async function collectToolFiles(rootDir, tool) {
|
|
3800
|
+
const prefixes = TOOL_PATH_PREFIXES[tool];
|
|
3801
|
+
if (!prefixes) return [];
|
|
3802
|
+
const files = [];
|
|
3803
|
+
for (const prefix of prefixes) {
|
|
3804
|
+
const absPath = join14(rootDir, prefix);
|
|
3805
|
+
if (prefix.endsWith("/")) {
|
|
3806
|
+
try {
|
|
3807
|
+
const entries = await readdir5(absPath, { recursive: true, withFileTypes: true });
|
|
3808
|
+
for (const entry of entries) {
|
|
3809
|
+
if (entry.isFile()) {
|
|
3810
|
+
const parent = entry.parentPath ?? entry.path ?? absPath;
|
|
3811
|
+
const relPath = toPosixPath(join14(prefix, parent.slice(absPath.length), entry.name));
|
|
3812
|
+
files.push(relPath);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
} catch {
|
|
3816
|
+
}
|
|
3817
|
+
} else if (await fileExists2(absPath)) {
|
|
3818
|
+
files.push(prefix);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
return files;
|
|
3822
|
+
}
|
|
3823
|
+
async function archiveToolOutputs(rootDir, tool) {
|
|
3824
|
+
const filesToArchive = await collectToolFiles(rootDir, tool);
|
|
3825
|
+
if (filesToArchive.length === 0) {
|
|
3826
|
+
return { archivedFiles: [], migrations: [] };
|
|
3827
|
+
}
|
|
3828
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3829
|
+
const archiveBase = join14(rootDir, ARCHIVE_DIR, tool, timestamp);
|
|
3830
|
+
const archivedFiles = [];
|
|
3831
|
+
const migrations = [];
|
|
3832
|
+
for (const relPath of filesToArchive) {
|
|
3833
|
+
const absPath = join14(rootDir, relPath);
|
|
3834
|
+
if (!await fileExists2(absPath)) continue;
|
|
3448
3835
|
let content;
|
|
3449
3836
|
try {
|
|
3450
|
-
|
|
3451
|
-
content = await readFile12(filePath, "utf-8");
|
|
3837
|
+
content = await readFile12(absPath, "utf-8");
|
|
3452
3838
|
} catch {
|
|
3453
3839
|
continue;
|
|
3454
3840
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
if (
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3841
|
+
if (hasManagedBlock(content)) {
|
|
3842
|
+
const customContent = stripFrontmatter(extractCustomContent(content));
|
|
3843
|
+
if (customContent.length > 0) {
|
|
3844
|
+
const parsed = parseOutputPath(relPath);
|
|
3845
|
+
if (parsed) {
|
|
3846
|
+
const customizePath = join14(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
|
|
3847
|
+
if (!await fileExists2(customizePath)) {
|
|
3848
|
+
await mkdir3(dirname6(customizePath), { recursive: true });
|
|
3849
|
+
await writeFile3(customizePath, customContent + "\n", "utf-8");
|
|
3850
|
+
migrations.push({
|
|
3851
|
+
from: relPath,
|
|
3852
|
+
to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
|
|
3853
|
+
type: parsed.type,
|
|
3854
|
+
id: parsed.id
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3462
3858
|
}
|
|
3463
3859
|
}
|
|
3860
|
+
const archiveDest = join14(archiveBase, relPath);
|
|
3861
|
+
await mkdir3(dirname6(archiveDest), { recursive: true });
|
|
3862
|
+
await cp(absPath, archiveDest);
|
|
3863
|
+
const srcStat = await stat(absPath);
|
|
3864
|
+
const destStat = await stat(archiveDest);
|
|
3865
|
+
if (destStat.size !== srcStat.size) {
|
|
3866
|
+
throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
|
|
3867
|
+
}
|
|
3868
|
+
await rm(absPath);
|
|
3869
|
+
archivedFiles.push(relPath);
|
|
3464
3870
|
}
|
|
3465
|
-
|
|
3871
|
+
await cleanEmptyDirs(rootDir, filesToArchive);
|
|
3872
|
+
return { archivedFiles, migrations };
|
|
3466
3873
|
}
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3874
|
+
async function cleanEmptyDirs(rootDir, paths) {
|
|
3875
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3876
|
+
for (const p of paths) {
|
|
3877
|
+
let dir = dirname6(join14(rootDir, p));
|
|
3878
|
+
while (dir !== rootDir && dir.length > rootDir.length) {
|
|
3879
|
+
dirs.add(dir);
|
|
3880
|
+
dir = dirname6(dir);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
const sorted = [...dirs].sort((a, b) => b.length - a.length);
|
|
3884
|
+
for (const dir of sorted) {
|
|
3885
|
+
try {
|
|
3886
|
+
const entries = await readdir5(dir);
|
|
3887
|
+
if (entries.length === 0) {
|
|
3888
|
+
await rm(dir, { recursive: true });
|
|
3889
|
+
}
|
|
3890
|
+
} catch {
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function removeManagedFilesForPaths(manifest, paths) {
|
|
3895
|
+
const pathSet = new Set(paths);
|
|
3896
|
+
manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
|
|
3897
|
+
}
|
|
3898
|
+
var MAX_ARCHIVE_ENTRIES = 5;
|
|
3899
|
+
async function pruneArchives(rootDir) {
|
|
3900
|
+
const archiveRoot = join14(rootDir, ARCHIVE_DIR);
|
|
3901
|
+
const pruned = [];
|
|
3902
|
+
let toolDirs;
|
|
3903
|
+
try {
|
|
3904
|
+
toolDirs = await readdir5(archiveRoot);
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
if (err.code === "ENOENT") return [];
|
|
3907
|
+
throw err;
|
|
3908
|
+
}
|
|
3909
|
+
for (const toolDir of toolDirs) {
|
|
3910
|
+
const toolPath = join14(archiveRoot, toolDir);
|
|
3911
|
+
let entries;
|
|
3912
|
+
try {
|
|
3913
|
+
const s = await stat(toolPath);
|
|
3914
|
+
if (!s.isDirectory()) continue;
|
|
3915
|
+
entries = await readdir5(toolPath);
|
|
3916
|
+
} catch {
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
entries.sort((a, b) => b.localeCompare(a));
|
|
3920
|
+
for (const entry of entries.slice(MAX_ARCHIVE_ENTRIES)) {
|
|
3921
|
+
const entryPath = join14(toolPath, entry);
|
|
3922
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
3923
|
+
pruned.push(`${toolDir}/${entry}`);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
return pruned;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// src/content/index.ts
|
|
3930
|
+
import { readFile as readFile13, readdir as readdir6, writeFile as writeFile4, cp as cp2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
|
|
3931
|
+
import { join as join15, dirname as dirname7, normalize, isAbsolute } from "path";
|
|
3932
|
+
function assertSafePath(relativePath, label2) {
|
|
3933
|
+
const sanitized = relativePath.replace(/\0/g, "");
|
|
3934
|
+
const normalized = normalize(sanitized);
|
|
3935
|
+
if (normalized.startsWith("..") || isAbsolute(normalized)) {
|
|
3936
|
+
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
|
|
3937
|
+
}
|
|
3938
|
+
if (sanitized !== relativePath) {
|
|
3939
|
+
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
function extractContentReferences(content) {
|
|
3943
|
+
const refs = /* @__PURE__ */ new Set();
|
|
3944
|
+
const pattern = /`(hatch3r-[a-z0-9-]+)`/g;
|
|
3945
|
+
let match;
|
|
3946
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3947
|
+
refs.add(match[1]);
|
|
3948
|
+
}
|
|
3949
|
+
return [...refs];
|
|
3950
|
+
}
|
|
3951
|
+
async function validateCrossReferences(contentRoot, index) {
|
|
3952
|
+
const warnings = [];
|
|
3953
|
+
const allIds = new Set(index.items.map((item) => item.id));
|
|
3954
|
+
for (const item of index.items) {
|
|
3955
|
+
let content;
|
|
3956
|
+
try {
|
|
3957
|
+
const filePath = item.type === "skill" ? join15(contentRoot, item.relativePath, "SKILL.md") : join15(contentRoot, `${item.relativePath}`);
|
|
3958
|
+
content = await readFile13(filePath, "utf-8");
|
|
3959
|
+
} catch {
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const refs = extractContentReferences(content);
|
|
3963
|
+
for (const ref of refs) {
|
|
3964
|
+
if (ref === item.id) continue;
|
|
3965
|
+
if (!allIds.has(ref)) {
|
|
3966
|
+
warnings.push(
|
|
3967
|
+
`${item.type} "${item.id}" references "${ref}" which does not exist in the content index`
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return { warnings };
|
|
3973
|
+
}
|
|
3974
|
+
var ORCHESTRATION_REQUIRED_AGENTS = [
|
|
3975
|
+
"hatch3r-researcher",
|
|
3976
|
+
"hatch3r-implementer",
|
|
3977
|
+
"hatch3r-reviewer",
|
|
3978
|
+
"hatch3r-test-writer",
|
|
3472
3979
|
"hatch3r-security-auditor"
|
|
3473
3980
|
];
|
|
3474
3981
|
function validateOrchestrationDependencies(selection) {
|
|
@@ -3485,6 +3992,12 @@ function validateOrchestrationDependencies(selection) {
|
|
|
3485
3992
|
}
|
|
3486
3993
|
return warnings;
|
|
3487
3994
|
}
|
|
3995
|
+
function typeIdKey(type, id) {
|
|
3996
|
+
return `${type}:${id}`;
|
|
3997
|
+
}
|
|
3998
|
+
function getAllItemsById(index, id) {
|
|
3999
|
+
return index.items.filter((item) => item.id === id);
|
|
4000
|
+
}
|
|
3488
4001
|
var CONTENT_TYPE_CONFIGS = [
|
|
3489
4002
|
{ dir: "agents", type: "agent", strategy: "glob" },
|
|
3490
4003
|
{ dir: "commands", type: "command", strategy: "glob" },
|
|
@@ -3497,20 +4010,20 @@ var CONTENT_TYPE_CONFIGS = [
|
|
|
3497
4010
|
async function buildContentIndex(contentRoot) {
|
|
3498
4011
|
const items = [];
|
|
3499
4012
|
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
3500
|
-
const dirPath =
|
|
4013
|
+
const dirPath = join15(contentRoot, config.dir);
|
|
3501
4014
|
if (config.strategy === "subdirectory") {
|
|
3502
4015
|
let dirents;
|
|
3503
4016
|
try {
|
|
3504
|
-
dirents = await
|
|
4017
|
+
dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3505
4018
|
} catch (err) {
|
|
3506
4019
|
if (err.code === "ENOENT") continue;
|
|
3507
4020
|
throw err;
|
|
3508
4021
|
}
|
|
3509
4022
|
for (const dirent of dirents) {
|
|
3510
4023
|
if (!dirent.isDirectory()) continue;
|
|
3511
|
-
const skillPath =
|
|
4024
|
+
const skillPath = join15(dirPath, dirent.name, "SKILL.md");
|
|
3512
4025
|
try {
|
|
3513
|
-
const raw = await
|
|
4026
|
+
const raw = await readFile13(skillPath, "utf-8");
|
|
3514
4027
|
const { metadata } = parseFrontmatter(raw);
|
|
3515
4028
|
const id = metadata.id || metadata.name || dirent.name;
|
|
3516
4029
|
items.push({
|
|
@@ -3519,7 +4032,7 @@ async function buildContentIndex(contentRoot) {
|
|
|
3519
4032
|
description: metadata.description ?? "",
|
|
3520
4033
|
tags: metadata.tags ?? [],
|
|
3521
4034
|
protected: metadata.protected,
|
|
3522
|
-
relativePath:
|
|
4035
|
+
relativePath: join15(config.dir, dirent.name)
|
|
3523
4036
|
});
|
|
3524
4037
|
} catch (err) {
|
|
3525
4038
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -3528,15 +4041,15 @@ async function buildContentIndex(contentRoot) {
|
|
|
3528
4041
|
} else {
|
|
3529
4042
|
let entries;
|
|
3530
4043
|
try {
|
|
3531
|
-
const all = await
|
|
3532
|
-
entries = all.filter((f) => f.endsWith(".md"));
|
|
4044
|
+
const all = await readdir6(dirPath);
|
|
4045
|
+
entries = all.filter((f) => f.endsWith(".md")).sort();
|
|
3533
4046
|
} catch (err) {
|
|
3534
4047
|
if (err.code === "ENOENT") continue;
|
|
3535
4048
|
throw err;
|
|
3536
4049
|
}
|
|
3537
4050
|
for (const file of entries) {
|
|
3538
|
-
const filePath =
|
|
3539
|
-
const raw = await
|
|
4051
|
+
const filePath = join15(dirPath, file);
|
|
4052
|
+
const raw = await readFile13(filePath, "utf-8");
|
|
3540
4053
|
const { metadata } = parseFrontmatter(raw);
|
|
3541
4054
|
const id = metadata.id || metadata.name || file.replace(/\.md$/, "");
|
|
3542
4055
|
const item = {
|
|
@@ -3545,13 +4058,13 @@ async function buildContentIndex(contentRoot) {
|
|
|
3545
4058
|
description: metadata.description ?? "",
|
|
3546
4059
|
tags: metadata.tags ?? [],
|
|
3547
4060
|
protected: metadata.protected,
|
|
3548
|
-
relativePath:
|
|
4061
|
+
relativePath: join15(config.dir, file)
|
|
3549
4062
|
};
|
|
3550
4063
|
if (config.type === "rule") {
|
|
3551
4064
|
const mdcFile = file.replace(/\.md$/, ".mdc");
|
|
3552
4065
|
try {
|
|
3553
|
-
await
|
|
3554
|
-
item.companionPath =
|
|
4066
|
+
await readFile13(join15(dirPath, mdcFile), "utf-8");
|
|
4067
|
+
item.companionPath = join15(config.dir, mdcFile);
|
|
3555
4068
|
} catch {
|
|
3556
4069
|
}
|
|
3557
4070
|
}
|
|
@@ -3561,18 +4074,36 @@ async function buildContentIndex(contentRoot) {
|
|
|
3561
4074
|
}
|
|
3562
4075
|
const byType = {};
|
|
3563
4076
|
const byId = /* @__PURE__ */ new Map();
|
|
4077
|
+
const byTypeAndId = /* @__PURE__ */ new Map();
|
|
4078
|
+
const collisions = [];
|
|
3564
4079
|
for (const item of items) {
|
|
3565
4080
|
if (!byType[item.type]) byType[item.type] = [];
|
|
3566
4081
|
byType[item.type].push(item);
|
|
4082
|
+
byTypeAndId.set(typeIdKey(item.type, item.id), item);
|
|
3567
4083
|
const existing = byId.get(item.id);
|
|
3568
|
-
if (existing
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
4084
|
+
if (existing) {
|
|
4085
|
+
const kind = existing.type !== item.type ? "cross-type" : "same-type";
|
|
4086
|
+
collisions.push({
|
|
4087
|
+
id: item.id,
|
|
4088
|
+
kind,
|
|
4089
|
+
existingType: existing.type,
|
|
4090
|
+
existingPath: existing.relativePath,
|
|
4091
|
+
duplicateType: item.type,
|
|
4092
|
+
duplicatePath: item.relativePath
|
|
4093
|
+
});
|
|
4094
|
+
if (kind === "cross-type") {
|
|
4095
|
+
console.warn(
|
|
4096
|
+
`[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} (${existing.relativePath}) and ${item.type} (${item.relativePath}). Use index.byTypeAndId for collision-safe lookup.`
|
|
4097
|
+
);
|
|
4098
|
+
} else {
|
|
4099
|
+
console.warn(
|
|
4100
|
+
`[hatch3r] Duplicate content ID: "${item.id}" found in ${existing.relativePath} and ${item.relativePath}. The later entry will shadow the earlier one in ID lookups.`
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
3572
4103
|
}
|
|
3573
4104
|
byId.set(item.id, item);
|
|
3574
4105
|
}
|
|
3575
|
-
return { items, byType, byId };
|
|
4106
|
+
return { items, byType, byId, byTypeAndId, collisions };
|
|
3576
4107
|
}
|
|
3577
4108
|
var TYPE_TO_SELECTION_KEY = {
|
|
3578
4109
|
agent: "agents",
|
|
@@ -3644,6 +4175,52 @@ function resolveSelection(preset, projectType, teamSize, index, customSelections
|
|
|
3644
4175
|
items
|
|
3645
4176
|
};
|
|
3646
4177
|
}
|
|
4178
|
+
function countPresetExclusions(preset, index) {
|
|
4179
|
+
if (preset.id === "custom") return 0;
|
|
4180
|
+
if (preset.id === "full") return 0;
|
|
4181
|
+
let count = 0;
|
|
4182
|
+
for (const item of index.items) {
|
|
4183
|
+
if (item.protected) continue;
|
|
4184
|
+
if (preset.includeTags.length > 0) {
|
|
4185
|
+
const includeSet = new Set(preset.includeTags);
|
|
4186
|
+
if (item.tags.length > 0 && !item.tags.some((t) => includeSet.has(t))) {
|
|
4187
|
+
count++;
|
|
4188
|
+
continue;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
if (preset.excludeTags.length > 0) {
|
|
4192
|
+
const excludeSet = new Set(preset.excludeTags);
|
|
4193
|
+
if (item.tags.every((t) => excludeSet.has(t))) {
|
|
4194
|
+
count++;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
return count;
|
|
4199
|
+
}
|
|
4200
|
+
function countProjectTypeExclusions(projectType, items) {
|
|
4201
|
+
const opposite = projectType === "greenfield" ? "brownfield" : "greenfield";
|
|
4202
|
+
let count = 0;
|
|
4203
|
+
for (const item of items) {
|
|
4204
|
+
if (item.protected) continue;
|
|
4205
|
+
if (item.tags.includes(opposite) && !item.tags.some((t) => t !== opposite && t !== "team" && t !== "solo")) {
|
|
4206
|
+
count++;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return count;
|
|
4210
|
+
}
|
|
4211
|
+
function countTeamSizeExclusions(teamSize, items) {
|
|
4212
|
+
if (teamSize !== "solo") return 0;
|
|
4213
|
+
let count = 0;
|
|
4214
|
+
for (const item of items) {
|
|
4215
|
+
if (item.protected) continue;
|
|
4216
|
+
if (!item.tags.includes("team") && !item.tags.includes("board")) continue;
|
|
4217
|
+
const hasOther = item.tags.some(
|
|
4218
|
+
(t) => t !== "team" && t !== "board" && t !== "solo" && t !== "greenfield" && t !== "brownfield"
|
|
4219
|
+
);
|
|
4220
|
+
if (!hasOther) count++;
|
|
4221
|
+
}
|
|
4222
|
+
return count;
|
|
4223
|
+
}
|
|
3647
4224
|
async function copySelectedContent(contentRoot, agentsDir, selection, index) {
|
|
3648
4225
|
const copied = [];
|
|
3649
4226
|
const selectedIds = /* @__PURE__ */ new Set();
|
|
@@ -3656,21 +4233,21 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
|
|
|
3656
4233
|
if (item.companionPath) {
|
|
3657
4234
|
assertSafePath(item.companionPath, "copySelectedContent companion");
|
|
3658
4235
|
}
|
|
3659
|
-
const srcPath =
|
|
3660
|
-
const destPath =
|
|
4236
|
+
const srcPath = join15(contentRoot, item.relativePath);
|
|
4237
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
3661
4238
|
if (item.type === "skill") {
|
|
3662
|
-
await
|
|
3663
|
-
await
|
|
4239
|
+
await mkdir4(destPath, { recursive: true });
|
|
4240
|
+
await cp2(srcPath, destPath, { recursive: true, force: true });
|
|
3664
4241
|
copied.push(item.relativePath);
|
|
3665
4242
|
} else {
|
|
3666
|
-
await
|
|
3667
|
-
await
|
|
4243
|
+
await mkdir4(dirname7(destPath), { recursive: true });
|
|
4244
|
+
await cp2(srcPath, destPath, { force: true });
|
|
3668
4245
|
copied.push(item.relativePath);
|
|
3669
4246
|
if (item.companionPath) {
|
|
3670
|
-
const mdcSrc =
|
|
3671
|
-
const mdcDest =
|
|
4247
|
+
const mdcSrc = join15(contentRoot, item.companionPath);
|
|
4248
|
+
const mdcDest = join15(agentsDir, item.companionPath);
|
|
3672
4249
|
try {
|
|
3673
|
-
await
|
|
4250
|
+
await cp2(mdcSrc, mdcDest, { force: true });
|
|
3674
4251
|
copied.push(item.companionPath);
|
|
3675
4252
|
} catch (err) {
|
|
3676
4253
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -3678,19 +4255,34 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
|
|
|
3678
4255
|
}
|
|
3679
4256
|
}
|
|
3680
4257
|
}
|
|
4258
|
+
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
4259
|
+
if (config.strategy !== "glob") continue;
|
|
4260
|
+
try {
|
|
4261
|
+
const dirEntries = await readdir6(join15(contentRoot, config.dir), { withFileTypes: true });
|
|
4262
|
+
for (const entry of dirEntries) {
|
|
4263
|
+
if (!entry.isDirectory() || entry.name.startsWith("hatch3r-")) continue;
|
|
4264
|
+
const subSrc = join15(contentRoot, config.dir, entry.name);
|
|
4265
|
+
const subDest = join15(agentsDir, config.dir, entry.name);
|
|
4266
|
+
await mkdir4(subDest, { recursive: true });
|
|
4267
|
+
await cp2(subSrc, subDest, { recursive: true, force: true });
|
|
4268
|
+
}
|
|
4269
|
+
} catch (err) {
|
|
4270
|
+
if (err.code !== "ENOENT") throw err;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
3681
4273
|
try {
|
|
3682
|
-
const checksSrc =
|
|
3683
|
-
const checksDest =
|
|
3684
|
-
await
|
|
3685
|
-
await
|
|
4274
|
+
const checksSrc = join15(contentRoot, "checks");
|
|
4275
|
+
const checksDest = join15(agentsDir, "checks");
|
|
4276
|
+
await mkdir4(checksDest, { recursive: true });
|
|
4277
|
+
await cp2(checksSrc, checksDest, { recursive: true, force: true });
|
|
3686
4278
|
} catch (err) {
|
|
3687
4279
|
if (err.code !== "ENOENT") throw err;
|
|
3688
4280
|
}
|
|
3689
4281
|
try {
|
|
3690
|
-
const mcpSrc =
|
|
3691
|
-
const mcpDest =
|
|
3692
|
-
await
|
|
3693
|
-
await
|
|
4282
|
+
const mcpSrc = join15(contentRoot, "mcp");
|
|
4283
|
+
const mcpDest = join15(agentsDir, "mcp");
|
|
4284
|
+
await mkdir4(mcpDest, { recursive: true });
|
|
4285
|
+
await cp2(mcpSrc, mcpDest, { recursive: true, force: true });
|
|
3694
4286
|
} catch (err) {
|
|
3695
4287
|
if (err.code !== "ENOENT") throw err;
|
|
3696
4288
|
}
|
|
@@ -3707,16 +4299,16 @@ async function buildSelectionsFromDisk(agentsDir) {
|
|
|
3707
4299
|
githubAgents: []
|
|
3708
4300
|
};
|
|
3709
4301
|
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
3710
|
-
const dirPath =
|
|
4302
|
+
const dirPath = join15(agentsDir, config.dir);
|
|
3711
4303
|
const key = TYPE_TO_SELECTION_KEY[config.type];
|
|
3712
4304
|
if (!key) continue;
|
|
3713
4305
|
if (config.strategy === "subdirectory") {
|
|
3714
4306
|
try {
|
|
3715
|
-
const dirents = await
|
|
4307
|
+
const dirents = await readdir6(dirPath, { withFileTypes: true });
|
|
3716
4308
|
for (const d of dirents) {
|
|
3717
4309
|
if (!d.isDirectory()) continue;
|
|
3718
4310
|
try {
|
|
3719
|
-
const raw = await
|
|
4311
|
+
const raw = await readFile13(join15(dirPath, d.name, "SKILL.md"), "utf-8");
|
|
3720
4312
|
const { metadata } = parseFrontmatter(raw);
|
|
3721
4313
|
items[key].push(metadata.id || metadata.name || d.name);
|
|
3722
4314
|
} catch {
|
|
@@ -3726,9 +4318,9 @@ async function buildSelectionsFromDisk(agentsDir) {
|
|
|
3726
4318
|
}
|
|
3727
4319
|
} else {
|
|
3728
4320
|
try {
|
|
3729
|
-
const files = await
|
|
4321
|
+
const files = await readdir6(dirPath);
|
|
3730
4322
|
for (const f of files.filter((f2) => f2.endsWith(".md"))) {
|
|
3731
|
-
const raw = await
|
|
4323
|
+
const raw = await readFile13(join15(dirPath, f), "utf-8");
|
|
3732
4324
|
const { metadata } = parseFrontmatter(raw);
|
|
3733
4325
|
items[key].push(metadata.id || metadata.name || f.replace(/\.md$/, ""));
|
|
3734
4326
|
}
|
|
@@ -3748,20 +4340,20 @@ async function addContentItem(contentRoot, agentsDir, item) {
|
|
|
3748
4340
|
if (item.companionPath) {
|
|
3749
4341
|
assertSafePath(item.companionPath, "addContentItem companion");
|
|
3750
4342
|
}
|
|
3751
|
-
const srcPath =
|
|
3752
|
-
const destPath =
|
|
4343
|
+
const srcPath = join15(contentRoot, item.relativePath);
|
|
4344
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
3753
4345
|
try {
|
|
3754
4346
|
if (item.type === "skill") {
|
|
3755
|
-
await
|
|
3756
|
-
await
|
|
4347
|
+
await mkdir4(destPath, { recursive: true });
|
|
4348
|
+
await cp2(srcPath, destPath, { recursive: true, force: true });
|
|
3757
4349
|
} else {
|
|
3758
|
-
await
|
|
3759
|
-
await
|
|
4350
|
+
await mkdir4(dirname7(destPath), { recursive: true });
|
|
4351
|
+
await cp2(srcPath, destPath, { force: true });
|
|
3760
4352
|
if (item.companionPath) {
|
|
3761
4353
|
try {
|
|
3762
|
-
await
|
|
3763
|
-
|
|
3764
|
-
|
|
4354
|
+
await cp2(
|
|
4355
|
+
join15(contentRoot, item.companionPath),
|
|
4356
|
+
join15(agentsDir, item.companionPath),
|
|
3765
4357
|
{ force: true }
|
|
3766
4358
|
);
|
|
3767
4359
|
} catch (err) {
|
|
@@ -3773,7 +4365,8 @@ async function addContentItem(contentRoot, agentsDir, item) {
|
|
|
3773
4365
|
if (err.code === "ENOENT") {
|
|
3774
4366
|
throw new HatchError(
|
|
3775
4367
|
`Content "${item.id}" (${item.type}) not found in package at ${item.relativePath}. It may have been renamed or removed in this hatch3r version.`,
|
|
3776
|
-
1
|
|
4368
|
+
1,
|
|
4369
|
+
"FS_ERROR"
|
|
3777
4370
|
);
|
|
3778
4371
|
}
|
|
3779
4372
|
throw err;
|
|
@@ -3784,13 +4377,13 @@ async function removeContentItem(agentsDir, item, options) {
|
|
|
3784
4377
|
if (item.companionPath) {
|
|
3785
4378
|
assertSafePath(item.companionPath, "removeContentItem companion");
|
|
3786
4379
|
}
|
|
3787
|
-
const destPath =
|
|
4380
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
3788
4381
|
if (item.type === "skill") {
|
|
3789
|
-
await
|
|
4382
|
+
await rm2(destPath, { recursive: true, force: true });
|
|
3790
4383
|
} else {
|
|
3791
|
-
await
|
|
4384
|
+
await rm2(destPath, { force: true });
|
|
3792
4385
|
if (item.companionPath) {
|
|
3793
|
-
await
|
|
4386
|
+
await rm2(join15(agentsDir, item.companionPath), { force: true });
|
|
3794
4387
|
}
|
|
3795
4388
|
}
|
|
3796
4389
|
if (options?.rootDir) {
|
|
@@ -3802,10 +4395,10 @@ async function removeContentItem(agentsDir, item, options) {
|
|
|
3802
4395
|
};
|
|
3803
4396
|
const customDir = typeToDir[item.type];
|
|
3804
4397
|
if (customDir) {
|
|
3805
|
-
const yamlPath =
|
|
3806
|
-
const mdPath =
|
|
3807
|
-
await
|
|
3808
|
-
await
|
|
4398
|
+
const yamlPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.yaml`);
|
|
4399
|
+
const mdPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.md`);
|
|
4400
|
+
await rm2(yamlPath, { force: true });
|
|
4401
|
+
await rm2(mdPath, { force: true });
|
|
3809
4402
|
}
|
|
3810
4403
|
}
|
|
3811
4404
|
}
|
|
@@ -3816,6 +4409,10 @@ function getAllContentIds(selection) {
|
|
|
3816
4409
|
}
|
|
3817
4410
|
return ids;
|
|
3818
4411
|
}
|
|
4412
|
+
function estimatePresetItemCount(preset, projectType, teamSize, index) {
|
|
4413
|
+
const selection = resolveSelection(preset, projectType, teamSize, index);
|
|
4414
|
+
return Object.values(selection.items).reduce((sum, arr) => sum + arr.length, 0);
|
|
4415
|
+
}
|
|
3819
4416
|
function countSelectionItems(selection) {
|
|
3820
4417
|
return Object.values(selection.items).reduce((sum, arr) => sum + arr.length, 0);
|
|
3821
4418
|
}
|
|
@@ -3833,40 +4430,40 @@ function selectionSummary(selection) {
|
|
|
3833
4430
|
}
|
|
3834
4431
|
|
|
3835
4432
|
// src/cli/commands/update.ts
|
|
3836
|
-
var __dirname =
|
|
4433
|
+
var __dirname = dirname8(fileURLToPath(import.meta.url));
|
|
3837
4434
|
var CONTENT_DIRS = ["agents", "commands", "rules", "skills", "prompts", "github-agents", "mcp", "hooks"];
|
|
3838
4435
|
var ALWAYS_COPY_FILES = /* @__PURE__ */ new Set(["mcp.json"]);
|
|
3839
4436
|
async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selectedIds) {
|
|
3840
4437
|
const copied = [];
|
|
3841
4438
|
let entries;
|
|
3842
4439
|
try {
|
|
3843
|
-
entries = await
|
|
4440
|
+
entries = await readdir7(srcDir, { withFileTypes: true });
|
|
3844
4441
|
} catch (err) {
|
|
3845
4442
|
if (err.code === "ENOENT") return [];
|
|
3846
4443
|
throw err;
|
|
3847
4444
|
}
|
|
3848
4445
|
for (const entry of entries) {
|
|
3849
|
-
const srcPath =
|
|
3850
|
-
const destPath =
|
|
4446
|
+
const srcPath = join16(srcDir, entry.name);
|
|
4447
|
+
const destPath = join16(destDir, entry.name);
|
|
3851
4448
|
if (entry.isDirectory()) {
|
|
3852
4449
|
if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
|
|
3853
4450
|
if (!selectedIds.has(entry.name)) continue;
|
|
3854
4451
|
}
|
|
3855
|
-
await
|
|
4452
|
+
await mkdir5(destPath, { recursive: true });
|
|
3856
4453
|
const subCopied = await copyHatch3rFiles(
|
|
3857
4454
|
srcPath,
|
|
3858
4455
|
destPath,
|
|
3859
|
-
entry.name.startsWith(HATCH3R_PREFIX),
|
|
4456
|
+
insideHatch3rDir || !entry.name.startsWith(HATCH3R_PREFIX),
|
|
3860
4457
|
selectedIds
|
|
3861
4458
|
);
|
|
3862
|
-
copied.push(...subCopied.map((p) =>
|
|
4459
|
+
copied.push(...subCopied.map((p) => join16(entry.name, p)));
|
|
3863
4460
|
} else if (entry.name.startsWith(HATCH3R_PREFIX) || insideHatch3rDir || ALWAYS_COPY_FILES.has(entry.name)) {
|
|
3864
4461
|
if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
|
|
3865
4462
|
const baseId = entry.name.replace(/\.(md|mdc)$/, "");
|
|
3866
4463
|
if (!selectedIds.has(baseId)) continue;
|
|
3867
4464
|
}
|
|
3868
|
-
await
|
|
3869
|
-
await
|
|
4465
|
+
await mkdir5(dirname8(destPath), { recursive: true });
|
|
4466
|
+
await cp3(srcPath, destPath, { force: true });
|
|
3870
4467
|
copied.push(entry.name);
|
|
3871
4468
|
}
|
|
3872
4469
|
}
|
|
@@ -3875,7 +4472,7 @@ async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selec
|
|
|
3875
4472
|
async function runUpdate(rootDir, manifest, options = {}) {
|
|
3876
4473
|
const offset = options.stepOffset ?? 0;
|
|
3877
4474
|
const total = options.totalSteps ?? 4;
|
|
3878
|
-
const agentsDir =
|
|
4475
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
3879
4476
|
let contentRoot = findPackageRoot(__dirname);
|
|
3880
4477
|
const pm = await detectPackageManager(rootDir);
|
|
3881
4478
|
const s0 = createSpinner(step(offset + 1, total, "Updating package..."));
|
|
@@ -3889,7 +4486,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
3889
4486
|
const msg = isTimeout ? "Package update timed out after 30s. Check network connectivity and retry." : err instanceof Error ? err.message : String(err);
|
|
3890
4487
|
s0.fail(step(offset + 1, total, "Failed to update package"));
|
|
3891
4488
|
error(msg);
|
|
3892
|
-
throw new HatchError(msg, 1);
|
|
4489
|
+
throw new HatchError(msg, 1, isTimeout ? "NETWORK_ERROR" : "UNKNOWN_ERROR");
|
|
3893
4490
|
}
|
|
3894
4491
|
s0.succeed(step(offset + 1, total, "Package updated"));
|
|
3895
4492
|
const s1 = createSpinner(step(offset + 2, total, "Updating canonical files..."));
|
|
@@ -3903,16 +4500,16 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
3903
4500
|
}
|
|
3904
4501
|
const copied = [];
|
|
3905
4502
|
for (const dir of CONTENT_DIRS) {
|
|
3906
|
-
const srcDir =
|
|
4503
|
+
const srcDir = join16(contentRoot, dir);
|
|
3907
4504
|
try {
|
|
3908
|
-
const dirCopied = await copyHatch3rFiles(srcDir,
|
|
3909
|
-
copied.push(...dirCopied.map((p) =>
|
|
4505
|
+
const dirCopied = await copyHatch3rFiles(srcDir, join16(agentsDir, dir), false, selectedIds);
|
|
4506
|
+
copied.push(...dirCopied.map((p) => join16(dir, p)));
|
|
3910
4507
|
} catch (err) {
|
|
3911
4508
|
if (err.code !== "ENOENT") throw err;
|
|
3912
4509
|
}
|
|
3913
4510
|
}
|
|
3914
4511
|
const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
|
|
3915
|
-
await safeWriteFile(
|
|
4512
|
+
await safeWriteFile(join16(agentsDir, "AGENTS.md"), canonicalAgentsMd);
|
|
3916
4513
|
s1.succeed(step(offset + 2, total, `Updated ${copied.length} canonical files`));
|
|
3917
4514
|
const s2 = createSpinner(step(offset + 3, total, "Re-syncing adapter output..."));
|
|
3918
4515
|
s2.start();
|
|
@@ -3925,7 +4522,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
3925
4522
|
warn(w);
|
|
3926
4523
|
}
|
|
3927
4524
|
for (const out of outputs) {
|
|
3928
|
-
const fullPath =
|
|
4525
|
+
const fullPath = join16(rootDir, out.path);
|
|
3929
4526
|
if (out.managedContent) {
|
|
3930
4527
|
await safeWriteFile(fullPath, out.content, {
|
|
3931
4528
|
managedContent: out.managedContent
|
|
@@ -3947,7 +4544,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
3947
4544
|
}
|
|
3948
4545
|
if (adapterFailures.length === manifest.tools.length) {
|
|
3949
4546
|
s2.fail(step(offset + 3, total, "All adapters failed"));
|
|
3950
|
-
throw new HatchError("All adapters failed", 1);
|
|
4547
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
3951
4548
|
}
|
|
3952
4549
|
}
|
|
3953
4550
|
s2.succeed(step(offset + 3, total, adapterFailures.length > 0 ? `Re-synced ${manifest.tools.length - adapterFailures.length}/${manifest.tools.length} tool(s)` : `Re-synced ${manifest.tools.length} tool(s)`));
|
|
@@ -3957,6 +4554,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
3957
4554
|
await writeManifest(rootDir, manifest);
|
|
3958
4555
|
const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
|
|
3959
4556
|
await writeIntegrityManifest(agentsDir, integrityManifest);
|
|
4557
|
+
await pruneArchives(rootDir);
|
|
3960
4558
|
s3.succeed(step(offset + 4, total, "Manifest updated"));
|
|
3961
4559
|
return {
|
|
3962
4560
|
copiedFiles: copied.length,
|
|
@@ -3969,35 +4567,40 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
3969
4567
|
{
|
|
3970
4568
|
id: "content-selections-init",
|
|
3971
4569
|
condition: async (manifest) => manifest.content === void 0,
|
|
3972
|
-
execute: async (manifest, rootDir) => {
|
|
3973
|
-
const agentsDir =
|
|
4570
|
+
execute: async (manifest, rootDir, headless) => {
|
|
4571
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
3974
4572
|
const content = await buildSelectionsFromDisk(agentsDir);
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4573
|
+
if (headless) {
|
|
4574
|
+
content.projectType = "brownfield";
|
|
4575
|
+
content.teamSize = "team";
|
|
4576
|
+
} else {
|
|
4577
|
+
const { projectType } = await inquirer.prompt([
|
|
4578
|
+
{
|
|
4579
|
+
type: "list",
|
|
4580
|
+
name: "projectType",
|
|
4581
|
+
message: "For content tracking \u2014 is this a greenfield or brownfield project?",
|
|
4582
|
+
choices: [
|
|
4583
|
+
{ name: "Greenfield \u2014 new project", value: "greenfield" },
|
|
4584
|
+
{ name: "Brownfield \u2014 existing codebase", value: "brownfield" }
|
|
4585
|
+
],
|
|
4586
|
+
default: "brownfield"
|
|
4587
|
+
}
|
|
4588
|
+
]);
|
|
4589
|
+
const { teamSize } = await inquirer.prompt([
|
|
4590
|
+
{
|
|
4591
|
+
type: "list",
|
|
4592
|
+
name: "teamSize",
|
|
4593
|
+
message: "Solo developer or team?",
|
|
4594
|
+
choices: [
|
|
4595
|
+
{ name: "Solo", value: "solo" },
|
|
4596
|
+
{ name: "Team", value: "team" }
|
|
4597
|
+
],
|
|
4598
|
+
default: "team"
|
|
4599
|
+
}
|
|
4600
|
+
]);
|
|
4601
|
+
content.projectType = projectType;
|
|
4602
|
+
content.teamSize = teamSize;
|
|
4603
|
+
}
|
|
4001
4604
|
return {
|
|
4002
4605
|
manifest: { ...manifest, content },
|
|
4003
4606
|
notices: ["Migrated to explicit content tracking (all existing items preserved)"]
|
|
@@ -4007,20 +4610,26 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4007
4610
|
{
|
|
4008
4611
|
id: "platform-selection",
|
|
4009
4612
|
condition: async (manifest) => !manifest.platform,
|
|
4010
|
-
execute: async (manifest) => {
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4613
|
+
execute: async (manifest, _rootDir, headless) => {
|
|
4614
|
+
let platform;
|
|
4615
|
+
if (headless) {
|
|
4616
|
+
platform = "github";
|
|
4617
|
+
} else {
|
|
4618
|
+
const answer = await inquirer.prompt([
|
|
4619
|
+
{
|
|
4620
|
+
type: "list",
|
|
4621
|
+
name: "platform",
|
|
4622
|
+
message: "hatch3r now supports multiple platforms. Select your platform:",
|
|
4623
|
+
choices: [
|
|
4624
|
+
{ name: "GitHub", value: "github" },
|
|
4625
|
+
{ name: "Azure DevOps", value: "azure-devops" },
|
|
4626
|
+
{ name: "GitLab", value: "gitlab" }
|
|
4627
|
+
],
|
|
4628
|
+
default: "github"
|
|
4629
|
+
}
|
|
4630
|
+
]);
|
|
4631
|
+
platform = answer.platform;
|
|
4632
|
+
}
|
|
4024
4633
|
const updated = { ...manifest, platform };
|
|
4025
4634
|
const notices = [];
|
|
4026
4635
|
if (platform === "github") {
|
|
@@ -4048,12 +4657,12 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4048
4657
|
{
|
|
4049
4658
|
id: "customize-yaml-size",
|
|
4050
4659
|
condition: async (_manifest, rootDir) => {
|
|
4051
|
-
const agentsDir =
|
|
4660
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4052
4661
|
try {
|
|
4053
|
-
const entries = await
|
|
4662
|
+
const entries = await readdir7(agentsDir, { recursive: true });
|
|
4054
4663
|
for (const entry of entries) {
|
|
4055
4664
|
if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
|
|
4056
|
-
const s = await
|
|
4665
|
+
const s = await stat2(join16(agentsDir, entry));
|
|
4057
4666
|
if (s.size > 10240) return true;
|
|
4058
4667
|
}
|
|
4059
4668
|
}
|
|
@@ -4062,14 +4671,14 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4062
4671
|
}
|
|
4063
4672
|
return false;
|
|
4064
4673
|
},
|
|
4065
|
-
execute: async (manifest, rootDir) => {
|
|
4674
|
+
execute: async (manifest, rootDir, _headless) => {
|
|
4066
4675
|
const notices = [];
|
|
4067
|
-
const agentsDir =
|
|
4676
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4068
4677
|
try {
|
|
4069
|
-
const entries = await
|
|
4678
|
+
const entries = await readdir7(agentsDir, { recursive: true });
|
|
4070
4679
|
for (const entry of entries) {
|
|
4071
4680
|
if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
|
|
4072
|
-
const s = await
|
|
4681
|
+
const s = await stat2(join16(agentsDir, entry));
|
|
4073
4682
|
if (s.size > 10240) {
|
|
4074
4683
|
notices.push(`Large customize file detected: ${entry} (${Math.round(s.size / 1024)}KB) \u2014 consider splitting`);
|
|
4075
4684
|
}
|
|
@@ -4088,18 +4697,24 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4088
4697
|
const worktreeCapableTools = /* @__PURE__ */ new Set(["claude"]);
|
|
4089
4698
|
return manifest.tools.some((t) => worktreeCapableTools.has(t));
|
|
4090
4699
|
},
|
|
4091
|
-
execute: async (manifest, rootDir) => {
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4700
|
+
execute: async (manifest, rootDir, headless) => {
|
|
4701
|
+
let enabled;
|
|
4702
|
+
if (headless) {
|
|
4703
|
+
enabled = true;
|
|
4704
|
+
} else {
|
|
4705
|
+
const answer = await inquirer.prompt([{
|
|
4706
|
+
type: "confirm",
|
|
4707
|
+
name: "enabled",
|
|
4708
|
+
message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
|
|
4709
|
+
default: true
|
|
4710
|
+
}]);
|
|
4711
|
+
enabled = answer.enabled;
|
|
4712
|
+
}
|
|
4098
4713
|
const updated = { ...manifest, worktree: { enabled } };
|
|
4099
4714
|
const notices = [];
|
|
4100
4715
|
if (enabled) {
|
|
4101
4716
|
const wtContent = await generateWorktreeInclude(updated, rootDir);
|
|
4102
|
-
await safeWriteFile(
|
|
4717
|
+
await safeWriteFile(join16(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
|
|
4103
4718
|
appendIfNoBlock: true
|
|
4104
4719
|
});
|
|
4105
4720
|
notices.push("Worktree isolation enabled \u2014 .worktreeinclude generated");
|
|
@@ -4110,12 +4725,12 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4110
4725
|
}
|
|
4111
4726
|
}
|
|
4112
4727
|
];
|
|
4113
|
-
async function runMigrationCheckpoints(manifest, rootDir) {
|
|
4728
|
+
async function runMigrationCheckpoints(manifest, rootDir, headless = false) {
|
|
4114
4729
|
let current = manifest;
|
|
4115
4730
|
const allNotices = [];
|
|
4116
4731
|
for (const checkpoint of MIGRATION_CHECKPOINTS) {
|
|
4117
4732
|
if (await checkpoint.condition(current, rootDir)) {
|
|
4118
|
-
const { manifest: updated, notices } = await checkpoint.execute(current, rootDir);
|
|
4733
|
+
const { manifest: updated, notices } = await checkpoint.execute(current, rootDir, headless);
|
|
4119
4734
|
current = updated;
|
|
4120
4735
|
allNotices.push(...notices);
|
|
4121
4736
|
}
|
|
@@ -4129,9 +4744,10 @@ async function updateCommand(_opts) {
|
|
|
4129
4744
|
if (!manifest) {
|
|
4130
4745
|
error("No .agents/hatch.json found.");
|
|
4131
4746
|
console.log(chalk4.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
4132
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
4747
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
4133
4748
|
}
|
|
4134
|
-
const
|
|
4749
|
+
const headless = !!_opts?.yes;
|
|
4750
|
+
const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir, headless);
|
|
4135
4751
|
const m = migrated;
|
|
4136
4752
|
for (const notice of allNotices) {
|
|
4137
4753
|
warn(notice);
|
|
@@ -4152,165 +4768,659 @@ async function updateCommand(_opts) {
|
|
|
4152
4768
|
], "success");
|
|
4153
4769
|
}
|
|
4154
4770
|
|
|
4155
|
-
// src/
|
|
4156
|
-
import {
|
|
4157
|
-
import {
|
|
4158
|
-
|
|
4159
|
-
|
|
4771
|
+
// src/workspace/manifest.ts
|
|
4772
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4773
|
+
import { join as join17, normalize as normalize2, isAbsolute as isAbsolute2 } from "path";
|
|
4774
|
+
|
|
4775
|
+
// src/workspace/types.ts
|
|
4776
|
+
var WORKSPACE_MANIFEST_FILE = "workspace.json";
|
|
4777
|
+
var WORKSPACE_MANIFEST_VERSION = "1.0.0";
|
|
4778
|
+
|
|
4779
|
+
// src/workspace/manifest.ts
|
|
4780
|
+
function isUnsafeRepoPath(repoPath) {
|
|
4781
|
+
if (repoPath.includes("\0")) return true;
|
|
4782
|
+
if (isAbsolute2(repoPath)) return true;
|
|
4783
|
+
const normalized = normalize2(repoPath);
|
|
4784
|
+
if (normalized.startsWith("..")) return true;
|
|
4785
|
+
return false;
|
|
4160
4786
|
}
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
for (const
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4787
|
+
function validateWorkspaceManifest(data) {
|
|
4788
|
+
if (!data || typeof data !== "object") return false;
|
|
4789
|
+
const obj = data;
|
|
4790
|
+
if (typeof obj.version !== "string") return false;
|
|
4791
|
+
if (typeof obj.hatch3rVersion !== "string") return false;
|
|
4792
|
+
if (typeof obj.name !== "string") return false;
|
|
4793
|
+
if (!Array.isArray(obj.repos)) return false;
|
|
4794
|
+
if (typeof obj.syncStrategy !== "string") return false;
|
|
4795
|
+
if (!["manual", "on-sync"].includes(obj.syncStrategy)) return false;
|
|
4796
|
+
if (!obj.defaults || typeof obj.defaults !== "object") return false;
|
|
4797
|
+
const defaults = obj.defaults;
|
|
4798
|
+
if (!Array.isArray(defaults.tools)) return false;
|
|
4799
|
+
if (!defaults.features || typeof defaults.features !== "object") return false;
|
|
4800
|
+
if (!defaults.mcp || typeof defaults.mcp !== "object") return false;
|
|
4801
|
+
const mcp = defaults.mcp;
|
|
4802
|
+
if (!Array.isArray(mcp.servers)) return false;
|
|
4803
|
+
if (defaults.content !== void 0) {
|
|
4804
|
+
if (typeof defaults.content !== "object" || defaults.content === null) return false;
|
|
4805
|
+
const content = defaults.content;
|
|
4806
|
+
if (typeof content.preset !== "string") return false;
|
|
4807
|
+
if (typeof content.projectType !== "string") return false;
|
|
4808
|
+
if (typeof content.teamSize !== "string") return false;
|
|
4809
|
+
if (!content.items || typeof content.items !== "object") return false;
|
|
4810
|
+
}
|
|
4811
|
+
for (const repo of obj.repos) {
|
|
4812
|
+
if (!repo || typeof repo !== "object") return false;
|
|
4813
|
+
const r = repo;
|
|
4814
|
+
if (typeof r.path !== "string") return false;
|
|
4815
|
+
if (isUnsafeRepoPath(r.path)) return false;
|
|
4816
|
+
if (typeof r.sync !== "boolean") return false;
|
|
4817
|
+
if (r.owner !== void 0 && typeof r.owner !== "string") return false;
|
|
4818
|
+
if (r.repo !== void 0 && typeof r.repo !== "string") return false;
|
|
4819
|
+
if (r.defaultBranch !== void 0 && typeof r.defaultBranch !== "string") return false;
|
|
4820
|
+
if (r.platform !== void 0 && typeof r.platform !== "string") return false;
|
|
4821
|
+
}
|
|
4822
|
+
return true;
|
|
4823
|
+
}
|
|
4824
|
+
async function readWorkspaceManifest(rootDir) {
|
|
4825
|
+
const manifestPath = join17(rootDir, AGENTS_DIR, WORKSPACE_MANIFEST_FILE);
|
|
4826
|
+
let raw;
|
|
4827
|
+
try {
|
|
4828
|
+
raw = await readFile14(manifestPath, "utf-8");
|
|
4829
|
+
} catch (err) {
|
|
4830
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
4831
|
+
return null;
|
|
4194
4832
|
}
|
|
4833
|
+
throw err;
|
|
4195
4834
|
}
|
|
4196
|
-
|
|
4835
|
+
let parsed;
|
|
4836
|
+
try {
|
|
4837
|
+
parsed = JSON.parse(raw);
|
|
4838
|
+
} catch (err) {
|
|
4839
|
+
throw new HatchError(
|
|
4840
|
+
`Malformed JSON in ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4841
|
+
1,
|
|
4842
|
+
"CONFIG_ERROR"
|
|
4843
|
+
);
|
|
4844
|
+
}
|
|
4845
|
+
if (!validateWorkspaceManifest(parsed)) {
|
|
4846
|
+
throw new HatchError(
|
|
4847
|
+
`Invalid workspace manifest in ${manifestPath}: required fields missing or malformed.`,
|
|
4848
|
+
1,
|
|
4849
|
+
"VALIDATION_ERROR"
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
return parsed;
|
|
4197
4853
|
}
|
|
4198
|
-
function
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4854
|
+
async function writeWorkspaceManifest(rootDir, manifest) {
|
|
4855
|
+
const manifestPath = join17(rootDir, AGENTS_DIR, WORKSPACE_MANIFEST_FILE);
|
|
4856
|
+
await atomicWriteFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
4857
|
+
}
|
|
4858
|
+
function createWorkspaceManifest(name, defaults, repos, syncStrategy = "manual") {
|
|
4859
|
+
return {
|
|
4860
|
+
version: WORKSPACE_MANIFEST_VERSION,
|
|
4861
|
+
hatch3rVersion: HATCH3R_VERSION,
|
|
4862
|
+
name,
|
|
4863
|
+
repos,
|
|
4864
|
+
defaults,
|
|
4865
|
+
syncStrategy
|
|
4866
|
+
};
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
// src/workspace/detect.ts
|
|
4870
|
+
import { readdir as readdir8, stat as stat3, access as access4 } from "fs/promises";
|
|
4871
|
+
import { join as join18, dirname as dirname9, relative as relative2 } from "path";
|
|
4872
|
+
async function detectSubRepos(rootDir) {
|
|
4873
|
+
const repos = [];
|
|
4874
|
+
let entries;
|
|
4875
|
+
try {
|
|
4876
|
+
entries = await readdir8(rootDir, { withFileTypes: true });
|
|
4877
|
+
} catch {
|
|
4878
|
+
return repos;
|
|
4879
|
+
}
|
|
4880
|
+
for (const entry of entries) {
|
|
4881
|
+
if (!entry.isDirectory()) continue;
|
|
4882
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
4883
|
+
const subDir = join18(rootDir, entry.name);
|
|
4884
|
+
const gitPath = join18(subDir, ".git");
|
|
4885
|
+
let isGitRepo = false;
|
|
4886
|
+
try {
|
|
4887
|
+
const gitStat = await stat3(gitPath);
|
|
4888
|
+
isGitRepo = gitStat.isDirectory() || gitStat.isFile();
|
|
4889
|
+
} catch {
|
|
4204
4890
|
}
|
|
4891
|
+
if (!isGitRepo) continue;
|
|
4892
|
+
let hasHatch3r = false;
|
|
4893
|
+
try {
|
|
4894
|
+
await access4(join18(subDir, AGENTS_DIR, "hatch.json"));
|
|
4895
|
+
hasHatch3r = true;
|
|
4896
|
+
} catch {
|
|
4897
|
+
}
|
|
4898
|
+
repos.push({
|
|
4899
|
+
path: entry.name,
|
|
4900
|
+
name: entry.name,
|
|
4901
|
+
hasHatch3r
|
|
4902
|
+
});
|
|
4205
4903
|
}
|
|
4206
|
-
return
|
|
4904
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
4207
4905
|
}
|
|
4208
|
-
async function
|
|
4906
|
+
async function hasGitDir(dir) {
|
|
4209
4907
|
try {
|
|
4210
|
-
await
|
|
4211
|
-
return
|
|
4908
|
+
const gitStat = await stat3(join18(dir, ".git"));
|
|
4909
|
+
return gitStat.isDirectory() || gitStat.isFile();
|
|
4212
4910
|
} catch {
|
|
4213
4911
|
return false;
|
|
4214
4912
|
}
|
|
4215
4913
|
}
|
|
4216
|
-
async function
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4914
|
+
async function shouldSuggestWorkspace(dir) {
|
|
4915
|
+
if (await hasGitDir(dir)) return false;
|
|
4916
|
+
const repos = await detectSubRepos(dir);
|
|
4917
|
+
return repos.length > 0;
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
// src/workspace/sync.ts
|
|
4921
|
+
import { createHash as createHash2 } from "crypto";
|
|
4922
|
+
import { mkdir as mkdir6, access as access6, readFile as readFile16 } from "fs/promises";
|
|
4923
|
+
import { join as join20, relative as relative3 } from "path";
|
|
4924
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4925
|
+
import { dirname as dirname10 } from "path";
|
|
4926
|
+
|
|
4927
|
+
// src/detect/repoAnalyzer.ts
|
|
4928
|
+
import { access as access5, readFile as readFile15, readdir as readdir9 } from "fs/promises";
|
|
4929
|
+
import { join as join19 } from "path";
|
|
4930
|
+
async function analyzeRepo(rootDir) {
|
|
4931
|
+
const [languages, pm, isMonorepo, hasExistingAgents, existingTools, frameworks] = await Promise.all([
|
|
4932
|
+
detectLanguages(rootDir),
|
|
4933
|
+
detectPackageManager(rootDir),
|
|
4934
|
+
detectMonorepo(rootDir),
|
|
4935
|
+
detectExistingAgents(rootDir),
|
|
4936
|
+
detectExistingTools(rootDir),
|
|
4937
|
+
detectFrameworks(rootDir)
|
|
4938
|
+
]);
|
|
4939
|
+
const packageManager = pm.name;
|
|
4940
|
+
return {
|
|
4941
|
+
languages,
|
|
4942
|
+
packageManager,
|
|
4943
|
+
frameworks,
|
|
4944
|
+
isMonorepo,
|
|
4945
|
+
hasExistingAgents,
|
|
4946
|
+
existingTools,
|
|
4947
|
+
rootDir
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
async function detectLanguages(rootDir) {
|
|
4951
|
+
const languages = [];
|
|
4952
|
+
const indicators = {
|
|
4953
|
+
typescript: ["tsconfig.json", "tsconfig.base.json"],
|
|
4954
|
+
javascript: ["jsconfig.json"],
|
|
4955
|
+
python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
|
|
4956
|
+
rust: ["Cargo.toml", "Cargo.lock"],
|
|
4957
|
+
go: ["go.mod", "go.sum"],
|
|
4958
|
+
java: ["pom.xml", "build.gradle"],
|
|
4959
|
+
kotlin: ["build.gradle.kts"],
|
|
4960
|
+
ruby: ["Gemfile"],
|
|
4961
|
+
php: ["composer.json"],
|
|
4962
|
+
swift: ["Package.swift"],
|
|
4963
|
+
dart: ["pubspec.yaml"],
|
|
4964
|
+
elixir: ["mix.exs"]
|
|
4965
|
+
};
|
|
4966
|
+
for (const [lang, files] of Object.entries(indicators)) {
|
|
4967
|
+
for (const file of files) {
|
|
4968
|
+
if (await pathExists(join19(rootDir, file))) {
|
|
4969
|
+
languages.push(lang);
|
|
4970
|
+
break;
|
|
4233
4971
|
}
|
|
4234
|
-
} else if (await fileExists2(absPath)) {
|
|
4235
|
-
files.push(prefix);
|
|
4236
4972
|
}
|
|
4237
4973
|
}
|
|
4238
|
-
|
|
4974
|
+
try {
|
|
4975
|
+
const rootEntries = await readdir9(rootDir);
|
|
4976
|
+
if (rootEntries.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
|
|
4977
|
+
languages.push("csharp");
|
|
4978
|
+
}
|
|
4979
|
+
} catch (err) {
|
|
4980
|
+
if (err.code !== "ENOENT") throw err;
|
|
4981
|
+
}
|
|
4982
|
+
if (languages.length === 0) {
|
|
4983
|
+
languages.push("unknown");
|
|
4984
|
+
}
|
|
4985
|
+
return languages;
|
|
4239
4986
|
}
|
|
4240
|
-
async function
|
|
4241
|
-
|
|
4242
|
-
if (
|
|
4243
|
-
|
|
4987
|
+
async function detectMonorepo(rootDir) {
|
|
4988
|
+
if (await pathExists(join19(rootDir, "pnpm-workspace.yaml"))) return true;
|
|
4989
|
+
if (await pathExists(join19(rootDir, "lerna.json"))) return true;
|
|
4990
|
+
if (await pathExists(join19(rootDir, "nx.json"))) return true;
|
|
4991
|
+
if (await pathExists(join19(rootDir, "turbo.json"))) return true;
|
|
4992
|
+
if (await pathExists(join19(rootDir, "pants.toml"))) return true;
|
|
4993
|
+
try {
|
|
4994
|
+
const pkgJson = await readFile15(join19(rootDir, "package.json"), "utf-8");
|
|
4995
|
+
const pkg = JSON.parse(pkgJson);
|
|
4996
|
+
if (pkg.workspaces) return true;
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
|
|
4999
|
+
if (!isExpected) throw err;
|
|
4244
5000
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
5001
|
+
return false;
|
|
5002
|
+
}
|
|
5003
|
+
async function detectExistingAgents(rootDir) {
|
|
5004
|
+
return pathExists(join19(rootDir, ".agents"));
|
|
5005
|
+
}
|
|
5006
|
+
var TOOL_INDICATORS = [
|
|
5007
|
+
{ tool: "cursor", paths: [".cursor"] },
|
|
5008
|
+
{ tool: "copilot", paths: [join19(".github", "copilot-instructions.md")] },
|
|
5009
|
+
{ tool: "claude", paths: ["CLAUDE.md", ".claude"] },
|
|
5010
|
+
{ tool: "opencode", paths: ["opencode.json", "opencode.jsonc"] },
|
|
5011
|
+
{ tool: "windsurf", paths: [".windsurfrules"] },
|
|
5012
|
+
{ tool: "amp", paths: [".amp"] },
|
|
5013
|
+
{ tool: "codex", paths: [".codex"] },
|
|
5014
|
+
{ tool: "gemini", paths: [".gemini", "GEMINI.md"] },
|
|
5015
|
+
{ tool: "cline", paths: [".clinerules", ".roo", ".roomodes"] },
|
|
5016
|
+
{ tool: "aider", paths: [".aider", ".aider.conf.yml"] },
|
|
5017
|
+
{ tool: "kiro", paths: [".kiro"] },
|
|
5018
|
+
{ tool: "goose", paths: [".goosehints", ".goose"] },
|
|
5019
|
+
{ tool: "zed", paths: [".rules"] },
|
|
5020
|
+
{ tool: "amazon-q", paths: [".amazonq"] }
|
|
5021
|
+
];
|
|
5022
|
+
async function detectExistingTools(rootDir) {
|
|
5023
|
+
const results = await Promise.allSettled(
|
|
5024
|
+
TOOL_INDICATORS.map(async ({ tool, paths }) => {
|
|
5025
|
+
for (const p of paths) {
|
|
5026
|
+
if (await pathExists(join19(rootDir, p))) return tool;
|
|
5027
|
+
}
|
|
5028
|
+
return null;
|
|
5029
|
+
})
|
|
5030
|
+
);
|
|
5031
|
+
return results.filter(
|
|
5032
|
+
(r) => r.status === "fulfilled" && r.value !== null
|
|
5033
|
+
).map((r) => r.value);
|
|
5034
|
+
}
|
|
5035
|
+
var FRAMEWORK_CONFIG_INDICATORS = [
|
|
5036
|
+
{ framework: "next", configs: ["next.config.js", "next.config.mjs", "next.config.ts"] },
|
|
5037
|
+
{ framework: "angular", configs: ["angular.json"] },
|
|
5038
|
+
{ framework: "svelte", configs: ["svelte.config.js", "svelte.config.ts"] },
|
|
5039
|
+
{ framework: "nuxt", configs: ["nuxt.config.js", "nuxt.config.ts"] },
|
|
5040
|
+
{ framework: "astro", configs: ["astro.config.mjs", "astro.config.ts"] }
|
|
5041
|
+
];
|
|
5042
|
+
var FRAMEWORK_DEP_INDICATORS = [
|
|
5043
|
+
{ framework: "next", deps: ["next"] },
|
|
5044
|
+
{ framework: "angular", deps: ["@angular/core"] },
|
|
5045
|
+
{ framework: "sveltekit", deps: ["@sveltejs/kit"] },
|
|
5046
|
+
{ framework: "svelte", deps: ["svelte"] },
|
|
5047
|
+
{ framework: "nuxt", deps: ["nuxt"] },
|
|
5048
|
+
{ framework: "remix", deps: ["@remix-run/react"] },
|
|
5049
|
+
{ framework: "astro", deps: ["astro"] },
|
|
5050
|
+
{ framework: "vue", deps: ["vue"] },
|
|
5051
|
+
{ framework: "react", deps: ["react"] },
|
|
5052
|
+
{ framework: "express", deps: ["express"] },
|
|
5053
|
+
{ framework: "fastify", deps: ["fastify"] },
|
|
5054
|
+
{ framework: "hono", deps: ["hono"] }
|
|
5055
|
+
];
|
|
5056
|
+
var FRAMEWORK_SUPPRESSION = {
|
|
5057
|
+
next: "react",
|
|
5058
|
+
remix: "react",
|
|
5059
|
+
nuxt: "vue",
|
|
5060
|
+
sveltekit: "svelte"
|
|
5061
|
+
};
|
|
5062
|
+
async function detectFrameworks(rootDir) {
|
|
5063
|
+
const detected = /* @__PURE__ */ new Set();
|
|
5064
|
+
const configResults = await Promise.allSettled(
|
|
5065
|
+
FRAMEWORK_CONFIG_INDICATORS.map(async ({ framework, configs }) => {
|
|
5066
|
+
for (const cfg of configs) {
|
|
5067
|
+
if (await pathExists(join19(rootDir, cfg))) return framework;
|
|
5068
|
+
}
|
|
5069
|
+
return null;
|
|
5070
|
+
})
|
|
5071
|
+
);
|
|
5072
|
+
for (const r of configResults) {
|
|
5073
|
+
if (r.status === "fulfilled" && r.value !== null) {
|
|
5074
|
+
detected.add(r.value);
|
|
4257
5075
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
|
|
4270
|
-
type: parsed.type,
|
|
4271
|
-
id: parsed.id
|
|
4272
|
-
});
|
|
4273
|
-
}
|
|
4274
|
-
}
|
|
5076
|
+
}
|
|
5077
|
+
try {
|
|
5078
|
+
const raw = await readFile15(join19(rootDir, "package.json"), "utf-8");
|
|
5079
|
+
const pkg = JSON.parse(raw);
|
|
5080
|
+
const allDeps = {
|
|
5081
|
+
...pkg.dependencies,
|
|
5082
|
+
...pkg.devDependencies
|
|
5083
|
+
};
|
|
5084
|
+
for (const { framework, deps } of FRAMEWORK_DEP_INDICATORS) {
|
|
5085
|
+
if (deps.some((d) => d in allDeps)) {
|
|
5086
|
+
detected.add(framework);
|
|
4275
5087
|
}
|
|
4276
5088
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
if (
|
|
4283
|
-
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
|
|
5091
|
+
if (!isExpected) throw err;
|
|
5092
|
+
}
|
|
5093
|
+
for (const [meta, base] of Object.entries(FRAMEWORK_SUPPRESSION)) {
|
|
5094
|
+
if (detected.has(meta)) {
|
|
5095
|
+
detected.delete(base);
|
|
4284
5096
|
}
|
|
4285
|
-
await rm2(absPath);
|
|
4286
|
-
archivedFiles.push(relPath);
|
|
4287
5097
|
}
|
|
4288
|
-
|
|
4289
|
-
return { archivedFiles, migrations };
|
|
5098
|
+
return [...detected];
|
|
4290
5099
|
}
|
|
4291
|
-
async function
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
5100
|
+
async function pathExists(path) {
|
|
5101
|
+
try {
|
|
5102
|
+
await access5(path);
|
|
5103
|
+
return true;
|
|
5104
|
+
} catch (err) {
|
|
5105
|
+
if (err.code !== "ENOENT") throw err;
|
|
5106
|
+
return false;
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
// src/workspace/resolve.ts
|
|
5111
|
+
function resolveRepoConfig(defaults, overrides, protectedIds) {
|
|
5112
|
+
const tools = overrides?.tools ?? defaults.tools;
|
|
5113
|
+
const features = { ...defaults.features, ...overrides?.features ?? {} };
|
|
5114
|
+
const mcp = overrides?.mcp ?? defaults.mcp;
|
|
5115
|
+
const platform = overrides?.platform ?? defaults.platform;
|
|
5116
|
+
let models;
|
|
5117
|
+
if (defaults.models || overrides?.models) {
|
|
5118
|
+
models = {
|
|
5119
|
+
...defaults.models,
|
|
5120
|
+
...overrides?.models,
|
|
5121
|
+
agents: {
|
|
5122
|
+
...defaults.models?.agents,
|
|
5123
|
+
...overrides?.models?.agents
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
if (!models.default && !models.agents) models = void 0;
|
|
5127
|
+
}
|
|
5128
|
+
const contentIds = getAllContentIds(defaults.content);
|
|
5129
|
+
const excludedContent = [];
|
|
5130
|
+
const addedContent = [];
|
|
5131
|
+
if (overrides?.contentOverrides?.exclude) {
|
|
5132
|
+
for (const id of overrides.contentOverrides.exclude) {
|
|
5133
|
+
if (protectedIds?.has(id)) continue;
|
|
5134
|
+
if (contentIds.has(id)) {
|
|
5135
|
+
contentIds.delete(id);
|
|
5136
|
+
excludedContent.push(id);
|
|
5137
|
+
}
|
|
4298
5138
|
}
|
|
4299
5139
|
}
|
|
4300
|
-
|
|
4301
|
-
|
|
5140
|
+
if (overrides?.contentOverrides?.include) {
|
|
5141
|
+
for (const id of overrides.contentOverrides.include) {
|
|
5142
|
+
if (!contentIds.has(id)) {
|
|
5143
|
+
contentIds.add(id);
|
|
5144
|
+
addedContent.push(id);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return { platform, tools, features, mcp, models, contentIds, excludedContent, addedContent };
|
|
5149
|
+
}
|
|
5150
|
+
function buildSelectionFromIds(ids, baseSelection, allItems) {
|
|
5151
|
+
const items = {
|
|
5152
|
+
agents: [],
|
|
5153
|
+
skills: [],
|
|
5154
|
+
rules: [],
|
|
5155
|
+
commands: [],
|
|
5156
|
+
prompts: [],
|
|
5157
|
+
hooks: [],
|
|
5158
|
+
githubAgents: []
|
|
5159
|
+
};
|
|
5160
|
+
for (const item of allItems) {
|
|
5161
|
+
if (!ids.has(item.id)) continue;
|
|
5162
|
+
const key = TYPE_TO_SELECTION_KEY[item.type];
|
|
5163
|
+
if (key) items[key].push(item.id);
|
|
5164
|
+
}
|
|
5165
|
+
return {
|
|
5166
|
+
preset: "custom",
|
|
5167
|
+
projectType: baseSelection.projectType,
|
|
5168
|
+
teamSize: baseSelection.teamSize,
|
|
5169
|
+
items
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// src/workspace/git.ts
|
|
5174
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5175
|
+
function parseGitRemote(cwd) {
|
|
5176
|
+
try {
|
|
5177
|
+
const url = execFileSync4("git", ["remote", "get-url", "origin"], {
|
|
5178
|
+
cwd,
|
|
5179
|
+
stdio: "pipe"
|
|
5180
|
+
}).toString().trim();
|
|
5181
|
+
const sshMatch = url.match(/[:\/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
5182
|
+
if (sshMatch) {
|
|
5183
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
5184
|
+
}
|
|
5185
|
+
return { owner: "", repo: "" };
|
|
5186
|
+
} catch {
|
|
5187
|
+
return { owner: "", repo: "" };
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
function parseGitDefaultBranch(cwd) {
|
|
5191
|
+
try {
|
|
5192
|
+
const ref = execFileSync4("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], {
|
|
5193
|
+
cwd,
|
|
5194
|
+
stdio: "pipe"
|
|
5195
|
+
}).toString().trim();
|
|
5196
|
+
if (ref && ref.startsWith("origin/")) {
|
|
5197
|
+
return ref.replace(/^origin\//, "");
|
|
5198
|
+
}
|
|
5199
|
+
return "main";
|
|
5200
|
+
} catch {
|
|
5201
|
+
return "main";
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
function detectPlatformFromRemote(remoteUrl) {
|
|
5205
|
+
if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com")) return "azure-devops";
|
|
5206
|
+
if (remoteUrl.includes("gitlab.com") || remoteUrl.includes("gitlab.")) return "gitlab";
|
|
5207
|
+
return "github";
|
|
5208
|
+
}
|
|
5209
|
+
function getGitRemoteUrl(cwd) {
|
|
5210
|
+
try {
|
|
5211
|
+
return execFileSync4("git", ["remote", "get-url", "origin"], { cwd, stdio: "pipe" }).toString().trim();
|
|
5212
|
+
} catch {
|
|
5213
|
+
return "";
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
function detectRepoGitIdentity(repoDir) {
|
|
5217
|
+
const remoteUrl = getGitRemoteUrl(repoDir);
|
|
5218
|
+
const { owner, repo } = parseGitRemote(repoDir);
|
|
5219
|
+
const defaultBranch = parseGitDefaultBranch(repoDir);
|
|
5220
|
+
const platform = remoteUrl ? detectPlatformFromRemote(remoteUrl) : "github";
|
|
5221
|
+
return { owner, repo, defaultBranch, platform };
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
// src/workspace/sync.ts
|
|
5225
|
+
var __dirname2 = dirname10(fileURLToPath2(import.meta.url));
|
|
5226
|
+
var CONTENT_ROOT = findPackageRoot(__dirname2);
|
|
5227
|
+
var CHARS_PER_TOKEN = 4;
|
|
5228
|
+
async function estimateTokensForContent(contentIds, index) {
|
|
5229
|
+
let totalChars = 0;
|
|
5230
|
+
for (const id of contentIds) {
|
|
5231
|
+
const items = getAllItemsById(index, id);
|
|
5232
|
+
for (const item of items) {
|
|
5233
|
+
try {
|
|
5234
|
+
if (item.type === "skill") {
|
|
5235
|
+
const skillPath = join20(CONTENT_ROOT, item.relativePath, "SKILL.md");
|
|
5236
|
+
const content = await readFile16(skillPath, "utf-8");
|
|
5237
|
+
totalChars += content.length;
|
|
5238
|
+
} else {
|
|
5239
|
+
const filePath = join20(CONTENT_ROOT, item.relativePath);
|
|
5240
|
+
const content = await readFile16(filePath, "utf-8");
|
|
5241
|
+
totalChars += content.length;
|
|
5242
|
+
}
|
|
5243
|
+
} catch {
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
return Math.ceil(totalChars / CHARS_PER_TOKEN);
|
|
5248
|
+
}
|
|
5249
|
+
async function syncWorkspaceRepos(workspaceRoot, options = {}) {
|
|
5250
|
+
const wsManifest = await readWorkspaceManifest(workspaceRoot);
|
|
5251
|
+
if (!wsManifest) {
|
|
5252
|
+
return { repos: [] };
|
|
5253
|
+
}
|
|
5254
|
+
const wsChecksum = createHash2("sha256").update(JSON.stringify(wsManifest)).digest("hex");
|
|
5255
|
+
const index = await buildContentIndex(CONTENT_ROOT);
|
|
5256
|
+
const protectedIds = new Set(
|
|
5257
|
+
index.items.filter((item) => item.protected).map((item) => item.id)
|
|
5258
|
+
);
|
|
5259
|
+
const targetRepos = options.repos?.length ? wsManifest.repos.filter((r) => options.repos.includes(r.path)) : wsManifest.repos.filter((r) => r.sync);
|
|
5260
|
+
const results = [];
|
|
5261
|
+
for (const repoEntry of targetRepos) {
|
|
4302
5262
|
try {
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
|
|
5263
|
+
const result = await syncSingleRepo(
|
|
5264
|
+
workspaceRoot,
|
|
5265
|
+
wsManifest,
|
|
5266
|
+
wsChecksum,
|
|
5267
|
+
repoEntry,
|
|
5268
|
+
index,
|
|
5269
|
+
protectedIds,
|
|
5270
|
+
options
|
|
5271
|
+
);
|
|
5272
|
+
results.push(result);
|
|
5273
|
+
} catch (err) {
|
|
5274
|
+
results.push({
|
|
5275
|
+
path: repoEntry.path,
|
|
5276
|
+
added: [],
|
|
5277
|
+
removed: [],
|
|
5278
|
+
toolsSynced: [],
|
|
5279
|
+
action: "error",
|
|
5280
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
if (!options.dryRun) {
|
|
5285
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5286
|
+
for (const result of results) {
|
|
5287
|
+
if (result.action === "synced") {
|
|
5288
|
+
const entry = wsManifest.repos.find((r) => r.path === result.path);
|
|
5289
|
+
if (entry) entry.lastSync = now;
|
|
4306
5290
|
}
|
|
4307
|
-
} catch {
|
|
4308
5291
|
}
|
|
5292
|
+
await writeWorkspaceManifest(workspaceRoot, wsManifest);
|
|
4309
5293
|
}
|
|
5294
|
+
return { repos: results };
|
|
4310
5295
|
}
|
|
4311
|
-
function
|
|
4312
|
-
const
|
|
4313
|
-
|
|
5296
|
+
async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry, index, protectedIds, options) {
|
|
5297
|
+
const repoDir = join20(workspaceRoot, repoEntry.path);
|
|
5298
|
+
const repoAgentsDir = join20(repoDir, AGENTS_DIR);
|
|
5299
|
+
try {
|
|
5300
|
+
await access6(repoDir);
|
|
5301
|
+
} catch {
|
|
5302
|
+
return {
|
|
5303
|
+
path: repoEntry.path,
|
|
5304
|
+
added: [],
|
|
5305
|
+
removed: [],
|
|
5306
|
+
toolsSynced: [],
|
|
5307
|
+
action: "error",
|
|
5308
|
+
error: `Directory not found: ${repoEntry.path}`
|
|
5309
|
+
};
|
|
5310
|
+
}
|
|
5311
|
+
const resolved = resolveRepoConfig(wsManifest.defaults, repoEntry.overrides, protectedIds);
|
|
5312
|
+
const effectiveSelection = buildSelectionFromIds(resolved.contentIds, wsManifest.defaults.content, index.items);
|
|
5313
|
+
const existingManifest = await readManifest(repoDir);
|
|
5314
|
+
const previousIds = existingManifest?.content ? getAllContentIds(existingManifest.content) : /* @__PURE__ */ new Set();
|
|
5315
|
+
const toAdd = [...resolved.contentIds].filter((id) => !previousIds.has(id));
|
|
5316
|
+
const toRemove = [...previousIds].filter(
|
|
5317
|
+
(id) => !resolved.contentIds.has(id) && !existingManifest?.workspace?.localContent?.includes(id)
|
|
5318
|
+
);
|
|
5319
|
+
if (options.dryRun) {
|
|
5320
|
+
const estimatedTokens = await estimateTokensForContent(resolved.contentIds, index);
|
|
5321
|
+
return {
|
|
5322
|
+
path: repoEntry.path,
|
|
5323
|
+
added: toAdd,
|
|
5324
|
+
removed: toRemove,
|
|
5325
|
+
toolsSynced: resolved.tools,
|
|
5326
|
+
action: "dry-run",
|
|
5327
|
+
estimatedTokens
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5330
|
+
options.onProgress?.(`Syncing ${repoEntry.name ?? repoEntry.path}...`);
|
|
5331
|
+
await mkdir6(repoAgentsDir, { recursive: true });
|
|
5332
|
+
await copySelectedContent(CONTENT_ROOT, repoAgentsDir, effectiveSelection, index);
|
|
5333
|
+
for (const id of toRemove) {
|
|
5334
|
+
const items = getAllItemsById(index, id);
|
|
5335
|
+
for (const item of items) {
|
|
5336
|
+
await removeContentItem(repoAgentsDir, item, { rootDir: repoDir });
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
const canonicalAgentsMd = await generateCanonicalAgentsMd(repoAgentsDir);
|
|
5340
|
+
await safeWriteFile(join20(repoAgentsDir, "AGENTS.md"), canonicalAgentsMd, { force: true });
|
|
5341
|
+
const repoInfo = await analyzeRepo(repoDir);
|
|
5342
|
+
let gitOwner = repoEntry.owner ?? "";
|
|
5343
|
+
let gitRepo = repoEntry.repo ?? "";
|
|
5344
|
+
let gitBranch = repoEntry.defaultBranch ?? "";
|
|
5345
|
+
let gitPlatform = repoEntry.platform;
|
|
5346
|
+
if (!gitOwner && !gitRepo) {
|
|
5347
|
+
const identity = detectRepoGitIdentity(repoDir);
|
|
5348
|
+
gitOwner = identity.owner;
|
|
5349
|
+
gitRepo = identity.repo;
|
|
5350
|
+
gitBranch = gitBranch || identity.defaultBranch;
|
|
5351
|
+
gitPlatform = gitPlatform ?? identity.platform;
|
|
5352
|
+
}
|
|
5353
|
+
if (!gitOwner && !gitRepo && existingManifest) {
|
|
5354
|
+
gitOwner = existingManifest.owner;
|
|
5355
|
+
gitRepo = existingManifest.repo;
|
|
5356
|
+
}
|
|
5357
|
+
if (!gitBranch) gitBranch = "main";
|
|
5358
|
+
const manifest = createManifest({
|
|
5359
|
+
platform: gitPlatform ?? resolved.platform,
|
|
5360
|
+
owner: gitOwner,
|
|
5361
|
+
repo: gitRepo,
|
|
5362
|
+
namespace: gitOwner,
|
|
5363
|
+
project: gitRepo,
|
|
5364
|
+
defaultBranch: gitBranch,
|
|
5365
|
+
tools: resolved.tools,
|
|
5366
|
+
features: resolved.features,
|
|
5367
|
+
mcpServers: resolved.mcp.servers,
|
|
5368
|
+
content: effectiveSelection,
|
|
5369
|
+
languages: repoInfo.languages
|
|
5370
|
+
});
|
|
5371
|
+
manifest.workspace = {
|
|
5372
|
+
rootPath: relative3(repoDir, workspaceRoot),
|
|
5373
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5374
|
+
syncVersion: HATCH3R_VERSION,
|
|
5375
|
+
workspaceChecksum: wsChecksum,
|
|
5376
|
+
excludedContent: resolved.excludedContent.length > 0 ? resolved.excludedContent : void 0,
|
|
5377
|
+
localContent: existingManifest?.workspace?.localContent
|
|
5378
|
+
};
|
|
5379
|
+
if (resolved.models) {
|
|
5380
|
+
manifest.models = resolved.models;
|
|
5381
|
+
}
|
|
5382
|
+
await writeManifest(repoDir, manifest);
|
|
5383
|
+
await safeWriteFile(join20(repoDir, "AGENTS.md"), AGENTS_MD_FULL, {
|
|
5384
|
+
managedContent: AGENTS_MD_INNER,
|
|
5385
|
+
appendIfNoBlock: true
|
|
5386
|
+
});
|
|
5387
|
+
addManagedFile(manifest, "AGENTS.md");
|
|
5388
|
+
const toolsSynced = [];
|
|
5389
|
+
for (const tool of resolved.tools) {
|
|
5390
|
+
try {
|
|
5391
|
+
const adapter = getAdapter(tool);
|
|
5392
|
+
const outputs = await adapter.generate(repoAgentsDir, manifest);
|
|
5393
|
+
for (const w of adapter.warnings) {
|
|
5394
|
+
options.onWarn?.(w);
|
|
5395
|
+
}
|
|
5396
|
+
for (const out of outputs) {
|
|
5397
|
+
await safeWriteFile(join20(repoDir, out.path), out.content, {
|
|
5398
|
+
managedContent: out.managedContent,
|
|
5399
|
+
appendIfNoBlock: true
|
|
5400
|
+
});
|
|
5401
|
+
addManagedFile(manifest, out.path);
|
|
5402
|
+
}
|
|
5403
|
+
toolsSynced.push(tool);
|
|
5404
|
+
} catch (err) {
|
|
5405
|
+
options.onWarn?.(
|
|
5406
|
+
`Failed to generate ${tool} output for ${repoEntry.path}: ${err instanceof Error ? err.message : String(err)}`
|
|
5407
|
+
);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
await writeManifest(repoDir, manifest);
|
|
5411
|
+
const integrityManifest = await generateIntegrityManifest(repoAgentsDir, HATCH3R_VERSION);
|
|
5412
|
+
await writeIntegrityManifest(repoAgentsDir, integrityManifest);
|
|
5413
|
+
if (manifest.features.mcp && manifest.mcp.servers.length > 0) {
|
|
5414
|
+
await ensureEnvMcp(repoDir, manifest.mcp.servers);
|
|
5415
|
+
await ensureGitignoreEntry(repoDir);
|
|
5416
|
+
}
|
|
5417
|
+
return {
|
|
5418
|
+
path: repoEntry.path,
|
|
5419
|
+
added: toAdd,
|
|
5420
|
+
removed: toRemove,
|
|
5421
|
+
toolsSynced,
|
|
5422
|
+
action: "synced"
|
|
5423
|
+
};
|
|
4314
5424
|
}
|
|
4315
5425
|
|
|
4316
5426
|
// src/cli/shared/constants.ts
|
|
@@ -4329,7 +5439,8 @@ var TOOL_DISPLAY_NAMES = {
|
|
|
4329
5439
|
kiro: "Kiro",
|
|
4330
5440
|
goose: "Goose",
|
|
4331
5441
|
zed: "Zed",
|
|
4332
|
-
"amazon-q": "Amazon Q"
|
|
5442
|
+
"amazon-q": "Amazon Q",
|
|
5443
|
+
antigravity: "Antigravity"
|
|
4333
5444
|
};
|
|
4334
5445
|
var TOOL_PROMPT_CHOICES = TOOLS.map((t) => ({
|
|
4335
5446
|
name: TOOL_DISPLAY_NAMES[t],
|
|
@@ -4359,6 +5470,42 @@ var PLATFORM_MCP_SERVER = {
|
|
|
4359
5470
|
"azure-devops": "azure-devops",
|
|
4360
5471
|
gitlab: "gitlab"
|
|
4361
5472
|
};
|
|
5473
|
+
var TOOL_COMMAND_SYNTAX = {
|
|
5474
|
+
cursor: "/",
|
|
5475
|
+
copilot: "/",
|
|
5476
|
+
claude: "/",
|
|
5477
|
+
opencode: "/",
|
|
5478
|
+
windsurf: "run workflow ",
|
|
5479
|
+
amp: "/",
|
|
5480
|
+
codex: "prompt with ",
|
|
5481
|
+
gemini: "/",
|
|
5482
|
+
cline: "run workflow ",
|
|
5483
|
+
aider: "prompt with ",
|
|
5484
|
+
kiro: "/",
|
|
5485
|
+
goose: "prompt with ",
|
|
5486
|
+
zed: "/",
|
|
5487
|
+
"amazon-q": "/",
|
|
5488
|
+
antigravity: "/"
|
|
5489
|
+
};
|
|
5490
|
+
function formatCommandHint(tools, commandName) {
|
|
5491
|
+
const allSlash = tools.every((t) => TOOL_COMMAND_SYNTAX[t] === "/");
|
|
5492
|
+
if (allSlash) {
|
|
5493
|
+
return `/${commandName}`;
|
|
5494
|
+
}
|
|
5495
|
+
return `the ${commandName} command`;
|
|
5496
|
+
}
|
|
5497
|
+
var TOOL_SECRET_NOTES = {
|
|
5498
|
+
cursor: "Cursor: auto-loads .env.mcp from project root",
|
|
5499
|
+
copilot: "VS Code / Copilot: auto-loads .env.mcp from project root",
|
|
5500
|
+
claude: "Claude Code: reads .env.mcp via shell sourcing (run `set -a && source .env.mcp && set +a` before starting)",
|
|
5501
|
+
windsurf: "Windsurf: auto-loads .env.mcp from project root",
|
|
5502
|
+
cline: "Cline / Roo Code: reads env from VS Code settings; copy values to .vscode/settings.json or use shell sourcing",
|
|
5503
|
+
amp: "Amp: reads env from shell; source .env.mcp in your shell profile",
|
|
5504
|
+
codex: "Codex CLI: reads env from shell; source .env.mcp before running",
|
|
5505
|
+
gemini: "Gemini CLI: reads env from shell; source .env.mcp before running",
|
|
5506
|
+
aider: "Aider: reads env from shell; source .env.mcp before running",
|
|
5507
|
+
opencode: "OpenCode: reads env from shell; source .env.mcp before running"
|
|
5508
|
+
};
|
|
4362
5509
|
function sanitizeInput(value) {
|
|
4363
5510
|
return value.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
4364
5511
|
}
|
|
@@ -4372,7 +5519,7 @@ function isWSL() {
|
|
|
4372
5519
|
}
|
|
4373
5520
|
|
|
4374
5521
|
// src/cli/commands/config.ts
|
|
4375
|
-
var
|
|
5522
|
+
var __dirname3 = dirname11(fileURLToPath3(import.meta.url));
|
|
4376
5523
|
function computeDiff(oldManifest, newTools, newFeatures, newMcp, newPlatform, newOwner, newRepo, newNamespace, newProject) {
|
|
4377
5524
|
const oldToolSet = new Set(oldManifest.tools);
|
|
4378
5525
|
const newToolSet = new Set(newTools);
|
|
@@ -4425,7 +5572,13 @@ async function configCommand() {
|
|
|
4425
5572
|
if (!manifest) {
|
|
4426
5573
|
error("No .agents/hatch.json found.");
|
|
4427
5574
|
console.log(chalk5.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
4428
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
5575
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
5576
|
+
}
|
|
5577
|
+
if (manifest.workspace) {
|
|
5578
|
+
warn(
|
|
5579
|
+
`This repo is managed by workspace at ${manifest.workspace.rootPath}. Changes here may be overwritten on next workspace sync.`
|
|
5580
|
+
);
|
|
5581
|
+
console.log();
|
|
4429
5582
|
}
|
|
4430
5583
|
printCurrentConfig(manifest);
|
|
4431
5584
|
const wslTheme = isWSL() ? { icon: { checked: chalk5.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
|
|
@@ -4499,7 +5652,7 @@ async function configCommand() {
|
|
|
4499
5652
|
const tools = toolAnswers.tools;
|
|
4500
5653
|
if (tools.length === 0) {
|
|
4501
5654
|
error("At least one tool must be selected.");
|
|
4502
|
-
throw new HatchError("At least one tool must be selected.", 1);
|
|
5655
|
+
throw new HatchError("At least one tool must be selected.", 1, "VALIDATION_ERROR");
|
|
4503
5656
|
}
|
|
4504
5657
|
const currentFeatureKeys = Object.keys(DEFAULT_FEATURES).filter((k) => manifest.features[k]);
|
|
4505
5658
|
const featureAnswers = await inquirer2.prompt([
|
|
@@ -4560,8 +5713,12 @@ async function configCommand() {
|
|
|
4560
5713
|
}
|
|
4561
5714
|
]);
|
|
4562
5715
|
if (manageContent.manage) {
|
|
4563
|
-
|
|
4564
|
-
|
|
5716
|
+
info(
|
|
5717
|
+
chalk5.dim("Config adds/removes content items. To customize an item's behavior without ") + chalk5.dim("removing it, use .hatch3r/<type>/<id>.customize.yaml instead.")
|
|
5718
|
+
);
|
|
5719
|
+
console.log();
|
|
5720
|
+
const contentRoot = findPackageRoot(__dirname3);
|
|
5721
|
+
const agentsDir = join21(rootDir, AGENTS_DIR);
|
|
4565
5722
|
const index = await buildContentIndex(contentRoot);
|
|
4566
5723
|
const currentIds = /* @__PURE__ */ new Set();
|
|
4567
5724
|
for (const ids of Object.values(manifest.content.items)) {
|
|
@@ -4581,6 +5738,63 @@ async function configCommand() {
|
|
|
4581
5738
|
}
|
|
4582
5739
|
]);
|
|
4583
5740
|
const newIds = new Set(contentAnswer.items);
|
|
5741
|
+
const pendingRemovals = [];
|
|
5742
|
+
for (const id of currentIds) {
|
|
5743
|
+
if (!newIds.has(id)) pendingRemovals.push(id);
|
|
5744
|
+
}
|
|
5745
|
+
if (pendingRemovals.length > 0) {
|
|
5746
|
+
const dependencyWarnings = [];
|
|
5747
|
+
for (const removedId of pendingRemovals) {
|
|
5748
|
+
const dependents = [];
|
|
5749
|
+
for (const keepId of contentAnswer.items) {
|
|
5750
|
+
const keepItem = index.byId.get(keepId);
|
|
5751
|
+
if (!keepItem) continue;
|
|
5752
|
+
try {
|
|
5753
|
+
const filePath = keepItem.type === "skill" ? join21(agentsDir, keepItem.relativePath, "SKILL.md") : join21(agentsDir, keepItem.relativePath);
|
|
5754
|
+
const content = await readFile17(filePath, "utf-8");
|
|
5755
|
+
const refs = extractContentReferences(content);
|
|
5756
|
+
if (refs.includes(removedId)) {
|
|
5757
|
+
dependents.push(keepId);
|
|
5758
|
+
}
|
|
5759
|
+
} catch {
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
if (dependents.length > 0) {
|
|
5763
|
+
dependencyWarnings.push(
|
|
5764
|
+
`Removing "${removedId}" \u2014 referenced by: ${dependents.join(", ")}`
|
|
5765
|
+
);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
const proposedSelection = {
|
|
5769
|
+
...manifest.content,
|
|
5770
|
+
items: {
|
|
5771
|
+
agents: [],
|
|
5772
|
+
skills: [],
|
|
5773
|
+
rules: [],
|
|
5774
|
+
commands: [],
|
|
5775
|
+
prompts: [],
|
|
5776
|
+
hooks: [],
|
|
5777
|
+
githubAgents: []
|
|
5778
|
+
}
|
|
5779
|
+
};
|
|
5780
|
+
for (const id of contentAnswer.items) {
|
|
5781
|
+
const proposedItem = index.byId.get(id);
|
|
5782
|
+
if (proposedItem) {
|
|
5783
|
+
const key = TYPE_TO_SELECTION_KEY[proposedItem.type];
|
|
5784
|
+
if (key) proposedSelection.items[key].push(proposedItem.id);
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
const orchWarnings = validateOrchestrationDependencies(proposedSelection);
|
|
5788
|
+
dependencyWarnings.push(...orchWarnings);
|
|
5789
|
+
if (dependencyWarnings.length > 0) {
|
|
5790
|
+
console.log();
|
|
5791
|
+
warn("Dependency warnings for removed content:");
|
|
5792
|
+
for (const w of dependencyWarnings) {
|
|
5793
|
+
console.log(chalk5.dim(` ${w}`));
|
|
5794
|
+
}
|
|
5795
|
+
console.log();
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
4584
5798
|
for (const id of contentAnswer.items) {
|
|
4585
5799
|
if (!currentIds.has(id)) {
|
|
4586
5800
|
const item = index.byId.get(id);
|
|
@@ -4618,7 +5832,7 @@ async function configCommand() {
|
|
|
4618
5832
|
manifest.content.items = newItems;
|
|
4619
5833
|
if (contentChanges.added.length > 0 || contentChanges.removed.length > 0) {
|
|
4620
5834
|
const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
|
|
4621
|
-
await safeWriteFile(
|
|
5835
|
+
await safeWriteFile(join21(agentsDir, "AGENTS.md"), canonicalAgentsMd);
|
|
4622
5836
|
}
|
|
4623
5837
|
}
|
|
4624
5838
|
}
|
|
@@ -4685,7 +5899,7 @@ async function configCommand() {
|
|
|
4685
5899
|
if (manifest.worktree?.enabled) {
|
|
4686
5900
|
const wtContent = await generateWorktreeInclude(manifest, rootDir);
|
|
4687
5901
|
const wtManaged = extractManagedContent(wtContent);
|
|
4688
|
-
await safeWriteFile(
|
|
5902
|
+
await safeWriteFile(join21(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
|
|
4689
5903
|
managedContent: wtManaged
|
|
4690
5904
|
});
|
|
4691
5905
|
}
|
|
@@ -4755,132 +5969,156 @@ async function configCommand() {
|
|
|
4755
5969
|
}
|
|
4756
5970
|
console.log();
|
|
4757
5971
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
import { execFileSync as execFileSync4 } from "child_process";
|
|
4765
|
-
import chalk6 from "chalk";
|
|
4766
|
-
import inquirer3 from "inquirer";
|
|
4767
|
-
|
|
4768
|
-
// src/detect/repoAnalyzer.ts
|
|
4769
|
-
import { access as access4, readFile as readFile14, readdir as readdir8 } from "fs/promises";
|
|
4770
|
-
import { join as join18 } from "path";
|
|
4771
|
-
async function analyzeRepo(rootDir) {
|
|
4772
|
-
const [languages, pm, isMonorepo, hasExistingAgents, existingTools] = await Promise.all([
|
|
4773
|
-
detectLanguages(rootDir),
|
|
4774
|
-
detectPackageManager(rootDir),
|
|
4775
|
-
detectMonorepo(rootDir),
|
|
4776
|
-
detectExistingAgents(rootDir),
|
|
4777
|
-
detectExistingTools(rootDir)
|
|
4778
|
-
]);
|
|
4779
|
-
const packageManager = pm.name;
|
|
4780
|
-
return {
|
|
4781
|
-
languages,
|
|
4782
|
-
packageManager,
|
|
4783
|
-
isMonorepo,
|
|
4784
|
-
hasExistingAgents,
|
|
4785
|
-
existingTools,
|
|
4786
|
-
rootDir
|
|
4787
|
-
};
|
|
4788
|
-
}
|
|
4789
|
-
async function detectLanguages(rootDir) {
|
|
4790
|
-
const languages = [];
|
|
4791
|
-
const indicators = {
|
|
4792
|
-
typescript: ["tsconfig.json", "tsconfig.base.json"],
|
|
4793
|
-
javascript: ["jsconfig.json"],
|
|
4794
|
-
python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
|
|
4795
|
-
rust: ["Cargo.toml", "Cargo.lock"],
|
|
4796
|
-
go: ["go.mod", "go.sum"],
|
|
4797
|
-
java: ["pom.xml", "build.gradle"],
|
|
4798
|
-
kotlin: ["build.gradle.kts"],
|
|
4799
|
-
ruby: ["Gemfile"],
|
|
4800
|
-
php: ["composer.json"],
|
|
4801
|
-
swift: ["Package.swift"],
|
|
4802
|
-
dart: ["pubspec.yaml"],
|
|
4803
|
-
elixir: ["mix.exs"]
|
|
4804
|
-
};
|
|
4805
|
-
for (const [lang, files] of Object.entries(indicators)) {
|
|
4806
|
-
for (const file of files) {
|
|
4807
|
-
if (await pathExists(join18(rootDir, file))) {
|
|
4808
|
-
languages.push(lang);
|
|
4809
|
-
break;
|
|
4810
|
-
}
|
|
5972
|
+
if (diff.addedTools.length > 0 || diff.removedTools.length > 0) {
|
|
5973
|
+
console.log();
|
|
5974
|
+
info("Tool migration notes:");
|
|
5975
|
+
if (diff.removedTools.length > 0) {
|
|
5976
|
+
info(chalk5.dim(` Removed tool output archived to .hatch3r-archive/ (recoverable).`));
|
|
5977
|
+
info(chalk5.dim(` Customizations in .hatch3r/ are tool-agnostic and carry forward.`));
|
|
4811
5978
|
}
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
if (rootEntries.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
|
|
4816
|
-
languages.push("csharp");
|
|
5979
|
+
if (diff.addedTools.length > 0) {
|
|
5980
|
+
info(chalk5.dim(` New tool output generated. Restart your editor to pick up changes.`));
|
|
5981
|
+
info(chalk5.dim(` MCP secrets (.env.mcp) are shared across tools \u2014 no re-entry needed.`));
|
|
4817
5982
|
}
|
|
4818
|
-
|
|
4819
|
-
if (err.code !== "ENOENT") throw err;
|
|
4820
|
-
}
|
|
4821
|
-
if (languages.length === 0) {
|
|
4822
|
-
languages.push("unknown");
|
|
4823
|
-
}
|
|
4824
|
-
return languages;
|
|
4825
|
-
}
|
|
4826
|
-
async function detectMonorepo(rootDir) {
|
|
4827
|
-
if (await pathExists(join18(rootDir, "pnpm-workspace.yaml"))) return true;
|
|
4828
|
-
if (await pathExists(join18(rootDir, "lerna.json"))) return true;
|
|
4829
|
-
if (await pathExists(join18(rootDir, "nx.json"))) return true;
|
|
4830
|
-
if (await pathExists(join18(rootDir, "turbo.json"))) return true;
|
|
4831
|
-
if (await pathExists(join18(rootDir, "pants.toml"))) return true;
|
|
4832
|
-
try {
|
|
4833
|
-
const pkgJson = await readFile14(join18(rootDir, "package.json"), "utf-8");
|
|
4834
|
-
const pkg = JSON.parse(pkgJson);
|
|
4835
|
-
if (pkg.workspaces) return true;
|
|
4836
|
-
} catch (err) {
|
|
4837
|
-
const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
|
|
4838
|
-
if (!isExpected) throw err;
|
|
5983
|
+
console.log();
|
|
4839
5984
|
}
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
{ tool: "gemini", paths: [".gemini", "GEMINI.md"] },
|
|
4854
|
-
{ tool: "cline", paths: [".clinerules", ".roo", ".roomodes"] },
|
|
4855
|
-
{ tool: "aider", paths: [".aider", ".aider.conf.yml"] },
|
|
4856
|
-
{ tool: "kiro", paths: [".kiro"] },
|
|
4857
|
-
{ tool: "goose", paths: [".goosehints", ".goose"] },
|
|
4858
|
-
{ tool: "zed", paths: [".rules"] },
|
|
4859
|
-
{ tool: "amazon-q", paths: [".amazonq"] }
|
|
4860
|
-
];
|
|
4861
|
-
async function detectExistingTools(rootDir) {
|
|
4862
|
-
const results = await Promise.allSettled(
|
|
4863
|
-
TOOL_INDICATORS.map(async ({ tool, paths }) => {
|
|
4864
|
-
for (const p of paths) {
|
|
4865
|
-
if (await pathExists(join18(rootDir, p))) return tool;
|
|
5985
|
+
const wsManifest = await readWorkspaceManifest(rootDir);
|
|
5986
|
+
if (wsManifest) {
|
|
5987
|
+
console.log();
|
|
5988
|
+
info(chalk5.bold("Workspace configuration"));
|
|
5989
|
+
const currentRepos = wsManifest.repos.map((r) => r.path);
|
|
5990
|
+
console.log(chalk5.dim(` Repos: ${currentRepos.join(", ") || "(none)"}`));
|
|
5991
|
+
console.log(chalk5.dim(` Sync strategy: ${wsManifest.syncStrategy}`));
|
|
5992
|
+
const { manageWorkspace } = await inquirer2.prompt([
|
|
5993
|
+
{
|
|
5994
|
+
type: "confirm",
|
|
5995
|
+
name: "manageWorkspace",
|
|
5996
|
+
message: "Configure workspace settings?",
|
|
5997
|
+
default: false
|
|
4866
5998
|
}
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
5999
|
+
]);
|
|
6000
|
+
if (manageWorkspace) {
|
|
6001
|
+
const detectedRepos = await detectSubRepos(rootDir);
|
|
6002
|
+
const existingPaths = new Set(wsManifest.repos.map((r) => r.path));
|
|
6003
|
+
const newRepos = detectedRepos.filter((r) => !existingPaths.has(r.path));
|
|
6004
|
+
if (newRepos.length > 0) {
|
|
6005
|
+
const { addRepos } = await inquirer2.prompt([
|
|
6006
|
+
{
|
|
6007
|
+
type: "checkbox",
|
|
6008
|
+
name: "addRepos",
|
|
6009
|
+
message: "New repos detected. Add to workspace?",
|
|
6010
|
+
choices: newRepos.map((r) => ({
|
|
6011
|
+
name: r.name,
|
|
6012
|
+
value: r.path,
|
|
6013
|
+
checked: false
|
|
6014
|
+
})),
|
|
6015
|
+
...wslTheme && { theme: wslTheme }
|
|
6016
|
+
}
|
|
6017
|
+
]);
|
|
6018
|
+
for (const path of addRepos) {
|
|
6019
|
+
wsManifest.repos.push({ path, name: path, sync: false });
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
if (wsManifest.repos.length > 0) {
|
|
6023
|
+
const { syncRepos } = await inquirer2.prompt([
|
|
6024
|
+
{
|
|
6025
|
+
type: "checkbox",
|
|
6026
|
+
name: "syncRepos",
|
|
6027
|
+
message: "Select repos to sync:",
|
|
6028
|
+
choices: wsManifest.repos.map((r) => ({
|
|
6029
|
+
name: r.name ?? r.path,
|
|
6030
|
+
value: r.path,
|
|
6031
|
+
checked: r.sync
|
|
6032
|
+
})),
|
|
6033
|
+
...wslTheme && { theme: wslTheme }
|
|
6034
|
+
}
|
|
6035
|
+
]);
|
|
6036
|
+
const syncSet = new Set(syncRepos);
|
|
6037
|
+
for (const repo2 of wsManifest.repos) {
|
|
6038
|
+
repo2.sync = syncSet.has(repo2.path);
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
if (wsManifest.repos.length > 0) {
|
|
6042
|
+
const { editIdentity } = await inquirer2.prompt([
|
|
6043
|
+
{
|
|
6044
|
+
type: "list",
|
|
6045
|
+
name: "editIdentity",
|
|
6046
|
+
message: "Repo git identities:",
|
|
6047
|
+
choices: [
|
|
6048
|
+
{ name: "Keep current", value: "keep" },
|
|
6049
|
+
{ name: "Re-detect all from git remotes", value: "detect" },
|
|
6050
|
+
{ name: "Edit manually", value: "edit" }
|
|
6051
|
+
],
|
|
6052
|
+
default: "keep"
|
|
6053
|
+
}
|
|
6054
|
+
]);
|
|
6055
|
+
if (editIdentity === "detect") {
|
|
6056
|
+
for (const repo2 of wsManifest.repos) {
|
|
6057
|
+
const identity = detectRepoGitIdentity(join21(rootDir, repo2.path));
|
|
6058
|
+
repo2.owner = identity.owner || void 0;
|
|
6059
|
+
repo2.repo = identity.repo || void 0;
|
|
6060
|
+
repo2.defaultBranch = identity.defaultBranch || void 0;
|
|
6061
|
+
repo2.platform = identity.platform || void 0;
|
|
6062
|
+
}
|
|
6063
|
+
info("Re-detected git identities for all repos.");
|
|
6064
|
+
} else if (editIdentity === "edit") {
|
|
6065
|
+
for (const repo2 of wsManifest.repos) {
|
|
6066
|
+
console.log(chalk5.bold(`
|
|
6067
|
+
${repo2.name ?? repo2.path}:`));
|
|
6068
|
+
const identity = await inquirer2.prompt([
|
|
6069
|
+
{ type: "input", name: "owner", message: " Owner:", default: repo2.owner || void 0 },
|
|
6070
|
+
{ type: "input", name: "repo", message: " Repo:", default: repo2.repo || void 0 },
|
|
6071
|
+
{ type: "input", name: "defaultBranch", message: " Default branch:", default: repo2.defaultBranch || "main" }
|
|
6072
|
+
]);
|
|
6073
|
+
repo2.owner = sanitizeInput(identity.owner) || void 0;
|
|
6074
|
+
repo2.repo = sanitizeInput(identity.repo) || void 0;
|
|
6075
|
+
repo2.defaultBranch = identity.defaultBranch.trim() || void 0;
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
const { strategy } = await inquirer2.prompt([
|
|
6080
|
+
{
|
|
6081
|
+
type: "list",
|
|
6082
|
+
name: "strategy",
|
|
6083
|
+
message: "Sync strategy:",
|
|
6084
|
+
choices: [
|
|
6085
|
+
{ name: "Manual \u2014 sync sub-repos only with --repos flag", value: "manual" },
|
|
6086
|
+
{ name: "On sync \u2014 auto-sync sub-repos when running hatch3r sync", value: "on-sync" }
|
|
6087
|
+
],
|
|
6088
|
+
default: wsManifest.syncStrategy
|
|
6089
|
+
}
|
|
6090
|
+
]);
|
|
6091
|
+
wsManifest.syncStrategy = strategy;
|
|
6092
|
+
await writeWorkspaceManifest(rootDir, wsManifest);
|
|
6093
|
+
const syncCount = wsManifest.repos.filter((r) => r.sync).length;
|
|
6094
|
+
if (syncCount > 0) {
|
|
6095
|
+
const { syncNow } = await inquirer2.prompt([
|
|
6096
|
+
{
|
|
6097
|
+
type: "confirm",
|
|
6098
|
+
name: "syncNow",
|
|
6099
|
+
message: `Sync ${syncCount} repo(s) now?`,
|
|
6100
|
+
default: false
|
|
6101
|
+
}
|
|
6102
|
+
]);
|
|
6103
|
+
if (syncNow) {
|
|
6104
|
+
const wsSpinner = createSpinner(`Syncing ${syncCount} repo(s)...`);
|
|
6105
|
+
wsSpinner.start();
|
|
6106
|
+
const result = await syncWorkspaceRepos(rootDir, { onWarn: (msg) => warn(msg) });
|
|
6107
|
+
const succeeded = result.repos.filter((r) => r.action === "synced").length;
|
|
6108
|
+
wsSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
4881
6112
|
}
|
|
4882
6113
|
}
|
|
4883
6114
|
|
|
6115
|
+
// src/cli/commands/init.ts
|
|
6116
|
+
import { access as access7, mkdir as mkdir7, readFile as readFile18 } from "fs/promises";
|
|
6117
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6118
|
+
import { basename as basename2, dirname as dirname12, join as join22 } from "path";
|
|
6119
|
+
import chalk6 from "chalk";
|
|
6120
|
+
import inquirer3 from "inquirer";
|
|
6121
|
+
|
|
4884
6122
|
// src/content/presets.ts
|
|
4885
6123
|
var PRESETS = [
|
|
4886
6124
|
{
|
|
@@ -4920,66 +6158,45 @@ function getPreset(id) {
|
|
|
4920
6158
|
}
|
|
4921
6159
|
|
|
4922
6160
|
// src/cli/commands/init.ts
|
|
4923
|
-
var
|
|
4924
|
-
var
|
|
6161
|
+
var __dirname4 = dirname12(fileURLToPath4(import.meta.url));
|
|
6162
|
+
var CONTENT_ROOT2 = findPackageRoot(__dirname4);
|
|
4925
6163
|
var DEFAULT_TOOLS = ["cursor"];
|
|
4926
6164
|
var DEFAULT_FEATURE_KEYS = Object.keys(DEFAULT_FEATURES);
|
|
4927
6165
|
var DEFAULT_MCP = ["playwright", "github", "context7"];
|
|
4928
|
-
function
|
|
4929
|
-
|
|
4930
|
-
const url = execFileSync4("git", ["remote", "get-url", "origin"], {
|
|
4931
|
-
stdio: "pipe"
|
|
4932
|
-
}).toString().trim();
|
|
4933
|
-
const sshMatch = url.match(/[:\/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
4934
|
-
if (sshMatch) {
|
|
4935
|
-
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
4936
|
-
}
|
|
4937
|
-
return { owner: "", repo: "" };
|
|
4938
|
-
} catch (err) {
|
|
4939
|
-
const e = err;
|
|
4940
|
-
if (e.code === "ENOENT") return { owner: "", repo: "" };
|
|
4941
|
-
if (e.status === 128) return { owner: "", repo: "" };
|
|
4942
|
-
throw err;
|
|
4943
|
-
}
|
|
4944
|
-
}
|
|
4945
|
-
function parseGitDefaultBranch() {
|
|
4946
|
-
try {
|
|
4947
|
-
const ref = execFileSync4("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], {
|
|
4948
|
-
stdio: "pipe"
|
|
4949
|
-
}).toString().trim();
|
|
4950
|
-
if (ref && ref.startsWith("origin/")) {
|
|
4951
|
-
return ref.replace(/^origin\//, "");
|
|
4952
|
-
}
|
|
4953
|
-
return "main";
|
|
4954
|
-
} catch (err) {
|
|
4955
|
-
const e = err;
|
|
4956
|
-
if (e.code === "ENOENT") return "main";
|
|
4957
|
-
if (e.status === 128) return "main";
|
|
4958
|
-
throw err;
|
|
4959
|
-
}
|
|
6166
|
+
function selectionHasBoardContent(selection) {
|
|
6167
|
+
return selection.items.commands.some((id) => id.startsWith("hatch3r-board"));
|
|
4960
6168
|
}
|
|
4961
|
-
function
|
|
4962
|
-
if (
|
|
4963
|
-
|
|
4964
|
-
|
|
6169
|
+
function warnBoardPrerequisites(selection) {
|
|
6170
|
+
if (!selectionHasBoardContent(selection)) return;
|
|
6171
|
+
info(
|
|
6172
|
+
`Board commands selected. Prerequisites: ${chalk6.bold("GitHub Projects V2")} must be enabled and your PAT needs the ${chalk6.bold("project")} scope. See ${chalk6.dim("https://docs.github.com/en/issues/planning-and-tracking-with-projects")}`
|
|
6173
|
+
);
|
|
4965
6174
|
}
|
|
4966
|
-
function
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
6175
|
+
function deriveWorkspacePlatform(identities) {
|
|
6176
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6177
|
+
for (const id of identities) {
|
|
6178
|
+
counts.set(id.platform, (counts.get(id.platform) ?? 0) + 1);
|
|
6179
|
+
}
|
|
6180
|
+
let best = "github";
|
|
6181
|
+
let max = 0;
|
|
6182
|
+
for (const [p, c] of counts) {
|
|
6183
|
+
if (c > max) {
|
|
6184
|
+
best = p;
|
|
6185
|
+
max = c;
|
|
6186
|
+
}
|
|
4971
6187
|
}
|
|
6188
|
+
return best;
|
|
4972
6189
|
}
|
|
4973
6190
|
async function runInit(options) {
|
|
4974
6191
|
const { rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection } = options;
|
|
4975
|
-
const agentsDir =
|
|
6192
|
+
const agentsDir = join22(rootDir, AGENTS_DIR);
|
|
4976
6193
|
const totalSteps = 4;
|
|
4977
6194
|
const s1 = createSpinner(step(1, totalSteps, "Creating canonical files..."));
|
|
4978
6195
|
s1.start();
|
|
4979
|
-
await
|
|
6196
|
+
await mkdir7(agentsDir, { recursive: true });
|
|
4980
6197
|
const existingManifest = await readManifest(rootDir);
|
|
4981
|
-
const index = await buildContentIndex(
|
|
4982
|
-
await copySelectedContent(
|
|
6198
|
+
const index = await buildContentIndex(CONTENT_ROOT2);
|
|
6199
|
+
await copySelectedContent(CONTENT_ROOT2, agentsDir, contentSelection, index);
|
|
4983
6200
|
if (existingManifest?.content) {
|
|
4984
6201
|
const oldIds = getAllContentIds(existingManifest.content);
|
|
4985
6202
|
const newIds = getAllContentIds(contentSelection);
|
|
@@ -4990,10 +6207,10 @@ async function runInit(options) {
|
|
|
4990
6207
|
}
|
|
4991
6208
|
}
|
|
4992
6209
|
}
|
|
4993
|
-
await
|
|
4994
|
-
const mcpPath =
|
|
6210
|
+
await mkdir7(join22(agentsDir, "learnings"), { recursive: true });
|
|
6211
|
+
const mcpPath = join22(agentsDir, "mcp", "mcp.json");
|
|
4995
6212
|
try {
|
|
4996
|
-
const mcpRaw = await
|
|
6213
|
+
const mcpRaw = await readFile18(mcpPath, "utf-8");
|
|
4997
6214
|
const mcpParsed = JSON.parse(mcpRaw);
|
|
4998
6215
|
if (mcpParsed.mcpServers) {
|
|
4999
6216
|
const selected = new Set(mcpServers);
|
|
@@ -5015,7 +6232,7 @@ async function runInit(options) {
|
|
|
5015
6232
|
if (!isExpected) throw err;
|
|
5016
6233
|
}
|
|
5017
6234
|
const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
|
|
5018
|
-
await safeWriteFile(
|
|
6235
|
+
await safeWriteFile(join22(agentsDir, "AGENTS.md"), canonicalAgentsMd, { force: true });
|
|
5019
6236
|
s1.succeed(step(1, totalSteps, `Canonical files created (${countSelectionItems(contentSelection)} items)`));
|
|
5020
6237
|
const s2 = createSpinner(step(2, totalSteps, "Writing manifest..."));
|
|
5021
6238
|
s2.start();
|
|
@@ -5026,7 +6243,7 @@ async function runInit(options) {
|
|
|
5026
6243
|
step(3, totalSteps, `Generating ${tools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")} output...`)
|
|
5027
6244
|
);
|
|
5028
6245
|
s3.start();
|
|
5029
|
-
await safeWriteFile(
|
|
6246
|
+
await safeWriteFile(join22(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
|
|
5030
6247
|
managedContent: AGENTS_MD_INNER,
|
|
5031
6248
|
appendIfNoBlock: true
|
|
5032
6249
|
});
|
|
@@ -5040,7 +6257,7 @@ async function runInit(options) {
|
|
|
5040
6257
|
warn(w);
|
|
5041
6258
|
}
|
|
5042
6259
|
for (const out of outputs) {
|
|
5043
|
-
await safeWriteFile(
|
|
6260
|
+
await safeWriteFile(join22(rootDir, out.path), out.content, {
|
|
5044
6261
|
managedContent: out.managedContent,
|
|
5045
6262
|
appendIfNoBlock: true
|
|
5046
6263
|
});
|
|
@@ -5059,7 +6276,7 @@ async function runInit(options) {
|
|
|
5059
6276
|
}
|
|
5060
6277
|
if (adapterFailures.length === tools.length) {
|
|
5061
6278
|
s3.fail(step(3, totalSteps, "All adapters failed"));
|
|
5062
|
-
throw new HatchError("All adapters failed", 1);
|
|
6279
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
5063
6280
|
}
|
|
5064
6281
|
}
|
|
5065
6282
|
s3.succeed(step(3, totalSteps, adapterFailures.length > 0 ? `Adapter output generated (${adapterFailures.length} failed)` : "Adapter output generated"));
|
|
@@ -5077,7 +6294,7 @@ async function runInit(options) {
|
|
|
5077
6294
|
if (manifest.worktree?.enabled) {
|
|
5078
6295
|
const wtContent = await generateWorktreeInclude(manifest, rootDir);
|
|
5079
6296
|
const wtManaged = extractManagedContent(wtContent);
|
|
5080
|
-
await safeWriteFile(
|
|
6297
|
+
await safeWriteFile(join22(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
|
|
5081
6298
|
managedContent: wtManaged,
|
|
5082
6299
|
appendIfNoBlock: true
|
|
5083
6300
|
});
|
|
@@ -5125,22 +6342,21 @@ async function runInit(options) {
|
|
|
5125
6342
|
const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
|
|
5126
6343
|
summaryLines.push("");
|
|
5127
6344
|
if (isGreenfield) {
|
|
5128
|
-
summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold("
|
|
6345
|
+
summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold(formatCommandHint(tools, "project-spec"))} to define your new project`);
|
|
5129
6346
|
} else {
|
|
5130
|
-
summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold("
|
|
6347
|
+
summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold(formatCommandHint(tools, "codebase-map"))} to map your existing codebase`);
|
|
5131
6348
|
}
|
|
5132
|
-
printBox("Hatch complete", summaryLines, "success");
|
|
5133
6349
|
if (envResult && envResult.newVars.length > 0) {
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
);
|
|
5137
|
-
info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
|
|
6350
|
+
summaryLines.push("");
|
|
6351
|
+
summaryLines.push(`${chalk6.yellow("!")} Add your secrets to ${chalk6.bold(".env.mcp")}: ${envResult.newVars.join(", ")}`);
|
|
6352
|
+
summaryLines.push(` Then run: ${chalk6.dim(getSourceEnvMcpCommand())}`);
|
|
5138
6353
|
}
|
|
6354
|
+
printBox("Hatch complete", summaryLines, "success");
|
|
5139
6355
|
}
|
|
5140
6356
|
async function checkExisting(rootDir, skipPrompt, newSelection) {
|
|
5141
|
-
const hatchJsonPath =
|
|
6357
|
+
const hatchJsonPath = join22(rootDir, AGENTS_DIR, "hatch.json");
|
|
5142
6358
|
try {
|
|
5143
|
-
await
|
|
6359
|
+
await access7(hatchJsonPath);
|
|
5144
6360
|
if (!skipPrompt) {
|
|
5145
6361
|
let message = "Existing .agents/ found. This will overwrite managed files. Continue?";
|
|
5146
6362
|
if (newSelection) {
|
|
@@ -5180,13 +6396,40 @@ function validateFlag(value, valid, fallback, name) {
|
|
|
5180
6396
|
if (!value) return fallback;
|
|
5181
6397
|
if (!valid.includes(value)) {
|
|
5182
6398
|
error(`Invalid --${name}: "${value}". Valid: ${valid.join(", ")}`);
|
|
5183
|
-
throw new HatchError(`Invalid --${name}: "${value}"`, 1);
|
|
6399
|
+
throw new HatchError(`Invalid --${name}: "${value}"`, 1, "VALIDATION_ERROR");
|
|
5184
6400
|
}
|
|
5185
6401
|
return value;
|
|
5186
6402
|
}
|
|
5187
6403
|
async function initCommand(opts = {}) {
|
|
5188
6404
|
printBanner();
|
|
5189
6405
|
const rootDir = process.cwd();
|
|
6406
|
+
if (!opts.workspace) {
|
|
6407
|
+
const suggestWs = await shouldSuggestWorkspace(rootDir);
|
|
6408
|
+
if (suggestWs) {
|
|
6409
|
+
const detectedRepos = await detectSubRepos(rootDir);
|
|
6410
|
+
if (opts.yes) {
|
|
6411
|
+
opts.workspace = true;
|
|
6412
|
+
info(chalk6.dim(`No git repo found. ${detectedRepos.length} git repo(s) detected in subdirectories \u2014 initializing as workspace.`));
|
|
6413
|
+
} else {
|
|
6414
|
+
info(`No git repo found, but ${detectedRepos.length} git repo(s) detected in subdirectories.`);
|
|
6415
|
+
const { useWorkspace } = await inquirer3.prompt([
|
|
6416
|
+
{
|
|
6417
|
+
type: "confirm",
|
|
6418
|
+
name: "useWorkspace",
|
|
6419
|
+
message: "Initialize as a multi-repo workspace?",
|
|
6420
|
+
default: true
|
|
6421
|
+
}
|
|
6422
|
+
]);
|
|
6423
|
+
opts.workspace = useWorkspace;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
if (opts.workspace) {
|
|
6428
|
+
const detectedRepos = await detectSubRepos(rootDir);
|
|
6429
|
+
const repoInfo2 = await analyzeRepo(rootDir);
|
|
6430
|
+
await runWorkspaceInit(rootDir, detectedRepos, repoInfo2, opts);
|
|
6431
|
+
return;
|
|
6432
|
+
}
|
|
5190
6433
|
const detectSpinner = createSpinner("Detecting repository...");
|
|
5191
6434
|
detectSpinner.start();
|
|
5192
6435
|
const repoInfo = await analyzeRepo(rootDir);
|
|
@@ -5217,7 +6460,7 @@ async function initCommand(opts = {}) {
|
|
|
5217
6460
|
if (invalid.length > 0) {
|
|
5218
6461
|
error(`Invalid tool(s): ${invalid.join(", ")}`);
|
|
5219
6462
|
console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
|
|
5220
|
-
throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
|
|
6463
|
+
throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
|
|
5221
6464
|
}
|
|
5222
6465
|
tools2 = rawTools;
|
|
5223
6466
|
} else if (repoInfo.existingTools.length > 0) {
|
|
@@ -5234,12 +6477,13 @@ async function initCommand(opts = {}) {
|
|
|
5234
6477
|
const projectType2 = validateFlag(opts.projectType, ["greenfield", "brownfield"], isGreenfield ? "greenfield" : "brownfield", "project-type");
|
|
5235
6478
|
const teamSize2 = validateFlag(opts.teamSize, ["solo", "team"], "solo", "team-size");
|
|
5236
6479
|
const preset = getPreset(presetId);
|
|
5237
|
-
const index = await buildContentIndex(
|
|
6480
|
+
const index = await buildContentIndex(CONTENT_ROOT2);
|
|
5238
6481
|
const contentSelection2 = resolveSelection(preset, projectType2, teamSize2, index);
|
|
5239
6482
|
const orchWarnings2 = validateOrchestrationDependencies(contentSelection2);
|
|
5240
6483
|
for (const w of orchWarnings2) {
|
|
5241
6484
|
warn(w);
|
|
5242
6485
|
}
|
|
6486
|
+
warnBoardPrerequisites(contentSelection2);
|
|
5243
6487
|
await checkExisting(rootDir, true, contentSelection2);
|
|
5244
6488
|
await runInit({ rootDir, platform: platform2, owner: owner2, repo: repo2, namespace: namespace2, project: project2, defaultBranch: defaultBranch2, tools: tools2, features: features2, mcpServers: mcpServers2, repoInfo, contentSelection: contentSelection2 });
|
|
5245
6489
|
return;
|
|
@@ -5304,42 +6548,53 @@ async function initCommand(opts = {}) {
|
|
|
5304
6548
|
}
|
|
5305
6549
|
]);
|
|
5306
6550
|
const defaultBranch = defaultBranchAnswers.defaultBranch.trim() || defaultBranchDefault;
|
|
6551
|
+
const filterIndex = await buildContentIndex(CONTENT_ROOT2);
|
|
5307
6552
|
const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
|
|
6553
|
+
const greenfieldExcl = countProjectTypeExclusions("greenfield", filterIndex.items);
|
|
6554
|
+
const brownfieldExcl = countProjectTypeExclusions("brownfield", filterIndex.items);
|
|
5308
6555
|
const projectTypeAnswer = await inquirer3.prompt([
|
|
5309
6556
|
{
|
|
5310
6557
|
type: "list",
|
|
5311
6558
|
name: "projectType",
|
|
5312
6559
|
message: "Is this a new (greenfield) or existing (brownfield) project?",
|
|
5313
6560
|
choices: [
|
|
5314
|
-
{ name:
|
|
5315
|
-
{ name:
|
|
6561
|
+
{ name: `Greenfield \u2014 new project from scratch${greenfieldExcl > 0 ? ` (filters out ${greenfieldExcl} brownfield-only item${greenfieldExcl === 1 ? "" : "s"})` : ""}`, value: "greenfield" },
|
|
6562
|
+
{ name: `Brownfield \u2014 existing codebase${brownfieldExcl > 0 ? ` (filters out ${brownfieldExcl} greenfield-only item${brownfieldExcl === 1 ? "" : "s"})` : ""}`, value: "brownfield" }
|
|
5316
6563
|
],
|
|
5317
6564
|
default: isAutoGreenfield ? "greenfield" : "brownfield"
|
|
5318
6565
|
}
|
|
5319
6566
|
]);
|
|
5320
6567
|
const projectType = projectTypeAnswer.projectType;
|
|
6568
|
+
const soloExcl = countTeamSizeExclusions("solo", filterIndex.items);
|
|
5321
6569
|
const teamSizeAnswer = await inquirer3.prompt([
|
|
5322
6570
|
{
|
|
5323
6571
|
type: "list",
|
|
5324
6572
|
name: "teamSize",
|
|
5325
6573
|
message: "Solo developer or team collaboration?",
|
|
5326
6574
|
choices: [
|
|
5327
|
-
{ name:
|
|
6575
|
+
{ name: `Solo \u2014 just me${soloExcl > 0 ? ` (filters out ${soloExcl} team-only item${soloExcl === 1 ? "" : "s"})` : ""}`, value: "solo" },
|
|
5328
6576
|
{ name: "Team \u2014 multiple contributors", value: "team" }
|
|
5329
6577
|
],
|
|
5330
6578
|
default: "solo"
|
|
5331
6579
|
}
|
|
5332
6580
|
]);
|
|
5333
6581
|
const teamSize = teamSizeAnswer.teamSize;
|
|
6582
|
+
const totalItems = filterIndex.items.length;
|
|
5334
6583
|
const presetAnswer = await inquirer3.prompt([
|
|
5335
6584
|
{
|
|
5336
6585
|
type: "list",
|
|
5337
6586
|
name: "preset",
|
|
5338
6587
|
message: "Select content profile:",
|
|
5339
|
-
choices: PRESETS.map((p) =>
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
6588
|
+
choices: PRESETS.map((p) => {
|
|
6589
|
+
const excluded = countPresetExclusions(p, filterIndex);
|
|
6590
|
+
const estimated = p.id !== "custom" ? estimatePresetItemCount(p, projectType, teamSize, filterIndex) : 0;
|
|
6591
|
+
const countHint = estimated > 0 ? ` (~${estimated} items)` : "";
|
|
6592
|
+
const suffix = excluded > 0 ? ` (excludes ${excluded} of ${totalItems})` : "";
|
|
6593
|
+
return {
|
|
6594
|
+
name: `${p.name} \u2014 ${p.description}${countHint}${suffix}`,
|
|
6595
|
+
value: p.id
|
|
6596
|
+
};
|
|
6597
|
+
}),
|
|
5343
6598
|
default: "standard"
|
|
5344
6599
|
}
|
|
5345
6600
|
]);
|
|
@@ -5347,23 +6602,46 @@ async function initCommand(opts = {}) {
|
|
|
5347
6602
|
const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
|
|
5348
6603
|
let customSelections;
|
|
5349
6604
|
if (selectedPreset.id === "custom") {
|
|
5350
|
-
const
|
|
6605
|
+
const contentIndex = filterIndex;
|
|
5351
6606
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
5352
|
-
for (const item of
|
|
6607
|
+
for (const item of contentIndex.items) {
|
|
5353
6608
|
const primaryTag = item.tags[0] ?? "other";
|
|
5354
6609
|
if (!tagGroups.has(primaryTag)) tagGroups.set(primaryTag, []);
|
|
5355
6610
|
tagGroups.get(primaryTag).push(item);
|
|
5356
6611
|
}
|
|
6612
|
+
const TAG_LABELS = {
|
|
6613
|
+
core: "Core",
|
|
6614
|
+
planning: "Planning",
|
|
6615
|
+
implementation: "Implementation",
|
|
6616
|
+
review: "Review",
|
|
6617
|
+
devops: "DevOps",
|
|
6618
|
+
maintenance: "Maintenance",
|
|
6619
|
+
greenfield: "Greenfield",
|
|
6620
|
+
brownfield: "Brownfield",
|
|
6621
|
+
board: "Board",
|
|
6622
|
+
security: "Security",
|
|
6623
|
+
a11y: "Accessibility",
|
|
6624
|
+
performance: "Performance",
|
|
6625
|
+
customize: "Customization",
|
|
6626
|
+
other: "Other"
|
|
6627
|
+
};
|
|
6628
|
+
const groupedChoices = [];
|
|
6629
|
+
for (const [tag, items] of tagGroups) {
|
|
6630
|
+
groupedChoices.push(new inquirer3.Separator(`\u2500\u2500 ${TAG_LABELS[tag] ?? tag} (${items.length}) \u2500\u2500`));
|
|
6631
|
+
for (const item of items) {
|
|
6632
|
+
groupedChoices.push({
|
|
6633
|
+
name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
|
|
6634
|
+
value: item.id,
|
|
6635
|
+
checked: item.protected || item.tags.includes("core")
|
|
6636
|
+
});
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
5357
6639
|
const customAnswer = await inquirer3.prompt([
|
|
5358
6640
|
{
|
|
5359
6641
|
type: "checkbox",
|
|
5360
6642
|
name: "items",
|
|
5361
6643
|
message: "Select content items:",
|
|
5362
|
-
choices:
|
|
5363
|
-
name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
|
|
5364
|
-
value: item.id,
|
|
5365
|
-
checked: item.protected || item.tags.includes("core")
|
|
5366
|
-
})),
|
|
6644
|
+
choices: groupedChoices,
|
|
5367
6645
|
...wslTheme && { theme: wslTheme }
|
|
5368
6646
|
}
|
|
5369
6647
|
]);
|
|
@@ -5381,11 +6659,18 @@ async function initCommand(opts = {}) {
|
|
|
5381
6659
|
}
|
|
5382
6660
|
]);
|
|
5383
6661
|
const tools = toolAnswers.tools.length > 0 ? toolAnswers.tools : DEFAULT_TOOLS;
|
|
6662
|
+
const secretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
|
|
6663
|
+
if (secretNotes.length > 0) {
|
|
6664
|
+
info(chalk6.dim("MCP secret loading by tool:"));
|
|
6665
|
+
for (const note of secretNotes) {
|
|
6666
|
+
info(chalk6.dim(` ${note}`));
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
5384
6669
|
const featureAnswers = await inquirer3.prompt([
|
|
5385
6670
|
{
|
|
5386
6671
|
type: "checkbox",
|
|
5387
6672
|
name: "features",
|
|
5388
|
-
message: "Select features:",
|
|
6673
|
+
message: "Select features (MCP provides tool-server integration):",
|
|
5389
6674
|
choices: FEATURE_CHOICES,
|
|
5390
6675
|
default: DEFAULT_FEATURE_KEYS,
|
|
5391
6676
|
...wslTheme && { theme: wslTheme }
|
|
@@ -5417,35 +6702,380 @@ async function initCommand(opts = {}) {
|
|
|
5417
6702
|
mcpServers.unshift(platformMcp);
|
|
5418
6703
|
}
|
|
5419
6704
|
}
|
|
5420
|
-
const
|
|
5421
|
-
const contentSelection = resolveSelection(selectedPreset, projectType, teamSize, contentIndex, customSelections);
|
|
6705
|
+
const contentSelection = resolveSelection(selectedPreset, projectType, teamSize, filterIndex, customSelections);
|
|
5422
6706
|
const orchWarnings = validateOrchestrationDependencies(contentSelection);
|
|
5423
6707
|
for (const w of orchWarnings) {
|
|
5424
6708
|
warn(w);
|
|
5425
6709
|
}
|
|
6710
|
+
warnBoardPrerequisites(contentSelection);
|
|
5426
6711
|
await checkExisting(rootDir, false, contentSelection);
|
|
5427
6712
|
await runInit({ rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection });
|
|
5428
6713
|
}
|
|
6714
|
+
async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
|
|
6715
|
+
const headless = !!opts.yes;
|
|
6716
|
+
console.log();
|
|
6717
|
+
const wsSpinner = createSpinner("Detecting workspace repos...");
|
|
6718
|
+
wsSpinner.start();
|
|
6719
|
+
if (detectedRepos.length === 0) {
|
|
6720
|
+
wsSpinner.succeed("Workspace created (no sub-repos found)");
|
|
6721
|
+
const platform2 = "github";
|
|
6722
|
+
const tools2 = resolveToolsFromOpts(opts.tools, repoInfo);
|
|
6723
|
+
const features2 = { ...DEFAULT_FEATURES };
|
|
6724
|
+
const platformMcp = PLATFORM_MCP_SERVER[platform2];
|
|
6725
|
+
const mcpServers2 = features2.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
|
|
6726
|
+
const index = await buildContentIndex(CONTENT_ROOT2);
|
|
6727
|
+
const contentSelection2 = resolveSelection(getPreset("standard"), "brownfield", "solo", index);
|
|
6728
|
+
const wsManifest2 = createWorkspaceManifest(
|
|
6729
|
+
basename2(rootDir) || "workspace",
|
|
6730
|
+
{ platform: platform2, tools: tools2, features: features2, mcp: { servers: mcpServers2 }, content: contentSelection2 },
|
|
6731
|
+
[],
|
|
6732
|
+
"manual"
|
|
6733
|
+
);
|
|
6734
|
+
await writeWorkspaceManifest(rootDir, wsManifest2);
|
|
6735
|
+
return;
|
|
6736
|
+
}
|
|
6737
|
+
const enriched = detectedRepos.map((r) => ({
|
|
6738
|
+
...r,
|
|
6739
|
+
...detectRepoGitIdentity(join22(rootDir, r.path))
|
|
6740
|
+
}));
|
|
6741
|
+
wsSpinner.succeed(`Workspace: ${detectedRepos.length} repo(s) detected`);
|
|
6742
|
+
console.log();
|
|
6743
|
+
console.log(chalk6.dim(" Repo Platform Owner/Repo Branch"));
|
|
6744
|
+
for (const r of enriched) {
|
|
6745
|
+
const name = (r.name ?? r.path).padEnd(16);
|
|
6746
|
+
if (r.owner && r.repo) {
|
|
6747
|
+
const platLabel = PLATFORM_DISPLAY_NAMES[r.platform].padEnd(14);
|
|
6748
|
+
const identity = `${r.owner}/${r.repo}`.padEnd(32);
|
|
6749
|
+
console.log(` ${name}${chalk6.dim(platLabel)}${chalk6.dim(identity)}${chalk6.dim(r.defaultBranch)}`);
|
|
6750
|
+
} else {
|
|
6751
|
+
console.log(` ${name}${chalk6.dim("(no remote detected)")}`);
|
|
6752
|
+
}
|
|
6753
|
+
}
|
|
6754
|
+
console.log();
|
|
6755
|
+
if (!headless) {
|
|
6756
|
+
const { acceptIdentity } = await inquirer3.prompt([
|
|
6757
|
+
{
|
|
6758
|
+
type: "confirm",
|
|
6759
|
+
name: "acceptIdentity",
|
|
6760
|
+
message: "Accept detected repo identities?",
|
|
6761
|
+
default: true
|
|
6762
|
+
}
|
|
6763
|
+
]);
|
|
6764
|
+
if (!acceptIdentity) {
|
|
6765
|
+
for (const r of enriched) {
|
|
6766
|
+
console.log(chalk6.bold(`
|
|
6767
|
+
${r.name ?? r.path}:`));
|
|
6768
|
+
const identity = await inquirer3.prompt([
|
|
6769
|
+
{ type: "input", name: "owner", message: " Owner:", default: r.owner || void 0 },
|
|
6770
|
+
{ type: "input", name: "repo", message: " Repo:", default: r.repo || void 0 },
|
|
6771
|
+
{ type: "input", name: "defaultBranch", message: " Default branch:", default: r.defaultBranch || "main" }
|
|
6772
|
+
]);
|
|
6773
|
+
r.owner = sanitizeInput(identity.owner);
|
|
6774
|
+
r.repo = sanitizeInput(identity.repo);
|
|
6775
|
+
r.defaultBranch = identity.defaultBranch.trim() || "main";
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
const platform = deriveWorkspacePlatform(enriched);
|
|
6780
|
+
let tools;
|
|
6781
|
+
let features;
|
|
6782
|
+
let mcpServers;
|
|
6783
|
+
let contentSelection;
|
|
6784
|
+
if (headless) {
|
|
6785
|
+
tools = resolveToolsFromOpts(opts.tools, repoInfo);
|
|
6786
|
+
features = { ...DEFAULT_FEATURES };
|
|
6787
|
+
const platformMcp = PLATFORM_MCP_SERVER[platform];
|
|
6788
|
+
mcpServers = features.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
|
|
6789
|
+
const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
|
|
6790
|
+
const presetId = validateFlag(opts.preset, ["minimal", "standard", "full"], "standard", "preset");
|
|
6791
|
+
const projectType = validateFlag(opts.projectType, ["greenfield", "brownfield"], isGreenfield ? "greenfield" : "brownfield", "project-type");
|
|
6792
|
+
const teamSize = validateFlag(opts.teamSize, ["solo", "team"], "solo", "team-size");
|
|
6793
|
+
const preset = getPreset(presetId);
|
|
6794
|
+
const index = await buildContentIndex(CONTENT_ROOT2);
|
|
6795
|
+
contentSelection = resolveSelection(preset, projectType, teamSize, index);
|
|
6796
|
+
} else {
|
|
6797
|
+
const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
|
|
6798
|
+
const wsFilterIndex = await buildContentIndex(CONTENT_ROOT2);
|
|
6799
|
+
const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
|
|
6800
|
+
const wsGreenfieldExcl = countProjectTypeExclusions("greenfield", wsFilterIndex.items);
|
|
6801
|
+
const wsBrownfieldExcl = countProjectTypeExclusions("brownfield", wsFilterIndex.items);
|
|
6802
|
+
const projectTypeAnswer = await inquirer3.prompt([
|
|
6803
|
+
{
|
|
6804
|
+
type: "list",
|
|
6805
|
+
name: "projectType",
|
|
6806
|
+
message: "Is this a new (greenfield) or existing (brownfield) project?",
|
|
6807
|
+
choices: [
|
|
6808
|
+
{ name: `Greenfield \u2014 new project from scratch${wsGreenfieldExcl > 0 ? ` (filters out ${wsGreenfieldExcl} brownfield-only item${wsGreenfieldExcl === 1 ? "" : "s"})` : ""}`, value: "greenfield" },
|
|
6809
|
+
{ name: `Brownfield \u2014 existing codebase${wsBrownfieldExcl > 0 ? ` (filters out ${wsBrownfieldExcl} greenfield-only item${wsBrownfieldExcl === 1 ? "" : "s"})` : ""}`, value: "brownfield" }
|
|
6810
|
+
],
|
|
6811
|
+
default: isAutoGreenfield ? "greenfield" : "brownfield"
|
|
6812
|
+
}
|
|
6813
|
+
]);
|
|
6814
|
+
const projectType = projectTypeAnswer.projectType;
|
|
6815
|
+
const wsSoloExcl = countTeamSizeExclusions("solo", wsFilterIndex.items);
|
|
6816
|
+
const teamSizeAnswer = await inquirer3.prompt([
|
|
6817
|
+
{
|
|
6818
|
+
type: "list",
|
|
6819
|
+
name: "teamSize",
|
|
6820
|
+
message: "Solo developer or team collaboration?",
|
|
6821
|
+
choices: [
|
|
6822
|
+
{ name: `Solo \u2014 just me${wsSoloExcl > 0 ? ` (filters out ${wsSoloExcl} team-only item${wsSoloExcl === 1 ? "" : "s"})` : ""}`, value: "solo" },
|
|
6823
|
+
{ name: "Team \u2014 multiple contributors", value: "team" }
|
|
6824
|
+
],
|
|
6825
|
+
default: "solo"
|
|
6826
|
+
}
|
|
6827
|
+
]);
|
|
6828
|
+
const teamSize = teamSizeAnswer.teamSize;
|
|
6829
|
+
const wsTotalItems = wsFilterIndex.items.length;
|
|
6830
|
+
const presetAnswer = await inquirer3.prompt([
|
|
6831
|
+
{
|
|
6832
|
+
type: "list",
|
|
6833
|
+
name: "preset",
|
|
6834
|
+
message: "Select content profile:",
|
|
6835
|
+
choices: PRESETS.map((p) => {
|
|
6836
|
+
const excluded = countPresetExclusions(p, wsFilterIndex);
|
|
6837
|
+
const wsEstimated = p.id !== "custom" ? estimatePresetItemCount(p, projectType, teamSize, wsFilterIndex) : 0;
|
|
6838
|
+
const wsCountHint = wsEstimated > 0 ? ` (~${wsEstimated} items)` : "";
|
|
6839
|
+
const suffix = excluded > 0 ? ` (excludes ${excluded} of ${wsTotalItems})` : "";
|
|
6840
|
+
return {
|
|
6841
|
+
name: `${p.name} \u2014 ${p.description}${wsCountHint}${suffix}`,
|
|
6842
|
+
value: p.id
|
|
6843
|
+
};
|
|
6844
|
+
}),
|
|
6845
|
+
default: "standard"
|
|
6846
|
+
}
|
|
6847
|
+
]);
|
|
6848
|
+
const selectedPreset = getPreset(presetAnswer.preset);
|
|
6849
|
+
let customSelections;
|
|
6850
|
+
if (selectedPreset.id === "custom") {
|
|
6851
|
+
const contentIndex = wsFilterIndex;
|
|
6852
|
+
const wsTagGroups = /* @__PURE__ */ new Map();
|
|
6853
|
+
for (const item of contentIndex.items) {
|
|
6854
|
+
const primaryTag = item.tags[0] ?? "other";
|
|
6855
|
+
if (!wsTagGroups.has(primaryTag)) wsTagGroups.set(primaryTag, []);
|
|
6856
|
+
wsTagGroups.get(primaryTag).push(item);
|
|
6857
|
+
}
|
|
6858
|
+
const WS_TAG_LABELS = {
|
|
6859
|
+
core: "Core",
|
|
6860
|
+
planning: "Planning",
|
|
6861
|
+
implementation: "Implementation",
|
|
6862
|
+
review: "Review",
|
|
6863
|
+
devops: "DevOps",
|
|
6864
|
+
maintenance: "Maintenance",
|
|
6865
|
+
greenfield: "Greenfield",
|
|
6866
|
+
brownfield: "Brownfield",
|
|
6867
|
+
board: "Board",
|
|
6868
|
+
security: "Security",
|
|
6869
|
+
a11y: "Accessibility",
|
|
6870
|
+
performance: "Performance",
|
|
6871
|
+
customize: "Customization",
|
|
6872
|
+
other: "Other"
|
|
6873
|
+
};
|
|
6874
|
+
const wsGroupedChoices = [];
|
|
6875
|
+
for (const [tag, items] of wsTagGroups) {
|
|
6876
|
+
wsGroupedChoices.push(new inquirer3.Separator(`\u2500\u2500 ${WS_TAG_LABELS[tag] ?? tag} (${items.length}) \u2500\u2500`));
|
|
6877
|
+
for (const item of items) {
|
|
6878
|
+
wsGroupedChoices.push({
|
|
6879
|
+
name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
|
|
6880
|
+
value: item.id,
|
|
6881
|
+
checked: item.protected || item.tags.includes("core")
|
|
6882
|
+
});
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
const customAnswer = await inquirer3.prompt([
|
|
6886
|
+
{
|
|
6887
|
+
type: "checkbox",
|
|
6888
|
+
name: "items",
|
|
6889
|
+
message: "Select content items:",
|
|
6890
|
+
choices: wsGroupedChoices,
|
|
6891
|
+
...wslTheme && { theme: wslTheme }
|
|
6892
|
+
}
|
|
6893
|
+
]);
|
|
6894
|
+
customSelections = customAnswer.items;
|
|
6895
|
+
}
|
|
6896
|
+
const toolDefaults = repoInfo.existingTools.length > 0 ? repoInfo.existingTools : DEFAULT_TOOLS;
|
|
6897
|
+
const toolAnswers = await inquirer3.prompt([
|
|
6898
|
+
{
|
|
6899
|
+
type: "checkbox",
|
|
6900
|
+
name: "tools",
|
|
6901
|
+
message: "Select tools to configure:",
|
|
6902
|
+
choices: TOOL_PROMPT_CHOICES,
|
|
6903
|
+
default: toolDefaults,
|
|
6904
|
+
...wslTheme && { theme: wslTheme }
|
|
6905
|
+
}
|
|
6906
|
+
]);
|
|
6907
|
+
tools = toolAnswers.tools.length > 0 ? toolAnswers.tools : DEFAULT_TOOLS;
|
|
6908
|
+
const wsSecretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
|
|
6909
|
+
if (wsSecretNotes.length > 0) {
|
|
6910
|
+
info(chalk6.dim("MCP secret loading by tool:"));
|
|
6911
|
+
for (const note of wsSecretNotes) {
|
|
6912
|
+
info(chalk6.dim(` ${note}`));
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
const featureAnswers = await inquirer3.prompt([
|
|
6916
|
+
{
|
|
6917
|
+
type: "checkbox",
|
|
6918
|
+
name: "features",
|
|
6919
|
+
message: "Select features:",
|
|
6920
|
+
choices: FEATURE_CHOICES,
|
|
6921
|
+
default: DEFAULT_FEATURE_KEYS,
|
|
6922
|
+
...wslTheme && { theme: wslTheme }
|
|
6923
|
+
}
|
|
6924
|
+
]);
|
|
6925
|
+
const selectedFeatures = featureAnswers.features;
|
|
6926
|
+
features = { ...DEFAULT_FEATURES };
|
|
6927
|
+
for (const k of Object.keys(features)) {
|
|
6928
|
+
features[k] = selectedFeatures.includes(k);
|
|
6929
|
+
}
|
|
6930
|
+
mcpServers = [];
|
|
6931
|
+
if (features.mcp) {
|
|
6932
|
+
const platformMcp = PLATFORM_MCP_SERVER[platform];
|
|
6933
|
+
const defaultMcpForPlatform = Array.from(
|
|
6934
|
+
/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])
|
|
6935
|
+
);
|
|
6936
|
+
const mcpAnswers = await inquirer3.prompt([
|
|
6937
|
+
{
|
|
6938
|
+
type: "checkbox",
|
|
6939
|
+
name: "mcp",
|
|
6940
|
+
message: "Select MCP servers:",
|
|
6941
|
+
choices: MCP_CHOICES,
|
|
6942
|
+
default: defaultMcpForPlatform,
|
|
6943
|
+
...wslTheme && { theme: wslTheme }
|
|
6944
|
+
}
|
|
6945
|
+
]);
|
|
6946
|
+
mcpServers = mcpAnswers.mcp ?? [];
|
|
6947
|
+
if (!mcpServers.includes(platformMcp)) {
|
|
6948
|
+
mcpServers.unshift(platformMcp);
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
contentSelection = resolveSelection(selectedPreset, projectType, teamSize, wsFilterIndex, customSelections);
|
|
6952
|
+
}
|
|
6953
|
+
const orchWarnings = validateOrchestrationDependencies(contentSelection);
|
|
6954
|
+
for (const w of orchWarnings) {
|
|
6955
|
+
warn(w);
|
|
6956
|
+
}
|
|
6957
|
+
warnBoardPrerequisites(contentSelection);
|
|
6958
|
+
await checkExisting(rootDir, headless, contentSelection);
|
|
6959
|
+
await runInit({
|
|
6960
|
+
rootDir,
|
|
6961
|
+
platform,
|
|
6962
|
+
owner: "",
|
|
6963
|
+
repo: "",
|
|
6964
|
+
namespace: "",
|
|
6965
|
+
project: "",
|
|
6966
|
+
defaultBranch: "",
|
|
6967
|
+
tools,
|
|
6968
|
+
features,
|
|
6969
|
+
mcpServers,
|
|
6970
|
+
repoInfo,
|
|
6971
|
+
contentSelection
|
|
6972
|
+
});
|
|
6973
|
+
let repoEntries;
|
|
6974
|
+
if (headless) {
|
|
6975
|
+
repoEntries = enriched.map((r) => ({
|
|
6976
|
+
path: r.path,
|
|
6977
|
+
name: r.name,
|
|
6978
|
+
sync: false,
|
|
6979
|
+
owner: r.owner || void 0,
|
|
6980
|
+
repo: r.repo || void 0,
|
|
6981
|
+
defaultBranch: r.defaultBranch || void 0,
|
|
6982
|
+
platform: r.platform || void 0
|
|
6983
|
+
}));
|
|
6984
|
+
} else {
|
|
6985
|
+
const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
|
|
6986
|
+
const { syncRepos } = await inquirer3.prompt([
|
|
6987
|
+
{
|
|
6988
|
+
type: "checkbox",
|
|
6989
|
+
name: "syncRepos",
|
|
6990
|
+
message: "Select repos to sync workspace content to:",
|
|
6991
|
+
choices: enriched.map((r) => ({
|
|
6992
|
+
name: `${r.name}${r.hasHatch3r ? chalk6.dim(" (has existing hatch3r)") : ""}`,
|
|
6993
|
+
value: r.path,
|
|
6994
|
+
checked: false
|
|
6995
|
+
})),
|
|
6996
|
+
...wslTheme && { theme: wslTheme }
|
|
6997
|
+
}
|
|
6998
|
+
]);
|
|
6999
|
+
const syncSet = new Set(syncRepos);
|
|
7000
|
+
repoEntries = enriched.map((r) => ({
|
|
7001
|
+
path: r.path,
|
|
7002
|
+
name: r.name,
|
|
7003
|
+
sync: syncSet.has(r.path),
|
|
7004
|
+
owner: r.owner || void 0,
|
|
7005
|
+
repo: r.repo || void 0,
|
|
7006
|
+
defaultBranch: r.defaultBranch || void 0,
|
|
7007
|
+
platform: r.platform || void 0
|
|
7008
|
+
}));
|
|
7009
|
+
}
|
|
7010
|
+
const dirName = basename2(rootDir) || "workspace";
|
|
7011
|
+
const wsManifest = createWorkspaceManifest(
|
|
7012
|
+
dirName,
|
|
7013
|
+
{ platform, tools, features, mcp: { servers: mcpServers }, content: contentSelection },
|
|
7014
|
+
repoEntries,
|
|
7015
|
+
"manual"
|
|
7016
|
+
);
|
|
7017
|
+
await writeWorkspaceManifest(rootDir, wsManifest);
|
|
7018
|
+
const syncCount = repoEntries.filter((r) => r.sync).length;
|
|
7019
|
+
if (syncCount > 0) {
|
|
7020
|
+
const syncSpinner = createSpinner(`Syncing ${syncCount} repo(s)...`);
|
|
7021
|
+
syncSpinner.start();
|
|
7022
|
+
const result = await syncWorkspaceRepos(rootDir, {
|
|
7023
|
+
onWarn: (msg) => warn(msg)
|
|
7024
|
+
});
|
|
7025
|
+
const succeeded = result.repos.filter((r) => r.action === "synced").length;
|
|
7026
|
+
const failed = result.repos.filter((r) => r.action === "error").length;
|
|
7027
|
+
if (failed > 0) {
|
|
7028
|
+
syncSpinner.warn(`Workspace sync: ${succeeded} synced, ${failed} failed`);
|
|
7029
|
+
for (const r of result.repos.filter((r2) => r2.action === "error")) {
|
|
7030
|
+
error(` ${r.path}: ${r.error}`);
|
|
7031
|
+
}
|
|
7032
|
+
} else {
|
|
7033
|
+
syncSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
console.log();
|
|
7037
|
+
const wsLines = [
|
|
7038
|
+
label("Mode", "workspace"),
|
|
7039
|
+
label("Repos", `${repoEntries.length} registered, ${syncCount} synced`),
|
|
7040
|
+
label("Strategy", "manual (use hatch3r sync --repos to propagate)"),
|
|
7041
|
+
label("Manifest", `${AGENTS_DIR}/workspace.json`)
|
|
7042
|
+
];
|
|
7043
|
+
printBox("Workspace ready", wsLines, "success");
|
|
7044
|
+
}
|
|
7045
|
+
function resolveToolsFromOpts(toolsFlag, repoInfo) {
|
|
7046
|
+
if (toolsFlag) {
|
|
7047
|
+
const rawTools = toolsFlag.split(",").map((t) => t.trim());
|
|
7048
|
+
const invalid = rawTools.filter((t) => !VALID_TOOLS.has(t));
|
|
7049
|
+
if (invalid.length > 0) {
|
|
7050
|
+
error(`Invalid tool(s): ${invalid.join(", ")}`);
|
|
7051
|
+
console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
|
|
7052
|
+
throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
|
|
7053
|
+
}
|
|
7054
|
+
return rawTools;
|
|
7055
|
+
}
|
|
7056
|
+
if (repoInfo.existingTools.length > 0) return repoInfo.existingTools;
|
|
7057
|
+
return DEFAULT_TOOLS;
|
|
7058
|
+
}
|
|
5429
7059
|
|
|
5430
7060
|
// src/cli/commands/sync.ts
|
|
5431
|
-
import { stat as
|
|
5432
|
-
import { join as
|
|
7061
|
+
import { stat as stat4, readdir as readdir10 } from "fs/promises";
|
|
7062
|
+
import { join as join23 } from "path";
|
|
5433
7063
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
5434
7064
|
import chalk7 from "chalk";
|
|
5435
7065
|
async function checkSpecFreshness(rootDir) {
|
|
5436
|
-
const specsDir =
|
|
7066
|
+
const specsDir = join23(rootDir, "docs", "specs");
|
|
5437
7067
|
try {
|
|
5438
|
-
await
|
|
7068
|
+
await stat4(specsDir);
|
|
5439
7069
|
} catch {
|
|
5440
7070
|
return;
|
|
5441
7071
|
}
|
|
5442
7072
|
let oldestSpecMtime = Date.now();
|
|
5443
7073
|
try {
|
|
5444
|
-
const entries = await
|
|
7074
|
+
const entries = await readdir10(specsDir, { withFileTypes: true, recursive: true });
|
|
5445
7075
|
for (const entry of entries) {
|
|
5446
7076
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5447
7077
|
const parentPath = entry.parentPath ?? entry.path ?? specsDir;
|
|
5448
|
-
const fileStat = await
|
|
7078
|
+
const fileStat = await stat4(join23(parentPath, entry.name));
|
|
5449
7079
|
if (fileStat.mtimeMs < oldestSpecMtime) {
|
|
5450
7080
|
oldestSpecMtime = fileStat.mtimeMs;
|
|
5451
7081
|
}
|
|
@@ -5467,15 +7097,15 @@ async function checkSpecFreshness(rootDir) {
|
|
|
5467
7097
|
} catch {
|
|
5468
7098
|
}
|
|
5469
7099
|
}
|
|
5470
|
-
async function syncCommand() {
|
|
7100
|
+
async function syncCommand(opts = {}) {
|
|
5471
7101
|
printBanner(true);
|
|
5472
7102
|
const rootDir = process.cwd();
|
|
5473
|
-
const agentsDir =
|
|
7103
|
+
const agentsDir = join23(rootDir, AGENTS_DIR);
|
|
5474
7104
|
const manifest = await readManifest(rootDir);
|
|
5475
7105
|
if (!manifest) {
|
|
5476
7106
|
error("No .agents/hatch.json found.");
|
|
5477
7107
|
console.log(chalk7.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
5478
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
7108
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
5479
7109
|
}
|
|
5480
7110
|
const m = manifest;
|
|
5481
7111
|
const integrityResults = await verifyIntegrity(agentsDir);
|
|
@@ -5497,28 +7127,32 @@ async function syncCommand() {
|
|
|
5497
7127
|
let currentStep = 0;
|
|
5498
7128
|
const s1 = createSpinner(step(++currentStep, totalSteps, "Syncing AGENTS.md..."));
|
|
5499
7129
|
s1.start();
|
|
5500
|
-
const agentsMdResult = await safeWriteFile(
|
|
7130
|
+
const agentsMdResult = await safeWriteFile(join23(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
|
|
5501
7131
|
managedContent: AGENTS_MD_INNER
|
|
5502
7132
|
});
|
|
5503
7133
|
if (agentsMdResult.warning) warn(agentsMdResult.warning);
|
|
5504
7134
|
results.push({ path: "AGENTS.md", action: agentsMdResult.action });
|
|
5505
7135
|
const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
|
|
5506
|
-
const canonicalResult = await safeWriteFile(
|
|
7136
|
+
const canonicalResult = await safeWriteFile(join23(agentsDir, "AGENTS.md"), canonicalAgentsMd);
|
|
5507
7137
|
if (canonicalResult.warning) warn(canonicalResult.warning);
|
|
5508
7138
|
results.push({ path: `${AGENTS_DIR}/AGENTS.md`, action: canonicalResult.action });
|
|
5509
7139
|
s1.succeed(step(currentStep, totalSteps, "AGENTS.md synced"));
|
|
7140
|
+
const generationMode = opts.minimal ? "minimal" : "standard";
|
|
7141
|
+
if (opts.minimal) {
|
|
7142
|
+
info("Minimal generation mode: output will be stripped-down to reduce token usage.");
|
|
7143
|
+
}
|
|
5510
7144
|
const adapterFailures = [];
|
|
5511
7145
|
for (const tool of m.tools) {
|
|
5512
7146
|
const s = createSpinner(step(++currentStep, totalSteps, `Generating ${tool} output...`));
|
|
5513
7147
|
s.start();
|
|
5514
7148
|
try {
|
|
5515
7149
|
const adapter = getAdapter(tool);
|
|
5516
|
-
const outputs = await adapter.generate(agentsDir, m);
|
|
7150
|
+
const outputs = await adapter.generate(agentsDir, m, generationMode);
|
|
5517
7151
|
for (const w of adapter.warnings) {
|
|
5518
7152
|
warn(w);
|
|
5519
7153
|
}
|
|
5520
7154
|
for (const out of outputs) {
|
|
5521
|
-
const fullPath =
|
|
7155
|
+
const fullPath = join23(rootDir, out.path);
|
|
5522
7156
|
if (out.managedContent) {
|
|
5523
7157
|
const result = await safeWriteFile(fullPath, out.content, {
|
|
5524
7158
|
managedContent: out.managedContent
|
|
@@ -5545,7 +7179,7 @@ async function syncCommand() {
|
|
|
5545
7179
|
error(`Failed to generate ${f.tool}: ${f.error}`);
|
|
5546
7180
|
}
|
|
5547
7181
|
if (adapterFailures.length === m.tools.length) {
|
|
5548
|
-
throw new HatchError("All adapters failed", 1);
|
|
7182
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
5549
7183
|
}
|
|
5550
7184
|
}
|
|
5551
7185
|
for (const tool of m.tools) {
|
|
@@ -5558,7 +7192,7 @@ async function syncCommand() {
|
|
|
5558
7192
|
const wtContent = await generateWorktreeInclude(m, rootDir);
|
|
5559
7193
|
const wtManaged = extractManagedContent(wtContent);
|
|
5560
7194
|
const wtResult = await safeWriteFile(
|
|
5561
|
-
|
|
7195
|
+
join23(rootDir, WORKTREE_INCLUDE_FILE),
|
|
5562
7196
|
wtContent,
|
|
5563
7197
|
{ managedContent: wtManaged }
|
|
5564
7198
|
);
|
|
@@ -5578,6 +7212,9 @@ async function syncCommand() {
|
|
|
5578
7212
|
info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
|
|
5579
7213
|
}
|
|
5580
7214
|
}
|
|
7215
|
+
const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
|
|
7216
|
+
await writeIntegrityManifest(agentsDir, integrityManifest);
|
|
7217
|
+
await pruneArchives(rootDir);
|
|
5581
7218
|
await checkSpecFreshness(rootDir);
|
|
5582
7219
|
console.log();
|
|
5583
7220
|
const icons = {
|
|
@@ -5590,11 +7227,55 @@ async function syncCommand() {
|
|
|
5590
7227
|
return `${icon} ${r.path} ${chalk7.dim(`(${r.action})`)}`;
|
|
5591
7228
|
});
|
|
5592
7229
|
printBox("Sync complete", summaryLines, "success");
|
|
7230
|
+
const wsManifest = await readWorkspaceManifest(rootDir);
|
|
7231
|
+
if (!wsManifest) return;
|
|
7232
|
+
const syncReposRequested = opts.repos !== void 0;
|
|
7233
|
+
const syncOnSync = wsManifest.syncStrategy === "on-sync";
|
|
7234
|
+
const syncableCount = wsManifest.repos.filter((r) => r.sync).length;
|
|
7235
|
+
if (!syncReposRequested && !syncOnSync) {
|
|
7236
|
+
if (syncableCount > 0) {
|
|
7237
|
+
info(`Workspace: ${syncableCount} repo(s) available for sync. Run ${chalk7.bold("hatch3r sync --repos")} to propagate.`);
|
|
7238
|
+
}
|
|
7239
|
+
return;
|
|
7240
|
+
}
|
|
7241
|
+
const repoPaths = Array.isArray(opts.repos) ? opts.repos : void 0;
|
|
7242
|
+
console.log();
|
|
7243
|
+
const wsSpinner = createSpinner(
|
|
7244
|
+
opts.dryRun ? "Workspace sync (dry run)..." : `Syncing workspace to ${repoPaths ? repoPaths.length : syncableCount} repo(s)...`
|
|
7245
|
+
);
|
|
7246
|
+
wsSpinner.start();
|
|
7247
|
+
const wsResult = await syncWorkspaceRepos(rootDir, {
|
|
7248
|
+
repos: repoPaths,
|
|
7249
|
+
dryRun: opts.dryRun,
|
|
7250
|
+
force: opts.force,
|
|
7251
|
+
onWarn: (msg) => warn(msg)
|
|
7252
|
+
});
|
|
7253
|
+
if (opts.dryRun) {
|
|
7254
|
+
wsSpinner.succeed("Workspace sync (dry run)");
|
|
7255
|
+
for (const r of wsResult.repos) {
|
|
7256
|
+
const changes = [];
|
|
7257
|
+
if (r.added.length > 0) changes.push(`+${r.added.length} content`);
|
|
7258
|
+
if (r.removed.length > 0) changes.push(`-${r.removed.length} content`);
|
|
7259
|
+
if (r.toolsSynced.length > 0) changes.push(`${r.toolsSynced.length} tools`);
|
|
7260
|
+
info(` ${r.path}: ${changes.length > 0 ? changes.join(", ") : "up to date"}`);
|
|
7261
|
+
}
|
|
7262
|
+
return;
|
|
7263
|
+
}
|
|
7264
|
+
const succeeded = wsResult.repos.filter((r) => r.action === "synced").length;
|
|
7265
|
+
const failed = wsResult.repos.filter((r) => r.action === "error").length;
|
|
7266
|
+
if (failed > 0) {
|
|
7267
|
+
wsSpinner.warn(`Workspace sync: ${succeeded} synced, ${failed} failed`);
|
|
7268
|
+
for (const r of wsResult.repos.filter((r2) => r2.action === "error")) {
|
|
7269
|
+
error(` ${r.path}: ${r.error}`);
|
|
7270
|
+
}
|
|
7271
|
+
} else {
|
|
7272
|
+
wsSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
|
|
7273
|
+
}
|
|
5593
7274
|
}
|
|
5594
7275
|
|
|
5595
7276
|
// src/cli/commands/validate.ts
|
|
5596
|
-
import { readdir as
|
|
5597
|
-
import { join as
|
|
7277
|
+
import { readdir as readdir11, readFile as readFile19, access as access8 } from "fs/promises";
|
|
7278
|
+
import { join as join24, posix as posix2 } from "path";
|
|
5598
7279
|
import chalk8 from "chalk";
|
|
5599
7280
|
import { parse as parseYaml4 } from "yaml";
|
|
5600
7281
|
var DEFAULT_KNOWN_AGENTS = /* @__PURE__ */ new Set([
|
|
@@ -5630,7 +7311,7 @@ async function validateManifest2(rootDir, manifest, result) {
|
|
|
5630
7311
|
if (!manifest.tools || manifest.tools.length === 0) result.warnings.push("hatch.json: no tools configured");
|
|
5631
7312
|
for (const managedFile of manifest.managedFiles ?? []) {
|
|
5632
7313
|
try {
|
|
5633
|
-
await
|
|
7314
|
+
await access8(join24(rootDir, managedFile));
|
|
5634
7315
|
} catch (err) {
|
|
5635
7316
|
if (err.code !== "ENOENT") throw err;
|
|
5636
7317
|
result.warnings.push(`Managed file missing from disk: ${managedFile}`);
|
|
@@ -5642,7 +7323,7 @@ async function validateDirectories(agentsDir, result) {
|
|
|
5642
7323
|
const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
|
|
5643
7324
|
for (const dir of requiredDirs) {
|
|
5644
7325
|
try {
|
|
5645
|
-
await
|
|
7326
|
+
await access8(join24(agentsDir, dir));
|
|
5646
7327
|
} catch (err) {
|
|
5647
7328
|
if (err.code !== "ENOENT") throw err;
|
|
5648
7329
|
result.errors.push(`Required directory missing: .agents/${dir}/`);
|
|
@@ -5650,7 +7331,7 @@ async function validateDirectories(agentsDir, result) {
|
|
|
5650
7331
|
}
|
|
5651
7332
|
for (const dir of optionalDirs) {
|
|
5652
7333
|
try {
|
|
5653
|
-
await
|
|
7334
|
+
await access8(join24(agentsDir, dir));
|
|
5654
7335
|
} catch (err) {
|
|
5655
7336
|
if (err.code !== "ENOENT") throw err;
|
|
5656
7337
|
result.warnings.push(`Optional directory missing: .agents/${dir}/`);
|
|
@@ -5661,13 +7342,13 @@ async function validateFrontmatter(agentsDir, result) {
|
|
|
5661
7342
|
const requiredDirs = ["agents", "skills", "rules"];
|
|
5662
7343
|
const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
|
|
5663
7344
|
for (const dir of [...requiredDirs, ...optionalDirs]) {
|
|
5664
|
-
const dirPath =
|
|
7345
|
+
const dirPath = join24(agentsDir, dir);
|
|
5665
7346
|
try {
|
|
5666
|
-
const entries = await
|
|
7347
|
+
const entries = await readdir11(dirPath, { withFileTypes: true });
|
|
5667
7348
|
for (const entry of entries) {
|
|
5668
7349
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5669
|
-
const filePath =
|
|
5670
|
-
const content = await
|
|
7350
|
+
const filePath = join24(dirPath, entry.name);
|
|
7351
|
+
const content = await readFile19(filePath, "utf-8");
|
|
5671
7352
|
if (!content.startsWith("---")) {
|
|
5672
7353
|
result.warnings.push(`Missing frontmatter: .agents/${dir}/${entry.name}`);
|
|
5673
7354
|
} else {
|
|
@@ -5686,9 +7367,9 @@ async function validateFrontmatter(agentsDir, result) {
|
|
|
5686
7367
|
}
|
|
5687
7368
|
}
|
|
5688
7369
|
} else if (entry.isDirectory()) {
|
|
5689
|
-
const skillPath =
|
|
7370
|
+
const skillPath = join24(dirPath, entry.name, "SKILL.md");
|
|
5690
7371
|
try {
|
|
5691
|
-
await
|
|
7372
|
+
await access8(skillPath);
|
|
5692
7373
|
} catch (err) {
|
|
5693
7374
|
if (err.code !== "ENOENT") throw err;
|
|
5694
7375
|
result.warnings.push(`Skill directory missing SKILL.md: .agents/${dir}/${entry.name}/`);
|
|
@@ -5700,7 +7381,7 @@ async function validateFrontmatter(agentsDir, result) {
|
|
|
5700
7381
|
}
|
|
5701
7382
|
}
|
|
5702
7383
|
try {
|
|
5703
|
-
await
|
|
7384
|
+
await access8(join24(agentsDir, "AGENTS.md"));
|
|
5704
7385
|
} catch (err) {
|
|
5705
7386
|
if (err.code !== "ENOENT") throw err;
|
|
5706
7387
|
result.warnings.push("Missing .agents/AGENTS.md");
|
|
@@ -5719,22 +7400,22 @@ async function validateManagedFilePrefixes(manifest, result) {
|
|
|
5719
7400
|
}
|
|
5720
7401
|
async function validateHooks(agentsDir, manifest, result) {
|
|
5721
7402
|
if (!manifest.features.hooks) return;
|
|
5722
|
-
const hooksDir =
|
|
7403
|
+
const hooksDir = join24(agentsDir, "hooks");
|
|
5723
7404
|
try {
|
|
5724
|
-
const hookFiles = await
|
|
7405
|
+
const hookFiles = await readdir11(hooksDir);
|
|
5725
7406
|
const mdHooks = hookFiles.filter((f) => f.endsWith(".md"));
|
|
5726
7407
|
if (mdHooks.length === 0) {
|
|
5727
7408
|
result.warnings.push("Hooks feature enabled but no hook definitions found in .agents/hooks/");
|
|
5728
7409
|
}
|
|
5729
7410
|
let agentFiles;
|
|
5730
7411
|
try {
|
|
5731
|
-
const agentEntries = await
|
|
7412
|
+
const agentEntries = await readdir11(join24(agentsDir, "agents"));
|
|
5732
7413
|
agentFiles = new Set(agentEntries.filter((f) => f.endsWith(".md")));
|
|
5733
7414
|
} catch (err) {
|
|
5734
7415
|
if (err.code !== "ENOENT") throw err;
|
|
5735
7416
|
}
|
|
5736
7417
|
for (const hookFile of mdHooks) {
|
|
5737
|
-
const hookContent = await
|
|
7418
|
+
const hookContent = await readFile19(join24(hooksDir, hookFile), "utf-8");
|
|
5738
7419
|
if (!hookContent.startsWith("---")) {
|
|
5739
7420
|
result.warnings.push(`Hook missing frontmatter: .agents/hooks/${hookFile}`);
|
|
5740
7421
|
continue;
|
|
@@ -5766,9 +7447,9 @@ async function validateHooks(agentsDir, manifest, result) {
|
|
|
5766
7447
|
}
|
|
5767
7448
|
async function validateMcp(agentsDir, manifest, result) {
|
|
5768
7449
|
if (!manifest.features.mcp || manifest.mcp.servers.length === 0) return;
|
|
5769
|
-
const mcpPath =
|
|
7450
|
+
const mcpPath = join24(agentsDir, "mcp", "mcp.json");
|
|
5770
7451
|
try {
|
|
5771
|
-
const mcpContent = await
|
|
7452
|
+
const mcpContent = await readFile19(mcpPath, "utf-8");
|
|
5772
7453
|
const mcpParsed = JSON.parse(mcpContent);
|
|
5773
7454
|
if (!mcpParsed.mcpServers || typeof mcpParsed.mcpServers !== "object") {
|
|
5774
7455
|
result.errors.push("MCP config missing 'mcpServers' key");
|
|
@@ -5794,17 +7475,101 @@ async function validateModels(manifest, result) {
|
|
|
5794
7475
|
}
|
|
5795
7476
|
}
|
|
5796
7477
|
}
|
|
7478
|
+
async function validateCustomizeYaml(rootDir, result) {
|
|
7479
|
+
const VALID_FIELDS = /* @__PURE__ */ new Set(["model", "scope", "description", "enabled"]);
|
|
7480
|
+
const FIELD_TYPES = {
|
|
7481
|
+
model: "string",
|
|
7482
|
+
scope: "string",
|
|
7483
|
+
description: "string",
|
|
7484
|
+
enabled: "boolean"
|
|
7485
|
+
};
|
|
7486
|
+
for (const { dir } of CUSTOMIZATION_TYPES) {
|
|
7487
|
+
const customDir = join24(rootDir, ".hatch3r", dir);
|
|
7488
|
+
let files;
|
|
7489
|
+
try {
|
|
7490
|
+
files = await readdir11(customDir);
|
|
7491
|
+
} catch (err) {
|
|
7492
|
+
if (err.code === "ENOENT") continue;
|
|
7493
|
+
throw err;
|
|
7494
|
+
}
|
|
7495
|
+
const yamlFiles = files.filter((f) => f.endsWith(".customize.yaml"));
|
|
7496
|
+
for (const file of yamlFiles) {
|
|
7497
|
+
const filePath = join24(customDir, file);
|
|
7498
|
+
const itemId = file.replace(".customize.yaml", "");
|
|
7499
|
+
let raw;
|
|
7500
|
+
try {
|
|
7501
|
+
raw = await readFile19(filePath, "utf-8");
|
|
7502
|
+
} catch {
|
|
7503
|
+
continue;
|
|
7504
|
+
}
|
|
7505
|
+
if (Buffer.byteLength(raw, "utf-8") > 10240) {
|
|
7506
|
+
result.warnings.push(
|
|
7507
|
+
`.customize.yaml for "${itemId}" exceeds 10KB limit and will be skipped during generation`
|
|
7508
|
+
);
|
|
7509
|
+
continue;
|
|
7510
|
+
}
|
|
7511
|
+
let parsed;
|
|
7512
|
+
try {
|
|
7513
|
+
parsed = parseYaml4(raw);
|
|
7514
|
+
} catch {
|
|
7515
|
+
result.errors.push(
|
|
7516
|
+
`Invalid YAML syntax in .hatch3r/${dir}/${file}`
|
|
7517
|
+
);
|
|
7518
|
+
continue;
|
|
7519
|
+
}
|
|
7520
|
+
if (!parsed || typeof parsed !== "object") {
|
|
7521
|
+
result.warnings.push(
|
|
7522
|
+
`.customize.yaml for "${itemId}" is empty or not an object`
|
|
7523
|
+
);
|
|
7524
|
+
continue;
|
|
7525
|
+
}
|
|
7526
|
+
for (const key of Object.keys(parsed)) {
|
|
7527
|
+
if (!VALID_FIELDS.has(key)) {
|
|
7528
|
+
result.warnings.push(
|
|
7529
|
+
`.hatch3r/${dir}/${file}: unknown field "${key}" (valid: ${[...VALID_FIELDS].join(", ")})`
|
|
7530
|
+
);
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
for (const [key, expectedType] of Object.entries(FIELD_TYPES)) {
|
|
7534
|
+
if (key in parsed && parsed[key] !== void 0 && parsed[key] !== null) {
|
|
7535
|
+
const actualType = typeof parsed[key];
|
|
7536
|
+
if (actualType !== expectedType) {
|
|
7537
|
+
result.warnings.push(
|
|
7538
|
+
`.hatch3r/${dir}/${file}: field "${key}" should be ${expectedType} but is ${actualType}`
|
|
7539
|
+
);
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
for (const field of ["description", "scope"]) {
|
|
7544
|
+
const value = parsed[field];
|
|
7545
|
+
if (typeof value === "string") {
|
|
7546
|
+
const violations = scanForDeniedPatterns(value);
|
|
7547
|
+
for (const v of violations) {
|
|
7548
|
+
result.warnings.push(
|
|
7549
|
+
`.hatch3r/${dir}/${file}: field "${field}" contains denied pattern: ${v}`
|
|
7550
|
+
);
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
const type = dir;
|
|
7555
|
+
const readResult = await readCustomizationWithWarnings(rootDir, type, itemId);
|
|
7556
|
+
for (const w of readResult.warnings) {
|
|
7557
|
+
result.warnings.push(w);
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
5797
7562
|
async function validateCustomizations(rootDir, agentsDir, manifest, result) {
|
|
5798
7563
|
for (const { dir, canonical } of CUSTOMIZATION_TYPES) {
|
|
5799
|
-
const customDir =
|
|
7564
|
+
const customDir = join24(rootDir, ".hatch3r", dir);
|
|
5800
7565
|
try {
|
|
5801
|
-
const customFiles = await
|
|
7566
|
+
const customFiles = await readdir11(customDir);
|
|
5802
7567
|
for (const file of customFiles) {
|
|
5803
7568
|
if (file.endsWith(".customize.yaml")) {
|
|
5804
7569
|
const itemId = file.replace(".customize.yaml", "");
|
|
5805
|
-
const canonicalPath = canonical === "skills" ?
|
|
7570
|
+
const canonicalPath = canonical === "skills" ? join24(agentsDir, canonical, itemId) : join24(agentsDir, canonical, `${itemId}.md`);
|
|
5806
7571
|
try {
|
|
5807
|
-
await
|
|
7572
|
+
await access8(canonicalPath);
|
|
5808
7573
|
} catch (err) {
|
|
5809
7574
|
if (err.code !== "ENOENT") throw err;
|
|
5810
7575
|
result.warnings.push(`Customization file for non-existent ${canonical.slice(0, -1)}: .hatch3r/${dir}/${file}`);
|
|
@@ -5830,9 +7595,9 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
|
|
|
5830
7595
|
for (const [key, cfg] of Object.entries(contentDirs)) {
|
|
5831
7596
|
const ids = manifest.content.items[key];
|
|
5832
7597
|
for (const id of ids) {
|
|
5833
|
-
const checkPath = cfg.strategy === "subdir" ?
|
|
7598
|
+
const checkPath = cfg.strategy === "subdir" ? join24(agentsDir, cfg.dir, id, "SKILL.md") : join24(agentsDir, cfg.dir, `${id}.md`);
|
|
5834
7599
|
try {
|
|
5835
|
-
await
|
|
7600
|
+
await access8(checkPath);
|
|
5836
7601
|
} catch {
|
|
5837
7602
|
result.warnings.push(`Content "${id}" (${key}) in manifest but missing from .agents/${cfg.dir}/`);
|
|
5838
7603
|
}
|
|
@@ -5843,9 +7608,9 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
|
|
|
5843
7608
|
for (const id of ids) allContentIds.add(id);
|
|
5844
7609
|
}
|
|
5845
7610
|
for (const { dir } of CUSTOMIZATION_TYPES) {
|
|
5846
|
-
const customDir =
|
|
7611
|
+
const customDir = join24(rootDir, ".hatch3r", dir);
|
|
5847
7612
|
try {
|
|
5848
|
-
const files = await
|
|
7613
|
+
const files = await readdir11(customDir);
|
|
5849
7614
|
for (const f of files.filter((f2) => f2.endsWith(".customize.yaml") || f2.endsWith(".customize.md"))) {
|
|
5850
7615
|
const itemId = f.replace(/\.customize\.(yaml|md)$/, "");
|
|
5851
7616
|
if (!allContentIds.has(itemId) && !allContentIds.has(`${HATCH3R_PREFIX}${itemId}`)) {
|
|
@@ -5857,12 +7622,12 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
|
|
|
5857
7622
|
}
|
|
5858
7623
|
}
|
|
5859
7624
|
}
|
|
5860
|
-
const learningsDir =
|
|
7625
|
+
const learningsDir = join24(agentsDir, "learnings");
|
|
5861
7626
|
try {
|
|
5862
|
-
const learningFiles = await
|
|
7627
|
+
const learningFiles = await readdir11(learningsDir);
|
|
5863
7628
|
const mdFiles = learningFiles.filter((f) => f.endsWith(".md"));
|
|
5864
7629
|
for (const file of mdFiles) {
|
|
5865
|
-
const content = await
|
|
7630
|
+
const content = await readFile19(join24(learningsDir, file), "utf-8");
|
|
5866
7631
|
const violations = scanForDeniedPatterns(content);
|
|
5867
7632
|
if (violations.length > 0) {
|
|
5868
7633
|
for (const v of violations) {
|
|
@@ -5877,18 +7642,18 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
|
|
|
5877
7642
|
async function validateCommand() {
|
|
5878
7643
|
printBanner(true);
|
|
5879
7644
|
const rootDir = process.cwd();
|
|
5880
|
-
const agentsDir =
|
|
7645
|
+
const agentsDir = join24(rootDir, AGENTS_DIR);
|
|
5881
7646
|
const result = { errors: [], warnings: [] };
|
|
5882
7647
|
const spinner = createSpinner("Validating .agents/ structure...");
|
|
5883
7648
|
spinner.start();
|
|
5884
7649
|
try {
|
|
5885
|
-
await
|
|
7650
|
+
await access8(agentsDir);
|
|
5886
7651
|
} catch (err) {
|
|
5887
7652
|
if (err.code !== "ENOENT") throw err;
|
|
5888
7653
|
spinner.fail("Validation failed");
|
|
5889
7654
|
error(".agents/ directory not found. Run `hatch3r init` first.");
|
|
5890
7655
|
console.log();
|
|
5891
|
-
throw new HatchError(".agents/ directory not found.", 1);
|
|
7656
|
+
throw new HatchError(".agents/ directory not found.", 1, "CONFIG_ERROR");
|
|
5892
7657
|
}
|
|
5893
7658
|
const manifest = await readManifest(rootDir);
|
|
5894
7659
|
await validateManifest2(rootDir, manifest, result);
|
|
@@ -5900,6 +7665,7 @@ async function validateCommand() {
|
|
|
5900
7665
|
await validateMcp(agentsDir, manifest, result);
|
|
5901
7666
|
await validateModels(manifest, result);
|
|
5902
7667
|
await validateCustomizations(rootDir, agentsDir, manifest, result);
|
|
7668
|
+
await validateCustomizeYaml(rootDir, result);
|
|
5903
7669
|
await validateContentConsistency(rootDir, agentsDir, manifest, result);
|
|
5904
7670
|
try {
|
|
5905
7671
|
const index = await buildContentIndex(agentsDir);
|
|
@@ -5909,6 +7675,18 @@ async function validateCommand() {
|
|
|
5909
7675
|
result.warnings.push(w);
|
|
5910
7676
|
}
|
|
5911
7677
|
}
|
|
7678
|
+
const EXPECTED_CROSS_TYPE_PAIRS = /* @__PURE__ */ new Set(["command", "skill"]);
|
|
7679
|
+
for (const collision of index.collisions) {
|
|
7680
|
+
if (collision.kind === "cross-type") {
|
|
7681
|
+
const types = /* @__PURE__ */ new Set([collision.existingType, collision.duplicateType]);
|
|
7682
|
+
if (types.size === 2 && [...types].every((t) => EXPECTED_CROSS_TYPE_PAIRS.has(t))) {
|
|
7683
|
+
continue;
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
result.warnings.push(
|
|
7687
|
+
`Content ID collision: "${collision.id}" exists as ${collision.existingType} (${collision.existingPath}) and ${collision.duplicateType} (${collision.duplicatePath})`
|
|
7688
|
+
);
|
|
7689
|
+
}
|
|
5912
7690
|
} catch {
|
|
5913
7691
|
}
|
|
5914
7692
|
if (manifest.content) {
|
|
@@ -5919,8 +7697,22 @@ async function validateCommand() {
|
|
|
5919
7697
|
}
|
|
5920
7698
|
}
|
|
5921
7699
|
spinner.stop();
|
|
7700
|
+
let hasCustomizations = false;
|
|
7701
|
+
for (const { dir } of CUSTOMIZATION_TYPES) {
|
|
7702
|
+
try {
|
|
7703
|
+
const files = await readdir11(join24(rootDir, ".hatch3r", dir));
|
|
7704
|
+
if (files.some((f) => f.endsWith(".customize.yaml") || f.endsWith(".customize.md"))) {
|
|
7705
|
+
hasCustomizations = true;
|
|
7706
|
+
break;
|
|
7707
|
+
}
|
|
7708
|
+
} catch {
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
5922
7711
|
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
5923
7712
|
printBox("Validation", [chalk8.green("All checks passed")], "success");
|
|
7713
|
+
if (hasCustomizations) {
|
|
7714
|
+
printCustomizationHint();
|
|
7715
|
+
}
|
|
5924
7716
|
return;
|
|
5925
7717
|
}
|
|
5926
7718
|
console.log();
|
|
@@ -5942,7 +7734,7 @@ async function validateCommand() {
|
|
|
5942
7734
|
`${chalk8.yellow("\u26A0")} ${result.warnings.length} warning(s)`
|
|
5943
7735
|
];
|
|
5944
7736
|
printBox("Validation failed", summaryLines, "error");
|
|
5945
|
-
throw new HatchError("Validation failed", 1);
|
|
7737
|
+
throw new HatchError("Validation failed", 1, "VALIDATION_ERROR");
|
|
5946
7738
|
} else {
|
|
5947
7739
|
const summaryLines = [
|
|
5948
7740
|
`${chalk8.green("\u2714")} 0 errors`,
|
|
@@ -5950,15 +7742,29 @@ async function validateCommand() {
|
|
|
5950
7742
|
];
|
|
5951
7743
|
printBox("Validation passed", summaryLines, "success");
|
|
5952
7744
|
}
|
|
7745
|
+
if (hasCustomizations) {
|
|
7746
|
+
printCustomizationHint();
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
function printCustomizationHint() {
|
|
7750
|
+
console.log();
|
|
7751
|
+
info(chalk8.bold("Customization mechanisms detected. Quick reference:"));
|
|
7752
|
+
console.log(chalk8.dim(" 1. hatch3r- prefix: Files prefixed with hatch3r- are managed by hatch3r and"));
|
|
7753
|
+
console.log(chalk8.dim(" overwritten on update. Do not edit these directly."));
|
|
7754
|
+
console.log(chalk8.dim(" 2. Managed blocks: Sections between <!-- MANAGED-BLOCK:BEGIN --> and"));
|
|
7755
|
+
console.log(chalk8.dim(" <!-- MANAGED-BLOCK:END --> are auto-updated. Add content outside these markers."));
|
|
7756
|
+
console.log(chalk8.dim(" 3. .customize.yaml/.md: Place in .hatch3r/{type}/ to override model, scope,"));
|
|
7757
|
+
console.log(chalk8.dim(" description, or disable items. Use .customize.md for content additions."));
|
|
7758
|
+
console.log(chalk8.dim(" See: https://docs.hatch3r.com/docs/guides/customization"));
|
|
5953
7759
|
}
|
|
5954
7760
|
|
|
5955
7761
|
// src/cli/commands/verify.ts
|
|
5956
|
-
import { join as
|
|
7762
|
+
import { join as join25 } from "path";
|
|
5957
7763
|
import chalk9 from "chalk";
|
|
5958
7764
|
async function verifyCommand() {
|
|
5959
7765
|
printBanner(true);
|
|
5960
7766
|
const rootDir = process.cwd();
|
|
5961
|
-
const agentsDir =
|
|
7767
|
+
const agentsDir = join25(rootDir, AGENTS_DIR);
|
|
5962
7768
|
const spinner = createSpinner("Verifying file integrity...");
|
|
5963
7769
|
spinner.start();
|
|
5964
7770
|
const manifest = await readIntegrityManifest(agentsDir);
|
|
@@ -5966,7 +7772,7 @@ async function verifyCommand() {
|
|
|
5966
7772
|
spinner.fail("No integrity manifest found");
|
|
5967
7773
|
error("Missing .agents/.integrity.json \u2014 run `hatch3r init` or `hatch3r update` to generate it.");
|
|
5968
7774
|
console.log();
|
|
5969
|
-
throw new HatchError("Missing .agents/.integrity.json", 1);
|
|
7775
|
+
throw new HatchError("Missing .agents/.integrity.json", 1, "INTEGRITY_ERROR");
|
|
5970
7776
|
}
|
|
5971
7777
|
const results = await verifyIntegrity(agentsDir);
|
|
5972
7778
|
spinner.stop();
|
|
@@ -6015,30 +7821,30 @@ async function verifyCommand() {
|
|
|
6015
7821
|
info(`Modified files may have been tampered with. Run ${chalk9.bold("hatch3r update")} to restore originals.`);
|
|
6016
7822
|
}
|
|
6017
7823
|
console.log();
|
|
6018
|
-
throw new HatchError("Integrity check failed", 1);
|
|
7824
|
+
throw new HatchError("Integrity check failed", 1, "INTEGRITY_ERROR");
|
|
6019
7825
|
} else {
|
|
6020
7826
|
printBox("Integrity check passed", summaryLines, "success");
|
|
6021
7827
|
}
|
|
6022
7828
|
}
|
|
6023
7829
|
|
|
6024
7830
|
// src/cli/commands/status.ts
|
|
6025
|
-
import { readFile as
|
|
6026
|
-
import { join as
|
|
7831
|
+
import { readFile as readFile20, readdir as readdir12, stat as stat5 } from "fs/promises";
|
|
7832
|
+
import { join as join26 } from "path";
|
|
6027
7833
|
import chalk10 from "chalk";
|
|
6028
7834
|
async function dirCharCount(dir) {
|
|
6029
7835
|
let total = 0;
|
|
6030
7836
|
let entries;
|
|
6031
7837
|
try {
|
|
6032
|
-
entries = await
|
|
7838
|
+
entries = await readdir12(dir, { withFileTypes: true });
|
|
6033
7839
|
} catch {
|
|
6034
7840
|
return 0;
|
|
6035
7841
|
}
|
|
6036
7842
|
for (const entry of entries) {
|
|
6037
|
-
const fullPath =
|
|
7843
|
+
const fullPath = join26(dir, entry.name);
|
|
6038
7844
|
if (entry.isDirectory()) {
|
|
6039
7845
|
total += await dirCharCount(fullPath);
|
|
6040
7846
|
} else if (entry.isFile()) {
|
|
6041
|
-
const info2 = await
|
|
7847
|
+
const info2 = await stat5(fullPath);
|
|
6042
7848
|
total += info2.size;
|
|
6043
7849
|
}
|
|
6044
7850
|
}
|
|
@@ -6047,12 +7853,12 @@ async function dirCharCount(dir) {
|
|
|
6047
7853
|
async function statusCommand() {
|
|
6048
7854
|
printBanner(true);
|
|
6049
7855
|
const rootDir = process.cwd();
|
|
6050
|
-
const agentsDir =
|
|
7856
|
+
const agentsDir = join26(rootDir, AGENTS_DIR);
|
|
6051
7857
|
const manifest = await readManifest(rootDir);
|
|
6052
7858
|
if (!manifest) {
|
|
6053
7859
|
error("No .agents/hatch.json found.");
|
|
6054
7860
|
console.log(chalk10.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
6055
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
7861
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
6056
7862
|
}
|
|
6057
7863
|
const spinner = createSpinner("Checking sync status...");
|
|
6058
7864
|
spinner.start();
|
|
@@ -6063,9 +7869,9 @@ async function statusCommand() {
|
|
|
6063
7869
|
const outputs = await adapter.generate(agentsDir, manifest);
|
|
6064
7870
|
fileLines.push(chalk10.bold(`${tool}:`));
|
|
6065
7871
|
for (const out of outputs) {
|
|
6066
|
-
const destPath =
|
|
7872
|
+
const destPath = join26(rootDir, out.path);
|
|
6067
7873
|
try {
|
|
6068
|
-
const existing = await
|
|
7874
|
+
const existing = await readFile20(destPath, "utf-8");
|
|
6069
7875
|
const existingBlock = extractManagedBlock(existing);
|
|
6070
7876
|
const expectedBlock = out.managedContent ?? extractManagedBlock(out.content);
|
|
6071
7877
|
if (existingBlock !== null && expectedBlock !== null ? existingBlock === expectedBlock : existing === out.content) {
|
|
@@ -6107,6 +7913,36 @@ async function statusCommand() {
|
|
|
6107
7913
|
info(`Run ${chalk10.bold("hatch3r sync")} to regenerate drifted/missing files.`);
|
|
6108
7914
|
console.log();
|
|
6109
7915
|
}
|
|
7916
|
+
const wsManifest = await readWorkspaceManifest(rootDir);
|
|
7917
|
+
if (wsManifest && wsManifest.repos.length > 0) {
|
|
7918
|
+
const wsLines = [];
|
|
7919
|
+
for (const repo of wsManifest.repos) {
|
|
7920
|
+
const icon = repo.sync ? chalk10.green("\u2713") : chalk10.dim("\u25CB");
|
|
7921
|
+
let detail;
|
|
7922
|
+
if (!repo.sync) {
|
|
7923
|
+
detail = chalk10.dim("sync disabled");
|
|
7924
|
+
} else if (repo.lastSync) {
|
|
7925
|
+
const elapsed = Math.max(0, Date.now() - new Date(repo.lastSync).getTime());
|
|
7926
|
+
const hours = Math.floor(elapsed / (1e3 * 60 * 60));
|
|
7927
|
+
const timeAgo = hours < 1 ? "just now" : hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
|
|
7928
|
+
detail = `synced ${timeAgo}`;
|
|
7929
|
+
} else {
|
|
7930
|
+
detail = chalk10.yellow("never synced");
|
|
7931
|
+
}
|
|
7932
|
+
const identity = repo.owner && repo.repo ? chalk10.dim(`${repo.owner}/${repo.repo}`) : "";
|
|
7933
|
+
const branch = repo.defaultBranch ? chalk10.dim(`[${repo.defaultBranch}]`) : "";
|
|
7934
|
+
const identityPart = identity || branch ? ` ${identity} ${branch}` : "";
|
|
7935
|
+
wsLines.push(`${icon} ${repo.name ?? repo.path}${identityPart} ${chalk10.dim(`(${detail})`)}`);
|
|
7936
|
+
}
|
|
7937
|
+
printBox(`Workspace: ${wsManifest.name} (${wsManifest.repos.length} repos)`, wsLines, "info");
|
|
7938
|
+
}
|
|
7939
|
+
if (manifest.workspace) {
|
|
7940
|
+
const wsInfo = [
|
|
7941
|
+
`Managed by workspace at ${chalk10.bold(manifest.workspace.rootPath)}`,
|
|
7942
|
+
`Last synced: ${manifest.workspace.lastSync ? new Date(manifest.workspace.lastSync).toLocaleString() : "never"}`
|
|
7943
|
+
];
|
|
7944
|
+
printBox("Workspace member", wsInfo, "info");
|
|
7945
|
+
}
|
|
6110
7946
|
}
|
|
6111
7947
|
|
|
6112
7948
|
// src/cli/index.ts
|
|
@@ -6117,15 +7953,67 @@ program.name("hatch3r").description(
|
|
|
6117
7953
|
program.command("init").description("Install a complete agent setup into the current repo").option(
|
|
6118
7954
|
"--tools <tools>",
|
|
6119
7955
|
`Comma-separated tools (${TOOL_CHOICES})`
|
|
6120
|
-
).option("--yes", "Skip interactive prompts, use defaults").option("--preset <preset>", "Content preset: minimal, standard, full").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").action(initCommand);
|
|
6121
|
-
program.command("sync").description("Re-generate tool outputs from canonical .agents/ state").action(syncCommand);
|
|
7956
|
+
).option("--yes", "Skip interactive prompts, use defaults").option("--preset <preset>", "Content preset: minimal, standard, full").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").option("--workspace", "Initialize as a multi-repo workspace").action(initCommand);
|
|
7957
|
+
program.command("sync").description("Re-generate tool outputs from canonical .agents/ state").option("--repos [paths...]", "Sync workspace content to sub-repos (all opted-in if no paths given)").option("--dry-run", "Show what would change without modifying files").option("--force", "Overwrite locally modified files in sub-repos").option("--minimal", "Generate stripped-down output (no comments, minimal formatting) to reduce token usage").action(syncCommand);
|
|
6122
7958
|
program.command("status").description("Check sync status between canonical .agents/ and generated files").action(statusCommand);
|
|
6123
|
-
program.command("update").description("Pull latest hatch3r templates with safe merge").action(updateCommand);
|
|
7959
|
+
program.command("update").description("Pull latest hatch3r templates with safe merge").option("--yes", "Skip interactive prompts, use defaults").action(updateCommand);
|
|
6124
7960
|
program.command("validate").description("Validate the canonical .agents/ structure").action(validateCommand);
|
|
6125
7961
|
program.command("verify").description("Verify integrity of canonical agent files").action(verifyCommand);
|
|
6126
7962
|
program.command("config").description("Reconfigure tools, MCP servers, features, and platform").action(configCommand);
|
|
6127
7963
|
program.command("add [pack]").description("Install a community pack (coming soon)").action(addCommand);
|
|
6128
7964
|
program.command("worktree-setup [worktree-path]").description("Set up gitignored files in a git worktree").option("--from <path>", "Main repo path (auto-detected by default)").option("--dry-run", "Show what would be done without changes").option("--force", "Overwrite existing files in the worktree").action(worktreeSetupCommand);
|
|
7965
|
+
var AGENT_COMMAND_NAMES = /* @__PURE__ */ new Set([
|
|
7966
|
+
"review",
|
|
7967
|
+
"workflow",
|
|
7968
|
+
"project-spec",
|
|
7969
|
+
"codebase-map",
|
|
7970
|
+
"debug",
|
|
7971
|
+
"release",
|
|
7972
|
+
"refactor-plan",
|
|
7973
|
+
"test-plan",
|
|
7974
|
+
"bug-plan",
|
|
7975
|
+
"roadmap",
|
|
7976
|
+
"onboard",
|
|
7977
|
+
"recipe",
|
|
7978
|
+
"board-init",
|
|
7979
|
+
"board-pickup",
|
|
7980
|
+
"board-groom",
|
|
7981
|
+
"board-refresh",
|
|
7982
|
+
"security-audit",
|
|
7983
|
+
"dep-audit",
|
|
7984
|
+
"benchmark",
|
|
7985
|
+
"healthcheck",
|
|
7986
|
+
"context-health",
|
|
7987
|
+
"learn",
|
|
7988
|
+
"revision",
|
|
7989
|
+
"cost-tracking",
|
|
7990
|
+
"api-spec",
|
|
7991
|
+
"hooks",
|
|
7992
|
+
"quick-change",
|
|
7993
|
+
"command-customize"
|
|
7994
|
+
]);
|
|
7995
|
+
program.on("command:*", (operands) => {
|
|
7996
|
+
const cmd = operands[0];
|
|
7997
|
+
if (cmd && AGENT_COMMAND_NAMES.has(cmd)) {
|
|
7998
|
+
console.error(
|
|
7999
|
+
`
|
|
8000
|
+
"${cmd}" is a hatch3r agent command meant to be run inside your AI editor (e.g. /${cmd}).
|
|
8001
|
+
It cannot be invoked from the terminal CLI.
|
|
8002
|
+
|
|
8003
|
+
To use agent commands, open your project in Cursor, Claude Code, or another supported tool
|
|
8004
|
+
and type /${cmd} in the AI chat.
|
|
8005
|
+
`
|
|
8006
|
+
);
|
|
8007
|
+
} else {
|
|
8008
|
+
console.error(
|
|
8009
|
+
`
|
|
8010
|
+
Unknown command: ${cmd}
|
|
8011
|
+
Run "hatch3r --help" for available commands.
|
|
8012
|
+
`
|
|
8013
|
+
);
|
|
8014
|
+
}
|
|
8015
|
+
process.exit(1);
|
|
8016
|
+
});
|
|
6129
8017
|
var nodeVersion = parseInt(process.version.slice(1), 10);
|
|
6130
8018
|
if (nodeVersion < 22) {
|
|
6131
8019
|
console.error(
|