harnessed 3.4.2 → 3.4.4

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 (35) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.mjs +1218 -733
  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 +15 -0
  8. package/workflows/capabilities.yaml +1 -1
  9. package/workflows/discuss/auto/SKILL.md +15 -2
  10. package/workflows/discuss/phase/SKILL.md +10 -8
  11. package/workflows/discuss/strategic/SKILL.md +11 -9
  12. package/workflows/discuss/subtask/SKILL.md +10 -8
  13. package/workflows/execute-task/SKILL.md +7 -6
  14. package/workflows/execute-task/workflow.yaml +93 -0
  15. package/workflows/plan/architecture/SKILL.md +10 -8
  16. package/workflows/plan/auto/SKILL.md +15 -2
  17. package/workflows/plan/phase/SKILL.md +10 -8
  18. package/workflows/research/SKILL.md +44 -2
  19. package/workflows/retro/SKILL.md +7 -14
  20. package/workflows/role-prompts.yaml +477 -0
  21. package/workflows/task/auto/SKILL.md +15 -2
  22. package/workflows/task/clarify/SKILL.md +7 -20
  23. package/workflows/task/code/SKILL.md +7 -20
  24. package/workflows/task/deliver/SKILL.md +8 -21
  25. package/workflows/task/test/SKILL.md +7 -20
  26. package/workflows/verify/auto/SKILL.md +14 -1
  27. package/workflows/verify/code-review/SKILL.md +8 -15
  28. package/workflows/verify/design/SKILL.md +7 -14
  29. package/workflows/verify/multispec/SKILL.md +8 -15
  30. package/workflows/verify/paranoid/SKILL.md +8 -15
  31. package/workflows/verify/progress/SKILL.md +7 -14
  32. package/workflows/verify/qa/SKILL.md +8 -15
  33. package/workflows/verify/security/SKILL.md +8 -15
  34. package/workflows/verify/simplify/SKILL.md +8 -15
  35. package/workflows/execute-task/phases.yaml +0 -73
package/dist/cli.mjs CHANGED
@@ -1,18 +1,19 @@
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';
8
8
  import { LineCounter, parseDocument, parse, isSeq, isScalar } from 'yaml';
9
- import { readFile, readdir, unlink, writeFile, stat, rm, cp, access, mkdir, rename } from 'fs/promises';
9
+ import { readFile, readdir, unlink, writeFile, stat, rm, cp, mkdir, access, rename } from 'fs/promises';
10
10
  import lockfile from 'proper-lockfile';
11
11
  import { Command } from 'commander';
12
12
  import { Ajv } from 'ajv';
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.2"};
856
+ version: "3.4.4"};
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,19 @@ 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
+ },
2653
+ required: ["status", "phase"]
2654
+ };
2655
+
2656
+ // src/workflow/lib/sdkReconcile.ts
2631
2657
  function toSdkAgentDefinition(def) {
2632
2658
  return {
2633
2659
  description: def.description,
@@ -2664,7 +2690,7 @@ ${def.criticalSystemReminder_EXPERIMENTAL}`);
2664
2690
  return parts.join("\n\n");
2665
2691
  }
2666
2692
 
2667
- // src/routing/lib/sdkSpawn.ts
2693
+ // src/workflow/lib/sdkSpawn.ts
2668
2694
  var SpawnFailError = class extends Error {
2669
2695
  constructor(lastMessage) {
2670
2696
  super("sdkSpawn produced no result message");
@@ -2704,146 +2730,30 @@ async function sdkSpawn(def, opts) {
2704
2730
  };
2705
2731
  return JSON.stringify(envelope);
2706
2732
  }
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
2733
 
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
-
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
- };
2734
+ // src/workflow/interpolate.ts
2735
+ var InterpolationError = class extends Error {
2736
+ constructor(message) {
2737
+ super(message);
2738
+ this.name = "InterpolationError";
2741
2739
  }
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)`
2740
+ };
2741
+ var STRICT = /\{\{\s*(\w+)\s*\}\}/g;
2742
+ var ANY_TEMPLATE = /\{\{[^}]*\}\}/;
2743
+ function interpolate(template, vars) {
2744
+ const out = template.replace(STRICT, (_m, name) => {
2745
+ const v = vars[name];
2746
+ if (v === void 0) {
2747
+ throw new InterpolationError(
2748
+ `undefined template variable '${name}' (template excerpt: ${template.slice(0, 80)})`
2761
2749
  );
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
2750
  }
2751
+ return v;
2818
2752
  });
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 };
2753
+ if (ANY_TEMPLATE.test(out)) {
2754
+ throw new InterpolationError(`unsupported template syntax in: ${out.slice(0, 80)}`);
2846
2755
  }
2756
+ return out;
2847
2757
  }
2848
2758
  var ModelTier = Type.Union([
2849
2759
  Type.Literal("haiku"),
@@ -3020,7 +2930,7 @@ var WorkflowPhaseV3 = Type.Object(
3020
2930
  },
3021
2931
  { additionalProperties: false }
3022
2932
  );
3023
- Type.Object(
2933
+ var WorkflowSchemaV3 = Type.Object(
3024
2934
  {
3025
2935
  schema_version: Type.Literal(SCHEMA_VERSIONS.workflow_v3),
3026
2936
  workflow: Type.String({ minLength: 1 }),
@@ -3049,8 +2959,12 @@ var PhasesValidationError = class extends Error {
3049
2959
  function loadPhases(yamlPath, vars) {
3050
2960
  const raw = readFileSync(yamlPath, "utf8");
3051
2961
  const parsed = parse(raw);
3052
- const isV2 = parsed?.schema_version === "harnessed.workflow.v2";
3053
- if (isV2) {
2962
+ const version = parsed?.schema_version;
2963
+ if (version === "harnessed.workflow.v3") {
2964
+ if (!Value.Check(WorkflowSchemaV3, parsed)) {
2965
+ throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV3, parsed)]);
2966
+ }
2967
+ } else if (version === "harnessed.workflow.v2") {
3054
2968
  if (!Value.Check(WorkflowSchemaV2, parsed)) {
3055
2969
  throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV2, parsed)]);
3056
2970
  }
@@ -3060,10 +2974,590 @@ function loadPhases(yamlPath, vars) {
3060
2974
  }
3061
2975
  }
3062
2976
  const validated = parsed;
2977
+ if (vars && validated.phases) {
2978
+ for (const ph of validated.phases) {
2979
+ if (ph.invokes) ph.invokes = interpolate(ph.invokes, vars);
2980
+ }
2981
+ }
3063
2982
  return validated;
3064
2983
  }
