harnessed 3.8.0 → 3.9.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/dist/cli.mjs CHANGED
@@ -3,11 +3,12 @@ import { execSync, spawnSync, spawn } from 'child_process';
3
3
  import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync } from 'fs';
4
4
  import { join, dirname, resolve, relative } from 'path';
5
5
  import { homedir } from 'os';
6
+ import { readFile, readdir, unlink, writeFile, stat, rm, cp, mkdir, access, rename } from 'fs/promises';
6
7
  import { Type } from '@sinclair/typebox';
7
8
  import { Value } from '@sinclair/typebox/value';
8
9
  import { LineCounter, parseDocument, parse, isSeq, isScalar } from 'yaml';
9
- import { readFile, readdir, unlink, writeFile, stat, rm, cp, mkdir, access, rename } from 'fs/promises';
10
10
  import lockfile from 'proper-lockfile';
11
+ import * as p from '@clack/prompts';
11
12
  import { Command } from 'commander';
12
13
  import { Ajv } from 'ajv';
13
14
  import * as ajvFormatsNs from 'ajv-formats';
@@ -15,7 +16,6 @@ import { fileURLToPath } from 'url';
15
16
  import { createHash } from 'crypto';
16
17
  import { Parser } from 'expr-eval';
17
18
  import { query } from '@anthropic-ai/claude-agent-sdk';
18
- import * as p from '@clack/prompts';
19
19
  import { createPatch } from 'diff';
20
20
  import pc from 'picocolors';
21
21
  import { stdout, stdin } from 'process';
@@ -119,6 +119,89 @@ var init_harnessedRoot = __esm({
119
119
  "src/installers/lib/harnessedRoot.ts"() {
120
120
  }
121
121
  });
122
+ function checkNodeVersion() {
123
+ const v = process.versions.node;
124
+ const major = Number.parseInt(v.split(".")[0] ?? "0", 10);
125
+ return major >= 22 ? { name: "node \u2265 22", status: "pass", message: `node ${v}` } : {
126
+ name: "node \u2265 22",
127
+ status: "fail",
128
+ message: `node ${v} (need \u2265 22)`,
129
+ fix: "nvm install 22 && nvm use 22"
130
+ };
131
+ }
132
+ async function checkMcpScope() {
133
+ const projectMcp = join(process.cwd(), ".mcp.json");
134
+ const userClaude = join(homedir(), ".claude.json");
135
+ let projectExists = false;
136
+ try {
137
+ await readFile(projectMcp, "utf8");
138
+ projectExists = true;
139
+ } catch {
140
+ }
141
+ let userHasMcp = false;
142
+ try {
143
+ const raw = await readFile(userClaude, "utf8");
144
+ const parsed = JSON.parse(raw);
145
+ userHasMcp = !!parsed.mcpServers && Object.keys(parsed.mcpServers).length > 0;
146
+ } catch {
147
+ }
148
+ if (userHasMcp) {
149
+ return {
150
+ name: "mcp scope = project",
151
+ status: "fail",
152
+ message: `~/.claude.json has user-scope mcpServers (CC #54803 risk)`,
153
+ fix: "remove user-scope entries; re-add via `claude mcp add --scope project ...`"
154
+ };
155
+ }
156
+ return {
157
+ name: "mcp scope = project",
158
+ status: "pass",
159
+ message: projectExists ? "project .mcp.json present" : "no MCP servers installed"
160
+ };
161
+ }
162
+ function checkJq() {
163
+ const finder = process.platform === "win32" ? "where" : "which";
164
+ const r = spawnSync(finder, ["jq"], { encoding: "utf8" });
165
+ if (r.status === 0 && r.stdout.trim().length > 0) {
166
+ return {
167
+ name: "jq present",
168
+ status: "pass",
169
+ message: r.stdout.split(/\r?\n/)[0]?.trim() ?? "jq found"
170
+ };
171
+ }
172
+ const fix = process.platform === "win32" ? "winget install jqlang.jq (or: scoop install jq)" : process.platform === "darwin" ? "brew install jq" : "apt-get install jq (or: dnf install jq)";
173
+ return { name: "jq present", status: "fail", message: "jq not found in PATH", fix };
174
+ }
175
+ function checkWinBash() {
176
+ if (process.platform !== "win32") {
177
+ return { name: "bash flavor (win)", status: "pass", message: "skipped (non-Windows)" };
178
+ }
179
+ const where = spawnSync("where", ["bash"], { encoding: "utf8" });
180
+ const firstBash = (where.stdout ?? "").split(/\r?\n/)[0]?.trim() ?? "(not found)";
181
+ if (where.status !== 0 || !firstBash || firstBash === "(not found)") {
182
+ return {
183
+ name: "bash flavor (win)",
184
+ status: "fail",
185
+ message: "no bash on PATH",
186
+ fix: "install Git for Windows (Git Bash) and ensure it is on PATH"
187
+ };
188
+ }
189
+ const probe = spawnSync("bash", ["-c", "echo $WSL_DISTRO_NAME"], { encoding: "utf8" });
190
+ const distro = (probe.stdout ?? "").trim();
191
+ if (distro.length > 0) {
192
+ return {
193
+ name: "bash flavor (win)",
194
+ status: "fail",
195
+ message: `WSL bash (${distro}) \u2014 ralph-loop subagent fork breaks under WSL`,
196
+ fix: "reorder PATH so Git Bash precedes WSL bash.exe (Settings \u2192 System \u2192 Environment Variables)"
197
+ };
198
+ }
199
+ return { name: "bash flavor (win)", status: "pass", message: `${firstBash} (Git Bash / native)` };
200
+ }
201
+ var init_check_builtin = __esm({
202
+ "src/cli/lib/check-builtin.ts"() {
203
+ }
204
+ });
122
205
 
