harnessed 3.4.3 → 3.5.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.
Files changed (75) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.mjs +1119 -745
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/workflows/auto/SKILL.md +10 -4
  8. package/workflows/capabilities.yaml +18 -19
  9. package/workflows/disciplines/karpathy.yaml +1 -1
  10. package/workflows/disciplines/language.yaml +1 -1
  11. package/workflows/disciplines/operational.yaml +2 -2
  12. package/workflows/disciplines/output-style.yaml +1 -1
  13. package/workflows/disciplines/priority.yaml +1 -1
  14. package/workflows/disciplines/protocols.yaml +1 -1
  15. package/workflows/discuss/auto/SKILL.md +10 -6
  16. package/workflows/discuss/auto/workflow.yaml +1 -2
  17. package/workflows/discuss/phase/SKILL.md +11 -30
  18. package/workflows/discuss/phase/workflow.yaml +1 -1
  19. package/workflows/discuss/strategic/SKILL.md +12 -33
  20. package/workflows/discuss/strategic/workflow.yaml +2 -3
  21. package/workflows/discuss/subtask/SKILL.md +11 -30
  22. package/workflows/discuss/subtask/workflow.yaml +1 -1
  23. package/workflows/execute-task/SKILL.md +7 -6
  24. package/workflows/execute-task/workflow.yaml +93 -0
  25. package/workflows/judgments/fallback.yaml +1 -1
  26. package/workflows/judgments/parallelism-gate.yaml +4 -3
  27. package/workflows/judgments/phase-gate.yaml +2 -2
  28. package/workflows/judgments/strategic-gate.yaml +2 -2
  29. package/workflows/judgments/subtask-gate.yaml +2 -2
  30. package/workflows/judgments/tdd-gate.yaml +2 -2
  31. package/workflows/judgments/web-design-routing.yaml +1 -1
  32. package/workflows/judgments/web-search-routing.yaml +1 -1
  33. package/workflows/judgments/web-testing-routing.yaml +1 -1
  34. package/workflows/plan/architecture/SKILL.md +13 -34
  35. package/workflows/plan/architecture/workflow.yaml +2 -2
  36. package/workflows/plan/auto/SKILL.md +10 -6
  37. package/workflows/plan/auto/workflow.yaml +1 -2
  38. package/workflows/plan/phase/SKILL.md +14 -35
  39. package/workflows/plan/phase/workflow.yaml +3 -3
  40. package/workflows/plan-feature/SKILL.md +4 -4
  41. package/workflows/research/SKILL.md +19 -6
  42. package/workflows/research/workflow.yaml +4 -4
  43. package/workflows/retro/SKILL.md +13 -32
  44. package/workflows/retro/workflow.yaml +1 -2
  45. package/workflows/role-prompts.yaml +4 -3
  46. package/workflows/task/auto/SKILL.md +11 -7
  47. package/workflows/task/auto/workflow.yaml +2 -3
  48. package/workflows/task/clarify/SKILL.md +11 -30
  49. package/workflows/task/code/SKILL.md +14 -35
  50. package/workflows/task/code/workflow.yaml +0 -1
  51. package/workflows/task/deliver/SKILL.md +15 -38
  52. package/workflows/task/deliver/workflow.yaml +7 -6
  53. package/workflows/task/test/SKILL.md +11 -32
  54. package/workflows/task/test/workflow.yaml +1 -2
  55. package/workflows/verify/auto/SKILL.md +14 -10
  56. package/workflows/verify/auto/workflow.yaml +4 -5
  57. package/workflows/verify/code-review/SKILL.md +14 -38
  58. package/workflows/verify/code-review/workflow.yaml +1 -3
  59. package/workflows/verify/design/SKILL.md +14 -38
  60. package/workflows/verify/design/workflow.yaml +4 -5
  61. package/workflows/verify/multispec/SKILL.md +17 -39
  62. package/workflows/verify/multispec/workflow.yaml +5 -8
  63. package/workflows/verify/paranoid/SKILL.md +13 -38
  64. package/workflows/verify/paranoid/workflow.yaml +1 -2
  65. package/workflows/verify/progress/SKILL.md +13 -32
  66. package/workflows/verify/progress/workflow.yaml +0 -1
  67. package/workflows/verify/qa/SKILL.md +15 -36
  68. package/workflows/verify/qa/workflow.yaml +1 -2
  69. package/workflows/verify/security/SKILL.md +12 -35
  70. package/workflows/verify/security/workflow.yaml +1 -2
  71. package/workflows/verify/simplify/SKILL.md +13 -34
  72. package/workflows/verify/simplify/workflow.yaml +1 -2
  73. package/workflows/verify-work/SKILL.md +5 -7
  74. package/workflows/verify-work/workflow.yaml +5 -7
  75. package/workflows/execute-task/phases.yaml +0 -73
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync, spawn } from 'child_process';
3
- import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync, appendFileSync } from 'fs';
4
- import { join, resolve, dirname, relative } from 'path';
3
+ import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync } from 'fs';
4
+ import { join, dirname, resolve, relative } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { Type } from '@sinclair/typebox';
7
7
  import { Value } from '@sinclair/typebox/value';
@@ -13,6 +13,7 @@ import { Ajv } from 'ajv';
13
13
  import * as ajvFormatsNs from 'ajv-formats';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { createHash } from 'crypto';
16
+ import { Parser } from 'expr-eval';
16
17
  import { query } from '@anthropic-ai/claude-agent-sdk';
17
18
  import * as p from '@clack/prompts';
18
19
  import { createPatch } from 'diff';
@@ -688,6 +689,11 @@ async function activate(phase, checkpointPath = null) {
688
689
  started_at: (/* @__PURE__ */ new Date()).toISOString()
689
690
  });
690
691
  }