2984
+ function resolveMasterYamlPath(masterName, packageRoot) {
2985
+ return masterName === "auto" ? resolve(packageRoot, "workflows", "auto", "workflow.yaml") : resolve(packageRoot, "workflows", masterName, "auto", "workflow.yaml");
2986
+ }
2987
+ function resolveSubYamlPath(masterName, subName, packageRoot) {
2988
+ if (masterName === "auto") {
2989
+ if (subName === "research" || subName === "retro") {
2990
+ return resolve(packageRoot, "workflows", subName, "workflow.yaml");
2991
+ }
2992
+ return resolve(packageRoot, "workflows", subName, "auto", "workflow.yaml");
2993
+ }
2994
+ return resolve(packageRoot, "workflows", masterName, subName, "workflow.yaml");
2995
+ }
2996
+ var defaultSpawnDriver = async (masterName, subName, _context, packageRoot) => {
2997
+ const subYamlPath = resolveSubYamlPath(masterName, subName, packageRoot);
2998
+ try {
2999
+ await runWorkflow(subYamlPath, {}, { packageRoot, gateContext: _context });
3000
+ } catch (err2) {
3001
+ console.warn(
3002
+ `\u26A0\uFE0F master spawnSubWorkflow Path A failed for ${masterName}/${subName} (${err2.message});Path B sub-shell fallback deferred T3.5.W2.1.`
3003
+ );
3004
+ }
3005
+ };
3006
+ var defaultPauseFn = async (stageName) => {
3007
+ const readline2 = await import('readline/promises');
3008
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3009
+ try {
3010
+ await rl.question(
3011
+ `
3012
+ [--staged] Stage '${stageName}' complete. Press Enter to continue (Ctrl+C to abort)... `
3013
+ );
3014
+ } finally {
3015
+ rl.close();
3016
+ }
3017
+ };
3018
+ var defaultAssessComplexity = async (_taskDescription) => {
3019
+ return "medium";
3020
+ };
3021
+ var defaultPromptUserUnderstanding = async (prompter = defaultPrompter) => {
3022
+ const answer = await prompter(
3023
+ "\n[Phase 0.5] \u5BF9\u9700\u6C42\u6709\u6E05\u6670\u8BA4\u77E5\u5417? [Y/n] (n = \u5148\u8DD1 /research \u591A\u6E90\u8C03\u7814): "
3024
+ );
3025
+ const a = answer.trim().toLowerCase();
3026
+ return !(a === "n" || a === "no");
3027
+ };
3028
+ var defaultPrompter = async (question) => {
3029
+ const readline2 = await import('readline/promises');
3030
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3031
+ try {
3032
+ return await rl.question(question);
3033
+ } finally {
3034
+ rl.close();
3035
+ }
3036
+ };
3037
+ async function runAutoPreFlight(ctx, opts) {
3038
+ const assess = opts.assessComplexity ?? defaultAssessComplexity;
3039
+ const prompter = opts.prompter ?? defaultPrompter;
3040
+ const taskDesc = typeof ctx.task_description === "string" ? ctx.task_description : "";
3041
+ const size = await assess(taskDesc);
3042
+ console.log(`[/auto Phase 0] Complexity assessment: ${size}`);
3043
+ let nextOpts = opts;
3044
+ if (size === "large") {
3045
+ const a = (await prompter(
3046
+ "\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]: "
3047
+ )).trim().toLowerCase();
3048
+ if (a === "n" || a === "no") {
3049
+ console.log(
3050
+ "\n[/auto Phase 0] User declined --staged. Aborting auto mode \u2014 \u5EFA\u8BAE\u624B\u52A8 `/discuss` \u542F\u52A8\u3002"
3051
+ );
3052
+ return { proceed: false, opts };
3053
+ }
3054
+ console.log("[/auto Phase 0] Switching to --staged mode.");
3055
+ nextOpts = { ...opts, pauseBetweenStages: true };
3056
+ }
3057
+ const promptUnderstanding = opts.promptUserUnderstanding ?? defaultPromptUserUnderstanding;
3058
+ const isClear = await promptUnderstanding(prompter);
3059
+ ctx.user_understanding_unclear = !isClear;
3060
+ console.log(
3061
+ `[/auto Phase 0.5] user_understanding_unclear=${!isClear} (research will ${isClear ? "skip" : "fire"})`
3062
+ );
3063
+ return { proceed: true, opts: nextOpts };
3064
+ }
3065
+ function emitGateTransparency(masterName, totalGates, gateEvalled) {
3066
+ console.log(`[${masterName} master] Evaluating ${totalGates} sub-workflow gates:`);
3067
+ for (const g of gateEvalled) {
3068
+ const mark = g.passes ? "\u2713" : "\u2298";
3069
+ const tail = g.passes ? g.clause.gate ? ` (${g.clause.gate} == true)` : " (unconditional)" : ` skipped \u2014 ${g.reason}`;
3070
+ console.log(` ${mark} ${g.clause.sub}${tail}`);
3071
+ }
3072
+ }
3073
+ async function maybeArbitrate(firedClauses, packageRoot) {
3074
+ if (firedClauses.length <= 1) return;
3075
+ const firedCaps = firedClauses.map((c) => ({ name: c.sub, tier: c.sub }));
3076
+ try {
3077
+ await arbitrateBeforeSpawn(firedCaps, packageRoot);
3078
+ console.warn(
3079
+ `\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)`
3080
+ );
3081
+ } catch (e) {
3082
+ console.warn(`\u26A0\uFE0F arbitrate failed (${e.message}) \u2014 proceeding default order`);
3083
+ }
3084
+ }
3085
+
3086
+ // src/workflow/masterOrchestrator.ts
3087
+ async function runMasterOrchestrator(masterName, context, packageRoot, spawnDriver = defaultSpawnDriver, opts = {}) {
3088
+ const yamlPath = resolveMasterYamlPath(masterName, packageRoot);
3089
+ const raw = await readFile(yamlPath, "utf8");
3090
+ const parsed = parse(raw);
3091
+ if (!Value.Check(WorkflowSchemaV3, parsed)) {
3092
+ const errors = [...Value.Errors(WorkflowSchemaV3, parsed)].slice(0, 3).map((e) => `${e.path} ${e.message}`).join("; ");
3093
+ throw new Error(`Invalid master workflow.yaml at ${yamlPath}: ${errors}`);
3094
+ }
3095
+ const master = parsed;
3096
+ if (!master.delegates_to || master.delegates_to.length === 0) {
3097
+ throw new Error(`Master workflow ${masterName} missing delegates_to`);
3098
+ }
3099
+ let effectiveOpts = opts;
3100
+ const preflightActive = masterName === "auto" && (opts.assessComplexity !== void 0 || opts.prompter !== void 0 || opts.promptUserUnderstanding !== void 0);
3101
+ if (preflightActive) {
3102
+ const pre = await runAutoPreFlight(context, opts);
3103
+ if (!pre.proceed) {
3104
+ console.log(`[${masterName} master] Aborted by user (complexity gate decline).`);
3105
+ return { master: masterName, fired: [], skipped: master.delegates_to.map((c) => c.sub) };
3106
+ }
3107
+ effectiveOpts = pre.opts;
3108
+ }
3109
+ const gateEvalled = [];
3110
+ for (const clause of master.delegates_to) {
3111
+ if (!clause.gate) {
3112
+ gateEvalled.push({ clause, passes: true });
3113
+ continue;
3114
+ }
3115
+ try {
3116
+ const passes = await resolveJudgmentGate(clause.gate, context, packageRoot);
3117
+ gateEvalled.push({
3118
+ clause,
3119
+ passes,
3120
+ reason: passes ? void 0 : `gate ${clause.gate} = false`
3121
+ });
3122
+ } catch (e) {
3123
+ gateEvalled.push({
3124
+ clause,
3125
+ passes: false,
3126
+ reason: `gate eval error: ${e.message}`
3127
+ });
3128
+ }
3129
+ }
3130
+ emitGateTransparency(masterName, master.delegates_to.length, gateEvalled);
3131
+ const PARALLEL_MID_ANCHOR = 50;
3132
+ const firedClauses = gateEvalled.filter((g) => g.passes).map((g) => g.clause);
3133
+ const serialLeading = firedClauses.filter((c) => c.mode === "serial" && (c.order ?? 0) < PARALLEL_MID_ANCHOR).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
3134
+ const serialTrailing = firedClauses.filter((c) => c.mode === "serial" && (c.order ?? 0) >= PARALLEL_MID_ANCHOR).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
3135
+ const parallelClauses = firedClauses.filter((c) => (c.mode ?? "parallel") === "parallel");
3136
+ const serialN = serialLeading.length + serialTrailing.length;
3137
+ const parallelN = parallelClauses.length;
3138
+ await maybeArbitrate(firedClauses, packageRoot);
3139
+ const modeLabel = serialN > 0 && parallelN > 0 ? "serial+parallel" : serialN > 0 ? "serial" : "parallel";
3140
+ console.log(`Firing ${firedClauses.length} sub in ${modeLabel}:`);
3141
+ const fired = [];
3142
+ const pauseHook = effectiveOpts.pauseBetweenStages ? effectiveOpts.pauseFn ?? defaultPauseFn : void 0;
3143
+ for (const clause of serialLeading) {
3144
+ console.log(` \u2192 ${clause.sub} (serial order=${clause.order ?? 0})`);
3145
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3146
+ fired.push(clause.sub);
3147
+ if (pauseHook) await pauseHook(clause.sub);
3148
+ }
3149
+ const parallelResults = await Promise.allSettled(
3150
+ parallelClauses.map(async (clause) => {
3151
+ console.log(` \u2192 ${clause.sub} (parallel)`);
3152
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3153
+ return clause.sub;
3154
+ })
3155
+ );
3156
+ for (const r of parallelResults) {
3157
+ if (r.status === "fulfilled") fired.push(r.value);
3158
+ }
3159
+ for (const clause of serialTrailing) {
3160
+ console.log(` \u2192 ${clause.sub} (serial order=${clause.order ?? 0})`);
3161
+ await spawnDriver(masterName, clause.sub, context, packageRoot);
3162
+ fired.push(clause.sub);
3163
+ if (pauseHook) await pauseHook(clause.sub);
3164
+ }
3165
+ const skipped = gateEvalled.filter((g) => !g.passes).map((g) => g.clause.sub);
3166
+ console.log(`[${masterName} master] Complete: ${fired.length} fired, ${skipped.length} skipped.`);
3167
+ return { master: masterName, fired, skipped };
3168
+ }
3169
+
3170
+ // src/workflow/run.ts
3171
+ var MASTER_NAMES = ["discuss", "plan", "task", "verify", "auto"];
3172
+ var RALPH_DEFAULT_MAX_ITER = 20;
3173
+ var RALPH_HARD_UPPER_LIMIT = 100;
3174
+ function buildAgentDef(skillName, rolePrompts, workflowName, modelTierOverride) {
3175
+ const rp = rolePrompts?.[skillName] ?? (workflowName ? rolePrompts?.[workflowName] : void 0);
3176
+ if (!rp) {
3177
+ return {
3178
+ description: `harnessed workflow phase: ${skillName}`,
3179
+ prompt: `You are executing the '${skillName}' workflow phase. Follow the phase intent and emit a structured COMPLETE signal when done.`,
3180
+ ...modelTierOverride ? { model: modelTierOverride } : {}
3181
+ };
3182
+ }
3183
+ const checklist = rp.checklist.length ? `
3184
+
3185
+ Checklist:
3186
+ ${rp.checklist.map((c, i) => ` ${i + 1}. ${c}`).join("\n")}` : "";
3187
+ const prompt = [
3188
+ `You are a ${rp.specialist}.`,
3189
+ ``,
3190
+ rp.responsibility.trim(),
3191
+ checklist,
3192
+ ``,
3193
+ `Severity scale: ${rp.severity}`,
3194
+ ``,
3195
+ `Emit a structured COMPLETE signal when done.`
3196
+ ].join("\n");
3197
+ return {
3198
+ description: rp.description,
3199
+ prompt,
3200
+ ...modelTierOverride ? { model: modelTierOverride } : {}
3201
+ };
3202
+ }
3203
+ function isRalphLoopOptIn(phase) {
3204
+ if (!phase || typeof phase !== "object") return false;
3205
+ const p4 = phase;
3206
+ if (p4.max_iterations !== void 0 && p4.max_iterations !== null) return true;
3207
+ if (p4.upstream === "ralph-loop") return true;
3208
+ const fb = p4.fallback;
3209
+ if (fb?.max_iterations_exceeded !== void 0) return true;
3210
+ return false;
3211
+ }
3212
+ function resolveMaxIterations(phase, gateContext) {
3213
+ const fromCli = typeof gateContext.maxIterations === "number" ? gateContext.maxIterations : void 0;
3214
+ let fromYaml;
3215
+ if (phase && typeof phase === "object") {
3216
+ const raw = phase.max_iterations;
3217
+ if (typeof raw === "number") fromYaml = raw;
3218
+ else if (typeof raw === "string") {
3219
+ const n = Number.parseInt(raw, 10);
3220
+ if (Number.isFinite(n) && n > 0) fromYaml = n;
3221
+ }
3222
+ }
3223
+ const chosen = fromCli ?? fromYaml ?? RALPH_DEFAULT_MAX_ITER;
3224
+ return Math.min(Math.max(1, chosen), RALPH_HARD_UPPER_LIMIT);
3225
+ }
3226
+ var _dispatchSkillStub = {
3227
+ fn: async (skillName, phase, opts) => {
3228
+ const optIn = isRalphLoopOptIn(phase);
3229
+ const spawnOnce = async (resumeSessionId, onSessionId) => sdkSpawn(
3230
+ buildAgentDef(skillName, opts?.rolePrompts, opts?.workflowName, opts?.modelTierOverride),
3231
+ {
3232
+ expertName: skillName,
3233
+ ...resumeSessionId ? { resumeSessionId } : {},
3234
+ ...onSessionId ? { onSessionId } : {}
3235
+ }
3236
+ );
3237
+ let envelopeJson;
3238
+ try {
3239
+ if (optIn) {
3240
+ const maxIter = opts?.maxIter ?? RALPH_DEFAULT_MAX_ITER;
3241
+ envelopeJson = await ralphLoopWrap(spawnOnce, maxIter);
3242
+ } else {
3243
+ envelopeJson = await spawnOnce();
3244
+ }
3245
+ } catch (err2) {
3246
+ if (err2 instanceof MaxIterationsExceededError && opts?.fallback) {
3247
+ handleMaxIterationsExceeded(err2, opts.fallback, {
3248
+ subtaskSummary: `phase ${skillName}`,
3249
+ // Phase 4 — plumbed actual workflow name (was hardcoded 'harnessed-run'
3250
+ // pre-Phase 4); falls back to literal when opts.workflowName absent
3251
+ // (preserves Phase 3 behavior for callers that don't pass workflowName).
3252
+ workflowName: opts.workflowName ?? "harnessed-run",
3253
+ phaseId: skillName,
3254
+ maxIterations: opts?.maxIter ?? RALPH_DEFAULT_MAX_ITER
3255
+ });
3256
+ }
3257
+ return {
3258
+ status: "fail",
3259
+ output: err2 instanceof MaxIterationsExceededError ? `ralph-loop max-iterations exceeded (${err2.iterations}) for ${skillName}` : `sdkSpawn failed for ${skillName}: ${err2.message}`
3260
+ };
3261
+ }
3262
+ const env = JSON.parse(envelopeJson);
3263
+ const status = env.structured_output?.status === "COMPLETE" || env.subtype === "success" ? "ok" : "fail";
3264
+ return {
3265
+ status,
3266
+ output: env.text ?? env.result ?? "",
3267
+ ...env.structured_output?.status ? { decision: env.structured_output.status } : {}
3268
+ };
3269
+ }
3270
+ };
3271
+ async function runWorkflow(yamlPath, vars, opts = {}) {
3272
+ const parsed = loadPhases(yamlPath, vars);
3273
+ const yamlDir = dirname(resolve(yamlPath));
3274
+ const inferredRoot = resolve(yamlDir, "..", "..");
3275
+ const packageRoot = opts.packageRoot ?? inferredRoot;
3276
+ const gateContext = { ...opts.gateContext ?? {} };
3277
+ let rolePrompts = {};
3278
+ try {
3279
+ rolePrompts = await loadRolePrompts(join(packageRoot, "workflows"));
3280
+ } catch (err2) {
3281
+ console.warn(
3282
+ `\u26A0\uFE0F loadRolePrompts failed (${err2.message}); proceeding without role-prompt enrichment (ADR 0029 fail-soft).`
3283
+ );
3284
+ }
3285
+ const workflowName = parsed.workflow;
3286
+ const isMaster = "delegates_to" in parsed && Array.isArray(parsed.delegates_to) && parsed.delegates_to.length > 0 && MASTER_NAMES.includes(workflowName);
3287
+ if (isMaster) {
3288
+ const r = await runMasterOrchestrator(workflowName, gateContext, packageRoot);
3289
+ return {
3290
+ status: "complete",
3291
+ phasesRun: r.fired.length,
3292
+ ...r.skipped.length > 0 ? { skippedPhases: r.skipped } : {}
3293
+ };
3294
+ }
3295
+ const disciplinesApplied = "disciplines_applied" in parsed && Array.isArray(parsed.disciplines_applied) ? parsed.disciplines_applied : void 0;
3296
+ try {
3297
+ const disciplines = await loadDisciplinesForPhase(disciplinesApplied, packageRoot);
3298
+ gateContext.disciplines = disciplines;
3299
+ } catch (err2) {
3300
+ console.warn(
3301
+ `\u26A0\uFE0F loadDisciplinesForPhase failed (${err2.message}); proceeding without disciplines map (sister ADR 0029 fail-soft).`
3302
+ );
3303
+ }
3304
+ const skippedPhases = [];
3305
+ const phases = parsed.phases ?? [];
3306
+ for (let i = 0; i < phases.length; i++) {
3307
+ const ph = phases[i];
3308
+ if (!ph) continue;
3309
+ await activatePhase(ph.id);
3310
+ const invokesTools = "invokes_tools" in ph && Array.isArray(ph.invokes_tools) ? ph.invokes_tools : void 0;
3311
+ if (invokesTools && invokesTools.length > 1) {
3312
+ try {
3313
+ const firedCaps = invokesTools.map((c) => ({ name: c.tool, tier: c.tool }));
3314
+ await arbitrateBeforeSpawn(firedCaps, packageRoot);
3315
+ } catch (err2) {
3316
+ console.warn(
3317
+ `\u26A0\uFE0F phase ${ph.id} before-spawn arbitrate failed (${err2.message}); proceeding default tool order (K14 warn-not-halt).`
3318
+ );
3319
+ }
3320
+ }
3321
+ if (await isVetoed()) {
3322
+ await pause();
3323
+ return { status: "paused-veto", phasesRun: i, lastPhaseId: ph.id };
3324
+ }
3325
+ if ("gate" in ph && ph.gate) {
3326
+ let fires = true;
3327
+ try {
3328
+ fires = await resolveJudgmentGate(ph.gate, gateContext, packageRoot);
3329
+ } catch (err2) {
3330
+ console.warn(
3331
+ `\u26A0\uFE0F phase ${ph.id} gate ${ph.gate} eval failed (${err2.message}); proceeding with phase as if gate fired=true (ADR 0029 fail-soft).`
3332
+ );
3333
+ }
3334
+ if (!fires) {
3335
+ skippedPhases.push(ph.id);
3336
+ await completePhase({
3337
+ phaseId: ph.id,
3338
+ lastTask: `phase ${ph.id} skipped: gate ${ph.gate} evaluated false`
3339
+ });
3340
+ continue;
3341
+ }
3342
+ }
3343
+ const skillName = "skills" in ph && ph.skills?.[0] || ph.id;
3344
+ const maxIter = resolveMaxIterations(ph, gateContext);
3345
+ const fallback = "fallback" in ph && ph.fallback?.max_iterations_exceeded ? ph.fallback.max_iterations_exceeded : void 0;
3346
+ const r = await _dispatchSkillStub.fn(skillName, ph, {
3347
+ maxIter,
3348
+ ...fallback ? { fallback } : {},
3349
+ workflowName,
3350
+ rolePrompts,
3351
+ ...typeof gateContext.modelTierOverride === "string" ? { modelTierOverride: gateContext.modelTierOverride } : {}
3352
+ });
3353
+ if (r.status !== "ok") {
3354
+ return { status: "failed", phasesRun: i, lastPhaseId: ph.id };
3355
+ }
3356
+ if (r.target === "chat") {
3357
+ try {
3358
+ await runAfterOutputHook({
3359
+ responseText: r.output,
3360
+ responseTarget: "chat",
3361
+ userRequestedEmoji: false,
3362
+ packageRoot
3363
+ });
3364
+ } catch (err2) {
3365
+ console.warn(
3366
+ `\u26A0\uFE0F phase ${ph.id} after-output hook failed (${err2.message}); proceeding (ADR 0029 fail-soft).`
3367
+ );
3368
+ }
3369
+ }
3370
+ if (r.triggers_commit === true) {
3371
+ try {
3372
+ await runBeforeCommitHook({
3373
+ changedFiles: [],
3374
+ cmdArgs: [],
3375
+ packageRoot,
3376
+ cmdType: "git-commit",
3377
+ hasUserApproval: false
3378
+ });
3379
+ } catch (err2) {
3380
+ console.warn(
3381
+ `\u26A0\uFE0F phase ${ph.id} before-commit hook failed (${err2.message}); proceeding (ADR 0029 fail-soft).`
3382
+ );
3383
+ }
3384
+ }
3385
+ await completePhase({
3386
+ phaseId: ph.id,
3387
+ lastTask: `phase ${ph.id} complete: ${r.output}`
3388
+ });
3389
+ }
3390
+ return {
3391
+ status: "complete",
3392
+ phasesRun: phases.length,
3393
+ ...skippedPhases.length > 0 ? { skippedPhases } : {}
3394
+ };
3395
+ }
3396
+ function getPackageRoot() {
3397
+ const thisFile = fileURLToPath(import.meta.url);
3398
+ const thisDir = dirname(thisFile);
3399
+ if (thisDir.endsWith("dist") || thisDir.replace(/\\/g, "/").endsWith("/dist")) {
3400
+ return resolve(thisDir, "..");
3401
+ }
3402
+ return resolve(thisDir, "..", "..", "..");
3403
+ }
3404
+ var PACKAGE_ROOT = getPackageRoot();
3405
+ var WORKFLOWS_DIR = join(PACKAGE_ROOT, "workflows");
3406
+ var _autoChainCache = null;
3407
+ var _autoChainLoadFailed = false;
3408
+ function registerRun(program2) {
3409
+ program2.command("run").description(
3410
+ "Run a harnessed workflow (master orchestrator or sub-workflow). Slash commands invoke via this subcommand."
3411
+ ).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(
3412
+ "--max-iterations <n>",
3413
+ "ralph-loop max iter (default 20; honored Phase 3 onward)",
3414
+ (v) => parseInt(v, 10)
3415
+ ).option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").option(
3416
+ "--dry-run",
3417
+ "validate yaml load + walk runtime without spawning (Phase 1 default for verification)"
3418
+ ).option("--staged", "/auto super-master: pause between stages for user review").option("--list", "print all known workflow names and exit").action(async (name, raw) => {
3419
+ if (raw.list) {
3420
+ const names = await listWorkflowNames(WORKFLOWS_DIR);
3421
+ for (const n of names) console.log(n);
3422
+ process.exit(0);
3423
+ }
3424
+ if (!name) {
3425
+ console.error("error: workflow name required (or pass --list to enumerate)");
3426
+ process.exit(2);
3427
+ }
3428
+ let task = "";
3429
+ if (typeof raw.task === "string") {
3430
+ task = raw.task;
3431
+ } else if (raw.taskStdin) {
3432
+ task = await readStdinToEnd();
3433
+ }
3434
+ const yamlPath = await resolveWorkflowYaml(name, WORKFLOWS_DIR);
3435
+ if (!yamlPath) {
3436
+ console.error(
3437
+ `error: workflow '${name}' not found under workflows/. Run \`harnessed run --list\` to enumerate.`
3438
+ );
3439
+ process.exit(2);
3440
+ }
3441
+ const gateContext = {
3442
+ task,
3443
+ ...raw.model ? { modelOverride: raw.model } : {},
3444
+ ...raw.maxIterations ? { maxIterations: raw.maxIterations } : {},
3445
+ ...raw.staged ? { staged: true } : {}
3446
+ };
3447
+ if (raw.dryRun) {
3448
+ console.log(JSON.stringify({ workflow: name, yamlPath, gateContext }, null, 2));
3449
+ process.exit(0);
3450
+ }
3451
+ let result;
3452
+ try {
3453
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT, gateContext });
3454
+ } catch (err2) {
3455
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
3456
+ process.exit(1);
3457
+ return;
3458
+ }
3459
+ const hint = await getNextHint(name);
3460
+ process.stderr.write(`[stage ${name} ${result.status}]
3461
+ `);
3462
+ if (hint) {
3463
+ process.stderr.write(`Next stage: harnessed run ${hint}
3464
+ (In Claude Code: /${hint})
3465
+ `);
3466
+ }
3467
+ process.exit(result.status === "failed" ? 1 : 0);
3468
+ });
3469
+ }
3470
+ async function resolveWorkflowYaml(name, workflowsDir) {
3471
+ const tier1 = join(workflowsDir, name, "workflow.yaml");
3472
+ if (existsSync(tier1)) return tier1;
3473
+ const tier2 = join(workflowsDir, name, "auto", "workflow.yaml");
3474
+ if (existsSync(tier2)) return tier2;
3475
+ const dashIdx = name.indexOf("-");
3476
+ if (dashIdx > 0) {
3477
+ const stage = name.slice(0, dashIdx);
3478
+ const sub = name.slice(dashIdx + 1);
3479
+ const tier3 = join(workflowsDir, stage, sub, "workflow.yaml");
3480
+ if (existsSync(tier3)) return tier3;
3481
+ }
3482
+ return null;
3483
+ }
3484
+ async function listWorkflowNames(workflowsDir) {
3485
+ const names = [];
3486
+ const entries = await readdir(workflowsDir);
3487
+ for (const e of entries.sort()) {
3488
+ const p4 = join(workflowsDir, e);
3489
+ const s = await stat(p4).catch(() => null);
3490
+ if (!s?.isDirectory()) continue;
3491
+ if (await fileExists(join(p4, "workflow.yaml"))) {
3492
+ names.push(e);
3493
+ continue;
3494
+ }
3495
+ if (await fileExists(join(p4, "auto", "workflow.yaml"))) {
3496
+ names.push(e);
3497
+ const subs = await readdir(p4).catch(() => []);
3498
+ for (const sub of subs.sort()) {
3499
+ if (sub === "auto") continue;
3500
+ if (await fileExists(join(p4, sub, "workflow.yaml"))) {
3501
+ names.push(`${e}-${sub}`);
3502
+ }
3503
+ }
3504
+ }
3505
+ }
3506
+ return names;
3507
+ }
3508
+ async function fileExists(path) {
3509
+ return stat(path).then(() => true).catch(() => false);
3510
+ }
3511
+ async function readStdinToEnd() {
3512
+ const chunks = [];
3513
+ for await (const chunk of process.stdin) {
3514
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3515
+ }
3516
+ return Buffer.concat(chunks).toString("utf8").trim();
3517
+ }
3518
+ function loadAutoChain() {
3519
+ if (_autoChainCache !== null) return _autoChainCache;
3520
+ if (_autoChainLoadFailed) return [];
3521
+ try {
3522
+ const yamlPath = join(WORKFLOWS_DIR, "auto", "workflow.yaml");
3523
+ const parsed = loadPhases(yamlPath);
3524
+ const delegates = "delegates_to" in parsed && Array.isArray(parsed.delegates_to) ? parsed.delegates_to : [];
3525
+ const sorted = [...delegates].sort((a, b) => {
3526
+ const ao = typeof a.order === "number" ? a.order : Number.MAX_SAFE_INTEGER;
3527
+ const bo = typeof b.order === "number" ? b.order : Number.MAX_SAFE_INTEGER;
3528
+ return ao - bo;
3529
+ });
3530
+ _autoChainCache = sorted.map((d) => d.sub).filter((s) => typeof s === "string");
3531
+ return _autoChainCache;
3532
+ } catch (err2) {
3533
+ _autoChainLoadFailed = true;
3534
+ process.stderr.write(`\u26A0\uFE0F getNextHint failed (${err2.message}); skipping hint.
3535
+ `);
3536
+ return [];
3537
+ }
3538
+ }
3539
+ async function getNextHint(workflowName) {
3540
+ if (workflowName === "auto") return null;
3541
+ const chain = loadAutoChain();
3542
+ if (chain.length === 0) return null;
3543
+ const directIdx = chain.indexOf(workflowName);
3544
+ if (directIdx >= 0) {
3545
+ return directIdx + 1 < chain.length ? chain[directIdx + 1] ?? null : null;
3546
+ }
3547
+ const dashIdx = workflowName.indexOf("-");
3548
+ if (dashIdx > 0) {
3549
+ const parentStage = workflowName.slice(0, dashIdx);
3550
+ const parentIdx = chain.indexOf(parentStage);
3551
+ if (parentIdx >= 0) {
3552
+ return parentIdx + 1 < chain.length ? chain[parentIdx + 1] ?? null : null;
3553
+ }
3554
+ }
3555
+ return null;
3556
+ }
3065
3557
 
