@vaubaan/harness 0.0.0-stage → 1.3.2

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.
Files changed (53) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +307 -2
  4. package/dist/agents/coding.agent.md +182 -0
  5. package/dist/agents/merge.agent.md +139 -0
  6. package/dist/agents/planning.agent.md +194 -0
  7. package/dist/agents/review.agent.md +176 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +2924 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/commands/clean-code.cmd.md +26 -0
  12. package/dist/commands/decision.cmd.md +19 -0
  13. package/dist/commands/doc-audit.cmd.md +24 -0
  14. package/dist/commands/drift.cmd.md +18 -0
  15. package/dist/commands/handbook.cmd.md +21 -0
  16. package/dist/commands/readme.cmd.md +19 -0
  17. package/dist/commands/spec.cmd.md +20 -0
  18. package/dist/commands/test-plan.cmd.md +22 -0
  19. package/dist/commands/workflow.cmd.md +19 -0
  20. package/dist/index.d.ts +36 -0
  21. package/dist/index.js +1706 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/init.d.ts +42 -0
  24. package/dist/init.js +1088 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/linters/index.d.ts +43 -0
  27. package/dist/linters/index.js +727 -0
  28. package/dist/linters/index.js.map +1 -0
  29. package/dist/parser/index.d.ts +247 -0
  30. package/dist/parser/index.js +114 -0
  31. package/dist/parser/index.js.map +1 -0
  32. package/dist/render-B9_kNtQc.d.ts +50 -0
  33. package/dist/render.d.ts +3 -0
  34. package/dist/render.js +572 -0
  35. package/dist/render.js.map +1 -0
  36. package/dist/skills/clean-code-guardian.skill.md +391 -0
  37. package/dist/skills/decision-record-writer.skill.md +166 -0
  38. package/dist/skills/doc-audit.skill.md +257 -0
  39. package/dist/skills/drift-detector.skill.md +173 -0
  40. package/dist/skills/handbook-writer.skill.md +185 -0
  41. package/dist/skills/readme-writer.skill.md +131 -0
  42. package/dist/skills/spec-writer.skill.md +165 -0
  43. package/dist/skills/test-plan-writer.skill.md +179 -0
  44. package/dist/skills/workflow-writer.skill.md +168 -0
  45. package/dist/templates/code-quality.md.tmpl +57 -0
  46. package/dist/templates/coding-workflow.md.tmpl +88 -0
  47. package/dist/templates/decision.md.tmpl +60 -0
  48. package/dist/templates/e2e-tests.md.tmpl +62 -0
  49. package/dist/templates/handbook.md.tmpl +58 -0
  50. package/dist/templates/integration-tests.md.tmpl +50 -0
  51. package/dist/templates/readme.md.tmpl +51 -0
  52. package/dist/templates/spec.md.tmpl +64 -0
  53. package/package.json +82 -4