692
+ async function pause() {
693
+ const s = await readCurrentWorkflow();
694
+ if (!s) return;
695
+ await writeCurrentWorkflow({ ...s, status: "paused", paused_at: (/* @__PURE__ */ new Date()).toISOString() });
696
+ }
691
697
  async function complete() {
692
698
  const s = await readCurrentWorkflow();
693
699
  if (!s) return;
@@ -847,7 +853,7 @@ var init_resume = __esm({
847
853
 
848
854
  // package.json
849
855
  var package_default = {
850
- version: "3.4.3"};
856
+ version: "3.5.0"};
851
857
 
852
858
  // src/manifest/errors.ts
853
859
  function instancePathToKeyPath(instancePath) {
@@ -1698,7 +1704,7 @@ function renderHumanTable(records) {
1698
1704
  }
1699
1705
  }
1700
1706
  function pipeToJq(filterExpr, lines) {
1701
- return new Promise((resolve12, reject) => {
1707
+ return new Promise((resolve14, reject) => {
1702
1708
  const child = spawn("jq", [filterExpr], {
1703
1709
  stdio: ["pipe", "inherit", "inherit"],
1704
1710
  windowsHide: true
@@ -1707,12 +1713,12 @@ function pipeToJq(filterExpr, lines) {
1707
1713
  const e = err2;
1708
1714
  if (e.code === "ENOENT") {
1709
1715
  console.error(t("audit_log.jq_missing"));
1710
- resolve12(1);
1716
+ resolve14(1);
1711
1717
  } else {
1712
1718
  reject(err2);
1713
1719
  }
1714
1720
  });
1715
- child.on("close", (code) => resolve12(code ?? 0));
1721
+ child.on("close", (code) => resolve14(code ?? 0));
1716
1722
  child.stdin.write(lines.join("\n"));
1717
1723
  child.stdin.end();
1718
1724
  });
@@ -2137,6 +2143,14 @@ var Discipline = Type.Object(
2137
2143
  );
2138
2144
 
2139
2145
  // src/workflow/disciplineLoader.ts
2146
+ var DEFAULT_APPLIED = [
2147
+ "karpathy",
2148
+ "output-style",
2149
+ "language",
2150
+ "operational",
2151
+ "priority",
2152
+ "protocols"
2153
+ ];
2140
2154
  var _cache = /* @__PURE__ */ new Map();
2141
2155
  async function loadDiscipline(basename2, packageRoot) {
2142
2156
  const cached = _cache.get(basename2);
@@ -2152,12 +2166,20 @@ async function loadDiscipline(basename2, packageRoot) {
2152
2166
  _cache.set(basename2, parsed);
2153
2167
  return parsed;
2154
2168
  }
2169
+ async function loadAllApplied(disciplines_applied, packageRoot) {
2170
+ const applied = disciplines_applied && disciplines_applied.length > 0 ? disciplines_applied : DEFAULT_APPLIED;
2171
+ const out = /* @__PURE__ */ new Map();
2172
+ for (const basename2 of applied) {
2173
+ out.set(basename2, await loadDiscipline(basename2, packageRoot));
2174
+ }
2175
+ return out;
2176
+ }
2155
2177
 
2156
2178
  // src/discipline/enforcement/before-commit.ts
2157
2179
  var TS_JS_RE = /\.(ts|tsx|js|mjs)$/;
2158
2180
  async function runBeforeCommitHook(ctx) {
2159
2181
  const d = await loadDiscipline("operational", ctx.packageRoot);
2160
- if (ctx.changedFiles.some((f) => TS_JS_RE.test(f))) {
2182
+ if (ctx.cmdType === "git-commit" && ctx.changedFiles.some((f) => TS_JS_RE.test(f))) {
2161
2183
  const rule = d.rules.find((r) => r.id === "biome-preempt");
2162
2184
  if (rule?.auto_fix_cmd) {
2163
2185
  console.warn("\u26A0\uFE0F biome preempt \u2014 running auto-fix before commit");
@@ -2168,341 +2190,10 @@ async function runBeforeCommitHook(ctx) {
2168
2190
  console.error("\u274C no-skip-hooks violated: --no-verify forbidden");
2169
2191
  process.exit(2);
2170
2192
  }
2171
- }
2172
-
2173
- // src/routing/completionSchema.ts
2174
- var COMPLETION_SCHEMA = {
2175
- type: "object",
2176
- properties: {
2177
- status: { type: "string", enum: ["COMPLETE", "PARTIAL", "BLOCKED"] },
2178
- phase: { type: "string", enum: ["01-clarify", "02-code", "03-test", "04-deliver"] },
2179
- summary: { type: "string" },
2180
- blockers: { type: "array", items: { type: "string" } }
2181
- },
2182
- required: ["status", "phase"]
2183
- };
2184
-
2185
- // src/routing/systemPrompt.ts
2186
- `## RULE: subagent COMPLETE marker
2187
- When you spawn a subagent and it returns a final message:
2188
- 1. **DO NOT summarize, paraphrase, or interpret the subagent's final message**
2189
- 2. **DO NOT skip or omit the COMPLETE marker**
2190
- 3. The subagent will emit \`<promise>COMPLETE</promise>\` (exact verbatim XML wrapper) when done
2191
- 4. You MUST verbatim grep \`<promise>COMPLETE</promise>\` from final message \u2192 if present, treat task as done
2192
- 5. If \`<promise>COMPLETE</promise>\` absent \u2192 re-spawn subagent (max 20 iterations); after max, throw VerbatimCompleteFailError
2193
-
2194
- ## skills fail-fast handling
2195
- - SkillNotInstalledError \u2192 print user-friendly fix command (e.g., "Run: harnessed install <name>")
2196
- - RestartRequiredError \u2192 print "\u8BF7 exit + restart Claude Code \u8BA9 plugin \u751F\u6548"
2197
-
2198
- ## \u515C\u5E95 max-iterations
2199
- - max-iterations 20 (external ralph-loop) \xD7 maxTurns 50 (internal AgentDefinition) = 1000 round-trips worst case
2200
-
2201
- ## Completion signal (dual-signal \u2014 emit BOTH)
2202
- If \`outputFormat: { type: 'json_schema' }\` is set on this query, emit a final-turn output conforming to the schema (status/phase/summary/blockers \u2014 keys: ${Object.keys(COMPLETION_SCHEMA.properties).join(", ")}).
2203
- AND emit \`<promise>COMPLETE</promise>\` (FALLBACK signal \u2014 required regardless of structured output presence, for inner-layer subagent completion detection per RESEARCH \xA7 1.3).
2204
- `;
2205
- var COMPLETE_INSTRUCTION = `## CRITICAL RULE: COMPLETE marker
2206
- When your task is done, emit the exact verbatim XML wrapper
2207
- \`<promise>COMPLETE</promise>\` on its own line and nothing else after it. Do
2208
- NOT emit any other variant \u2014 not "completed", not "DONE", not bare \`COMPLETE\`,
2209
- not "\u2705". The parent agent will verbatim grep \`<promise>COMPLETE</promise>\` \u2014
2210
- any deviation fails the round-trip and forces a re-spawn (max 20 iterations).
2211
- `;
2212
-
2213
- // src/routing/agentDefinition.ts
2214
- var SkillNotInstalledError = class extends Error {
2215
- constructor(skill) {
2216
- super(`Skill not installed: ${skill}. Run: harnessed install ${skill} --apply`);
2217
- this.skill = skill;
2218
- this.name = "SkillNotInstalledError";
2219
- }
2220
- skill;
2221
- };
2222
- var InvalidDecisionError = class extends Error {
2223
- constructor(message) {
2224
- super(message);
2225
- this.name = "InvalidDecisionError";
2226
- }
2227
- };
2228
- var MissingSkillsError = class extends Error {
2229
- constructor(missing) {
2230
- super(`Required skills missing: ${missing.join(", ")}`);
2231
- this.missing = missing;
2232
- this.name = "MissingSkillsError";
2233
- }
2234
- missing;
2235
- };
2236
- var RestartRequiredError = class extends Error {
2237
- constructor(hint) {
2238
- super(hint);
2239
- this.hint = hint;
2240
- this.name = "RestartRequiredError";
2241
- }
2242
- hint;
2243
- };
2244
- var KARPATHY_BASELINE = `## \u5FC3\u6CD5 (always-on baseline)
2245
- - Think Before Coding: read, plan, then write \u2014 never write before understanding.
2246
- - Simplicity First: minimum effective code; avoid unnecessary abstraction.
2247
- - Surgical Changes: small atomic edits; keep history clean.
2248
- - Goal-Driven Execution: each step earns its place by satisfying a goal.
2249
- `;
2250
- async function createAgent(task, decision, opts = {}) {
2251
- if (!decision.primary_expert) {
2252
- throw new InvalidDecisionError("decision.primary_expert is required (got null)");
2253
- }
2254
- const skillsRoot = opts.skillsRoot ?? join(homedir(), ".claude", "skills");
2255
- const requested = decision.required_skills ?? (decision.primary_expert ? [decision.primary_expert] : []);
2256
- const isInstalled2 = (name) => existsSync(join(skillsRoot, name, "SKILL.md"));
2257
- const missing = requested.filter((s) => !isInstalled2(s));
2258
- if (missing.length === requested.length && requested.length > 0) {
2259
- throw new MissingSkillsError(missing);
2260
- }
2261
- if (missing.length > 0) {
2262
- throw new SkillNotInstalledError(missing[0]);
2263
- }
2264
- const promptBody = `${KARPATHY_BASELINE}
2265
- ## \u4EFB\u52A1
2266
- ${task.task}
2267
-
2268
- ${COMPLETE_INSTRUCTION}`;
2269
- return {
2270
- description: `Routing-engine spawned subagent (${decision.category}) \u2014 primary: ${decision.primary_expert}`,
2271
- prompt: promptBody,
2272
- tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"],
2273
- disallowedTools: decision.forbidden_skills?.map((s) => `Skill(${s})`),
2274
- model: opts.modelOverride ?? process.env.HARNESSED_AGENT_MODEL ?? "inherit",
2275
- skills: requested,
2276
- mcpServers: void 0,
2277
- memory: "project",
2278
- maxTurns: opts.maxTurnsOverride ?? 50,
2279
- background: false,
2280
- effort: opts.effortOverride ?? "medium",
2281
- permissionMode: opts.permissionModeOverride ?? "default",
2282
- // v1.1 errata (ADR 0009 § Decision Errata 1) — `initialPrompt` carries the
2283
- // task body for the plugin-main-thread upgrade path;
2284
- // `criticalSystemReminder_EXPERIMENTAL` re-asserts the verbatim
2285
- // <promise>COMPLETE</promise> contract at the system-prompt layer.
2286
- initialPrompt: task.task,
2287
- criticalSystemReminder_EXPERIMENTAL: COMPLETE_INSTRUCTION
2288
- };
2289
- }
2290
-
2291
- // src/routing/dag.ts
2292
- function resolveDag(nodes) {
2293
- const ids = new Set(nodes.map((n) => n.id));
2294
- const adj = /* @__PURE__ */ new Map();
2295
- const indegree = /* @__PURE__ */ new Map();
2296
- for (const id of ids) {
2297
- adj.set(id, []);
2298
- indegree.set(id, 0);
2299
- }
2300
- for (const node of nodes) {
2301
- for (const dep of node.deps) {
2302
- if (!ids.has(dep)) continue;
2303
- adj.get(dep)?.push(node.id);
2304
- indegree.set(node.id, (indegree.get(node.id) ?? 0) + 1);
2305
- }
2306
- }
2307
- const queue = [];
2308
- for (const [id, deg] of indegree) {
2309
- if (deg === 0) queue.push(id);
2310
- }
2311
- queue.sort();
2312
- const order = [];
2313
- while (queue.length > 0) {
2314
- const id = queue.shift();
2315
- order.push(id);
2316
- let exposed = false;
2317
- for (const dependent of adj.get(id) ?? []) {
2318
- const next = (indegree.get(dependent) ?? 0) - 1;
2319
- indegree.set(dependent, next);
2320
- if (next === 0) {
2321
- queue.push(dependent);
2322
- exposed = true;
2323
- }
2324
- }
2325
- if (exposed) queue.sort();
2326
- }
2327
- if (order.length !== ids.size) {
2328
- const cycle = [];
2329
- for (const [id, deg] of indegree) {
2330
- if (deg > 0) cycle.push(id);
2331
- }
2332
- cycle.sort();
2333
- return { ok: false, cycle };
2334
- }
2335
- return { ok: true, order };
2336
- }
2337
- var Domain = Type.Union([
2338
- Type.Literal("meta"),
2339
- Type.Literal("engineering"),
2340
- Type.Literal("design"),
2341
- Type.Literal("content"),
2342
- Type.Literal("testing"),
2343
- Type.Literal("search")
2344
- ]);
2345
- var HitPolicy = Type.Union([Type.Literal("P"), Type.Literal("F"), Type.Literal("U")]);
2346
- var Str1 = Type.String({ minLength: 1 });
2347
- var ObjStrict = (p4) => Type.Object(p4, { additionalProperties: false });
2348
- var RuleSchema = ObjStrict({
2349
- id: Str1,
2350
- priority: Type.Integer({ minimum: 0 }),
2351
- domain: Domain,
2352
- when: Type.Record(Type.String(), Type.Unknown()),
2353
- decision: Type.Record(Type.String(), Type.Unknown())
2354
- });
2355
- var PhaseEntrySchema = ObjStrict({
2356
- skills: Type.Array(Str1, { minItems: 1 }),
2357
- triggers: Type.Array(Str1, { minItems: 1 })
2358
- });
2359
- var MattpocockPhasesSchema = ObjStrict({
2360
- discuss: PhaseEntrySchema,
2361
- plan: PhaseEntrySchema,
2362
- execute: PhaseEntrySchema,
2363
- verify: PhaseEntrySchema
2364
- });
2365
- var DecisionRulesFileSchema = ObjStrict({
2366
- // v2 additive (D1.5-10): accept v1 OR v2 — schema stays backward compatible.
2367
- version: Type.Union([Type.Literal(1), Type.Literal(2)]),
2368
- hit_policy: HitPolicy,
2369
- rules: Type.Array(RuleSchema, { minItems: 1 }),
2370
- // v2 — optional mattpocock_phases map; absent in v1 files.
2371
- mattpocock_phases: Type.Optional(MattpocockPhasesSchema),
2372
- fallback_supervisor: Type.Optional(ObjStrict({ trigger: Str1, llm: Str1 })),
2373
- deprecated: Type.Optional(Type.Array(ObjStrict({ id: Str1, reason: Str1, fallback: Str1 })))
2374
- });
2375
- var ajv2 = new Ajv({ strict: true, allErrors: true, allowUnionTypes: false });
2376
- var _compiled2 = null;
2377
- function getValidator2() {
2378
- if (!_compiled2) _compiled2 = ajv2.compile(DecisionRulesFileSchema);
2379
- return _compiled2;
2380
- }
2381
- function scanShellInjection(node, path = "") {
2382
- if (typeof node === "string") {
2383
- const hit = checkCmdString(node);
2384
- return hit ? `${path || "<root>"}: ${hit.label} (${hit.hint})` : null;
2385
- }
2386
- if (Array.isArray(node)) {
2387
- for (let i = 0; i < node.length; i++) {
2388
- const v = scanShellInjection(node[i], `${path}[${i}]`);
2389
- if (v) return v;
2390
- }
2391
- return null;
2392
- }
2393
- if (node && typeof node === "object") {
2394
- for (const [k, v] of Object.entries(node)) {
2395
- const hit = scanShellInjection(v, path ? `${path}.${k}` : k);
2396
- if (hit) return hit;
2397
- }
2398
- }
2399
- return null;
2400
- }
2401
- function loadDecisionRules(yamlPath) {
2402
- const doc = parseDocument(readFileSync(yamlPath, "utf8"));
2403
- if (doc.errors.length > 0) {
2404
- throw new Error(`decision_rules yaml parse error: ${doc.errors[0]?.message}`);
2405
- }
2406
- const data = doc.toJS();
2407
- const validate = getValidator2();
2408
- if (!validate(data)) {
2409
- const e = validate.errors?.[0];
2410
- throw new Error(`decision_rules schema invalid: ${e?.instancePath || "/"} ${e?.message}`);
2411
- }
2412
- const inj = scanShellInjection(data);
2413
- if (inj) throw new Error(`decision_rules security violation: ${inj}`);
2414
- return data;
2415
- }
2416
- function arbitrate(rules, task) {
2417
- const matches = rules.filter((r) => matchesWhen(r.when, task));
2418
- const [top, second] = [...matches].sort((a, b) => b.priority - a.priority);
2419
- if (!top) return null;
2420
- if (second && second.priority === top.priority) return null;
2421
- return top;
2422
- }
2423
- var ARRAY_TRIGGER_FIELDS = /* @__PURE__ */ new Set(["keywords", "signals", "override_keywords"]);
2424
- function taskHas(task, needle, extraKey) {
2425
- const n = needle.toLowerCase();
2426
- const prompt = typeof task.prompt === "string" ? task.prompt.toLowerCase() : "";
2427
- if (prompt.includes(n)) return true;
2428
- const sources = [];
2429
- if (Array.isArray(task.signals)) sources.push(...task.signals);
2430
- if (extraKey && extraKey !== "signals" && Array.isArray(task[extraKey])) {
2431
- sources.push(...task[extraKey]);
2432
- }
2433
- return sources.some((s) => typeof s === "string" && s.toLowerCase().includes(n));
2434
- }
2435
- function matchesWhen(when, task) {
2436
- for (const [k, v] of Object.entries(when)) {
2437
- if (ARRAY_TRIGGER_FIELDS.has(k) && Array.isArray(v)) {
2438
- if (!v.some((kw) => typeof kw === "string" && taskHas(task, kw, k))) return false;
2439
- continue;
2440
- }
2441
- if (Array.isArray(v)) {
2442
- if (!v.includes(task[k])) return false;
2443
- continue;
2444
- }
2445
- if (task[k] !== v) return false;
2193
+ if (ctx.cmdType === "git-push" && !ctx.hasUserApproval) {
2194
+ console.error("\u274C no-push-without-approval: user explicit approval required");
2195
+ process.exit(2);
2446
2196
  }
2447
- return true;
2448
- }
2449
-
2450
- // src/audit/log.ts
2451
- init_harnessedRoot();
2452
- function auditPath2() {
2453
- return harnessedFile("audit.log");
2454
- }
2455
- Type.Object(
2456
- {
2457
- ts: Type.String(),
2458
- phase: Type.String(),
2459
- task_excerpt: Type.String(),
2460
- task_sha1: Type.String(),
2461
- matched_rule_id: Type.Union([Type.String(), Type.Null()]),
2462
- primary_expert: Type.Union([Type.String(), Type.Null()]),
2463
- secondary_expert: Type.Union([Type.String(), Type.Null()]),
2464
- category: Type.String(),
2465
- route_layer: Type.String(),
2466
- outcome: Type.String(),
2467
- session_id: Type.Union([Type.String(), Type.Null()]),
2468
- iter_count: Type.Union([Type.Number(), Type.Null()])
2469
- },
2470
- { additionalProperties: false }
2471
- );
2472
- function buildAuditRecord(task, decision, matched, ctx) {
2473
- return {
2474
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2475
- phase: task.phaseId ?? "unknown",
2476
- task_excerpt: task.task.slice(0, 200),
2477
- task_sha1: createHash("sha1").update(task.task).digest("hex"),
2478
- matched_rule_id: matched?.id ?? null,
2479
- primary_expert: decision.primary_expert ?? null,
2480
- secondary_expert: decision.secondary_expert ?? null,
2481
- category: decision.category,
2482
- route_layer: ctx.routeLayer,
2483
- outcome: ctx.outcome,
2484
- session_id: ctx.sessionId ?? null,
2485
- iter_count: ctx.iterCount
2486
- };
2487
- }
2488
- function emitAuditRecord(record) {
2489
- const path = auditPath2();
2490
- mkdirSync(dirname(path), { recursive: true });
2491
- appendFileSync(path, `${JSON.stringify(record)}
2492
- `);
2493
- }
2494
-
2495
- // src/audit/hook.ts
2496
- function emitAudit(task, decision, matched, outcome, sessionId) {
2497
- emitAuditRecord(
2498
- buildAuditRecord(task, decision, matched, {
2499
- outcome,
2500
- routeLayer: matched ? "L1-keyword" : "L3-fallback",
2501
- sessionId,
2502
- iterCount: null
2503
- // Phase 4.3 YAGNI (ralphLoopWrap returns string only; defer v0.5+ per RESEARCH § 7 Q2)
2504
- })
2505
- );
2506
2197
  }
2507
2198
 
2508
2199
  // src/checkpoint/engineHook.ts
@@ -2536,7 +2227,363 @@ async function completePhase(ctx) {
2536
2227
  await complete();
2537
2228
  }
2538
2229
 
2539
- // src/routing/lib/fallbackHandlers.ts
2230
+ // src/workflow/run.ts
2231
+ init_state();
2232
+ async function loadRolePrompts(workflowsDir) {
2233
+ const path = join(workflowsDir, "role-prompts.yaml");
2234
+ let raw;
2235
+ try {
2236
+ raw = await readFile(path, "utf8");
2237
+ } catch {
2238
+ return {};
2239
+ }
2240
+ const doc = parse(raw);
2241
+ return doc?.prompts ?? {};
2242
+ }
2243
+ function generateCommandFile(name, prompt, _capabilities, _installedPlugins, _installedUserSkills) {
2244
+ const isMaster = prompt.is_master === true;
2245
+ const argHint = isMaster ? "[task description]" : "[requirement text or omit]";
2246
+ const stagedNote = name === "auto" ? "\n- For stage-by-stage review, append `--staged` (pauses between stages for user review)." : "";
2247
+ const body = [
2248
+ `# /${name}`,
2249
+ ``,
2250
+ prompt.description,
2251
+ ``,
2252
+ `## How to invoke`,
2253
+ ``,
2254
+ `Use the Bash tool to run:`,
2255
+ ``,
2256
+ "```bash",
2257
+ `echo "$ARGUMENTS" | harnessed run ${name} --task-stdin`,
2258
+ "```",
2259
+ ``,
2260
+ `If \`$ARGUMENTS\` is empty (slash command invoked with no args), run \`harnessed run ${name}\` (no stdin pipe).`,
2261
+ ``,
2262
+ `After completion, the Bash output prints a \`Next:\` hint on stderr suggesting the next stage. Decide whether to invoke based on conversation context \u2014 the hint is informational, not prescriptive.`,
2263
+ ``,
2264
+ `## Notes`,
2265
+ ``,
2266
+ `- This file is generated by \`harnessed setup\` v3.4.4+. Re-run \`harnessed setup\` after a harnessed upgrade to refresh.`,
2267
+ `- The sister \`~/.claude/skills/${name}/SKILL.md\` is the Skill-tool entry point (Claude loads it when triggers match \`trigger_phrases:\`). Both files invoke the same \`harnessed run ${name}\` Bash command.${stagedNote}`,
2268
+ `- Workflow runtime: \`src/workflow/run.ts\` walks \`workflows/${nameToYamlHintPath(name)}\` with disciplines + judgments + master orchestration applied per the yaml \`delegates_to[]\` + \`gate\` clauses.`,
2269
+ ``,
2270
+ `<!-- harnessed-generated:v3.4.4 -->`,
2271
+ ``
2272
+ ].join("\n");
2273
+ const warnings = [];
2274
+ const frontmatter = [
2275
+ "---",
2276
+ `description: ${JSON.stringify(prompt.description)}`,
2277
+ `argument-hint: ${JSON.stringify(argHint)}`,
2278
+ "---",
2279
+ ""
2280
+ ].join("\n");
2281
+ return { content: frontmatter + body, warnings };
2282
+ }
2283
+ function nameToYamlHintPath(name) {
2284
+ if (["auto", "research", "retro"].includes(name)) return `${name}/workflow.yaml`;
2285
+ if (["discuss", "plan", "task", "verify"].includes(name)) return `${name}/auto/workflow.yaml`;
2286
+ const dashIdx = name.indexOf("-");
2287
+ if (dashIdx > 0) {
2288
+ return `${name.slice(0, dashIdx)}/${name.slice(dashIdx + 1)}/workflow.yaml`;
2289
+ }
2290
+ return `${name}/workflow.yaml`;
2291
+ }
2292
+ var HARNESSED_MARKER_RX = /<!--\s*harnessed-generated:v3\.4\.\d+\s*-->/;
2293
+ var V3_4_3_SIGNATURE_SUB_RX = /\*\*Preferred path\*\*[\s\S]*use the SlashCommand tool[\s\S]*\*\*Fallback path\*\*[\s\S]*use the Task tool to spawn/;
2294
+ var V3_4_3_SIGNATURE_MASTER_RX = /\*\*Preferred path\*\*[\s\S]*dispatch to the per-sub-workflow/;
2295
+ function shouldOverwriteFile(content) {
2296
+ return HARNESSED_MARKER_RX.test(content) || V3_4_3_SIGNATURE_SUB_RX.test(content) || V3_4_3_SIGNATURE_MASTER_RX.test(content);
2297
+ }
2298
+ async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists2 = existsSync, readFileSync9 = (p4) => readFileSync(p4, "utf8")) {
2299
+ const results = [];
2300
+ const aggregatedWarnings = /* @__PURE__ */ new Set();
2301
+ for (const name of slashNames) {
2302
+ const path = join(commandsDir, `${name}.md`);
2303
+ const prompt = rolePrompts[name];
2304
+ if (!prompt) {
2305
+ results.push({
2306
+ name,
2307
+ path,
2308
+ written: false,
2309
+ warning: `no role-prompts.yaml entry for '${name}' \u2014 skipping commands/${name}.md generation`
2310
+ });
2311
+ aggregatedWarnings.add(`role-prompts.yaml missing entry for '${name}'`);
2312
+ continue;
2313
+ }
2314
+ if (fileExists2(path)) {
2315
+ let existing = "";
2316
+ try {
2317
+ existing = readFileSync9(path);
2318
+ } catch {
2319
+ existing = "";
2320
+ }
2321
+ if (!shouldOverwriteFile(existing)) {
2322
+ results.push({
2323
+ name,
2324
+ path,
2325
+ written: false,
2326
+ warning: `commands/${name}.md is user-authored (no harnessed marker) \u2014 leaving unchanged. Delete the file to force regenerate.`
2327
+ });
2328
+ continue;
2329
+ }
2330
+ }
2331
+ const { content, warnings } = generateCommandFile(
2332
+ name,
2333
+ prompt);
2334
+ try {
2335
+ await writer(path, content);
2336
+ results.push({ name, path, written: true });
2337
+ } catch (e) {
2338
+ results.push({
2339
+ name,
2340
+ path,
2341
+ written: false,
2342
+ warning: `write failed for commands/${name}.md: ${e.message}`
2343
+ });
2344
+ }
2345
+ for (const w of warnings) aggregatedWarnings.add(w);
2346
+ }
2347
+ return { results, warnings: [...aggregatedWarnings] };
2348
+ }
2349
+
2350
+ // src/discipline/enforcement/after-output.ts
2351
+ var EM_DASH_RE = /——|—/;
2352
+ var EMOJI_RE = new RegExp("\\p{Emoji_Presentation}", "u");
2353
+ var SYCOPHANTIC_RE = /(好问题|太棒了|完美|希望对你有帮助|还需要别的吗|要不要我帮你)/;
2354
+ var END_RECAP_RE = /## 总结|## Summary|综上所述|In summary/;
2355
+ async function runAfterOutputHook(ctx) {
2356
+ const warns = [];
2357
+ await Promise.all([
2358
+ loadDiscipline("output-style", ctx.packageRoot),
2359
+ loadDiscipline("language", ctx.packageRoot)
2360
+ ]);
2361
+ const firstSentence = ctx.responseText.split(/[。.!?\n]/)[0] ?? "";
2362
+ if (firstSentence.length > 100) warns.push("BLUF missing: first sentence > 100 char");
2363
+ if (EM_DASH_RE.test(ctx.responseText)) warns.push("em-dash detected (auto-fix recommended)");
2364
+ if (EMOJI_RE.test(ctx.responseText)) {
2365
+ warns.push("emoji used without explicit user request");
2366
+ }
2367
+ if (SYCOPHANTIC_RE.test(ctx.responseText)) warns.push("sycophantic phrase detected");
2368
+ const lastChunk = ctx.responseText.slice(-200);
2369
+ if (END_RECAP_RE.test(lastChunk)) warns.push("redundant end recap detected");
2370
+ for (const w of warns) console.warn(`\u26A0\uFE0F output-style: ${w}`);
2371
+ return warns;
2372
+ }
2373
+
2374
+ // src/discipline/enforcement/before-phase-execute.ts
2375
+ async function loadDisciplinesForPhase(disciplines_applied, packageRoot) {
2376
+ return loadAllApplied(disciplines_applied, packageRoot);
2377
+ }
2378
+
2379
+ // src/discipline/enforcement/before-spawn.ts
2380
+ async function arbitrateBeforeSpawn(fired, packageRoot) {
2381
+ if (fired.length <= 1) return fired;
2382
+ const d = await loadDiscipline("priority", packageRoot);
2383
+ const hierarchy = d.priority_hierarchy ?? [];
2384
+ const rank = (tier) => {
2385
+ const i = hierarchy.indexOf(tier);
2386
+ return i === -1 ? Number.MAX_SAFE_INTEGER : i;
2387
+ };
2388
+ return [...fired].sort((a, b) => rank(a.tier) - rank(b.tier));
2389
+ }
2390
+
2391
+ // src/workflow/governance.ts
2392
+ init_harnessedRoot();
2393
+ init_schemaVersion();
2394
+
2395
+ // src/workflow/schema/governance.ts
2396
+ init_schemaVersion();
2397
+ var GovernanceStatus = Type.Union([Type.Literal("active"), Type.Literal("vetoed")]);
2398
+ var GovernanceV1 = Type.Object(
2399
+ {
2400
+ schemaVersion: Type.Literal(SCHEMA_VERSIONS.governance),
2401
+ // 'harnessed.governance.v1'
2402
+ status: GovernanceStatus,
2403
+ reason: Type.Optional(Type.String({ maxLength: 500 })),
2404
+ // DOS cap per RESEARCH § 11.4
2405
+ // ISO-8601 date-time regex (Phase 3.2 W2 Rule 1 fix — TypeBox `format: 'date-time'`
2406
+ // requires FormatRegistry.Set which is not registered project-wide; using
2407
+ // `pattern` is zero-config equivalent. Sister checkpoint.v1 uses ISO pattern too.).
2408
+ vetoed_at: Type.Optional(
2409
+ Type.String({
2410
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
2411
+ })
2412
+ ),
2413
+ vetoed_by: Type.Optional(Type.String({ maxLength: 100 }))
2414
+ // e.g. 'CEO' (gstack role)
2415
+ },
2416
+ { additionalProperties: false }
2417
+ );
2418
+
2419
+ // src/workflow/governance.ts
2420
+ function govPath() {
2421
+ return harnessedFile("governance.json");
2422
+ }
2423
+ async function readGovernance() {
2424
+ let raw;
2425
+ try {
2426
+ raw = await readFile(govPath(), "utf8");
2427
+ } catch {
2428
+ return null;
2429
+ }
2430
+ let parsed;
2431
+ try {
2432
+ parsed = JSON.parse(raw);
2433
+ } catch {
2434
+ return null;
2435
+ }
2436
+ const v = parsed.schemaVersion ?? "";
2437
+ return branchOnSchemaVersion(v, {
2438
+ v1: () => Value.Check(GovernanceV1, parsed) ? parsed : null,
2439
+ unknown: () => null
2440
+ });
2441
+ }
2442
+ async function isVetoed() {
2443
+ return (await readGovernance())?.status === "vetoed";
2444
+ }
2445
+ var PARSER_OPTIONS = {
2446
+ operators: {
2447
+ add: false,
2448
+ subtract: false,
2449
+ multiply: false,
2450
+ divide: false,
2451
+ logical: true,
2452
+ comparison: true,
2453
+ in: true,
2454
+ assignment: false
2455
+ }
2456
+ };
2457
+ var _parserSingleton = new Parser(PARSER_OPTIONS);
2458
+ var GateEvalError = class extends Error {
2459
+ constructor(message, expression) {
2460
+ super(message);
2461
+ this.expression = expression;
2462
+ this.name = "GateEvalError";
2463
+ }
2464
+ expression;
2465
+ };
2466
+ function evalGate(expression, context) {
2467
+ try {
2468
+ const parsed = _parserSingleton.parse(expression);
2469
+ const result = parsed.evaluate(context);
2470
+ if (typeof result !== "boolean") {
2471
+ throw new GateEvalError(
2472
+ `Expression must evaluate to boolean, got ${typeof result}`,
2473
+ expression
2474
+ );
2475
+ }
2476
+ return result;
2477
+ } catch (err2) {
2478
+ if (err2 instanceof GateEvalError) throw err2;
2479
+ throw new GateEvalError(`Gate eval failed: ${err2.message}`, expression);
2480
+ }
2481
+ }
2482
+
2483
+ // src/workflow/schema/judgment.ts
2484
+ init_schemaVersion();
2485
+ var TriggerInvocation = Type.Object(
2486
+ {
2487
+ capability: Type.String()
2488
+ },
2489
+ { additionalProperties: false }
2490
+ );
2491
+ var RequiresCapabilities = Type.Object(
2492
+ {
2493
+ capabilities: Type.Array(Type.String())
2494
+ },
2495
+ { additionalProperties: false }
2496
+ );
2497
+ var JudgmentTrigger = Type.Object(
2498
+ {
2499
+ description: Type.Optional(Type.String()),
2500
+ fires_when: Type.Optional(Type.String()),
2501
+ skips_when: Type.Optional(Type.String()),
2502
+ invokes: Type.Optional(Type.Array(TriggerInvocation)),
2503
+ requires: Type.Optional(RequiresCapabilities),
2504
+ wraps: Type.Optional(Type.Array(Type.String()))
2505
+ },
2506
+ { additionalProperties: false }
2507
+ );
2508
+ var FallbackRule = Type.Object(
2509
+ {
2510
+ description: Type.Optional(Type.String()),
2511
+ fallback_action: Type.Optional(Type.String()),
2512
+ message_template: Type.Optional(Type.String()),
2513
+ override_signal: Type.Optional(Type.Array(Type.String())),
2514
+ chain_isolation: Type.Optional(Type.Boolean())
2515
+ },
2516
+ { additionalProperties: false }
2517
+ );
2518
+ var JudgmentTriggersFile = Type.Object(
2519
+ {
2520
+ schema_version: Type.Literal(SCHEMA_VERSIONS.judgment),
2521
+ triggers: Type.Record(Type.String(), JudgmentTrigger)
2522
+ },
2523
+ { additionalProperties: false }
2524
+ );
2525
+ var JudgmentRulesFile = Type.Object(
2526
+ {
2527
+ schema_version: Type.Literal(SCHEMA_VERSIONS.judgment),
2528
+ rules: Type.Record(Type.String(), FallbackRule)
2529
+ },
2530
+ { additionalProperties: false }
2531
+ );
2532
+ Type.Union([JudgmentTriggersFile, JudgmentRulesFile]);
2533
+
2534
+ // src/workflow/judgmentResolver.ts
2535
+ var TriggerNotFoundError = class extends Error {
2536
+ constructor(trigger, fileName) {
2537
+ super(`Trigger '${trigger}' not found in judgments/${fileName}.yaml`);
2538
+ this.trigger = trigger;
2539
+ this.fileName = fileName;
2540
+ this.name = "TriggerNotFoundError";
2541
+ }
2542
+ trigger;
2543
+ fileName;
2544
+ };
2545
+ var _fileCache = /* @__PURE__ */ new Map();
2546
+ async function resolveJudgmentGate(gateRef, context, packageRoot) {
2547
+ const parts = gateRef.split(".");
2548
+ if (parts.length !== 4 || parts[0] !== "judgments") {
2549
+ throw new Error(`Invalid gate ref: ${gateRef}`);
2550
+ }
2551
+ const [, fileName, triggerName, fieldName] = parts;
2552
+ let parsed = _fileCache.get(fileName);
2553
+ if (!parsed) {
2554
+ const yamlPath = resolve(packageRoot, "workflows", "judgments", `${fileName}.yaml`);
2555
+ const raw = await readFile(yamlPath, "utf8");
2556
+ const parsedRaw = parse(raw);
2557
+ const schema = fileName === "fallback" ? JudgmentRulesFile : JudgmentTriggersFile;
2558
+ if (!Value.Check(schema, parsedRaw)) {
2559
+ const errors = [...Value.Errors(schema, parsedRaw)].slice(0, 3).map((e) => `${e.path} ${e.message}`).join("; ");
2560
+ throw new Error(`Invalid judgment file ${fileName}.yaml: ${errors}`);
2561
+ }
2562
+ parsed = parsedRaw;
2563
+ _fileCache.set(fileName, parsed);
2564
+ }
2565
+ const entries = "triggers" in parsed ? parsed.triggers : parsed.rules;
2566
+ const trigger = entries[triggerName];
2567
+ if (!trigger) {
2568
+ throw new TriggerNotFoundError(triggerName, fileName);
2569
+ }
2570
+ const expr = fieldName === "fires" ? trigger.fires_when : fieldName === "skips" ? trigger.skips_when : void 0;
2571
+ if (!expr) {
2572
+ throw new Error(
2573
+ `Field '${fieldName}' has no expression in trigger '${triggerName}' of ${fileName}.yaml`
2574
+ );
2575
+ }
2576
+ return evalGate(expr, context);
2577
+ }
2578
+
2579
+ // src/workflow/lib/promiseExtract.ts
2580
+ var PROMISE_PATTERN = /<promise>([^<]+)<\/promise>/;
2581
+ function extractPromise(text) {
2582
+ const match = text.match(PROMISE_PATTERN);
2583
+ return match ? match[1] : null;
2584
+ }
2585
+
2586
+ // src/workflow/lib/fallbackHandlers.ts
2540
2587
  function handleMaxIterationsExceeded(err2, fallback, ctx) {
2541
2588
  const yamlShort = fallback.message.replace(
2542
2589
  /\{\{\s*args\.max_iterations\s*\}\}/g,
@@ -2562,33 +2609,8 @@ ${yamlShort}`;
2562
2609
  process.exit(fallback.exit_code);
2563
2610
  throw new Error("unreachable");
2564
2611
  }
2565
- function handleVerbatimCompleteFail(err2, fallback, ctx) {
2566
- const truncated = err2.lastMessage.slice(0, 500);
2567
- const uxText = `\u274C ralph-loop verbatim COMPLETE signal missing (F33 P1).
2568
- Sub-task: ${ctx.subtaskSummary}
2569
- Workflow: ${ctx.workflowName} / phase ${ctx.phaseId}
2570
- Last subagent output (truncated): ${truncated}
2571
- The subagent's final message lacked verbatim "<promise>COMPLETE</promise>" tag.
2572
- This indicates one of:
2573
- 1. Subagent skipped the completion-promise contract (review system prompt)
2574
- 2. Output format misconfigured (check outputFormat schema)
2575
- Manual options:
2576
- A) Re-run with explicit COMPLETE instruction in subagent prompt
2577
- B) Abort cleanly: exit ${fallback.exit_code}
2578
- Exit code: ${fallback.exit_code}`;
2579
- console.error(uxText);
2580
- process.exit(fallback.exit_code);
2581
- throw new Error("unreachable");
2582
- }
2583
-
2584
- // src/routing/lib/promiseExtract.ts
2585
- var PROMISE_PATTERN = /<promise>([^<]+)<\/promise>/;
2586
- function extractPromise(text) {
2587
- const match2 = text.match(PROMISE_PATTERN);
2588
- return match2 ? match2[1] : null;
2589
- }
2590
2612
 
2591
- // src/routing/lib/ralphLoop.ts
2613
+ // src/workflow/lib/ralphLoop.ts
2592
2614
  function isComplete(output) {
2593
2615
  try {
2594
2616
  const env = JSON.parse(output);
@@ -2607,14 +2629,6 @@ var MaxIterationsExceededError = class extends Error {
2607
2629
  }
2608
2630
  iterations;
2609
2631
  };
2610
- var VerbatimCompleteFailError = class extends Error {
2611
- constructor(lastMessage) {
2612
- super("subagent final message lacked verbatim <promise>COMPLETE</promise> (F33 P1 mitigation)");
2613
- this.lastMessage = lastMessage;
2614
- this.name = "VerbatimCompleteFailError";
2615
- }
2616
- lastMessage;
2617
- };
2618
2632
  async function ralphLoopWrap(spawn6, maxIter) {
2619
2633
  let last = "";
2620
2634
  let sessionId;
@@ -2627,7 +2641,26 @@ async function ralphLoopWrap(spawn6, maxIter) {
2627
2641
  throw new MaxIterationsExceededError(maxIter);
2628
2642
  }
2629
2643
 
2630
- // src/routing/lib/sdkReconcile.ts
2644
+ // src/workflow/lib/completionSchema.ts
2645
+ var COMPLETION_SCHEMA = {
2646
+ type: "object",
2647
+ properties: {
2648
+ status: { type: "string", enum: ["COMPLETE", "PARTIAL", "BLOCKED"] },
2649
+ phase: { type: "string", enum: ["01-clarify", "02-code", "03-test", "04-deliver"] },
2650
+ summary: { type: "string" },
2651
+ blockers: { type: "array", items: { type: "string" } },
2652
+ // v3.5.0 Phase 2 — Option 1-Lite signal-driven Agent Teams escalation.
2653
+ // spawned subagent SHOULD set this when any of parallelism-gate.yaml 5
2654
+ // upgrade triggers fire. harnessed runtime propagates to stderr hint;
2655
+ // user opens team in main Claude Code session (TeamCreate not exposed to
2656
+ // spawned subagents via SDK v0.3.142 — see PHASE-2-SPEC.md § Why).
2657
+ needs_teams_escalation: { type: "boolean" },
2658
+ escalation_reason: { type: "string" }
2659
+ },
2660
+ required: ["status", "phase"]
2661
+ };
2662
+
2663
+ // src/workflow/lib/sdkReconcile.ts
2631
2664
  function toSdkAgentDefinition(def) {
2632
2665
  return {
2633
2666
  description: def.description,
@@ -2664,7 +2697,7 @@ ${def.criticalSystemReminder_EXPERIMENTAL}`);
2664
2697
  return parts.join("\n\n");
2665
2698
  }
2666
2699
 
2667
- // src/routing/lib/sdkSpawn.ts
2700
+ // src/workflow/lib/sdkSpawn.ts
2668
2701
  var SpawnFailError = class extends Error {
2669
2702
  constructor(lastMessage) {
2670
2703
  super("sdkSpawn produced no result message");
@@ -2704,146 +2737,30 @@ async function sdkSpawn(def, opts) {
2704
2737
  };
2705
2738
  return JSON.stringify(envelope);
2706
2739
  }
2707
- function isInstalled(skill, root) {
2708
- return existsSync(join(root, skill, "SKILL.md"));
2709
- }
2710
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2711
- async function ensureSkillsInstalled(required, skillsRoot) {
2712
- for (const name of required) {
2713
- if (isInstalled(name, skillsRoot)) continue;
2714
- await sleep(500);
2715
- let retries = 3;
2716
- while (retries-- > 0 && !isInstalled(name, skillsRoot)) await sleep(300);
2717
- if (!isInstalled(name, skillsRoot)) {
2718
- throw new RestartRequiredError(
2719
- `Skill ${name} still missing after retry \u2014 please exit + restart Claude Code so the plugin takes effect.`
2720
- );
2721
- }
2722
- }
2723
- }
2724
-
2725
- // src/routing/semanticRouter.ts
2726
- var DEFAULT_SEMANTIC_THRESHOLD = 0.85;
2727
- async function match(prompt, threshold = DEFAULT_SEMANTIC_THRESHOLD) {
2728
- return { matched: false, rule: null, confidence: 0 };
2729
- }
2730
2740
 
2731
- // src/routing/engine.ts
2732
- var defaultSpawn = (def, ctx) => sdkSpawn(def, ctx);
2733
- function buildDecision(matched) {
2734
- if (!matched) {
2735
- return {
2736
- matched_rule_id: null,
2737
- primary_expert: null,
2738
- secondary_expert: null,
2739
- category: "meta"
2740
- };
2741
+ // src/workflow/interpolate.ts
2742
+ var InterpolationError = class extends Error {
2743
+ constructor(message) {
2744
+ super(message);
2745
+ this.name = "InterpolationError";
2741
2746
  }
2742
- return {
2743
- matched_rule_id: matched.id,
2744
- primary_expert: matched.decision.primary_expert ?? null,
2745
- secondary_expert: matched.decision.secondary_expert ?? null,
2746
- category: matched.domain,
2747
- forbidden_skills: matched.decision.forbidden,
2748
- required_skills: matched.decision.required_skills
2749
- };
2750
- }
2751
- async function runRouting(task, opts = {}) {
2752
- const rulesPath = opts.rulesPath ?? join("routing", "decision_rules.yaml");
2753
- const skillsRoot = opts.skillsRoot ?? join(homedir(), ".claude", "skills");
2754
- const maxIter = opts.maxIterations ?? 20;
2755
- const userSpawn = opts.spawn;
2756
- if (opts.dagNodes && opts.dagNodes.length > 0) {
2757
- const dag = resolveDag(opts.dagNodes);
2758
- if (!dag.ok) {
2759
- const error = new InvalidDecisionError(
2760
- `skill dependency cycle: ${dag.cycle.join(" \u2192 ")} (see ADR 0009 \xA7 DAG resolver)`
2747
+ };
2748
+ var STRICT = /\{\{\s*(\w+)\s*\}\}/g;
2749
+ var ANY_TEMPLATE = /\{\{[^}]*\}\}/;
2750
+ function interpolate(template, vars) {
2751
+ const out = template.replace(STRICT, (_m, name) => {
2752
+ const v = vars[name];
2753
+ if (v === void 0) {
2754
+ throw new InterpolationError(
2755
+ `undefined template variable '${name}' (template excerpt: ${template.slice(0, 80)})`
2761
2756
  );
2762
- return { ok: false, phase: "arbitrate", error };
2763
- }
2764
- }
2765
- let rules;
2766
- try {
2767
- rules = loadDecisionRules(rulesPath).rules;
2768
- } catch (error) {
2769
- return { ok: false, phase: "arbitrate", error };
2770
- }
2771
- const taskCtx = {
2772
- task_type: task.task_type,
2773
- ...task.override_keywords ? { override_keywords: task.override_keywords } : {}
2774
- };
2775
- const matched = arbitrate(rules, taskCtx);
2776
- const decision = buildDecision(matched);
2777
- if (!matched) {
2778
- const semantic = await match(task.task, opts.semanticThreshold);
2779
- void semantic.rule;
2780
- if (opts.fallbackSupervisor) {
2781
- const result = await opts.fallbackSupervisor(task);
2782
- return { ok: true, result, matchedRule: null };
2783
- }
2784
- return {
2785
- ok: false,
2786
- phase: "arbitrate",
2787
- error: new InvalidDecisionError("no rule matched and no fallbackSupervisor provided")
2788
- };
2789
- }
2790
- try {
2791
- await ensureSkillsInstalled(decision.required_skills ?? [], skillsRoot);
2792
- } catch (error) {
2793
- return { ok: false, phase: "install", error };
2794
- }
2795
- let agentDef;
2796
- try {
2797
- agentDef = await createAgent(task, decision, { ...opts.agentOpts, skillsRoot });
2798
- } catch (error) {
2799
- if (error instanceof SkillNotInstalledError || error instanceof MissingSkillsError) {
2800
- return { ok: false, phase: "install", error };
2801
- }
2802
- if (error instanceof InvalidDecisionError) {
2803
- return { ok: false, phase: "arbitrate", error };
2804
- }
2805
- return { ok: false, phase: "spawn", error };
2806
- }
2807
- const phaseId = task.phaseId ?? "unknown";
2808
- await activatePhase(phaseId);
2809
- const expertName = matched.decision.primary_expert ?? "unknown";
2810
- let capturedSessionId;
2811
- const wrappedSpawn = async (resumeSessionId, onSessionIdInner) => userSpawn ? userSpawn(agentDef) : defaultSpawn(agentDef, {
2812
- expertName,
2813
- ...resumeSessionId ? { resumeSessionId } : {},
2814
- onSessionId: (id) => {
2815
- capturedSessionId = id;
2816
- onSessionIdInner?.(id);
2817
2757
  }
2758
+ return v;
2818
2759
  });
2819
- const fbCtx = {
2820
- subtaskSummary: task.task,
2821
- workflowName: task.task_type ?? "unknown",
2822
- phaseId: opts.fallbackPhaseId ?? task.phaseId ?? "unknown"
2823
- };
2824
- try {
2825
- const result = await ralphLoopWrap(wrappedSpawn, maxIter);
2826
- await completePhase({ phaseId, sessionId: capturedSessionId, status: "complete" });
2827
- emitAudit(task, decision, matched, "complete", capturedSessionId);
2828
- return { ok: true, result, matchedRule: matched };
2829
- } catch (error) {
2830
- if (error instanceof MaxIterationsExceededError) {
2831
- emitAudit(task, decision, matched, "max-iter", capturedSessionId);
2832
- if (opts.fallbackConfig)
2833
- handleMaxIterationsExceeded(error, opts.fallbackConfig, {
2834
- ...fbCtx,
2835
- maxIterations: maxIter
2836
- });
2837
- return { aborted: true, reason: error.message };
2838
- }
2839
- if (error instanceof VerbatimCompleteFailError) {
2840
- emitAudit(task, decision, matched, "verbatim-fail", capturedSessionId);
2841
- if (opts.fallbackConfig) handleVerbatimCompleteFail(error, opts.fallbackConfig, fbCtx);
2842
- return { ok: false, phase: "verbatim", error };
2843
- }
2844
- emitAudit(task, decision, matched, "spawn-err", capturedSessionId);
2845
- return { ok: false, phase: "spawn", error };
2760
+ if (ANY_TEMPLATE.test(out)) {
2761
+ throw new InterpolationError(`unsupported template syntax in: ${out.slice(0, 80)}`);
2846
2762
  }
2763
+ return out;
2847
2764
  }
2848
2765
  var ModelTier = Type.Union([
2849
2766
  Type.Literal("haiku"),
@@ -3020,7 +2937,7 @@ var WorkflowPhaseV3 = Type.Object(
3020
2937
  },
3021
2938
  { additionalProperties: false }
3022
2939
  );
3023
- Type.Object(
2940
+ var WorkflowSchemaV3 = Type.Object(
3024
2941
  {
3025
2942
  schema_version: Type.Literal(SCHEMA_VERSIONS.workflow_v3),
3026
2943
  workflow: Type.String({ minLength: 1 }),
@@ -3044,26 +2961,638 @@ var PhasesValidationError = class extends Error {
3044
2961
  this.errors = errors;
3045
2962
  this.name = "PhasesValidationError";
3046
2963
  }
3047
- errors;
2964
+ errors;
2965
+ };
2966
+ function loadPhases(yamlPath, vars) {
2967
+ const raw = readFileSync(yamlPath, "utf8");
2968
+ const parsed = parse(raw);
2969
+ const version = parsed?.schema_version;
2970
+ if (version === "harnessed.workflow.v3") {
2971
+ if (!Value.Check(WorkflowSchemaV3, parsed)) {
2972
+ throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV3, parsed)]);
2973
+ }
2974
+ } else if (version === "harnessed.workflow.v2") {
2975
+ if (!Value.Check(WorkflowSchemaV2, parsed)) {
2976
+ throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV2, parsed)]);
2977
+ }
2978
+ } else {
2979
+ if (!Value.Check(PhasesSchema, parsed)) {
2980
+ throw new PhasesValidationError([...Value.Errors(PhasesSchema, parsed)]);
2981
+ }
2982
+ }
2983
+ const validated = parsed;
2984
+ if (vars && validated.phases) {
2985
+ for (const ph of validated.phases) {
2986
+ if (ph.invokes) ph.invokes = interpolate(ph.invokes, vars);
2987
+ }
2988
+ }
2989
+ return validated;
2990
+ }
2991
+ function resolveMasterYamlPath(masterName, packageRoot) {
2992
+ return masterName === "auto" ? resolve(packageRoot, "workflows", "auto", "workflow.yaml") : resolve(packageRoot, "workflows", masterName, "auto", "workflow.yaml");
2993
+ }
2994
+ function resolveSubYamlPath(masterName, subName, packageRoot) {
2995
+ if (masterName === "auto") {
2996
+ if (subName === "research" || subName === "retro") {
2997
+ return resolve(packageRoot, "workflows", subName, "workflow.yaml");
2998
+ }
2999
+ return resolve(packageRoot, "workflows", subName, "auto", "workflow.yaml");
3000
+ }
3001
+ return resolve(packageRoot, "workflows", masterName, subName, "workflow.yaml");
3002
+ }
3003
+ var defaultSpawnDriver = async (masterName, subName, _context, packageRoot) => {
3004
+ const subYamlPath = resolveSubYamlPath(masterName, subName, packageRoot);
3005
+ try {
3006
+ await runWorkflow(subYamlPath, {}, { packageRoot, gateContext: _context });
3007
+ } catch (err2) {
3008
+ console.warn(
3009
+ `\u26A0\uFE0F master spawnSubWorkflow Path A failed for ${masterName}/${subName} (${err2.message});Path B sub-shell fallback deferred T3.5.W2.1.`
3010
+ );
3011
+ }
3012
+ };
3013
+ var defaultPauseFn = async (stageName) => {
3014
+ const readline2 = await import('readline/promises');
3015
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3016
+ try {
3017
+ await rl.question(
3018
+ `
3019
+ [--staged] Stage '${stageName}' complete. Press Enter to continue (Ctrl+C to abort)... `
3020
+ );
3021
+ } finally {
3022
+ rl.close();
3023
+ }
3024
+ };
3025
+ var defaultAssessComplexity = async (_taskDescription) => {
3026
+ return "medium";
3027
+ };
3028
+ var defaultPromptUserUnderstanding = async (prompter = defaultPrompter) => {
3029
+ const answer = await prompter(
3030
+ "\n[Phase 0.5] \u5BF9\u9700\u6C42\u6709\u6E05\u6670\u8BA4\u77E5\u5417? [Y/n] (n = \u5148\u8DD1 /research \u591A\u6E90\u8C03\u7814): "
3031
+ );
3032
+ const a = answer.trim().toLowerCase();
3033
+ return !(a === "n" || a === "no");
3034
+ };
3035
+ var defaultPrompter = async (question) => {
3036
+ const readline2 = await import('readline/promises');
3037
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3038
+ try {
3039
+ return await rl.question(question);
3040
+ } finally {
3041
+ rl.close();
3042
+ }
3043
+ };
3044
+ async function runAutoPreFlight(ctx, opts) {
3045
+ const assess = opts.assessComplexity ?? defaultAssessComplexity;
3046
+ const prompter = opts.prompter ?? defaultPrompter;
3047
+ const taskDesc = typeof ctx.task_description === "string" ? ctx.task_description : "";
3048
+ const size = await assess(taskDesc);
3049
+ console.log(`[/auto Phase 0] Complexity assessment: ${size}`);
3050
+ let nextOpts = opts;
3051
+ if (size === "large") {
3052
+ const a = (await prompter(
3053
+ "\n[Phase 0] \u9700\u6C42\u590D\u6742\u5EA6\u8F83\u5927,\u5168\u7A0B auto \u53EF\u80FD\u65F6\u95F4\u8FC7\u957F / \u4E0A\u4E0B\u6587\u8D85\u9650\u3002\u5EFA\u8BAE `--staged` \u6A21\u5F0F (\u6BCF stage \u5B8C\u505C review)\u3002\u662F\u5426\u5207\u6362? [Y/n]: "
3054
+ )).trim().toLowerCase();
3055
+ if (a === "n" || a === "no") {
3056
+ console.log(
3057
+ "\n[/auto Phase 0] User declined --staged. Aborting auto mode \u2014 \u5EFA\u8BAE\u624B\u52A8 `/discuss` \u542F\u52A8\u3002"
3058
+ );
3059
+ return { proceed: false, opts };
3060
+ }
3061
+ console.log("[/auto Phase 0] Switching to --staged mode.");
3062
+ nextOpts = { ...opts, pauseBetweenStages: true };
3063
+ }
3064
+ const promptUnderstanding = opts.promptUserUnderstanding ?? defaultPromptUserUnderstanding;
3065
+ const isClear = await promptUnderstanding(prompter);
3066
+ ctx.user_understanding_unclear = !isClear;
3067
+ console.log(
3068
+ `[/auto Phase 0.5] user_understanding_unclear=${!isClear} (research will ${isClear ? "skip" : "fire"})`
3069
+ );
3070
+ return { proceed: true, opts: nextOpts };
3071
+ }
3072
+ function emitGateTransparency(masterName, totalGates, gateEvalled) {
3073
+ console.log(`[${masterName} master] Evaluating ${totalGates} sub-workflow gates:`);
3074
+ for (const g of gateEvalled) {
3075
+ const mark = g.passes ? "\u2713" : "\u2298";
3076
+ const tail = g.passes ? g.clause.gate ? ` (${g.clause.gate} == true)` : " (unconditional)" : ` skipped \u2014 ${g.reason}`;
3077
+ console.log(` ${mark} ${g.clause.sub}${tail}`);
3078
+ }
3079
+ }
3080
+ async function maybeArbitrate(firedClauses, packageRoot) {
3081
+ if (firedClauses.length <= 1) return;
3082
+ const firedCaps = firedClauses.map((c) => ({ name: c.sub, tier: c.sub }));
3083
+ try {
3084
+ await arbitrateBeforeSpawn(firedCaps, packageRoot);
3085
+ console.warn(
3086
+ `\u26A0\uFE0F multi-capability fires (${firedClauses.length} sub), arbitrating by priority hierarchy (K14 warn-not-halt; v3.0 sub-as-tier placeholder, real cross-tier sort defer v3.x)`
3087
+ );
3088
+ } catch (e) {
3089
+ console.warn(`\u26A0\uFE0F arbitrate failed (${e.message}) \u2014 proceeding default order`);
3090
+ }
3091
+ }
3092
+
3093
+ // src/workflow/masterOrchestrator.ts
3094
+ async function runMasterOrchestrator(masterName, context, packageRoot, spawnDriver = defaultSpawnDriver, opts = {}) {
3095
+ const yamlPath = resolveMasterYamlPath(masterName, packageRoot);
3096
+ const raw = await readFile(yamlPath, "utf8");
3097
+ const parsed = parse(raw);
3098
+ if (!Value.Check(WorkflowSchemaV3, parsed)) {
3099
+ const errors = [...Value.Errors(WorkflowSchemaV3, parsed)].slice(0, 3).map((e) => `${e.path} ${e.message}`).join("; ");
3100
+ throw new Error(`Invalid master workflow.yaml at ${yamlPath}: ${errors}`);
3101
+ }
3102
+ const master = parsed;
3103
+ if (!master.delegates_to || master.delegates_to.length === 0) {
3104
+ throw new Error(`Master workflow ${masterName} missing delegates_to`);
3105
+ }
3106
+ let effectiveOpts = opts;
3107
+ const preflightActive = masterName === "auto" && (opts.assessComplexity !== void 0 || opts.prompter !== void 0 || opts.promptUserUnderstanding !== void 0);
3108
+ if (preflightActive) {
3109
+ const pre = await runAutoPreFlight(context, opts);
3110
+ if (!pre.proceed) {
3111
+ console.log(`[${masterName} master] Aborted by user (complexity gate decline).`);
3112
+ return { master: masterName, fired: [], skipped: master.delegates_to.map((c) => c.sub) };
3113
+ }
3114
+ effectiveOpts = pre.opts;
3115
+ }
3116
+ const gateEvalled = [];
3117
+ for (const clause of master.delegates_to) {
3118
+ if (!clause.gate) {
3119
+ gateEvalled.push({ clause, passes: true });
3120
+ continue;
3121
+ }
3122
+ try {
3123
+ const passes = await resolveJudgmentGate(clause.gate, context, packageRoot);
3124
+ gateEvalled.push({
3125
+ clause,
3126
+ passes,
3127
+ reason: passes ? void 0 : `gate ${clause.gate} = false`
3128
+ });
3129
+ } catch (e) {
3130
+ gateEvalled.push({
3131
+ clause,
3132
+ passes: false,
3133
+ reason: `gate eval error: ${e.message}`
3134
+ });
3135
+ }
3136
+ }
3137
+ emitGateTransparency(masterName, master.delegates_to.length, gateEvalled);
3138
+ const PARALLEL_MID_ANCHOR = 50;
3139
+ const firedClauses = gateEvalled.filter((g) => g.passes).map((g) => g.clause);
3140
+ const serialLeading = firedClauses.filter((c) => c.mode === "serial" && (c.order ?? 0) < PARALLEL_MID_ANCHOR).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
3141
+ const serialTrailing = firedClauses.filter((c) => c.mode === "serial" && (c.order ?? 0) >= PARALLEL_MID_ANCHOR).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
3142
+ const parallelClauses = firedClauses.filter((c) => (c.mode ?? "parallel") === "parallel");
3143
+ const serialN = serialLeading.length + serialTrailing.length;
3144
+ const parallelN = parallelClauses.length;
3145
+ await maybeArbitrate(firedClauses, packageRoot);
3146
+ const modeLabel = serialN > 0 && parallelN > 0 ? "serial+parallel" : serialN > 0 ? "serial" : "parallel";
3147
+ console.log(`Firing ${firedClauses.length} sub in ${modeLabel}:`);
3148
+ const fired = [];
3149
+ const pauseHook = effectiveOpts.pauseBetweenStages ? effectiveOpts.pauseFn ?? defaultPauseFn : void 0;
3150
+ for (const clause of serialLeading) {
3151
+ console.log(` \u2192 ${clause.sub} (serial order=${clause.order ?? 0})`);
3152
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3153
+ fired.push(clause.sub);
3154
+ if (pauseHook) await pauseHook(clause.sub);
3155
+ }
3156
+ const parallelResults = await Promise.allSettled(
3157
+ parallelClauses.map(async (clause) => {
3158
+ console.log(` \u2192 ${clause.sub} (parallel)`);
3159
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3160
+ return clause.sub;
3161
+ })
3162
+ );
3163
+ for (const r of parallelResults) {
3164
+ if (r.status === "fulfilled") fired.push(r.value);
3165
+ }
3166
+ for (const clause of serialTrailing) {
3167
+ console.log(` \u2192 ${clause.sub} (serial order=${clause.order ?? 0})`);
3168
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3169
+ fired.push(clause.sub);
3170
+ if (pauseHook) await pauseHook(clause.sub);
3171
+ }
3172
+ const skipped = gateEvalled.filter((g) => !g.passes).map((g) => g.clause.sub);
3173
+ console.log(`[${masterName} master] Complete: ${fired.length} fired, ${skipped.length} skipped.`);
3174
+ return { master: masterName, fired, skipped };
3175
+ }
3176
+
3177
+ // src/workflow/run.ts
3178
+ var MASTER_NAMES = ["discuss", "plan", "task", "verify", "auto"];
3179
+ var RALPH_DEFAULT_MAX_ITER = 20;
3180
+ var RALPH_HARD_UPPER_LIMIT = 100;
3181
+ var ESCALATION_RULES = `If during this task you detect ANY of the following 5 conditions, set \`needs_teams_escalation: true\` in your structured output and fill \`escalation_reason\` with the trigger name + one-sentence specifics. These are signals to the human user in the main Claude Code session \u2014 do NOT attempt to call TeamCreate/SendMessage/TeamDelete yourself (those tools are not available to you).
3182
+
3183
+ Five triggers (any one suffices):
3184
+
3185
+ 1. **teammate_send_message_needed** \u2014 the task requires two or more subagents to exchange messages mid-task (e.g., reconciling API contract proposals across frontend and backend), not just fan-out + report.
3186
+
3187
+ 2. **subagent_context_overflow** \u2014 your context budget is filling and a separate subagent is needed to take over a portion of the work.
3188
+
3189
+ 3. **shared_task_list** \u2014 multiple subagents need to coordinate self-assignment from a shared task list (not pre-partitioned work).
3190
+
3191
+ 4. **opposing_hypothesis_debate** \u2014 the task requires two subagents to defend opposing hypotheses to a lead arbiter (e.g., root-cause debugging where two competing theories need separate evidence-gathering).
3192
+
3193
+ 5. **fullstack_three_way** \u2014 the task is a synchronized fullstack push (frontend + backend + tests) requiring API contract alignment across three roles simultaneously.
3194
+
3195
+ If none of the five apply, omit \`needs_teams_escalation\` (defaults to false) and proceed normally.`;
3196
+ function buildAgentDef(skillName, rolePrompts, workflowName, modelTierOverride) {
3197
+ const rp = rolePrompts?.[skillName] ?? (workflowName ? rolePrompts?.[workflowName] : void 0);
3198
+ if (!rp) {
3199
+ return {
3200
+ description: `harnessed workflow phase: ${skillName}`,
3201
+ prompt: `You are executing the '${skillName}' workflow phase. Follow the phase intent and emit a structured COMPLETE signal when done.`,
3202
+ criticalSystemReminder_EXPERIMENTAL: ESCALATION_RULES,
3203
+ ...modelTierOverride ? { model: modelTierOverride } : {}
3204
+ };
3205
+ }
3206
+ const checklist = rp.checklist.length ? `
3207
+
3208
+ Checklist:
3209
+ ${rp.checklist.map((c, i) => ` ${i + 1}. ${c}`).join("\n")}` : "";
3210
+ const prompt = [
3211
+ `You are a ${rp.specialist}.`,
3212
+ ``,
3213
+ rp.responsibility.trim(),
3214
+ checklist,
3215
+ ``,
3216
+ `Severity scale: ${rp.severity}`,
3217
+ ``,
3218
+ `Emit a structured COMPLETE signal when done.`
3219
+ ].join("\n");
3220
+ return {
3221
+ description: rp.description,
3222
+ prompt,
3223
+ criticalSystemReminder_EXPERIMENTAL: ESCALATION_RULES,
3224
+ ...modelTierOverride ? { model: modelTierOverride } : {}
3225
+ };
3226
+ }
3227
+ function isRalphLoopOptIn(phase) {
3228
+ if (!phase || typeof phase !== "object") return false;
3229
+ const p4 = phase;
3230
+ if (p4.max_iterations !== void 0 && p4.max_iterations !== null) return true;
3231
+ if (p4.upstream === "ralph-loop") return true;
3232
+ const fb = p4.fallback;
3233
+ if (fb?.max_iterations_exceeded !== void 0) return true;
3234
+ return false;
3235
+ }
3236
+ function resolveMaxIterations(phase, gateContext) {
3237
+ const fromCli = typeof gateContext.maxIterations === "number" ? gateContext.maxIterations : void 0;
3238
+ let fromYaml;
3239
+ if (phase && typeof phase === "object") {
3240
+ const raw = phase.max_iterations;
3241
+ if (typeof raw === "number") fromYaml = raw;
3242
+ else if (typeof raw === "string") {
3243
+ const n = Number.parseInt(raw, 10);
3244
+ if (Number.isFinite(n) && n > 0) fromYaml = n;
3245
+ }
3246
+ }
3247
+ const chosen = fromCli ?? fromYaml ?? RALPH_DEFAULT_MAX_ITER;
3248
+ return Math.min(Math.max(1, chosen), RALPH_HARD_UPPER_LIMIT);
3249
+ }
3250
+ var _dispatchSkillStub = {
3251
+ fn: async (skillName, phase, opts) => {
3252
+ const optIn = isRalphLoopOptIn(phase);
3253
+ const spawnOnce = async (resumeSessionId, onSessionId) => sdkSpawn(
3254
+ buildAgentDef(skillName, opts?.rolePrompts, opts?.workflowName, opts?.modelTierOverride),
3255
+ {
3256
+ expertName: skillName,
3257
+ ...resumeSessionId ? { resumeSessionId } : {},
3258
+ ...onSessionId ? { onSessionId } : {}
3259
+ }
3260
+ );
3261
+ let envelopeJson;
3262
+ try {
3263
+ if (optIn) {
3264
+ const maxIter = opts?.maxIter ?? RALPH_DEFAULT_MAX_ITER;
3265
+ envelopeJson = await ralphLoopWrap(spawnOnce, maxIter);
3266
+ } else {
3267
+ envelopeJson = await spawnOnce();
3268
+ }
3269
+ } catch (err2) {
3270
+ if (err2 instanceof MaxIterationsExceededError && opts?.fallback) {
3271
+ handleMaxIterationsExceeded(err2, opts.fallback, {
3272
+ subtaskSummary: `phase ${skillName}`,
3273
+ // Phase 4 — plumbed actual workflow name (was hardcoded 'harnessed-run'
3274
+ // pre-Phase 4); falls back to literal when opts.workflowName absent
3275
+ // (preserves Phase 3 behavior for callers that don't pass workflowName).
3276
+ workflowName: opts.workflowName ?? "harnessed-run",
3277
+ phaseId: skillName,
3278
+ maxIterations: opts?.maxIter ?? RALPH_DEFAULT_MAX_ITER
3279
+ });
3280
+ }
3281
+ return {
3282
+ status: "fail",
3283
+ output: err2 instanceof MaxIterationsExceededError ? `ralph-loop max-iterations exceeded (${err2.iterations}) for ${skillName}` : `sdkSpawn failed for ${skillName}: ${err2.message}`
3284
+ };
3285
+ }
3286
+ const env = JSON.parse(envelopeJson);
3287
+ const status = env.structured_output?.status === "COMPLETE" || env.subtype === "success" ? "ok" : "fail";
3288
+ const escalation = env.structured_output?.needs_teams_escalation === true;
3289
+ return {
3290
+ status,
3291
+ output: env.text ?? env.result ?? "",
3292
+ ...env.structured_output?.status ? { decision: env.structured_output.status } : {},
3293
+ ...escalation ? {
3294
+ needsTeamsEscalation: true,
3295
+ ...env.structured_output?.escalation_reason ? { escalationReason: env.structured_output.escalation_reason } : {}
3296
+ } : {}
3297
+ };
3298
+ }
3048
3299
  };
3049
- function loadPhases(yamlPath, vars) {
3050
- const raw = readFileSync(yamlPath, "utf8");
3051
- const parsed = parse(raw);
3052
- const isV2 = parsed?.schema_version === "harnessed.workflow.v2";
3053
- if (isV2) {
3054
- if (!Value.Check(WorkflowSchemaV2, parsed)) {
3055
- throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV2, parsed)]);
3300
+ async function runWorkflow(yamlPath, vars, opts = {}) {
3301
+ const parsed = loadPhases(yamlPath, vars);
3302
+ const yamlDir = dirname(resolve(yamlPath));
3303
+ const inferredRoot = resolve(yamlDir, "..", "..");
3304
+ const packageRoot = opts.packageRoot ?? inferredRoot;
3305
+ const gateContext = { ...opts.gateContext ?? {} };
3306
+ let rolePrompts = {};
3307
+ try {
3308
+ rolePrompts = await loadRolePrompts(join(packageRoot, "workflows"));
3309
+ } catch (err2) {
3310
+ console.warn(
3311
+ `\u26A0\uFE0F loadRolePrompts failed (${err2.message}); proceeding without role-prompt enrichment (ADR 0029 fail-soft).`
3312
+ );
3313
+ }
3314
+ const workflowName = parsed.workflow;
3315
+ const isMaster = "delegates_to" in parsed && Array.isArray(parsed.delegates_to) && parsed.delegates_to.length > 0 && MASTER_NAMES.includes(workflowName);
3316
+ if (isMaster) {
3317
+ const r = await runMasterOrchestrator(workflowName, gateContext, packageRoot);
3318
+ return {
3319
+ status: "complete",
3320
+ phasesRun: r.fired.length,
3321
+ ...r.skipped.length > 0 ? { skippedPhases: r.skipped } : {}
3322
+ };
3323
+ }
3324
+ const disciplinesApplied = "disciplines_applied" in parsed && Array.isArray(parsed.disciplines_applied) ? parsed.disciplines_applied : void 0;
3325
+ try {
3326
+ const disciplines = await loadDisciplinesForPhase(disciplinesApplied, packageRoot);
3327
+ gateContext.disciplines = disciplines;
3328
+ } catch (err2) {
3329
+ console.warn(
3330
+ `\u26A0\uFE0F loadDisciplinesForPhase failed (${err2.message}); proceeding without disciplines map (sister ADR 0029 fail-soft).`
3331
+ );
3332
+ }
3333
+ const skippedPhases = [];
3334
+ const phases = parsed.phases ?? [];
3335
+ for (let i = 0; i < phases.length; i++) {
3336
+ const ph = phases[i];
3337
+ if (!ph) continue;
3338
+ await activatePhase(ph.id);
3339
+ const invokesTools = "invokes_tools" in ph && Array.isArray(ph.invokes_tools) ? ph.invokes_tools : void 0;
3340
+ if (invokesTools && invokesTools.length > 1) {
3341
+ try {
3342
+ const firedCaps = invokesTools.map((c) => ({ name: c.tool, tier: c.tool }));
3343
+ await arbitrateBeforeSpawn(firedCaps, packageRoot);
3344
+ } catch (err2) {
3345
+ console.warn(
3346
+ `\u26A0\uFE0F phase ${ph.id} before-spawn arbitrate failed (${err2.message}); proceeding default tool order (K14 warn-not-halt).`
3347
+ );
3348
+ }
3056
3349
  }
3057
- } else {
3058
- if (!Value.Check(PhasesSchema, parsed)) {
3059
- throw new PhasesValidationError([...Value.Errors(PhasesSchema, parsed)]);
3350
+ if (await isVetoed()) {
3351
+ await pause();
3352
+ return { status: "paused-veto", phasesRun: i, lastPhaseId: ph.id };
3353
+ }
3354
+ if ("gate" in ph && ph.gate) {
3355
+ let fires = true;
3356
+ try {
3357
+ fires = await resolveJudgmentGate(ph.gate, gateContext, packageRoot);
3358
+ } catch (err2) {
3359
+ console.warn(
3360
+ `\u26A0\uFE0F phase ${ph.id} gate ${ph.gate} eval failed (${err2.message}); proceeding with phase as if gate fired=true (ADR 0029 fail-soft).`
3361
+ );
3362
+ }
3363
+ if (!fires) {
3364
+ skippedPhases.push(ph.id);
3365
+ await completePhase({
3366
+ phaseId: ph.id,
3367
+ lastTask: `phase ${ph.id} skipped: gate ${ph.gate} evaluated false`
3368
+ });
3369
+ continue;
3370
+ }
3371
+ }
3372
+ const skillName = "skills" in ph && ph.skills?.[0] || ph.id;
3373
+ const maxIter = resolveMaxIterations(ph, gateContext);
3374
+ const fallback = "fallback" in ph && ph.fallback?.max_iterations_exceeded ? ph.fallback.max_iterations_exceeded : void 0;
3375
+ const r = await _dispatchSkillStub.fn(skillName, ph, {
3376
+ maxIter,
3377
+ ...fallback ? { fallback } : {},
3378
+ workflowName,
3379
+ rolePrompts,
3380
+ ...typeof gateContext.modelTierOverride === "string" ? { modelTierOverride: gateContext.modelTierOverride } : {}
3381
+ });
3382
+ if (r.status !== "ok") {
3383
+ return { status: "failed", phasesRun: i, lastPhaseId: ph.id };
3384
+ }
3385
+ if (r.needsTeamsEscalation === true) {
3386
+ const reason = r.escalationReason ?? "unspecified trigger";
3387
+ console.error(
3388
+ `\u26A0\uFE0F phase ${ph.id} suggests Agent Teams escalation \u2014 ${reason}. Consider opening a team in your main Claude Code session (TeamCreate) if continuing this work benefits from teammate coordination. See workflows/judgments/parallelism-gate.yaml for the 5 upgrade triggers.`
3389
+ );
3390
+ }
3391
+ if (r.target === "chat") {
3392
+ try {
3393
+ await runAfterOutputHook({
3394
+ responseText: r.output,
3395
+ responseTarget: "chat",
3396
+ userRequestedEmoji: false,
3397
+ packageRoot
3398
+ });
3399
+ } catch (err2) {
3400
+ console.warn(
3401
+ `\u26A0\uFE0F phase ${ph.id} after-output hook failed (${err2.message}); proceeding (ADR 0029 fail-soft).`
3402
+ );
3403
+ }
3404
+ }
3405
+ if (r.triggers_commit === true) {
3406
+ try {
3407
+ await runBeforeCommitHook({
3408
+ changedFiles: [],
3409
+ cmdArgs: [],
3410
+ packageRoot,
3411
+ cmdType: "git-commit",
3412
+ hasUserApproval: false
3413
+ });
3414
+ } catch (err2) {
3415
+ console.warn(
3416
+ `\u26A0\uFE0F phase ${ph.id} before-commit hook failed (${err2.message}); proceeding (ADR 0029 fail-soft).`
3417
+ );
3418
+ }
3060
3419
  }
3420
+ await completePhase({
3421
+ phaseId: ph.id,
3422
+ lastTask: `phase ${ph.id} complete: ${r.output}`
3423
+ });
3061
3424
  }
3062
- const validated = parsed;
3063
- return validated;
3425
+ return {
3426
+ status: "complete",
3427
+ phasesRun: phases.length,
3428
+ ...skippedPhases.length > 0 ? { skippedPhases } : {}
3429
+ };
3430
+ }
3431
+ function getPackageRoot() {
3432
+ const thisFile = fileURLToPath(import.meta.url);
3433
+ const thisDir = dirname(thisFile);
3434
+ if (thisDir.endsWith("dist") || thisDir.replace(/\\/g, "/").endsWith("/dist")) {
3435
+ return resolve(thisDir, "..");
3436
+ }
3437
+ return resolve(thisDir, "..", "..", "..");
3438
+ }
3439
+ var PACKAGE_ROOT = getPackageRoot();
3440
+ var WORKFLOWS_DIR = join(PACKAGE_ROOT, "workflows");
3441
+ var _autoChainCache = null;
3442
+ var _autoChainLoadFailed = false;
3443
+ function registerRun(program2) {
3444
+ program2.command("run").description(
3445
+ "Run a harnessed workflow (master orchestrator or sub-workflow). Slash commands invoke via this subcommand."
3446
+ ).argument("[name]", "workflow name (e.g. discuss, verify-paranoid, research, auto)").option("--task <text>", "task description (passed as workflow gateContext.task)").option("--task-stdin", "read task description from stdin until EOF (avoids shell-escape)").option(
3447
+ "--max-iterations <n>",
3448
+ "ralph-loop max iter (default 20; honored Phase 3 onward)",
3449
+ (v) => parseInt(v, 10)
3450
+ ).option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").option(
3451
+ "--dry-run",
3452
+ "validate yaml load + walk runtime without spawning (Phase 1 default for verification)"
3453
+ ).option("--staged", "/auto super-master: pause between stages for user review").option("--list", "print all known workflow names and exit").action(async (name, raw) => {
3454
+ if (raw.list) {
3455
+ const names = await listWorkflowNames(WORKFLOWS_DIR);
3456
+ for (const n of names) console.log(n);
3457
+ process.exit(0);
3458
+ }
3459
+ if (!name) {
3460
+ console.error("error: workflow name required (or pass --list to enumerate)");
3461
+ process.exit(2);
3462
+ }
3463
+ let task = "";
3464
+ if (typeof raw.task === "string") {
3465
+ task = raw.task;
3466
+ } else if (raw.taskStdin) {
3467
+ task = await readStdinToEnd();
3468
+ }
3469
+ const yamlPath = await resolveWorkflowYaml(name, WORKFLOWS_DIR);
3470
+ if (!yamlPath) {
3471
+ console.error(
3472
+ `error: workflow '${name}' not found under workflows/. Run \`harnessed run --list\` to enumerate.`
3473
+ );
3474
+ process.exit(2);
3475
+ }
3476
+ const gateContext = {
3477
+ task,
3478
+ ...raw.model ? { modelOverride: raw.model } : {},
3479
+ ...raw.maxIterations ? { maxIterations: raw.maxIterations } : {},
3480
+ ...raw.staged ? { staged: true } : {}
3481
+ };
3482
+ if (raw.dryRun) {
3483
+ console.log(JSON.stringify({ workflow: name, yamlPath, gateContext }, null, 2));
3484
+ process.exit(0);
3485
+ }
3486
+ let result;
3487
+ try {
3488
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT, gateContext });
3489
+ } catch (err2) {
3490
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
3491
+ process.exit(1);
3492
+ return;
3493
+ }
3494
+ const hint = await getNextHint(name);
3495
+ process.stderr.write(`[stage ${name} ${result.status}]
3496
+ `);
3497
+ if (hint) {
3498
+ process.stderr.write(`Next stage: harnessed run ${hint}
3499
+ (In Claude Code: /${hint})
3500
+ `);
3501
+ }
3502
+ process.exit(result.status === "failed" ? 1 : 0);
3503
+ });
3504
+ }
3505
+ async function resolveWorkflowYaml(name, workflowsDir) {
3506
+ const tier1 = join(workflowsDir, name, "workflow.yaml");
3507
+ if (existsSync(tier1)) return tier1;
3508
+ const tier2 = join(workflowsDir, name, "auto", "workflow.yaml");
3509
+ if (existsSync(tier2)) return tier2;
3510
+ const dashIdx = name.indexOf("-");
3511
+ if (dashIdx > 0) {
3512
+ const stage = name.slice(0, dashIdx);
3513
+ const sub = name.slice(dashIdx + 1);
3514
+ const tier3 = join(workflowsDir, stage, sub, "workflow.yaml");
3515
+ if (existsSync(tier3)) return tier3;
3516
+ }
3517
+ return null;
3518
+ }
3519
+ async function listWorkflowNames(workflowsDir) {
3520
+ const names = [];
3521
+ const entries = await readdir(workflowsDir);
3522
+ for (const e of entries.sort()) {
3523
+ const p4 = join(workflowsDir, e);
3524
+ const s = await stat(p4).catch(() => null);
3525
+ if (!s?.isDirectory()) continue;
3526
+ if (await fileExists(join(p4, "workflow.yaml"))) {
3527
+ names.push(e);
3528
+ continue;
3529
+ }
3530
+ if (await fileExists(join(p4, "auto", "workflow.yaml"))) {
3531
+ names.push(e);
3532
+ const subs = await readdir(p4).catch(() => []);
3533
+ for (const sub of subs.sort()) {
3534
+ if (sub === "auto") continue;
3535
+ if (await fileExists(join(p4, sub, "workflow.yaml"))) {
3536
+ names.push(`${e}-${sub}`);
3537
+ }
3538
+ }
3539
+ }
3540
+ }
3541
+ return names;
3542
+ }
3543
+ async function fileExists(path) {
3544
+ return stat(path).then(() => true).catch(() => false);
3545
+ }
3546
+ async function readStdinToEnd() {
3547
+ const chunks = [];
3548
+ for await (const chunk of process.stdin) {
3549
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3550
+ }
3551
+ return Buffer.concat(chunks).toString("utf8").trim();
3552
+ }
3553
+ function loadAutoChain() {
3554
+ if (_autoChainCache !== null) return _autoChainCache;
3555
+ if (_autoChainLoadFailed) return [];
3556
+ try {
3557
+ const yamlPath = join(WORKFLOWS_DIR, "auto", "workflow.yaml");
3558
+ const parsed = loadPhases(yamlPath);
3559
+ const delegates = "delegates_to" in parsed && Array.isArray(parsed.delegates_to) ? parsed.delegates_to : [];
3560
+ const sorted = [...delegates].sort((a, b) => {
3561
+ const ao = typeof a.order === "number" ? a.order : Number.MAX_SAFE_INTEGER;
3562
+ const bo = typeof b.order === "number" ? b.order : Number.MAX_SAFE_INTEGER;
3563
+ return ao - bo;
3564
+ });
3565
+ _autoChainCache = sorted.map((d) => d.sub).filter((s) => typeof s === "string");
3566
+ return _autoChainCache;
3567
+ } catch (err2) {
3568
+ _autoChainLoadFailed = true;
3569
+ process.stderr.write(`\u26A0\uFE0F getNextHint failed (${err2.message}); skipping hint.
3570
+ `);
3571
+ return [];
3572
+ }
3573
+ }
3574
+ async function getNextHint(workflowName) {
3575
+ if (workflowName === "auto") return null;
3576
+ const chain = loadAutoChain();
3577
+ if (chain.length === 0) return null;
3578
+ const directIdx = chain.indexOf(workflowName);
3579
+ if (directIdx >= 0) {
3580
+ return directIdx + 1 < chain.length ? chain[directIdx + 1] ?? null : null;
3581
+ }
3582
+ const dashIdx = workflowName.indexOf("-");
3583
+ if (dashIdx > 0) {
3584
+ const parentStage = workflowName.slice(0, dashIdx);
3585
+ const parentIdx = chain.indexOf(parentStage);
3586
+ if (parentIdx >= 0) {
3587
+ return parentIdx + 1 < chain.length ? chain[parentIdx + 1] ?? null : null;
3588
+ }
3589
+ }
3590
+ return null;
3064
3591
  }
3065
3592
 
3066
3593
  // src/cli/execute-task.ts
3594
+ var PACKAGE_ROOT2 = getPackageRoot();
3595
+ var WORKFLOWS_DIR2 = join(PACKAGE_ROOT2, "workflows");
3067
3596
  function registerExecuteTask(program2) {
3068
3597
  program2.command("execute-task").description(
3069
3598
  "Run execute-task workflow (4-phase chain \u2192 ralph-loop COMPLETE; immediate by default \u2014 use --dry-run for preview)"
@@ -3073,40 +3602,27 @@ function registerExecuteTask(program2) {
3073
3602
  process.exit(2);
3074
3603
  }
3075
3604
  const workflowName = raw.workflow ?? "execute-task";
3076
- let phases;
3077
- try {
3078
- phases = loadPhases(`workflows/${workflowName}/phases.yaml`);
3079
- } catch (error) {
3605
+ const yamlPath = await resolveWorkflowYaml(workflowName, WORKFLOWS_DIR2);
3606
+ if (!yamlPath) {
3080
3607
  console.error(
3081
3608
  t("execute_task.load_phases_failed", {
3082
3609
  workflow: workflowName,
3083
- message: error.message
3610
+ message: "workflow.yaml not found"
3084
3611
  })
3085
3612
  );
3086
3613
  process.exit(2);
3614
+ return;
3087
3615
  }
3088
- if (raw.modelTier === "inherit") {
3089
- phases = {
3090
- ...phases,
3091
- phases: phases.phases.map((p4) => ({ ...p4, model: "inherit" }))
3092
- };
3093
- }
3094
- const taskCtx = { task: raw.task, task_type: "execute-task" };
3095
- const isDryRun = raw.dryRun === true;
3096
- if (isDryRun) {
3097
- console.log(
3098
- JSON.stringify({ workflow: phases.workflow, phases: phases.phases, taskCtx }, null, 2)
3099
- );
3616
+ const gateContext = {
3617
+ task: raw.task,
3618
+ ...raw.model ? { modelOverride: raw.model } : {},
3619
+ ...raw.modelTier ? { modelTierOverride: raw.modelTier } : {},
3620
+ ...raw.maxIterations ? { maxIterations: raw.maxIterations } : {}
3621
+ };
3622
+ if (raw.dryRun === true) {
3623
+ console.log(JSON.stringify({ workflow: workflowName, yamlPath, gateContext }, null, 2));
3100
3624
  process.exit(0);
3101
- }
3102
- let fallbackConfig;
3103
- let fallbackPhaseId;
3104
- for (const ph of phases.phases) {
3105
- if ("fallback" in ph && ph.fallback?.max_iterations_exceeded) {
3106
- fallbackConfig = ph.fallback.max_iterations_exceeded;
3107
- fallbackPhaseId = ph.id;
3108
- break;
3109
- }
3625
+ return;
3110
3626
  }
3111
3627
  try {
3112
3628
  const stagedOut = execSync("git status --porcelain", { encoding: "utf8" });
@@ -3122,22 +3638,15 @@ function registerExecuteTask(program2) {
3122
3638
  } catch (err2) {
3123
3639
  console.warn(t("execute_task.precommit_skipped", { message: err2.message }));
3124
3640
  }
3125
- const result = await runRouting(taskCtx, {
3126
- maxIterations: raw.maxIterations ?? 20,
3127
- ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {},
3128
- ...fallbackConfig ? { fallbackConfig } : {},
3129
- ...fallbackPhaseId ? { fallbackPhaseId } : {}
3130
- });
3131
- if ("aborted" in result) {
3132
- console.error(t("install.aborted", { reason: result.reason }));
3133
- process.exit(2);
3134
- }
3135
- if ("ok" in result && result.ok === false) {
3136
- console.error(`error: ${result.phase} \u2014 ${result.error.message}`);
3641
+ let result;
3642
+ try {
3643
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT2, gateContext });
3644
+ } catch (err2) {
3645
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
3137
3646
  process.exit(1);
3647
+ return;
3138
3648
  }
3139
- console.log(result.result);
3140
- process.exit(0);
3649
+ process.exit(result.status === "failed" ? 1 : 0);
3141
3650
  });
3142
3651
  }
3143
3652
  var DURATION_RE = /^(\d+)([dhmw])$/;
@@ -3543,7 +4052,7 @@ async function isPluginRegistered(pluginName) {
3543
4052
  return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
3544
4053
  }
3545
4054
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3546
- return new Promise((resolve12) => {
4055
+ return new Promise((resolve14) => {
3547
4056
  const isWin = process.platform === "win32";
3548
4057
  const child = isWin ? spawn("cmd.exe", ["/c", "claude", ...claudeArgs], { cwd, windowsHide: true }) : spawn("claude", claudeArgs, { cwd, shell: false });
3549
4058
  let stderr = "";
@@ -3552,15 +4061,15 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3552
4061
  });
3553
4062
  const timer = setTimeout(() => {
3554
4063
  child.kill("SIGKILL");
3555
- resolve12({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
4064
+ resolve14({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
3556
4065
  }, timeoutMs);
3557
4066
  child.on("error", (e) => {
3558
4067
  clearTimeout(timer);
3559
- resolve12({ exitCode: -1, stderr: `${stderr}${e.message}` });
4068
+ resolve14({ exitCode: -1, stderr: `${stderr}${e.message}` });
3560
4069
  });
3561
4070
  child.on("close", (code) => {
3562
4071
  clearTimeout(timer);
3563
- resolve12({ exitCode: code ?? -1, stderr });
4072
+ resolve14({ exitCode: code ?? -1, stderr });
3564
4073
  });
3565
4074
  });
3566
4075
  }
@@ -3739,10 +4248,10 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3739
4248
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
3740
4249
  stderr += chunk;
3741
4250
  });
3742
- return await new Promise((resolve12) => {
4251
+ return await new Promise((resolve14) => {
3743
4252
  const timer = setTimeout(() => {
3744
4253
  child.kill("SIGKILL");
3745
- resolve12({
4254
+ resolve14({
3746
4255
  ok: false,
3747
4256
  phase: "spawn",
3748
4257
  error: {
@@ -3757,7 +4266,7 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3757
4266
  }, effectiveTimeoutMs);
3758
4267
  child.on("error", (err2) => {
3759
4268
  clearTimeout(timer);
3760
- resolve12({
4269
+ resolve14({
3761
4270
  ok: false,
3762
4271
  phase: "spawn",
3763
4272
  error: {
@@ -3772,14 +4281,14 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3772
4281
  });
3773
4282
  child.on("close", (code) => {
3774
4283
  clearTimeout(timer);
3775
- resolve12({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
4284
+ resolve14({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3776
4285
  });
3777
4286
  });
3778
4287
  }
3779
4288
 
3780
4289
  // src/installers/gitCloneWithSetup.ts
3781
4290
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
3782
- return new Promise((resolve12) => {
4291
+ return new Promise((resolve14) => {
3783
4292
  const isWin = process.platform === "win32";
3784
4293
  const child = isWin ? spawn("cmd.exe", ["/c", "git", "rev-parse", "HEAD"], { cwd, windowsHide: true }) : spawn("git", ["rev-parse", "HEAD"], { cwd, shell: false });
3785
4294
  let stdout2 = "";
@@ -3788,15 +4297,15 @@ function gitRevParseHead(cwd, timeoutMs = 1e4) {
3788
4297
  });
3789
4298
  const timer = setTimeout(() => {
3790
4299
  child.kill("SIGKILL");
3791
- resolve12({ sha: "", exit: -1 });
4300
+ resolve14({ sha: "", exit: -1 });
3792
4301
  }, timeoutMs);
3793
4302
  child.on("error", () => {
3794
4303
  clearTimeout(timer);
3795
- resolve12({ sha: "", exit: -1 });
4304
+ resolve14({ sha: "", exit: -1 });
3796
4305
  });
3797
4306
  child.on("close", (code) => {
3798
4307
  clearTimeout(timer);
3799
- resolve12({ sha: stdout2.trim(), exit: code ?? -1 });
4308
+ resolve14({ sha: stdout2.trim(), exit: code ?? -1 });
3800
4309
  });
3801
4310
  });
3802
4311
  }
@@ -3967,16 +4476,16 @@ var installGitCloneWithSetup = async (ctx) => {
3967
4476
  function resolveEnvVars(value) {
3968
4477
  const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
3969
4478
  let resolved = value;
3970
- let match2;
4479
+ let match;
3971
4480
  pattern.lastIndex = 0;
3972
- while ((match2 = pattern.exec(value)) !== null) {
3973
- const name = match2[1];
4481
+ while ((match = pattern.exec(value)) !== null) {
4482
+ const name = match[1];
3974
4483
  if (name === void 0) continue;
3975
4484
  const v = process.env[name];
3976
4485
  if (v === void 0 || v === "") {
3977
4486
  return { ok: false, missing: name };
3978
4487
  }
3979
- resolved = resolved.replace(match2[0], v);
4488
+ resolved = resolved.replace(match[0], v);
3980
4489
  }
3981
4490
  return { ok: true, resolved };
3982
4491
  }
@@ -4508,16 +5017,6 @@ async function runInstall(manifest, opts) {
4508
5017
 
4509
5018
  // src/cli/install.ts
4510
5019
  init_path_guard();
4511
- function getPackageRoot() {
4512
- const thisFile = fileURLToPath(import.meta.url);
4513
- const thisDir = dirname(thisFile);
4514
- if (thisDir.endsWith("dist") || thisDir.replace(/\\/g, "/").endsWith("/dist")) {
4515
- return resolve(thisDir, "..");
4516
- }
4517
- return resolve(thisDir, "..", "..", "..");
4518
- }
4519
-
4520
- // src/cli/install.ts
4521
5020
  function formatError(e) {
4522
5021
  const head = `error: ${e.message}`;
4523
5022
  const where = e.path && e.path !== "/" ? `
@@ -4715,8 +5214,8 @@ function registerManifestAdd(program2) {
4715
5214
  process.exit(0);
4716
5215
  });
4717
5216
  }
4718
-
4719
- // src/cli/research.ts
5217
+ var PACKAGE_ROOT3 = getPackageRoot();
5218
+ var WORKFLOWS_DIR3 = join(PACKAGE_ROOT3, "workflows");
4720
5219
  function registerResearch(program2) {
4721
5220
  program2.command("research").description(
4722
5221
  "Run research workflow (search category sub-routing \u2192 spawn \u2192 verbatim COMPLETE; immediate by default \u2014 use --dry-run for preview)"
@@ -4725,48 +5224,30 @@ function registerResearch(program2) {
4725
5224
  console.error(t("research.require_query"));
4726
5225
  process.exit(2);
4727
5226
  }
4728
- const taskCtx = { task: raw.query, task_type: "search" };
5227
+ const yamlPath = await resolveWorkflowYaml("research", WORKFLOWS_DIR3);
5228
+ if (!yamlPath) {
5229
+ console.error(`error: workflows/research/workflow.yaml not found under ${WORKFLOWS_DIR3}`);
5230
+ process.exit(2);
5231
+ return;
5232
+ }
5233
+ const gateContext = {
5234
+ task: raw.query,
5235
+ ...raw.model ? { modelOverride: raw.model } : {}
5236
+ };
4729
5237
  if (raw.dryRun === true) {
4730
- const preview = await runRouting(taskCtx, {
4731
- skillsRoot: void 0,
4732
- // Stub spawn — dry-run never reaches it; explicit COMPLETE keeps shape happy.
4733
- spawn: async () => "dry-run preview\nCOMPLETE",
4734
- maxIterations: 1,
4735
- ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {}
4736
- });
4737
- if ("aborted" in preview) {
4738
- console.error(t("install.aborted", { reason: preview.reason }));
4739
- process.exit(2);
4740
- }
4741
- if ("ok" in preview && preview.ok === false) {
4742
- console.error(`error: ${preview.phase} \u2014 ${preview.error.message}`);
4743
- process.exit(1);
4744
- }
4745
- console.log(
4746
- t("research.dry_run.matched_rule", {
4747
- rule: preview.matchedRule?.id ?? "(fallback supervisor)"
4748
- })
4749
- );
4750
- console.log(t("research.dry_run.query", { query: raw.query }));
4751
- console.log(t("research.dry_run.run_hint"));
5238
+ console.log(JSON.stringify({ workflow: "research", yamlPath, gateContext }, null, 2));
4752
5239
  process.exit(0);
5240
+ return;
4753
5241
  }
4754
- const result = await runRouting(taskCtx, {
4755
- ...raw.model ? { agentOpts: { modelOverride: raw.model } } : {}
4756
- });
4757
- if ("aborted" in result) {
4758
- console.error(t("install.aborted", { reason: result.reason }));
4759
- process.exit(2);
4760
- }
4761
- if ("ok" in result && result.ok === false) {
4762
- console.error(`error: ${result.phase} \u2014 ${result.error.message}`);
4763
- if (result.phase === "install") {
4764
- console.error(t("research.install_fix_hint"));
4765
- }
5242
+ let result;
5243
+ try {
5244
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT3, gateContext });
5245
+ } catch (err2) {
5246
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
4766
5247
  process.exit(1);
5248
+ return;
4767
5249
  }
4768
- console.log(result.result);
4769
- process.exit(0);
5250
+ process.exit(result.status === "failed" ? 1 : 0);
4770
5251
  });
4771
5252
  }
4772
5253
 
@@ -4939,13 +5420,13 @@ function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills)
4939
5420
  var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
4940
5421
  function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
4941
5422
  const warningsSet = /* @__PURE__ */ new Set();
4942
- const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
5423
+ const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match, name) => {
4943
5424
  const cap = capabilities[name];
4944
5425
  if (!cap) {
4945
5426
  warningsSet.add(
4946
5427
  `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
4947
5428
  );
4948
- return match2;
5429
+ return match;
4949
5430
  }
4950
5431
  const { renderedCmd, warning } = resolveCapabilityCmd(
4951
5432
  cap,
@@ -5123,114 +5604,6 @@ async function atomicWrite2(path, content) {
5123
5604
  return `write ${path} failed: ${err2.message}`;
5124
5605
  }
5125
5606
  }
5126
- async function loadRolePrompts(workflowsDir) {
5127
- const path = join(workflowsDir, "role-prompts.yaml");
5128
- let raw;
5129
- try {
5130
- raw = await readFile(path, "utf8");
5131
- } catch {
5132
- return {};
5133
- }
5134
- const doc = parse(raw);
5135
- return doc?.prompts ?? {};
5136
- }
5137
- function generateCommandFile(name, prompt, capabilities, installedPlugins, installedUserSkills) {
5138
- const isMaster = prompt.is_master === true;
5139
- const primaryCmdLine = prompt.primary_cap && capabilities[prompt.primary_cap] ? `{{ capabilities.${prompt.primary_cap}.cmd }}` : "";
5140
- const checklistBlock = prompt.checklist.length ? prompt.checklist.map((item, i) => `> ${i + 1}. ${item}`).join("\n>\n") : "> (Master orchestrator \u2014 dispatches to per-sub-workflow slash commands listed below.)";
5141
- const fallbackPath = isMaster ? `**Fallback path** (when no slash command from the sub-list resolves): run each missing sub-workflow inline using its own role prompt (see \`~/.claude/skills/<sub-name>/SKILL.md\` for the per-sub fallback prompt). Do NOT skip stages silently \u2014 each sub either runs or is logged as "skipped: <reason>".` : [
5142
- `**Fallback path** (when the upstream isn't installed or returns no result): use the Task tool to spawn a general-purpose subagent with this prompt:`,
5143
- ``,
5144
- `> You are a **${prompt.specialist}**.`,
5145
- `>`,
5146
- `> **Mission**: ${prompt.responsibility.trim().replace(/\n/g, " ")}`,
5147
- `>`,
5148
- `> **Default-suspect mode**: assume the change is broken / risky / incomplete until proven otherwise. Cite \`file:line\` for every finding; do not generalize.`,
5149
- `>`,
5150
- `> **Review checklist**:`,
5151
- checklistBlock,
5152
- `>`,
5153
- `> **Output format**: structured report with severity-classified findings (${prompt.severity}). One finding per line: \`[severity] file:line \u2014 problem (one sentence); fix: suggested change\`. If no findings, say so explicitly. No preamble, no end-of-report summary.`,
5154
- ``,
5155
- `(The role prompt is self-contained \u2014 works even when the upstream \`${prompt.primary_cap || "specialist"}\` user-skill / plugin isn't installed.)`
5156
- ].join("\n");
5157
- const preferredPath = primaryCmdLine ? `**Preferred path** (when the upstream specialist is installed): use the SlashCommand tool to run \`${primaryCmdLine}\` \u2014 the upstream specialist takes over.` : `**Preferred path** (master orchestrator): dispatch to the per-sub-workflow slash commands in the order this stage prescribes. Each sub command is its own \`~/.claude/commands/<sub-name>.md\` and has its own dual-path fallback.`;
5158
- const rawBody = [
5159
- `# /${name}`,
5160
- ``,
5161
- prompt.description,
5162
- ``,
5163
- `## How to invoke`,
5164
- ``,
5165
- preferredPath,
5166
- ``,
5167
- fallbackPath,
5168
- ``,
5169
- `## Notes`,
5170
- ``,
5171
- `- This file (\`~/.claude/commands/${name}.md\`) is generated by \`harnessed setup\` from \`workflows/role-prompts.yaml\` + \`workflows/<stage>/<sub>/SKILL.md\`. To regenerate after a harnessed upgrade, re-run \`harnessed setup\`.`,
5172
- `- The companion \`~/.claude/skills/${name}/SKILL.md\` is the Skill-tool entry point (Claude loads it when triggers match \`trigger_phrases:\`). Both files carry the same dual-path instruction.`,
5173
- `- If your shell shows a \`\u26A0\uFE0F ... not installed\` warning from \`harnessed setup\` for this command, the upstream is missing on disk \u2014 install per the warning, OR rely on the fallback Task-spawn role prompt above (it does not require the upstream).`,
5174
- ``
5175
- ].join("\n");
5176
- const { body, warnings } = renderSkillBody(
5177
- rawBody,
5178
- capabilities,
5179
- installedPlugins,
5180
- installedUserSkills
5181
- );
5182
- const frontmatter = ["---", `description: ${JSON.stringify(prompt.description)}`, "---", ""].join(
5183
- "\n"
5184
- );
5185
- return { content: frontmatter + body, warnings };
5186
- }
5187
- async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists = existsSync) {
5188
- const results = [];
5189
- const aggregatedWarnings = /* @__PURE__ */ new Set();
5190
- for (const name of slashNames) {
5191
- const path = join(commandsDir, `${name}.md`);
5192
- const prompt = rolePrompts[name];
5193
- if (!prompt) {
5194
- results.push({
5195
- name,
5196
- path,
5197
- written: false,
5198
- warning: `no role-prompts.yaml entry for '${name}' \u2014 skipping commands/${name}.md generation`
5199
- });
5200
- aggregatedWarnings.add(`role-prompts.yaml missing entry for '${name}'`);
5201
- continue;
5202
- }
5203
- if (fileExists(path)) {
5204
- results.push({
5205
- name,
5206
- path,
5207
- written: false,
5208
- warning: `commands/${name}.md already exists \u2014 leaving user file unchanged`
5209
- });
5210
- continue;
5211
- }
5212
- const { content, warnings } = generateCommandFile(
5213
- name,
5214
- prompt,
5215
- capabilities,
5216
- installedPlugins,
5217
- installedUserSkills
5218
- );
5219
- try {
5220
- await writer(path, content);
5221
- results.push({ name, path, written: true });
5222
- } catch (e) {
5223
- results.push({
5224
- name,
5225
- path,
5226
- written: false,
5227
- warning: `write failed for commands/${name}.md: ${e.message}`
5228
- });
5229
- }
5230
- for (const w of warnings) aggregatedWarnings.add(w);
5231
- }
5232
- return { results, warnings: [...aggregatedWarnings] };
5233
- }
5234
5607
  async function loadCapabilities(workflowsDir) {
5235
5608
  const path = join(workflowsDir, "capabilities.yaml");
5236
5609
  const raw = await readFile(path, "utf8");
@@ -5865,7 +6238,7 @@ var uninstallNpmCli = async (ctx) => {
5865
6238
  const m = install.cmd.match(/npm\s+(?:install|i)\s+(?:-g\s+)?(\S+)/);
5866
6239
  const pkg = m?.[1] ?? ctx.manifest.metadata.upstream.source;
5867
6240
  const isWin = process.platform === "win32";
5868
- const result = await new Promise((resolve12) => {
6241
+ const result = await new Promise((resolve14) => {
5869
6242
  const child = isWin ? spawn("cmd.exe", ["/c", "npm", "uninstall", "-g", pkg], { windowsHide: true }) : spawn("npm", ["uninstall", "-g", pkg], { shell: false });
5870
6243
  let stderr = "";
5871
6244
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -5873,15 +6246,15 @@ var uninstallNpmCli = async (ctx) => {
5873
6246
  });
5874
6247
  const timer = setTimeout(() => {
5875
6248
  child.kill("SIGKILL");
5876
- resolve12({ exitCode: -1, stderr: `${stderr}[timeout]` });
6249
+ resolve14({ exitCode: -1, stderr: `${stderr}[timeout]` });
5877
6250
  }, 3e4);
5878
6251
  child.on("error", (e) => {
5879
6252
  clearTimeout(timer);
5880
- resolve12({ exitCode: -1, stderr: e.message });
6253
+ resolve14({ exitCode: -1, stderr: e.message });
5881
6254
  });
5882
6255
  child.on("close", (code) => {
5883
6256
  clearTimeout(timer);
5884
- resolve12({ exitCode: code ?? -1, stderr });
6257
+ resolve14({ exitCode: code ?? -1, stderr });
5885
6258
  });
5886
6259
  });
5887
6260
  if (result.exitCode !== 0) {
@@ -6030,6 +6403,7 @@ registerGc(program);
6030
6403
  registerResume(program);
6031
6404
  registerUninstall(program);
6032
6405
  registerSetup(program);
6406
+ registerRun(program);
6033
6407
  program.parse(process.argv);
6034
6408
  //# sourceMappingURL=cli.mjs.map
6035
6409
  //# sourceMappingURL=cli.mjs.map