@kaddo/cli 3.25.0 → 3.26.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/README.md +2 -0
- package/dist/index.js +353 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -515,6 +515,8 @@ create --from roadmap → owners → guard → explain`.
|
|
|
515
515
|
| v3.23.2 | Impact report defaults to `all` scope (accumulated impact), graph built in memory; `--scope active`; `scope_source`/`default_scope` in JSON |
|
|
516
516
|
| v3.24 | Estimated Savings Model: `kaddo savings` (+ `savings init`, `.kaddo/savings.yml`); evidence-based time/value estimates, confidence; MCP resource + tool |
|
|
517
517
|
| v3.25 | Guard history & drift trend: `kaddo guard --record`, `kaddo drift`; feeds impact Guard Activity + savings Drift Prevention; MCP drift/guard-history resources + tool |
|
|
518
|
+
| v3.25.1 | Savings guard-history messaging fix: distinguishes "no history" from "history with 0 resolved warnings" (drift prevention available at 0 h) |
|
|
519
|
+
| v3.26 | Open-questions readiness gate: `kaddo questions`/`readiness`, blocking/important/deferred classification; MCP open-questions + roadmap-readiness resources + tool; roadmap/work-item/implementation/bootstrap agents check it |
|
|
518
520
|
|
|
519
521
|
**Optional modules (installed with `kaddo add`):**
|
|
520
522
|
|
package/dist/index.js
CHANGED
|
@@ -696,6 +696,10 @@ var COMMAND_HELP = {
|
|
|
696
696
|
drift: {
|
|
697
697
|
question: "Are code and knowledge drifting apart over time?",
|
|
698
698
|
next: "Review open warnings; `kaddo guard --record` before commits"
|
|
699
|
+
},
|
|
700
|
+
questions: {
|
|
701
|
+
question: "Are there open questions to decide before the roadmap?",
|
|
702
|
+
next: "Resolve, assume or defer blocking questions, then generate the roadmap"
|
|
699
703
|
}
|
|
700
704
|
};
|
|
701
705
|
function commandFooterLines(name) {
|
|
@@ -2401,6 +2405,21 @@ open question):
|
|
|
2401
2405
|
- \`knowledge/knowledge.md\`
|
|
2402
2406
|
- business priorities
|
|
2403
2407
|
|
|
2408
|
+
## Readiness Gate (check first)
|
|
2409
|
+
|
|
2410
|
+
Before generating the roadmap, check **roadmap readiness** for open questions that affect scope,
|
|
2411
|
+
architecture or the MVP. Read \`kaddo://roadmap-readiness\` (MCP) or run \`kaddo questions\`.
|
|
2412
|
+
|
|
2413
|
+
If readiness is \`needs_decisions\` (there are **blocking** open questions), do **not** generate the
|
|
2414
|
+
roadmap yet. Instead, list the blocking questions, propose reasonable assumptions for each, and ask
|
|
2415
|
+
the user to confirm, e.g.:
|
|
2416
|
+
|
|
2417
|
+
> Before generating the roadmap I found blocking open questions that affect the MVP scope.
|
|
2418
|
+
> I can proceed with these assumptions: \u2026 Confirm and continue?
|
|
2419
|
+
|
|
2420
|
+
Only generate the roadmap once the user confirms the assumptions (or resolves/defers the
|
|
2421
|
+
questions). Record confirmed assumptions explicitly in the roadmap.
|
|
2422
|
+
|
|
2404
2423
|
## Expected Output
|
|
2405
2424
|
|
|
2406
2425
|
A single Markdown artifact intended to be saved as \`knowledge/delivery/roadmap.md\`.
|
|
@@ -2709,6 +2728,13 @@ Work Items into clear, traceable units of work.
|
|
|
2709
2728
|
You do not write code. You sharpen the problem, validate the Knowledge Level and make the
|
|
2710
2729
|
Work Item actionable for a human.
|
|
2711
2730
|
|
|
2731
|
+
## Readiness Gate (check first)
|
|
2732
|
+
|
|
2733
|
+
For high-impact Work Items, check \`kaddo://roadmap-readiness\` (or \`kaddo questions\`) for
|
|
2734
|
+
**blocking** open questions related to this Work Item's scope. If any are unresolved, surface them
|
|
2735
|
+
and propose assumptions for the user to confirm before refining \u2014 don't bake in invisible
|
|
2736
|
+
assumptions.
|
|
2737
|
+
|
|
2712
2738
|
## When to Use
|
|
2713
2739
|
|
|
2714
2740
|
Use this agent after a roadmap exists (\`knowledge/delivery/roadmap.md\`) or when an existing Work
|
|
@@ -3210,6 +3236,13 @@ You are the Kaddo Bootstrap Agent. You guide the transition from business defini
|
|
|
3210
3236
|
initial architecture direction, quality attributes, a roadmap and first Work Items for a
|
|
3211
3237
|
new project. You propose; the human decides.
|
|
3212
3238
|
|
|
3239
|
+
## Readiness Gate
|
|
3240
|
+
|
|
3241
|
+
Bootstrap surfaces \`## Open Questions\` in the knowledge files. Before moving to the roadmap,
|
|
3242
|
+
explicitly recommend reviewing them (\`kaddo questions\` / \`kaddo://roadmap-readiness\`) and turning
|
|
3243
|
+
**blocking** ones into resolved decisions or confirmed assumptions \u2014 so the roadmap isn't built on
|
|
3244
|
+
invisible assumptions.
|
|
3245
|
+
|
|
3213
3246
|
## When to Use
|
|
3214
3247
|
|
|
3215
3248
|
Use this agent after \`kaddo bootstrap\` and after the business artifacts are drafted.
|
|
@@ -3321,6 +3354,13 @@ may suggest a Git branch, and only by respecting the project's Git strategy.
|
|
|
3321
3354
|
You never run Git yourself. The Kaddo CLI never runs Git either. Every git action is the
|
|
3322
3355
|
human's, and commits/pushes/merges happen only with explicit human confirmation.
|
|
3323
3356
|
|
|
3357
|
+
## Readiness Gate (check first)
|
|
3358
|
+
|
|
3359
|
+
Before implementing a high-impact Work Item, check \`kaddo://roadmap-readiness\` (or
|
|
3360
|
+
\`kaddo questions\`) for **blocking** open questions about stack, architecture, persistence,
|
|
3361
|
+
authentication or the Work Item's scope. If any are unresolved, pause and ask the user to confirm
|
|
3362
|
+
assumptions or resolve them before writing code.
|
|
3363
|
+
|
|
3324
3364
|
## When to Use
|
|
3325
3365
|
|
|
3326
3366
|
Use this agent after the work-item-agent has produced a clear, traceable Work Item under
|
|
@@ -8162,6 +8202,224 @@ function renderDeliveryLifecycle(wi) {
|
|
|
8162
8202
|
];
|
|
8163
8203
|
}
|
|
8164
8204
|
|
|
8205
|
+
// src/core/open-questions.ts
|
|
8206
|
+
var SOURCES = [
|
|
8207
|
+
"knowledge/business/business.md",
|
|
8208
|
+
"knowledge/product/product.md",
|
|
8209
|
+
"knowledge/tech/codebase.md",
|
|
8210
|
+
"knowledge/delivery/roadmap.md"
|
|
8211
|
+
];
|
|
8212
|
+
var BLOCKING = [
|
|
8213
|
+
"mvp",
|
|
8214
|
+
"stack",
|
|
8215
|
+
"arquitect",
|
|
8216
|
+
"architecture",
|
|
8217
|
+
"api",
|
|
8218
|
+
"web app",
|
|
8219
|
+
"webapp",
|
|
8220
|
+
"cli",
|
|
8221
|
+
"backend",
|
|
8222
|
+
"auth",
|
|
8223
|
+
"autenticaci",
|
|
8224
|
+
"persist",
|
|
8225
|
+
"base de datos",
|
|
8226
|
+
"database",
|
|
8227
|
+
"roadmap",
|
|
8228
|
+
"alcance",
|
|
8229
|
+
"scope",
|
|
8230
|
+
"primera versi\xF3n",
|
|
8231
|
+
"primera version",
|
|
8232
|
+
"first version",
|
|
8233
|
+
"fastify",
|
|
8234
|
+
"nestjs",
|
|
8235
|
+
"framework"
|
|
8236
|
+
];
|
|
8237
|
+
var DEFERRED = [
|
|
8238
|
+
"integraci",
|
|
8239
|
+
"integration",
|
|
8240
|
+
"analytics",
|
|
8241
|
+
"notificaci",
|
|
8242
|
+
"notification",
|
|
8243
|
+
"google calendar",
|
|
8244
|
+
"eventbrite",
|
|
8245
|
+
"meetup",
|
|
8246
|
+
"dashboard",
|
|
8247
|
+
"multi-tenant",
|
|
8248
|
+
"multitenant",
|
|
8249
|
+
"mobile",
|
|
8250
|
+
"m\xF3vil",
|
|
8251
|
+
"pago",
|
|
8252
|
+
"payment",
|
|
8253
|
+
"ticketing",
|
|
8254
|
+
"multiple tracks",
|
|
8255
|
+
"m\xFAltiples tracks",
|
|
8256
|
+
"webhook"
|
|
8257
|
+
];
|
|
8258
|
+
var IMPORTANT = [
|
|
8259
|
+
"validaci",
|
|
8260
|
+
"validation",
|
|
8261
|
+
"paginaci",
|
|
8262
|
+
"pagination",
|
|
8263
|
+
"soft delete",
|
|
8264
|
+
"sponsor",
|
|
8265
|
+
"agenda",
|
|
8266
|
+
"ubicaci",
|
|
8267
|
+
"location",
|
|
8268
|
+
"rol",
|
|
8269
|
+
"role",
|
|
8270
|
+
"permis",
|
|
8271
|
+
"permission"
|
|
8272
|
+
];
|
|
8273
|
+
function matchesAny(text3, keywords) {
|
|
8274
|
+
return keywords.some((k) => text3.includes(k));
|
|
8275
|
+
}
|
|
8276
|
+
function classifyQuestion(question) {
|
|
8277
|
+
const t = question.toLowerCase();
|
|
8278
|
+
if (matchesAny(t, BLOCKING)) {
|
|
8279
|
+
return { classification: "blocking", reason: "Affects scope, architecture or the first Work Items." };
|
|
8280
|
+
}
|
|
8281
|
+
if (matchesAny(t, DEFERRED) && !matchesAny(t, IMPORTANT)) {
|
|
8282
|
+
return { classification: "deferred", reason: "Can be moved to a later phase." };
|
|
8283
|
+
}
|
|
8284
|
+
if (matchesAny(t, IMPORTANT)) {
|
|
8285
|
+
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
8286
|
+
}
|
|
8287
|
+
return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
|
|
8288
|
+
}
|
|
8289
|
+
function extractFromMarkdown(md) {
|
|
8290
|
+
const lines = md.split(/\r?\n/);
|
|
8291
|
+
const start = lines.findIndex((l) => /^#{1,6}\s+open questions\s*$/i.test(l.trim()) || /^#{1,6}\s+preguntas abiertas\s*$/i.test(l.trim()));
|
|
8292
|
+
if (start < 0) return [];
|
|
8293
|
+
const out = [];
|
|
8294
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
8295
|
+
const line = lines[i];
|
|
8296
|
+
if (/^#{1,6}\s+/.test(line)) break;
|
|
8297
|
+
const m = line.match(/^\s*(?:[-*]|\d+\.)\s+(.+?)\s*$/);
|
|
8298
|
+
if (m) {
|
|
8299
|
+
const q = m[1].trim();
|
|
8300
|
+
if (q && !/^_.*_$/.test(q)) out.push(q);
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
return out;
|
|
8304
|
+
}
|
|
8305
|
+
function suggestedAssumption(question) {
|
|
8306
|
+
const t = question.toLowerCase();
|
|
8307
|
+
if (matchesAny(t, ["api", "web app", "webapp", "cli", "backend"])) {
|
|
8308
|
+
return "Start as a backend API to keep the initial scope small.";
|
|
8309
|
+
}
|
|
8310
|
+
if (matchesAny(t, ["auth", "autenticaci"])) {
|
|
8311
|
+
return "No authentication in the MVP.";
|
|
8312
|
+
}
|
|
8313
|
+
if (matchesAny(t, ["fastify", "nestjs", "framework", "stack"])) {
|
|
8314
|
+
return "Choose the lighter stack option to minimize MVP scope.";
|
|
8315
|
+
}
|
|
8316
|
+
if (matchesAny(t, ["persist", "base de datos", "database"])) {
|
|
8317
|
+
return "Use a simple local datastore (e.g. SQLite) for the MVP.";
|
|
8318
|
+
}
|
|
8319
|
+
return "Proceed with the smallest reasonable MVP interpretation and revisit later.";
|
|
8320
|
+
}
|
|
8321
|
+
function buildOpenQuestionsReport(dir, now = /* @__PURE__ */ new Date()) {
|
|
8322
|
+
const project = loadConfig(dir)?.project.name ?? "unknown";
|
|
8323
|
+
const questions = [];
|
|
8324
|
+
let seq = 0;
|
|
8325
|
+
for (const source of SOURCES) {
|
|
8326
|
+
const p2 = join(dir, source);
|
|
8327
|
+
if (!exists(p2)) continue;
|
|
8328
|
+
let md;
|
|
8329
|
+
try {
|
|
8330
|
+
md = readFile(p2);
|
|
8331
|
+
} catch {
|
|
8332
|
+
continue;
|
|
8333
|
+
}
|
|
8334
|
+
for (const q of extractFromMarkdown(md)) {
|
|
8335
|
+
seq += 1;
|
|
8336
|
+
const { classification, reason } = classifyQuestion(q);
|
|
8337
|
+
questions.push({
|
|
8338
|
+
id: `OQ-${String(seq).padStart(3, "0")}`,
|
|
8339
|
+
source,
|
|
8340
|
+
section: "Open Questions",
|
|
8341
|
+
question: q,
|
|
8342
|
+
classification,
|
|
8343
|
+
reason,
|
|
8344
|
+
...classification === "blocking" ? { suggested_assumption: suggestedAssumption(q) } : {}
|
|
8345
|
+
});
|
|
8346
|
+
}
|
|
8347
|
+
}
|
|
8348
|
+
const blocking = questions.filter((q) => q.classification === "blocking");
|
|
8349
|
+
const important = questions.filter((q) => q.classification === "important");
|
|
8350
|
+
const deferred = questions.filter((q) => q.classification === "deferred");
|
|
8351
|
+
let readiness;
|
|
8352
|
+
if (questions.length === 0) readiness = "unknown";
|
|
8353
|
+
else if (blocking.length > 0) readiness = "needs_decisions";
|
|
8354
|
+
else readiness = "ready";
|
|
8355
|
+
const suggested_assumptions = blocking.map((q) => q.suggested_assumption).filter(Boolean);
|
|
8356
|
+
const recommended_next_step = readiness === "needs_decisions" ? "Confirm suggested assumptions with the user (resolve, assume or defer blocking questions) before generating the roadmap." : readiness === "ready" ? "No blocking questions \u2014 you can proceed to the roadmap (review important questions when convenient)." : "No open questions found. Run `kaddo bootstrap` to capture early questions, or proceed.";
|
|
8357
|
+
return {
|
|
8358
|
+
generated_at: now.toISOString(),
|
|
8359
|
+
project,
|
|
8360
|
+
summary: {
|
|
8361
|
+
open_questions: questions.length,
|
|
8362
|
+
roadmap_readiness: readiness,
|
|
8363
|
+
blocking: blocking.length,
|
|
8364
|
+
important: important.length,
|
|
8365
|
+
deferred: deferred.length
|
|
8366
|
+
},
|
|
8367
|
+
questions,
|
|
8368
|
+
blocking_questions: blocking,
|
|
8369
|
+
important_questions: important,
|
|
8370
|
+
deferred_questions: deferred,
|
|
8371
|
+
suggested_assumptions,
|
|
8372
|
+
recommended_next_step
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
function renderOpenQuestionsMarkdown(r) {
|
|
8376
|
+
const L = [];
|
|
8377
|
+
L.push("# Kaddo Open Questions Readiness Report", "");
|
|
8378
|
+
L.push(`Generated at: ${r.generated_at}`);
|
|
8379
|
+
L.push(`Project: ${r.project}`);
|
|
8380
|
+
L.push("");
|
|
8381
|
+
L.push("## Summary", "");
|
|
8382
|
+
L.push(`- Open questions found: ${r.summary.open_questions}`);
|
|
8383
|
+
L.push(`- Roadmap readiness: ${r.summary.roadmap_readiness}`);
|
|
8384
|
+
L.push(`- Blocking: ${r.summary.blocking}`);
|
|
8385
|
+
L.push(`- Important: ${r.summary.important}`);
|
|
8386
|
+
L.push(`- Deferred: ${r.summary.deferred}`);
|
|
8387
|
+
L.push("");
|
|
8388
|
+
if (r.summary.open_questions === 0) {
|
|
8389
|
+
L.push("No `## Open Questions` sections found in business / product / codebase / roadmap.", "");
|
|
8390
|
+
L.push("## Recommended Next Step", "", r.recommended_next_step, "");
|
|
8391
|
+
return L.join("\n");
|
|
8392
|
+
}
|
|
8393
|
+
L.push("## Blocking Questions", "");
|
|
8394
|
+
if (r.blocking_questions.length === 0) L.push("_None._", "");
|
|
8395
|
+
for (const q of r.blocking_questions) {
|
|
8396
|
+
L.push(`### ${q.id}`, "");
|
|
8397
|
+
L.push(`- Source: \`${q.source}\``);
|
|
8398
|
+
L.push(`- Question: ${q.question}`);
|
|
8399
|
+
L.push(`- Reason: ${q.reason}`);
|
|
8400
|
+
if (q.suggested_assumption) L.push(`- Suggested assumption: ${q.suggested_assumption}`);
|
|
8401
|
+
L.push("");
|
|
8402
|
+
}
|
|
8403
|
+
L.push("## Important Questions", "");
|
|
8404
|
+
if (r.important_questions.length === 0) L.push("_None._");
|
|
8405
|
+
for (const q of r.important_questions) L.push(`- ${q.question}`);
|
|
8406
|
+
L.push("");
|
|
8407
|
+
L.push("## Deferred Questions", "");
|
|
8408
|
+
if (r.deferred_questions.length === 0) L.push("_None._");
|
|
8409
|
+
for (const q of r.deferred_questions) L.push(`- ${q.question}`);
|
|
8410
|
+
L.push("");
|
|
8411
|
+
if (r.suggested_assumptions.length > 0) {
|
|
8412
|
+
L.push("## Suggested Assumptions", "");
|
|
8413
|
+
for (const a of r.suggested_assumptions) L.push(`- ${a}`);
|
|
8414
|
+
L.push("");
|
|
8415
|
+
}
|
|
8416
|
+
L.push("## Recommended Next Step", "", r.recommended_next_step, "");
|
|
8417
|
+
return L.join("\n");
|
|
8418
|
+
}
|
|
8419
|
+
function serializeOpenQuestionsJson(r) {
|
|
8420
|
+
return JSON.stringify(r, null, 2) + "\n";
|
|
8421
|
+
}
|
|
8422
|
+
|
|
8165
8423
|
// src/commands/understand.ts
|
|
8166
8424
|
function runUnderstand() {
|
|
8167
8425
|
const dir = cwd();
|
|
@@ -8234,6 +8492,12 @@ function runUnderstand() {
|
|
|
8234
8492
|
console.log(" \u2192 Review graph hints before continuing with implementation.");
|
|
8235
8493
|
console.log(" Suggested agent: graph-agent (see .kaddo/graph-hints.md)");
|
|
8236
8494
|
}
|
|
8495
|
+
const oq = buildOpenQuestionsReport(dir);
|
|
8496
|
+
if (oq.summary.blocking > 0) {
|
|
8497
|
+
console.log("");
|
|
8498
|
+
console.log(`Open questions: ${oq.summary.blocking} blocking decision(s) before the roadmap (roadmap readiness: ${oq.summary.roadmap_readiness}).`);
|
|
8499
|
+
console.log(" \u2192 Run `kaddo questions` to review them, then resolve, assume or defer before `kaddo create --from roadmap`.");
|
|
8500
|
+
}
|
|
8237
8501
|
const active = activeWorkItems(dir);
|
|
8238
8502
|
if (active.length > 0) {
|
|
8239
8503
|
console.log("");
|
|
@@ -8291,35 +8555,35 @@ var DEP_PATTERNS = [
|
|
|
8291
8555
|
function normalizePath2(p2) {
|
|
8292
8556
|
return p2.split(path5.sep).join("/");
|
|
8293
8557
|
}
|
|
8294
|
-
function
|
|
8558
|
+
function matchesAny2(file, patterns) {
|
|
8295
8559
|
const normalized = normalizePath2(file);
|
|
8296
8560
|
return patterns.some((p2) => p2.test(normalized));
|
|
8297
8561
|
}
|
|
8298
8562
|
function detectSignals(touchedFiles) {
|
|
8299
8563
|
const signals = [];
|
|
8300
8564
|
for (const file of touchedFiles) {
|
|
8301
|
-
if (
|
|
8565
|
+
if (matchesAny2(file, MIGRATION_PATTERNS)) {
|
|
8302
8566
|
signals.push({
|
|
8303
8567
|
name: "DB migration",
|
|
8304
8568
|
file,
|
|
8305
8569
|
implication: "Data schema change \u2014 typically K4 or Migration type",
|
|
8306
8570
|
suggestedLevel: "K4"
|
|
8307
8571
|
});
|
|
8308
|
-
} else if (
|
|
8572
|
+
} else if (matchesAny2(file, CONTRACT_PATTERNS)) {
|
|
8309
8573
|
signals.push({
|
|
8310
8574
|
name: "API/event contract",
|
|
8311
8575
|
file,
|
|
8312
8576
|
implication: "Contract change affects consumers \u2014 typically K3/K4",
|
|
8313
8577
|
suggestedLevel: "K3"
|
|
8314
8578
|
});
|
|
8315
|
-
} else if (
|
|
8579
|
+
} else if (matchesAny2(file, INFRA_PATTERNS)) {
|
|
8316
8580
|
signals.push({
|
|
8317
8581
|
name: "Infrastructure",
|
|
8318
8582
|
file,
|
|
8319
8583
|
implication: "Infrastructure change \u2014 typically K3/K4",
|
|
8320
8584
|
suggestedLevel: "K3"
|
|
8321
8585
|
});
|
|
8322
|
-
} else if (
|
|
8586
|
+
} else if (matchesAny2(file, DEP_PATTERNS)) {
|
|
8323
8587
|
signals.push({
|
|
8324
8588
|
name: "Dependency",
|
|
8325
8589
|
file,
|
|
@@ -11656,11 +11920,23 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11656
11920
|
const graphMult = GRAPH_MULT[graphQuality] ?? 0;
|
|
11657
11921
|
const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
|
|
11658
11922
|
const guard = buildGuardHistory(dir);
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
hours:
|
|
11662
|
-
|
|
11663
|
-
|
|
11923
|
+
let driftSavings;
|
|
11924
|
+
if (!guard.available) {
|
|
11925
|
+
driftSavings = { available: false, hours: 0, reason: "Guard history is not persisted yet." };
|
|
11926
|
+
} else if (guard.resolved > 0) {
|
|
11927
|
+
driftSavings = {
|
|
11928
|
+
available: true,
|
|
11929
|
+
hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
|
|
11930
|
+
formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
|
|
11931
|
+
};
|
|
11932
|
+
} else {
|
|
11933
|
+
driftSavings = {
|
|
11934
|
+
available: true,
|
|
11935
|
+
hours: 0,
|
|
11936
|
+
formula: `0 resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`,
|
|
11937
|
+
reason: "Guard history exists, but no resolved drift warnings have been recorded yet."
|
|
11938
|
+
};
|
|
11939
|
+
}
|
|
11664
11940
|
const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
|
|
11665
11941
|
const totalValue = Math.round(totalHours * a.hourly_cost);
|
|
11666
11942
|
const reasons = [];
|
|
@@ -11675,15 +11951,27 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11675
11951
|
level = "Medium";
|
|
11676
11952
|
reasons.push("Strong knowledge and graph evidence.");
|
|
11677
11953
|
}
|
|
11678
|
-
if (!guard.available)
|
|
11679
|
-
|
|
11954
|
+
if (!guard.available) {
|
|
11955
|
+
reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
|
|
11956
|
+
} else if (guard.resolved > 0) {
|
|
11957
|
+
reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
|
|
11958
|
+
} else {
|
|
11959
|
+
reasons.push("Guard history available, but no resolved drift warnings have been recorded yet.");
|
|
11960
|
+
}
|
|
11680
11961
|
reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
|
|
11681
11962
|
const actions = [];
|
|
11682
11963
|
if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
|
|
11683
11964
|
actions.push("Calibrate hourly cost and team assumptions with real team data.");
|
|
11684
11965
|
actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
|
|
11685
11966
|
actions.push("Use this report as directional evidence, not accounting data.");
|
|
11686
|
-
|
|
11967
|
+
if (!guard.available) {
|
|
11968
|
+
actions.push("Future: persist Guard history to estimate avoided rework.");
|
|
11969
|
+
} else if (guard.resolved === 0) {
|
|
11970
|
+
actions.push("Continue running `kaddo guard --record` before important commits to capture resolved drift warnings.");
|
|
11971
|
+
if (guard.open > 0) actions.push("Review open drift warnings with `kaddo drift`.");
|
|
11972
|
+
} else {
|
|
11973
|
+
actions.push("Use drift history to calibrate `rework_hours_avoided_per_resolved_drift`.");
|
|
11974
|
+
}
|
|
11687
11975
|
return {
|
|
11688
11976
|
generated_at: now.toISOString(),
|
|
11689
11977
|
project: impact.project,
|
|
@@ -11709,7 +11997,9 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
|
|
|
11709
11997
|
graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
|
|
11710
11998
|
graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
|
|
11711
11999
|
context_readiness: readiness,
|
|
11712
|
-
guard_history_available: guard.available
|
|
12000
|
+
guard_history_available: guard.available,
|
|
12001
|
+
guard_runs_recorded: guard.total_runs,
|
|
12002
|
+
resolved_drift_warnings: guard.resolved
|
|
11713
12003
|
},
|
|
11714
12004
|
estimated_savings: {
|
|
11715
12005
|
context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
|
|
@@ -11759,7 +12049,13 @@ function renderSavingsMarkdown(r) {
|
|
|
11759
12049
|
if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
|
|
11760
12050
|
if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
|
|
11761
12051
|
L.push(`- Context readiness: ${e.context_readiness}`);
|
|
11762
|
-
|
|
12052
|
+
if (e.guard_history_available) {
|
|
12053
|
+
L.push("- Guard history: available");
|
|
12054
|
+
L.push(`- Guard runs recorded: ${e.guard_runs_recorded}`);
|
|
12055
|
+
L.push(`- Resolved drift warnings: ${e.resolved_drift_warnings}`);
|
|
12056
|
+
} else {
|
|
12057
|
+
L.push("- Guard history: not available");
|
|
12058
|
+
}
|
|
11763
12059
|
L.push("");
|
|
11764
12060
|
L.push("## Estimated Savings", "");
|
|
11765
12061
|
const s = r.estimated_savings;
|
|
@@ -11776,7 +12072,9 @@ function renderSavingsMarkdown(r) {
|
|
|
11776
12072
|
L.push("### Drift Prevention", "");
|
|
11777
12073
|
if (s.drift_prevention.available) {
|
|
11778
12074
|
L.push(`- Formula: ${s.drift_prevention.formula}`);
|
|
11779
|
-
L.push(`- Estimated: ${s.drift_prevention.hours} h
|
|
12075
|
+
L.push(`- Estimated: ${s.drift_prevention.hours} h`);
|
|
12076
|
+
if (s.drift_prevention.reason) L.push(`- Reason: ${s.drift_prevention.reason}`);
|
|
12077
|
+
L.push("");
|
|
11780
12078
|
} else {
|
|
11781
12079
|
L.push("- Not available yet.");
|
|
11782
12080
|
L.push(`- Reason: ${s.drift_prevention.reason}`, "");
|
|
@@ -11961,6 +12259,42 @@ function runDrift(opts = {}) {
|
|
|
11961
12259
|
console.log(content);
|
|
11962
12260
|
}
|
|
11963
12261
|
|
|
12262
|
+
// src/commands/questions.ts
|
|
12263
|
+
function runQuestions(opts = {}) {
|
|
12264
|
+
const dir = cwd();
|
|
12265
|
+
requireConfig(dir);
|
|
12266
|
+
const report = buildOpenQuestionsReport(dir);
|
|
12267
|
+
if (opts.output) {
|
|
12268
|
+
intro2("kaddo questions");
|
|
12269
|
+
writeFile(join(dir, opts.output), opts.json ? serializeOpenQuestionsJson(report) : renderOpenQuestionsMarkdown(report));
|
|
12270
|
+
log2.success(`Wrote ${opts.output.replace(/\\/g, "/")}`);
|
|
12271
|
+
printCommandFooter("questions");
|
|
12272
|
+
outro2("Questions report written.");
|
|
12273
|
+
return;
|
|
12274
|
+
}
|
|
12275
|
+
if (opts.json) {
|
|
12276
|
+
console.log(serializeOpenQuestionsJson(report));
|
|
12277
|
+
return;
|
|
12278
|
+
}
|
|
12279
|
+
console.log("");
|
|
12280
|
+
console.log(`Open questions detected: ${report.summary.open_questions}`);
|
|
12281
|
+
console.log(`Roadmap readiness: ${report.summary.roadmap_readiness === "needs_decisions" ? "needs decisions" : report.summary.roadmap_readiness}`);
|
|
12282
|
+
const list2 = (title, qs) => {
|
|
12283
|
+
if (qs.length === 0) return;
|
|
12284
|
+
console.log("");
|
|
12285
|
+
console.log(`${title}:`);
|
|
12286
|
+
for (const q of qs.slice(0, 8)) console.log(`- ${q.question}`);
|
|
12287
|
+
if (qs.length > 8) console.log(` \u2026and ${qs.length - 8} more`);
|
|
12288
|
+
};
|
|
12289
|
+
list2("Blocking", report.blocking_questions);
|
|
12290
|
+
list2("Important", report.important_questions);
|
|
12291
|
+
list2("Deferred", report.deferred_questions);
|
|
12292
|
+
console.log("");
|
|
12293
|
+
console.log("Suggested next:");
|
|
12294
|
+
console.log(report.recommended_next_step);
|
|
12295
|
+
printCommandFooter("questions");
|
|
12296
|
+
}
|
|
12297
|
+
|
|
11964
12298
|
// src/index.ts
|
|
11965
12299
|
var require2 = createRequire(import.meta.url);
|
|
11966
12300
|
var { version } = require2("../package.json");
|
|
@@ -12011,6 +12345,9 @@ reportCmd.command("drift").description("Drift Trend Report from recorded guard h
|
|
|
12011
12345
|
program.command("drift").description("Drift Trend Report from recorded `kaddo guard --record` history").option("--json", "Output JSON instead of Markdown").option("--output <path>", "Write the report to a file").action((opts) => {
|
|
12012
12346
|
runDrift(opts);
|
|
12013
12347
|
});
|
|
12348
|
+
var questionsAction = (opts) => runQuestions(opts);
|
|
12349
|
+
program.command("questions").description("Open-questions readiness gate: blocking/important/deferred decisions before the roadmap").option("--json", "Output JSON instead of a summary").option("--output <path>", "Write the report to a file (e.g. .kaddo/reports/questions-report.md)").action(questionsAction);
|
|
12350
|
+
program.command("readiness").description("Alias for `kaddo questions`").option("--json", "Output JSON instead of a summary").option("--output <path>", "Write the report to a file").action(questionsAction);
|
|
12014
12351
|
program.command("guard").description("Check if modified code has related artifacts that were not updated").option("--staged", "Check only staged files").option("--no-interactive", "Disable interactive ignore prompts").option("--ci", "CI mode: output JSON, no prompts, non-blocking").option("--json", "Output JSON (alias for --ci)").option("--workspace", "Also check local mapped module repos from .kaddo/modules.yml (opt-in)").option("--include-archived", "Include archived Work Items in ownership matching (excluded by default)").option("--record", "Record this run to .kaddo/history/ for drift trend reporting").action(async (opts) => {
|
|
12015
12352
|
await runGuard(opts);
|
|
12016
12353
|
});
|