@dzhechkov/harness-cli 0.3.251 → 0.3.252
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/.dz-manifest.json +8 -8
- package/README.md +42 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +165 -33
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +7 -7
- package/src/cli.ts +151 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.252",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"sbom.json"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@dzhechkov/harness-core": "^0.3.
|
|
44
|
+
"@dzhechkov/harness-core": "^0.3.143",
|
|
45
45
|
"@dzhechkov/harness-presets": "^0.5.0",
|
|
46
46
|
"@dzhechkov/scout": "^0.8.0",
|
|
47
47
|
"@dzhechkov/skills-devops": "^0.3.0",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "ff1b1a2239a254f500c999c5c25ab9303561b433fb1cac420a2b28a7bd0d9fd6"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "c9b415287eda0a7c2ad32dbb7cfc3ecce19aa3b90c96d3fbeedd62d20fe34bc7"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hashes": [
|
|
116
116
|
{
|
|
117
117
|
"alg": "SHA-256",
|
|
118
|
-
"content": "
|
|
118
|
+
"content": "56b4da2639c1721180387f28166d84d065718a4002f556307a6598570d335809"
|
|
119
119
|
}
|
|
120
120
|
]
|
|
121
121
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"hashes": [
|
|
126
126
|
{
|
|
127
127
|
"alg": "SHA-256",
|
|
128
|
-
"content": "
|
|
128
|
+
"content": "4a172ba5384c9716fa48570979b7ffba969599ed5c639d7e7268a829d2b9b4e1"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"hashes": [
|
|
186
186
|
{
|
|
187
187
|
"alg": "SHA-256",
|
|
188
|
-
"content": "
|
|
188
|
+
"content": "65e245ef567ccecdc382bb040f27dd98e01ce2c3a104935aa2dc0558b22220e2"
|
|
189
189
|
}
|
|
190
190
|
]
|
|
191
191
|
},
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"hashes": [
|
|
206
206
|
{
|
|
207
207
|
"alg": "SHA-256",
|
|
208
|
-
"content": "
|
|
208
|
+
"content": "1aceceef391cac55d00f7872632d28c4116a82025b9235343b81398fc515f944"
|
|
209
209
|
}
|
|
210
210
|
]
|
|
211
211
|
},
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"hashes": [
|
|
226
226
|
{
|
|
227
227
|
"alg": "SHA-256",
|
|
228
|
-
"content": "
|
|
228
|
+
"content": "7d6ba26ec1188db3b3ac89f50b015ba1f0c6117b5c30d99b22da41da10c96c12"
|
|
229
229
|
}
|
|
230
230
|
]
|
|
231
231
|
},
|
package/src/cli.ts
CHANGED
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
auditRecord,
|
|
118
118
|
guardExitCode,
|
|
119
119
|
DEFAULT_RULES,
|
|
120
|
+
parsePnpmLockImporters,
|
|
120
121
|
decideProvenance,
|
|
121
122
|
isInsideTree,
|
|
122
123
|
signManifest,
|
|
@@ -214,6 +215,10 @@ import {
|
|
|
214
215
|
ideaId,
|
|
215
216
|
dedupIdea,
|
|
216
217
|
readGoalMap,
|
|
218
|
+
readGoalMapDetailed,
|
|
219
|
+
parseEffort,
|
|
220
|
+
ensureBacklogGitignored,
|
|
221
|
+
isSafeId,
|
|
217
222
|
alignIdea,
|
|
218
223
|
mirrorIdeaVector,
|
|
219
224
|
snapshotIdeas,
|
|
@@ -4855,11 +4860,17 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
4855
4860
|
// a resolvable workspace dep becomes its real semver (safe → the rule passes); an UNRESOLVABLE one (points at
|
|
4856
4861
|
// no workspace package, or not a pnpm workspace) stays `workspace:*` so the rule catches a dep that WOULD ship
|
|
4857
4862
|
// raw. That is the genuinely dangerous case the rule exists for.
|
|
4858
|
-
|
|
4863
|
+
type Manifest = { name?: string; version?: string; private?: boolean; dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
4864
|
+
const manifests: Manifest[] = [];
|
|
4865
|
+
const located: { dir: string; m: Manifest }[] = [];
|
|
4859
4866
|
try {
|
|
4860
4867
|
const out = execSync('git ls-files "packages/@dzhechkov/*/package.json"', { cwd: root, encoding: 'utf-8' });
|
|
4861
4868
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
4862
|
-
try {
|
|
4869
|
+
try {
|
|
4870
|
+
const m = JSON.parse(readFileSync(join(root, rel), 'utf8')) as Manifest;
|
|
4871
|
+
manifests.push(m);
|
|
4872
|
+
located.push({ dir: rel.replace(/\/package\.json$/, ''), m });
|
|
4873
|
+
} catch { /* skip unreadable */ }
|
|
4863
4874
|
}
|
|
4864
4875
|
} catch { /* not a git repo */ }
|
|
4865
4876
|
const versionByName = new Map<string, string>();
|
|
@@ -4909,6 +4920,28 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
4909
4920
|
}
|
|
4910
4921
|
facts['skillPacks'] = skillPacks;
|
|
4911
4922
|
} catch { /* unreadable packages dir — rule reports nothing rather than inventing a violation */ }
|
|
4923
|
+
|
|
4924
|
+
// lockfile-in-sync: the ERR_PNPM_OUTDATED_LOCKFILE class — a @dzhechkov/* dep spec bumped in a
|
|
4925
|
+
// package.json while pnpm-lock.yaml still records the old specifier. FAIL-OPEN by construction: an
|
|
4926
|
+
// unreadable/unparseable lockfile yields `{parsed:false}` and the rule reports NOTHING.
|
|
4927
|
+
try {
|
|
4928
|
+
const lockText = readFileSync(join(root, 'pnpm-lock.yaml'), 'utf8');
|
|
4929
|
+
const importers = parsePnpmLockImporters(lockText);
|
|
4930
|
+
if (importers === undefined) {
|
|
4931
|
+
facts['lockfile'] = { parsed: false };
|
|
4932
|
+
} else {
|
|
4933
|
+
const rows = located
|
|
4934
|
+
.map(({ dir, m }) => {
|
|
4935
|
+
const declared: Record<string, string> = {};
|
|
4936
|
+
for (const [dep, spec] of Object.entries({ ...(m.dependencies ?? {}), ...(m.devDependencies ?? {}) })) {
|
|
4937
|
+
if (dep.startsWith('@dzhechkov/') && typeof spec === 'string') declared[dep] = spec;
|
|
4938
|
+
}
|
|
4939
|
+
return { importer: dir, declared, locked: importers[dir] };
|
|
4940
|
+
})
|
|
4941
|
+
.filter((r) => Object.keys(r.declared).length > 0);
|
|
4942
|
+
facts['lockfile'] = { parsed: true, importers: rows };
|
|
4943
|
+
}
|
|
4944
|
+
} catch { facts['lockfile'] = { parsed: false }; /* no lockfile (not a pnpm workspace) — rule stays silent */ }
|
|
4912
4945
|
}
|
|
4913
4946
|
if (op === 'consolidate') {
|
|
4914
4947
|
try { facts['drift'] = sweepSkillDrift(root, { scope: 'packages', allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name); } catch { /* skip */ }
|
|
@@ -6199,9 +6232,18 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6199
6232
|
if (sub === 'add') {
|
|
6200
6233
|
const text = (options.get('_positional_1') ?? '').trim();
|
|
6201
6234
|
if (text === '') return emitErr('an idea text is required: dz backlog add "<idea>"');
|
|
6202
|
-
|
|
6235
|
+
// The clamp is ECHOED, never silent (idea 86096d6d): `--effort 13` used to store 5 and say nothing.
|
|
6236
|
+
const eff = parseEffort(options.get('effort'), cfg.roulette.defaultEffort);
|
|
6237
|
+
if (eff.adjusted && !json && eff.note !== undefined) write(`dz backlog: ${eff.note}`);
|
|
6203
6238
|
const dryRun = flags.has('dry-run');
|
|
6204
6239
|
const verdict = await dedupIdea(projectRoot, text, cfg);
|
|
6240
|
+
// The TOP-MATCH pair (id @ cosine) is the ADR-002 calibration surface (idea ce914ac2) — observational
|
|
6241
|
+
// only: the band itself is unchanged, but a RELATED verdict now shows WHICH idea produced the cosine.
|
|
6242
|
+
// A negative cosine is a VALID observation (anti-correlated embedding) — only non-finite is absent
|
|
6243
|
+
// (Codex re-QE LOW: `>= 0` suppressed a legitimate negative top match).
|
|
6244
|
+
const topMatch = verdict.topMatchId !== undefined && Number.isFinite(verdict.cosine)
|
|
6245
|
+
? { id: verdict.topMatchId, cosine: verdict.cosine }
|
|
6246
|
+
: undefined;
|
|
6205
6247
|
if (verdict.action === 'duplicate') {
|
|
6206
6248
|
// DUPLICATE ⇒ snapshot + reinforce the existing root; NO new record (ADR-002 T-002b).
|
|
6207
6249
|
const ideas = readIdeas(projectRoot);
|
|
@@ -6212,8 +6254,11 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6212
6254
|
match.uses += 1;
|
|
6213
6255
|
writeIdeas(projectRoot, ideas);
|
|
6214
6256
|
}
|
|
6215
|
-
if (json) write(JSON.stringify({ action: 'duplicate', matchedId: verdict.matchedId, cosine: verdict.cosine, exitCode: 0 }, null, 2));
|
|
6216
|
-
else
|
|
6257
|
+
if (json) write(JSON.stringify({ action: 'duplicate', matchedId: verdict.matchedId, cosine: verdict.cosine, ...(topMatch !== undefined ? { topMatch } : {}), ...(eff.note !== undefined ? { effortNote: eff.note } : {}), exitCode: 0 }, null, 2));
|
|
6258
|
+
else {
|
|
6259
|
+
write(`dz backlog: DUPLICATE of ${verdict.matchedId} (cosine ${verdict.cosine.toFixed(3)}${verdict.exactTextOnly ? ', exact-text' : ''}) — reinforced, no new record`);
|
|
6260
|
+
if (topMatch !== undefined) write(` top match ${topMatch.id} @ cosine ${topMatch.cosine.toFixed(3)} (DUPLICATE band ≥ ${cfg.dedup.duplicateThreshold})`);
|
|
6261
|
+
}
|
|
6217
6262
|
return 0;
|
|
6218
6263
|
}
|
|
6219
6264
|
const goalMap = readGoalMap(projectRoot);
|
|
@@ -6224,7 +6269,7 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6224
6269
|
text,
|
|
6225
6270
|
status: 'new',
|
|
6226
6271
|
createdTs,
|
|
6227
|
-
effort:
|
|
6272
|
+
effort: eff.effort,
|
|
6228
6273
|
goalId: align.goalId,
|
|
6229
6274
|
goalAlignment: align.goalAlignment,
|
|
6230
6275
|
relatedIds: [...verdict.relatedIds],
|
|
@@ -6234,19 +6279,39 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6234
6279
|
const proposal = options.get('proposal');
|
|
6235
6280
|
if (proposal !== undefined) rec.proposal = proposal; // agent prose ONLY — the CLI never fabricates it
|
|
6236
6281
|
if (dryRun) {
|
|
6237
|
-
if (json) write(JSON.stringify({ action: verdict.action, dryRun: true, idea: rec, exitCode: 0 }, null, 2));
|
|
6238
|
-
else
|
|
6282
|
+
if (json) write(JSON.stringify({ action: verdict.action, dryRun: true, idea: rec, ...(topMatch !== undefined ? { topMatch } : {}), ...(eff.note !== undefined ? { effortNote: eff.note } : {}), exitCode: 0 }, null, 2));
|
|
6283
|
+
else {
|
|
6284
|
+
write(`dz backlog (dry-run): ${verdict.action.toUpperCase()} — would create ${rec.id}; align ${rec.goalAlignment.toFixed(3)}${rec.goalId !== null ? ` → ${rec.goalId}` : ''}`);
|
|
6285
|
+
// The calibration surface belongs on the dry-run too (QE LOW-8) — a dry-run is exactly where a
|
|
6286
|
+
// user checks whether a near-duplicate should have crossed the band.
|
|
6287
|
+
if (topMatch !== undefined) write(` top match ${topMatch.id} @ cosine ${topMatch.cosine.toFixed(3)} (DUPLICATE band ≥ ${cfg.dedup.duplicateThreshold})`);
|
|
6288
|
+
}
|
|
6239
6289
|
return 0;
|
|
6240
6290
|
}
|
|
6291
|
+
// Privacy scaffold (idea ec4cd60d): the store we are about to create holds raw prompt-class ideas —
|
|
6292
|
+
// ensure `.dz/backlog/` is gitignored BEFORE the first write. No-op once covered.
|
|
6293
|
+
const ignore = ensureBacklogGitignored(projectRoot);
|
|
6241
6294
|
const ideas = readIdeas(projectRoot);
|
|
6242
6295
|
ideas.push(rec);
|
|
6243
6296
|
writeIdeas(projectRoot, ideas);
|
|
6244
6297
|
const mirror = await mirrorIdeaVector(projectRoot, rec); // best-effort — never blocks capture
|
|
6245
|
-
if (json) write(JSON.stringify({ action: verdict.action, idea: rec, related: verdict.relatedIds, exitCode: 0 }, null, 2));
|
|
6298
|
+
if (json) write(JSON.stringify({ action: verdict.action, idea: rec, related: verdict.relatedIds, ...(topMatch !== undefined ? { topMatch } : {}), ...(eff.note !== undefined ? { effortNote: eff.note } : {}), gitignore: ignore, exitCode: 0 }, null, 2));
|
|
6246
6299
|
else {
|
|
6247
6300
|
write(`dz backlog: ${verdict.action.toUpperCase()} — captured ${rec.id}`);
|
|
6301
|
+
if (verdict.action === 'related' && topMatch !== undefined) {
|
|
6302
|
+
write(` top match ${topMatch.id} @ cosine ${topMatch.cosine.toFixed(3)} (DUPLICATE band ≥ ${cfg.dedup.duplicateThreshold})`);
|
|
6303
|
+
}
|
|
6248
6304
|
if (rec.goalId !== null) write(` top goal: ${rec.goalId} (alignment ${rec.goalAlignment.toFixed(3)})`);
|
|
6249
6305
|
if (verdict.relatedIds.length > 0) write(` related: ${verdict.relatedIds.join(', ')}`);
|
|
6306
|
+
if (ignore.action === 'created' || ignore.action === 'appended') {
|
|
6307
|
+
write(` privacy: ${ignore.action} \`.dz/backlog/\` in ${ignore.path} (ideas are private prompt-class content)`);
|
|
6308
|
+
} else if (ignore.action === 'skipped') {
|
|
6309
|
+
// A privacy scaffold that FAILED must never be silent (QE MED-5): the store now holds raw ideas
|
|
6310
|
+
// that git can see, and only the user can fix it.
|
|
6311
|
+
write(` ⚠ privacy: could NOT gitignore \`.dz/backlog/\` (${ignore.reason ?? 'unknown error'}) — your ideas are visible to git; add \`.dz/backlog/\` to ${ignore.path} yourself`);
|
|
6312
|
+
} else if (ignore.action === 'user-opted-out') {
|
|
6313
|
+
write(' privacy: `.dz/backlog/` is explicitly un-ignored (a `!` negation in .gitignore) — respecting that; ideas WILL be tracked by git');
|
|
6314
|
+
}
|
|
6250
6315
|
write(' proposal: (write your own — dz backlog add --proposal "<text>"; the CLI never fabricates it)');
|
|
6251
6316
|
if (mirror.error !== undefined) write(` ↳ vector mirror deferred: ${mirror.error}`);
|
|
6252
6317
|
}
|
|
@@ -6282,21 +6347,42 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6282
6347
|
}
|
|
6283
6348
|
|
|
6284
6349
|
if (sub === 'goals') {
|
|
6285
|
-
const goalMap = readGoalMap(projectRoot);
|
|
6286
6350
|
if (flags.has('validate')) {
|
|
6351
|
+
// A vacuous "valid (0 goal(s))" HID the user's mistake (idea 960c9f26): the defensive reader silently
|
|
6352
|
+
// dropped every malformed entry (e.g. `text` where `statement` belongs). Validate reads the DETAILED
|
|
6353
|
+
// view, warns per dropped entry, and refuses to call a fully-dropped compass valid.
|
|
6354
|
+
const detail = readGoalMapDetailed(projectRoot);
|
|
6355
|
+
const goalMap = detail.goalMap;
|
|
6287
6356
|
const problems: string[] = [];
|
|
6357
|
+
const warnings: string[] = [];
|
|
6358
|
+
if (detail.parseError !== undefined) problems.push(detail.parseError);
|
|
6359
|
+
for (const d of detail.dropped) warnings.push(`goal[${d.index}]: ${d.reason} — dropped`);
|
|
6360
|
+
// The reader CLAMPS a bad weight before any validation could see it, so validating the clamped
|
|
6361
|
+
// value can only ever pass (QE MED-7 — the branch below was dead code). Warn from the RAW value
|
|
6362
|
+
// the user wrote, and say which value the runtime is actually using.
|
|
6363
|
+
for (const r of detail.repaired) warnings.push(`goal[${r.index}] (${r.id}): ${r.reason} — using ${r.used}`);
|
|
6288
6364
|
const seen = new Set<string>();
|
|
6289
6365
|
for (const g of goalMap.goals) {
|
|
6290
6366
|
if (seen.has(g.id)) problems.push(`duplicate goal id: ${g.id}`);
|
|
6291
6367
|
seen.add(g.id);
|
|
6292
6368
|
if (g.statement.trim() === '') problems.push(`goal ${g.id} has an empty statement`);
|
|
6369
|
+
// Defence in depth: the reader guarantees the clamp, so this can only fire if that ever regresses.
|
|
6293
6370
|
if (!(g.weight > 0 && g.weight <= 1)) problems.push(`goal ${g.id} weight ${g.weight} is out of (0,1]`);
|
|
6294
6371
|
}
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6372
|
+
// A compass whose entries were ALL dropped is a user error, not an empty compass.
|
|
6373
|
+
const allDropped = detail.present > 0 && goalMap.goals.length === 0;
|
|
6374
|
+
if (allDropped) problems.push(`all ${detail.present} goal entr${detail.present === 1 ? 'y was' : 'ies were'} dropped as malformed — the compass is EMPTY, not valid`);
|
|
6375
|
+
const valid = problems.length === 0;
|
|
6376
|
+
if (json) {
|
|
6377
|
+
write(JSON.stringify({ valid, problems, warnings, dropped: detail.dropped, repaired: detail.repaired, goals: goalMap.goals.length, present: detail.present, exitCode: valid ? 0 : 1 }, null, 2));
|
|
6378
|
+
} else {
|
|
6379
|
+
for (const w of warnings) write(`dz backlog goals: WARNING ${w}`);
|
|
6380
|
+
if (valid) write(`dz backlog goals: valid (${goalMap.goals.length} goal(s)${detail.dropped.length > 0 ? `, ${detail.dropped.length} dropped` : ''})`);
|
|
6381
|
+
else { write('dz backlog goals: INVALID'); for (const p of problems) write(` - ${p}`); }
|
|
6382
|
+
}
|
|
6383
|
+
return valid ? 0 : 1;
|
|
6299
6384
|
}
|
|
6385
|
+
const goalMap = readGoalMap(projectRoot);
|
|
6300
6386
|
if (json) {
|
|
6301
6387
|
write(JSON.stringify({ goals: goalMap.goals, exitCode: 0 }, null, 2));
|
|
6302
6388
|
return 0;
|
|
@@ -6313,7 +6399,37 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6313
6399
|
if (sub === 'roulette') {
|
|
6314
6400
|
const ideas = readIdeas(projectRoot);
|
|
6315
6401
|
const pool = eligibleIdeas(ideas);
|
|
6316
|
-
|
|
6402
|
+
// ── Commit-argument validation runs BEFORE any early return (QE HIGH-2): an empty eligible pool
|
|
6403
|
+
// used to return 0 first, so `--commit <garbage>` reported success having validated nothing. A
|
|
6404
|
+
// mutation request is answered on its own merits, whatever the pool looks like.
|
|
6405
|
+
// parseArgs turns `--commit <id>` into an OPTION and a bare `--commit` into a FLAG. A trailing
|
|
6406
|
+
// positional id is only a commit target when --commit was actually passed (QE HIGH-1): a bare
|
|
6407
|
+
// `dz backlog roulette <id>` must never mutate.
|
|
6408
|
+
const positionalId = options.get('_positional_1');
|
|
6409
|
+
const commitOpt = options.get('commit');
|
|
6410
|
+
const wantsCommit = flags.has('commit') || commitOpt !== undefined;
|
|
6411
|
+
if (!wantsCommit && positionalId !== undefined) {
|
|
6412
|
+
return emitErr(`an idea id alone does not start it — a spin never mutates. Use: dz backlog roulette --commit ${positionalId}`);
|
|
6413
|
+
}
|
|
6414
|
+
const commitId = wantsCommit ? commitOpt ?? positionalId : undefined;
|
|
6415
|
+
if (wantsCommit && commitId !== undefined) {
|
|
6416
|
+
if (!isSafeId(commitId)) return emitErr(`refusing an unsafe idea id: ${JSON.stringify(commitId)}`);
|
|
6417
|
+
const target = ideas.find((i) => i.id === commitId);
|
|
6418
|
+
if (target === undefined) return emitErr(`no idea ${commitId} — run dz backlog list to see the ids`);
|
|
6419
|
+
// TERMINAL ideas must not be silently resurrected (Codex re-QE). in-progress is allowed —
|
|
6420
|
+
// re-committing it is an idempotent no-op, not a resurrection.
|
|
6421
|
+
if (target.status === 'shipped' || target.status === 'dropped') {
|
|
6422
|
+
return emitErr(`idea ${commitId} is ${target.status} — a terminal idea cannot be (re)started`);
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
// `--pick N` is a read-only ranking view; combining it with a mutation request is ambiguous and was
|
|
6426
|
+
// silently ignoring the commit (Codex re-QE) — refuse the combination explicitly.
|
|
6427
|
+
if (wantsCommit && options.get('pick') !== undefined) {
|
|
6428
|
+
return emitErr('--pick cannot be combined with --commit — commit a single explicit id without --pick');
|
|
6429
|
+
}
|
|
6430
|
+
if (pool.length === 0 && commitId === undefined) {
|
|
6431
|
+
// A bare `--commit` with nothing to spin cannot name a pick to paste, so it says exactly that.
|
|
6432
|
+
if (wantsCommit) return emitErr('roulette --commit requires an idea id, and there is no eligible idea to suggest one — run dz backlog list');
|
|
6317
6433
|
write(json ? JSON.stringify({ pick: null, eligible: 0, exitCode: 0 }) : 'dz backlog roulette: no eligible ideas (status new/enriched)');
|
|
6318
6434
|
return 0;
|
|
6319
6435
|
}
|
|
@@ -6328,18 +6444,31 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6328
6444
|
}
|
|
6329
6445
|
const seedOpt = options.get('seed');
|
|
6330
6446
|
const seed = seedOpt !== undefined && Number.isFinite(Number(seedOpt)) ? Number(seedOpt) : Math.floor(Math.random() * 2 ** 31);
|
|
6331
|
-
|
|
6447
|
+
// With an empty pool but a valid --commit id, there is nothing to spin — the commit still stands.
|
|
6448
|
+
const spun = pool.length > 0 ? spinRoulette(ideas, cfg.roulette, seededRng(seed), now) : undefined;
|
|
6449
|
+
if (wantsCommit && commitId === undefined) {
|
|
6450
|
+
// --commit takes an EXPLICIT id (idea 5fcd9a2d): the spin is a function of the CURRENT pool, so a
|
|
6451
|
+
// bare `--commit` after the pool changed could start a DIFFERENT idea than the dry-run showed. The
|
|
6452
|
+
// id the user saw is the id that gets committed — the spin no longer decides a mutation.
|
|
6453
|
+
const msg = `roulette --commit requires the idea id you are committing to: dz backlog roulette --commit ${spun!.id} (this spin's pick) — a bare --commit could start a different idea than the one you were shown`;
|
|
6454
|
+
if (json) write(JSON.stringify({ error: msg, pickId: spun!.id, exitCode: 1 }, null, 2));
|
|
6455
|
+
else write(`dz backlog: ${msg}`);
|
|
6456
|
+
return 1;
|
|
6457
|
+
}
|
|
6458
|
+
let pick = spun;
|
|
6332
6459
|
let committed = false;
|
|
6333
|
-
if (
|
|
6334
|
-
|
|
6460
|
+
if (commitId !== undefined) {
|
|
6461
|
+
// Validated above (safe id + known id) BEFORE any early return.
|
|
6462
|
+
const idx = ideas.findIndex((i) => i.id === commitId);
|
|
6335
6463
|
ideas[idx]!.status = 'in-progress';
|
|
6336
6464
|
writeIdeas(projectRoot, ideas);
|
|
6465
|
+
pick = ideas[idx]!;
|
|
6337
6466
|
committed = true;
|
|
6338
6467
|
}
|
|
6339
|
-
if (json) write(JSON.stringify({ pick, seed, committed, exitCode: 0 }, null, 2));
|
|
6340
|
-
else {
|
|
6468
|
+
if (json) write(JSON.stringify({ pick: pick ?? null, seed, committed, exitCode: 0 }, null, 2));
|
|
6469
|
+
else if (pick !== undefined) {
|
|
6341
6470
|
write(`dz backlog roulette (seed ${seed}): ${pick.id} — ${pick.text.slice(0, 70)}`);
|
|
6342
|
-
write(` align ${pick.goalAlignment.toFixed(3)}${pick.goalId !== null ? ` (${pick.goalId})` : ''} · effort ${pick.effort}${committed ? ' · → in-progress' :
|
|
6471
|
+
write(` align ${pick.goalAlignment.toFixed(3)}${pick.goalId !== null ? ` (${pick.goalId})` : ''} · effort ${pick.effort}${committed ? ' · → in-progress' : ` · (start it with: dz backlog roulette --commit ${pick.id})`}`);
|
|
6343
6472
|
}
|
|
6344
6473
|
return 0;
|
|
6345
6474
|
}
|
|
@@ -6408,7 +6537,7 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
6408
6537
|
write(' list [--status <s>][--goal <id>] list ideas');
|
|
6409
6538
|
write(' show <id> full record');
|
|
6410
6539
|
write(' goals [--validate] the compass (.dz/backlog/goals.json)');
|
|
6411
|
-
write(' roulette [--pick N][--seed n][--commit]
|
|
6540
|
+
write(' roulette [--pick N][--seed n][--commit <id>] weighted draw; --commit takes the id you saw');
|
|
6412
6541
|
write(' enrich <id> stage the idea2prd hand-off (agent expands)');
|
|
6413
6542
|
write(` jira <id> draft a Jira issue (adapter: ${[...BACKLOG_BACKENDS].join('|')})`);
|
|
6414
6543
|
write(' harmonize [--apply][--threshold 0-1] batch semantic dedup of the backlog');
|