harnessed 3.4.1 → 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync, spawn } from 'child_process';
3
- import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from 'fs';
3
+ import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync, appendFileSync } from 'fs';
4
4
  import { join, resolve, dirname, relative } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { Type } from '@sinclair/typebox';
7
7
  import { Value } from '@sinclair/typebox/value';
8
8
  import { LineCounter, parseDocument, parse, isSeq, isScalar } from 'yaml';
9
- import { readFile, readdir, unlink, writeFile, stat, rm, cp, access, mkdir, rename } from 'fs/promises';
9
+ import { readFile, readdir, unlink, writeFile, stat, rm, cp, mkdir, access, rename } from 'fs/promises';
10
10
  import lockfile from 'proper-lockfile';
11
11
  import { Command } from 'commander';
12
12
  import { Ajv } from 'ajv';
@@ -847,7 +847,7 @@ var init_resume = __esm({
847
847
 
848
848
  // package.json
849
849
  var package_default = {
850
- version: "3.4.1"};
850
+ version: "3.4.3"};
851
851
 
852
852
  // src/manifest/errors.ts
853
853
  function instancePathToKeyPath(instancePath) {
@@ -4854,6 +4854,109 @@ ${t("rollback.metadata_unreadable.fix")}`
4854
4854
  console.log(t("rollback.restored", { count: meta.files.length, timestamp }));
4855
4855
  });
4856
4856
  }
4857
+ function readInstalledPlugins(homedirOverride) {
4858
+ const home = homedir();
4859
+ const path = join(home, ".claude", "plugins", "installed_plugins.json");
4860
+ let raw;
4861
+ try {
4862
+ raw = readFileSync(path, "utf8");
4863
+ } catch {
4864
+ return /* @__PURE__ */ new Set();
4865
+ }
4866
+ let parsed;
4867
+ try {
4868
+ parsed = JSON.parse(raw);
4869
+ } catch {
4870
+ return /* @__PURE__ */ new Set();
4871
+ }
4872
+ if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ new Set();
4873
+ const plugins = parsed.plugins;
4874
+ if (!plugins || typeof plugins !== "object") return /* @__PURE__ */ new Set();
4875
+ const out = /* @__PURE__ */ new Set();
4876
+ for (const key of Object.keys(plugins)) {
4877
+ const at = key.indexOf("@");
4878
+ if (at <= 0) continue;
4879
+ out.add(key.slice(0, at));
4880
+ }
4881
+ return out;
4882
+ }
4883
+ function readInstalledUserSkills(homedirOverride) {
4884
+ const home = homedir();
4885
+ const skillsRoot = join(home, ".claude", "skills");
4886
+ try {
4887
+ const entries = readdirSync(skillsRoot, { withFileTypes: true });
4888
+ const out = /* @__PURE__ */ new Set();
4889
+ for (const e of entries) if (e.isDirectory()) out.add(e.name);
4890
+ return out;
4891
+ } catch {
4892
+ return /* @__PURE__ */ new Set();
4893
+ }
4894
+ }
4895
+ function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills) {
4896
+ const { cmd, install_type, plugin_id, skill_dir } = capability;
4897
+ if (!install_type) return { renderedCmd: cmd };
4898
+ const types = Array.isArray(install_type) ? install_type : [install_type];
4899
+ const uniqueTypes = [...new Set(types)];
4900
+ const missingHints = [];
4901
+ let anyDetected = false;
4902
+ for (const t2 of uniqueTypes) {
4903
+ if (t2 === "plugin") {
4904
+ if (!plugin_id) {
4905
+ missingHints.push(
4906
+ `install_type=plugin declared but no plugin_id (capabilities.yaml schema bug)`
4907
+ );
4908
+ continue;
4909
+ }
4910
+ if (installedPlugins.has(plugin_id)) {
4911
+ anyDetected = true;
4912
+ break;
4913
+ }
4914
+ missingHints.push(`plugin '${plugin_id}' (\`claude plugin install ${plugin_id}\`)`);
4915
+ } else {
4916
+ if (!skill_dir) {
4917
+ missingHints.push(
4918
+ `install_type=user-skill declared but no skill_dir (capabilities.yaml schema bug)`
4919
+ );
4920
+ continue;
4921
+ }
4922
+ if (installedUserSkills.has(skill_dir)) {
4923
+ anyDetected = true;
4924
+ break;
4925
+ }
4926
+ missingHints.push(
4927
+ `user-skill '${skill_dir}' under ~/.claude/skills/ (git clone the official repo; e.g. gstack: \`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup\`)`
4928
+ );
4929
+ }
4930
+ }
4931
+ if (anyDetected) return { renderedCmd: cmd };
4932
+ const prefix = uniqueTypes.length > 1 ? "[multi]" : `[${uniqueTypes[0]}]`;
4933
+ const joined = missingHints.join(" OR ");
4934
+ return {
4935
+ renderedCmd: cmd,
4936
+ warning: `${prefix} '${cmd}' backing missing \u2014 install either: ${joined}.`
4937
+ };
4938
+ }
4939
+ var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
4940
+ function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
4941
+ const warningsSet = /* @__PURE__ */ new Set();
4942
+ const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
4943
+ const cap = capabilities[name];
4944
+ if (!cap) {
4945
+ warningsSet.add(
4946
+ `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
4947
+ );
4948
+ return match2;
4949
+ }
4950
+ const { renderedCmd, warning } = resolveCapabilityCmd(
4951
+ cap,
4952
+ installedPlugins,
4953
+ installedUserSkills
4954
+ );
4955
+ if (warning) warningsSet.add(warning);
4956
+ return renderedCmd;
4957
+ });
4958
+ return { body: out, warnings: [...warningsSet] };
4959
+ }
4857
4960
 
4858
4961
  // src/cli/lib/enableAgentTeamsInSettings.ts
4859
4962
  init_harnessedRoot();
@@ -5020,72 +5123,121 @@ async function atomicWrite2(path, content) {
5020
5123
  return `write ${path} failed: ${err2.message}`;
5021
5124
  }
5022
5125
  }
5023
- function readInstalledPlugins(homedirOverride) {
5024
- const home = homedir();
5025
- const path = join(home, ".claude", "plugins", "installed_plugins.json");
5126
+ async function loadRolePrompts(workflowsDir) {
5127
+ const path = join(workflowsDir, "role-prompts.yaml");
5026
5128
  let raw;
5027
5129
  try {
5028
- raw = readFileSync(path, "utf8");
5029
- } catch {
5030
- return /* @__PURE__ */ new Set();
5031
- }
5032
- let parsed;
5033
- try {
5034
- parsed = JSON.parse(raw);
5130
+ raw = await readFile(path, "utf8");
5035
5131
  } catch {
5036
- return /* @__PURE__ */ new Set();
5037
- }
5038
- if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ new Set();
5039
- const plugins = parsed.plugins;
5040
- if (!plugins || typeof plugins !== "object") return /* @__PURE__ */ new Set();
5041
- const out = /* @__PURE__ */ new Set();
5042
- for (const key of Object.keys(plugins)) {
5043
- const at = key.indexOf("@");
5044
- if (at <= 0) continue;
5045
- out.add(key.slice(0, at));
5046
- }
5047
- return out;
5048
- }
5049
- function resolveCapabilityCmd(capability, installedPlugins) {
5050
- const { cmd, plugin_namespace } = capability;
5051
- if (!plugin_namespace) return { renderedCmd: cmd };
5052
- if (cmd.includes(":")) return { renderedCmd: cmd };
5053
- if (!cmd.startsWith("/")) return { renderedCmd: cmd };
5054
- if (!installedPlugins.has(plugin_namespace)) {
5055
- return {
5056
- renderedCmd: cmd,
5057
- warning: `plugin '${plugin_namespace}' not installed \u2014 '${cmd}' will not resolve via Claude Code. install: 'claude plugin install ${plugin_namespace}' or see plugin docs.`
5058
- };
5132
+ return {};
5059
5133
  }
5060
- const bare = cmd.slice(1);
5061
- return { renderedCmd: `/${plugin_namespace}:${bare}` };
5134
+ const doc = parse(raw);
5135
+ return doc?.prompts ?? {};
5136
+ }
5137
+ function generateCommandFile(name, prompt, capabilities, installedPlugins, installedUserSkills) {
5138
+ const isMaster = prompt.is_master === true;
5139
+ const primaryCmdLine = prompt.primary_cap && capabilities[prompt.primary_cap] ? `{{ capabilities.${prompt.primary_cap}.cmd }}` : "";
5140
+ const checklistBlock = prompt.checklist.length ? prompt.checklist.map((item, i) => `> ${i + 1}. ${item}`).join("\n>\n") : "> (Master orchestrator \u2014 dispatches to per-sub-workflow slash commands listed below.)";
5141
+ const fallbackPath = isMaster ? `**Fallback path** (when no slash command from the sub-list resolves): run each missing sub-workflow inline using its own role prompt (see \`~/.claude/skills/<sub-name>/SKILL.md\` for the per-sub fallback prompt). Do NOT skip stages silently \u2014 each sub either runs or is logged as "skipped: <reason>".` : [
5142
+ `**Fallback path** (when the upstream isn't installed or returns no result): use the Task tool to spawn a general-purpose subagent with this prompt:`,
5143
+ ``,
5144
+ `> You are a **${prompt.specialist}**.`,
5145
+ `>`,
5146
+ `> **Mission**: ${prompt.responsibility.trim().replace(/\n/g, " ")}`,
5147
+ `>`,
5148
+ `> **Default-suspect mode**: assume the change is broken / risky / incomplete until proven otherwise. Cite \`file:line\` for every finding; do not generalize.`,
5149
+ `>`,
5150
+ `> **Review checklist**:`,
5151
+ checklistBlock,
5152
+ `>`,
5153
+ `> **Output format**: structured report with severity-classified findings (${prompt.severity}). One finding per line: \`[severity] file:line \u2014 problem (one sentence); fix: suggested change\`. If no findings, say so explicitly. No preamble, no end-of-report summary.`,
5154
+ ``,
5155
+ `(The role prompt is self-contained \u2014 works even when the upstream \`${prompt.primary_cap || "specialist"}\` user-skill / plugin isn't installed.)`
5156
+ ].join("\n");
5157
+ const preferredPath = primaryCmdLine ? `**Preferred path** (when the upstream specialist is installed): use the SlashCommand tool to run \`${primaryCmdLine}\` \u2014 the upstream specialist takes over.` : `**Preferred path** (master orchestrator): dispatch to the per-sub-workflow slash commands in the order this stage prescribes. Each sub command is its own \`~/.claude/commands/<sub-name>.md\` and has its own dual-path fallback.`;
5158
+ const rawBody = [
5159
+ `# /${name}`,
5160
+ ``,
5161
+ prompt.description,
5162
+ ``,
5163
+ `## How to invoke`,
5164
+ ``,
5165
+ preferredPath,
5166
+ ``,
5167
+ fallbackPath,
5168
+ ``,
5169
+ `## Notes`,
5170
+ ``,
5171
+ `- This file (\`~/.claude/commands/${name}.md\`) is generated by \`harnessed setup\` from \`workflows/role-prompts.yaml\` + \`workflows/<stage>/<sub>/SKILL.md\`. To regenerate after a harnessed upgrade, re-run \`harnessed setup\`.`,
5172
+ `- The companion \`~/.claude/skills/${name}/SKILL.md\` is the Skill-tool entry point (Claude loads it when triggers match \`trigger_phrases:\`). Both files carry the same dual-path instruction.`,
5173
+ `- If your shell shows a \`\u26A0\uFE0F ... not installed\` warning from \`harnessed setup\` for this command, the upstream is missing on disk \u2014 install per the warning, OR rely on the fallback Task-spawn role prompt above (it does not require the upstream).`,
5174
+ ``
5175
+ ].join("\n");
5176
+ const { body, warnings } = renderSkillBody(
5177
+ rawBody,
5178
+ capabilities,
5179
+ installedPlugins,
5180
+ installedUserSkills
5181
+ );
5182
+ const frontmatter = ["---", `description: ${JSON.stringify(prompt.description)}`, "---", ""].join(
5183
+ "\n"
5184
+ );
5185
+ return { content: frontmatter + body, warnings };
5062
5186
  }
5063
- var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
5064
- function renderSkillBody(body, capabilities, installedPlugins) {
5065
- const warningsSet = /* @__PURE__ */ new Set();
5066
- const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
5067
- const cap = capabilities[name];
5068
- if (!cap) {
5069
- warningsSet.add(
5070
- `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
5071
- );
5072
- return match2;
5187
+ async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists = existsSync) {
5188
+ const results = [];
5189
+ const aggregatedWarnings = /* @__PURE__ */ new Set();
5190
+ for (const name of slashNames) {
5191
+ const path = join(commandsDir, `${name}.md`);
5192
+ const prompt = rolePrompts[name];
5193
+ if (!prompt) {
5194
+ results.push({
5195
+ name,
5196
+ path,
5197
+ written: false,
5198
+ warning: `no role-prompts.yaml entry for '${name}' \u2014 skipping commands/${name}.md generation`
5199
+ });
5200
+ aggregatedWarnings.add(`role-prompts.yaml missing entry for '${name}'`);
5201
+ continue;
5073
5202
  }
5074
- const { renderedCmd, warning } = resolveCapabilityCmd(cap, installedPlugins);
5075
- if (warning) warningsSet.add(warning);
5076
- return renderedCmd;
5077
- });
5078
- return { body: out, warnings: [...warningsSet] };
5203
+ if (fileExists(path)) {
5204
+ results.push({
5205
+ name,
5206
+ path,
5207
+ written: false,
5208
+ warning: `commands/${name}.md already exists \u2014 leaving user file unchanged`
5209
+ });
5210
+ continue;
5211
+ }
5212
+ const { content, warnings } = generateCommandFile(
5213
+ name,
5214
+ prompt,
5215
+ capabilities,
5216
+ installedPlugins,
5217
+ installedUserSkills
5218
+ );
5219
+ try {
5220
+ await writer(path, content);
5221
+ results.push({ name, path, written: true });
5222
+ } catch (e) {
5223
+ results.push({
5224
+ name,
5225
+ path,
5226
+ written: false,
5227
+ warning: `write failed for commands/${name}.md: ${e.message}`
5228
+ });
5229
+ }
5230
+ for (const w of warnings) aggregatedWarnings.add(w);
5231
+ }
5232
+ return { results, warnings: [...aggregatedWarnings] };
5079
5233
  }
5080
-
5081
- // src/cli/lib/renderSkillTemplates.ts
5082
5234
  async function loadCapabilities(workflowsDir) {
5083
5235
  const path = join(workflowsDir, "capabilities.yaml");
5084
5236
  const raw = await readFile(path, "utf8");
5085
5237
  const doc = parse(raw);
5086
5238
  return doc?.capabilities ?? {};
5087
5239
  }
5088
- async function renderSkillFile(skillName, skillsBase, capabilities, installedPlugins) {
5240
+ async function renderSkillFile(skillName, skillsBase, capabilities, installedPlugins, installedUserSkills) {
5089
5241
  const skillPath = join(skillsBase, skillName, "SKILL.md");
5090
5242
  const result = {
5091
5243
  name: skillName,
@@ -5100,7 +5252,7 @@ async function renderSkillFile(skillName, skillsBase, capabilities, installedPlu
5100
5252
  result.error = `read failed: ${e.message}`;
5101
5253
  return result;
5102
5254
  }
5103
- const rendered = renderSkillBody(body, capabilities, installedPlugins);
5255
+ const rendered = renderSkillBody(body, capabilities, installedPlugins, installedUserSkills);
5104
5256
  if (rendered.body === body) {
5105
5257
  result.warnings = rendered.warnings;
5106
5258
  return result;
@@ -5133,10 +5285,17 @@ async function renderAllSkills(skillNames, skillsBase, workflowsDir, homedirOver
5133
5285
  };
5134
5286
  }
5135
5287
  const installedPlugins = readInstalledPlugins();
5288
+ const installedUserSkills = readInstalledUserSkills();
5136
5289
  const results = [];
5137
5290
  const warningSet = /* @__PURE__ */ new Set();
5138
5291
  for (const name of skillNames) {
5139
- const r = await renderSkillFile(name, skillsBase, capabilities, installedPlugins);
5292
+ const r = await renderSkillFile(
5293
+ name,
5294
+ skillsBase,
5295
+ capabilities,
5296
+ installedPlugins,
5297
+ installedUserSkills
5298
+ );
5140
5299
  results.push(r);
5141
5300
  for (const w of r.warnings) warningSet.add(w);
5142
5301
  if (r.error) warningSet.add(`${name}: ${r.error}`);
@@ -5378,6 +5537,46 @@ function registerSetup(program2) {
5378
5537
  console.warn(` - ${w}`);
5379
5538
  }
5380
5539
  }
5540
+ const commandsBase = resolve(homedir(), ".claude", "commands");
5541
+ try {
5542
+ await mkdir(commandsBase, { recursive: true });
5543
+ } catch (e) {
5544
+ console.warn(
5545
+ ` [A.6] could not create ${commandsBase} \u2014 skipping commands/ generation (${e.message})`
5546
+ );
5547
+ }
5548
+ let capabilitiesMap = {};
5549
+ try {
5550
+ capabilitiesMap = await loadCapabilities(workflowsDir);
5551
+ } catch (e) {
5552
+ console.warn(
5553
+ ` [A.6] capabilities.yaml unreadable \u2014 skipping commands/ generation (${e.message})`
5554
+ );
5555
+ }
5556
+ const rolePrompts = await loadRolePrompts(workflowsDir);
5557
+ const installedPlugins = readInstalledPlugins();
5558
+ const installedUserSkills = readInstalledUserSkills();
5559
+ const cmdResult = await writeAllCommands(
5560
+ skillNames,
5561
+ commandsBase,
5562
+ rolePrompts,
5563
+ capabilitiesMap,
5564
+ installedPlugins,
5565
+ installedUserSkills,
5566
+ async (p4, c) => writeFile(p4, c, "utf8")
5567
+ );
5568
+ const writtenCount = cmdResult.results.filter((r) => r.written).length;
5569
+ const skippedCount = cmdResult.results.filter((r) => !r.written && r.warning).length;
5570
+ console.log(
5571
+ ` [A.6] generated ${writtenCount} commands/<x>.md file(s) (${skippedCount} skipped \u2014 existing user file or schema warn)`
5572
+ );
5573
+ for (const r of cmdResult.results) {
5574
+ if (r.written) {
5575
+ console.log(` [A.6] wrote /${r.name} \u2192 ${r.path}`);
5576
+ } else if (r.warning) {
5577
+ console.warn(` [A.6] skipped /${r.name}: ${r.warning}`);
5578
+ }
5579
+ }
5381
5580
  const cResult = await enableAgentTeamsInSettings();
5382
5581
  if (cResult.status === "created") {
5383
5582
  console.log(t("setup.step_c.created", { path: cResult.path }));