Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Each entry links the GitHub issue (the canonical spec) and the merge PR (the shi

### Changed

- **Doctor advisory checks and dead lib.js readers deleted** — `backlog-doctor.js` drops `in_flight_staleness` (`--stale-days`) and `context_bloat`; both re-derive from `status.sh --json` `age_days` and `wc -l` and nothing consumed them. `lib.js` drops `parseMarkerMonth` and the triage config readers orphaned by #433 (259 lines with tests). The three guard checks and their JSON shapes are unchanged. Closes [#458](https://github.com/sungjunlee/dev-backlog/issues/458).
- **status.sh trailing sections deleted** — the Local Files block (counted `tasks/*.md` mirrors by a Backlog.md `status:` field, a format parked at `v0.11.0`), the Relay Runs block (re-derived another tool's run-directory slug and grepped its manifests), and the Past sprints line are gone (54 lines with their smoke fixture); each was one command away and nothing consumed them. Output now ends after the Active Sprint block; `--json` and `--track` are unchanged. Closes [#459](https://github.com/sungjunlee/dev-backlog/issues/459).

## [0.12.0] — 2026-09-17
Expand Down
109 changes: 4 additions & 105 deletions skills/dev-backlog/scripts/backlog-doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/**
* Aggregate deterministic dev-backlog health checks into one CI-ready probe.
* Each check normalizes to one verdict: active-sprint invariant, sprint shape,
* in-flight trace/staleness, _context.md bloat. Reassess is a human call at
* sprint close, not a counter (charter rev 19, #446).
* in-flight trace. Reassess is a human call at sprint close, not a counter
* (charter rev 19, #446).
*/

const fs = require("fs");
Expand All @@ -24,23 +24,19 @@ const {
const { repoDisplayPath } = require("./portable-path.js");

const SCHEMA_VERSION = 1;
const DEFAULT_STALE_DAYS = 7;
const CONTEXT_BLOAT_LINE_THRESHOLD = 200;
const REQUIRED_ACTIVE_SECTIONS = ["Goal", "Plan", "Running Context", "Progress"];

function usage() {
return [
"Usage: backlog-doctor.js [--json] [--stale-days N] [backlog-dir]",
"Usage: backlog-doctor.js [--json] [backlog-dir]",
"",
"Runs active-sprint, sprint-shape, in-flight trace/staleness,",
"and _context.md bloat checks.",
"Runs active-sprint, sprint-shape, and in-flight trace checks.",
].join("\n");
}

function parseArgs(args) {
const options = {
backlogDir: DEFAULT_BACKLOG_DIR,
staleDays: DEFAULT_STALE_DAYS,
json: false,
};
let backlogDirSet = false;
Expand All @@ -52,21 +48,6 @@ function parseArgs(args) {
options.json = true;
continue;
}
if (arg === "--stale-days") {
const next = args[i + 1];
if (!next) return { ...options, error: `Missing value for --stale-days. ${usage()}` };
const parsed = parseStaleDays(next);
if (parsed.error) return { ...options, error: parsed.error };
options.staleDays = parsed.value;
i += 1;
continue;
}
if (arg.startsWith("--stale-days=")) {
const parsed = parseStaleDays(arg.slice("--stale-days=".length));
if (parsed.error) return { ...options, error: parsed.error };
options.staleDays = parsed.value;
continue;
}
if (arg.startsWith("--")) {
return { ...options, error: `Unknown argument: ${arg}. ${usage()}` };
}
Expand All @@ -80,19 +61,10 @@ function parseArgs(args) {
return options;
}

function parseStaleDays(raw) {
if (!/^\d+$/.test(raw)) {
return { error: `Invalid --stale-days value: ${raw}. Expected a non-negative integer.` };
}
return { value: Number.parseInt(raw, 10) };
}

function runDoctor({
repoRoot = process.cwd(),
backlogDir = DEFAULT_BACKLOG_DIR,
staleDays = DEFAULT_STALE_DAYS,
today = new Date(),
contextLineThreshold = CONTEXT_BLOAT_LINE_THRESHOLD,
} = {}) {
const root = path.resolve(repoRoot);
const backlogPath = resolvePath(root, backlogDir);
Expand All @@ -115,19 +87,13 @@ function runDoctor({
tag,
),
in_flight_trace: tagTrack(checkInFlightTrace({ sprintState, activeStatus: active.status }), tag),
in_flight_staleness: tagTrack(
checkInFlightStaleness({ sprintState, activeStatus: active.status, staleDays }),
tag,
),
};
});

const checks = [
active,
...perTrack.map((run) => run.sprint_shape),
...perTrack.map((run) => run.in_flight_trace),
...perTrack.map((run) => run.in_flight_staleness),
checkContextBloat({ repoRoot: root, sprintsDir, threshold: contextLineThreshold }),
];
Comment on lines 94 to 97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the living contract before removing these checks

For stale in-flight work or an _context.md over 200 lines, this list now lets the doctor report no corresponding warning, but spec/capabilities.md:100 still explicitly declares in_flight_staleness and context_bloat as retained doctor checks. Either preserve them or obtain explicit approval and amend the durable capability spec so shipped behavior does not contradict the living contract.

AGENTS.md reference: AGENTS.md:L56-L60

Useful? React with 👍 / 👎.


return {
Expand Down Expand Up @@ -348,71 +314,6 @@ function checkInFlightTrace({ sprintState, activeStatus }) {
});
}

function checkInFlightStaleness({ sprintState, activeStatus, staleDays }) {
if (sprintState.error) {
return verdict("in_flight_staleness", activeStatus === "fail" ? "warn" : "fail", {
summary: `Skipped in-flight staleness check: ${sprintState.error.message}`,
stale_days: staleDays,
});
}
if (!sprintState.state || !sprintState.state.active_sprint) {
return verdict("in_flight_staleness", "pass", {
summary: "No active sprint; no in-flight age to check.",
stale_days: staleDays,
});
}

const stale = sprintState.state.in_flight
.filter((item) => item.age_days !== null && item.age_days > staleDays);
if (stale.length > 0) {
return verdict("in_flight_staleness", "warn", {
summary: `${stale.length} in-flight item(s) are older than ${staleDays} day(s).`,
stale_days: staleDays,
items: stale.map(publicPlanItem),
});
}

return verdict("in_flight_staleness", "pass", {
summary: `No in-flight items are older than ${staleDays} day(s).`,
stale_days: staleDays,
in_flight_count: sprintState.state.in_flight.length,
});
}

function checkContextBloat({ repoRoot, sprintsDir, threshold }) {
const contextPath = path.join(sprintsDir, "_context.md");
if (!fs.existsSync(contextPath)) {
return verdict("context_bloat", "pass", {
summary: "_context.md is absent; no cross-sprint context bloat detected.",
threshold_lines: threshold,
line_count: 0,
});
}

const content = fs.readFileSync(contextPath, "utf-8");
const lineCount = countLines(content);
if (lineCount > threshold) {
return verdict("context_bloat", "warn", {
summary: `_context.md is ${lineCount} lines, above the ${threshold}-line bloat threshold.`,
context_path: displayPath(repoRoot, contextPath),
threshold_lines: threshold,
line_count: lineCount,
});
}

return verdict("context_bloat", "pass", {
summary: `_context.md is within the ${threshold}-line bloat threshold.`,
context_path: displayPath(repoRoot, contextPath),
threshold_lines: threshold,
line_count: lineCount,
});
}

function countLines(content) {
if (content === "") return 0;
return content.split(/\r?\n/).length;
}

// Whitelist (order-stable) of the plan-item fields a verdict may publish.
const PUBLIC_PLAN_FIELDS = [
"line", "tracker", "id", "ref", "issue_number",
Expand Down Expand Up @@ -477,8 +378,6 @@ if (require.main === module) main();
module.exports = {
SCHEMA_VERSION,
DEFAULT_BACKLOG_DIR,
DEFAULT_STALE_DAYS,
CONTEXT_BLOAT_LINE_THRESHOLD,
REQUIRED_ACTIVE_SECTIONS,
parseArgs,
runDoctor,
Expand Down
73 changes: 2 additions & 71 deletions skills/dev-backlog/scripts/lib.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
/**
* Shared library for dev-backlog Node scripts.
*/
/** Shared library for dev-backlog Node scripts. */

const fs = require("fs");
const path = require("path");
const { DEFAULT_BACKLOG_DIR } = require("./execution-root.js");

const GH_EXEC_DEFAULTS = Object.freeze({
Expand Down Expand Up @@ -49,11 +45,6 @@ function containsIssueRef(text, identity) {
return false;
}

// Progress-issue publication was removed (#340); this marker survives only to
// recognize leftover machine-managed issue bodies in triage-collect.
const PROGRESS_MARKER_PREFIX = "<!-- dev-backlog:progress-issue month=";
const MARKER_SUFFIX = " -->";

function slugify(text) {
return text
.replace(/[^a-zA-Z0-9]/g, "-")
Expand All @@ -62,24 +53,6 @@ function slugify(text) {
.toLowerCase();
}

function parseMarkerMonth(body) {
if (!body) return null;
const start = body.indexOf(PROGRESS_MARKER_PREFIX);
if (start === -1) return null;
const valueStart = start + PROGRESS_MARKER_PREFIX.length;
const end = body.indexOf(MARKER_SUFFIX, valueStart);
return end === -1 ? null : body.slice(valueStart, end).trim();
}

const TRIAGE_CONFIG_DEFAULTS = {
theme_keywords: {},
activity_days: {
warm: 14,
cold: 60,
},
stale_days: 60,
};

function stripQuotes(text) {
if (
(text.startsWith('"') && text.endsWith('"')) ||
Expand All @@ -89,13 +62,11 @@ function stripQuotes(text) {
}
return text;
}

function parseInlineArray(raw) {
const inner = raw.slice(1, -1).trim();
if (!inner) return [];
return inner.split(",").map((part) => stripQuotes(part.trim()));
}

function stripYamlSeparationComment(raw) {
let quote = null;
const firstNonSpace = raw.search(/\S/);
Expand Down Expand Up @@ -123,7 +94,6 @@ function stripYamlSeparationComment(raw) {
}
return raw;
}

function parseYamlScalar(raw) {
const value = stripYamlSeparationComment(raw).trim();
if (!value) return "";
Expand All @@ -143,12 +113,10 @@ function parseYamlScalar(raw) {
if (value === "false") return false;
return value;
}

function isBlockScalarValue(raw) {
const value = stripYamlSeparationComment(raw).trim();
return /^(?:(?:[&!]\S+)\s+)*(?:[>|](?:[1-9][+-]?|[+-][1-9]?)?)$/.test(value);
}

function quotedScalarCloses(text, quote, start = 0) {
for (let index = start; index < text.length; index += 1) {
if (quote === "'" && text[index] === "'" && text[index + 1] === "'") {
Expand All @@ -161,7 +129,6 @@ function quotedScalarCloses(text, quote, start = 0) {
}
return false;
}

function parseSimpleYaml(raw) {
const root = {};
const stack = [{ indent: -1, value: root }];
Expand Down Expand Up @@ -212,38 +179,6 @@ function parseSimpleYaml(raw) {
return root;
}

function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}

function mergeConfig(defaults, parsed) {
const merged = { ...defaults };
for (const [key, value] of Object.entries(parsed || {})) {
if (isPlainObject(value) && isPlainObject(defaults[key])) {
merged[key] = mergeConfig(defaults[key], value);
continue;
}
merged[key] = value;
}
return merged;
}

function readYamlConfig(configPath, defaults) {
try {
const raw = fs.readFileSync(configPath, "utf-8");
return mergeConfig(defaults, parseSimpleYaml(raw));
} catch {
return mergeConfig(defaults, {});
}
}

function readTriageConfig(backlogDir) {
return readYamlConfig(
path.join(backlogDir || DEFAULT_BACKLOG_DIR, "triage-config.yml"),
TRIAGE_CONFIG_DEFAULTS
);
}

/**
* Resolve a sprint's scope key from its frontmatter (multi-track partitioning).
* Priority: non-empty `component:` wins; else `scope:` path globs; else none.
Expand All @@ -260,8 +195,7 @@ function sprintScopeKey(frontmatter) {
return { kind: "none" };
}

// Reduce a path glob to a comparable directory prefix:
// "src/auth/**" -> "src/auth", "src/auth/*" -> "src/auth", "src/auth/" -> "src/auth".
// Path glob -> directory prefix: "src/auth/**" / "src/auth/*" / "src/auth/" -> "src/auth".
function normalizeScopePrefix(glob) {
return String(glob).replace(/\/+\**$/, "").replace(/\/+$/, "");
}
Expand Down Expand Up @@ -293,16 +227,13 @@ function scopesOverlap(frontmatterA, frontmatterB) {

module.exports = {
slugify,
parseMarkerMonth,
parseSimpleYaml,
sprintScopeKey,
scopesOverlap,
readTriageConfig,
ISSUE_REF_RE,
parseIssueRef,
parsePlanCheckbox,
containsIssueRef,
TRIAGE_CONFIG_DEFAULTS,
DEFAULT_BACKLOG_DIR,
GH_EXEC_DEFAULTS,
};
Loading
Loading