harnessed 1.0.4 → 2.0.1

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
@@ -148,8 +148,8 @@ var init_path_guard = __esm({
148
148
  }
149
149
  });
150
150
  function branchOnSchemaVersion(v, handlers) {
151
- const isKnownV1 = Object.values(SCHEMA_VERSIONS).includes(v);
152
- return isKnownV1 ? handlers.v1() : handlers.unknown();
151
+ const isKnownVersion = Object.values(SCHEMA_VERSIONS).includes(v);
152
+ return isKnownVersion ? handlers.v1() : handlers.unknown();
153
153
  }
154
154
  var SCHEMA_VERSIONS;
155
155
  var init_schemaVersion = __esm({
@@ -172,8 +172,14 @@ var init_schemaVersion = __esm({
172
172
  // ← Phase 3.3 W0 T0.5 BACKFILL 11th surface (sister Phase 3.2 W2 T2.2 b875e21 commit msg claim "11th surface" was LATENT STALE — never registered; T0.5 surgical fix per sister Phase 3.2 W2 T2.6 latent W1 c37ee29 Rule 1 pattern)
173
173
  aliases: "harnessed.aliases.v1",
174
174
  // ← Phase 3.3 W1 T1.1 ADD 12th surface (D-01 RICH manifests/aliases.yaml upstream rename redirect + metadata)
175
- knownGood: "harnessed.known-good.v1"
175
+ knownGood: "harnessed.known-good.v1",
176
176
  // ← Phase 3.3 W1 T1.1 ADD 13th surface (D-03 YAML versions/<harnessed-ver>-known-good.yaml per-version lock)
177
+ capabilities: "harnessed.capabilities.v1",
178
+ // ← Phase v2.0-2.3 W0 T2.3.W0.6 ADD 14th surface (R20.2 flat yaml capabilities manifest validate)
179
+ judgment: "harnessed.judgment.v1",
180
+ // ← Phase v2.0-2.3 W0 T2.3.W0.6 ADD 15th surface (R20.4 multi-file judgments triggers/rules validate)
181
+ workflow: "harnessed.workflow.v2"
182
+ // ← Phase v2.0-2.4 W0 T2.4.W0.1 ADD 16th surface (R20.1 + R20.2 + R20.9 — workflow.yaml v2 schema: gate / on / capability / args / fallback / parallelism 字段, sister 4 workflow.yaml: plan-feature + execute-task + research + verify-work)
177
183
  };
178
184
  Type.Union([
179
185
  Type.Literal(SCHEMA_VERSIONS.routingSnapshot),
@@ -193,8 +199,14 @@ var init_schemaVersion = __esm({
193
199
  // ← Phase 3.3 W0 T0.5 BACKFILL 11th surface
194
200
  Type.Literal(SCHEMA_VERSIONS.aliases),
195
201
  // ← Phase 3.3 W1 T1.1 ADD 12th surface
196
- Type.Literal(SCHEMA_VERSIONS.knownGood)
202
+ Type.Literal(SCHEMA_VERSIONS.knownGood),
197
203
  // ← Phase 3.3 W1 T1.1 ADD 13th surface
204
+ Type.Literal(SCHEMA_VERSIONS.capabilities),
205
+ // ← Phase v2.0-2.3 W0 T2.3.W0.6 ADD 14th surface
206
+ Type.Literal(SCHEMA_VERSIONS.judgment),
207
+ // ← Phase v2.0-2.3 W0 T2.3.W0.6 ADD 15th surface
208
+ Type.Literal(SCHEMA_VERSIONS.workflow)
209
+ // ← Phase v2.0-2.4 W0 T2.4.W0.1 ADD 16th surface (workflow.yaml v2 schema, NOTE first .v2 surface in union)
198
210
  ]);
199
211
  }
200
212
  });
@@ -456,6 +468,109 @@ var init_check_token_budget = __esm({
456
468
  PER_SKILL_THRESHOLD = 5e3;
457
469
  }
458
470
  });
471
+ async function checkAgentTeams() {
472
+ const envValue = process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
473
+ const envOn = envValue === "1";
474
+ let settingsValue;
475
+ let settingsOn = false;
476
+ try {
477
+ const path = resolve(homedir(), ".claude", "settings.json");
478
+ const raw = await readFile(path, "utf8");
479
+ const data = JSON.parse(raw);
480
+ settingsValue = data.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
481
+ settingsOn = settingsValue === "1";
482
+ } catch {
483
+ }
484
+ const detected = { env: envOn, settingsJson: settingsOn };
485
+ if (envOn || settingsOn) {
486
+ return { status: "pass", detected, envValue, settingsValue };
487
+ }
488
+ return {
489
+ status: "missing",
490
+ detected,
491
+ envValue,
492
+ settingsValue,
493
+ remediation: 'Agent Teams not enabled. Add to ~/.claude/settings.json:\n "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" }\nOR run: claude config set env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1\nOR export env var:\n export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1\nThen restart Claude Code (CC >= 2.1.133 required).'
494
+ };
495
+ }
496
+ var init_checkAgentTeams = __esm({
497
+ "src/cli/lib/checkAgentTeams.ts"() {
498
+ }
499
+ });
500
+
501
+ // src/cli/lib/check-agent-teams-doctor.ts
502
+ var check_agent_teams_doctor_exports = {};
503
+ __export(check_agent_teams_doctor_exports, {
504
+ checkAgentTeamsDoctor: () => checkAgentTeamsDoctor
505
+ });
506
+ async function checkAgentTeamsDoctor() {
507
+ const r = await checkAgentTeams();
508
+ if (r.status === "pass") {
509
+ const source = r.detected.env ? "env var" : "settings.json";
510
+ return {
511
+ name: "Agent Teams env",
512
+ status: "pass",
513
+ message: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (${source})`
514
+ };
515
+ }
516
+ return {
517
+ name: "Agent Teams env",
518
+ status: "warn",
519
+ message: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS not set (Agent Teams disabled)",
520
+ fix: r.remediation
521
+ };
522
+ }
523
+ var init_check_agent_teams_doctor = __esm({
524
+ "src/cli/lib/check-agent-teams-doctor.ts"() {
525
+ init_checkAgentTeams();
526
+ }
527
+ });
528
+
529
+ // src/cli/lib/check-planning-with-files.ts
530
+ var check_planning_with_files_exports = {};
531
+ __export(check_planning_with_files_exports, {
532
+ checkPlanningWithFiles: () => checkPlanningWithFiles
533
+ });
534
+ async function checkPlanningWithFiles() {
535
+ const root = join(
536
+ homedir(),
537
+ ".claude",
538
+ "plugins",
539
+ "cache",
540
+ "planning-with-files",
541
+ "planning-with-files"
542
+ );
543
+ try {
544
+ const entries = await readdir(root);
545
+ const versions = entries.filter((e) => /^\d+\.\d+/.test(e));
546
+ if (versions.length > 0) {
547
+ return {
548
+ name: "planning-with-files plugin",
549
+ status: "pass",
550
+ message: `installed (version ${versions.join(", ")})`
551
+ };
552
+ }
553
+ return {
554
+ name: "planning-with-files plugin",
555
+ status: "warn",
556
+ message: "plugin directory exists but no version subdir found",
557
+ fix: REMEDIATION
558
+ };
559
+ } catch {
560
+ return {
561
+ name: "planning-with-files plugin",
562
+ status: "warn",
563
+ message: "not installed (plugin cache path missing)",
564
+ fix: REMEDIATION
565
+ };
566
+ }
567
+ }
568
+ var REMEDIATION;
569
+ var init_check_planning_with_files = __esm({
570
+ "src/cli/lib/check-planning-with-files.ts"() {
571
+ REMEDIATION = "install via Claude Code plugin marketplace: `claude plugin install planning-with-files` (requires >=2.2.0 per R20.15 + D-15)";
572
+ }
573
+ });
459
574
  async function withLock(fn) {
460
575
  let release;
461
576
  try {
@@ -670,7 +785,7 @@ var init_resume = __esm({
670
785
 
671
786
  // package.json
672
787
  var package_default = {
673
- version: "1.0.4"};
788
+ version: "2.0.1"};
674
789
 
675
790
  // src/manifest/errors.ts
676
791
  function instancePathToKeyPath(instancePath) {
@@ -1451,7 +1566,7 @@ function renderHumanTable(records) {
1451
1566
  }
1452
1567
  }
1453
1568
  function pipeToJq(filterExpr, lines) {
1454
- return new Promise((resolve10, reject) => {
1569
+ return new Promise((resolve8, reject) => {
1455
1570
  const child = spawn("jq", [filterExpr], {
1456
1571
  stdio: ["pipe", "inherit", "inherit"],
1457
1572
  windowsHide: true
@@ -1460,12 +1575,12 @@ function pipeToJq(filterExpr, lines) {
1460
1575
  const e = err2;
1461
1576
  if (e.code === "ENOENT") {
1462
1577
  console.error("\u2717 jq not found in PATH \u2014 run: harnessed doctor");
1463
- resolve10(1);
1578
+ resolve8(1);
1464
1579
  } else {
1465
1580
  reject(err2);
1466
1581
  }
1467
1582
  });
1468
- child.on("close", (code) => resolve10(code ?? 0));
1583
+ child.on("close", (code) => resolve8(code ?? 0));
1469
1584
  child.stdin.write(lines.join("\n"));
1470
1585
  child.stdin.end();
1471
1586
  });
@@ -1523,19 +1638,132 @@ function registerAuditLog(program2) {
1523
1638
  }
1524
1639
  );
1525
1640
  }
1641
+ function getBackupRoot() {
1642
+ return join(homedir(), ".harnessed", "backups");
1643
+ }
1644
+ var HOME_DIR = process.env.HOME ?? process.env.USERPROFILE ?? "";
1645
+ function mirrorPath(target, scope, backupDir) {
1646
+ const root = scope === "HOME" ? HOME_DIR : ".";
1647
+ const rel = root ? relative(root, target) : target;
1648
+ if (!rel || rel.startsWith("..")) {
1649
+ const flat = createHash("sha1").update(target).digest("hex").slice(0, 16);
1650
+ return join(backupDir, scope, flat);
1651
+ }
1652
+ return join(backupDir, scope, rel);
1653
+ }
1654
+ function detectEol(buf) {
1655
+ return buf.includes("\r\n") ? "crlf" : "lf";
1656
+ }
1657
+ async function backup(plan, ctx) {
1658
+ const filename = ctx.manifest.metadata.name;
1659
+ const backupId = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
1660
+ const backupDir = join(getBackupRoot(), backupId);
1661
+ try {
1662
+ await mkdir(backupDir, { recursive: true });
1663
+ } catch (err2) {
1664
+ return {
1665
+ ok: false,
1666
+ error: {
1667
+ file: filename,
1668
+ path: "/",
1669
+ message: `failed to create backup dir ${backupDir}: ${err2.message}`,
1670
+ line: null,
1671
+ column: null,
1672
+ keyword: "backup-mkdir-failed"
1673
+ }
1674
+ };
1675
+ }
1676
+ const entries = [];
1677
+ for (const file of plan.files) {
1678
+ let buf;
1679
+ try {
1680
+ buf = await readFile(file.target);
1681
+ } catch (err2) {
1682
+ const code = err2.code;
1683
+ if (code === "ENOENT" && file.oldText === "") {
1684
+ entries.push({
1685
+ target: file.target,
1686
+ backup: "",
1687
+ // sentinel: no backup written; rollback should unlink target
1688
+ sha1: "",
1689
+ eol: "lf"
1690
+ // moot for non-existent file; default to lf
1691
+ });
1692
+ continue;
1693
+ }
1694
+ return {
1695
+ ok: false,
1696
+ error: {
1697
+ file: filename,
1698
+ path: file.target,
1699
+ message: `failed to read original file for backup: ${err2.message}`,
1700
+ line: null,
1701
+ column: null,
1702
+ keyword: "backup-read-failed"
1703
+ }
1704
+ };
1705
+ }
1706
+ const sha1 = createHash("sha1").update(buf).digest("hex");
1707
+ const eol = detectEol(buf);
1708
+ const dest = mirrorPath(file.target, file.scope, backupDir);
1709
+ try {
1710
+ await mkdir(dirname(dest), { recursive: true });
1711
+ await writeFile(dest, buf);
1712
+ } catch (err2) {
1713
+ return {
1714
+ ok: false,
1715
+ error: {
1716
+ file: filename,
1717
+ path: dest,
1718
+ message: `failed to write backup copy: ${err2.message}`,
1719
+ line: null,
1720
+ column: null,
1721
+ keyword: "backup-write-failed"
1722
+ }
1723
+ };
1724
+ }
1725
+ entries.push({ target: file.target, backup: dest, sha1, eol });
1726
+ }
1727
+ const metadata = {
1728
+ installer: filename,
1729
+ manifest: filename,
1730
+ timestamp: backupId,
1731
+ files: entries
1732
+ };
1733
+ const metadataPath = join(backupDir, "metadata.json");
1734
+ try {
1735
+ await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
1736
+ `, "utf8");
1737
+ } catch (err2) {
1738
+ return {
1739
+ ok: false,
1740
+ error: {
1741
+ file: filename,
1742
+ path: metadataPath,
1743
+ message: `failed to write metadata.json: ${err2.message}`,
1744
+ line: null,
1745
+ column: null,
1746
+ keyword: "backup-metadata-failed"
1747
+ }
1748
+ };
1749
+ }
1750
+ return { ok: true, backupId, backupDir };
1751
+ }
1752
+
1753
+ // src/cli/backup-list.ts
1526
1754
  function registerBackupList(program2) {
1527
1755
  const backup2 = program2.command("backup").description("Backup snapshot operations");
1528
1756
  backup2.command("list").description("List backup snapshots under .harnessed-backup/").action(async () => {
1529
- const root = resolve(process.cwd(), ".harnessed-backup");
1757
+ const root = getBackupRoot();
1530
1758
  let dirs;
1531
1759
  try {
1532
1760
  dirs = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1533
1761
  } catch {
1534
- console.log("no backups found (.harnessed-backup/ absent)");
1762
+ console.log(`no backups found (${root} absent)`);
1535
1763
  return;
1536
1764
  }
1537
1765
  if (dirs.length === 0) {
1538
- console.log("no backups found (.harnessed-backup/ empty)");
1766
+ console.log(`no backups found (${root} empty)`);
1539
1767
  return;
1540
1768
  }
1541
1769
  for (const ts of dirs) {
@@ -1649,29 +1877,36 @@ async function checkDeprecations2() {
1649
1877
  async function checkTokenBudget2() {
1650
1878
  return (await Promise.resolve().then(() => (init_check_token_budget(), check_token_budget_exports))).checkTokenBudget();
1651
1879
  }
1880
+ async function checkAgentTeamsEnv() {
1881
+ return (await Promise.resolve().then(() => (init_check_agent_teams_doctor(), check_agent_teams_doctor_exports))).checkAgentTeamsDoctor();
1882
+ }
1883
+ async function checkPlanningPlugin() {
1884
+ return (await Promise.resolve().then(() => (init_check_planning_with_files(), check_planning_with_files_exports))).checkPlanningWithFiles();
1885
+ }
1652
1886
  function registerDoctor(program2) {
1653
1887
  program2.command("doctor").description(
1654
- "Preflight checks (Node / MCP scope / jq / Win bash / origin URL / gstack prefix / deprecations / token budget)"
1888
+ "Preflight checks (Node / MCP scope / jq / Win bash / origin URL / gstack prefix / deprecations / token budget / Agent Teams / planning-with-files)"
1655
1889
  ).option("--json", "output JSON instead of human-readable").action(async (opts) => {
1656
- const [mcpScope, originUrl, gstackPrefix, deprecations, tokenBudget] = await Promise.all([
1890
+ const [mcp, origin, gstack, dep, tok, at, ppwf] = await Promise.all([
1657
1891
  checkMcpScope(),
1658
1892
  checkOriginUrl(),
1659
1893
  checkGstackPrefix(),
1660
- // ← Phase 3.2 W1 T1.5 ADD 6th check (D-01 PROBE)
1661
1894
  checkDeprecations2(),
1662
- // ← Phase 3.3 W1 T1.7 ADD 7th check (D-02 DOCTOR-ONLY-WARN)
1663
- checkTokenBudget2()
1664
- // ← Phase 3.4 W1 T1.2 ADD 8th check (D-03 + D-04 DOCTOR WARN)
1895
+ checkTokenBudget2(),
1896
+ checkAgentTeamsEnv(),
1897
+ checkPlanningPlugin()
1665
1898
  ]);
1666
1899
  const results = [
1667
1900
  checkNodeVersion(),
1668
- mcpScope,
1901
+ mcp,
1669
1902
  checkJq(),
1670
1903
  checkWinBash(),
1671
- originUrl,
1672
- gstackPrefix,
1673
- deprecations,
1674
- tokenBudget
1904
+ origin,
1905
+ gstack,
1906
+ dep,
1907
+ tok,
1908
+ at,
1909
+ ppwf
1675
1910
  ];
1676
1911
  const hasFail = results.some((r) => r.status === "fail");
1677
1912
  const hasWarn = results.some((r) => r.status === "warn");
@@ -2056,6 +2291,51 @@ async function completePhase(ctx) {
2056
2291
  await complete();
2057
2292
  }
2058
2293
 
2294
+ // src/routing/lib/fallbackHandlers.ts
2295
+ function handleMaxIterationsExceeded(err2, fallback, ctx) {
2296
+ const yamlShort = fallback.message.replace(
2297
+ /\{\{\s*args\.max_iterations\s*\}\}/g,
2298
+ String(ctx.maxIterations)
2299
+ );
2300
+ const truncated = (ctx.lastMessage ?? "<empty>").slice(0, 500);
2301
+ const uxText = `\u274C ralph-loop max-iterations exceeded (${err2.iterations}/${ctx.maxIterations}).
2302
+ Sub-task: ${ctx.subtaskSummary}
2303
+ Workflow: ${ctx.workflowName} / phase ${ctx.phaseId}
2304
+ Last subagent output (truncated): ${truncated}
2305
+ The subagent attempted ${err2.iterations} iterations without emitting verbatim "<promise>COMPLETE</promise>".
2306
+ This indicates one of:
2307
+ 1. Sub-task is genuinely incomplete (escalate to user / re-scope)
2308
+ 2. Subagent is stuck in a loop (review prompt / system instructions)
2309
+ 3. max-iterations too low (override via --max-iterations <N>, hard upper limit 100)
2310
+ Manual options:
2311
+ A) Continue with current state: \`harnessed workflow resume --skip-completion-gate\`
2312
+ B) Re-run from last checkpoint: \`harnessed workflow resume --from-checkpoint\`
2313
+ C) Abort cleanly: exit 1
2314
+ Exit code: ${fallback.exit_code}
2315
+ ${yamlShort}`;
2316
+ console.error(uxText);
2317
+ process.exit(fallback.exit_code);
2318
+ throw new Error("unreachable");
2319
+ }
2320
+ function handleVerbatimCompleteFail(err2, fallback, ctx) {
2321
+ const truncated = err2.lastMessage.slice(0, 500);
2322
+ const uxText = `\u274C ralph-loop verbatim COMPLETE signal missing (F33 P1).
2323
+ Sub-task: ${ctx.subtaskSummary}
2324
+ Workflow: ${ctx.workflowName} / phase ${ctx.phaseId}
2325
+ Last subagent output (truncated): ${truncated}
2326
+ The subagent's final message lacked verbatim "<promise>COMPLETE</promise>" tag.
2327
+ This indicates one of:
2328
+ 1. Subagent skipped the completion-promise contract (review system prompt)
2329
+ 2. Output format misconfigured (check outputFormat schema)
2330
+ Manual options:
2331
+ A) Re-run with explicit COMPLETE instruction in subagent prompt
2332
+ B) Abort cleanly: exit ${fallback.exit_code}
2333
+ Exit code: ${fallback.exit_code}`;
2334
+ console.error(uxText);
2335
+ process.exit(fallback.exit_code);
2336
+ throw new Error("unreachable");
2337
+ }
2338
+
2059
2339
  // src/routing/lib/promiseExtract.ts
2060
2340
  var PROMISE_PATTERN = /<promise>([^<]+)<\/promise>/;
2061
2341
  function extractPromise(text) {
@@ -2265,9 +2545,6 @@ async function runRouting(task, opts = {}) {
2265
2545
  try {
2266
2546
  await ensureSkillsInstalled(decision.required_skills ?? [], skillsRoot);
2267
2547
  } catch (error) {
2268
- if (error instanceof RestartRequiredError || error instanceof SkillNotInstalledError || error instanceof MissingSkillsError) {
2269
- return { ok: false, phase: "install", error };
2270
- }
2271
2548
  return { ok: false, phase: "install", error };
2272
2549
  }
2273
2550
  let agentDef;
@@ -2294,6 +2571,11 @@ async function runRouting(task, opts = {}) {
2294
2571
  onSessionIdInner?.(id);
2295
2572
  }
2296
2573
  });
2574
+ const fbCtx = {
2575
+ subtaskSummary: task.task,
2576
+ workflowName: task.task_type ?? "unknown",
2577
+ phaseId: opts.fallbackPhaseId ?? task.phaseId ?? "unknown"
2578
+ };
2297
2579
  try {
2298
2580
  const result = await ralphLoopWrap(wrappedSpawn, maxIter);
2299
2581
  await completePhase({ phaseId, sessionId: capturedSessionId, status: "complete" });
@@ -2302,10 +2584,16 @@ async function runRouting(task, opts = {}) {
2302
2584
  } catch (error) {
2303
2585
  if (error instanceof MaxIterationsExceededError) {
2304
2586
  emitAudit(task, decision, matched, "max-iter", capturedSessionId);
2587
+ if (opts.fallbackConfig)
2588
+ handleMaxIterationsExceeded(error, opts.fallbackConfig, {
2589
+ ...fbCtx,
2590
+ maxIterations: maxIter
2591
+ });
2305
2592
  return { aborted: true, reason: error.message };
2306
2593
  }
2307
2594
  if (error instanceof VerbatimCompleteFailError) {
2308
2595
  emitAudit(task, decision, matched, "verbatim-fail", capturedSessionId);
2596
+ if (opts.fallbackConfig) handleVerbatimCompleteFail(error, opts.fallbackConfig, fbCtx);
2309
2597
  return { ok: false, phase: "verbatim", error };
2310
2598
  }
2311
2599
  emitAudit(task, decision, matched, "spawn-err", capturedSessionId);
@@ -2346,6 +2634,70 @@ var PhasesSchema = Type.Object(
2346
2634
  { additionalProperties: false }
2347
2635
  );
2348
2636
 
2637
+ // src/workflow/schema/workflow.ts
2638
+ init_schemaVersion();
2639
+ var ModelTier2 = Type.Union([Type.Literal("haiku"), Type.Literal("sonnet"), Type.Literal("opus")]);
2640
+ var OnAction = Type.Union([Type.Literal("skip"), Type.Literal("invoke")]);
2641
+ var OnClause = Type.Object(
2642
+ {
2643
+ if: Type.String(),
2644
+ // expr-eval expression OR judgments.<file>.<gate>.fires ref
2645
+ invoke: Type.Optional(Type.String()),
2646
+ // '{{ capabilities.<name>.cmd }}' OR literal
2647
+ action: Type.Optional(OnAction)
2648
+ },
2649
+ { additionalProperties: false }
2650
+ );
2651
+ var FallbackMaxIterationsExceeded = Type.Object(
2652
+ {
2653
+ action: Type.Literal("emit_warning_and_halt"),
2654
+ // R20.10 acceptance c "explicit NOT silent"
2655
+ message: Type.String(),
2656
+ exit_code: Type.Number()
2657
+ },
2658
+ { additionalProperties: false }
2659
+ );
2660
+ var PhaseFallback = Type.Object(
2661
+ {
2662
+ max_iterations_exceeded: Type.Optional(FallbackMaxIterationsExceeded)
2663
+ },
2664
+ { additionalProperties: false }
2665
+ );
2666
+ var WorkflowPhaseV2 = Type.Object(
2667
+ {
2668
+ id: Type.String({ minLength: 1 }),
2669
+ name: Type.Optional(Type.String()),
2670
+ upstream: Type.Optional(Type.String()),
2671
+ capability: Type.Optional(Type.String()),
2672
+ // '{{ capabilities.ralph-loop.cmd }}'
2673
+ model: Type.Optional(ModelTier2),
2674
+ invokes: Type.Optional(Type.String()),
2675
+ // legacy slash-cmd OR JINJA template
2676
+ args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
2677
+ gate: Type.Optional(Type.String()),
2678
+ // judgments.<file>.<gate>.fires 4-level ref
2679
+ on: Type.Optional(Type.Array(OnClause)),
2680
+ parallelism: Type.Optional(Type.String()),
2681
+ // judgments.parallelism-gate.<route>.fires
2682
+ fallback: Type.Optional(PhaseFallback),
2683
+ max_iterations: Type.Optional(
2684
+ Type.Union([Type.Number(), Type.String()])
2685
+ // numeric literal OR jinja '{{ defaults.x.y }}'
2686
+ ),
2687
+ artifacts_expected: Type.Optional(Type.Array(Type.String()))
2688
+ },
2689
+ { additionalProperties: false }
2690
+ );
2691
+ var WorkflowSchemaV2 = Type.Object(
2692
+ {
2693
+ schema_version: Type.Literal(SCHEMA_VERSIONS.workflow),
2694
+ workflow: Type.String({ minLength: 1 }),
2695
+ description: Type.Optional(Type.String()),
2696
+ phases: Type.Array(WorkflowPhaseV2, { minItems: 1 })
2697
+ },
2698
+ { additionalProperties: false }
2699
+ );
2700
+
2349
2701
  // src/workflow/loadPhases.ts
2350
2702
  var PhasesValidationError = class extends Error {
2351
2703
  constructor(errors) {
@@ -2358,10 +2710,18 @@ var PhasesValidationError = class extends Error {
2358
2710
  function loadPhases(yamlPath, vars) {
2359
2711
  const raw = readFileSync(yamlPath, "utf8");
2360
2712
  const parsed = parse(raw);
2361
- if (!Value.Check(PhasesSchema, parsed)) {
2362
- throw new PhasesValidationError([...Value.Errors(PhasesSchema, parsed)]);
2713
+ const isV2 = parsed?.schema_version === "harnessed.workflow.v2";
2714
+ if (isV2) {
2715
+ if (!Value.Check(WorkflowSchemaV2, parsed)) {
2716
+ throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV2, parsed)]);
2717
+ }
2718
+ } else {
2719
+ if (!Value.Check(PhasesSchema, parsed)) {
2720
+ throw new PhasesValidationError([...Value.Errors(PhasesSchema, parsed)]);
2721
+ }
2363
2722
  }
2364
- return parsed;
2723
+ const validated = parsed;
2724
+ return validated;
2365
2725
  }
2366
2726
 
2367
2727
  // src/cli/lib/validateFlags.ts
@@ -2407,9 +2767,20 @@ function registerExecuteTask(program2) {
2407
2767
  );
2408
2768
  process.exit(0);
2409
2769
  }
2770
+ let fallbackConfig;
2771
+ let fallbackPhaseId;
2772
+ for (const ph of phases.phases) {
2773
+ if ("fallback" in ph && ph.fallback?.max_iterations_exceeded) {
2774
+ fallbackConfig = ph.fallback.max_iterations_exceeded;
2775
+ fallbackPhaseId = ph.id;
2776
+ break;
2777
+ }
2778
+ }
2410
2779
  const result = await runRouting(taskCtx, {
2411
2780
  maxIterations: raw.maxIterations ?? 20,
2412
- ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {}
2781
+ ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {},
2782
+ ...fallbackConfig ? { fallbackConfig } : {},
2783
+ ...fallbackPhaseId ? { fallbackPhaseId } : {}
2413
2784
  });
2414
2785
  if ("aborted" in result) {
2415
2786
  console.error(`aborted: ${result.reason}`);
@@ -2463,12 +2834,12 @@ function registerGc(program2) {
2463
2834
  return;
2464
2835
  }
2465
2836
  const keepLast = Number.parseInt(opts.keepLast ?? "0", 10);
2466
- const root = resolve(process.cwd(), ".harnessed-backup");
2837
+ const root = getBackupRoot();
2467
2838
  let dirs;
2468
2839
  try {
2469
2840
  dirs = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
2470
2841
  } catch {
2471
- console.log("no backups found (.harnessed-backup/ absent) \u2014 nothing to gc");
2842
+ console.log(`no backups found (${root} absent) \u2014 nothing to gc`);
2472
2843
  return;
2473
2844
  }
2474
2845
  const cutoff = Date.now() - olderMs;
@@ -2505,114 +2876,6 @@ function registerGc(program2) {
2505
2876
  if (dryRun) console.log("\n(dry-run \u2014 re-run with --apply to actually delete)");
2506
2877
  });
2507
2878
  }
2508
- var HOME_DIR = process.env.HOME ?? process.env.USERPROFILE ?? "";
2509
- function mirrorPath(target, scope, backupDir) {
2510
- const root = scope === "HOME" ? HOME_DIR : ".";
2511
- const rel = root ? relative(root, target) : target;
2512
- if (!rel || rel.startsWith("..")) {
2513
- const flat = createHash("sha1").update(target).digest("hex").slice(0, 16);
2514
- return join(backupDir, scope, flat);
2515
- }
2516
- return join(backupDir, scope, rel);
2517
- }
2518
- function detectEol(buf) {
2519
- return buf.includes("\r\n") ? "crlf" : "lf";
2520
- }
2521
- async function backup(plan, ctx) {
2522
- const filename = ctx.manifest.metadata.name;
2523
- const backupId = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
2524
- const backupDir = join(ctx.cwd, ".harnessed-backup", backupId);
2525
- try {
2526
- await mkdir(backupDir, { recursive: true });
2527
- } catch (err2) {
2528
- return {
2529
- ok: false,
2530
- error: {
2531
- file: filename,
2532
- path: "/",
2533
- message: `failed to create backup dir ${backupDir}: ${err2.message}`,
2534
- line: null,
2535
- column: null,
2536
- keyword: "backup-mkdir-failed"
2537
- }
2538
- };
2539
- }
2540
- const entries = [];
2541
- for (const file of plan.files) {
2542
- let buf;
2543
- try {
2544
- buf = await readFile(file.target);
2545
- } catch (err2) {
2546
- const code = err2.code;
2547
- if (code === "ENOENT" && file.oldText === "") {
2548
- entries.push({
2549
- target: file.target,
2550
- backup: "",
2551
- // sentinel: no backup written; rollback should unlink target
2552
- sha1: "",
2553
- eol: "lf"
2554
- // moot for non-existent file; default to lf
2555
- });
2556
- continue;
2557
- }
2558
- return {
2559
- ok: false,
2560
- error: {
2561
- file: filename,
2562
- path: file.target,
2563
- message: `failed to read original file for backup: ${err2.message}`,
2564
- line: null,
2565
- column: null,
2566
- keyword: "backup-read-failed"
2567
- }
2568
- };
2569
- }
2570
- const sha1 = createHash("sha1").update(buf).digest("hex");
2571
- const eol = detectEol(buf);
2572
- const dest = mirrorPath(file.target, file.scope, backupDir);
2573
- try {
2574
- await mkdir(dirname(dest), { recursive: true });
2575
- await writeFile(dest, buf);
2576
- } catch (err2) {
2577
- return {
2578
- ok: false,
2579
- error: {
2580
- file: filename,
2581
- path: dest,
2582
- message: `failed to write backup copy: ${err2.message}`,
2583
- line: null,
2584
- column: null,
2585
- keyword: "backup-write-failed"
2586
- }
2587
- };
2588
- }
2589
- entries.push({ target: file.target, backup: dest, sha1, eol });
2590
- }
2591
- const metadata = {
2592
- installer: filename,
2593
- manifest: filename,
2594
- timestamp: backupId,
2595
- files: entries
2596
- };
2597
- const metadataPath = join(backupDir, "metadata.json");
2598
- try {
2599
- await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
2600
- `, "utf8");
2601
- } catch (err2) {
2602
- return {
2603
- ok: false,
2604
- error: {
2605
- file: filename,
2606
- path: metadataPath,
2607
- message: `failed to write metadata.json: ${err2.message}`,
2608
- line: null,
2609
- column: null,
2610
- keyword: "backup-metadata-failed"
2611
- }
2612
- };
2613
- }
2614
- return { ok: true, backupId, backupDir };
2615
- }
2616
2879
  async function confirmAt(level, ctx) {
2617
2880
  if (level === "L4" && !ctx.opts.system) {
2618
2881
  if (!ctx.opts.nonInteractive) {
@@ -2894,7 +3157,7 @@ var installCcHookAdd = async (ctx) => {
2894
3157
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath] };
2895
3158
  };
2896
3159
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
2897
- return new Promise((resolve10) => {
3160
+ return new Promise((resolve8) => {
2898
3161
  const isWin = process.platform === "win32";
2899
3162
  const child = isWin ? spawn("cmd.exe", ["/c", "claude", ...claudeArgs], { cwd, windowsHide: true }) : spawn("claude", claudeArgs, { cwd, shell: false });
2900
3163
  let stderr = "";
@@ -2903,15 +3166,15 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
2903
3166
  });
2904
3167
  const timer = setTimeout(() => {
2905
3168
  child.kill("SIGKILL");
2906
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
3169
+ resolve8({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
2907
3170
  }, timeoutMs);
2908
3171
  child.on("error", (e) => {
2909
3172
  clearTimeout(timer);
2910
- resolve10({ exitCode: -1, stderr: `${stderr}${e.message}` });
3173
+ resolve8({ exitCode: -1, stderr: `${stderr}${e.message}` });
2911
3174
  });
2912
3175
  child.on("close", (code) => {
2913
3176
  clearTimeout(timer);
2914
- resolve10({ exitCode: code ?? -1, stderr });
3177
+ resolve8({ exitCode: code ?? -1, stderr });
2915
3178
  });
2916
3179
  });
2917
3180
  }
@@ -3052,7 +3315,7 @@ ${newEntry}
3052
3315
  )
3053
3316
  };
3054
3317
  }
3055
- const vr = await new Promise((resolve10) => {
3318
+ const vr = await new Promise((resolve8) => {
3056
3319
  const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
3057
3320
  let stderr = "";
3058
3321
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -3060,15 +3323,15 @@ ${newEntry}
3060
3323
  });
3061
3324
  const timer = setTimeout(() => {
3062
3325
  child.kill("SIGKILL");
3063
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout]` });
3326
+ resolve8({ exitCode: -1, stderr: `${stderr}[timeout]` });
3064
3327
  }, 15e3);
3065
3328
  child.on("error", (e) => {
3066
3329
  clearTimeout(timer);
3067
- resolve10({ exitCode: -1, stderr: e.message });
3330
+ resolve8({ exitCode: -1, stderr: e.message });
3068
3331
  });
3069
3332
  child.on("close", (code) => {
3070
3333
  clearTimeout(timer);
3071
- resolve10({ exitCode: code ?? -1, stderr });
3334
+ resolve8({ exitCode: code ?? -1, stderr });
3072
3335
  });
3073
3336
  });
3074
3337
  if (vr.exitCode !== 0) {
@@ -3124,10 +3387,10 @@ async function spawnCmd(ctx, cmd, args) {
3124
3387
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
3125
3388
  stderr += chunk;
3126
3389
  });
3127
- return await new Promise((resolve10) => {
3390
+ return await new Promise((resolve8) => {
3128
3391
  const timer = setTimeout(() => {
3129
3392
  child.kill("SIGKILL");
3130
- resolve10({
3393
+ resolve8({
3131
3394
  ok: false,
3132
3395
  phase: "spawn",
3133
3396
  error: {
@@ -3142,7 +3405,7 @@ async function spawnCmd(ctx, cmd, args) {
3142
3405
  }, timeoutMs);
3143
3406
  child.on("error", (err2) => {
3144
3407
  clearTimeout(timer);
3145
- resolve10({
3408
+ resolve8({
3146
3409
  ok: false,
3147
3410
  phase: "spawn",
3148
3411
  error: {
@@ -3157,14 +3420,14 @@ async function spawnCmd(ctx, cmd, args) {
3157
3420
  });
3158
3421
  child.on("close", (code) => {
3159
3422
  clearTimeout(timer);
3160
- resolve10({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3423
+ resolve8({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3161
3424
  });
3162
3425
  });
3163
3426
  }
3164
3427
 
3165
3428
  // src/installers/gitCloneWithSetup.ts
3166
3429
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
3167
- return new Promise((resolve10) => {
3430
+ return new Promise((resolve8) => {
3168
3431
  const isWin = process.platform === "win32";
3169
3432
  const child = isWin ? spawn("cmd.exe", ["/c", "git", "rev-parse", "HEAD"], { cwd, windowsHide: true }) : spawn("git", ["rev-parse", "HEAD"], { cwd, shell: false });
3170
3433
  let stdout2 = "";
@@ -3173,15 +3436,15 @@ function gitRevParseHead(cwd, timeoutMs = 1e4) {
3173
3436
  });
3174
3437
  const timer = setTimeout(() => {
3175
3438
  child.kill("SIGKILL");
3176
- resolve10({ sha: "", exit: -1 });
3439
+ resolve8({ sha: "", exit: -1 });
3177
3440
  }, timeoutMs);
3178
3441
  child.on("error", () => {
3179
3442
  clearTimeout(timer);
3180
- resolve10({ sha: "", exit: -1 });
3443
+ resolve8({ sha: "", exit: -1 });
3181
3444
  });
3182
3445
  child.on("close", (code) => {
3183
3446
  clearTimeout(timer);
3184
- resolve10({ sha: stdout2.trim(), exit: code ?? -1 });
3447
+ resolve8({ sha: stdout2.trim(), exit: code ?? -1 });
3185
3448
  });
3186
3449
  });
3187
3450
  }
@@ -3521,7 +3784,7 @@ ${newEntry}
3521
3784
  )
3522
3785
  };
3523
3786
  }
3524
- const vr = await new Promise((resolve10) => {
3787
+ const vr = await new Promise((resolve8) => {
3525
3788
  const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
3526
3789
  let stderr = "";
3527
3790
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -3529,15 +3792,15 @@ ${newEntry}
3529
3792
  });
3530
3793
  const timer = setTimeout(() => {
3531
3794
  child.kill("SIGKILL");
3532
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout]` });
3795
+ resolve8({ exitCode: -1, stderr: `${stderr}[timeout]` });
3533
3796
  }, 15e3);
3534
3797
  child.on("error", (e) => {
3535
3798
  clearTimeout(timer);
3536
- resolve10({ exitCode: -1, stderr: e.message });
3799
+ resolve8({ exitCode: -1, stderr: e.message });
3537
3800
  });
3538
3801
  child.on("close", (code) => {
3539
3802
  clearTimeout(timer);
3540
- resolve10({ exitCode: code ?? -1, stderr });
3803
+ resolve8({ exitCode: code ?? -1, stderr });
3541
3804
  });
3542
3805
  });
3543
3806
  if (vr.exitCode !== 0) {
@@ -3669,7 +3932,7 @@ ${newEntry}
3669
3932
  )
3670
3933
  };
3671
3934
  }
3672
- const vr = await new Promise((resolve10) => {
3935
+ const vr = await new Promise((resolve8) => {
3673
3936
  const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
3674
3937
  let stderr = "";
3675
3938
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -3677,15 +3940,15 @@ ${newEntry}
3677
3940
  });
3678
3941
  const timer = setTimeout(() => {
3679
3942
  child.kill("SIGKILL");
3680
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout]` });
3943
+ resolve8({ exitCode: -1, stderr: `${stderr}[timeout]` });
3681
3944
  }, 15e3);
3682
3945
  child.on("error", (e) => {
3683
3946
  clearTimeout(timer);
3684
- resolve10({ exitCode: -1, stderr: e.message });
3947
+ resolve8({ exitCode: -1, stderr: e.message });
3685
3948
  });
3686
3949
  child.on("close", (code) => {
3687
3950
  clearTimeout(timer);
3688
- resolve10({ exitCode: code ?? -1, stderr });
3951
+ resolve8({ exitCode: code ?? -1, stderr });
3689
3952
  });
3690
3953
  });
3691
3954
  if (vr.exitCode !== 0) {
@@ -4257,7 +4520,7 @@ function normalizeEol(buf, eol) {
4257
4520
  }
4258
4521
  function registerRollback(program2) {
4259
4522
  program2.command("rollback <timestamp>").description("Restore files from a backup snapshot (preserves original LF/CRLF)").action(async (timestamp) => {
4260
- const dir = resolve(process.cwd(), ".harnessed-backup", timestamp);
4523
+ const dir = join(getBackupRoot(), timestamp);
4261
4524
  const metaPath = join(dir, "metadata.json");
4262
4525
  let meta;
4263
4526
  try {
@@ -4298,12 +4561,96 @@ function registerRollback(program2) {
4298
4561
  console.log(`restored ${meta.files.length} file(s) from ${timestamp}`);
4299
4562
  });
4300
4563
  }
4564
+ init_checkAgentTeams();
4301
4565
  var PHASE_212 = /* @__PURE__ */ new Set([
4302
4566
  "cc-plugin-marketplace",
4303
4567
  "git-clone-with-setup",
4304
4568
  "npx-skill-installer",
4305
4569
  "mcp-http-add"
4306
4570
  ]);
4571
+ async function warnIfAgentTeamsMissing() {
4572
+ const r = await checkAgentTeams();
4573
+ if (r.status !== "missing") return;
4574
+ console.warn("\n\u26A0\uFE0F Agent Teams \u672A\u542F\u7528 \u2014 parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u4E0D\u53EF\u7528");
4575
+ console.warn(" \u4FEE\u590D: claude config set env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1");
4576
+ console.warn(
4577
+ " \u8BF4\u660E: harnessed v2.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u9700 CC 2.1.133+ Agent Teams enable (sister ~/.claude/rules/agent-teams.md)"
4578
+ );
4579
+ console.warn(
4580
+ " \u4E0D\u963B\u585E setup,\u540E\u7EED parallelism-gate workflow phase \u89E6\u53D1\u65F6\u81EA\u52A8\u964D\u7EA7 subagent fan-out\n"
4581
+ );
4582
+ }
4583
+ async function scanWorkflowsWithSkill(workflowsDir, entries) {
4584
+ const out = [];
4585
+ for (const entry of entries.sort()) {
4586
+ const src = join(workflowsDir, entry);
4587
+ try {
4588
+ const s = await stat(src);
4589
+ if (!s.isDirectory()) continue;
4590
+ await stat(join(src, "SKILL.md"));
4591
+ out.push(entry);
4592
+ } catch {
4593
+ }
4594
+ }
4595
+ return out;
4596
+ }
4597
+ async function runStepBInstall(manifestPaths) {
4598
+ const opts = {
4599
+ apply: true,
4600
+ dryRun: false,
4601
+ system: false,
4602
+ nonInteractive: true,
4603
+ fullDiff: false,
4604
+ color: "auto"
4605
+ };
4606
+ const start = Date.now();
4607
+ const settled = await Promise.allSettled(
4608
+ manifestPaths.map(async (path) => {
4609
+ let yamlSrc;
4610
+ try {
4611
+ yamlSrc = await readFile(path, "utf8");
4612
+ } catch (e) {
4613
+ return { status: "failed", name: path, reason: `read: ${e.message}` };
4614
+ }
4615
+ const v = validateManifestFile(yamlSrc, path);
4616
+ if (!v.ok) {
4617
+ return {
4618
+ status: "failed",
4619
+ name: path,
4620
+ reason: `validate: ${v.errors[0]?.message ?? "unknown"}`
4621
+ };
4622
+ }
4623
+ const name = v.manifest.metadata.name;
4624
+ const method = v.manifest.spec.install.method;
4625
+ if (PHASE_212.has(method)) return { status: "skipped", name };
4626
+ const r = await runInstall(v.manifest, opts);
4627
+ if ("aborted" in r) return { status: "skipped", name };
4628
+ if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled)
4629
+ return { status: "already-installed", name };
4630
+ if (r.ok) return { status: "installed", name };
4631
+ return { status: "failed", name, reason: r.error.message };
4632
+ })
4633
+ );
4634
+ const installed = [];
4635
+ const alreadyInstalled = [];
4636
+ const skipped = [];
4637
+ const failed = [];
4638
+ for (const s of settled) {
4639
+ const v = s.status === "fulfilled" ? s.value : {
4640
+ status: "failed",
4641
+ name: "?",
4642
+ reason: String(s.reason)
4643
+ };
4644
+ if (v.status === "installed") installed.push(v.name);
4645
+ else if (v.status === "already-installed") alreadyInstalled.push(v.name);
4646
+ else if (v.status === "skipped") skipped.push(v.name);
4647
+ else
4648
+ failed.push(`${v.name}: ${v.reason}`);
4649
+ }
4650
+ return { installed, alreadyInstalled, skipped, failed, elapsedMs: Date.now() - start };
4651
+ }
4652
+
4653
+ // src/cli/setup.ts
4307
4654
  async function listBaseManifests2(pkgRoot) {
4308
4655
  const out = [];
4309
4656
  for (const d of ["manifests/tools", "manifests/skill-packs"]) {
@@ -4323,6 +4670,7 @@ function registerSetup(program2) {
4323
4670
  const pkgRoot = getPackageRoot();
4324
4671
  const workflowsDir = resolve(pkgRoot, "workflows");
4325
4672
  const skillsBase = resolve(homedir(), ".claude", "skills");
4673
+ await warnIfAgentTeamsMissing();
4326
4674
  let entries;
4327
4675
  try {
4328
4676
  entries = await readdir(workflowsDir);
@@ -4330,17 +4678,7 @@ function registerSetup(program2) {
4330
4678
  console.error(`error: workflows directory not found at ${workflowsDir}`);
4331
4679
  process.exit(1);
4332
4680
  }
4333
- const toInstall = [];
4334
- for (const entry of entries.sort()) {
4335
- const src = join(workflowsDir, entry);
4336
- try {
4337
- const s = await stat(src);
4338
- if (!s.isDirectory()) continue;
4339
- await stat(join(src, "SKILL.md"));
4340
- toInstall.push(entry);
4341
- } catch {
4342
- }
4343
- }
4681
+ const toInstall = await scanWorkflowsWithSkill(workflowsDir, entries);
4344
4682
  if (toInstall.length === 0) {
4345
4683
  console.log("setup: no workflow directories with SKILL.md found \u2014 nothing to install");
4346
4684
  process.exit(2);
@@ -4372,88 +4710,35 @@ function registerSetup(program2) {
4372
4710
  `
4373
4711
  Step A complete: ${skillsInstalled} workflow skill(s) installed to ${skillsBase}`
4374
4712
  );
4375
- const opts = {
4376
- apply: true,
4377
- dryRun: false,
4378
- system: false,
4379
- nonInteractive: true,
4380
- fullDiff: false,
4381
- color: "auto"
4382
- };
4383
4713
  const manifestPaths = await listBaseManifests2(pkgRoot);
4384
- const stepBStart = Date.now();
4385
- const settled = await Promise.allSettled(
4386
- manifestPaths.map(async (path) => {
4387
- let yamlSrc;
4388
- try {
4389
- yamlSrc = await readFile(path, "utf8");
4390
- } catch (e) {
4391
- return {
4392
- status: "failed",
4393
- name: path,
4394
- reason: `read: ${e.message}`
4395
- };
4396
- }
4397
- const v = validateManifestFile(yamlSrc, path);
4398
- if (!v.ok) {
4399
- return {
4400
- status: "failed",
4401
- name: path,
4402
- reason: `validate: ${v.errors[0]?.message ?? "unknown"}`
4403
- };
4404
- }
4405
- const name = v.manifest.metadata.name;
4406
- const method = v.manifest.spec.install.method;
4407
- if (PHASE_212.has(method)) {
4408
- return { status: "skipped", name };
4409
- }
4410
- const r = await runInstall(v.manifest, opts);
4411
- if ("aborted" in r) return { status: "skipped", name };
4412
- if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled)
4413
- return { status: "already-installed", name };
4414
- if (r.ok) return { status: "installed", name };
4415
- return { status: "failed", name, reason: r.error.message };
4416
- })
4417
- );
4418
- const baseInstalled = [];
4419
- const baseAlreadyInstalled = [];
4420
- const baseSkipped = [];
4421
- const baseFailed = [];
4422
- for (const s of settled) {
4423
- const v = s.status === "fulfilled" ? s.value : {
4424
- status: "failed",
4425
- name: "?",
4426
- reason: String(s.reason)
4427
- };
4428
- if (v.status === "installed") baseInstalled.push(v.name);
4429
- else if (v.status === "already-installed") baseAlreadyInstalled.push(v.name);
4430
- else if (v.status === "skipped") baseSkipped.push(v.name);
4431
- else
4432
- baseFailed.push(
4433
- `${v.name}: ${v.reason}`
4434
- );
4435
- }
4436
- const stepBMs = ((Date.now() - stepBStart) / 1e3).toFixed(1);
4714
+ const b = await runStepBInstall(manifestPaths);
4715
+ const stepBMs = (b.elapsedMs / 1e3).toFixed(1);
4437
4716
  console.log(
4438
- `Step B complete: ${baseInstalled.length} manifest(s) installed / ${baseAlreadyInstalled.length} already-installed / ${baseSkipped.length} skipped / ${baseFailed.length} failed [parallel ${stepBMs}s]`
4717
+ `Step B complete: ${b.installed.length} manifest(s) installed / ${b.alreadyInstalled.length} already-installed / ${b.skipped.length} skipped / ${b.failed.length} failed [parallel ${stepBMs}s]`
4439
4718
  );
4440
- for (const n of baseInstalled) console.log(` [B] installed ${n}`);
4441
- for (const n of baseAlreadyInstalled)
4719
+ for (const n of b.installed) console.log(` [B] installed ${n}`);
4720
+ for (const n of b.alreadyInstalled)
4442
4721
  console.log(
4443
4722
  ` [B] already-installed ${n} \u2014 run \`/mcp\` in Claude Code to verify connection`
4444
4723
  );
4445
- for (const n of baseSkipped) console.log(` [B] skipped ${n}`);
4446
- for (const n of baseFailed) console.error(` [B] failed ${n}`);
4724
+ for (const n of b.skipped) console.log(` [B] skipped ${n}`);
4725
+ for (const n of b.failed) console.error(` [B] failed ${n}`);
4447
4726
  console.log(
4448
4727
  `
4449
- setup complete: ${skillsInstalled} workflow skill(s) + ${baseInstalled.length + baseAlreadyInstalled.length} base manifest(s) configured`
4728
+ setup complete: ${skillsInstalled} workflow skill(s) + ${b.installed.length + b.alreadyInstalled.length} base manifest(s) configured`
4450
4729
  );
4451
- if (baseAlreadyInstalled.length > 0 || baseInstalled.length > 0) {
4730
+ if (b.alreadyInstalled.length > 0 || b.installed.length > 0) {
4452
4731
  console.log(
4453
4732
  `
4454
4733
  MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's connection status. If a server shows disconnected, restart Claude Code or check the MCP command spec.`
4455
4734
  );
4456
4735
  }
4736
+ console.log(
4737
+ "\n\u2713 harnessed v2.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA bundled \u2014 4 workflows + 6 judgments + 37 capabilities ready"
4738
+ );
4739
+ console.log(
4740
+ " workflows in <packageRoot>/workflows/ (Pure bundled, NOT user-dir override per D-01)"
4741
+ );
4457
4742
  process.exit(0);
4458
4743
  });
4459
4744
  }
@@ -4678,7 +4963,7 @@ var uninstallNpmCli = async (ctx) => {
4678
4963
  const m = install.cmd.match(/npm\s+(?:install|i)\s+(?:-g\s+)?(\S+)/);
4679
4964
  const pkg = m?.[1] ?? ctx.manifest.metadata.upstream.source;
4680
4965
  const isWin = process.platform === "win32";
4681
- const result = await new Promise((resolve10) => {
4966
+ const result = await new Promise((resolve8) => {
4682
4967
  const child = isWin ? spawn("cmd.exe", ["/c", "npm", "uninstall", "-g", pkg], { windowsHide: true }) : spawn("npm", ["uninstall", "-g", pkg], { shell: false });
4683
4968
  let stderr = "";
4684
4969
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -4686,15 +4971,15 @@ var uninstallNpmCli = async (ctx) => {
4686
4971
  });
4687
4972
  const timer = setTimeout(() => {
4688
4973
  child.kill("SIGKILL");
4689
- resolve10({ exitCode: -1, stderr: `${stderr}[timeout]` });
4974
+ resolve8({ exitCode: -1, stderr: `${stderr}[timeout]` });
4690
4975
  }, 3e4);
4691
4976
  child.on("error", (e) => {
4692
4977
  clearTimeout(timer);
4693
- resolve10({ exitCode: -1, stderr: e.message });
4978
+ resolve8({ exitCode: -1, stderr: e.message });
4694
4979
  });
4695
4980
  child.on("close", (code) => {
4696
4981
  clearTimeout(timer);
4697
- resolve10({ exitCode: code ?? -1, stderr });
4982
+ resolve8({ exitCode: code ?? -1, stderr });
4698
4983
  });
4699
4984
  });
4700
4985
  if (result.exitCode !== 0) {