harnessed 3.4.3 → 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.
- package/README.md +3 -0
- package/dist/cli.mjs +1084 -745
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/workflows/auto/SKILL.md +10 -4
- package/workflows/capabilities.yaml +1 -1
- package/workflows/discuss/auto/SKILL.md +9 -4
- package/workflows/discuss/phase/SKILL.md +11 -29
- package/workflows/discuss/strategic/SKILL.md +11 -31
- package/workflows/discuss/subtask/SKILL.md +11 -29
- package/workflows/execute-task/SKILL.md +7 -6
- package/workflows/execute-task/workflow.yaml +93 -0
- package/workflows/plan/architecture/SKILL.md +11 -31
- package/workflows/plan/auto/SKILL.md +9 -4
- package/workflows/plan/phase/SKILL.md +11 -31
- package/workflows/research/SKILL.md +14 -1
- package/workflows/retro/SKILL.md +11 -29
- package/workflows/task/auto/SKILL.md +9 -4
- package/workflows/task/clarify/SKILL.md +11 -29
- package/workflows/task/code/SKILL.md +11 -31
- package/workflows/task/deliver/SKILL.md +12 -32
- package/workflows/task/test/SKILL.md +11 -31
- package/workflows/verify/auto/SKILL.md +9 -4
- package/workflows/verify/code-review/SKILL.md +11 -33
- package/workflows/verify/design/SKILL.md +11 -33
- package/workflows/verify/multispec/SKILL.md +11 -31
- package/workflows/verify/paranoid/SKILL.md +11 -35
- package/workflows/verify/progress/SKILL.md +11 -29
- package/workflows/verify/qa/SKILL.md +11 -31
- package/workflows/verify/security/SKILL.md +11 -33
- package/workflows/verify/simplify/SKILL.md +11 -31
- package/workflows/execute-task/phases.yaml +0 -73
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync, spawn } from 'child_process';
|
|
3
|
-
import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync
|
|
4
|
-
import { join,
|
|
3
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve, relative } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { Type } from '@sinclair/typebox';
|
|
7
7
|
import { Value } from '@sinclair/typebox/value';
|
|
@@ -13,6 +13,7 @@ import { Ajv } from 'ajv';
|
|
|
13
13
|
import * as ajvFormatsNs from 'ajv-formats';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
16
|
+
import { Parser } from 'expr-eval';
|
|
16
17
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
17
18
|
import * as p from '@clack/prompts';
|
|
18
19
|
import { createPatch } from 'diff';
|
|
@@ -688,6 +689,11 @@ async function activate(phase, checkpointPath = null) {
|
|
|
688
689
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
689
690
|
});
|
|
690
691
|
}
|
|
692
|
+
async function pause() {
|
|
693
|
+
const s = await readCurrentWorkflow();
|
|
694
|
+
if (!s) return;
|
|
695
|
+
await writeCurrentWorkflow({ ...s, status: "paused", paused_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
696
|
+
}
|
|
691
697
|
async function complete() {
|
|
692
698
|
const s = await readCurrentWorkflow();
|
|
693
699
|
if (!s) return;
|
|
@@ -847,7 +853,7 @@ var init_resume = __esm({
|
|
|
847
853
|
|
|
848
854
|
// package.json
|
|
849
855
|
var package_default = {
|
|
850
|
-
version: "3.4.
|
|
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((
|
|
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
|
-
|
|
1716
|
+
resolve14(1);
|
|
1711
1717
|
} else {
|
|
1712
1718
|
reject(err2);
|
|
1713
1719
|
}
|
|
1714
1720
|
});
|
|
1715
|
-
child.on("close", (code) =>
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
2726
|
-
var
|
|
2727
|
-
|
|
2728
|
-
|
|
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
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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
|
-
|
|
2820
|
-
|
|
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 }),
|
|
@@ -3044,26 +2954,610 @@ var PhasesValidationError = class extends Error {
|
|
|
3044
2954
|
this.errors = errors;
|
|
3045
2955
|
this.name = "PhasesValidationError";
|
|
3046
2956
|
}
|
|
3047
|
-
errors;
|
|
2957
|
+
errors;
|
|
2958
|
+
};
|
|
2959
|
+
function loadPhases(yamlPath, vars) {
|
|
2960
|
+
const raw = readFileSync(yamlPath, "utf8");
|
|
2961
|
+
const parsed = parse(raw);
|
|
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") {
|
|
2968
|
+
if (!Value.Check(WorkflowSchemaV2, parsed)) {
|
|
2969
|
+
throw new PhasesValidationError([...Value.Errors(WorkflowSchemaV2, parsed)]);
|
|
2970
|
+
}
|
|
2971
|
+
} else {
|
|
2972
|
+
if (!Value.Check(PhasesSchema, parsed)) {
|
|
2973
|
+
throw new PhasesValidationError([...Value.Errors(PhasesSchema, parsed)]);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
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
|
+
}
|
|
2982
|
+
return validated;
|
|
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
|
+
}
|
|
3048
3270
|
};
|
|
3049
|
-
function
|
|
3050
|
-
const
|
|
3051
|
-
const
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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
|
+
}
|
|
3056
3320
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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
|
+
}
|
|
3060
3384
|
}
|
|
3385
|
+
await completePhase({
|
|
3386
|
+
phaseId: ph.id,
|
|
3387
|
+
lastTask: `phase ${ph.id} complete: ${r.output}`
|
|
3388
|
+
});
|
|
3061
3389
|
}
|
|
3062
|
-
|
|
3063
|
-
|
|
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;
|
|
3064
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
|
-
|
|
3077
|
-
|
|
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:
|
|
3575
|
+
message: "workflow.yaml not found"
|
|
3084
3576
|
})
|
|
3085
3577
|
);
|
|
3086
3578
|
process.exit(2);
|
|
3579
|
+
return;
|
|
3087
3580
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
|
|
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" });
|
|
@@ -3122,22 +3603,15 @@ function registerExecuteTask(program2) {
|
|
|
3122
3603
|
} catch (err2) {
|
|
3123
3604
|
console.warn(t("execute_task.precommit_skipped", { message: err2.message }));
|
|
3124
3605
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
});
|
|
3131
|
-
if ("aborted" in result) {
|
|
3132
|
-
console.error(t("install.aborted", { reason: result.reason }));
|
|
3133
|
-
process.exit(2);
|
|
3134
|
-
}
|
|
3135
|
-
if ("ok" in result && result.ok === false) {
|
|
3136
|
-
console.error(`error: ${result.phase} \u2014 ${result.error.message}`);
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
4029
|
+
resolve14({ exitCode: -1, stderr: `${stderr}[timeout after ${timeoutMs}ms]` });
|
|
3556
4030
|
}, timeoutMs);
|
|
3557
4031
|
child.on("error", (e) => {
|
|
3558
4032
|
clearTimeout(timer);
|
|
3559
|
-
|
|
4033
|
+
resolve14({ exitCode: -1, stderr: `${stderr}${e.message}` });
|
|
3560
4034
|
});
|
|
3561
4035
|
child.on("close", (code) => {
|
|
3562
4036
|
clearTimeout(timer);
|
|
3563
|
-
|
|
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((
|
|
4216
|
+
return await new Promise((resolve14) => {
|
|
3743
4217
|
const timer = setTimeout(() => {
|
|
3744
4218
|
child.kill("SIGKILL");
|
|
3745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
4265
|
+
resolve14({ sha: "", exit: -1 });
|
|
3792
4266
|
}, timeoutMs);
|
|
3793
4267
|
child.on("error", () => {
|
|
3794
4268
|
clearTimeout(timer);
|
|
3795
|
-
|
|
4269
|
+
resolve14({ sha: "", exit: -1 });
|
|
3796
4270
|
});
|
|
3797
4271
|
child.on("close", (code) => {
|
|
3798
4272
|
clearTimeout(timer);
|
|
3799
|
-
|
|
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
|
|
4444
|
+
let match;
|
|
3971
4445
|
pattern.lastIndex = 0;
|
|
3972
|
-
while ((
|
|
3973
|
-
const name =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
console.error(
|
|
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
|
-
|
|
4769
|
-
process.exit(0);
|
|
5215
|
+
process.exit(result.status === "failed" ? 1 : 0);
|
|
4770
5216
|
});
|
|
4771
5217
|
}
|
|
4772
5218
|
|
|
@@ -4939,13 +5385,13 @@ function resolveCapabilityCmd(capability, installedPlugins, installedUserSkills)
|
|
|
4939
5385
|
var CAPABILITY_CMD_TEMPLATE = /\{\{\s*capabilities\.([a-zA-Z0-9_-]+)\.cmd\s*\}\}/g;
|
|
4940
5386
|
function renderSkillBody(body, capabilities, installedPlugins, installedUserSkills) {
|
|
4941
5387
|
const warningsSet = /* @__PURE__ */ new Set();
|
|
4942
|
-
const out = body.replace(CAPABILITY_CMD_TEMPLATE, (
|
|
5388
|
+
const out = body.replace(CAPABILITY_CMD_TEMPLATE, (match, name) => {
|
|
4943
5389
|
const cap = capabilities[name];
|
|
4944
5390
|
if (!cap) {
|
|
4945
5391
|
warningsSet.add(
|
|
4946
5392
|
`capability '${name}' referenced in SKILL.md but not defined in capabilities.yaml`
|
|
4947
5393
|
);
|
|
4948
|
-
return
|
|
5394
|
+
return match;
|
|
4949
5395
|
}
|
|
4950
5396
|
const { renderedCmd, warning } = resolveCapabilityCmd(
|
|
4951
5397
|
cap,
|
|
@@ -5123,114 +5569,6 @@ async function atomicWrite2(path, content) {
|
|
|
5123
5569
|
return `write ${path} failed: ${err2.message}`;
|
|
5124
5570
|
}
|
|
5125
5571
|
}
|
|
5126
|
-
async function loadRolePrompts(workflowsDir) {
|
|
5127
|
-
const path = join(workflowsDir, "role-prompts.yaml");
|
|
5128
|
-
let raw;
|
|
5129
|
-
try {
|
|
5130
|
-
raw = await readFile(path, "utf8");
|
|
5131
|
-
} catch {
|
|
5132
|
-
return {};
|
|
5133
|
-
}
|
|
5134
|
-
const doc = parse(raw);
|
|
5135
|
-
return doc?.prompts ?? {};
|
|
5136
|
-
}
|
|
5137
|
-
function generateCommandFile(name, prompt, capabilities, installedPlugins, installedUserSkills) {
|
|
5138
|
-
const isMaster = prompt.is_master === true;
|
|
5139
|
-
const primaryCmdLine = prompt.primary_cap && capabilities[prompt.primary_cap] ? `{{ capabilities.${prompt.primary_cap}.cmd }}` : "";
|
|
5140
|
-
const checklistBlock = prompt.checklist.length ? prompt.checklist.map((item, i) => `> ${i + 1}. ${item}`).join("\n>\n") : "> (Master orchestrator \u2014 dispatches to per-sub-workflow slash commands listed below.)";
|
|
5141
|
-
const fallbackPath = isMaster ? `**Fallback path** (when no slash command from the sub-list resolves): run each missing sub-workflow inline using its own role prompt (see \`~/.claude/skills/<sub-name>/SKILL.md\` for the per-sub fallback prompt). Do NOT skip stages silently \u2014 each sub either runs or is logged as "skipped: <reason>".` : [
|
|
5142
|
-
`**Fallback path** (when the upstream isn't installed or returns no result): use the Task tool to spawn a general-purpose subagent with this prompt:`,
|
|
5143
|
-
``,
|
|
5144
|
-
`> You are a **${prompt.specialist}**.`,
|
|
5145
|
-
`>`,
|
|
5146
|
-
`> **Mission**: ${prompt.responsibility.trim().replace(/\n/g, " ")}`,
|
|
5147
|
-
`>`,
|
|
5148
|
-
`> **Default-suspect mode**: assume the change is broken / risky / incomplete until proven otherwise. Cite \`file:line\` for every finding; do not generalize.`,
|
|
5149
|
-
`>`,
|
|
5150
|
-
`> **Review checklist**:`,
|
|
5151
|
-
checklistBlock,
|
|
5152
|
-
`>`,
|
|
5153
|
-
`> **Output format**: structured report with severity-classified findings (${prompt.severity}). One finding per line: \`[severity] file:line \u2014 problem (one sentence); fix: suggested change\`. If no findings, say so explicitly. No preamble, no end-of-report summary.`,
|
|
5154
|
-
``,
|
|
5155
|
-
`(The role prompt is self-contained \u2014 works even when the upstream \`${prompt.primary_cap || "specialist"}\` user-skill / plugin isn't installed.)`
|
|
5156
|
-
].join("\n");
|
|
5157
|
-
const preferredPath = primaryCmdLine ? `**Preferred path** (when the upstream specialist is installed): use the SlashCommand tool to run \`${primaryCmdLine}\` \u2014 the upstream specialist takes over.` : `**Preferred path** (master orchestrator): dispatch to the per-sub-workflow slash commands in the order this stage prescribes. Each sub command is its own \`~/.claude/commands/<sub-name>.md\` and has its own dual-path fallback.`;
|
|
5158
|
-
const rawBody = [
|
|
5159
|
-
`# /${name}`,
|
|
5160
|
-
``,
|
|
5161
|
-
prompt.description,
|
|
5162
|
-
``,
|
|
5163
|
-
`## How to invoke`,
|
|
5164
|
-
``,
|
|
5165
|
-
preferredPath,
|
|
5166
|
-
``,
|
|
5167
|
-
fallbackPath,
|
|
5168
|
-
``,
|
|
5169
|
-
`## Notes`,
|
|
5170
|
-
``,
|
|
5171
|
-
`- This file (\`~/.claude/commands/${name}.md\`) is generated by \`harnessed setup\` from \`workflows/role-prompts.yaml\` + \`workflows/<stage>/<sub>/SKILL.md\`. To regenerate after a harnessed upgrade, re-run \`harnessed setup\`.`,
|
|
5172
|
-
`- The companion \`~/.claude/skills/${name}/SKILL.md\` is the Skill-tool entry point (Claude loads it when triggers match \`trigger_phrases:\`). Both files carry the same dual-path instruction.`,
|
|
5173
|
-
`- If your shell shows a \`\u26A0\uFE0F ... not installed\` warning from \`harnessed setup\` for this command, the upstream is missing on disk \u2014 install per the warning, OR rely on the fallback Task-spawn role prompt above (it does not require the upstream).`,
|
|
5174
|
-
``
|
|
5175
|
-
].join("\n");
|
|
5176
|
-
const { body, warnings } = renderSkillBody(
|
|
5177
|
-
rawBody,
|
|
5178
|
-
capabilities,
|
|
5179
|
-
installedPlugins,
|
|
5180
|
-
installedUserSkills
|
|
5181
|
-
);
|
|
5182
|
-
const frontmatter = ["---", `description: ${JSON.stringify(prompt.description)}`, "---", ""].join(
|
|
5183
|
-
"\n"
|
|
5184
|
-
);
|
|
5185
|
-
return { content: frontmatter + body, warnings };
|
|
5186
|
-
}
|
|
5187
|
-
async function writeAllCommands(slashNames, commandsDir, rolePrompts, capabilities, installedPlugins, installedUserSkills, writer, fileExists = existsSync) {
|
|
5188
|
-
const results = [];
|
|
5189
|
-
const aggregatedWarnings = /* @__PURE__ */ new Set();
|
|
5190
|
-
for (const name of slashNames) {
|
|
5191
|
-
const path = join(commandsDir, `${name}.md`);
|
|
5192
|
-
const prompt = rolePrompts[name];
|
|
5193
|
-
if (!prompt) {
|
|
5194
|
-
results.push({
|
|
5195
|
-
name,
|
|
5196
|
-
path,
|
|
5197
|
-
written: false,
|
|
5198
|
-
warning: `no role-prompts.yaml entry for '${name}' \u2014 skipping commands/${name}.md generation`
|
|
5199
|
-
});
|
|
5200
|
-
aggregatedWarnings.add(`role-prompts.yaml missing entry for '${name}'`);
|
|
5201
|
-
continue;
|
|
5202
|
-
}
|
|
5203
|
-
if (fileExists(path)) {
|
|
5204
|
-
results.push({
|
|
5205
|
-
name,
|
|
5206
|
-
path,
|
|
5207
|
-
written: false,
|
|
5208
|
-
warning: `commands/${name}.md already exists \u2014 leaving user file unchanged`
|
|
5209
|
-
});
|
|
5210
|
-
continue;
|
|
5211
|
-
}
|
|
5212
|
-
const { content, warnings } = generateCommandFile(
|
|
5213
|
-
name,
|
|
5214
|
-
prompt,
|
|
5215
|
-
capabilities,
|
|
5216
|
-
installedPlugins,
|
|
5217
|
-
installedUserSkills
|
|
5218
|
-
);
|
|
5219
|
-
try {
|
|
5220
|
-
await writer(path, content);
|
|
5221
|
-
results.push({ name, path, written: true });
|
|
5222
|
-
} catch (e) {
|
|
5223
|
-
results.push({
|
|
5224
|
-
name,
|
|
5225
|
-
path,
|
|
5226
|
-
written: false,
|
|
5227
|
-
warning: `write failed for commands/${name}.md: ${e.message}`
|
|
5228
|
-
});
|
|
5229
|
-
}
|
|
5230
|
-
for (const w of warnings) aggregatedWarnings.add(w);
|
|
5231
|
-
}
|
|
5232
|
-
return { results, warnings: [...aggregatedWarnings] };
|
|
5233
|
-
}
|
|
5234
5572
|
async function loadCapabilities(workflowsDir) {
|
|
5235
5573
|
const path = join(workflowsDir, "capabilities.yaml");
|
|
5236
5574
|
const raw = await readFile(path, "utf8");
|
|
@@ -5865,7 +6203,7 @@ var uninstallNpmCli = async (ctx) => {
|
|
|
5865
6203
|
const m = install.cmd.match(/npm\s+(?:install|i)\s+(?:-g\s+)?(\S+)/);
|
|
5866
6204
|
const pkg = m?.[1] ?? ctx.manifest.metadata.upstream.source;
|
|
5867
6205
|
const isWin = process.platform === "win32";
|
|
5868
|
-
const result = await new Promise((
|
|
6206
|
+
const result = await new Promise((resolve14) => {
|
|
5869
6207
|
const child = isWin ? spawn("cmd.exe", ["/c", "npm", "uninstall", "-g", pkg], { windowsHide: true }) : spawn("npm", ["uninstall", "-g", pkg], { shell: false });
|
|
5870
6208
|
let stderr = "";
|
|
5871
6209
|
child.stderr?.setEncoding("utf8").on("data", (c) => {
|
|
@@ -5873,15 +6211,15 @@ var uninstallNpmCli = async (ctx) => {
|
|
|
5873
6211
|
});
|
|
5874
6212
|
const timer = setTimeout(() => {
|
|
5875
6213
|
child.kill("SIGKILL");
|
|
5876
|
-
|
|
6214
|
+
resolve14({ exitCode: -1, stderr: `${stderr}[timeout]` });
|
|
5877
6215
|
}, 3e4);
|
|
5878
6216
|
child.on("error", (e) => {
|
|
5879
6217
|
clearTimeout(timer);
|
|
5880
|
-
|
|
6218
|
+
resolve14({ exitCode: -1, stderr: e.message });
|
|
5881
6219
|
});
|
|
5882
6220
|
child.on("close", (code) => {
|
|
5883
6221
|
clearTimeout(timer);
|
|
5884
|
-
|
|
6222
|
+
resolve14({ exitCode: code ?? -1, stderr });
|
|
5885
6223
|
});
|
|
5886
6224
|
});
|
|
5887
6225
|
if (result.exitCode !== 0) {
|
|
@@ -6030,6 +6368,7 @@ registerGc(program);
|
|
|
6030
6368
|
registerResume(program);
|
|
6031
6369
|
registerUninstall(program);
|
|
6032
6370
|
registerSetup(program);
|
|
6371
|
+
registerRun(program);
|
|
6033
6372
|
program.parse(process.argv);
|
|
6034
6373
|
//# sourceMappingURL=cli.mjs.map
|
|
6035
6374
|
//# sourceMappingURL=cli.mjs.map
|