3066
3558
  // src/cli/execute-task.ts
3559
+ var PACKAGE_ROOT2 = getPackageRoot();
3560
+ var WORKFLOWS_DIR2 = join(PACKAGE_ROOT2, "workflows");
3067
3561
  function registerExecuteTask(program2) {
3068
3562
  program2.command("execute-task").description(
3069
3563
  "Run execute-task workflow (4-phase chain \u2192 ralph-loop COMPLETE; immediate by default \u2014 use --dry-run for preview)"
@@ -3073,40 +3567,27 @@ function registerExecuteTask(program2) {
3073
3567
  process.exit(2);
3074
3568
  }
3075
3569
  const workflowName = raw.workflow ?? "execute-task";
3076
- let phases;
3077
- try {
3078
- phases = loadPhases(`workflows/${workflowName}/phases.yaml`);
3079
- } catch (error) {
3570
+ const yamlPath = await resolveWorkflowYaml(workflowName, WORKFLOWS_DIR2);
3571
+ if (!yamlPath) {
3080
3572
  console.error(
3081
3573
  t("execute_task.load_phases_failed", {
3082
3574
  workflow: workflowName,
3083
- message: error.message
3575
+ message: "workflow.yaml not found"
3084
3576
  })
3085
3577
  );
3086
3578
  process.exit(2);
3579
+ return;
3087
3580
  }
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
- );
3581
+ const gateContext = {
3582
+ task: raw.task,
3583
+ ...raw.model ? { modelOverride: raw.model } : {},
3584
+ ...raw.modelTier ? { modelTierOverride: raw.modelTier } : {},
3585
+ ...raw.maxIterations ? { maxIterations: raw.maxIterations } : {}
3586
+ };
3587
+ if (raw.dryRun === true) {
3588
+ console.log(JSON.stringify({ workflow: workflowName, yamlPath, gateContext }, null, 2));
3100
3589
  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
- }
3590
+ return;
3110
3591
  }
3111
3592
  try {
3112
3593
  const stagedOut = execSync("git status --porcelain", { encoding: "utf8" });
@@ -3118,26 +3599,19 @@ function registerExecuteTask(program2) {
3118
3599
  cmdType: "git-commit",
3119
3600
  hasUserApproval: true
3120
3601
  // apply-immediate default
3121
- });
3122
- } catch (err2) {
3123
- console.warn(t("execute_task.precommit_skipped", { message: err2.message }));
3124
- }
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);
3602
+ });
3603
+ } catch (err2) {
3604
+ console.warn(t("execute_task.precommit_skipped", { message: err2.message }));
3134
3605
  }
