harnessed 3.0.2 → 3.0.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,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync, spawn } from 'child_process';
3
- import { writeFileSync, existsSync, readFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs';
4
- import { resolve, join, dirname, relative } from 'path';
3
+ import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from 'fs';
4
+ import { join, resolve, dirname, relative } from 'path';
5
+ import { homedir } from 'os';
5
6
  import { Type } from '@sinclair/typebox';
6
7
  import { Value } from '@sinclair/typebox/value';
7
8
  import { LineCounter, parseDocument, parse, isSeq, isScalar } from 'yaml';
8
- import { homedir } from 'os';
9
9
  import { readFile, readdir, unlink, writeFile, stat, rm, cp, access, mkdir, rename } from 'fs/promises';
10
10
  import lockfile from 'proper-lockfile';
11
11
  import { Command } from 'commander';
@@ -78,6 +78,46 @@ var init_origin_check = __esm({
78
78
  "src/cli/lib/origin-check.ts"() {
79
79
  }
80
80
  });
81
+ function getHarnessedRoot() {
82
+ const override = process.env.HARNESSED_ROOT_OVERRIDE;
83
+ if (override !== void 0 && override !== "") return override;
84
+ return join(homedir(), ".claude", "harnessed");
85
+ }
86
+ function harnessedSubdir(name) {
87
+ return join(getHarnessedRoot(), name);
88
+ }
89
+ function harnessedFile(name) {
90
+ return join(getHarnessedRoot(), name);
91
+ }
92
+ function migrateLegacyHarnessedRoot() {
93
+ const legacyRoot = join(homedir(), ".harnessed");
94
+ const newRoot = join(homedir(), ".claude", "harnessed");
95
+ const claudeParent = join(homedir(), ".claude");
96
+ if (!existsSync(legacyRoot)) return;
97
+ if (!existsSync(newRoot)) {
98
+ mkdirSync(claudeParent, { recursive: true });
99
+ renameSync(legacyRoot, newRoot);
100
+ console.error(
101
+ `[harnessed] migrated legacy state directory ${legacyRoot} \u2192 ${newRoot} (v3.0.3 path change)`
102
+ );
103
+ return;
104
+ }
105
+ const safetyBak = join(homedir(), ".harnessed.legacy-bak");
106
+ if (existsSync(safetyBak)) {
107
+ console.error(
108
+ `[harnessed] WARN: ${legacyRoot} reappeared after a prior migration (existing bak at ${safetyBak}); leaving in place \u2014 inspect manually if needed`
109
+ );
110
+ return;
111
+ }
112
+ renameSync(legacyRoot, safetyBak);
113
+ console.error(
114
+ `[harnessed] both ${legacyRoot} and ${newRoot} existed \u2014 legacy directory preserved at ${safetyBak} (review manually if you need data from it; v3.0.3 path change)`
115
+ );
116
+ }
117
+ var init_harnessedRoot = __esm({
118
+ "src/installers/lib/harnessedRoot.ts"() {
119
+ }
120
+ });
81
121
 
82
122
  // src/cli/lib/probe-gstack.ts
83
123
  var probe_gstack_exports = {};
@@ -408,7 +448,7 @@ function writeCheckpoint(c, customPath) {
408
448
  throw new CheckpointWriteError(`Schema validation failed: ${errs}`);
409
449
  }
410
450
  const enforced = enforceBudget(c);
411
- const path = `.harnessed/checkpoints/${enforced.phase}.json`;
451
+ const path = join(harnessedSubdir("checkpoints"), `${enforced.phase}.json`);
412
452
  mkdirSync(dirname(path), { recursive: true });
413
453
  writeFileSync(path, JSON.stringify(enforced, null, 2), "utf8");
414
454
  return path;
@@ -416,6 +456,7 @@ function writeCheckpoint(c, customPath) {
416
456
  var BUDGET_TOKEN, CheckpointTooLargeError, CheckpointWriteError;
417
457
  var init_template = __esm({
418
458
  "src/checkpoint/template.ts"() {
459
+ init_harnessedRoot();
419
460
  init_schema();
420
461
  BUDGET_TOKEN = 1e3;
421
462
  CheckpointTooLargeError = class extends Error {
@@ -579,10 +620,25 @@ var init_check_planning_with_files = __esm({
579
620
  REMEDIATION = "install via Claude Code plugin marketplace: `claude plugin install planning-with-files` (requires >=2.2.0 per R20.15 + D-15)";
580
621
  }
581
622
  });
623
+ function statePath() {
624
+ return harnessedFile("current-workflow.json");
625
+ }
626
+ function lockTarget() {
627
+ return getHarnessedRoot();
628
+ }
629
+ function lockOpts() {
630
+ return {
631
+ stale: 1e4,
632
+ retries: { retries: 3, factor: 2, minTimeout: 100 },
633
+ lockfilePath: harnessedFile(".lock")
634
+ };
635
+ }
582
636
  async function withLock(fn) {
637
+ const target = lockTarget();
638
+ await mkdir(target, { recursive: true });
583
639
  let release;
584
640
  try {
585
- release = await lockfile.lock(LOCK_TARGET, LOCK_OPTS);
641
+ release = await lockfile.lock(target, lockOpts());
586
642
  } catch (e) {
587
643
  if (e.code === "ELOCKED") throw new LockHeldError();
588
644
  throw e;
@@ -596,7 +652,7 @@ async function withLock(fn) {
596
652
  async function readCurrentWorkflow() {
597
653
  let raw;
598
654
  try {
599
- raw = await readFile(STATE_PATH, "utf8");
655
+ raw = await readFile(statePath(), "utf8");
600
656
  } catch {
601
657
  return null;
602
658
  }
@@ -617,9 +673,10 @@ async function writeCurrentWorkflow(s) {
617
673
  const errs = [...Value.Errors(CurrentWorkflowV1, s)].map((e) => e.message).join("; ");
618
674
  throw new WorkflowStateError(`current-workflow schema validation failed: ${errs}`);
619
675
  }
620
- await mkdir(dirname(STATE_PATH), { recursive: true });
676
+ const path = statePath();
677
+ await mkdir(dirname(path), { recursive: true });
621
678
  await withLock(async () => {
622
- await writeFile(STATE_PATH, JSON.stringify(s, null, 2), "utf8");
679
+ await writeFile(path, JSON.stringify(s, null, 2), "utf8");
623
680
  });
624
681
  }
625
682
  async function activate(phase, checkpointPath = null) {
@@ -636,18 +693,12 @@ async function complete() {
636
693
  if (!s) return;
637
694
  await writeCurrentWorkflow({ ...s, status: "complete", completed_at: (/* @__PURE__ */ new Date()).toISOString() });
638
695
  }
639
- var STATE_PATH, LOCK_TARGET, LOCK_OPTS, WorkflowStateError, LockHeldError;
696
+ var WorkflowStateError, LockHeldError;
640
697
  var init_state = __esm({
641
698
  "src/checkpoint/state.ts"() {
699
+ init_harnessedRoot();
642
700
  init_schemaVersion();
643
701
  init_schema();
644
- STATE_PATH = ".harnessed/current-workflow.json";
645
- LOCK_TARGET = ".harnessed";
646
- LOCK_OPTS = {
647
- stale: 1e4,
648
- retries: { retries: 3, factor: 2, minTimeout: 100 },
649
- lockfilePath: ".harnessed/.lock"
650
- };
651
702
  WorkflowStateError = class extends Error {
652
703
  constructor(message) {
653
704
  super(message);
@@ -657,7 +708,7 @@ var init_state = __esm({
657
708
  LockHeldError = class _LockHeldError extends Error {
658
709
  constructor() {
659
710
  super(
660
- "another harnessed process holds the lock at .harnessed/.lock \u2014 wait or kill stale process (try: harnessed status)"
711
+ `another harnessed process holds the lock at ${harnessedFile(".lock")} \u2014 wait or kill stale process (try: harnessed status)`
661
712
  );
662
713
  this.name = "LockHeldError";
663
714
  Object.setPrototypeOf(this, _LockHeldError.prototype);
@@ -740,7 +791,10 @@ __export(resume_exports, {
740
791
  async function runResume() {
741
792
  const current = await readCurrentWorkflow();
742
793
  if (!current) {
743
- return { status: "no-paused-phase", error: "no .harnessed/current-workflow.json found" };
794
+ return {
795
+ status: "no-paused-phase",
796
+ error: "no current-workflow.json found under <harnessed-root>"
797
+ };
744
798
  }
745
799
  if (current.status !== "paused") {
746
800
  return {
@@ -793,7 +847,7 @@ var init_resume = __esm({
793
847
 
794
848
  // package.json
795
849
  var package_default = {
796
- version: "3.0.2"};
850
+ version: "3.0.3"};
797
851
 
798
852
  // src/manifest/errors.ts
799
853
  function instancePathToKeyPath(instancePath) {
@@ -1546,7 +1600,12 @@ audited ${yamls.length} manifest${yamls.length === 1 ? "" : "s"} \u2014 ${findin
1546
1600
  process.exit(errorCount > 0 ? 1 : 0);
1547
1601
  });
1548
1602
  }
1549
- var AUDIT_PATH = ".harnessed/audit.log";
1603
+
1604
+ // src/cli/audit-log.ts
1605
+ init_harnessedRoot();
1606
+ function auditPath() {
1607
+ return harnessedFile("audit.log");
1608
+ }
1550
1609
  var REDACT_PATTERNS = [
1551
1610
  [/api[_-]?key\s*[:=]\s*\S+/gi, "api_key=[REDACTED]"],
1552
1611
  [/\btoken\s*[:=]\s*\S+/gi, "token=[REDACTED]"],
@@ -1594,7 +1653,9 @@ function pipeToJq(filterExpr, lines) {
1594
1653
  });
1595
1654
  }
1596
1655
  function registerAuditLog(program2) {
1597
- program2.command("audit-log").description("Query routing audit log (.harnessed/audit.log) with optional jq filter (R10.1)").option("--filter <expr>", `jq filter expression (e.g. '.category=="engineering"')`).option("--tail <n>", "show N most recent records (default 50)", "50").option("--head <n>", "show N oldest records (--head takes priority over --tail)").option("--reverse", "flip output order").option("--json", "output full 12-field JSON instead of human table").action(
1656
+ program2.command("audit-log").description(
1657
+ "Query routing audit log (<harnessed-root>/audit.log) with optional jq filter (R10.1)"
1658
+ ).option("--filter <expr>", `jq filter expression (e.g. '.category=="engineering"')`).option("--tail <n>", "show N most recent records (default 50)", "50").option("--head <n>", "show N oldest records (--head takes priority over --tail)").option("--reverse", "flip output order").option("--json", "output full 12-field JSON instead of human table").action(
1598
1659
  async (opts) => {
1599
1660
  const tailN = opts.tail !== void 0 ? Number(opts.tail) : 50;
1600
1661
  if (Number.isNaN(tailN) || tailN < 1) {
@@ -1606,11 +1667,12 @@ function registerAuditLog(program2) {
1606
1667
  console.error("\u2717 --head must be a positive integer");
1607
1668
  process.exit(2);
1608
1669
  }
1609
- if (!existsSync(AUDIT_PATH)) {
1610
- console.log("no audit records found (.harnessed/audit.log does not exist)");
1670
+ const path = auditPath();
1671
+ if (!existsSync(path)) {
1672
+ console.log(`no audit records found (${path} does not exist)`);
1611
1673
  process.exit(0);
1612
1674
  }
1613
- const raw = readFileSync(AUDIT_PATH, "utf8");
1675
+ const raw = readFileSync(path, "utf8");
1614
1676
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
1615
1677
  if (lines.length === 0) {
1616
1678
  console.log("no audit records found (audit.log is empty)");
@@ -1646,8 +1708,11 @@ function registerAuditLog(program2) {
1646
1708
  }
1647
1709
  );
1648
1710
  }
1711
+
1712
+ // src/installers/lib/backup.ts
1713
+ init_harnessedRoot();
1649
1714
  function getBackupRoot() {
1650
- return join(homedir(), ".harnessed", "backups");
1715
+ return harnessedSubdir("backups");
1651
1716
  }
1652
1717
  var HOME_DIR = process.env.HOME ?? process.env.USERPROFILE ?? "";
1653
1718
  function mirrorPath(target, scope, backupDir) {
@@ -2317,7 +2382,12 @@ function matchesWhen(when, task) {
2317
2382
  }
2318
2383
  return true;
2319
2384
  }
2320
- var AUDIT_PATH2 = ".harnessed/audit.log";
2385
+
2386
+ // src/audit/log.ts
2387
+ init_harnessedRoot();
2388
+ function auditPath2() {
2389
+ return harnessedFile("audit.log");
2390
+ }
2321
2391
  Type.Object(
2322
2392
  {
2323
2393
  ts: Type.String(),
@@ -2352,8 +2422,9 @@ function buildAuditRecord(task, decision, matched, ctx) {
2352
2422
  };
2353
2423
  }
2354
2424
  function emitAuditRecord(record) {
2355
- mkdirSync(dirname(AUDIT_PATH2), { recursive: true });
2356
- appendFileSync(AUDIT_PATH2, `${JSON.stringify(record)}
2425
+ const path = auditPath2();
2426
+ mkdirSync(dirname(path), { recursive: true });
2427
+ appendFileSync(path, `${JSON.stringify(record)}
2357
2428
  `);
2358
2429
  }
2359
2430
 
@@ -2371,18 +2442,19 @@ function emitAudit(task, decision, matched, outcome, sessionId) {
2371
2442
  }
2372
2443
 
2373
2444
  // src/checkpoint/engineHook.ts
2445
+ init_harnessedRoot();
2374
2446
  init_schemaVersion();
2375
2447
  init_state();
2376
2448
  init_template();
2377
2449
  async function activatePhase(phaseId) {
2378
- const checkpointPath = `.harnessed/checkpoints/${phaseId}.json`;
2450
+ const checkpointPath = join(harnessedSubdir("checkpoints"), `${phaseId}.json`);
2379
2451
  await activate(phaseId, checkpointPath);
2380
2452
  return { checkpointPath };
2381
2453
  }
2382
2454
  async function completePhase(ctx) {
2383
2455
  if (ctx.phaseId === "unknown") {
2384
2456
  console.error(
2385
- '[harnessed] WARN engineHook: phaseId="unknown" \u2014 checkpoint paths fall back to .harnessed/checkpoints/unknown.json (Karpathy fail-loud non-blocking; W-04 mitigation)'
2457
+ `[harnessed] WARN engineHook: phaseId="unknown" \u2014 checkpoint paths fall back to ${join(harnessedSubdir("checkpoints"), "unknown.json")} (Karpathy fail-loud non-blocking; W-04 mitigation)`
2386
2458
  );
2387
2459
  }
2388
2460
  writeCheckpoint({
@@ -2395,7 +2467,7 @@ async function completePhase(ctx) {
2395
2467
  ...ctx.sessionId ? { session_id: ctx.sessionId } : {},
2396
2468
  cwd: process.cwd(),
2397
2469
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2398
- archive_path: `.harnessed/archive/phase-${ctx.phaseId}/`
2470
+ archive_path: `${join(harnessedSubdir("archive"), `phase-${ctx.phaseId}`)}/`
2399
2471
  });
2400
2472
  await complete();
2401
2473
  }
@@ -2479,11 +2551,11 @@ var VerbatimCompleteFailError = class extends Error {
2479
2551
  }
2480
2552
  lastMessage;
2481
2553
  };
2482
- async function ralphLoopWrap(spawn9, maxIter) {
2554
+ async function ralphLoopWrap(spawn6, maxIter) {
2483
2555
  let last = "";
2484
2556
  let sessionId;
2485
2557
  for (let i = 0; i < maxIter; i++) {
2486
- last = await spawn9(sessionId, (id) => {
2558
+ last = await spawn6(sessionId, (id) => {
2487
2559
  sessionId = id;
2488
2560
  });
2489
2561
  if (isComplete(last)) return last;
@@ -3239,12 +3311,15 @@ function preflight(ctx) {
3239
3311
  }
3240
3312
  return { ok: errors.length === 0, errors };
3241
3313
  }
3314
+
3315
+ // src/installers/lib/state.ts
3316
+ init_harnessedRoot();
3242
3317
  var DEFAULT_STATE = { version: "1", installed: {} };
3243
- function statePath(cwd) {
3244
- return join(cwd, ".harnessed", "state.json");
3318
+ function statePath2(_cwd) {
3319
+ return harnessedFile("state.json");
3245
3320
  }
3246
3321
  async function readState(cwd) {
3247
- const path = statePath(cwd);
3322
+ const path = statePath2();
3248
3323
  let raw;
3249
3324
  try {
3250
3325
  raw = await readFile(path, "utf8");
@@ -3265,7 +3340,7 @@ async function readState(cwd) {
3265
3340
  }
3266
3341
  }
3267
3342
  async function writeState(cwd, state) {
3268
- const path = statePath(cwd);
3343
+ const path = statePath2();
3269
3344
  const tmp = `${path}.tmp`;
3270
3345
  await mkdir(dirname(path), { recursive: true });
3271
3346
  await writeFile(tmp, `${JSON.stringify(state, null, 2)}
@@ -3273,7 +3348,7 @@ async function writeState(cwd, state) {
3273
3348
  await rename(tmp, path);
3274
3349
  }
3275
3350
  async function updateInstalled(cwd, name, version, manifestSha1) {
3276
- const state = await readState(cwd);
3351
+ const state = await readState();
3277
3352
  state.installed[name] = {
3278
3353
  version,
3279
3354
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3382,6 +3457,39 @@ var installCcHookAdd = async (ctx) => {
3382
3457
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
3383
3458
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath] };
3384
3459
  };
3460
+ function getUserClaudeJsonPath() {
3461
+ return join(homedir(), ".claude.json");
3462
+ }
3463
+ async function readUserClaudeJson() {
3464
+ const path = getUserClaudeJsonPath();
3465
+ let raw;
3466
+ try {
3467
+ raw = await readFile(path, "utf8");
3468
+ } catch (err2) {
3469
+ if (err2.code === "ENOENT") return {};
3470
+ throw err2;
3471
+ }
3472
+ try {
3473
+ const parsed = JSON.parse(raw);
3474
+ if (parsed === null || typeof parsed !== "object") return {};
3475
+ return parsed;
3476
+ } catch {
3477
+ return {};
3478
+ }
3479
+ }
3480
+ async function isMcpServerRegistered(name) {
3481
+ const config = await readUserClaudeJson();
3482
+ const servers = config.mcpServers;
3483
+ if (!servers || typeof servers !== "object") return false;
3484
+ return Object.hasOwn(servers, name);
3485
+ }
3486
+ async function isPluginRegistered(pluginName) {
3487
+ const config = await readUserClaudeJson();
3488
+ const plugins = config.enabledPlugins;
3489
+ if (!plugins || typeof plugins !== "object") return false;
3490
+ if (Object.hasOwn(plugins, pluginName)) return true;
3491
+ return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
3492
+ }
3385
3493
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3386
3494
  return new Promise((resolve9) => {
3387
3495
  const isWin = process.platform === "win32";
@@ -3525,34 +3633,8 @@ ${newEntry}
3525
3633
  )
3526
3634
  };
3527
3635
  }
3528
- const vr = await new Promise((resolve9) => {
3529
- const child = spawn("claude", ["plugin", "list", "--json"], {
3530
- cwd: spawnCwd,
3531
- shell: process.platform === "win32",
3532
- windowsHide: true
3533
- });
3534
- let stdout2 = "";
3535
- let stderr = "";
3536
- child.stdout?.setEncoding("utf8").on("data", (c) => {
3537
- stdout2 += c;
3538
- });
3539
- child.stderr?.setEncoding("utf8").on("data", (c) => {
3540
- stderr += c;
3541
- });
3542
- const timer = setTimeout(() => {
3543
- child.kill("SIGKILL");
3544
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
3545
- }, 15e3);
3546
- child.on("error", (e) => {
3547
- clearTimeout(timer);
3548
- resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
3549
- });
3550
- child.on("close", (code) => {
3551
- clearTimeout(timer);
3552
- resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
3553
- });
3554
- });
3555
- if (vr.exitCode !== 0 || !vr.stdout.includes(pluginName)) {
3636
+ const registered = await isPluginRegistered(pluginName);
3637
+ if (!registered) {
3556
3638
  return {
3557
3639
  ok: false,
3558
3640
  phase: "verify",
@@ -3560,7 +3642,7 @@ ${newEntry}
3560
3642
  error: err(
3561
3643
  ctx,
3562
3644
  "/spec/verify/cmd",
3563
- `verify exit ${vr.exitCode} or '${pluginName}' not in plugin list stdout: ${vr.stderr.slice(0, 200)}`,
3645
+ `verify: plugin '${pluginName}' not found in enabledPlugins map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude plugin install wrote to a non-default location)`,
3564
3646
  "verify-failed"
3565
3647
  )
3566
3648
  };
@@ -3828,6 +3910,8 @@ var installGitCloneWithSetup = async (ctx) => {
3828
3910
  await updateInstalled(ctx.cwd, name, install.git_ref, "");
3829
3911
  return { ok: true, backupId: bk.backupId, appliedFiles: [cloneTarget] };
3830
3912
  };
3913
+
3914
+ // src/installers/mcpHttpAdd.ts
3831
3915
  function resolveEnvVars(value) {
3832
3916
  const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
3833
3917
  let resolved = value;
@@ -3977,34 +4061,8 @@ ${newEntry}
3977
4061
  )
3978
4062
  };
3979
4063
  }
3980
- const vr = await new Promise((resolve9) => {
3981
- const child = spawn("claude", ["mcp", "list"], {
3982
- cwd: spawnCwd,
3983
- shell: process.platform === "win32",
3984
- windowsHide: true
3985
- });
3986
- let stdout2 = "";
3987
- let stderr = "";
3988
- child.stdout?.setEncoding("utf8").on("data", (c) => {
3989
- stdout2 += c;
3990
- });
3991
- child.stderr?.setEncoding("utf8").on("data", (c) => {
3992
- stderr += c;
3993
- });
3994
- const timer = setTimeout(() => {
3995
- child.kill("SIGKILL");
3996
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
3997
- }, 15e3);
3998
- child.on("error", (e) => {
3999
- clearTimeout(timer);
4000
- resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
4001
- });
4002
- child.on("close", (code) => {
4003
- clearTimeout(timer);
4004
- resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
4005
- });
4006
- });
4007
- if (vr.exitCode !== 0 || !vr.stdout.includes(name)) {
4064
+ const registered = await isMcpServerRegistered(name);
4065
+ if (!registered) {
4008
4066
  return {
4009
4067
  ok: false,
4010
4068
  phase: "verify",
@@ -4012,7 +4070,7 @@ ${newEntry}
4012
4070
  error: err(
4013
4071
  ctx,
4014
4072
  "/spec/verify/cmd",
4015
- `verify exit ${vr.exitCode} or '${name}' not in mcp list stdout: ${vr.stderr.slice(0, 200)}`,
4073
+ `verify: '${name}' not found in mcpServers map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude mcp add wrote to a non-default location)`,
4016
4074
  "verify-failed"
4017
4075
  )
4018
4076
  };
@@ -4020,6 +4078,8 @@ ${newEntry}
4020
4078
  await updateInstalled(ctx.cwd, name, install.npm_version, "");
4021
4079
  return { ok: true, backupId: bk.backupId, appliedFiles: [mcpFile] };
4022
4080
  };
4081
+
4082
+ // src/installers/mcpStdioAdd.ts
4023
4083
  var installMcpStdioAdd = async (ctx) => {
4024
4084
  const install = ctx.manifest.spec.install;
4025
4085
  if (install.method !== "mcp-stdio-add") {
@@ -4117,34 +4177,8 @@ ${newEntry}
4117
4177
  )
4118
4178
  };
4119
4179
  }
4120
- const vr = await new Promise((resolve9) => {
4121
- const child = spawn("claude", ["mcp", "list"], {
4122
- cwd: spawnCwd,
4123
- shell: process.platform === "win32",
4124
- windowsHide: true
4125
- });
4126
- let stdout2 = "";
4127
- let stderr = "";
4128
- child.stdout?.setEncoding("utf8").on("data", (c) => {
4129
- stdout2 += c;
4130
- });
4131
- child.stderr?.setEncoding("utf8").on("data", (c) => {
4132
- stderr += c;
4133
- });
4134
- const timer = setTimeout(() => {
4135
- child.kill("SIGKILL");
4136
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
4137
- }, 15e3);
4138
- child.on("error", (e) => {
4139
- clearTimeout(timer);
4140
- resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
4141
- });
4142
- child.on("close", (code) => {
4143
- clearTimeout(timer);
4144
- resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
4145
- });
4146
- });
4147
- if (vr.exitCode !== 0 || !vr.stdout.includes(name)) {
4180
+ const registered = await isMcpServerRegistered(name);
4181
+ if (!registered) {
4148
4182
  return {
4149
4183
  ok: false,
4150
4184
  phase: "verify",
@@ -4152,7 +4186,7 @@ ${newEntry}
4152
4186
  error: err(
4153
4187
  ctx,
4154
4188
  "/spec/verify/cmd",
4155
- `verify exit ${vr.exitCode} or '${name}' not in mcp list stdout: ${vr.stderr.slice(0, 200)}`,
4189
+ `verify: '${name}' not found in mcpServers map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude mcp add wrote to a non-default location)`,
4156
4190
  "verify-failed"
4157
4191
  )
4158
4192
  };
@@ -5029,12 +5063,15 @@ MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's conn
5029
5063
  process.exit(0);
5030
5064
  });
5031
5065
  }
5066
+
5067
+ // src/cli/status.ts
5068
+ init_harnessedRoot();
5032
5069
  function registerStatus(program2) {
5033
- program2.command("status").description("Show installed upstreams (from .harnessed/state.json)").action(async () => {
5070
+ program2.command("status").description("Show installed upstreams (from <harnessed-root>/state.json)").action(async () => {
5034
5071
  const state = await readState(process.cwd());
5035
5072
  const names = Object.keys(state.installed).sort();
5036
5073
  if (names.length === 0) {
5037
- console.log("no installs recorded (.harnessed/state.json absent or empty)");
5074
+ console.log(`no installs recorded (${harnessedFile("state.json")} absent or empty)`);
5038
5075
  } else {
5039
5076
  for (const n of names) {
5040
5077
  const e = state.installed[n];
@@ -5044,18 +5081,19 @@ function registerStatus(program2) {
5044
5081
  console.log(`
5045
5082
  ${names.length} install${names.length === 1 ? "" : "s"} recorded`);
5046
5083
  }
5084
+ const lockPath = harnessedFile(".lock");
5047
5085
  try {
5048
- const isLocked = await lockfile.check(".harnessed", {
5049
- lockfilePath: ".harnessed/.lock",
5086
+ const isLocked = await lockfile.check(getHarnessedRoot(), {
5087
+ lockfilePath: lockPath,
5050
5088
  stale: 1e4
5051
5089
  });
5052
5090
  if (isLocked) {
5053
- const s = await stat(".harnessed/.lock");
5091
+ const s = await stat(lockPath);
5054
5092
  const ageMs = Date.now() - s.mtime.getTime();
5055
5093
  const stale = ageMs > 1e4;
5056
5094
  console.log(`
5057
5095
  lock: held (since ${s.mtime.toISOString()})${stale ? " \u2014 STALE" : ""}`);
5058
- console.log(" to release: wait for process to finish or delete .harnessed/.lock");
5096
+ console.log(` to release: wait for process to finish or delete ${lockPath}`);
5059
5097
  } else {
5060
5098
  console.log("\nlock: free");
5061
5099
  }
@@ -5384,6 +5422,8 @@ function registerUninstall(program2) {
5384
5422
  }
5385
5423
 
5386
5424
  // src/cli.ts
5425
+ init_harnessedRoot();
5426
+ migrateLegacyHarnessedRoot();
5387
5427
  var program = new Command();
5388
5428
  program.name("harnessed").description("AI coding harness package manager + composition orchestrator").version(package_default.version);
5389
5429
  registerInstall(program);