123
206
  // src/cli/lib/probe-gstack.ts
124
207
  var probe_gstack_exports = {};
@@ -720,6 +803,36 @@ var init_check_mcp_availability = __esm({
720
803
  TARGET_SERVERS = ["tavily-mcp", "exa-mcp", "chrome-devtools"];
721
804
  }
722
805
  });
806
+
807
+ // src/cli/lib/doctor-registry.ts
808
+ var CHECKS;
809
+ var init_doctor_registry = __esm({
810
+ "src/cli/lib/doctor-registry.ts"() {
811
+ init_check_builtin();
812
+ CHECKS = [
813
+ async () => checkNodeVersion(),
814
+ checkMcpScope,
815
+ async () => checkJq(),
816
+ async () => checkWinBash(),
817
+ async () => {
818
+ const { checkOrigin: checkOrigin2 } = await Promise.resolve().then(() => (init_origin_check(), origin_check_exports));
819
+ const r = checkOrigin2(process.cwd(), { allowFork: true });
820
+ return { name: "origin URL", status: r.status, message: r.detail, fix: r.fix };
821
+ },
822
+ async () => {
823
+ const { probeGstackPrefix: probeGstackPrefix2 } = await Promise.resolve().then(() => (init_probe_gstack(), probe_gstack_exports));
824
+ const r = probeGstackPrefix2();
825
+ return { name: "gstack prefix", status: r.status, message: r.detail, fix: r.fix };
826
+ },
827
+ async () => (await Promise.resolve().then(() => (init_check_deprecations(), check_deprecations_exports))).checkDeprecations(),
828
+ async () => (await Promise.resolve().then(() => (init_check_token_budget(), check_token_budget_exports))).checkTokenBudget(),
829
+ async () => (await Promise.resolve().then(() => (init_check_agent_teams_doctor(), check_agent_teams_doctor_exports))).checkAgentTeamsDoctor(),
830
+ async () => (await Promise.resolve().then(() => (init_check_planning_with_files(), check_planning_with_files_exports))).checkPlanningWithFiles(),
831
+ async () => (await Promise.resolve().then(() => (init_check_mattpocock_skills(), check_mattpocock_skills_exports))).checkMattpocockSkills(),
832
+ async () => (await Promise.resolve().then(() => (init_check_mcp_availability(), check_mcp_availability_exports))).checkMcpAvailability()
833
+ ];
834
+ }
835
+ });
723
836
  function statePath() {
724
837
  return harnessedFile("current-workflow.json");
725
838
  }
