karajan-code 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/package.json +1 -1
- package/packages/hu-board/src/db.js +11 -0
- package/scripts/verify-pack.mjs +41 -0
- package/src/autonomy/arbiter-role.js +71 -0
- package/src/autonomy/arbiter.js +22 -0
- package/src/autonomy/decider.js +18 -0
- package/src/autonomy/decision-resolver.js +58 -0
- package/src/autonomy/outcome-report.js +55 -0
- package/src/autonomy/policy.js +41 -0
- package/src/autonomy/stage-ask.js +30 -0
- package/src/checks/native-build.js +64 -0
- package/src/cli/register-pipeline.js +21 -0
- package/src/commands/autorun.js +49 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/plan/generate.js +3 -2
- package/src/commands/run.js +24 -7
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +2 -1
- package/src/orchestrator/flow-control.js +7 -1
- package/src/prompts/arbiter.js +38 -0
- package/src/spec-review/run-spec-review.js +38 -10
- package/src/utils/wizard.js +9 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v3.
|
|
26
|
+
> **v3.7.1 released** — Patch: fixes a `kj init` crash when configuring the Plan B fallback, and makes pnpm installs safe — `kj doctor` now flags when pnpm skipped `better-sqlite3`'s native build and tells you exactly how to fix it (`pnpm approve-builds better-sqlite3`). The release gate checks pnpm too. Built on **v3.7.0 — Autonomous delivery**: `kj autorun <spec>` chains spec → plan → run every user story → outcome in one command, unattended, with an **Arbiter** that resolves agent conflicts by picking the least-bad call; autonomy is opt-in, interactive runs are unchanged. Full notes in [CHANGELOG.md](CHANGELOG.md).
|
|
27
27
|
|
|
28
28
|
You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
|
|
29
29
|
|
|
@@ -71,6 +71,33 @@ Two technical facts worth keeping straight:
|
|
|
71
71
|
|
|
72
72
|
Full write-up with mental mapping for Genkit / Mastra / LangChain / Vercel AI SDK developers: **[docs/COMPARISON.md](./docs/COMPARISON.md)**.
|
|
73
73
|
|
|
74
|
+
## Loop engineering
|
|
75
|
+
|
|
76
|
+
Prompt engineering got one good answer from one good prompt. Context engineering
|
|
77
|
+
curated what the model saw. The 2026 frontier is **loop engineering**: you stop
|
|
78
|
+
prompting the agent by hand and design the control system that prompts it, checks
|
|
79
|
+
it, and decides what happens next — until the goal is met or it hands back to you.
|
|
80
|
+
|
|
81
|
+
Karajan is a loop-engineering runtime, built around the loop before the term
|
|
82
|
+
caught on:
|
|
83
|
+
|
|
84
|
+
- **Maker / checker split** — a coder against independent reviewer / tester /
|
|
85
|
+
security roles, with Solomon judging disputes. The maker never grades its own work.
|
|
86
|
+
- **Deterministic verification** — TDD, per-HU acceptance tests, SonarQube gates,
|
|
87
|
+
and deterministic guards. Checking is tests, not vibes.
|
|
88
|
+
- **The autonomy ladder (L1 → L2 → L3)** — the `interactive | assisted |
|
|
89
|
+
autonomous` axis (v3.7.0). Report, then assisted fixes, then unattended
|
|
90
|
+
`kj autorun` — defaulting to `interactive`, so you opt in.
|
|
91
|
+
- **A durable state spine** — sessions, the HU Board, journals, the RAG index and
|
|
92
|
+
`kj resume` keep the loop alive outside any one conversation.
|
|
93
|
+
|
|
94
|
+
The honest caveat loop engineering insists on — *unattended loops make unattended
|
|
95
|
+
mistakes* — is designed in: autonomous runs list their residual defects, every HU
|
|
96
|
+
lands behind a PR, and `kj-trash` snapshots destructive operations.
|
|
97
|
+
|
|
98
|
+
Full mapping of the loop-engineering building blocks onto Karajan:
|
|
99
|
+
**[docs/loop-engineering.md](./docs/loop-engineering.md)**.
|
|
100
|
+
|
|
74
101
|
## Install
|
|
75
102
|
|
|
76
103
|
**npm** (recommended):
|
|
@@ -83,6 +110,8 @@ npm install -g karajan-code
|
|
|
83
110
|
brew install manufosela/tap/karajan-code
|
|
84
111
|
```
|
|
85
112
|
|
|
113
|
+
> **Installing with pnpm?** pnpm blocks dependency build scripts by default, so `better-sqlite3`'s native addon won't compile on install and DB-backed commands (`board`, `rag`, cost tracking) will fail. After installing, run `pnpm approve-builds better-sqlite3` — or just use npm. `kj doctor` flags this if it happens.
|
|
114
|
+
|
|
86
115
|
**Standalone binary** (no Node.js needed):
|
|
87
116
|
```bash
|
|
88
117
|
# macOS (Apple Silicon)
|
package/package.json
CHANGED
|
@@ -361,6 +361,17 @@ export function getDb() {
|
|
|
361
361
|
return db;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Lazily initialise the DB if it isn't open yet (KJC-BUG-0090). The cost
|
|
366
|
+
* writers run from `kj run`/`kj autorun` paths that may never start the board
|
|
367
|
+
* (which is what normally calls initDb), so cost writes failed with
|
|
368
|
+
* "Database not initialized". This makes those writers self-sufficient.
|
|
369
|
+
*/
|
|
370
|
+
export function ensureDb() {
|
|
371
|
+
if (!db) initDb();
|
|
372
|
+
return db;
|
|
373
|
+
}
|
|
374
|
+
|
|
364
375
|
/**
|
|
365
376
|
* Inserts or updates a project record.
|
|
366
377
|
* @param {{ id: string, name?: string, first_seen?: string, last_activity?: string, total_stories?: number }} project
|
package/scripts/verify-pack.mjs
CHANGED
|
@@ -50,6 +50,7 @@ function fail(msg, detail) {
|
|
|
50
50
|
|
|
51
51
|
let tgzPath = null;
|
|
52
52
|
let tmpDir = null;
|
|
53
|
+
let pnpmTmp = null;
|
|
53
54
|
try {
|
|
54
55
|
console.log(`verify-pack: packing karajan-code@${expectedVersion}…`);
|
|
55
56
|
// --json gives us the exact filename without parsing human output.
|
|
@@ -114,8 +115,48 @@ try {
|
|
|
114
115
|
}
|
|
115
116
|
console.log("verify-pack: kj-trash ships + boots ✓");
|
|
116
117
|
|
|
118
|
+
// 5. pnpm install smoke (KJC-TSK-0580). pnpm's layout differs from npm's
|
|
119
|
+
// (a symlinked virtual store), so it can break resolution of the bundled
|
|
120
|
+
// @karajan/core the way npm packaging breakage did before (KJC-BUG-0082/0086).
|
|
121
|
+
// Verify a pnpm install resolves the JS tree and `kj --version` boots — every
|
|
122
|
+
// release catches a pnpm packaging regression. (pnpm also blocks
|
|
123
|
+
// better-sqlite3's native build by default, so DB-backed commands need
|
|
124
|
+
// `pnpm approve-builds`; that's surfaced as a doctor warning, KJC-BUG-0092 —
|
|
125
|
+
// not a gate failure, since it's inherent to pnpm.) Auto-skips without pnpm.
|
|
126
|
+
const hasPnpm = spawnSync("pnpm", ["--version"], { encoding: "utf8", env: childEnv }).status === 0;
|
|
127
|
+
if (!hasPnpm) {
|
|
128
|
+
console.log("verify-pack: pnpm not installed — skipping pnpm smoke (npm path covered above) ⚠");
|
|
129
|
+
} else {
|
|
130
|
+
pnpmTmp = fs.mkdtempSync(path.join(os.tmpdir(), "kj-verify-pnpm-"));
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(pnpmTmp, "package.json"),
|
|
133
|
+
JSON.stringify({ name: "kj-verify-pnpm", private: true }),
|
|
134
|
+
);
|
|
135
|
+
console.log(`verify-pack: installing the tarball with pnpm into ${pnpmTmp}…`);
|
|
136
|
+
// pnpm exits non-zero on ERR_PNPM_IGNORED_BUILDS (it skips native build
|
|
137
|
+
// scripts by default) — expected here, so don't treat the exit as failure.
|
|
138
|
+
spawnSync("pnpm", ["add", tgzPath, "--store-dir", path.join(pnpmTmp, ".store")], {
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
env: childEnv,
|
|
141
|
+
cwd: pnpmTmp,
|
|
142
|
+
});
|
|
143
|
+
const pnpmBin = path.join(pnpmTmp, "node_modules", ".bin", "kj");
|
|
144
|
+
if (!fs.existsSync(pnpmBin)) fail(`kj binary missing after pnpm install: ${pnpmBin}`);
|
|
145
|
+
let pnpmVersion;
|
|
146
|
+
try {
|
|
147
|
+
pnpmVersion = run(pnpmBin, ["--version"], { cwd: pnpmTmp }).trim();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
fail("`kj --version` crashed under a pnpm install (packaging regression)", err.stderr || err.message);
|
|
150
|
+
}
|
|
151
|
+
if (!pnpmVersion.includes(expectedVersion)) {
|
|
152
|
+
fail(`kj --version under pnpm returned "${pnpmVersion}", expected ${expectedVersion}`);
|
|
153
|
+
}
|
|
154
|
+
console.log(`verify-pack: pnpm install + kj --version → ${pnpmVersion} ✓ (DB build needs \`pnpm approve-builds\`)`);
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
console.log(`\n✓ verify-pack: karajan-code@${expectedVersion} installs clean and runs.`);
|
|
118
158
|
} finally {
|
|
119
159
|
if (tgzPath && fs.existsSync(tgzPath)) fs.rmSync(tgzPath, { force: true });
|
|
120
160
|
if (tmpDir && fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
+
if (pnpmTmp && fs.existsSync(pnpmTmp)) fs.rmSync(pnpmTmp, { recursive: true, force: true });
|
|
121
162
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArbiterRole — the autonomous decision authority that resolves agent conflicts
|
|
3
|
+
* by picking the least-bad verdict (KJC-TSK-0573, épica KJC-PCS-0062).
|
|
4
|
+
*
|
|
5
|
+
* Extends AgentRole (so it inherits Brain recovery / standby) following the
|
|
6
|
+
* TriageRole pattern: a cheap decisor model emits a closed-set verdict, and any
|
|
7
|
+
* parse failure degrades to BLOCK_AND_CONTINUE — the pipeline never crashes and
|
|
8
|
+
* never blocks on the Arbiter.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AgentRole } from "../roles/agent-role.js";
|
|
12
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
13
|
+
import { buildArbiterPrompt, VERDICTS } from "../prompts/arbiter.js";
|
|
14
|
+
|
|
15
|
+
const CONSERVATIVE = "BLOCK_AND_CONTINUE";
|
|
16
|
+
const VERDICT_SET = new Set(VERDICTS);
|
|
17
|
+
|
|
18
|
+
function clampConfidence(value) {
|
|
19
|
+
const n = Number(value);
|
|
20
|
+
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ArbiterRole extends AgentRole {
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
super({ ...opts, name: "arbiter" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async buildPrompt(input) {
|
|
29
|
+
return { prompt: buildArbiterPrompt(input) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parseOutput(raw) {
|
|
33
|
+
return extractFirstJson(raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
buildSuccessResult(parsed, provider) {
|
|
37
|
+
const verdict = VERDICT_SET.has(parsed?.verdict) ? parsed.verdict : CONSERVATIVE;
|
|
38
|
+
return {
|
|
39
|
+
verdict,
|
|
40
|
+
confidence: clampConfidence(parsed?.confidence),
|
|
41
|
+
rationale: String(parsed?.rationale || "").trim() || "no rationale",
|
|
42
|
+
defect: parsed?.defect ? String(parsed.defect).trim() : null,
|
|
43
|
+
directive: parsed?.directive ? String(parsed.directive).trim() : null,
|
|
44
|
+
provider,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
buildSummary(parsed) {
|
|
49
|
+
return `Arbiter: ${VERDICT_SET.has(parsed?.verdict) ? parsed.verdict : CONSERVATIVE}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Never crash on garbage: degrade to the conservative verdict so the pipeline
|
|
53
|
+
// moves on instead of aborting (ok:true keeps the run alive).
|
|
54
|
+
handleParseNull(agentResult, provider) {
|
|
55
|
+
return this.degraded(agentResult, provider, "unstructured output");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleParseError(err, agentResult, provider) {
|
|
59
|
+
return this.degraded(agentResult, provider, err?.message || "parse error");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
degraded(agentResult, provider, why) {
|
|
63
|
+
this.logger?.warn?.(`[arbiter] ${why} → ${CONSERVATIVE} (conservative)`);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
result: { verdict: CONSERVATIVE, confidence: 0, rationale: `degraded: ${why}`, defect: null, directive: null, provider },
|
|
67
|
+
summary: `Arbiter: ${CONSERVATIVE} (degraded)`,
|
|
68
|
+
usage: agentResult.usage,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createArbiter — adapt ArbiterRole to the function the decision-resolver
|
|
3
|
+
* injects (KJC-TSK-0573). The resolver calls `arbiter(req)` and expects
|
|
4
|
+
* `{ choice, rationale, ... }`; here `choice` is the Arbiter's verdict, which
|
|
5
|
+
* the resolver then validates against the caller's options (AUTO-A).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ArbiterRole } from "./arbiter-role.js";
|
|
9
|
+
|
|
10
|
+
export function createArbiter({ config = {}, logger = console, createAgentFn = null } = {}) {
|
|
11
|
+
return async function arbiter(req) {
|
|
12
|
+
const role = new ArbiterRole({ config, logger, createAgentFn });
|
|
13
|
+
const { result } = await role.execute(req);
|
|
14
|
+
return {
|
|
15
|
+
choice: result.verdict,
|
|
16
|
+
rationale: result.rationale,
|
|
17
|
+
confidence: result.confidence,
|
|
18
|
+
defect: result.defect,
|
|
19
|
+
directive: result.directive,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildDecider — wire the active autonomy level to a ready decision resolver
|
|
3
|
+
* (KJC-TSK-0572, design in docs/specs/autonomous-delivery.md §5).
|
|
4
|
+
*
|
|
5
|
+
* interactive → the human `ask`; assisted/autonomous → the Arbiter (AUTO-B).
|
|
6
|
+
* Consumers (spec-review, reviewer, checkpoint…) call `resolve(...)` instead of
|
|
7
|
+
* prompting directly, so the autonomy level decides who answers in one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createDecisionResolver } from "./decision-resolver.js";
|
|
11
|
+
import { resolveAutonomy } from "./policy.js";
|
|
12
|
+
import { createArbiter } from "./arbiter.js";
|
|
13
|
+
|
|
14
|
+
export function buildDecider({ config = {}, flags = {}, ask = null, env = process.env, logger = console, createAgentFn = null } = {}) {
|
|
15
|
+
const autonomy = resolveAutonomy({ config, flags, env });
|
|
16
|
+
const arbiter = autonomy === "interactive" ? null : createArbiter({ config, logger, createAgentFn });
|
|
17
|
+
return { autonomy, resolve: createDecisionResolver({ autonomy, ask, arbiter, logger }) };
|
|
18
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autonomy/decision-resolver — the single choke point for every pipeline
|
|
3
|
+
* question (KJC-TSK-0572, design in docs/specs/autonomous-delivery.md §5).
|
|
4
|
+
*
|
|
5
|
+
* Today the "ask a human" logic is scattered (spec-review, checkpoint,
|
|
6
|
+
* reviewer, board-bridge). This unifies it: callers ask `resolveDecision`
|
|
7
|
+
* instead of prompting directly, and the autonomy level decides who answers —
|
|
8
|
+
* the human (`ask`) or the Arbiter (`arbiter`, injected by AUTO-B).
|
|
9
|
+
*
|
|
10
|
+
* INVARIANT: in `autonomous` mode the resolver NEVER returns a null choice and
|
|
11
|
+
* NEVER blocks — if the Arbiter can't decide it degrades to the option flagged
|
|
12
|
+
* `conservative` (or the first option). Decisions are always traced.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { RISK, usesArbiter } from "./policy.js";
|
|
16
|
+
|
|
17
|
+
/** Pick the safe fallback: the option flagged conservative, else the first. */
|
|
18
|
+
function conservativeOf(options) {
|
|
19
|
+
return options.find((o) => o.conservative) ?? options[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decision(choice, rationale, source) {
|
|
23
|
+
return { choice: choice ?? null, rationale, source };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a resolver bound to an autonomy level and its two answer sources.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* @param {string} opts.autonomy - interactive | assisted | autonomous
|
|
31
|
+
* @param {(question: string, context?: object) => Promise<string|null>} [opts.ask] - human prompt
|
|
32
|
+
* @param {(input: object) => Promise<{choice: string, rationale?: string}>} [opts.arbiter] - AUTO-B
|
|
33
|
+
* @returns {(req: object) => Promise<{choice, rationale, source}>}
|
|
34
|
+
*/
|
|
35
|
+
export function createDecisionResolver({ autonomy = "interactive", ask = null, arbiter = null, logger = console } = {}) {
|
|
36
|
+
return async function resolveDecision({ question, options = [], context = null, risk = RISK.LOW } = {}) {
|
|
37
|
+
const fallback = conservativeOf(options);
|
|
38
|
+
|
|
39
|
+
// Human path: interactive, or assisted with a high-risk decision.
|
|
40
|
+
if (!usesArbiter(autonomy, risk)) {
|
|
41
|
+
const answer = ask ? await ask(question, context) : null;
|
|
42
|
+
return decision(answer, "human", "human");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Arbiter path: must always yield a valid choice, never block.
|
|
46
|
+
if (!arbiter) return decision(fallback?.value, "no arbiter available → conservative", "fallback");
|
|
47
|
+
let verdict;
|
|
48
|
+
try {
|
|
49
|
+
verdict = await arbiter({ question, options, context, risk });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.warn?.(`decision-resolver: arbiter failed (${err.message}) → conservative`);
|
|
52
|
+
return decision(fallback?.value, "arbiter error → conservative", "fallback");
|
|
53
|
+
}
|
|
54
|
+
const valid = verdict && options.some((o) => o.value === verdict.choice);
|
|
55
|
+
if (!valid) return decision(fallback?.value, "arbiter gave no valid choice → conservative", "fallback");
|
|
56
|
+
return decision(verdict.choice, verdict.rationale ?? "arbiter", "arbiter");
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcome-report — the auditable "meets the ask, with known defects" report
|
|
3
|
+
* for an autonomous run (KJC-TSK-0577, design in docs/specs/autonomous-delivery.md §10).
|
|
4
|
+
*
|
|
5
|
+
* Pure: builds a serializable outcome from the run results + Arbiter decisions,
|
|
6
|
+
* and renders it as lines (or the structure is emitted as --json). The point is
|
|
7
|
+
* that an imperfect-but-delivered result is explicit and auditable: which HUs
|
|
8
|
+
* met the ask, what the Arbiter decided, and what debt remains.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} [opts.planRef]
|
|
14
|
+
* @param {boolean} opts.delivered - the run met the minimum ask
|
|
15
|
+
* @param {Array<{id, title?, met: boolean, note?}>} [opts.hus]
|
|
16
|
+
* @param {Array<{verdict, rationale, hu?, defect?}>} [opts.decisions] - Arbiter log
|
|
17
|
+
*/
|
|
18
|
+
export function buildOutcome({ planRef = null, delivered = false, hus = [], decisions = [] } = {}) {
|
|
19
|
+
const met = hus.filter((h) => h.met).length;
|
|
20
|
+
const residualDefects = [
|
|
21
|
+
...hus.filter((h) => !h.met).map((h) => ({ hu: h.id, detail: h.note || "acceptance criteria not fully met" })),
|
|
22
|
+
...decisions.filter((d) => d.defect).map((d) => ({ hu: d.hu ?? null, detail: d.defect })),
|
|
23
|
+
];
|
|
24
|
+
return {
|
|
25
|
+
verdict: delivered ? "DELIVERED" : "INCOMPLETE",
|
|
26
|
+
planRef,
|
|
27
|
+
hus: { total: hus.length, met, unmet: hus.length - met },
|
|
28
|
+
decisions,
|
|
29
|
+
residualDefects,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Render an outcome (from buildOutcome) as human-readable lines. */
|
|
34
|
+
export function formatOutcomeReport(outcome) {
|
|
35
|
+
const lines = [
|
|
36
|
+
`kj autorun — outcome: ${outcome.verdict}`,
|
|
37
|
+
` plan: ${outcome.planRef ?? "?"}`,
|
|
38
|
+
` HUs: ${outcome.hus.met}/${outcome.hus.total} met the ask`,
|
|
39
|
+
];
|
|
40
|
+
if (outcome.decisions?.length) {
|
|
41
|
+
lines.push(" Decisions (Arbiter — least-bad calls):");
|
|
42
|
+
for (const d of outcome.decisions) {
|
|
43
|
+
lines.push(` • ${d.verdict}${d.hu ? ` [${d.hu}]` : ""} — ${d.rationale}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (outcome.residualDefects?.length) {
|
|
47
|
+
lines.push(" Known defects (delivered with debt):");
|
|
48
|
+
for (const def of outcome.residualDefects) {
|
|
49
|
+
lines.push(` • ${def.hu ? `[${def.hu}] ` : ""}${def.detail}`);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
lines.push(" No residual defects recorded.");
|
|
53
|
+
}
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autonomy/policy — autonomy levels for Autonomous Delivery (KJC-TSK-0572,
|
|
3
|
+
* épica KJC-PCS-0062, design in docs/specs/autonomous-delivery.md §4).
|
|
4
|
+
*
|
|
5
|
+
* One configurable axis decides who resolves a pipeline question:
|
|
6
|
+
* interactive — the human (current behaviour, default)
|
|
7
|
+
* assisted — the human only for high-risk decisions, the Arbiter otherwise
|
|
8
|
+
* autonomous — always the Arbiter; never asks a human, never blocks
|
|
9
|
+
*
|
|
10
|
+
* Precedence (most specific wins): CLI flags → env → config → default.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const AUTONOMY_LEVELS = Object.freeze(["interactive", "assisted", "autonomous"]);
|
|
14
|
+
|
|
15
|
+
/** Risk of a decision — drives whether `assisted` escalates to the human. */
|
|
16
|
+
export const RISK = Object.freeze({ LOW: "low", HIGH: "high" });
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LEVEL = "interactive";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the active autonomy level. `--autonomous` is sugar for the
|
|
22
|
+
* `autonomous` level; `--autonomy <level>` sets it explicitly. An unknown
|
|
23
|
+
* value degrades to the safe default rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveAutonomy({ config = {}, flags = {}, env = {} } = {}) {
|
|
26
|
+
const raw =
|
|
27
|
+
flags.autonomy ??
|
|
28
|
+
(flags.autonomous ? "autonomous" : null) ??
|
|
29
|
+
env.KJ_AUTONOMY ??
|
|
30
|
+
config.autonomy ??
|
|
31
|
+
DEFAULT_LEVEL;
|
|
32
|
+
const level = String(raw).toLowerCase();
|
|
33
|
+
return AUTONOMY_LEVELS.includes(level) ? level : DEFAULT_LEVEL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when the level should route decisions to the Arbiter for `risk`. */
|
|
37
|
+
export function usesArbiter(level, risk = RISK.LOW) {
|
|
38
|
+
if (level === "autonomous") return true;
|
|
39
|
+
if (level === "assisted") return risk !== RISK.HIGH;
|
|
40
|
+
return false; // interactive
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAutonomyAskQuestion — make the stages' question function non-blocking in
|
|
3
|
+
* autonomous mode (KJC-TSK-0576, AUTO-E, design in docs/specs/autonomous-delivery.md §9).
|
|
4
|
+
*
|
|
5
|
+
* In `autonomous` no stage asks a human or routes to the HU Board: the wrapper
|
|
6
|
+
* answers in-process with the least-bad "keep going" choice, so the run never
|
|
7
|
+
* pauses or aborts mid-loop. interactive and assisted keep the human prompts
|
|
8
|
+
* (baseAsk) unchanged.
|
|
9
|
+
*
|
|
10
|
+
* The answer is chosen per stage so the EXISTING branches proceed (verified
|
|
11
|
+
* against the stage code — no stage logic is touched):
|
|
12
|
+
* - architect clarification: null → the stage continues best-effort (a truthy
|
|
13
|
+
* answer would force a wasteful architect re-run).
|
|
14
|
+
* - everything else (Solomon escalation, checkpoint, …): "continue" → parsed
|
|
15
|
+
* as free-text guidance → the stage continues instead of pausing/aborting.
|
|
16
|
+
*/
|
|
17
|
+
export function createAutonomyAskQuestion({ baseAsk = null, autonomy = "interactive", logger = console } = {}) {
|
|
18
|
+
if (autonomy !== "autonomous") return baseAsk;
|
|
19
|
+
const auto = async (_question, context = {}) => {
|
|
20
|
+
const stage = context?.stage ?? null;
|
|
21
|
+
logger?.info?.(`[autonomy] ${stage ? `${stage}: ` : ""}auto-continue (no human in the loop)`);
|
|
22
|
+
return stage === "architect" ? null : "continue";
|
|
23
|
+
};
|
|
24
|
+
auto.interactive = false;
|
|
25
|
+
// Marker so the orchestrator can tell an autonomous (unattended) run from an
|
|
26
|
+
// interactive one even though both carry a truthy askQuestion — the wall-clock
|
|
27
|
+
// backstop relies on it (KJC-TSK-0575, AUTO-D).
|
|
28
|
+
auto.unattended = true;
|
|
29
|
+
return auto;
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native build availability check (KJC-BUG-0092).
|
|
3
|
+
*
|
|
4
|
+
* better-sqlite3 ships a native addon compiled in a postinstall step. pnpm
|
|
5
|
+
* blocks dependency build scripts by default (ERR_PNPM_IGNORED_BUILDS), so a
|
|
6
|
+
* `pnpm add -g karajan-code` leaves the addon uncompiled: `kj --version` still
|
|
7
|
+
* works (it never opens a DB) but any DB-backed command (board, rag, cost
|
|
8
|
+
* tracking) crashes with "Cannot find module 'better-sqlite3'". This surfaces a
|
|
9
|
+
* WARN in `kj doctor` with the exact remedy instead of a cryptic runtime crash.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { STRATEGY } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** pnpm lays packages out under a `.pnpm` virtual store and symlinks them in. */
|
|
15
|
+
function isPnpmInstall() {
|
|
16
|
+
return (
|
|
17
|
+
import.meta.url.includes("/.pnpm/") ||
|
|
18
|
+
(process.env.npm_config_user_agent || "").includes("pnpm")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Force the native binding to load by opening an in-memory database. */
|
|
23
|
+
async function defaultLoadDb() {
|
|
24
|
+
const Database = (await import("better-sqlite3")).default;
|
|
25
|
+
new Database(":memory:").close();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpmInstall } = {}) {
|
|
29
|
+
return {
|
|
30
|
+
name: "native-build",
|
|
31
|
+
label: "native modules (better-sqlite3)",
|
|
32
|
+
strategy: STRATEGY.MANUAL,
|
|
33
|
+
async detect() {
|
|
34
|
+
try {
|
|
35
|
+
await loadDb();
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
severity: "info",
|
|
39
|
+
detail: "better-sqlite3 native addon loads — DB-backed commands available",
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const firstLine = String(err?.message || err).split("\n")[0];
|
|
43
|
+
if (isPnpm()) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
severity: "warn",
|
|
47
|
+
detail: "better-sqlite3 native build was skipped — pnpm blocks dependency build scripts by default",
|
|
48
|
+
fix: "Approve the build then reinstall: `pnpm approve-builds better-sqlite3` — or install with npm: `npm i -g karajan-code`",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
severity: "warn",
|
|
54
|
+
detail: `better-sqlite3 failed to load: ${firstLine}`,
|
|
55
|
+
fix: "Reinstall to rebuild native modules: `npm i -g karajan-code`",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getNativeBuildChecks() {
|
|
63
|
+
return [createNativeBuildCheck()];
|
|
64
|
+
}
|
|
@@ -114,6 +114,8 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
114
114
|
.option("--skip-role <role...>", "Force one or more roles OFF regardless of triage (e.g. --skip-role tester security)")
|
|
115
115
|
.option("--force-role <role...>", "Force one or more roles ON regardless of triage")
|
|
116
116
|
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
117
|
+
.option("--autonomy <level>", "Autonomy level: interactive | assisted | autonomous (default: interactive)")
|
|
118
|
+
.option("--autonomous", "Shortcut for --autonomy autonomous: the Arbiter resolves every gate, no human prompts")
|
|
117
119
|
.option("--no-rag-update", "Skip the pre-run RAG drift check (KJC-TSK-0455). Same as config.rag.autoUpdate.onRun=false.")
|
|
118
120
|
.option("--dry-run", "Show what would be executed without running anything")
|
|
119
121
|
.option("--json", "Output JSON only (no styled display)")
|
|
@@ -143,6 +145,25 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
143
145
|
});
|
|
144
146
|
});
|
|
145
147
|
|
|
148
|
+
program
|
|
149
|
+
.command("autorun")
|
|
150
|
+
.description("Unattended delivery: spec → plan → run all HUs → outcome (defaults to --autonomous)")
|
|
151
|
+
.argument("[task]", "Spec/task description (or use --task-file)")
|
|
152
|
+
.option("--task-file <path>", "Read the spec from a file (e.g. spec.md)")
|
|
153
|
+
.option("--autonomy <level>", "interactive | assisted | autonomous (default: autonomous)")
|
|
154
|
+
.option("--autonomous", "Shortcut for --autonomy autonomous")
|
|
155
|
+
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
156
|
+
.action(async (task, flags) => {
|
|
157
|
+
const code = await withConfig(pkgVersion, "autorun", flags, async ({ config, logger }) => {
|
|
158
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
159
|
+
const { autorunCommand } = await import("../commands/autorun.js");
|
|
160
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
161
|
+
const res = await autorunCommand({ task: resolvedTask, config, logger, flags });
|
|
162
|
+
return res?.ok ? 0 : 1;
|
|
163
|
+
});
|
|
164
|
+
if (Number.isInteger(code)) process.exit(code);
|
|
165
|
+
});
|
|
166
|
+
|
|
146
167
|
program
|
|
147
168
|
.command("code")
|
|
148
169
|
.description("Run only coder")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj autorun <spec>` — the unattended delivery chain (KJC-TSK-0574, épica
|
|
3
|
+
* KJC-PCS-0062, design in docs/specs/autonomous-delivery.md §7).
|
|
4
|
+
*
|
|
5
|
+
* One command: spec → plan → run all HUs → outcome. It reuses `kj plan` and
|
|
6
|
+
* `kj run --plan` rather than reimplementing them, and defaults to the
|
|
7
|
+
* `autonomous` level so the Arbiter resolves every gate (no human in the loop).
|
|
8
|
+
* The rich outcome report lands in AUTO-F; here we chain + propagate exit code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { planGenerateCommand } from "./plan/generate.js";
|
|
12
|
+
import { runCommandHandler } from "./run.js";
|
|
13
|
+
import { buildOutcome, formatOutcomeReport } from "../autonomy/outcome-report.js";
|
|
14
|
+
|
|
15
|
+
export async function autorunCommand({
|
|
16
|
+
task,
|
|
17
|
+
config,
|
|
18
|
+
logger = console,
|
|
19
|
+
flags = {},
|
|
20
|
+
planFn = planGenerateCommand,
|
|
21
|
+
runFn = runCommandHandler,
|
|
22
|
+
} = {}) {
|
|
23
|
+
// autorun implies unattended: default to autonomous unless the user picked a
|
|
24
|
+
// level explicitly (so `kj autorun --autonomy assisted` still works).
|
|
25
|
+
const autoFlags = flags.autonomy || flags.autonomous ? flags : { ...flags, autonomous: true };
|
|
26
|
+
|
|
27
|
+
logger.info?.("kj autorun — planning…");
|
|
28
|
+
const plan = await planFn({ task, config, logger, flags: autoFlags });
|
|
29
|
+
if (!plan?.ok || !plan.planId) {
|
|
30
|
+
logger.error?.(`kj autorun: planning failed${plan?.reason ? ` (${plan.reason})` : ""}.`);
|
|
31
|
+
return { ok: false, stage: "plan", reason: plan?.reason ?? "plan-failed" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.info?.(`kj autorun — running plan ${plan.ref ?? plan.planId} (${plan.huCount ?? "?"} HUs)…`);
|
|
35
|
+
const run = await runFn({ config, logger, flags: { ...autoFlags, plan: plan.planId } });
|
|
36
|
+
|
|
37
|
+
const ok = run?.ok === true;
|
|
38
|
+
// AUTO-F: emit the auditable outcome — delivered/incomplete, HUs that met the
|
|
39
|
+
// ask, Arbiter decisions and residual defects (enriched as the run surfaces
|
|
40
|
+
// them; baseline today is delivered + plan ref).
|
|
41
|
+
const outcome = buildOutcome({
|
|
42
|
+
planRef: plan.ref ?? plan.planId,
|
|
43
|
+
delivered: ok,
|
|
44
|
+
hus: run?.hus ?? [],
|
|
45
|
+
decisions: run?.decisions ?? [],
|
|
46
|
+
});
|
|
47
|
+
for (const line of formatOutcomeReport(outcome)) logger.info?.(line);
|
|
48
|
+
return { ok, stage: ok ? "done" : "run", planId: plan.planId, ref: plan.ref ?? plan.planId, outcome };
|
|
49
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -26,6 +26,7 @@ import { getSqueezrChecks } from "../checks/squeezr.js";
|
|
|
26
26
|
import { getAiTrashChecks } from "../checks/ai-trash.js";
|
|
27
27
|
import { getQmdChecks } from "../checks/qmd.js";
|
|
28
28
|
import { getNodeChecks } from "../checks/node.js";
|
|
29
|
+
import { getNativeBuildChecks } from "../checks/native-build.js";
|
|
29
30
|
import { getPortChecks } from "../checks/ports.js";
|
|
30
31
|
import { getTokenChecks } from "../checks/tokens.js";
|
|
31
32
|
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
@@ -54,6 +55,7 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
54
55
|
return [
|
|
55
56
|
...getSystemChecks(),
|
|
56
57
|
...getNodeChecks(),
|
|
58
|
+
...getNativeBuildChecks(),
|
|
57
59
|
...getHardwareChecks(),
|
|
58
60
|
...getDirSetupChecks(),
|
|
59
61
|
...getConfigFileChecks(),
|
|
@@ -457,7 +457,7 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
457
457
|
|
|
458
458
|
if (json) {
|
|
459
459
|
console.log(JSON.stringify(plan, null, 2));
|
|
460
|
-
return { ok: true };
|
|
460
|
+
return { ok: true, planId, ref: plan.alias || planId, huCount: plan.hus.length };
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
if (parsed?.approach) {
|
|
@@ -524,5 +524,6 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
524
524
|
console.log(`\nVisualize: kj board start (http://localhost:${config?.hu_board?.port ?? 4000})`);
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
|
-
|
|
527
|
+
// KJC-TSK-0574: surface planId/ref so `kj autorun` can chain plan → run.
|
|
528
|
+
return { ok: true, planId, ref, huCount: plan.hus.length };
|
|
528
529
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -17,6 +17,8 @@ import { maybeAutoUpdate } from "../rag/auto-update.js";
|
|
|
17
17
|
// keep working. KJC-BUG-0081 round 2 split the body into utils/ because
|
|
18
18
|
// resume.js also needs the same contract.
|
|
19
19
|
import { createCliAskQuestion } from "../utils/cli-ask-question.js";
|
|
20
|
+
import { buildDecider } from "../autonomy/decider.js";
|
|
21
|
+
import { createAutonomyAskQuestion } from "../autonomy/stage-ask.js";
|
|
20
22
|
export { createCliAskQuestion };
|
|
21
23
|
|
|
22
24
|
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
@@ -129,12 +131,16 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
129
131
|
|
|
130
132
|
const askQuestion = createCliAskQuestion({ flags });
|
|
131
133
|
|
|
134
|
+
// Autonomy (KJC-PCS-0062): wire the level to a resolver. In autonomous mode
|
|
135
|
+
// every gate is decided by the Arbiter instead of the human; interactive
|
|
136
|
+
// keeps the human prompts. The decider is threaded into the gates below.
|
|
137
|
+
const { autonomy, resolve: decide } = buildDecider({ config, flags, ask: askQuestion, logger });
|
|
138
|
+
|
|
132
139
|
// Spec-reviewer pre-pipeline audit (KJC-PCS-0048). Runs BEFORE
|
|
133
|
-
// anything else — surfaces ambiguity / missing scope / missing AC
|
|
134
|
-
//
|
|
135
|
-
// Bypass with --skip-spec-review.
|
|
136
|
-
|
|
137
|
-
const reviewResult = await runSpecReview({ spec: task, config, logger, askQuestion, flags });
|
|
140
|
+
// anything else — surfaces ambiguity / missing scope / missing AC.
|
|
141
|
+
// In autonomous mode the Arbiter decides (never aborts); interactive
|
|
142
|
+
// lets the user bail. Bypass with --skip-spec-review.
|
|
143
|
+
const reviewResult = await runSpecReview({ spec: task, config, logger, askQuestion, flags, resolve: decide, autonomy });
|
|
138
144
|
if (!reviewResult.proceed) {
|
|
139
145
|
logger.info("Aborted by user after spec review.");
|
|
140
146
|
cleanupRegistry();
|
|
@@ -145,9 +151,14 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
145
151
|
// user actually approved.
|
|
146
152
|
const effectiveTask = reviewResult.refined && reviewResult.finalSpec ? reviewResult.finalSpec : task;
|
|
147
153
|
|
|
154
|
+
// AUTO-E: in autonomous mode the stages never ask a human or block on the
|
|
155
|
+
// board — they auto-continue with the least-bad choice. interactive/assisted
|
|
156
|
+
// keep the human prompts.
|
|
157
|
+
const stageAsk = createAutonomyAskQuestion({ baseAsk: askQuestion, autonomy, logger });
|
|
158
|
+
|
|
148
159
|
let result;
|
|
149
160
|
try {
|
|
150
|
-
result = await runFlow({ task: effectiveTask, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
161
|
+
result = await runFlow({ task: effectiveTask, config, logger, flags, emitter, askQuestion: stageAsk, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
151
162
|
} finally {
|
|
152
163
|
// KJC-TSK-0396: limpia el registro pase lo que pase (éxito,
|
|
153
164
|
// throw, o paused). Los handlers de SIGINT/SIGTERM/exit son
|
|
@@ -163,6 +174,12 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
163
174
|
// failed), tell the user the exact command to resume it.
|
|
164
175
|
printResumeHint(result);
|
|
165
176
|
}
|
|
166
|
-
|
|
177
|
+
// Surface per-HU results so `kj autorun` can build a real outcome report
|
|
178
|
+
// (KJC-BUG-0090: it showed "0/0 met the ask" because only { ok } came back).
|
|
179
|
+
// The plan run returns them as `huResults` (run-hu-batch finalResult).
|
|
180
|
+
const hus = Array.isArray(result?.huResults)
|
|
181
|
+
? result.huResults.map((r) => ({ id: r.huId, met: r.approved === true }))
|
|
182
|
+
: [];
|
|
183
|
+
return { ok: !result?.paused && result?.approved !== false, hus };
|
|
167
184
|
});
|
|
168
185
|
}
|
|
@@ -316,7 +316,8 @@ function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan,
|
|
|
316
316
|
await savePlanToDisk(projectDir, loadedPlan);
|
|
317
317
|
if (outcome && Number.isFinite(outcome.cost_usd)) {
|
|
318
318
|
try {
|
|
319
|
-
const { setStoryCost, setStoryCachedTokens } = await import("../../../../packages/hu-board/src/db.js");
|
|
319
|
+
const { ensureDb, setStoryCost, setStoryCachedTokens } = await import("../../../../packages/hu-board/src/db.js");
|
|
320
|
+
ensureDb(); // KJC-BUG-0090: autorun/run may not have started the board
|
|
320
321
|
setStoryCost(huId, outcome.cost_usd);
|
|
321
322
|
// Φ0-G (KJC-TSK-0525): cached/in tokens travel in the same
|
|
322
323
|
// outcome blob as cost. Write them together so the board
|
|
@@ -128,7 +128,13 @@ export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, confi
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
|
|
131
|
-
|
|
131
|
+
// Enforce the wall-clock when no human is watching the checkpoint: either a
|
|
132
|
+
// non-interactive run (no askQuestion) OR an autonomous one — where askQuestion
|
|
133
|
+
// is the auto-continue wrapper (truthy) but nobody is there to stop a runaway.
|
|
134
|
+
// Interactive runs keep skipping it (the human decides at the checkpoint).
|
|
135
|
+
// KJC-TSK-0575 (AUTO-D): without this, autonomous runs had NO wall-clock cap.
|
|
136
|
+
const unattended = !askQuestion || askQuestion.unattended === true;
|
|
137
|
+
if (!unattended || elapsedMinutes <= config.session.max_total_minutes) return;
|
|
132
138
|
|
|
133
139
|
await markSessionStatus(session, "failed");
|
|
134
140
|
emitProgress(
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arbiter prompt (KJC-TSK-0573, design in docs/specs/autonomous-delivery.md §6).
|
|
3
|
+
*
|
|
4
|
+
* The Arbiter is Karajan's autonomous decision authority: given a conflict, it
|
|
5
|
+
* picks the LEAST-BAD option from a closed set and moves forward — no human.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The closed set of conflict-resolution verdicts. BLOCK_AND_CONTINUE is safe. */
|
|
9
|
+
export const VERDICTS = Object.freeze([
|
|
10
|
+
"ACCEPT_WITH_DEFECT",
|
|
11
|
+
"RETRY_DIFFERENT_APPROACH",
|
|
12
|
+
"DESCOPE_HU",
|
|
13
|
+
"BLOCK_AND_CONTINUE",
|
|
14
|
+
"PROCEED",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function buildArbiterPrompt({ question, options = [], context = null } = {}) {
|
|
18
|
+
const allowed = options.length ? options.map((o) => o.value) : VERDICTS;
|
|
19
|
+
return [
|
|
20
|
+
"You are the Arbiter — Karajan's autonomous decision authority. There is no human to ask.",
|
|
21
|
+
"Resolve the conflict below by choosing the LEAST-BAD option and moving forward.",
|
|
22
|
+
"",
|
|
23
|
+
"Ground-truth order (what wins when signals disagree):",
|
|
24
|
+
" 1. Acceptance tests passing — beats any reviewer opinion.",
|
|
25
|
+
" 2. Must-fix issues (security, correctness, data) — beat style.",
|
|
26
|
+
" 3. Nice-to-have (style, optional refactors) — never block; record as a defect if dropped.",
|
|
27
|
+
"Goal: maximize 'the ask is met', minimize irreversible risk. Imperfect-but-delivered beats blocked.",
|
|
28
|
+
"",
|
|
29
|
+
`Conflict: ${question}`,
|
|
30
|
+
context ? `Context: ${JSON.stringify(context)}` : "",
|
|
31
|
+
"",
|
|
32
|
+
`Choose exactly one verdict from: ${allowed.join(", ")}`,
|
|
33
|
+
"Respond with ONLY this JSON (no prose):",
|
|
34
|
+
'{"verdict":"<one of the allowed>","confidence":0.0,"rationale":"why it is the least-bad option","defect":"residual debt if any, else null","directive":"instruction to the coder when RETRY_DIFFERENT_APPROACH, else null"}',
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join("\n");
|
|
38
|
+
}
|
|
@@ -7,10 +7,30 @@
|
|
|
7
7
|
import { SpecReviewerRole } from "../roles/spec-reviewer-role.js";
|
|
8
8
|
import { printFindings } from "../utils/display/spec-findings.js";
|
|
9
9
|
import { runRefinePass } from "./refine-loop.js";
|
|
10
|
+
import { RISK } from "../autonomy/policy.js";
|
|
10
11
|
|
|
11
12
|
const MAX_REFINE_ITERATIONS = 5;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Decide the gate action (`continue` | `refine`) autonomously: the Arbiter
|
|
16
|
+
* picks PROCEED (proceed with the best reading) or RETRY_DIFFERENT_APPROACH
|
|
17
|
+
* (auto-refine). It is never offered `cancel`, so an autonomous run never
|
|
18
|
+
* aborts on spec ambiguity (KJC-TSK-0572, closes gap G1).
|
|
19
|
+
*/
|
|
20
|
+
async function decideGateAutonomously(resolve, { severity, findings }) {
|
|
21
|
+
const { choice } = await resolve({
|
|
22
|
+
question: `Spec has ${findings.length} finding(s) at severity ${severity}: proceed with the best reading, or auto-refine?`,
|
|
23
|
+
options: [
|
|
24
|
+
{ value: "PROCEED", label: "proceed with the best reading", conservative: true },
|
|
25
|
+
{ value: "RETRY_DIFFERENT_APPROACH", label: "auto-refine the spec" },
|
|
26
|
+
],
|
|
27
|
+
context: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))] },
|
|
28
|
+
risk: severity === "fail" ? RISK.HIGH : RISK.LOW,
|
|
29
|
+
});
|
|
30
|
+
return choice === "RETRY_DIFFERENT_APPROACH" ? "refine" : "continue";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runSpecReview({ spec, config, logger, askQuestion, flags = {}, resolve = null, autonomy = "interactive" }) {
|
|
14
34
|
// VITEST guard — pre-existing test suites (e.g. tests/commands/command-run.test.js)
|
|
15
35
|
// mock `runFlow` + `assertAgentsAvailable` but predate this role and have
|
|
16
36
|
// no need to exercise it. Auto-skip in vitest unless the test passes
|
|
@@ -40,16 +60,24 @@ export async function runSpecReview({ spec, config, logger, askQuestion, flags =
|
|
|
40
60
|
return { proceed: true, severity: "ok", findings: [], finalSpec: currentSpec, refined: iter > 0 };
|
|
41
61
|
}
|
|
42
62
|
printFindings(findings, severity);
|
|
43
|
-
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
// Autonomous/assisted: the Arbiter decides (continue|refine), never cancels.
|
|
65
|
+
// Interactive (no resolver): the human answers [c]/[r]/[x] as before.
|
|
66
|
+
let action;
|
|
67
|
+
if (resolve && autonomy !== "interactive") {
|
|
68
|
+
action = await decideGateAutonomously(resolve, { severity, findings });
|
|
69
|
+
} else {
|
|
70
|
+
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
71
|
+
const answer = await askQuestion(
|
|
72
|
+
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: continue)`,
|
|
73
|
+
{ detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 } },
|
|
74
|
+
);
|
|
75
|
+
if (answer === null) return { proceed: false, cancelled: true, severity, findings };
|
|
76
|
+
const n = String(answer).trim().toLowerCase();
|
|
77
|
+
action = n.startsWith("x") || n === "cancel" ? "cancel" : n.startsWith("r") || n === "refine" ? "refine" : "continue";
|
|
78
|
+
}
|
|
79
|
+
if (action === "cancel") return { proceed: false, cancelled: true, severity, findings };
|
|
80
|
+
if (action === "refine") {
|
|
53
81
|
const refine = await runRefinePass({
|
|
54
82
|
spec: currentSpec, findings, role,
|
|
55
83
|
projectDir: config?.projectDir, taskFile: flags.taskFile, logger,
|
package/src/utils/wizard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
|
|
3
|
-
export function createWizard(
|
|
4
|
-
const rl = readline.createInterface({ input, output, terminal: false });
|
|
3
|
+
export function createWizard(inputStream = process.stdin, output = process.stdout) {
|
|
4
|
+
const rl = readline.createInterface({ input: inputStream, output, terminal: false });
|
|
5
5
|
|
|
6
6
|
function ask(question) {
|
|
7
7
|
return new Promise((resolve) => {
|
|
@@ -16,6 +16,12 @@ export function createWizard(input = process.stdin, output = process.stdout) {
|
|
|
16
16
|
return /^y(es)?$/i.test(answer);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
async function input(question, defaultValue = "") {
|
|
20
|
+
const hint = defaultValue !== "" ? ` [${defaultValue}]` : "";
|
|
21
|
+
const answer = await ask(`${question}${hint} `);
|
|
22
|
+
return answer === "" ? defaultValue : answer;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
async function select(question, options) {
|
|
20
26
|
output.write(`${question}\n`);
|
|
21
27
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -33,7 +39,7 @@ export function createWizard(input = process.stdin, output = process.stdout) {
|
|
|
33
39
|
rl.close();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
return { ask, confirm, select, close };
|
|
42
|
+
return { ask, confirm, input, select, close };
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export function isTTY() {
|