package/dist/init.js ADDED
@@ -0,0 +1,1088 @@
1
+ // src/cli/init.ts
2
+ import { existsSync as existsSync6, mkdirSync, writeFileSync as writeFileSync3 } from "fs";
3
+ import { dirname as dirname3, join as join7, resolve as resolve3 } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // src/loader.ts
7
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
8
+ import { join } from "path";
9
+ import fg from "fast-glob";
10
+ import { z as z2 } from "zod";
11
+
12
+ // src/parser/common.ts
13
+ import { readFileSync } from "fs";
14
+ import matter from "gray-matter";
15
+
16
+ // src/parser/schemas.ts
17
+ import { z } from "zod";
18
+
19
+ // src/types.ts
20
+ var AGENT_IDS = ["coding", "review", "merge", "planning"];
21
+ var ENGINES = ["claude-code", "codex-cli", "opencode"];
22
+ var CAPABILITIES = ["multi-step", "slash-command", "inline-exec"];
23
+ var CAPABILITIES_MATRIX = {
24
+ "claude-code": ["multi-step", "slash-command", "inline-exec"],
25
+ "codex-cli": ["multi-step", "slash-command", "inline-exec"],
26
+ opencode: ["multi-step", "slash-command", "inline-exec"]
27
+ };
28
+ var DEGRADED_CAPABILITIES = {
29
+ "codex-cli": ["multi-step"]
30
+ };
31
+
32
+ // src/parser/schemas.ts
33
+ var IdSchema = z.string().min(1).regex(/^[a-z][a-z0-9-]*$/, "id must be kebab-case (lowercase + digits + hyphens)");
34
+ var SkillFrontmatterSchema = z.object({
35
+ id: IdSchema,
36
+ description: z.string().min(1),
37
+ engines: z.array(z.enum(ENGINES)).min(1).optional(),
38
+ requires: z.array(z.enum(CAPABILITIES)).optional(),
39
+ tags: z.array(z.string()).optional()
40
+ }).strict();
41
+ var CmdArgEnumSchema = z.object({
42
+ name: z.string().min(1),
43
+ type: z.literal("enum"),
44
+ values: z.array(z.string()).min(1),
45
+ default: z.string().optional()
46
+ }).strict();
47
+ var CmdArgStringSchema = z.object({
48
+ name: z.string().min(1),
49
+ type: z.literal("string"),
50
+ default: z.string().optional()
51
+ }).strict();
52
+ var CmdArgSchema = z.discriminatedUnion("type", [CmdArgEnumSchema, CmdArgStringSchema]);
53
+ var CmdFrontmatterSchema = z.object({
54
+ id: IdSchema,
55
+ trigger: z.string().regex(/^\/[a-z][a-z0-9-]*$/, "trigger must be /kebab-case"),
56
+ invokes_skill: IdSchema,
57
+ args: z.array(CmdArgSchema).optional(),
58
+ description: z.string().min(1)
59
+ }).strict();
60
+ var AgentFrontmatterSchema = z.object({
61
+ id: z.enum(AGENT_IDS),
62
+ description: z.string().min(1)
63
+ }).strict();
64
+ var Schemas = {
65
+ Skill: SkillFrontmatterSchema,
66
+ Cmd: CmdFrontmatterSchema,
67
+ Agent: AgentFrontmatterSchema
68
+ };
69
+ var ParseError = class extends Error {
70
+ constructor(file, issues) {
71
+ const messages = issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
72
+ super(`Frontmatter validation failed for ${file}:
73
+ ${messages}`);
74
+ this.file = file;
75
+ this.issues = issues;
76
+ this.name = "ParseError";
77
+ }
78
+ file;
79
+ issues;
80
+ };
81
+
82
+ // src/parser/common.ts
83
+ function parseWithSchema(filePath, source, schema) {
84
+ const parsed = matter(source);
85
+ const result = schema.safeParse(parsed.data);
86
+ if (!result.success) {
87
+ throw new ParseError(filePath, result.error.issues);
88
+ }
89
+ return {
90
+ meta: result.data,
91
+ body: parsed.content.trim(),
92
+ sourcePath: filePath
93
+ };
94
+ }
95
+
96
+ // src/parser/agent.ts
97
+ function parseAgent(filePath, source) {
98
+ return parseWithSchema(filePath, source, Schemas.Agent);
99
+ }
100
+
101
+ // src/parser/cmd.ts
102
+ function parseCmd(filePath, source) {
103
+ return parseWithSchema(filePath, source, Schemas.Cmd);
104
+ }
105
+
106
+ // src/parser/skill.ts
107
+ function parseSkill(filePath, source) {
108
+ return parseWithSchema(filePath, source, Schemas.Skill);
109
+ }
110
+
111
+ // src/util/yaml.ts
112
+ import { parse as yamlParse } from "yaml";
113
+ function parseYAML(source) {
114
+ return yamlParse(source);
115
+ }
116
+
117
+ // src/loader.ts
118
+ var DisableYamlSchema = z2.object({ disable: z2.array(z2.string()).optional() }).passthrough();
119
+ function loadHarness(packageDir, opts = {}) {
120
+ const agentsDir = opts.agentsDir ?? join(packageDir, "agents");
121
+ const skillsDir = opts.skillsDir ?? join(packageDir, "skills");
122
+ const commandsDir = opts.commandsDir ?? join(packageDir, "commands");
123
+ const agentFiles = fg.sync("*.agent.md", { cwd: agentsDir, absolute: true });
124
+ const skillFiles = fg.sync("*.skill.md", { cwd: skillsDir, absolute: true });
125
+ const cmdFiles = fg.sync("*.cmd.md", { cwd: commandsDir, absolute: true });
126
+ return {
127
+ agents: agentFiles.map((f) => parseAgent(f, readFileSync2(f, "utf8"))),
128
+ skills: skillFiles.map((f) => parseSkill(f, readFileSync2(f, "utf8"))),
129
+ cmds: cmdFiles.map((f) => parseCmd(f, readFileSync2(f, "utf8")))
130
+ };
131
+ }
132
+ function loadProjectSkills(targetDir) {
133
+ const skillsDir = join(targetDir, "docs", "skills");
134
+ if (!existsSync(skillsDir)) return [];
135
+ const skillFiles = fg.sync("*.skill.md", { cwd: skillsDir, absolute: true });
136
+ return skillFiles.map((f) => parseSkill(f, readFileSync2(f, "utf8")));
137
+ }
138
+ function loadOverrides(worktreeDir) {
139
+ const overridesDir = join(worktreeDir, ".harness-overrides");
140
+ if (!existsSync(overridesDir)) return void 0;
141
+ const added = loadHarness(overridesDir, {
142
+ agentsDir: join(overridesDir, "agents"),
143
+ skillsDir: join(overridesDir, "skills"),
144
+ commandsDir: join(overridesDir, "commands")
145
+ });
146
+ const disablePath = join(overridesDir, "disable.yaml");
147
+ let disable = [];
148
+ if (existsSync(disablePath)) {
149
+ try {
150
+ const parsed = DisableYamlSchema.safeParse(parseYAML(readFileSync2(disablePath, "utf8")));
151
+ if (parsed.success && parsed.data.disable) {
152
+ disable = parsed.data.disable;
153
+ }
154
+ } catch {
155
+ }
156
+ }
157
+ return { add: added, disable };
158
+ }
159
+
160
+ // src/mcp.ts
161
+ var MCP_SERVER_IDS = ["context7"];
162
+ var MCP_SERVERS = {
163
+ context7: {
164
+ id: "context7",
165
+ description: "Upstash Context7 \u2014 fetches up-to-date library docs via resolve-library-id + get-library-docs.",
166
+ command: "npx",
167
+ args: ["-y", "@upstash/context7-mcp"]
168
+ }
169
+ };
170
+ function resolveMcpIds(requested) {
171
+ return requested.map((id) => {
172
+ if (!MCP_SERVER_IDS.includes(id)) {
173
+ throw new Error(`unknown MCP server: ${id} (expected one of ${MCP_SERVER_IDS.join(", ")})`);
174
+ }
175
+ return id;
176
+ });
177
+ }
178
+
179
+ // src/renderers/common.ts
180
+ import { stringify as yamlStringify } from "yaml";
181
+ var STOP_HOOK_PACKAGE = "@vaubaan/harness";
182
+ function checkSkillCapabilities(skill, engine) {
183
+ const blockingWarnings = [];
184
+ const degradedWarnings = [];
185
+ const supported = CAPABILITIES_MATRIX[engine];
186
+ const degraded = DEGRADED_CAPABILITIES[engine] ?? [];
187
+ if (!skill.meta.requires) return { blockingWarnings, degradedWarnings };
188
+ for (const cap of skill.meta.requires) {
189
+ if (!supported.includes(cap)) {
190
+ blockingWarnings.push(
191
+ `skill "${skill.meta.id}" requires "${cap}" not supported by "${engine}" \u2014 skipped`
192
+ );
193
+ } else if (degraded.includes(cap)) {
194
+ degradedWarnings.push(
195
+ `skill "${skill.meta.id}" requires "${cap}" \u2014 supported by "${engine}" with degradation`
196
+ );
197
+ }
198
+ }
199
+ return { blockingWarnings, degradedWarnings };
200
+ }
201
+ function isSkillEngineAllowed(skill, engine) {
202
+ if (!skill.meta.engines) return true;
203
+ return skill.meta.engines.includes(engine);
204
+ }
205
+ function yamlFrontmatter(data) {
206
+ const rendered = yamlStringify(data, { lineWidth: 0 }).trimEnd();
207
+ return `---
208
+ ${rendered}
209
+ ---
210
+ `;
211
+ }
212
+ var SCALAR_NEEDS_QUOTE = /[^a-zA-Z0-9_./-]/;
213
+ function tomlEscapeString(s) {
214
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
215
+ }
216
+ function tomlValue(v) {
217
+ if (Array.isArray(v)) {
218
+ return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
219
+ }
220
+ if (typeof v === "string") {
221
+ if (v.includes("\n")) return tomlEscapeString(v);
222
+ return SCALAR_NEEDS_QUOTE.test(v) ? tomlEscapeString(v) : `"${v}"`;
223
+ }
224
+ if (typeof v === "boolean" || typeof v === "number") {
225
+ return String(v);
226
+ }
227
+ return tomlEscapeString(String(v));
228
+ }
229
+ function tomlMultilineString(s) {
230
+ const escaped = s.replace(/\\/g, "\\\\").replace(/"""/g, '""\\"');
231
+ return `"""
232
+ ${escaped}
233
+ """`;
234
+ }
235
+ function tomlQuoteString(s) {
236
+ return tomlValue(s);
237
+ }
238
+ function tomlInlineArray(items) {
239
+ return tomlValue([...items]);
240
+ }
241
+ function tomlInlineTable(table) {
242
+ const entries = Object.entries(table).map(([k, v]) => `${k} = ${tomlValue(v)}`);
243
+ return `{ ${entries.join(", ")} }`;
244
+ }
245
+ function frontmatterDataForSkill(skill) {
246
+ const data = {
247
+ name: skill.meta.id,
248
+ description: skill.meta.description
249
+ };
250
+ if (skill.meta.tags && skill.meta.tags.length > 0) data.tags = skill.meta.tags;
251
+ return data;
252
+ }
253
+ function frontmatterDataForCmd(cmd) {
254
+ const data = {
255
+ id: cmd.meta.id,
256
+ trigger: cmd.meta.trigger,
257
+ description: cmd.meta.description
258
+ };
259
+ if (cmd.meta.args && cmd.meta.args.length > 0) {
260
+ data.args = cmd.meta.args.map((a) => {
261
+ if (a.type === "enum") {
262
+ return a.default !== void 0 ? { name: a.name, type: a.type, values: a.values, default: a.default } : { name: a.name, type: a.type, values: a.values };
263
+ }
264
+ return a.default !== void 0 ? { name: a.name, type: a.type, default: a.default } : { name: a.name, type: a.type };
265
+ });
266
+ }
267
+ return data;
268
+ }
269
+ function buildAgentBody(agent) {
270
+ return `${agent.body.trim()}
271
+ `;
272
+ }
273
+ function buildUmbrellaBody(_agents, extras) {
274
+ const triggerBySkill = new Map(
275
+ extras.availableCommands.map((c) => [c.meta.invokes_skill, c.meta.trigger])
276
+ );
277
+ const skillLines = extras.availableSkills.map((s) => {
278
+ const trigger = triggerBySkill.get(s.meta.id);
279
+ const label = trigger ? `\`${s.meta.id}\` (\`${trigger}\`)` : `\`${s.meta.id}\``;
280
+ return `- ${label} \u2014 ${s.meta.description.split("\n")[0]}`;
281
+ });
282
+ const sections = [
283
+ "# How we work",
284
+ "",
285
+ "This file describes **how to work in this project**. The **business context** (what the project does, how to run it, which subsystems it contains) lives in `README.md` and `docs/`. Read them first.",
286
+ "",
287
+ "## Project map",
288
+ "",
289
+ "- `README.md` \u2014 pitch, quick start, scripts. Entry point.",
290
+ "- `docs/spec/<subsystem>.md` \u2014 *what* and *why* (tech-agnostic).",
291
+ "- `docs/handbook/<feature>.md` \u2014 *how it actually works* today.",
292
+ "- `docs/decisions/<slug>.md` \u2014 ADRs: structural choices and rejected alternatives.",
293
+ "- `docs/tests/integration-tests.md` & `e2e-tests.md` \u2014 core scenarios.",
294
+ "- `docs/workflow/coding-workflow.md` \u2014 CI, branches, PR, release.",
295
+ ...extras.hasProjectSkills ? ["- `docs/skills/*.skill.md` \u2014 the project's domain skills."] : [],
296
+ ...extras.hasQualityDoc ? [
297
+ "- `docs/quality/code-quality.md` \u2014 the project's code-quality rules: mechanical checks, judgment rules, accepted deviations. Read it before writing or auditing code."
298
+ ] : [],
299
+ "",
300
+ "## Doc-first",
301
+ "",
302
+ "The order is: **understand the docs \u2192 fill the gaps \u2192 code \u2192 update the docs \u2192 commit**. If the docs are silent or stale on something you change, fill the gap upfront with the doc-skill writers \u2014 not downstream, not after the fact.",
303
+ "",
304
+ "- Missing spec for a subsystem you touch \u2192 **do not code**, report the gap.",
305
+ "- Implicit structural decision in your changes \u2192 an ADR (`/decision`).",
306
+ "- Changed feature behavior \u2192 update the matching handbook (`/handbook`).",
307
+ "- Suspected code \u2194 doc drift \u2192 `/drift` before moving on."
308
+ ];
309
+ if (skillLines.length > 0) {
310
+ sections.push("", "## Doc-skills", "", skillLines.join("\n"));
311
+ }
312
+ return `${sections.join("\n")}
313
+ `;
314
+ }
315
+
316
+ // src/renderers/claude-code.ts
317
+ var ENGINE = "claude-code";
318
+ var BASE = ".claude";
319
+ var claudeCodeRenderer = {
320
+ engine: ENGINE,
321
+ renderSkill(skill) {
322
+ if (!isSkillEngineAllowed(skill, ENGINE)) {
323
+ return {
324
+ files: [],
325
+ warnings: [`skill "${skill.meta.id}" excluded from engine "${ENGINE}"`]
326
+ };
327
+ }
328
+ const check = checkSkillCapabilities(skill, ENGINE);
329
+ if (check.blockingWarnings.length > 0) {
330
+ return { files: [], warnings: check.blockingWarnings };
331
+ }
332
+ const content = `${yamlFrontmatter(frontmatterDataForSkill(skill))}
333
+ ${skill.body}
334
+ `;
335
+ return {
336
+ files: [{ path: `${BASE}/skills/${skill.meta.id}/SKILL.md`, content }],
337
+ warnings: check.degradedWarnings
338
+ };
339
+ },
340
+ renderCmd(cmd) {
341
+ const content = `${yamlFrontmatter(frontmatterDataForCmd(cmd))}
342
+ ${cmd.body}
343
+ `;
344
+ return {
345
+ files: [{ path: `${BASE}/commands/${cmd.meta.id}.md`, content }],
346
+ warnings: []
347
+ };
348
+ },
349
+ renderAgents(agents) {
350
+ const files = [];
351
+ for (const agent of agents) {
352
+ const subagentFrontmatter = yamlFrontmatter({
353
+ name: agent.meta.id,
354
+ description: agent.meta.description
355
+ });
356
+ const body = buildAgentBody(agent);
357
+ const content = `${subagentFrontmatter}
358
+ ${body}`;
359
+ files.push({ path: `${BASE}/agents/${agent.meta.id}.md`, content });
360
+ }
361
+ return { files, warnings: [] };
362
+ },
363
+ renderMcp(servers) {
364
+ if (servers.length === 0) return { files: [], warnings: [] };
365
+ const mcpServers = {};
366
+ for (const s of servers) {
367
+ const entry = { command: s.command, args: [...s.args] };
368
+ if (s.env) entry.env = s.env;
369
+ mcpServers[s.id] = entry;
370
+ }
371
+ const content = `${JSON.stringify({ mcpServers }, null, 2)}
372
+ `;
373
+ return { files: [{ path: ".mcp.json", content }], warnings: [] };
374
+ },
375
+ renderStopHook() {
376
+ const settings = {
377
+ hooks: {
378
+ Stop: [
379
+ {
380
+ matcher: "",
381
+ hooks: [
382
+ {
383
+ type: "command",
384
+ command: `npx -y ${STOP_HOOK_PACKAGE} stop-hook --engine claude`
385
+ }
386
+ ]
387
+ }
388
+ ]
389
+ }
390
+ };
391
+ return {
392
+ files: [{ path: `${BASE}/settings.json`, content: `${JSON.stringify(settings, null, 2)}
393
+ ` }],
394
+ warnings: []
395
+ };
396
+ }
397
+ };
398
+
399
+ // src/renderers/codex-cli.ts
400
+ var ENGINE2 = "codex-cli";
401
+ var BASE2 = ".codex";
402
+ var SKILLS_BASE = ".agents/skills";
403
+ var codexCliRenderer = {
404
+ engine: ENGINE2,
405
+ renderSkill(skill) {
406
+ if (!isSkillEngineAllowed(skill, ENGINE2)) {
407
+ return {
408
+ files: [],
409
+ warnings: [`skill "${skill.meta.id}" excluded from engine "${ENGINE2}"`]
410
+ };
411
+ }
412
+ const check = checkSkillCapabilities(skill, ENGINE2);
413
+ if (check.blockingWarnings.length > 0) {
414
+ return { files: [], warnings: check.blockingWarnings };
415
+ }
416
+ const content = `${yamlFrontmatter(frontmatterDataForSkill(skill))}
417
+ ${skill.body}
418
+ `;
419
+ return {
420
+ files: [{ path: `${SKILLS_BASE}/${skill.meta.id}/SKILL.md`, content }],
421
+ warnings: check.degradedWarnings
422
+ };
423
+ },
424
+ // Codex custom prompts are deprecated in favour of skills: nothing to render.
425
+ renderCmd(_cmd) {
426
+ return { files: [], warnings: [] };
427
+ },
428
+ renderAgents(agents) {
429
+ const files = [];
430
+ for (const agent of agents) {
431
+ const content = [
432
+ `name = ${tomlQuoteString(agent.meta.id)}`,
433
+ `description = ${tomlQuoteString(agent.meta.description)}`,
434
+ `developer_instructions = ${tomlMultilineString(buildAgentBody(agent).trimEnd())}`,
435
+ ""
436
+ ].join("\n");
437
+ files.push({ path: `${BASE2}/agents/${agent.meta.id}.toml`, content });
438
+ }
439
+ return { files, warnings: [] };
440
+ },
441
+ renderMcp(servers) {
442
+ if (servers.length === 0) return { files: [], warnings: [] };
443
+ const blocks = [];
444
+ for (const s of servers) {
445
+ const lines = [
446
+ `[mcp_servers.${s.id}]`,
447
+ `command = ${tomlQuoteString(s.command)}`,
448
+ `args = ${tomlInlineArray([...s.args])}`
449
+ ];
450
+ if (s.env) lines.push(`env = ${tomlInlineTable(s.env)}`);
451
+ blocks.push(lines.join("\n"));
452
+ }
453
+ const content = `${blocks.join("\n\n")}
454
+ `;
455
+ return { files: [{ path: `${BASE2}/config.toml`, content }], warnings: [] };
456
+ },
457
+ renderStopHook() {
458
+ const hooks = {
459
+ hooks: {
460
+ Stop: [
461
+ {
462
+ hooks: [
463
+ {
464
+ type: "command",
465
+ command: `npx -y ${STOP_HOOK_PACKAGE} stop-hook --engine codex`
466
+ }
467
+ ]
468
+ }
469
+ ]
470
+ }
471
+ };
472
+ return {
473
+ files: [{ path: `${BASE2}/hooks.json`, content: `${JSON.stringify(hooks, null, 2)}
474
+ ` }],
475
+ warnings: []
476
+ };
477
+ }
478
+ };
479
+
480
+ // src/renderers/opencode.ts
481
+ var ENGINE3 = "opencode";
482
+ var BASE3 = ".opencode";
483
+ var opencodeRenderer = {
484
+ engine: ENGINE3,
485
+ renderSkill(skill) {
486
+ if (!isSkillEngineAllowed(skill, ENGINE3)) {
487
+ return {
488
+ files: [],
489
+ warnings: [`skill "${skill.meta.id}" excluded from engine "${ENGINE3}"`]
490
+ };
491
+ }
492
+ const check = checkSkillCapabilities(skill, ENGINE3);
493
+ if (check.blockingWarnings.length > 0) {
494
+ return { files: [], warnings: check.blockingWarnings };
495
+ }
496
+ const content = `${yamlFrontmatter(frontmatterDataForSkill(skill))}
497
+ ${skill.body}
498
+ `;
499
+ return {
500
+ files: [{ path: `${BASE3}/skills/${skill.meta.id}/SKILL.md`, content }],
501
+ warnings: check.degradedWarnings
502
+ };
503
+ },
504
+ renderCmd(cmd) {
505
+ const content = `${yamlFrontmatter({ description: cmd.meta.description })}
506
+ ${cmd.body}
507
+ `;
508
+ return {
509
+ files: [{ path: `${BASE3}/commands/${cmd.meta.id}.md`, content }],
510
+ warnings: []
511
+ };
512
+ },
513
+ renderAgents(agents) {
514
+ const files = [];
515
+ for (const agent of agents) {
516
+ const fm = yamlFrontmatter({
517
+ description: agent.meta.description,
518
+ mode: "all"
519
+ });
520
+ const body = buildAgentBody(agent);
521
+ files.push({ path: `${BASE3}/agents/${agent.meta.id}.md`, content: `${fm}
522
+ ${body}` });
523
+ }
524
+ return { files, warnings: [] };
525
+ },
526
+ renderMcp(servers) {
527
+ if (servers.length === 0) return { files: [], warnings: [] };
528
+ const mcp = {};
529
+ for (const s of servers) {
530
+ const entry = {
531
+ type: "local",
532
+ command: [s.command, ...s.args],
533
+ enabled: true
534
+ };
535
+ if (s.env) entry.environment = s.env;
536
+ mcp[s.id] = entry;
537
+ }
538
+ const config = { $schema: "https://opencode.ai/config.json", mcp };
539
+ const content = `${JSON.stringify(config, null, 2)}
540
+ `;
541
+ return { files: [{ path: `${BASE3}/opencode.json`, content }], warnings: [] };
542
+ },
543
+ renderStopHook() {
544
+ const content = `import { spawnSync } from "node:child_process";
545
+
546
+ // Runs the harness lint and returns the report to inject, or "" when the
547
+ // turn may end.
548
+ const lintReport = (directory, sessionID) => {
549
+ const r = spawnSync(
550
+ "npx",
551
+ ["-y", "${STOP_HOOK_PACKAGE}", "stop-hook", "--engine", "opencode", "--cwd", directory],
552
+ { input: JSON.stringify({ sessionID }), encoding: "utf-8" },
553
+ );
554
+ const report = (r.stderr ?? "").trim();
555
+ return r.status === 2 ? report : "";
556
+ };
557
+
558
+ // OpenCode 1.x entry point, exposed as server() below.
559
+ const HarnessStopHook = async ({ client, directory }) => ({
560
+ event: async ({ event }) => {
561
+ if (event.type !== "session.idle") return;
562
+ const sessionID = event.properties?.sessionID;
563
+ if (!sessionID) return;
564
+ const report = lintReport(directory, sessionID);
565
+ if (report === "") return;
566
+ // OpenCode defaults to its build agent when a prompt omits agent.
567
+ // Preserve the last conversation persona, excluding compaction summaries.
568
+ const history = await client.session.messages({ path: { id: sessionID } }).catch(() => undefined);
569
+ if (history?.error || !Array.isArray(history?.data)) return;
570
+ const current = history.data.findLast(({ info }) =>
571
+ (info.role === "user" || (info.role === "assistant" && !info.summary)) &&
572
+ typeof info.agent === "string" && info.agent.trim() !== "",
573
+ );
574
+ if (!current) return;
575
+ await client.session.prompt({
576
+ path: { id: sessionID },
577
+ body: { agent: current.info.agent, parts: [{ type: "text", text: report }] },
578
+ });
579
+ },
580
+ });
581
+
582
+ // One default export for both majors: OpenCode 1.x calls server(),
583
+ // OpenCode 2.x calls setup(). A V2 session keeps its agent, so the report
584
+ // is sent without naming the persona.
585
+ export default {
586
+ id: "vaubaan-harness-stop-hook",
587
+ server: HarnessStopHook,
588
+ setup(context) {
589
+ const directory = context.location.directory;
590
+ const controller = new AbortController();
591
+ const listen = async () => {
592
+ for await (const event of context.event.subscribe({ signal: controller.signal })) {
593
+ if (event.type !== "session.idle") continue;
594
+ if (event.location?.directory && event.location.directory !== directory) continue;
595
+ const sessionID = event.data?.sessionID;
596
+ if (!sessionID) continue;
597
+ const report = lintReport(directory, sessionID);
598
+ if (report === "") continue;
599
+ await context.session.prompt({ sessionID, text: report }).catch(() => undefined);
600
+ }
601
+ };
602
+ listen().catch(() => undefined);
603
+ return () => controller.abort();
604
+ },
605
+ };
606
+ `;
607
+ return {
608
+ files: [{ path: `${BASE3}/plugins/harness-stop-hook.ts`, content }],
609
+ warnings: []
610
+ };
611
+ }
612
+ };
613
+
614
+ // src/renderers/index.ts
615
+ var RENDERERS = {
616
+ "claude-code": claudeCodeRenderer,
617
+ "codex-cli": codexCliRenderer,
618
+ opencode: opencodeRenderer
619
+ };
620
+ function getRenderer(engine) {
621
+ return RENDERERS[engine];
622
+ }
623
+
624
+ // src/render.ts
625
+ var RenderError = class extends Error {
626
+ constructor(message) {
627
+ super(message);
628
+ this.name = "RenderError";
629
+ }
630
+ };
631
+ function mergeById(stock, added) {
632
+ const result = /* @__PURE__ */ new Map();
633
+ for (const item of stock) result.set(item.meta.id, item);
634
+ if (added) {
635
+ for (const item of added) result.set(item.meta.id, item);
636
+ }
637
+ return Array.from(result.values());
638
+ }
639
+ function removeByIds(items, disabled) {
640
+ if (disabled.length === 0) return [...items];
641
+ const disabledSet = new Set(disabled);
642
+ return items.filter((item) => !disabledSet.has(item.meta.id));
643
+ }
644
+ function applyOverrides(harness, overrides) {
645
+ const disabled = overrides?.disable ?? [];
646
+ return {
647
+ agents: removeByIds(mergeById(harness.agents, overrides?.add?.agents), disabled),
648
+ skills: removeByIds(mergeById(harness.skills, overrides?.add?.skills), disabled),
649
+ cmds: removeByIds(mergeById(harness.cmds, overrides?.add?.cmds), disabled)
650
+ };
651
+ }
652
+ var CLAUDE_MD_STUB = "@AGENTS.md\n";
653
+ function opencodeLast(engines) {
654
+ return [...engines].sort((a, b) => Number(a === "opencode") - Number(b === "opencode"));
655
+ }
656
+ function render(input) {
657
+ const { engines } = input;
658
+ if (engines.length === 0) {
659
+ throw new RenderError("at least one engine must be selected");
660
+ }
661
+ const base = input.projectSkills?.length ? { ...input.harness, skills: mergeById(input.harness.skills, input.projectSkills) } : input.harness;
662
+ const harness = applyOverrides(base, input.overrides);
663
+ if (harness.agents.length === 0) {
664
+ throw new RenderError("No agents found in harness");
665
+ }
666
+ const files = [];
667
+ const warnings = [];
668
+ const exposedSkills = /* @__PURE__ */ new Map();
669
+ const exposedCmds = /* @__PURE__ */ new Map();
670
+ for (const engine of opencodeLast(engines)) {
671
+ const renderer = getRenderer(engine);
672
+ const renderedSkills = [];
673
+ for (const skill of harness.skills) {
674
+ const result = renderer.renderSkill(skill);
675
+ warnings.push(...result.warnings);
676
+ if (result.files.length > 0) {
677
+ const alreadyVisible = engine === "opencode" && exposedSkills.has(skill.meta.id);
678
+ if (!alreadyVisible) files.push(...result.files);
679
+ renderedSkills.push(skill);
680
+ exposedSkills.set(skill.meta.id, skill);
681
+ }
682
+ }
683
+ const renderedSkillIds = new Set(renderedSkills.map((s) => s.meta.id));
684
+ const renderedCmds = [];
685
+ for (const cmd of harness.cmds) {
686
+ if (!renderedSkillIds.has(cmd.meta.invokes_skill)) continue;
687
+ const result = renderer.renderCmd(cmd);
688
+ warnings.push(...result.warnings);
689
+ if (result.files.length > 0) {
690
+ files.push(...result.files);
691
+ renderedCmds.push(cmd);
692
+ exposedCmds.set(cmd.meta.id, cmd);
693
+ }
694
+ }
695
+ const agentsResult = renderer.renderAgents(harness.agents);
696
+ warnings.push(...agentsResult.warnings);
697
+ files.push(...agentsResult.files);
698
+ if (input.mcpServers && input.mcpServers.length > 0) {
699
+ const specs = input.mcpServers.map((id) => MCP_SERVERS[id]);
700
+ const mcpResult = renderer.renderMcp(specs);
701
+ warnings.push(...mcpResult.warnings);
702
+ files.push(...mcpResult.files);
703
+ }
704
+ if (input.withStopHook !== false) {
705
+ const hookResult = renderer.renderStopHook();
706
+ warnings.push(...hookResult.warnings);
707
+ files.push(...hookResult.files);
708
+ }
709
+ }
710
+ const hasProjectSkills = (input.projectSkills?.length ?? 0) > 0;
711
+ const umbrellaContent = buildUmbrellaBody(harness.agents, {
712
+ availableSkills: Array.from(exposedSkills.values()),
713
+ availableCommands: Array.from(exposedCmds.values()),
714
+ hasProjectSkills,
715
+ hasQualityDoc: input.hasQualityDoc === true
716
+ });
717
+ files.push({ path: "AGENTS.md", content: umbrellaContent });
718
+ if (engines.includes("claude-code")) {
719
+ files.push({ path: "CLAUDE.md", content: CLAUDE_MD_STUB });
720
+ }
721
+ return { files, warnings };
722
+ }
723
+
724
+ // src/cli/managed-paths.ts
725
+ import { lstatSync, realpathSync, statSync } from "fs";
726
+ import { basename, dirname, isAbsolute, join as join2, resolve, win32 } from "path";
727
+ var MANAGED_FILES = /* @__PURE__ */ new Set([
728
+ "AGENTS.md",
729
+ "CLAUDE.md",
730
+ ".mcp.json",
731
+ ".harness-manifest.json",
732
+ ".claude/settings.json",
733
+ ".codex/config.toml",
734
+ ".codex/hooks.json",
735
+ ".opencode/opencode.json",
736
+ ".opencode/mcp.json"
737
+ ]);
738
+ var MANAGED_DIRECTORIES = [
739
+ ".claude/agents/",
740
+ ".claude/commands/",
741
+ ".claude/skills/",
742
+ ".codex/agents/",
743
+ ".codex/commands/",
744
+ ".codex/skills/",
745
+ ".agents/skills/",
746
+ ".opencode/agents/",
747
+ ".opencode/commands/",
748
+ ".opencode/skills/",
749
+ ".opencode/plugins/"
750
+ ];
751
+ function canonicalTargetRoot(root) {
752
+ let ancestor = resolve(root);
753
+ const missing = [];
754
+ while (!lstatSync(ancestor, { throwIfNoEntry: false })) {
755
+ missing.unshift(basename(ancestor));
756
+ ancestor = dirname(ancestor);
757
+ }
758
+ const canonical = realpathSync(ancestor);
759
+ if (!statSync(canonical).isDirectory()) {
760
+ throw new Error(`harness target is not a directory: ${root}`);
761
+ }
762
+ return join2(canonical, ...missing);
763
+ }
764
+ function assertManagedLocation(path) {
765
+ const invalidSegment = path.split("/").some((part) => part === "" || part === "." || part === "..");
766
+ if (isAbsolute(path) || win32.isAbsolute(path) || path.includes("\\") || path.includes("\0") || invalidSegment) {
767
+ throw new Error(`unsafe harness path: ${path}`);
768
+ }
769
+ if (!MANAGED_FILES.has(path) && !MANAGED_DIRECTORIES.some((prefix) => path.startsWith(prefix))) {
770
+ throw new Error(`path is outside harness-managed locations: ${path}`);
771
+ }
772
+ }
773
+ function assertNoSymbolicComponents(root, path) {
774
+ const parts = path.split("/");
775
+ let current = root;
776
+ for (const [index, part] of parts.entries()) {
777
+ current = join2(current, part);
778
+ const info = lstatSync(current, { throwIfNoEntry: false });
779
+ if (!info) return;
780
+ if (info.isSymbolicLink()) {
781
+ throw new Error(`symbolic link in harness path: ${path}`);
782
+ }
783
+ const isLeaf = index === parts.length - 1;
784
+ if (isLeaf ? !info.isFile() : !info.isDirectory()) {
785
+ throw new Error(`unexpected file type in harness path: ${path}`);
786
+ }
787
+ }
788
+ }
789
+ function assertManagedPaths(root, files) {
790
+ for (const path of files) assertManagedLocation(path);
791
+ const canonicalRoot = canonicalTargetRoot(root);
792
+ for (const path of files) assertNoSymbolicComponents(canonicalRoot, path);
793
+ }
794
+
795
+ // src/cli/manifest.ts
796
+ import { createHash } from "crypto";
797
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync } from "fs";
798
+ import { join as join5 } from "path";
799
+ import { z as z5 } from "zod";
800
+
801
+ // src/version.ts
802
+ var VERSION = "1.3.2";
803
+
804
+ // src/cli/opencode-config.ts
805
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
806
+ import { join as join3 } from "path";
807
+ import { isDeepStrictEqual } from "util";
808
+ import { z as z3 } from "zod";
809
+ var OPENCODE_CONFIG_PATH = ".opencode/opencode.json";
810
+ var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
811
+ var ObjectSchema = z3.record(z3.unknown());
812
+ var OpenCodeConfigOwnershipSchema = z3.object({
813
+ createdFile: z3.boolean(),
814
+ addedMcp: z3.boolean(),
815
+ addedSchema: z3.boolean(),
816
+ entries: z3.record(ObjectSchema)
817
+ });
818
+ function parseConfig(content) {
819
+ try {
820
+ const config = ObjectSchema.parse(JSON.parse(content));
821
+ if (Object.hasOwn(config, "mcp")) ObjectSchema.parse(config.mcp);
822
+ return config;
823
+ } catch {
824
+ throw new Error(`invalid ${OPENCODE_CONFIG_PATH}: expected a JSON object with an MCP object`);
825
+ }
826
+ }
827
+ function removeOwnedEntries(config, ownership, warnings) {
828
+ if (Object.hasOwn(config, "mcp")) {
829
+ const mcp = ObjectSchema.parse(config.mcp);
830
+ for (const [id, installed] of Object.entries(ownership.entries)) {
831
+ if (!Object.hasOwn(mcp, id)) continue;
832
+ if (isDeepStrictEqual(mcp[id], installed)) delete mcp[id];
833
+ else warnings.push(`preserved modified OpenCode MCP server: ${id}`);
834
+ }
835
+ config.mcp = mcp;
836
+ if (ownership.addedMcp && Object.keys(mcp).length === 0) Reflect.deleteProperty(config, "mcp");
837
+ }
838
+ if (ownership.addedSchema && config.$schema === OPENCODE_SCHEMA)
839
+ Reflect.deleteProperty(config, "$schema");
840
+ }
841
+ function addMissingEntries(config, desired, createdFile, warnings) {
842
+ const requested = ObjectSchema.parse(parseConfig(desired.content).mcp);
843
+ const mcp = ObjectSchema.parse(config.mcp ?? {});
844
+ const entries = {};
845
+ for (const [id, entry] of Object.entries(requested)) {
846
+ if (Object.hasOwn(mcp, id)) {
847
+ if (!isDeepStrictEqual(mcp[id], entry)) {
848
+ warnings.push(`preserved existing OpenCode MCP server instead of replacing it: ${id}`);
849
+ }
850
+ continue;
851
+ }
852
+ entries[id] = ObjectSchema.parse(entry);
853
+ mcp[id] = entry;
854
+ }
855
+ if (Object.keys(entries).length === 0) return void 0;
856
+ const ownership = {
857
+ createdFile,
858
+ addedMcp: !Object.hasOwn(config, "mcp"),
859
+ addedSchema: !Object.hasOwn(config, "$schema"),
860
+ entries
861
+ };
862
+ config.mcp = mcp;
863
+ if (ownership.addedSchema) config.$schema = OPENCODE_SCHEMA;
864
+ return ownership;
865
+ }
866
+ function planOpenCodeConfig(root, desired, previous) {
867
+ const warnings = [];
868
+ if (!desired && !previous) return { remove: false, warnings };
869
+ const path = join3(root, OPENCODE_CONFIG_PATH);
870
+ const existed = existsSync2(path);
871
+ if (!existed && !desired) return { remove: false, warnings };
872
+ const original = existed ? readFileSync3(path, "utf8") : void 0;
873
+ const config = original === void 0 ? {} : parseConfig(original);
874
+ const before = structuredClone(config);
875
+ if (previous) removeOwnedEntries(config, previous, warnings);
876
+ const createdFile = !existed || previous?.createdFile === true;
877
+ const ownership = desired ? addMissingEntries(config, desired, createdFile, warnings) : void 0;
878
+ if (!ownership && createdFile && Object.keys(config).length === 0) {
879
+ return { remove: existed, warnings };
880
+ }
881
+ return {
882
+ remove: false,
883
+ ownership,
884
+ warnings,
885
+ ...!existed || !isDeepStrictEqual(before, config) ? { write: { path: OPENCODE_CONFIG_PATH, content: `${JSON.stringify(config, null, 2)}
886
+ ` } } : {}
887
+ };
888
+ }
889
+
890
+ // src/cli/update-config.ts
891
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
892
+ import { join as join4 } from "path";
893
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
894
+ import { z as z4 } from "zod";
895
+ var ObjectSchema2 = z4.record(z4.unknown());
896
+ var HookEntrySchema = z4.object({ group: ObjectSchema2, hook: ObjectSchema2 });
897
+ var HookConfigOwnershipSchema = z4.object({
898
+ createdFile: z4.boolean(),
899
+ addedHooks: z4.boolean(),
900
+ addedStop: z4.boolean(),
901
+ entries: z4.array(HookEntrySchema)
902
+ });
903
+ var HookConfigsOwnershipSchema = z4.object({
904
+ ".claude/settings.json": HookConfigOwnershipSchema.optional(),
905
+ ".codex/hooks.json": HookConfigOwnershipSchema.optional()
906
+ }).strict();
907
+
908
+ // src/cli/manifest.ts
909
+ var MANIFEST_FILENAME = ".harness-manifest.json";
910
+ var MANIFEST_VERSION = "6";
911
+ var ManifestSchema = z5.object({
912
+ version: z5.enum(["4", "5", MANIFEST_VERSION]),
913
+ engines: z5.array(z5.string()),
914
+ renderedAt: z5.string(),
915
+ files: z5.array(z5.string()),
916
+ stopHookInstalled: z5.boolean().optional(),
917
+ stopHookEngines: z5.array(z5.string()).optional(),
918
+ opencodeConfig: OpenCodeConfigOwnershipSchema.optional(),
919
+ hookConfigs: HookConfigsOwnershipSchema.optional(),
920
+ harnessVersion: z5.string().optional(),
921
+ overridesDir: z5.string().optional(),
922
+ fileHashes: z5.record(z5.string().regex(/^[a-f0-9]{64}$/)).optional(),
923
+ preservedFiles: z5.array(z5.string()).optional()
924
+ });
925
+ function manifestPath(targetDir) {
926
+ return join5(targetDir, MANIFEST_FILENAME);
927
+ }
928
+ function writeManifest(targetDir, engines, files, extras = {}) {
929
+ assertManagedPaths(targetDir, [MANIFEST_FILENAME]);
930
+ const manifest = {
931
+ version: MANIFEST_VERSION,
932
+ harnessVersion: VERSION,
933
+ engines: [...engines].sort(),
934
+ renderedAt: (/* @__PURE__ */ new Date()).toISOString(),
935
+ files: [...files].sort(),
936
+ fileHashes: Object.fromEntries(
937
+ files.filter((file) => existsSync4(join5(targetDir, file))).sort().map((file) => [file, hashContent(readFileSync5(join5(targetDir, file)))])
938
+ ),
939
+ ...extras.overridesDir ? { overridesDir: extras.overridesDir } : {},
940
+ ...extras.preservedFiles ? { preservedFiles: extras.preservedFiles } : {},
941
+ ...extras.hookConfigs ? { hookConfigs: extras.hookConfigs } : {},
942
+ ...extras.opencodeConfig ? { opencodeConfig: extras.opencodeConfig } : {},
943
+ ...extras.stopHookInstalled !== void 0 ? { stopHookInstalled: extras.stopHookInstalled } : {},
944
+ ...extras.stopHookEngines !== void 0 ? { stopHookEngines: [...extras.stopHookEngines].sort() } : {}
945
+ };
946
+ const path = manifestPath(targetDir);
947
+ if (existsSync4(path)) chmodSync(path, 384);
948
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}
949
+ `, { mode: 384 });
950
+ }
951
+ function hashContent(content) {
952
+ return createHash("sha256").update(content).digest("hex");
953
+ }
954
+ function readManifest(targetDir) {
955
+ assertManagedPaths(targetDir, [MANIFEST_FILENAME]);
956
+ const path = manifestPath(targetDir);
957
+ if (!existsSync4(path)) return null;
958
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
959
+ return ManifestSchema.parse(raw);
960
+ }
961
+
962
+ // src/cli/uninstall.ts
963
+ import { existsSync as existsSync5, readdirSync, rmdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
964
+ import { dirname as dirname2, join as join6, relative, resolve as resolve2 } from "path";
965
+ function removeRenderedFiles(root, files) {
966
+ assertManagedPaths(root, files);
967
+ const filesRemoved = [];
968
+ const missing = [];
969
+ const dirsToConsider = /* @__PURE__ */ new Set();
970
+ for (const rel of files) {
971
+ const full = join6(root, rel);
972
+ if (existsSync5(full)) {
973
+ unlinkSync(full);
974
+ filesRemoved.push(rel);
975
+ dirsToConsider.add(dirname2(full));
976
+ } else {
977
+ missing.push(rel);
978
+ }
979
+ }
980
+ return { filesRemoved, dirsPruned: pruneEmptyDirs(root, dirsToConsider), missing };
981
+ }
982
+ function pruneEmptyDirs(root, leaves) {
983
+ const candidates = /* @__PURE__ */ new Set();
984
+ for (const leaf of leaves) {
985
+ let cur = leaf;
986
+ while (cur !== root && cur.startsWith(`${root}/`)) {
987
+ candidates.add(cur);
988
+ cur = dirname2(cur);
989
+ }
990
+ }
991
+ const pruned = [];
992
+ const sorted = [...candidates].sort((a, b) => b.length - a.length);
993
+ for (const dir of sorted) {
994
+ if (!existsSync5(dir)) continue;
995
+ if (readdirSync(dir).length > 0) continue;
996
+ rmdirSync(dir);
997
+ pruned.push(relative(root, dir));
998
+ }
999
+ return pruned;
1000
+ }
1001
+
1002
+ // src/cli/init.ts
1003
+ var QUALITY_DOC_PATH = "docs/quality/code-quality.md";
1004
+ function resolveEngineList(requested) {
1005
+ if (!requested || requested.length === 0) return [...ENGINES];
1006
+ return requested.map((e) => {
1007
+ if (!ENGINES.includes(e)) {
1008
+ throw new Error(`unknown engine: ${e} (expected one of ${ENGINES.join(", ")})`);
1009
+ }
1010
+ return e;
1011
+ });
1012
+ }
1013
+ function defaultPackageDir() {
1014
+ const here = dirname3(fileURLToPath(import.meta.url));
1015
+ if (existsSync6(join7(here, "agents"))) return here;
1016
+ return resolve3(here, "..");
1017
+ }
1018
+ function planInit(args) {
1019
+ const engines = resolveEngineList(args.engines);
1020
+ const packageDir = args.packageDir ?? defaultPackageDir();
1021
+ const harness = loadHarness(packageDir);
1022
+ const overrides = args.overridesDir ? loadOverrides(args.overridesDir) : void 0;
1023
+ const projectSkills = loadProjectSkills(args.targetDir);
1024
+ const hasQualityDoc = existsSync6(join7(args.targetDir, QUALITY_DOC_PATH));
1025
+ const mcpServers = args.mcpServers ? resolveMcpIds(args.mcpServers) : void 0;
1026
+ const stopHookInstalled = args.withStopHook !== false;
1027
+ const output = render({
1028
+ harness,
1029
+ engines,
1030
+ overrides,
1031
+ projectSkills,
1032
+ hasQualityDoc,
1033
+ mcpServers,
1034
+ withStopHook: stopHookInstalled
1035
+ });
1036
+ return { engines, output, stopHookInstalled };
1037
+ }
1038
+ function writeFilesToTarget(files, targetDir) {
1039
+ assertManagedPaths(
1040
+ targetDir,
1041
+ files.map((file) => file.path)
1042
+ );
1043
+ const written = [];
1044
+ for (const f of files) {
1045
+ const full = join7(targetDir, f.path);
1046
+ mkdirSync(dirname3(full), { recursive: true });
1047
+ writeFileSync3(full, f.content);
1048
+ written.push(f.path);
1049
+ }
1050
+ return written;
1051
+ }
1052
+ function initCommand(args) {
1053
+ const { engines, output, stopHookInstalled } = planInit(args);
1054
+ const previous = readManifest(args.targetDir);
1055
+ assertManagedPaths(args.targetDir, [
1056
+ MANIFEST_FILENAME,
1057
+ ...previous?.files ?? [],
1058
+ ...output.files.map((file) => file.path),
1059
+ ...previous?.opencodeConfig ? [OPENCODE_CONFIG_PATH] : []
1060
+ ]);
1061
+ const desiredConfig = output.files.find((file) => file.path === OPENCODE_CONFIG_PATH);
1062
+ const config = planOpenCodeConfig(args.targetDir, desiredConfig, previous?.opencodeConfig);
1063
+ const next = new Set(output.files.map((f) => f.path));
1064
+ const stale = (previous?.files ?? []).filter((f) => !next.has(f) && f !== OPENCODE_CONFIG_PATH);
1065
+ if (config.remove) stale.push(OPENCODE_CONFIG_PATH);
1066
+ const staleFilesRemoved = removeRenderedFiles(resolve3(args.targetDir), stale).filesRemoved;
1067
+ const ownedFiles = output.files.filter((file) => file.path !== OPENCODE_CONFIG_PATH);
1068
+ const filesWritten = writeFilesToTarget(ownedFiles, args.targetDir);
1069
+ if (config.write) writeFilesToTarget([config.write], args.targetDir);
1070
+ writeManifest(args.targetDir, engines, filesWritten, {
1071
+ stopHookInstalled,
1072
+ stopHookEngines: stopHookInstalled ? engines : void 0,
1073
+ opencodeConfig: config.ownership,
1074
+ overridesDir: args.overridesDir ? resolve3(args.overridesDir) === resolve3(args.targetDir) ? "." : resolve3(args.overridesDir) : void 0
1075
+ });
1076
+ if (config.write) filesWritten.push(OPENCODE_CONFIG_PATH);
1077
+ const warnings = [...output.warnings, ...config.warnings];
1078
+ if (previous?.files.includes(OPENCODE_CONFIG_PATH) && !previous.opencodeConfig) {
1079
+ warnings.push(`preserved legacy shared configuration: ${OPENCODE_CONFIG_PATH}`);
1080
+ }
1081
+ return { filesWritten, staleFilesRemoved, warnings, stopHookInstalled };
1082
+ }
1083
+ export {
1084
+ initCommand,
1085
+ planInit,
1086
+ writeFilesToTarget
1087
+ };
1088
+ //# sourceMappingURL=init.js.map