@@ -950,9 +1063,71 @@ var init_resume = __esm({
950
1063
  }
951
1064
  });
952
1065
 
1066
+ // src/cli/lib/auto-install.ts
1067
+ var auto_install_exports = {};
1068
+ __export(auto_install_exports, {
1069
+ extractPluginName: () => extractPluginName,
1070
+ runAutoInstall: () => runAutoInstall
1071
+ });
1072
+ function extractPluginName(fix) {
1073
+ const m = fix.match(/claude\s+plugin\s+install\s+([\w@\-/.]+)/);
1074
+ return m?.[1] ?? null;
1075
+ }
1076
+ async function runAutoInstall(opts) {
1077
+ const out = { installed: [], skipped: [], failed: [] };
1078
+ if (!opts.autoInstall) {
1079
+ return out;
1080
+ }
1081
+ const results = await Promise.all(CHECKS.map((c) => c()));
1082
+ const installables = results.filter((r) => r.status === "warn" && typeof r.fix === "string").map((r) => ({ check: r, plugin: extractPluginName(r.fix ?? "") })).filter((entry) => entry.plugin !== null);
1083
+ if (installables.length === 0) {
1084
+ return out;
1085
+ }
1086
+ console.log(
1087
+ `
1088
+ \u{1F4A1} ${installables.length} optional plugin(s) missing \u2014 harnessed can install them now:`
1089
+ );
1090
+ for (const { check, plugin } of installables) {
1091
+ if (opts.nonInteractive) {
1092
+ out.skipped.push(plugin);
1093
+ continue;
1094
+ }
1095
+ const ans = await p.confirm({
1096
+ message: `Install "${plugin}" via \`claude plugin install\`? (${check.name})`,
1097
+ initialValue: true
1098
+ });
1099
+ if (p.isCancel(ans) || ans !== true) {
1100
+ out.skipped.push(plugin);
1101
+ continue;
1102
+ }
1103
+ const r = spawnSync("claude", ["plugin", "install", plugin], {
1104
+ encoding: "utf8",
1105
+ stdio: "inherit"
1106
+ });
1107
+ if (r.status === 0) {
1108
+ out.installed.push(plugin);
1109
+ console.log(` \u2713 installed ${plugin}`);
1110
+ } else {
1111
+ const reason = r.error !== void 0 ? `spawn error: ${r.error.message}` : `exit code ${r.status ?? "<unknown>"}`;
1112
+ out.failed.push({ name: plugin, reason });
1113
+ console.error(` \u2717 failed ${plugin} \u2014 ${reason}`);
1114
+ }
1115
+ }
1116
+ console.log(
1117
+ `
1118
+ Auto-install summary: ${out.installed.length} installed / ${out.skipped.length} skipped / ${out.failed.length} failed`
1119
+ );
1120
+ return out;
1121
+ }
1122
+ var init_auto_install = __esm({
1123
+ "src/cli/lib/auto-install.ts"() {
1124
+ init_doctor_registry();
1125
+ }
1126
+ });
1127
+
953
1128
  // package.json
954
1129
  var package_default = {
955
- version: "3.8.0"};
1130
+ version: "3.9.0"};
956
1131
 
957
1132
  // src/manifest/errors.ts
958
1133
  function instancePathToKeyPath(instancePath) {
@@ -1442,8 +1617,8 @@ function checkSecurityViolations(doc, filename, lineCounter) {
1442
1617
  ["spec", "verify", "cmd"],
1443
1618
  ["spec", "uninstall", "cmd"]
1444
1619
  ];
1445
- for (const p4 of cmdPaths) {
1446
- const err2 = checkScalarCmd(doc, lineCounter, p4, filename);
1620
+ for (const p5 of cmdPaths) {
1621
+ const err2 = checkScalarCmd(doc, lineCounter, p5, filename);
1447
1622
  if (err2) errors.push(err2);
1448
1623
  }
1449
1624
  const cleanupNode = doc.getIn(["spec", "uninstall", "cleanup_paths"], true);
@@ -2024,111 +2199,9 @@ function registerBackupList(program2) {
2024
2199
  console.log(t("backup.total_snapshots", { count: dirs.length }));
2025
2200
  });
2026
2201
  }