3135
- if ("ok" in result && result.ok === false) {
3136
- console.error(`error: ${result.phase} \u2014 ${result.error.message}`);
3606
+ let result;
3607
+ try {
3608
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT2, gateContext });
3609
+ } catch (err2) {
3610
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
3137
3611
  process.exit(1);
3612
+ return;
3138
3613
  }
3139
- console.log(result.result);
3140
- process.exit(0);
3614
+ process.exit(result.status === "failed" ? 1 : 0);
3141
3615
  });
3142
3616
  }
3143
3617
  var DURATION_RE = /^(\d+)([dhmw])$/;
@@ -3543,7 +4017,7 @@ async function isPluginRegistered(pluginName) {
3543
4017
  return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
3544
4018
  }
3545
4019
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3546
- return new Promise((resolve12) => {
4020
+ return new Promise((resolve14) => {
3547
4021
  const isWin = process.platform === "win32";
3548
4022
  const child = isWin ? spawn("cmd.exe", ["/c", "claude", ...claudeArgs], { cwd, windowsHide: true }) : spawn("claude", claudeArgs, { cwd, shell: false });
3549
4023
  let stderr = "";
@@ -3552,15 +4026,15 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3552
4026
  });
3553
4027
  const timer = setTimeout(() => {
3554
4028
  child.kill("SIGKILL");
3555
- resolve12({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
4029
+ resolve14({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
3556
4030
  }, timeoutMs);
3557
4031
  child.on("error", (e) => {
3558
4032
  clearTimeout(timer);
3559
- resolve12({ exitCode: -1, stderr: `${stderr}${e.message}` });
4033
+ resolve14({ exitCode: -1, stderr: `${stderr}${e.message}` });
3560
4034
  });
3561
4035
  child.on("close", (code) => {
3562
4036
  clearTimeout(timer);
3563
- resolve12({ exitCode: code ?? -1, stderr });
4037
+ resolve14({ exitCode: code ?? -1, stderr });
3564
4038
  });
3565
4039
  });
3566
4040
  }
@@ -3739,10 +4213,10 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3739
4213
  child.stderr?.setEncoding("utf8").on("data", (chunk) => {
3740
4214
  stderr += chunk;
3741
4215
  });
3742
- return await new Promise((resolve12) => {
4216
+ return await new Promise((resolve14) => {
3743
4217
  const timer = setTimeout(() => {
3744
4218
  child.kill("SIGKILL");
3745
- resolve12({
4219
+ resolve14({
3746
4220
  ok: false,
3747
4221
  phase: "spawn",
3748
4222
  error: {
@@ -3757,7 +4231,7 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3757
4231
  }, effectiveTimeoutMs);
3758
4232
  child.on("error", (err2) => {
3759
4233
  clearTimeout(timer);
3760
- resolve12({
4234
+ resolve14({
3761
4235
  ok: false,
3762
4236
  phase: "spawn",
3763
4237
  error: {
@@ -3772,14 +4246,14 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3772
4246
  });
3773
4247
  child.on("close", (code) => {
3774
4248
  clearTimeout(timer);
3775
- resolve12({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
4249
+ resolve14({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
3776
4250
  });
3777
4251
  });
3778
4252
  }
3779
4253
 
3780
4254
  // src/installers/gitCloneWithSetup.ts
3781
4255
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
3782
- return new Promise((resolve12) => {
4256
+ return new Promise((resolve14) => {
3783
4257
  const isWin = process.platform === "win32";
3784
4258
  const child = isWin ? spawn("cmd.exe", ["/c", "git", "rev-parse", "HEAD"], { cwd, windowsHide: true }) : spawn("git", ["rev-parse", "HEAD"], { cwd, shell: false });
3785
4259
  let stdout2 = "";
@@ -3788,15 +4262,15 @@ function gitRevParseHead(cwd, timeoutMs = 1e4) {
3788
4262
  });
3789
4263
  const timer = setTimeout(() => {
3790
4264
  child.kill("SIGKILL");
3791
- resolve12({ sha: "", exit: -1 });
4265
+ resolve14({ sha: "", exit: -1 });
3792
4266
  }, timeoutMs);
3793
4267
  child.on("error", () => {
3794
4268
  clearTimeout(timer);
3795
- resolve12({ sha: "", exit: -1 });
4269
+ resolve14({ sha: "", exit: -1 });
3796
4270
  });
3797
4271
  child.on("close", (code) => {
3798
4272
  clearTimeout(timer);
3799
- resolve12({ sha: stdout2.trim(), exit: code ?? -1 });
4273
+ resolve14({ sha: stdout2.trim(), exit: code ?? -1 });
3800
4274
  });
3801
4275
  });
3802
4276
  }
@@ -3967,16 +4441,16 @@ var installGitCloneWithSetup = async (ctx) => {
3967
4441
  function resolveEnvVars(value) {
3968
4442
  const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
3969
4443
  let resolved = value;
3970
- let match2;
4444
+ let match;
3971
4445
  pattern.lastIndex = 0;
3972
- while ((match2 = pattern.exec(value)) !== null) {
3973
- const name = match2[1];
4446
+ while ((match = pattern.exec(value)) !== null) {
4447
+ const name = match[1];
3974
4448
  if (name === void 0) continue;
3975
4449
  const v = process.env[name];
3976
4450
  if (v === void 0 || v === "") {
3977
4451
  return { ok: false, missing: name };
3978
4452
  }
3979
- resolved = resolved.replace(match2[0], v);
4453
+ resolved = resolved.replace(match[0], v);
3980
4454
  }
3981
4455
  return { ok: true, resolved };
3982
4456
  }
@@ -4508,16 +4982,6 @@ async function runInstall(manifest, opts) {
4508
4982
 
4509
4983
  // src/cli/install.ts
4510
4984
  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
4985
  function formatError(e) {
4522
4986
  const head = `error: ${e.message}`;
4523
4987
  const where = e.path && e.path !== "/" ? `
@@ -4715,8 +5179,8 @@ function registerManifestAdd(program2) {
4715
5179
  process.exit(0);
4716
5180
  });
4717
5181
  }
4718
-
4719
- // src/cli/research.ts
5182
+ var PACKAGE_ROOT3 = getPackageRoot();
5183
+ var WORKFLOWS_DIR3 = join(PACKAGE_ROOT3, "workflows");
4720
5184
  function registerResearch(program2) {
4721
5185
  program2.command("research").description(
4722
5186
  "Run research workflow (search category sub-routing \u2192 spawn \u2192 verbatim COMPLETE; immediate by default \u2014 use --dry-run for preview)"
@@ -4725,48 +5189,30 @@ function registerResearch(program2) {
4725
5189
  console.error(t("research.require_query"));
4726
5190
  process.exit(2);
4727
5191
  }
4728
- const taskCtx = { task: raw.query, task_type: "search" };
5192
+ const yamlPath = await resolveWorkflowYaml("research", WORKFLOWS_DIR3);
5193
+ if (!yamlPath) {
5194
+ console.error(`error: workflows/research/workflow.yaml not found under ${WORKFLOWS_DIR3}`);
5195
+ process.exit(2);
5196
+ return;
5197
+ }
5198
+ const gateContext = {
5199
+ task: raw.query,
5200
+ ...raw.model ? { modelOverride: raw.model } : {}
5201
+ };
4729
5202
  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"));
5203
+ console.log(JSON.stringify({ workflow: "research", yamlPath, gateContext }, null, 2));
4752
5204
  process.exit(0);
5205
+ return;
4753
5206
  }
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
- }
5207
+ let result;
5208
+ try {
5209
+ result = await runWorkflow(yamlPath, {}, { packageRoot: PACKAGE_ROOT3, gateContext });
5210
+ } catch (err2) {
5211
+ console.error(`error: workflow runtime failed \u2014 ${err2.message}`);
4766
5212
  process.exit(1);
5213
+ return;
4767
5214
  }
4768
- console.log(result.result);
4769
- process.exit(0);
5215
+ process.exit(result.status === "failed" ? 1 : 0);
4770
5216
  });
4771
5217
  }
4772
5218
 
@@ -4854,6 +5300,109 @@ ${t("rollback.metadata_unreadable.fix")}`
4854
5300
  console.log(t("rollback.restored", { count: meta.files.length, timestamp }));
4855
5301
  });
4856
5302
  }
5303
+ function readInstalledPlugins(homedirOverride) {
5304
+ const home = homedir();
5305
+ const path = join(home, ".claude", "plugins", "installed_plugins.json");
5306
+ let raw;
5307
+ try {
5308
+ raw = readFileSync(path, "utf8");
5309
+ } catch {
5310
+ return /* @__PURE__ */ new Set();
5311
+ }
5312
+ let parsed;
5313
+ try {
5314
+ parsed = JSON.parse(raw);
5315
+ } catch {
5316
+ return /* @__PURE__ */ new Set();
5317
+ }
5318
+ if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ new Set();
5319
+ const plugins = parsed.plugins;
5320
+ if (!plugins || typeof plugins !== "object") return /* @__PURE__ */ new Set();
5321
+ const out = /* @__PURE__ */ new Set();
5322
+ for (const key of Object.keys(plugins)) {
5323
+ const at = key.indexOf("@");
5324
+ if (at <= 0) continue;
5325
+ out.add(key.slice(0, at));
5326
+ }
5327
+ return out;
5328
+ }
5329
+ function readInstalledUserSkills(homedirOverride) {
5330
+ const home = homedir();
5331
+ const skillsRoot = join(home, ".claude", "skills");
5332
+ try {
5333
+ const entries = readdirSync(skillsRoot, { withFileTypes: true });
5334
+ const out = /* @__PURE__ */ new Set();
5335
+ for (const e of entries) if (e.isDirectory()) out.add(e.name);
5336
+ return out;
5337
+ } catch {
5338
+ return /* @__PURE__ */ new Set();
5339
+ }
5340
+ }
5341
+ function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills) {
5342
+ const { cmd, install_type, plugin_id, skill_dir } = capability;
5343
+ if (!install_type) return { renderedCmd: cmd };
5344
+ const types = Array.isArray(install_type) ? install_type : [install_type];
5345
+ const uniqueTypes = [...new Set(types)];
5346
+ const missingHints = [];
5347
+ let anyDetected = false;
5348
+ for (const t2 of uniqueTypes) {
5349
+ if (t2 === "plugin") {
5350
+ if (!plugin_id) {
5351
+ missingHints.push(
5352
+ `install_type=plugin declared but no plugin_id (capabilities.yaml schema bug)`
5353
+ );
5354
+ continue;
5355
+ }
5356
+ if (installedPlugins.has(plugin_id)) {
5357
+ anyDetected = true;
5358
+ break;
5359
+ }
5360
+ missingHints.push(`plugin '${plugin_id}' (\`claude plugin install ${plugin_id}\`)`);
5361
+ } else {
5362
+ if (!skill_dir) {
5363
+ missingHints.push(
5364
+ `install_type=user-skill declared but no skill_dir (capabilities.yaml schema bug)`
5365
+ );
5366
+ continue;
5367
+ }
5368
+ if (installedUserSkills.has(skill_dir)) {
5369
+ anyDetected = true;
5370
+ break;
5371
+ }
5372
+ missingHints.push(
5373
+ `user-skill '${skill_dir}' under ~/.claude/skills/ (git clone the official repo; e.g. gstack: \`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup\`)`
5374
+ );
5375
+ }
5376
+ }
5377
+ if (anyDetected) return { renderedCmd: cmd };
5378
+ const prefix = uniqueTypes.length > 1 ? "[multi]" : `[${uniqueTypes[0]}]`;
5379
+ const joined = missingHints.join(" OR ");
5380
+ return {
5381
+ renderedCmd: cmd,
5382
+ warning: `${prefix} '${cmd}' backing missing \u2014 install either: ${joined}.`
5383
+ };
5384
+ }
5385
+ var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
5386
+ function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
5387
+ const warningsSet = /* @__PURE__ */ new Set();
5388
+ const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match, name) => {
5389
+ const cap = capabilities[name];
5390
+ if (!cap) {
5391
+ warningsSet.add(
5392
+ `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
5393
+ );
5394
+ return match;
5395
+ }
5396
+ const { renderedCmd, warning } = resolveCapabilityCmd(
5397
+ cap,
5398
+ installedPlugins,
5399
+ installedUserSkills
5400
+ );
5401
+ if (warning) warningsSet.add(warning);
5402
+ return renderedCmd;
5403
+ });
5404
+ return { body: out, warnings: [...warningsSet] };
5405
+ }
4857
5406
 
4858
5407
  // src/cli/lib/enableAgentTeamsInSettings.ts
4859
5408
  init_harnessedRoot();
@@ -5020,111 +5569,6 @@ async function atomicWrite2(path, content) {
5020
5569
  return `write ${path} failed: ${err2.message}`;
5021
5570
  }
5022
5571
  }
5023
- function readInstalledPlugins(homedirOverride) {
5024
- const home = homedir();
5025
- const path = join(home, ".claude", "plugins", "installed_plugins.json");
5026
- let raw;
5027
- try {
5028
- raw = readFileSync(path, "utf8");
5029
- } catch {
5030
- return /* @__PURE__ */ new Set();
5031
- }
5032
- let parsed;
5033
- try {
5034
- parsed = JSON.parse(raw);
5035
- } catch {
5036
- return /* @__PURE__ */ new Set();
5037
- }
5038
- if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ new Set();
5039
- const plugins = parsed.plugins;
5040
- if (!plugins || typeof plugins !== "object") return /* @__PURE__ */ new Set();
5041
- const out = /* @__PURE__ */ new Set();
5042
- for (const key of Object.keys(plugins)) {
5043
- const at = key.indexOf("@");
5044
- if (at <= 0) continue;
5045
- out.add(key.slice(0, at));
5046
- }
5047
- return out;
5048
- }
5049
- function readInstalledUserSkills(homedirOverride) {
5050
- const home = homedir();
5051
- const skillsRoot = join(home, ".claude", "skills");
5052
- try {
5053
- const entries = readdirSync(skillsRoot, { withFileTypes: true });
5054
- const out = /* @__PURE__ */ new Set();
5055
- for (const e of entries) if (e.isDirectory()) out.add(e.name);
5056
- return out;
5057
- } catch {
5058
- return /* @__PURE__ */ new Set();
5059
- }
5060
- }
5061
- function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills) {
5062
- const { cmd, install_type, plugin_id, skill_dir } = capability;
5063
- if (!install_type) return { renderedCmd: cmd };
5064
- const types = Array.isArray(install_type) ? install_type : [install_type];
5065
- const uniqueTypes = [...new Set(types)];
5066
- const missingHints = [];
5067
- let anyDetected = false;
5068
- for (const t2 of uniqueTypes) {
5069
- if (t2 === "plugin") {
5070
- if (!plugin_id) {
5071
- missingHints.push(
5072
- `install_type=plugin declared but no plugin_id (capabilities.yaml schema bug)`
5073
- );
5074
- continue;
5075
- }
5076
- if (installedPlugins.has(plugin_id)) {
5077
- anyDetected = true;
5078
- break;
5079
- }
5080
- missingHints.push(`plugin '${plugin_id}' (\`claude plugin install ${plugin_id}\`)`);
5081
- } else {
5082
- if (!skill_dir) {
5083
- missingHints.push(
5084
- `install_type=user-skill declared but no skill_dir (capabilities.yaml schema bug)`
5085
- );
5086
- continue;
5087
- }
5088
- if (installedUserSkills.has(skill_dir)) {
5089
- anyDetected = true;
5090
- break;
5091
- }
5092
- missingHints.push(
5093
- `user-skill '${skill_dir}' under ~/.claude/skills/ (git clone the official repo; e.g. gstack: \`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup\`)`
5094
- );
5095
- }
5096
- }
5097
- if (anyDetected) return { renderedCmd: cmd };
5098
- const prefix = uniqueTypes.length > 1 ? "[multi]" : `[${uniqueTypes[0]}]`;
5099
- const joined = missingHints.join(" OR ");
5100
- return {
5101
- renderedCmd: cmd,
5102
- warning: `${prefix} '${cmd}' backing missing \u2014 install either: ${joined}.`
5103
- };
5104
- }
5105
- var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
5106
- function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
5107
- const warningsSet = /* @__PURE__ */ new Set();
5108
- const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match2, name) => {
5109
- const cap = capabilities[name];
5110
- if (!cap) {
5111
- warningsSet.add(
5112
- `capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
5113
- );
5114
- return match2;
5115
- }
5116
- const { renderedCmd, warning } = resolveCapabilityCmd(
5117
- cap,
5118
- installedPlugins,
5119
- installedUserSkills
5120
- );
5121
- if (warning) warningsSet.add(warning);
5122
- return renderedCmd;
5123
- });
5124
- return { body: out, warnings: [...warningsSet] };
5125
- }
5126
-
5127
- // src/cli/lib/renderSkillTemplates.ts
5128
5572
  async function loadCapabilities(workflowsDir) {
5129
5573
  const path = join(workflowsDir, "capabilities.yaml");
5130
5574
  const raw = await readFile(path, "utf8");
@@ -5431,6 +5875,46 @@ function registerSetup(program2) {
5431
5875
  console.warn(` - ${w}`);
5432
5876
  }
5433
5877
  }
5878
+ const commandsBase = resolve(homedir(), ".claude", "commands");
5879
+ try {
5880
+ await mkdir(commandsBase, { recursive: true });
5881
+ } catch (e) {
5882
+ console.warn(
5883
+ ` [A.6] could not create ${commandsBase} \u2014 skipping commands/ generation (${e.message})`
5884
+ );
5885
+ }
5886
+ let capabilitiesMap = {};
5887
+ try {
5888
+ capabilitiesMap = await loadCapabilities(workflowsDir);
5889
+ } catch (e) {
5890
+ console.warn(
5891
+ ` [A.6] capabilities.yaml unreadable \u2014 skipping commands/ generation (${e.message})`
5892
+ );
5893
+ }
5894
+ const rolePrompts = await loadRolePrompts(workflowsDir);
5895
+ const installedPlugins = readInstalledPlugins();
5896
+ const installedUserSkills = readInstalledUserSkills();
5897
+ const cmdResult = await writeAllCommands(
5898
+ skillNames,
5899
+ commandsBase,
5900
+ rolePrompts,
5901
+ capabilitiesMap,
5902
+ installedPlugins,
5903
+ installedUserSkills,
5904
+ async (p4, c) => writeFile(p4, c, "utf8")
5905
+ );
5906
+ const writtenCount = cmdResult.results.filter((r) => r.written).length;
5907
+ const skippedCount = cmdResult.results.filter((r) => !r.written && r.warning).length;
5908
+ console.log(
5909
+ ` [A.6] generated ${writtenCount} commands/<x>.md file(s) (${skippedCount} skipped \u2014 existing user file or schema warn)`
5910
+ );
5911
+ for (const r of cmdResult.results) {
5912
+ if (r.written) {
5913
+ console.log(` [A.6] wrote /${r.name} \u2192 ${r.path}`);
5914
+ } else if (r.warning) {
5915
+ console.warn(` [A.6] skipped /${r.name}: ${r.warning}`);
5916
+ }
5917
+ }
5434
5918
  const cResult = await enableAgentTeamsInSettings();
5435
5919
  if (cResult.status === "created") {
5436
5920
  console.log(t("setup.step_c.created", { path: cResult.path }));
@@ -5719,7 +6203,7 @@ var uninstallNpmCli = async (ctx) => {
5719
6203
  const m = install.cmd.match(/npm\s+(?:install|i)\s+(?:-g\s+)?(\S+)/);
5720
6204
  const pkg = m?.[1] ?? ctx.manifest.metadata.upstream.source;
5721
6205
  const isWin = process.platform === "win32";
5722
- const result = await new Promise((resolve12) => {
6206
+ const result = await new Promise((resolve14) => {
5723
6207
  const child = isWin ? spawn("cmd.exe", ["/c", "npm", "uninstall", "-g", pkg], { windowsHide: true }) : spawn("npm", ["uninstall", "-g", pkg], { shell: false });
5724
6208
  let stderr = "";
5725
6209
  child.stderr?.setEncoding("utf8").on("data", (c) => {
@@ -5727,15 +6211,15 @@ var uninstallNpmCli = async (ctx) => {
5727
6211
  });
5728
6212
  const timer = setTimeout(() => {
5729
6213
  child.kill("SIGKILL");
5730
- resolve12({ exitCode: -1, stderr: `${stderr}[timeout]` });
6214
+ resolve14({ exitCode: -1, stderr: `${stderr}[timeout]` });
5731
6215
  }, 3e4);
5732
6216
  child.on("error", (e) => {
5733
6217
  clearTimeout(timer);
5734
- resolve12({ exitCode: -1, stderr: e.message });
6218
+ resolve14({ exitCode: -1, stderr: e.message });
5735
6219
  });
5736
6220
  child.on("close", (code) => {
5737
6221
  clearTimeout(timer);
5738
- resolve12({ exitCode: code ?? -1, stderr });
6222
+ resolve14({ exitCode: code ?? -1, stderr });
5739
6223
  });
5740
6224
  });
5741
6225
  if (result.exitCode !== 0) {
@@ -5884,6 +6368,7 @@ registerGc(program);
5884
6368
  registerResume(program);
5885
6369
  registerUninstall(program);
5886
6370
  registerSetup(program);
6371
+ registerRun(program);
5887
6372
  program.parse(process.argv);
5888
6373
  //# sourceMappingURL=cli.mjs.map
5889
6374
  //# sourceMappingURL=cli.mjs.map