harnessed 3.4.2 → 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
@@ -6,7 +6,7 @@ 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.2"};
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,111 +5123,114 @@ 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);
5035
- } 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 readInstalledUserSkills(homedirOverride) {
5050
- const home = homedir();
5051
- const skillsRoot = join(home, ".claude", "skills");
5052
- try {
5053
- const entries = readdirSync(skillsRoot, { withFileTypes: true });
5054
- const out = /* @__PURE__ */ new Set();
5055
- for (const e of entries) if (e.isDirectory()) out.add(e.name);
5056
- return out;
5130
+ raw = await readFile(path, "utf8");
5057
5131
  } catch {
5058
- return /* @__PURE__ */ new Set();
5132
+ return {};
5059
5133
  }
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 };
5060
5186
  }
5061
- function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills) {
5062
- const { cmd, install_type, plugin_id, skill_dir } = capability;
5063
- if (!install_type) return { renderedCmd: cmd };
5064
- const types = Array.isArray(install_type) ? install_type : [install_type];
5065
- const uniqueTypes = [...new Set(types)];
5066
- const missingHints = [];
5067
- let anyDetected = false;
5068
- for (const t2 of uniqueTypes) {
5069
- if (t2 === "plugin") {
5070
- if (!plugin_id) {
5071
- missingHints.push(
5072
- `install_type=plugin declared but no plugin_id (capabilities.yaml schema bug)`
5073
- );
5074
- continue;
5075
- }
5076
- if (installedPlugins.has(plugin_id)) {
5077
- anyDetected = true;
5078
- break;
5079
- }
5080
- missingHints.push(`plugin '${plugin_id}' (\`claude plugin install ${plugin_id}\`)`);
5081
- } else {
5082
- if (!skill_dir) {
5083
- missingHints.push(
5084
- `install_type=user-skill declared but no skill_dir (capabilities.yaml schema bug)`
5085
- );
5086
- continue;
5087
- }
5088
- if (installedUserSkills.has(skill_dir)) {
5089
- anyDetected = true;
5090
- break;
5091
- }
5092
- missingHints.push(
5093
- `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\`)`
5094
- );
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;
5095
5202
  }
5096
- }
5097
- if (anyDetected) return { renderedCmd: cmd };
5098
- const prefix = uniqueTypes.length > 1 ? "[multi]" : `[${uniqueTypes[0]}]`;
5099
- const joined = missingHints.join(" OR ");
5100
- return {
5101
- renderedCmd: cmd,
5102
- warning: `${prefix} '${cmd}' backing missing \u2014 install either: ${joined}.`
5103
- };
5104
- }
5105
- var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
5106
- function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
5107
- const warningsSet = /* @__PURE__ */ new Set();
5108
- const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
5109
- const cap = capabilities[name];
5110
- if (!cap) {
5111
- warningsSet.add(
5112
- `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
5113
- );
5114
- return match2;
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;
5115
5211
  }
5116
- const { renderedCmd, warning } = resolveCapabilityCmd(
5117
- cap,
5212
+ const { content, warnings } = generateCommandFile(
5213
+ name,
5214
+ prompt,
5215
+ capabilities,
5118
5216
  installedPlugins,
5119
5217
  installedUserSkills
5120
5218
  );
5121
- if (warning) warningsSet.add(warning);
5122
- return renderedCmd;
5123
- });
5124
- return { body: out, warnings: [...warningsSet] };
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] };
5125
5233
  }
5126
-
5127
- // src/cli/lib/renderSkillTemplates.ts
5128
5234
  async function loadCapabilities(workflowsDir) {
5129
5235
  const path = join(workflowsDir, "capabilities.yaml");
5130
5236
  const raw = await readFile(path, "utf8");
@@ -5431,6 +5537,46 @@ function registerSetup(program2) {
5431
5537
  console.warn(` - ${w}`);
5432
5538
  }
5433
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
+ }
5434
5580
  const cResult = await enableAgentTeamsInSettings();
5435
5581
  if (cResult.status === "created") {
5436
5582
  console.log(t("setup.step_c.created", { path: cResult.path }));