2027
- function checkNodeVersion() {
2028
- const v = process.versions.node;
2029
- const major = Number.parseInt(v.split(".")[0] ?? "0", 10);
2030
- return major >= 22 ? { name: "node \u2265 22", status: "pass", message: `node ${v}` } : {
2031
- name: "node \u2265 22",
2032
- status: "fail",
2033
- message: `node ${v} (need \u2265 22)`,
2034
- fix: "nvm install 22 && nvm use 22"
2035
- };
2036
- }
2037
- async function checkMcpScope() {
2038
- const projectMcp = join(process.cwd(), ".mcp.json");
2039
- const userClaude = join(homedir(), ".claude.json");
2040
- let projectExists = false;
2041
- try {
2042
- await readFile(projectMcp, "utf8");
2043
- projectExists = true;
2044
- } catch {
2045
- }
2046
- let userHasMcp = false;
2047
- try {
2048
- const raw = await readFile(userClaude, "utf8");
2049
- const parsed = JSON.parse(raw);
2050
- userHasMcp = !!parsed.mcpServers && Object.keys(parsed.mcpServers).length > 0;
2051
- } catch {
2052
- }
2053
- if (userHasMcp) {
2054
- return {
2055
- name: "mcp scope = project",
2056
- status: "fail",
2057
- message: `~/.claude.json has user-scope mcpServers (CC #54803 risk)`,
2058
- fix: "remove user-scope entries; re-add via `claude mcp add --scope project ...`"
2059
- };
2060
- }
2061
- return {
2062
- name: "mcp scope = project",
2063
- status: "pass",
2064
- message: projectExists ? "project .mcp.json present" : "no MCP servers installed"
2065
- };
2066
- }
2067
- function checkJq() {
2068
- const finder = process.platform === "win32" ? "where" : "which";
2069
- const r = spawnSync(finder, ["jq"], { encoding: "utf8" });
2070
- if (r.status === 0 && r.stdout.trim().length > 0) {
2071
- return {
2072
- name: "jq present",
2073
- status: "pass",
2074
- message: r.stdout.split(/\r?\n/)[0]?.trim() ?? "jq found"
2075
- };
2076
- }
2077
- const fix = process.platform === "win32" ? "winget install jqlang.jq (or: scoop install jq)" : process.platform === "darwin" ? "brew install jq" : "apt-get install jq (or: dnf install jq)";
2078
- return { name: "jq present", status: "fail", message: "jq not found in PATH", fix };
2079
- }
2080
- function checkWinBash() {
2081
- if (process.platform !== "win32") {
2082
- return { name: "bash flavor (win)", status: "pass", message: "skipped (non-Windows)" };
2083
- }
2084
- const where = spawnSync("where", ["bash"], { encoding: "utf8" });
2085
- const firstBash = (where.stdout ?? "").split(/\r?\n/)[0]?.trim() ?? "(not found)";
2086
- if (where.status !== 0 || !firstBash || firstBash === "(not found)") {
2087
- return {
2088
- name: "bash flavor (win)",
2089
- status: "fail",
2090
- message: "no bash on PATH",
2091
- fix: "install Git for Windows (Git Bash) and ensure it is on PATH"
2092
- };
2093
- }
2094
- const probe = spawnSync("bash", ["-c", "echo $WSL_DISTRO_NAME"], { encoding: "utf8" });
2095
- const distro = (probe.stdout ?? "").trim();
2096
- if (distro.length > 0) {
2097
- return {
2098
- name: "bash flavor (win)",
2099
- status: "fail",
2100
- message: `WSL bash (${distro}) \u2014 ralph-loop subagent fork breaks under WSL`,
2101
- fix: "reorder PATH so Git Bash precedes WSL bash.exe (Settings \u2192 System \u2192 Environment Variables)"
2102
- };
2103
- }
2104
- return { name: "bash flavor (win)", status: "pass", message: `${firstBash} (Git Bash / native)` };
2105
- }
2106
-
2107
- // src/cli/lib/doctor-registry.ts
2108
- var CHECKS = [
2109
- async () => checkNodeVersion(),
2110
- checkMcpScope,
2111
- async () => checkJq(),
2112
- async () => checkWinBash(),
2113
- async () => {
2114
- const { checkOrigin: checkOrigin2 } = await Promise.resolve().then(() => (init_origin_check(), origin_check_exports));
2115
- const r = checkOrigin2(process.cwd(), { allowFork: true });
2116
- return { name: "origin URL", status: r.status, message: r.detail, fix: r.fix };
2117
- },
2118
- async () => {
2119
- const { probeGstackPrefix: probeGstackPrefix2 } = await Promise.resolve().then(() => (init_probe_gstack(), probe_gstack_exports));
2120
- const r = probeGstackPrefix2();
2121
- return { name: "gstack prefix", status: r.status, message: r.detail, fix: r.fix };
2122
- },
2123
- async () => (await Promise.resolve().then(() => (init_check_deprecations(), check_deprecations_exports))).checkDeprecations(),
2124
- async () => (await Promise.resolve().then(() => (init_check_token_budget(), check_token_budget_exports))).checkTokenBudget(),
2125
- async () => (await Promise.resolve().then(() => (init_check_agent_teams_doctor(), check_agent_teams_doctor_exports))).checkAgentTeamsDoctor(),
2126
- async () => (await Promise.resolve().then(() => (init_check_planning_with_files(), check_planning_with_files_exports))).checkPlanningWithFiles(),
2127
- async () => (await Promise.resolve().then(() => (init_check_mattpocock_skills(), check_mattpocock_skills_exports))).checkMattpocockSkills(),
2128
- async () => (await Promise.resolve().then(() => (init_check_mcp_availability(), check_mcp_availability_exports))).checkMcpAvailability()
2129
- ];
2130
2202
 
