@harness-engineering/cli 7.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/harness.js +1 -1
- package/dist/{chunk-N4VEUBJ5.js → chunk-SHOTI4TA.js} +239 -68
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/templates/orchestrator/harness.orchestrator.local.md +195 -0
- package/dist/templates/orchestrator/harness.orchestrator.md +5 -0
- package/package.json +8 -8
package/dist/bin/harness.js
CHANGED
|
@@ -3338,7 +3338,7 @@ import { spawn } from "child_process";
|
|
|
3338
3338
|
import { existsSync as existsSync6 } from "fs";
|
|
3339
3339
|
import { createRequire } from "module";
|
|
3340
3340
|
import { dirname as dirname4, join as join10, resolve as resolve15 } from "path";
|
|
3341
|
-
import { setTimeout } from "timers";
|
|
3341
|
+
import { setTimeout as setTimeout2 } from "timers";
|
|
3342
3342
|
import { fileURLToPath } from "url";
|
|
3343
3343
|
var __filename = fileURLToPath(import.meta.url);
|
|
3344
3344
|
var __dirname = dirname4(__filename);
|
|
@@ -3413,7 +3413,7 @@ function runDashboard(opts) {
|
|
|
3413
3413
|
console.log(`Dashboard API starting on http://localhost:${apiPort}`);
|
|
3414
3414
|
console.log(`Open ${url} (pass --no-open to suppress)`);
|
|
3415
3415
|
if (opts.noOpen !== true) {
|
|
3416
|
-
|
|
3416
|
+
setTimeout2(() => openBrowser(url), 1500);
|
|
3417
3417
|
}
|
|
3418
3418
|
}
|
|
3419
3419
|
function createDashboardCommand() {
|
|
@@ -9518,9 +9518,12 @@ function createOrchestratorCommand() {
|
|
|
9518
9518
|
logger.error(`Failed to load workflow: ${result.error.message}`);
|
|
9519
9519
|
process.exit(ExitCode.ERROR);
|
|
9520
9520
|
}
|
|
9521
|
-
const { config, promptTemplate, warnings } = result.value;
|
|
9521
|
+
const { config, promptTemplate, localPromptTemplate, warnings } = result.value;
|
|
9522
9522
|
for (const w of warnings) logger.warn(w);
|
|
9523
|
-
const daemon = new Orchestrator(config, promptTemplate, {
|
|
9523
|
+
const daemon = new Orchestrator(config, promptTemplate, {
|
|
9524
|
+
discoverCandidates,
|
|
9525
|
+
...localPromptTemplate !== void 0 ? { localPromptTemplate } : {}
|
|
9526
|
+
});
|
|
9524
9527
|
const shutdown = () => {
|
|
9525
9528
|
daemon.stop();
|
|
9526
9529
|
process.exit(ExitCode.SUCCESS);
|
|
@@ -11792,7 +11795,7 @@ function firstModel(model) {
|
|
|
11792
11795
|
if (Array.isArray(model)) return model[0];
|
|
11793
11796
|
return model;
|
|
11794
11797
|
}
|
|
11795
|
-
function resolveTriageProvider(config, modelOverride) {
|
|
11798
|
+
function resolveTriageProvider(config, modelOverride, localModelPreference) {
|
|
11796
11799
|
const explicit = config?.intelligence?.provider;
|
|
11797
11800
|
if (explicit) {
|
|
11798
11801
|
if (explicit.kind === "anthropic") {
|
|
@@ -11815,7 +11818,7 @@ function resolveTriageProvider(config, modelOverride) {
|
|
|
11815
11818
|
if (!backends) return null;
|
|
11816
11819
|
const local = pickBackend(backends, ["local", "pi"]);
|
|
11817
11820
|
if (local && local.endpoint) {
|
|
11818
|
-
const model = modelOverride ?? firstModel(local.model);
|
|
11821
|
+
const model = modelOverride ?? localModelPreference ?? firstModel(local.model);
|
|
11819
11822
|
return new OpenAICompatibleAnalysisProvider2({
|
|
11820
11823
|
apiKey: local.apiKey ?? "ollama",
|
|
11821
11824
|
baseUrl: local.endpoint,
|
|
@@ -11841,6 +11844,48 @@ function pickBackend(backends, types) {
|
|
|
11841
11844
|
return null;
|
|
11842
11845
|
}
|
|
11843
11846
|
|
|
11847
|
+
// src/commands/roadmap/triage-pool.ts
|
|
11848
|
+
import {
|
|
11849
|
+
PoolStateStore,
|
|
11850
|
+
poolStateToCandidates
|
|
11851
|
+
} from "@harness-engineering/orchestrator";
|
|
11852
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
11853
|
+
async function probeServedModels(endpoint, apiKey, fetchImpl) {
|
|
11854
|
+
const url = endpoint.replace(/\/$/, "") + "/models";
|
|
11855
|
+
const controller = new AbortController();
|
|
11856
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
11857
|
+
try {
|
|
11858
|
+
const res = await fetchImpl(url, {
|
|
11859
|
+
signal: controller.signal,
|
|
11860
|
+
...apiKey !== void 0 ? { headers: { Authorization: `Bearer ${apiKey}` } } : {}
|
|
11861
|
+
});
|
|
11862
|
+
if (!res.ok) return void 0;
|
|
11863
|
+
const body = await res.json();
|
|
11864
|
+
const ids = (body.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
11865
|
+
return new Set(ids);
|
|
11866
|
+
} catch {
|
|
11867
|
+
return void 0;
|
|
11868
|
+
} finally {
|
|
11869
|
+
clearTimeout(timer);
|
|
11870
|
+
}
|
|
11871
|
+
}
|
|
11872
|
+
async function resolvePreferredLocalModel(profile, deps = {}) {
|
|
11873
|
+
try {
|
|
11874
|
+
const store = deps.store ?? new PoolStateStore();
|
|
11875
|
+
await store.load();
|
|
11876
|
+
const candidates = poolStateToCandidates(store.snapshot(), profile).filter(
|
|
11877
|
+
(name) => typeof name === "string" && name.length > 0
|
|
11878
|
+
);
|
|
11879
|
+
if (candidates.length === 0) return void 0;
|
|
11880
|
+
if (deps.endpoint === void 0) return void 0;
|
|
11881
|
+
const served = await probeServedModels(deps.endpoint, deps.apiKey, deps.fetchImpl ?? fetch);
|
|
11882
|
+
if (served === void 0) return void 0;
|
|
11883
|
+
return candidates.find((name) => served.has(name));
|
|
11884
|
+
} catch {
|
|
11885
|
+
return void 0;
|
|
11886
|
+
}
|
|
11887
|
+
}
|
|
11888
|
+
|
|
11844
11889
|
// src/commands/roadmap/triage-approve.ts
|
|
11845
11890
|
import {
|
|
11846
11891
|
detectScopeTier,
|
|
@@ -11987,15 +12032,46 @@ async function buildShapeHistory(cwd) {
|
|
|
11987
12032
|
}
|
|
11988
12033
|
return (shapeKey) => byShape.get(shapeKey) ?? [];
|
|
11989
12034
|
}
|
|
12035
|
+
function selectActionableFeatures(roadmap, opts = {}) {
|
|
12036
|
+
let features = roadmap.milestones.flatMap((m) => m.features).filter(isActionable);
|
|
12037
|
+
if (opts.only !== void 0 && opts.only.trim().length > 0) {
|
|
12038
|
+
const needle = opts.only.trim().toLowerCase();
|
|
12039
|
+
features = features.filter((f) => f.name.toLowerCase().includes(needle));
|
|
12040
|
+
}
|
|
12041
|
+
if (opts.limit !== void 0 && opts.limit >= 0) {
|
|
12042
|
+
features = features.slice(0, opts.limit);
|
|
12043
|
+
}
|
|
12044
|
+
return features;
|
|
12045
|
+
}
|
|
12046
|
+
function isPlausibleForModel(verdict) {
|
|
12047
|
+
const scope = verdict.levers.scope.value;
|
|
12048
|
+
const scopeResolvedBounded = scope !== "unknown" && scope.resolved.length > 0;
|
|
12049
|
+
const level = verdict.verdict.level;
|
|
12050
|
+
const inBand = level === "trivial" || level === "simple";
|
|
12051
|
+
return scopeResolvedBounded && inBand;
|
|
12052
|
+
}
|
|
11990
12053
|
async function runTriageReport(roadmap, deps = {}) {
|
|
11991
|
-
const features = roadmap
|
|
12054
|
+
const features = selectActionableFeatures(roadmap, {
|
|
12055
|
+
...deps.only !== void 0 ? { only: deps.only } : {},
|
|
12056
|
+
...deps.limit !== void 0 ? { limit: deps.limit } : {}
|
|
12057
|
+
});
|
|
12058
|
+
const useModel = deps.offline !== true && deps.provider != null;
|
|
12059
|
+
const cheapDeps = {
|
|
12060
|
+
...deps.graphStore ? { graphStore: deps.graphStore } : {},
|
|
12061
|
+
...deps.precedent ? { precedent: deps.precedent } : {},
|
|
12062
|
+
...deps.config ? { config: deps.config } : {},
|
|
12063
|
+
...useModel ? { modelDeferred: true } : {}
|
|
12064
|
+
};
|
|
11992
12065
|
const rows = [];
|
|
11993
12066
|
for (const feature of features) {
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
12067
|
+
let verdict = await triageIssue(featureToIssue(feature), cheapDeps);
|
|
12068
|
+
if (useModel && isPlausibleForModel(verdict)) {
|
|
12069
|
+
deps.onProgress?.(`Refining "${feature.name}" on the local model\u2026`);
|
|
12070
|
+
verdict = await triageIssue(featureToIssue(feature), {
|
|
12071
|
+
...cheapDeps,
|
|
12072
|
+
...deps.provider ? { provider: deps.provider } : {}
|
|
12073
|
+
});
|
|
12074
|
+
}
|
|
11999
12075
|
rows.push({
|
|
12000
12076
|
externalId: verdict.externalId,
|
|
12001
12077
|
name: feature.name,
|
|
@@ -12051,8 +12127,27 @@ function renderJson(rows) {
|
|
|
12051
12127
|
2
|
|
12052
12128
|
);
|
|
12053
12129
|
}
|
|
12130
|
+
function skippedBrainstormRow(feature, verdict, level) {
|
|
12131
|
+
return {
|
|
12132
|
+
externalId: verdict.externalId,
|
|
12133
|
+
name: feature.name,
|
|
12134
|
+
status: feature.status,
|
|
12135
|
+
level,
|
|
12136
|
+
result: {
|
|
12137
|
+
outcome: {
|
|
12138
|
+
kind: "halted",
|
|
12139
|
+
reason: "error",
|
|
12140
|
+
fork: { id: "scope-gate", question: "", options: [] },
|
|
12141
|
+
detail: `Skipped brainstorm \u2014 not a plausible candidate (${verdict.holdReason ?? "held"}). Held to human by the cheap scope/band gate.`
|
|
12142
|
+
}
|
|
12143
|
+
}
|
|
12144
|
+
};
|
|
12145
|
+
}
|
|
12054
12146
|
async function runBrainstormReport(roadmap, deps = {}) {
|
|
12055
|
-
const features = roadmap
|
|
12147
|
+
const features = selectActionableFeatures(roadmap, {
|
|
12148
|
+
...deps.only !== void 0 ? { only: deps.only } : {},
|
|
12149
|
+
...deps.limit !== void 0 ? { limit: deps.limit } : {}
|
|
12150
|
+
});
|
|
12056
12151
|
const rows = [];
|
|
12057
12152
|
for (const feature of features) {
|
|
12058
12153
|
const issue = featureToIssue(feature);
|
|
@@ -12063,6 +12158,11 @@ async function runBrainstormReport(roadmap, deps = {}) {
|
|
|
12063
12158
|
...deps.config ? { config: deps.config } : {}
|
|
12064
12159
|
});
|
|
12065
12160
|
const level = verdict.verdict.level;
|
|
12161
|
+
if (deps.gatePlausible === true && !isPlausibleForModel(verdict)) {
|
|
12162
|
+
rows.push(skippedBrainstormRow(feature, verdict, level));
|
|
12163
|
+
continue;
|
|
12164
|
+
}
|
|
12165
|
+
deps.onProgress?.(`Brainstorming "${feature.name}" [${level}]\u2026`);
|
|
12066
12166
|
const wiringDeps = {
|
|
12067
12167
|
...deps.generator ? { generator: deps.generator } : {},
|
|
12068
12168
|
...deps.provider ? { provider: deps.provider } : {},
|
|
@@ -12160,10 +12260,29 @@ function renderBrainstormJson(rows) {
|
|
|
12160
12260
|
2
|
|
12161
12261
|
);
|
|
12162
12262
|
}
|
|
12163
|
-
function resolveBrainstormProvider(configPath, model) {
|
|
12263
|
+
async function resolveBrainstormProvider(configPath, model) {
|
|
12164
12264
|
const configResult = resolveConfig(configPath);
|
|
12165
12265
|
if (!configResult.ok) return null;
|
|
12166
|
-
|
|
12266
|
+
const config = configResult.value;
|
|
12267
|
+
const localBackend = pickLocalBackendConn(config);
|
|
12268
|
+
const preferredLocalModel = await resolvePreferredLocalModel("reasoning", {
|
|
12269
|
+
...localBackend?.endpoint !== void 0 ? { endpoint: localBackend.endpoint } : {},
|
|
12270
|
+
...localBackend?.apiKey !== void 0 ? { apiKey: localBackend.apiKey } : {}
|
|
12271
|
+
});
|
|
12272
|
+
return resolveTriageProvider(config, model, preferredLocalModel);
|
|
12273
|
+
}
|
|
12274
|
+
function pickLocalBackendConn(config) {
|
|
12275
|
+
const backends = config.agent?.backends;
|
|
12276
|
+
if (!backends) return void 0;
|
|
12277
|
+
for (const def of Object.values(backends)) {
|
|
12278
|
+
if (def.type === "local" || def.type === "pi") {
|
|
12279
|
+
return {
|
|
12280
|
+
...def.endpoint !== void 0 ? { endpoint: def.endpoint } : {},
|
|
12281
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
12282
|
+
};
|
|
12283
|
+
}
|
|
12284
|
+
}
|
|
12285
|
+
return void 0;
|
|
12167
12286
|
}
|
|
12168
12287
|
async function runApproveCommand(cwd, opts) {
|
|
12169
12288
|
const configResult = resolveConfig(opts.config);
|
|
@@ -12180,7 +12299,7 @@ async function runApproveCommand(cwd, opts) {
|
|
|
12180
12299
|
const parsed = parseRoadmap(fs36.readFileSync(roadmapPath, "utf-8"));
|
|
12181
12300
|
if (!parsed.ok) return { marked: [], held: [], note: "roadmap-parse-error" };
|
|
12182
12301
|
const graphStore = await loadGraphStore(cwd);
|
|
12183
|
-
const provider = resolveBrainstormProvider(opts.config);
|
|
12302
|
+
const provider = await resolveBrainstormProvider(opts.config);
|
|
12184
12303
|
const precedent = await buildPrecedentLookup(cwd);
|
|
12185
12304
|
const rows = await runBrainstormReport(parsed.value, {
|
|
12186
12305
|
...graphStore ? { graphStore } : {},
|
|
@@ -12262,59 +12381,93 @@ function createRoadmapTriageCommand() {
|
|
|
12262
12381
|
).option(
|
|
12263
12382
|
"--brainstorm",
|
|
12264
12383
|
"Run the autonomous brainstorm per candidate: draft a spec (docs only) or halt to a human at the first fork it can not confidently recommend. No dispatch, no execution."
|
|
12265
|
-
).
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
logger.error(
|
|
12279
|
-
`No roadmap aggregate at ${roadmapPath}. If your roadmap is sharded, run \`harness roadmap regen\` first, then re-run triage.`
|
|
12280
|
-
);
|
|
12281
|
-
process.exitCode = 1;
|
|
12282
|
-
return;
|
|
12283
|
-
}
|
|
12284
|
-
const parsed = parseRoadmap(fs36.readFileSync(roadmapPath, "utf-8"));
|
|
12285
|
-
if (!parsed.ok) {
|
|
12286
|
-
logger.error(`Failed to parse roadmap: ${parsed.error.message}`);
|
|
12287
|
-
process.exitCode = 1;
|
|
12288
|
-
return;
|
|
12289
|
-
}
|
|
12290
|
-
const graphStore = await loadGraphStore(cwd);
|
|
12291
|
-
const precedent = await buildPrecedentLookup(cwd);
|
|
12292
|
-
if (opts.brainstorm) {
|
|
12293
|
-
const provider = resolveBrainstormProvider(globalOpts.config);
|
|
12294
|
-
const rows2 = await runBrainstormReport(parsed.value, {
|
|
12295
|
-
...graphStore ? { graphStore } : {},
|
|
12296
|
-
...provider ? { provider } : {},
|
|
12297
|
-
...precedent ? { precedent } : {},
|
|
12298
|
-
// Specs are written under docs/changes/<slug>/proposal.md (docs only, no dispatch).
|
|
12299
|
-
docsRoot: path60.join(cwd, "docs")
|
|
12300
|
-
});
|
|
12301
|
-
if (globalOpts.json) {
|
|
12302
|
-
process.stdout.write(renderBrainstormJson(rows2) + "\n");
|
|
12303
|
-
} else {
|
|
12304
|
-
logger.info(renderBrainstormHuman(rows2));
|
|
12305
|
-
}
|
|
12306
|
-
return;
|
|
12384
|
+
).option(
|
|
12385
|
+
"--only <substring>",
|
|
12386
|
+
'Process ONLY roadmap items whose title contains this substring (case-insensitive). Lets you triage/brainstorm a single item, e.g. --only "prefer-execfile".'
|
|
12387
|
+
).option(
|
|
12388
|
+
"--limit <n>",
|
|
12389
|
+
"Process at most N items (applied after --only). Useful for a quick partial scan.",
|
|
12390
|
+
(v) => Number.parseInt(v, 10)
|
|
12391
|
+
).option(
|
|
12392
|
+
"--offline",
|
|
12393
|
+
"Force the pure static path \u2014 never consult the local model. A fast scan that holds every item to a human (byte-identical to running without a resolvable model)."
|
|
12394
|
+
).action(
|
|
12395
|
+
async (opts, cmd) => {
|
|
12396
|
+
await runTriageCommandAction(opts, cmd);
|
|
12307
12397
|
}
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12398
|
+
).addCommand(createTriageApproveCommand());
|
|
12399
|
+
}
|
|
12400
|
+
async function runTriageCommandAction(opts, cmd) {
|
|
12401
|
+
const cwd = process.cwd();
|
|
12402
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
12403
|
+
const parsed = gateAndParseRoadmap(cwd, globalOpts.config);
|
|
12404
|
+
if (!parsed) return;
|
|
12405
|
+
const graphStore = await loadGraphStore(cwd);
|
|
12406
|
+
const precedent = await buildPrecedentLookup(cwd);
|
|
12407
|
+
const provider = opts.offline === true ? null : await resolveBrainstormProvider(globalOpts.config);
|
|
12408
|
+
const onProgress = globalOpts.json === true ? void 0 : (message) => logger.info(message);
|
|
12409
|
+
const shared = {
|
|
12410
|
+
...graphStore ? { graphStore } : {},
|
|
12411
|
+
...provider ? { provider } : {},
|
|
12412
|
+
...precedent ? { precedent } : {},
|
|
12413
|
+
...opts.only !== void 0 ? { only: opts.only } : {},
|
|
12414
|
+
...typeof opts.limit === "number" && !Number.isNaN(opts.limit) ? { limit: opts.limit } : {},
|
|
12415
|
+
...onProgress ? { onProgress } : {}
|
|
12416
|
+
};
|
|
12417
|
+
if (opts.brainstorm) {
|
|
12418
|
+
const rows2 = await runBrainstormReport(parsed, {
|
|
12419
|
+
...shared,
|
|
12420
|
+
gatePlausible: true,
|
|
12421
|
+
docsRoot: path60.join(cwd, "docs")
|
|
12311
12422
|
});
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12423
|
+
emitReport(
|
|
12424
|
+
globalOpts.json === true,
|
|
12425
|
+
() => renderBrainstormJson(rows2),
|
|
12426
|
+
() => renderBrainstormHuman(rows2)
|
|
12427
|
+
);
|
|
12428
|
+
return;
|
|
12429
|
+
}
|
|
12430
|
+
const rows = await runTriageReport(parsed, {
|
|
12431
|
+
...shared,
|
|
12432
|
+
...opts.offline === true ? { offline: true } : {}
|
|
12433
|
+
});
|
|
12434
|
+
emitReport(
|
|
12435
|
+
globalOpts.json === true,
|
|
12436
|
+
() => renderJson(rows),
|
|
12437
|
+
() => renderHuman(rows)
|
|
12438
|
+
);
|
|
12439
|
+
}
|
|
12440
|
+
function gateAndParseRoadmap(cwd, configPath) {
|
|
12441
|
+
const configResult = resolveConfig(configPath);
|
|
12442
|
+
const enabled = configResult.ok && configResult.value.roadmap?.autoTriage?.enabled === true;
|
|
12443
|
+
if (!enabled) {
|
|
12444
|
+
logger.info(
|
|
12445
|
+
"Roadmap auto-triage is disabled (roadmap.autoTriage.enabled is not true). Enable it in harness.config.json to run the read-only triage report. No changes made."
|
|
12446
|
+
);
|
|
12447
|
+
return null;
|
|
12448
|
+
}
|
|
12449
|
+
const roadmapPath = path60.join(cwd, "docs", "roadmap.md");
|
|
12450
|
+
if (!fs36.existsSync(roadmapPath)) {
|
|
12451
|
+
logger.error(
|
|
12452
|
+
`No roadmap aggregate at ${roadmapPath}. If your roadmap is sharded, run \`harness roadmap regen\` first, then re-run triage.`
|
|
12453
|
+
);
|
|
12454
|
+
process.exitCode = 1;
|
|
12455
|
+
return null;
|
|
12456
|
+
}
|
|
12457
|
+
const parsed = parseRoadmap(fs36.readFileSync(roadmapPath, "utf-8"));
|
|
12458
|
+
if (!parsed.ok) {
|
|
12459
|
+
logger.error(`Failed to parse roadmap: ${parsed.error.message}`);
|
|
12460
|
+
process.exitCode = 1;
|
|
12461
|
+
return null;
|
|
12462
|
+
}
|
|
12463
|
+
return parsed.value;
|
|
12464
|
+
}
|
|
12465
|
+
function emitReport(json, toJson, toHuman) {
|
|
12466
|
+
if (json) {
|
|
12467
|
+
process.stdout.write(toJson() + "\n");
|
|
12468
|
+
} else {
|
|
12469
|
+
logger.info(toHuman());
|
|
12470
|
+
}
|
|
12318
12471
|
}
|
|
12319
12472
|
|
|
12320
12473
|
// src/commands/roadmap/index.ts
|
|
@@ -14026,6 +14179,17 @@ function detectComplexity(projectPath) {
|
|
|
14026
14179
|
}
|
|
14027
14180
|
|
|
14028
14181
|
// src/commands/skill/preamble.ts
|
|
14182
|
+
var AUTONOMOUS_SECTION = `## Autonomous Decision-Making: Active
|
|
14183
|
+
|
|
14184
|
+
You are running this workflow **headless \u2014 there is no human in this session to answer questions.**
|
|
14185
|
+
|
|
14186
|
+
Wherever this skill instructs you to present options and a recommendation and then STOP and wait for the human (design forks, \`emit_interaction\`/\`AskUserQuestion\` prompts, spec sign-off, the next-skill handoff), do NOT wait. Instead:
|
|
14187
|
+
|
|
14188
|
+
1. Do the full analysis exactly as written \u2014 honor every gate (at least two approaches with tradeoffs, YAGNI, the persona council, soundness review). Autonomy narrows the decider, never the rigor.
|
|
14189
|
+
2. Commit to the option **you** recommend, and record the question, the chosen answer, and the rationale in the spec's **"Decisions made"** section.
|
|
14190
|
+
3. Continue to the next step. Self-approve the Phase-4 sign-off once the soundness review converges; proceed through the handoff to the next skill.
|
|
14191
|
+
|
|
14192
|
+
**Safety valve \u2014 surface, do not bury.** For any decision made at **low confidence**, and for any **strategy contradiction** the skill says to "never auto-resolve," still decide and proceed, but additionally record it under an **"## Autonomous decisions requiring review"** heading in the spec (question, your call, confidence, why) so the human's PR review sees exactly what you decided and where you were unsure. You are the decider, not a shortcut \u2014 the human's checkpoint is the PR, not a mid-run pause.`;
|
|
14029
14193
|
function buildActivePhasesSection(complexity, phases) {
|
|
14030
14194
|
const lines = [`## Active Phases (complexity: ${complexity})`];
|
|
14031
14195
|
for (const phase of phases) {
|
|
@@ -14050,6 +14214,9 @@ ${priorState}`);
|
|
|
14050
14214
|
}
|
|
14051
14215
|
function buildPreamble(options) {
|
|
14052
14216
|
const sections = [];
|
|
14217
|
+
if (options.autonomous) {
|
|
14218
|
+
sections.push(AUTONOMOUS_SECTION);
|
|
14219
|
+
}
|
|
14053
14220
|
if (options.complexity && options.phases && options.phases.length > 0) {
|
|
14054
14221
|
sections.push(buildActivePhasesSection(options.complexity, options.phases));
|
|
14055
14222
|
}
|
|
@@ -14168,7 +14335,8 @@ async function runSkill(name, opts) {
|
|
|
14168
14335
|
...opts.phase !== void 0 && { phase: opts.phase },
|
|
14169
14336
|
...priorState !== void 0 && { priorState },
|
|
14170
14337
|
...stateWarning !== void 0 && { stateWarning },
|
|
14171
|
-
...opts.party !== void 0 && { party: opts.party }
|
|
14338
|
+
...opts.party !== void 0 && { party: opts.party },
|
|
14339
|
+
...opts.autonomous !== void 0 && { autonomous: opts.autonomous }
|
|
14172
14340
|
});
|
|
14173
14341
|
const skillMdPath = path66.join(skillDir, "SKILL.md");
|
|
14174
14342
|
if (!fs42.existsSync(skillMdPath)) {
|
|
@@ -14189,6 +14357,9 @@ async function runSkill(name, opts) {
|
|
|
14189
14357
|
}
|
|
14190
14358
|
function createRunCommand3() {
|
|
14191
14359
|
return new Command104("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Rigor level: fast, standard, thorough", "standard").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").option(
|
|
14360
|
+
"--autonomous",
|
|
14361
|
+
"Headless mode: inject the autonomous-decider preamble so the agent decides every fork at full rigor (recording each decision in the spec) instead of pausing for a human"
|
|
14362
|
+
).option(
|
|
14192
14363
|
"--backend <name>",
|
|
14193
14364
|
"Spec B: one-shot routing override forwarded to the orchestrator as HARNESS_BACKEND_OVERRIDE"
|
|
14194
14365
|
).action(async (name, opts) => runSkill(name, opts));
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface PreambleOptions {
|
|
|
18
18
|
priorState?: string;
|
|
19
19
|
stateWarning?: string;
|
|
20
20
|
party?: boolean;
|
|
21
|
+
autonomous?: boolean;
|
|
21
22
|
}
|
|
22
23
|
declare function buildPreamble(options: PreambleOptions): string;
|
|
23
24
|
|
|
@@ -864,6 +865,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
864
865
|
model?: string | undefined;
|
|
865
866
|
} | undefined;
|
|
866
867
|
}>>;
|
|
868
|
+
workflowGates: z.ZodOptional<z.ZodEnum<["local", "primary"]>>;
|
|
867
869
|
}, "strict", z.ZodTypeAny, {
|
|
868
870
|
default: string | readonly [string, ...string[]];
|
|
869
871
|
'quick-fix'?: string | readonly [string, ...string[]] | undefined;
|
|
@@ -898,6 +900,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
898
900
|
model?: string | undefined;
|
|
899
901
|
} | undefined;
|
|
900
902
|
} | undefined;
|
|
903
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
901
904
|
}, {
|
|
902
905
|
default: string | readonly [string, ...string[]];
|
|
903
906
|
'quick-fix'?: string | readonly [string, ...string[]] | undefined;
|
|
@@ -932,6 +935,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
932
935
|
model?: string | undefined;
|
|
933
936
|
} | undefined;
|
|
934
937
|
} | undefined;
|
|
938
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
935
939
|
}>>;
|
|
936
940
|
}, "strip", z.ZodTypeAny, {
|
|
937
941
|
executor: "subprocess" | "cloud" | "noop";
|
|
@@ -1059,6 +1063,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
1059
1063
|
model?: string | undefined;
|
|
1060
1064
|
} | undefined;
|
|
1061
1065
|
} | undefined;
|
|
1066
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
1062
1067
|
} | undefined;
|
|
1063
1068
|
}, {
|
|
1064
1069
|
executor?: "subprocess" | "cloud" | "noop" | undefined;
|
|
@@ -1186,6 +1191,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
1186
1191
|
model?: string | undefined;
|
|
1187
1192
|
} | undefined;
|
|
1188
1193
|
} | undefined;
|
|
1194
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
1189
1195
|
} | undefined;
|
|
1190
1196
|
}>>;
|
|
1191
1197
|
/** Source-file ingestion controls (skip-dirs, exclude patterns, gitignore handling) */
|
|
@@ -2825,6 +2831,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
2825
2831
|
model?: string | undefined;
|
|
2826
2832
|
} | undefined;
|
|
2827
2833
|
} | undefined;
|
|
2834
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
2828
2835
|
} | undefined;
|
|
2829
2836
|
} | undefined;
|
|
2830
2837
|
ingest?: {
|
|
@@ -3247,6 +3254,7 @@ declare const HarnessConfigSchema: z.ZodObject<{
|
|
|
3247
3254
|
model?: string | undefined;
|
|
3248
3255
|
} | undefined;
|
|
3249
3256
|
} | undefined;
|
|
3257
|
+
workflowGates?: "local" | "primary" | undefined;
|
|
3250
3258
|
} | undefined;
|
|
3251
3259
|
} | undefined;
|
|
3252
3260
|
ingest?: {
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
tracker:
|
|
3
|
+
kind: roadmap
|
|
4
|
+
filePath: docs/roadmap.md
|
|
5
|
+
activeStates: [planned, in-progress]
|
|
6
|
+
terminalStates: [done]
|
|
7
|
+
polling:
|
|
8
|
+
intervalMs: 30000
|
|
9
|
+
workspace:
|
|
10
|
+
root: .harness/workspaces
|
|
11
|
+
hooks:
|
|
12
|
+
afterCreate: null
|
|
13
|
+
beforeRun: null
|
|
14
|
+
afterRun: null
|
|
15
|
+
beforeRemove: null
|
|
16
|
+
timeoutMs: 60000
|
|
17
|
+
agent:
|
|
18
|
+
# Named backend definitions (Spec 2). See docs/guides/multi-backend-routing.md.
|
|
19
|
+
# `capabilities` (tier/cost/privacy/context) let AMR compare backends and select a
|
|
20
|
+
# capability tier per dispatch — a backend without one is invisible to tier selection.
|
|
21
|
+
backends:
|
|
22
|
+
primary:
|
|
23
|
+
type: claude
|
|
24
|
+
command: claude
|
|
25
|
+
capabilities:
|
|
26
|
+
{ tier: strong, costPer1kTokens: 15, privacyClass: shared-cloud, contextWindow: 200000 }
|
|
27
|
+
# Local backend for autonomous execution of simple tasks.
|
|
28
|
+
# `model` accepts a string OR a prefer-and-fallback array — first
|
|
29
|
+
# match wins after a `/v1/models` probe.
|
|
30
|
+
local:
|
|
31
|
+
type: pi
|
|
32
|
+
# Ollama's OpenAI-compatible API lives under /v1 (the resolver probes
|
|
33
|
+
# `${endpoint}/models`). Names must match `ollama list` exactly.
|
|
34
|
+
endpoint: http://127.0.0.1:11434/v1
|
|
35
|
+
# Prefer-and-fallback: first name present in /v1/models wins. Coder model
|
|
36
|
+
# first (local routes are quick-fix/diagnostic), general model as fallback.
|
|
37
|
+
# Quote the names — the ':tag' colon otherwise breaks YAML flow parsing.
|
|
38
|
+
model: ['qwen2.5-coder:7b', 'gemma3n:e4b']
|
|
39
|
+
capabilities:
|
|
40
|
+
{ tier: fast, costPer1kTokens: 0, privacyClass: on-device, contextWindow: 32768 }
|
|
41
|
+
# Routing — controls WHICH backend handles each use case.
|
|
42
|
+
routing:
|
|
43
|
+
default: primary
|
|
44
|
+
quick-fix: local
|
|
45
|
+
diagnostic: local
|
|
46
|
+
# Route the intelligence pipeline (sel/pesl) to the local backend.
|
|
47
|
+
intelligence:
|
|
48
|
+
sel: local
|
|
49
|
+
pesl: local
|
|
50
|
+
# AMR (Adaptive Model Routing) — opt-in; its PRESENCE flips dispatch from the
|
|
51
|
+
# identity/default chain to complexity-aware tier selection. trivial/simple →
|
|
52
|
+
# local (fast, free); moderate/complex → claude (no `standard` backend, so a
|
|
53
|
+
# `standard` requirement resolves to the cheapest backend at-or-above it =
|
|
54
|
+
# primary). The always-on baseline-relative security-defect feeder climbs a
|
|
55
|
+
# unit's tier after `escalationThreshold` consecutive quality failures
|
|
56
|
+
# (strong-capped, then hard-fails to a human). Budget cap + LLM acceptance-eval
|
|
57
|
+
# are available opt-ins (docs/guides/adaptive-model-routing.md) — left off here.
|
|
58
|
+
policy:
|
|
59
|
+
complexityTierMatrix: { trivial: fast, simple: fast, moderate: standard, complex: strong }
|
|
60
|
+
escalationThreshold: 2
|
|
61
|
+
# Escalation — controls WHETHER a tier dispatches at all (orthogonal to routing).
|
|
62
|
+
escalation:
|
|
63
|
+
alwaysHuman: [full-exploration]
|
|
64
|
+
autoExecute: [quick-fix, diagnostic]
|
|
65
|
+
primaryExecute: [guided-change]
|
|
66
|
+
signalGated: []
|
|
67
|
+
diagnosticRetryBudget: 1
|
|
68
|
+
maxConcurrentAgents: 1
|
|
69
|
+
maxTurns: 10
|
|
70
|
+
maxRetryBackoffMs: 5000
|
|
71
|
+
maxConcurrentAgentsByState: {}
|
|
72
|
+
globalCooldownMs: 60000
|
|
73
|
+
maxRequestsPerMinute: 50
|
|
74
|
+
maxRequestsPerSecond: 1
|
|
75
|
+
# Default limits based on Anthropic Tier 3. Adjust according to your account tier.
|
|
76
|
+
# Tier 1: 40k ITPM / 10k OTPM
|
|
77
|
+
# Tier 2: 200k ITPM / 40k OTPM
|
|
78
|
+
# Tier 3: 400k ITPM / 80k OTPM
|
|
79
|
+
# Tier 4: 1m ITPM / 200k OTPM
|
|
80
|
+
maxInputTokensPerMinute: 400000
|
|
81
|
+
maxOutputTokensPerMinute: 80000
|
|
82
|
+
turnTimeoutMs: 300000
|
|
83
|
+
readTimeoutMs: 30000
|
|
84
|
+
stallTimeoutMs: 60000
|
|
85
|
+
intelligence:
|
|
86
|
+
enabled: true
|
|
87
|
+
requestTimeoutMs: 180000
|
|
88
|
+
server:
|
|
89
|
+
port: 8080
|
|
90
|
+
localModels:
|
|
91
|
+
enabled: true
|
|
92
|
+
pool:
|
|
93
|
+
diskBudgetGb: 100
|
|
94
|
+
allowedOrgs: [Qwen, deepseek-ai, meta-llama, google]
|
|
95
|
+
allowedFamilies: []
|
|
96
|
+
refresh:
|
|
97
|
+
intervalMs: 86400000
|
|
98
|
+
proposalThreshold: 5
|
|
99
|
+
jitterMs: 600000
|
|
100
|
+
installer:
|
|
101
|
+
backend: ollama
|
|
102
|
+
ollamaEndpoint: http://127.0.0.1:11434
|
|
103
|
+
# Built-in maintenance tasks run on cron when `maintenance.enabled: true`.
|
|
104
|
+
# Notable housekeeping tasks: `main-sync` (every 15 min) fast-forwards the
|
|
105
|
+
# orchestrator's local default branch from origin so files read from `cwd`
|
|
106
|
+
# (e.g., docs/roadmap.md, harness.orchestrator.md) stay current. Sync is
|
|
107
|
+
# fast-forward-only — never destructive — and skips with a structured
|
|
108
|
+
# warning event if the working tree is dirty, the branch is wrong, or the
|
|
109
|
+
# local default has diverged. Disable all maintenance via `maintenance.enabled: false`.
|
|
110
|
+
#
|
|
111
|
+
# Per-task Run Now (since 2026-05-09): the dashboard Maintenance page renders
|
|
112
|
+
# a Run Now button on every row of the schedule table. The previous single-
|
|
113
|
+
# button affordance (which always triggered `project-health`) has been
|
|
114
|
+
# removed. Each button is disabled while a `maintenance:started` event is in
|
|
115
|
+
# flight for that task ID and re-enables on the matching `maintenance:completed`
|
|
116
|
+
# or `maintenance:error` event.
|
|
117
|
+
maintenance:
|
|
118
|
+
enabled: true
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
# Prompt Template (Local Backend — bootstrap shim)
|
|
122
|
+
|
|
123
|
+
You are a local agent with `bash`, `read`, `write`, `grep`, and `find`. You do
|
|
124
|
+
NOT have `/harness:*` slash commands or harness MCP tools. You run the REAL
|
|
125
|
+
harness workflow skills by reading them over bash — this template carries no
|
|
126
|
+
methodology of its own.
|
|
127
|
+
|
|
128
|
+
## Issue: {{ issue.title }}
|
|
129
|
+
|
|
130
|
+
**Identifier:** {{ issue.identifier }}
|
|
131
|
+
**Description:** {{ issue.description }}
|
|
132
|
+
|
|
133
|
+
## How to run a harness skill (the indirection rule)
|
|
134
|
+
|
|
135
|
+
To run any harness workflow skill, execute it over bash and follow its output
|
|
136
|
+
**verbatim**:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
harness skill run <skill-name> --autonomous --path .
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`harness skill run` prints the skill's full instructions (the same content the
|
|
143
|
+
primary backend gets from a `/harness:*` slash command) to stdout as a plain CLI
|
|
144
|
+
read. `--autonomous` prepends the headless-decider preamble: you do the full
|
|
145
|
+
analysis at full rigor but YOU decide every fork and record it in the spec — you
|
|
146
|
+
never pause for a human.
|
|
147
|
+
|
|
148
|
+
**Redirect rule.** Whenever a skill's output instructs you to run `/harness:X`,
|
|
149
|
+
instead run `harness skill run harness-X --autonomous`. Slash commands are
|
|
150
|
+
unavailable on this backend; the skill-run indirection is their equivalent.
|
|
151
|
+
|
|
152
|
+
## Full-workflow entry sequence
|
|
153
|
+
|
|
154
|
+
Run these in order, each via `harness skill run <name> --autonomous --path .`,
|
|
155
|
+
following each skill's output before moving on:
|
|
156
|
+
|
|
157
|
+
1. `harness-brainstorming` — runs at full rigor (≥2 approaches, YAGNI, persona
|
|
158
|
+
council, soundness), but YOU decide the forks (autonomous) and record each
|
|
159
|
+
decision in the spec.
|
|
160
|
+
2. `harness-planning`
|
|
161
|
+
3. `harness-execution` (or `harness-tdd` for test-driven work)
|
|
162
|
+
4. `harness-verification`
|
|
163
|
+
5. `harness-code-review`
|
|
164
|
+
|
|
165
|
+
Run `harness skill list` to see the full roster of available skills.
|
|
166
|
+
|
|
167
|
+
## Gates (enforced by the harness, not just by you)
|
|
168
|
+
|
|
169
|
+
The orchestrator INDEPENDENTLY enforces `harness validate` plus the
|
|
170
|
+
verify/outcome-eval gates against your branch after you exit — you cannot ship
|
|
171
|
+
past a red gate. Reach a green state: run `harness validate` and the project's
|
|
172
|
+
own typecheck/lint/test yourself and fix every failure before shipping. A run
|
|
173
|
+
that cannot reach green is halted and re-dispatched rather than shipped, and
|
|
174
|
+
escalated to a human if the retry budget is exhausted.
|
|
175
|
+
|
|
176
|
+
## Ship (only after the gates are green)
|
|
177
|
+
|
|
178
|
+
- Create a topic branch if you are still on `main`/`master`
|
|
179
|
+
(e.g. `feat/{{ issue.identifier }}`).
|
|
180
|
+
- Stage your changes and create a descriptive commit (Conventional Commits style).
|
|
181
|
+
- Push the branch with `git push -u origin HEAD`.
|
|
182
|
+
- Open a pull request with `gh pr create`. Use a HEREDOC for the body:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
|
186
|
+
## Summary
|
|
187
|
+
|
|
188
|
+
<body content with real newlines>
|
|
189
|
+
EOF
|
|
190
|
+
)"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- Report the PR URL as your final output, then stop.
|
|
194
|
+
|
|
195
|
+
Attempt Number: {{ attempt }}
|
|
@@ -82,6 +82,11 @@ Your goal is to implement the following issue using the standard Harness Enginee
|
|
|
82
82
|
Follow these steps exactly, using the corresponding slash commands to ensure
|
|
83
83
|
high-quality, architecturally sound delivery:
|
|
84
84
|
|
|
85
|
+
<!-- This slash-command workflow targets Claude-shaped backends. A backend-specific
|
|
86
|
+
variant, `harness.orchestrator.local.md`, is auto-selected for `local`/`pi`
|
|
87
|
+
backends (which lack `/harness:*` slash commands) and expresses the same
|
|
88
|
+
workflow via bash `harness <gate>` CLI calls. See ADR 0071. -->
|
|
89
|
+
|
|
85
90
|
1. **Brainstorming:** Use `/harness:brainstorming` to explore the problem space
|
|
86
91
|
and draft a technical proposal in `docs/changes/`. The spec MUST include an
|
|
87
92
|
Integration Points section defining how the feature connects to the system.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "CLI for Harness Engineering toolkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
"web-tree-sitter": "^0.24.7",
|
|
40
40
|
"yaml": "^2.8.3",
|
|
41
41
|
"zod": "^3.25.76",
|
|
42
|
-
"@harness-engineering/core": "0.37.
|
|
43
|
-
"@harness-engineering/
|
|
44
|
-
"@harness-engineering/
|
|
42
|
+
"@harness-engineering/core": "0.37.1",
|
|
43
|
+
"@harness-engineering/dashboard": "0.14.5",
|
|
44
|
+
"@harness-engineering/graph": "0.11.9",
|
|
45
45
|
"@harness-engineering/linter-gen": "0.1.7",
|
|
46
|
-
"@harness-engineering/orchestrator": "0.
|
|
47
|
-
"@harness-engineering/
|
|
48
|
-
"@harness-engineering/
|
|
46
|
+
"@harness-engineering/orchestrator": "0.16.0",
|
|
47
|
+
"@harness-engineering/signals": "0.2.7",
|
|
48
|
+
"@harness-engineering/types": "0.23.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@harness-engineering/intelligence": "0.
|
|
51
|
+
"@harness-engineering/intelligence": "0.9.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@harness-engineering/intelligence": {
|