2131
2203
  // src/cli/doctor.ts
2204
+ init_doctor_registry();
2132
2205
  function registerDoctor(program2) {
2133
2206
  program2.command("doctor").description(
2134
2207
  "Preflight checks (Node / MCP scope / jq / Win bash / origin URL / gstack prefix / deprecations / token budget / Agent Teams / planning-with-files / mattpocock-skills / MCP availability)"
@@ -2378,7 +2451,7 @@ var V3_4_3_SIGNATURE_MASTER_RX = /\*\*Preferred path\*\*[\s\S]*dispatch to the p
2378
2451
  function shouldOverwriteFile(content) {
2379
2452
  return HARNESSED_MARKER_RX.test(content) || V3_4_3_SIGNATURE_SUB_RX.test(content) || V3_4_3_SIGNATURE_MASTER_RX.test(content);
2380
2453
  }
2381
- async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists2 = existsSync, readFileSync9 = (p4) => readFileSync(p4, "utf8")) {
2454
+ async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists2 = existsSync, readFileSync9 = (p5) => readFileSync(p5, "utf8")) {
2382
2455
  const results = [];
2383
2456
  const aggregatedWarnings = /* @__PURE__ */ new Set();
2384
2457
  for (const name of slashNames) {
@@ -3362,10 +3435,10 @@ ${rp.checklist.map((c, i) => ` ${i + 1}. ${c}`).join("\n")}` : "";
3362
3435
  }
3363
3436
  function isRalphLoopOptIn(phase) {
3364
3437
  if (!phase || typeof phase !== "object") return false;
3365
- const p4 = phase;
3366
- if (p4.max_iterations !== void 0 && p4.max_iterations !== null) return true;
3367
- if (p4.upstream === "ralph-loop") return true;
3368
- const fb = p4.fallback;
3438
+ const p5 = phase;
3439
+ if (p5.max_iterations !== void 0 && p5.max_iterations !== null) return true;
3440
+ if (p5.upstream === "ralph-loop") return true;
3441
+ const fb = p5.fallback;
3369
3442
  if (fb?.max_iterations_exceeded !== void 0) return true;
3370
3443
  return false;
3371
3444
  }
@@ -3713,19 +3786,19 @@ async function listWorkflowNames(workflowsDir) {
3713
3786
  const names = [];
3714
3787
  const entries = await readdir(workflowsDir);
3715
3788
  for (const e of entries.sort()) {
3716
- const p4 = join(workflowsDir, e);
3717
- const s = await stat(p4).catch(() => null);
3789
+ const p5 = join(workflowsDir, e);
3790
+ const s = await stat(p5).catch(() => null);
3718
3791
  if (!s?.isDirectory()) continue;
3719
- if (await fileExists(join(p4, "workflow.yaml"))) {
3792
+ if (await fileExists(join(p5, "workflow.yaml"))) {
3720
3793
  names.push(e);
3721
3794
  continue;
3722
3795
  }
3723
- if (await fileExists(join(p4, "auto", "workflow.yaml"))) {
3796
+ if (await fileExists(join(p5, "auto", "workflow.yaml"))) {
3724
3797
  names.push(e);
3725
- const subs = await readdir(p4).catch(() => []);
3798
+ const subs = await readdir(p5).catch(() => []);
3726
3799
  for (const sub of subs.sort()) {
3727
3800
  if (sub === "auto") continue;
3728
- if (await fileExists(join(p4, sub, "workflow.yaml"))) {
3801
+ if (await fileExists(join(p5, sub, "workflow.yaml"))) {
3729
3802
  names.push(`${e}-${sub}`);
3730
3803
  }
3731
3804
  }
@@ -3859,10 +3932,10 @@ async function dirSizeKb(dir) {
3859
3932
  if (!cur) break;
3860
3933
  const entries = await readdir(cur, { withFileTypes: true });
3861
3934
  for (const e of entries) {
3862
- const p4 = join(cur, e.name);
3863
- if (e.isDirectory()) stack.push(p4);
3935
+ const p5 = join(cur, e.name);
3936
+ if (e.isDirectory()) stack.push(p5);
3864
3937
  else if (e.isFile()) {
3865
- const st = await stat(p4);
3938
+ const st = await stat(p5);
3866
3939
  total += st.size;
3867
3940
  }
3868
3941
  }
@@ -6029,7 +6102,7 @@ function registerSetup(program2) {
6029
6102
  ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option(
6030
6103
  "--user-lang <code>",
6031
6104
  "override detected OS locale for env.HARNESSED_USER_LANG (en | zh-Hans / zh-CN / zh-TW)"
6032
- ).action(async (raw) => {
6105
+ ).option("--non-interactive", "skip all confirm prompts (CI / scripted setup)").option("--no-auto-install", "do not prompt to auto-install missing plugins (advisory only)").action(async (raw) => {
6033
6106
  const dryRun = raw.dryRun === true;
6034
6107
  const pkgRoot = getPackageRoot();
6035
6108
  const workflowsDir = resolve(pkgRoot, "workflows");
@@ -6112,7 +6185,7 @@ function registerSetup(program2) {
6112
6185
  capabilitiesMap,
6113
6186
  installedPlugins,
6114
6187
  installedUserSkills,
6115
- async (p4, c) => writeFile(p4, c, "utf8")
6188
+ async (p5, c) => writeFile(p5, c, "utf8")
6116
6189
  );
6117
6190
  const writtenCount = cmdResult.results.filter((r) => r.written).length;
6118
6191
  const skippedCount = cmdResult.results.filter((r) => !r.written && r.warning).length;
@@ -6188,6 +6261,15 @@ function registerSetup(program2) {
6188
6261
  console.log(t("setup.bundled_summary"));
6189
6262
  console.log(t("setup.bundled_location"));
6190
6263
  console.log(t("setup.doctor_hint"));
6264
+ if (!dryRun) {
6265
+ const isTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
6266
+ const { runAutoInstall: runAutoInstall2 } = await Promise.resolve().then(() => (init_auto_install(), auto_install_exports));
6267
+ await runAutoInstall2({
6268
+ nonInteractive: raw.nonInteractive === true || !isTty,
6269
+ autoInstall: raw.autoInstall !== false
6270
+ // commander default = true; --no-auto-install flips
6271
+ });
6272
+ }
6191
6273
  process.exit(0);
6192
6274
  });
6193
6275
  }