@dzhechkov/harness-cli 0.4.1 → 0.4.3
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 +17 -9
- package/README.md +114 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +385 -10
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +28 -8
- package/src/cli.ts +343 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "532051106b153c0d9bb7ad2abbeb71d418f88f1bc88e60a35e29cf42162af243"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"hashes": [
|
|
96
96
|
{
|
|
97
97
|
"alg": "SHA-256",
|
|
98
|
-
"content": "
|
|
98
|
+
"content": "5bdc153e789ac180543e9b4955ea36666dc27b7238637b05875e048fc49ae30f"
|
|
99
99
|
}
|
|
100
100
|
]
|
|
101
101
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "0183c01a98e232554a7460be48ee5fdf59dd3a7d09c719a31b08acc9c6b3528c"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hashes": [
|
|
116
116
|
{
|
|
117
117
|
"alg": "SHA-256",
|
|
118
|
-
"content": "
|
|
118
|
+
"content": "c5bd1b0b852dd21de328494d7a99db336afbde4397b7c687d416f1a6f3c297b2"
|
|
119
119
|
}
|
|
120
120
|
]
|
|
121
121
|
},
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"hashes": [
|
|
176
176
|
{
|
|
177
177
|
"alg": "SHA-256",
|
|
178
|
-
"content": "
|
|
178
|
+
"content": "4a164e5ac53ceec178463c1bdab9f80c1755311d5e4202c1f6234ca22c473b37"
|
|
179
179
|
}
|
|
180
180
|
]
|
|
181
181
|
},
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"hashes": [
|
|
196
196
|
{
|
|
197
197
|
"alg": "SHA-256",
|
|
198
|
-
"content": "
|
|
198
|
+
"content": "0a1097fc3a2294f934fc50f5871b287cfbc5ec85becdab914b7caeca42cb7d1a"
|
|
199
199
|
}
|
|
200
200
|
]
|
|
201
201
|
},
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"hashes": [
|
|
216
216
|
{
|
|
217
217
|
"alg": "SHA-256",
|
|
218
|
-
"content": "
|
|
218
|
+
"content": "c66bdd2fbbe2e0b622e3bc4e0e4521e865cc8088e581bd4b4862c1c550307900"
|
|
219
219
|
}
|
|
220
220
|
]
|
|
221
221
|
},
|
|
@@ -475,7 +475,27 @@
|
|
|
475
475
|
"hashes": [
|
|
476
476
|
{
|
|
477
477
|
"alg": "SHA-256",
|
|
478
|
-
"content": "
|
|
478
|
+
"content": "fe8e5792626cf58678c1e2bcb809f63413655c2e171439157b74deb8b4fd80fb"
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"type": "file",
|
|
484
|
+
"name": "test/statusline-panel.test.ts",
|
|
485
|
+
"hashes": [
|
|
486
|
+
{
|
|
487
|
+
"alg": "SHA-256",
|
|
488
|
+
"content": "b09b42c4ece1a83e5d3f3765629b0756a8f30a7122f3fc30363f93f092a44c0d"
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"type": "file",
|
|
494
|
+
"name": "test/trace-bundle-cli.test.ts",
|
|
495
|
+
"hashes": [
|
|
496
|
+
{
|
|
497
|
+
"alg": "SHA-256",
|
|
498
|
+
"content": "fdaad63359ca7ce351e505e613b547bbc653e0847403edfae88d22a8a524b7a0"
|
|
479
499
|
}
|
|
480
500
|
]
|
|
481
501
|
},
|
package/src/cli.ts
CHANGED
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
deriveUsageCalibration,
|
|
106
106
|
normalizeClaudeUsageModelKey,
|
|
107
107
|
readUsageLimits,
|
|
108
|
+
parseWeeklyResetAnchor,
|
|
108
109
|
claimCheck,
|
|
109
110
|
summarize,
|
|
110
111
|
queryBookKnowledge,
|
|
@@ -222,6 +223,10 @@ import {
|
|
|
222
223
|
selectAutoCost,
|
|
223
224
|
recordProvisional,
|
|
224
225
|
finalizeOutcome,
|
|
226
|
+
harvestStageOutcomes,
|
|
227
|
+
recommendModels,
|
|
228
|
+
planFeed,
|
|
229
|
+
GRADE_SUCCESS_FLOOR,
|
|
225
230
|
COST_LADDER,
|
|
226
231
|
splitScenarios,
|
|
227
232
|
budgetPlan,
|
|
@@ -297,6 +302,8 @@ import {
|
|
|
297
302
|
makeBacklogIO,
|
|
298
303
|
harmonizeBacklog,
|
|
299
304
|
transitionIdeas,
|
|
305
|
+
editIdea,
|
|
306
|
+
clearEmbedStale,
|
|
300
307
|
BACKLOG_BACKENDS,
|
|
301
308
|
applyDomainBoost,
|
|
302
309
|
DZ_OWNED_TASK_TYPES,
|
|
@@ -326,6 +333,13 @@ import {
|
|
|
326
333
|
mutationGateExitCode,
|
|
327
334
|
summarizeMutationResults,
|
|
328
335
|
renderMutationReport,
|
|
336
|
+
TRACE_BUNDLE_LEDGER_PATH,
|
|
337
|
+
TRACE_BUNDLE_SCHEMA,
|
|
338
|
+
TRACE_BUNDLE_RUN_META_FILE,
|
|
339
|
+
buildBundle,
|
|
340
|
+
serializeBundle,
|
|
341
|
+
parseBundle,
|
|
342
|
+
planImport,
|
|
329
343
|
} from '@dzhechkov/harness-core';
|
|
330
344
|
import type { MutationEntryResult, MutationObservation, MutationRegistryEntry } from '@dzhechkov/harness-core';
|
|
331
345
|
import type { ReqeDebt } from '@dzhechkov/harness-core';
|
|
@@ -355,6 +369,8 @@ Usage:
|
|
|
355
369
|
dz workflow blobs [--check] (list/self-check the subsystem blob registry)
|
|
356
370
|
dz workflow-lint <script.js> [--plan <plan.json>] [--require-plan|--legacy] [--json] (layer-1 gate; exit 0/1/3 — inconclusive is never a pass)
|
|
357
371
|
dz workflow-trace <runDir|--slug <s>|--run <id>> [--invariants <plan.json>] [--html <out.html>] [--json] (timeline + SEQ invariant runner)
|
|
372
|
+
dz workflow-trace export <run> --o <file> [--include-pairs --yes] [--strict] (one run's telemetry as ONE movable file: events, not aggregates; degradation is typed and LOUD, --strict fails closed)
|
|
373
|
+
dz workflow-trace import <bundle> --into <root> [--force] [--with-pairs] (reconstruct that run under an explicit root; FAIL-CLOSED — never writes over a run that already has content)
|
|
358
374
|
dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
|
|
359
375
|
dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
|
|
360
376
|
dz doctor [--project <dir>] [--pubkey <path>] [--require-signing] (health + signature check of installed packs)
|
|
@@ -381,6 +397,8 @@ Usage:
|
|
|
381
397
|
dz backlog roulette [--pick <N>] [--seed <n>] [--commit] [--project <dir>] [--json] (WEIGHTED draw over eligible ideas: alignment^alpha * recencyDecay * 1/effort, seeded; --pick N = ranked shortlist; --commit flips the pick to in-progress)
|
|
382
398
|
dz backlog ship <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (mark work DONE: new|enriched|in-progress → shipped, removing it from the roulette pool — run it after finishing a task; short id prefixes ok, ambiguous = loud error)
|
|
383
399
|
dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
|
|
400
|
+
dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run] [--project <dir>] [--json] (rewrite ONE idea's text, preserving every other field; re-embeds the dedup vector, and on a failed re-embed MARKS the record embedStale so dedup refuses to trust it — previous text preserved in .dz/backlog/edits.jsonl)
|
|
401
|
+
dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis: n runs, window, the grade-floor rule, skip reasons; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
|
|
384
402
|
dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
|
|
385
403
|
dz backlog enrich <id> [--project <dir>] [--json] (stage the idea2prd input scaffold in features/<slug>/ and hand off to the idea2prd-manual skill — the CLI never fabricates a PRD)
|
|
386
404
|
dz backlog jira <id> [--project <dir>] [--json] (draft a Jira issue via the configurable adapter (backlog.jira.adapter: jira-mcp|copilot-mcp|none); none writes an auditable jira-outbox/<id>.json stub)
|
|
@@ -406,7 +424,7 @@ Usage:
|
|
|
406
424
|
dz brain expand <kuId> [--source <slug>] [--json] (full-content lookup for a citation kuId; --json emits the full KU object)
|
|
407
425
|
dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
|
|
408
426
|
dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
|
|
409
|
-
dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
|
|
427
|
+
dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
|
|
410
428
|
dz usage [--json] [--project <dir>] | dz usage --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] [--project <dir>] (ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS; pct=null when limits unconfigured)
|
|
411
429
|
dz usage --by-stage [--run <runId> | --slug <slug>] [--epsilon <0..1>] [--write <file.jsonl>] [--json] (per-stage cost ledger for ONE feature-adr run + the reconciliation invariant: accounted + unaccounted = run total; verdict BALANCED | DEFECT | INSUFFICIENT_DATA; local transcript ESTIMATES — catches ATTRIBUTION errors, not pricing errors)
|
|
412
430
|
dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] (enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = root README.md + every discovered package's README.md + features/*/08_qe_report.md + docs/**/*.md (historical feature artifacts are NOT scanned — pass paths explicitly); exit 1 only at/above --fail-on, default high)
|
|
@@ -1143,7 +1161,215 @@ function cmdWorkflowLint(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
1143
1161
|
/** `dz workflow-trace` — timeline + invariant runner over a run's trace.jsonl. Scope is CAPPED
|
|
1144
1162
|
* (AM-8): <runDir|--slug|--run>, --invariants, --html, --json. NO watch/filter/compare/search/
|
|
1145
1163
|
* retention/access-control — adding one needs an ADR amendment (the surface test pins this). */
|
|
1164
|
+
/** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
|
|
1165
|
+
function traceBundleToolVersion(): string {
|
|
1166
|
+
try {
|
|
1167
|
+
const req = createRequire(import.meta.url);
|
|
1168
|
+
const pkg = req('../package.json') as { version?: unknown };
|
|
1169
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
1170
|
+
} catch { return 'unknown'; }
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** Resolve a run the SAME three ways the timeline reader does — positional, --slug, --run — so a
|
|
1174
|
+
* bundle can never address a run by a scheme the rest of the command does not understand. */
|
|
1175
|
+
function resolveTraceRun(options: Map<string, string>, cwd: string, positional: string | undefined):
|
|
1176
|
+
{ runDir: string; slug: string | null; runId: string | null } | null {
|
|
1177
|
+
const slug = options.get('slug');
|
|
1178
|
+
const runId = options.get('run');
|
|
1179
|
+
if (positional !== undefined && positional !== '') {
|
|
1180
|
+
return { runDir: resolve(cwd, positional), slug: slug ?? null, runId: runId ?? null };
|
|
1181
|
+
}
|
|
1182
|
+
if (slug !== undefined) return { runDir: resolve(cwd, 'features', slug), slug, runId: runId ?? null };
|
|
1183
|
+
if (runId !== undefined) return { runDir: resolve(cwd, '.dz', 'loop-trace', runId), slug: slug ?? null, runId };
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/** Every harness workflow record under the project, already parsed. An unparseable file is SKIPPED
|
|
1188
|
+
* here rather than guessed at — the pure half answers `unreadable` from what it is handed. */
|
|
1189
|
+
function readHarnessRecords(cwd: string): unknown[] {
|
|
1190
|
+
const out: unknown[] = [];
|
|
1191
|
+
const base = join(cwd, 'roam', 'claude-state');
|
|
1192
|
+
if (!existsSync(base)) return out;
|
|
1193
|
+
let sessions: string[] = [];
|
|
1194
|
+
try { sessions = readdirSync(base); } catch { return out; }
|
|
1195
|
+
for (const session of sessions) {
|
|
1196
|
+
const dir = join(base, session, 'workflows');
|
|
1197
|
+
if (!existsSync(dir)) continue;
|
|
1198
|
+
let names: string[] = [];
|
|
1199
|
+
try { names = readdirSync(dir); } catch { continue; }
|
|
1200
|
+
for (const name of names) {
|
|
1201
|
+
if (!name.endsWith('.json')) continue;
|
|
1202
|
+
try { out.push(JSON.parse(readFileSync(join(dir, name), 'utf-8'))); } catch { /* skipped, never guessed */ }
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return out;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** `dz workflow-trace export` — one run's telemetry as ONE movable file. */
|
|
1209
|
+
function cmdWorkflowTraceExport(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
1210
|
+
const out = options.get('o') ?? options.get('out');
|
|
1211
|
+
const run = resolveTraceRun(options, cwd, options.get('_positional_1'));
|
|
1212
|
+
if (run === null || out === undefined || out === '') {
|
|
1213
|
+
write('dz workflow-trace export: usage — dz workflow-trace export <runDir|--slug <s>|--run <id>> --o <file> [--include-pairs --yes] [--strict]');
|
|
1214
|
+
return 1;
|
|
1215
|
+
}
|
|
1216
|
+
const wantPairs = flags.has('include-pairs');
|
|
1217
|
+
if (wantPairs && !flags.has('yes')) {
|
|
1218
|
+
write('dz workflow-trace export: --include-pairs needs --yes — training pairs may carry TARGET-REPO CODE and full prompts, so shipping them off this machine is a second, explicit decision');
|
|
1219
|
+
return 1;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const readSlot = (abs: string, origin: string) =>
|
|
1223
|
+
existsSync(abs)
|
|
1224
|
+
? { present: true as const, member: { origin, content: readFileSync(abs, 'utf-8') } }
|
|
1225
|
+
: { present: false as const, reason: `absent on disk at ${origin}` };
|
|
1226
|
+
|
|
1227
|
+
const trace = readSlot(join(run.runDir, 'trace.jsonl'), 'trace.jsonl');
|
|
1228
|
+
const checkpoints = readSlot(join(run.runDir, '.fa-state', 'checkpoints.jsonl'), '.fa-state/checkpoints.jsonl');
|
|
1229
|
+
|
|
1230
|
+
const ledgerAbs = join(cwd, TRACE_BUNDLE_LEDGER_PATH);
|
|
1231
|
+
// An empty trailing line is not a malformed row — hand the pure half only real lines, or the
|
|
1232
|
+
// bundle reports a phantom parse failure on every well-formed ledger (MEASURED on first run).
|
|
1233
|
+
const ledgerLines = existsSync(ledgerAbs)
|
|
1234
|
+
? readFileSync(ledgerAbs, 'utf-8').split('\n').filter((line) => line.trim() !== '')
|
|
1235
|
+
: null;
|
|
1236
|
+
|
|
1237
|
+
const pairFiles: { origin: string; content: string }[] = [];
|
|
1238
|
+
if (wantPairs && run.slug !== null) {
|
|
1239
|
+
const pairDir = join(cwd, '.dz', 'fa-training', run.slug);
|
|
1240
|
+
if (existsSync(pairDir)) {
|
|
1241
|
+
for (const name of readdirSync(pairDir)) {
|
|
1242
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
1243
|
+
pairFiles.push({ origin: `.dz/fa-training/${run.slug}/${name}`, content: readFileSync(join(pairDir, name), 'utf-8') });
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const bundle = buildBundle({
|
|
1249
|
+
sourceRoot: cwd,
|
|
1250
|
+
runAddress: options.get('_positional_1') ?? (run.slug !== null ? `--slug ${run.slug}` : `--run ${String(run.runId)}`),
|
|
1251
|
+
slug: run.slug,
|
|
1252
|
+
runId: run.runId,
|
|
1253
|
+
toolVersion: traceBundleToolVersion(),
|
|
1254
|
+
createdAt: null,
|
|
1255
|
+
trace,
|
|
1256
|
+
checkpoints,
|
|
1257
|
+
ledgerLines,
|
|
1258
|
+
...(ledgerLines === null ? { ledgerReason: `no ledger at ${TRACE_BUNDLE_LEDGER_PATH}` } : {}),
|
|
1259
|
+
includePairs: wantPairs,
|
|
1260
|
+
pairFiles: wantPairs ? pairFiles : null,
|
|
1261
|
+
records: readHarnessRecords(cwd),
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
try {
|
|
1265
|
+
mkdirSync(dirname(resolve(cwd, out)), { recursive: true });
|
|
1266
|
+
writeFileSync(resolve(cwd, out), serializeBundle(bundle));
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
write(`dz workflow-trace export: could not write ${out} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1269
|
+
return 1;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Degradation is NAMED, never silent — and exactly one reason is actionable.
|
|
1273
|
+
let degraded = 0;
|
|
1274
|
+
if (!bundle.trace.present) { degraded++; write(`dz workflow-trace export: trace absent — ${bundle.trace.reason}`); }
|
|
1275
|
+
if (!bundle.checkpoints.present) { degraded++; write(`dz workflow-trace export: checkpoints absent — ${bundle.checkpoints.reason}`); }
|
|
1276
|
+
if (!bundle.ledger.present) { degraded++; write(`dz workflow-trace export: ledger absent — ${String(bundle.ledger.reason)}`); }
|
|
1277
|
+
if (!bundle.runMeta.resolved) {
|
|
1278
|
+
degraded++;
|
|
1279
|
+
const reason = bundle.runMeta.reason;
|
|
1280
|
+
write(reason === 'layout-unrecognised'
|
|
1281
|
+
? 'dz workflow-trace export: runMeta UNRESOLVED (layout-unrecognised) — the harness workflow-record layout CHANGED; update the reader in harness-core/src/trace-bundle.ts. This is the one reason that needs action'
|
|
1282
|
+
: `dz workflow-trace export: runMeta unresolved (${reason}) — no action needed, this is history or absence, not a layout change`);
|
|
1283
|
+
}
|
|
1284
|
+
write(`dz workflow-trace export: wrote ${out} (${bundle.schema}; ledger rows scanned ${bundle.ledger.scanned}, matched ${bundle.ledger.matched}, malformed ${bundle.ledger.malformed})`);
|
|
1285
|
+
if (degraded > 0 && flags.has('strict')) {
|
|
1286
|
+
write(`dz workflow-trace export: --strict — ${degraded} member(s) degraded, failing closed`);
|
|
1287
|
+
return 1;
|
|
1288
|
+
}
|
|
1289
|
+
return 0;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** `dz workflow-trace import` — reconstruct a run under an explicit root. FAIL-CLOSED. */
|
|
1293
|
+
function cmdWorkflowTraceImport(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
1294
|
+
const file = options.get('_positional_1');
|
|
1295
|
+
const into = options.get('into');
|
|
1296
|
+
if (file === undefined || file === '' || into === undefined || into === '') {
|
|
1297
|
+
write('dz workflow-trace import: usage — dz workflow-trace import <bundle.json> --into <root> [--force] [--with-pairs]');
|
|
1298
|
+
return 1;
|
|
1299
|
+
}
|
|
1300
|
+
const abs = resolve(cwd, file);
|
|
1301
|
+
if (!existsSync(abs)) { write(`dz workflow-trace import: no such bundle: ${file}`); return 1; }
|
|
1302
|
+
|
|
1303
|
+
const parsed = parseBundle(readFileSync(abs, 'utf-8'));
|
|
1304
|
+
if (!parsed.ok) {
|
|
1305
|
+
write(parsed.reason === 'unknown-schema'
|
|
1306
|
+
? `dz workflow-trace import: REFUSED — unknown bundle schema "${parsed.found}"; this dz reads ${TRACE_BUNDLE_SCHEMA}. A future bundle is never best-effort parsed`
|
|
1307
|
+
: parsed.reason === 'member-shape'
|
|
1308
|
+
? `dz workflow-trace import: REFUSED — member "${parsed.member}" fails its shape check; a partial import is never left behind`
|
|
1309
|
+
: 'dz workflow-trace import: REFUSED — the bundle is not parseable JSON');
|
|
1310
|
+
return 1;
|
|
1311
|
+
}
|
|
1312
|
+
const bundle = parsed.bundle;
|
|
1313
|
+
|
|
1314
|
+
const root = resolve(cwd, into);
|
|
1315
|
+
const runDirRel = bundle.provenance.slug !== null ? join('features', bundle.provenance.slug) : join('.dz', 'loop-trace', String(bundle.provenance.runId ?? 'unknown-run'));
|
|
1316
|
+
const runDirAbs = join(root, runDirRel);
|
|
1317
|
+
let runDirHasContent = false;
|
|
1318
|
+
try { runDirHasContent = existsSync(runDirAbs) && readdirSync(runDirAbs).length > 0; } catch { runDirHasContent = false; }
|
|
1319
|
+
|
|
1320
|
+
let runIdentity: { slug: string | null; runId: string | null } | null = null;
|
|
1321
|
+
const metaAbs = join(runDirAbs, TRACE_BUNDLE_RUN_META_FILE);
|
|
1322
|
+
if (existsSync(metaAbs)) {
|
|
1323
|
+
try {
|
|
1324
|
+
const meta = JSON.parse(readFileSync(metaAbs, 'utf-8')) as { slug?: unknown; runId?: unknown };
|
|
1325
|
+
runIdentity = {
|
|
1326
|
+
slug: typeof meta.slug === 'string' ? meta.slug : null,
|
|
1327
|
+
runId: typeof meta.runId === 'string' ? meta.runId : null,
|
|
1328
|
+
};
|
|
1329
|
+
} catch { runIdentity = null; }
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const plan = planImport(bundle, {
|
|
1333
|
+
runDir: runDirRel,
|
|
1334
|
+
existingPaths: [],
|
|
1335
|
+
runDirHasContent,
|
|
1336
|
+
runIdentity,
|
|
1337
|
+
force: flags.has('force'),
|
|
1338
|
+
withPairs: flags.has('with-pairs'),
|
|
1339
|
+
bundleName: basename(abs),
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
if (!plan.ok) {
|
|
1343
|
+
for (const refusal of plan.refusals) write(`dz workflow-trace import: REFUSED ${refusal.path} — ${refusal.reason}`);
|
|
1344
|
+
write('dz workflow-trace import: nothing was written');
|
|
1345
|
+
return 1;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
let written = 0;
|
|
1349
|
+
const failures: string[] = [];
|
|
1350
|
+
for (const entry of plan.writes) {
|
|
1351
|
+
const target = join(root, entry.path);
|
|
1352
|
+
try {
|
|
1353
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
1354
|
+
writeFileSync(target, entry.content);
|
|
1355
|
+
written++;
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
failures.push(`${entry.path} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
// Report what ACTUALLY landed, computed from the writes, not from the plan.
|
|
1361
|
+
for (const skipped of plan.refusals) write(`dz workflow-trace import: skipped ${skipped.path} — ${skipped.reason}`);
|
|
1362
|
+
for (const failure of failures) write(`dz workflow-trace import: FAILED ${failure}`);
|
|
1363
|
+
write(`dz workflow-trace import: wrote ${written} of ${plan.writes.length} member(s) under ${into}`);
|
|
1364
|
+
return failures.length > 0 ? 1 : 0;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1146
1367
|
function cmdWorkflowTrace(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
1368
|
+
// Two subcommands ride the SAME run addressing as the timeline reader (AM-8 keeps the surface
|
|
1369
|
+
// capped; these are bundle transport, not new query verbs).
|
|
1370
|
+
const sub = options.get('_positional_0');
|
|
1371
|
+
if (sub === 'export') return cmdWorkflowTraceExport(options, flags, cwd, write);
|
|
1372
|
+
if (sub === 'import') return cmdWorkflowTraceImport(options, flags, cwd, write);
|
|
1147
1373
|
const runDirArg = options.get('_positional_0');
|
|
1148
1374
|
const slug = options.get('slug');
|
|
1149
1375
|
const runId = options.get('run');
|
|
@@ -1621,11 +1847,13 @@ function cmdStatuslineInstall(options: Map<string, string>, cwd: string, write:
|
|
|
1621
1847
|
/**
|
|
1622
1848
|
* `dz statusline --fa-record` — the entry point the `/feature-adr` pipeline calls at Steps 0/8/9 to
|
|
1623
1849
|
* record its LIVE learning state (Pattern memory loop: pool / recalled / stored). Unlike the render
|
|
1624
|
-
* path this WRITES `.dz/feature-adr/learning-state
|
|
1625
|
-
* `writeFeatureAdrState`, which itself computes `pool` from the learned-pattern count).
|
|
1850
|
+
* path this WRITES a per-slug `.dz/feature-adr/learning-state/*.json` slot (via harness-core's
|
|
1851
|
+
* best-effort `writeFeatureAdrState`, which itself computes `pool` from the learned-pattern count).
|
|
1626
1852
|
*
|
|
1627
1853
|
* GUARD (feature-adr flag discipline): `--slug` and `--step` are required; a missing one exits 1
|
|
1628
1854
|
* with a copy-paste example. `--recalled`/`--stored` default to 0 and reject non-numeric input.
|
|
1855
|
+
* `--kind <feature-adr|loop>` identifies the producer, defaults to `feature-adr`, and rejects any
|
|
1856
|
+
* other value rather than silently weakening panel arbitration.
|
|
1629
1857
|
*/
|
|
1630
1858
|
function cmdStatuslineFaRecord(options: Map<string, string>, cwd: string, write: Write): number {
|
|
1631
1859
|
const slug = (options.get('slug') ?? '').trim();
|
|
@@ -1655,10 +1883,17 @@ function cmdStatuslineFaRecord(options: Map<string, string>, cwd: string, write:
|
|
|
1655
1883
|
const reinforced = parseCount('reinforced');
|
|
1656
1884
|
if (reinforced === undefined) return 1;
|
|
1657
1885
|
|
|
1886
|
+
const kindRaw = options.get('kind') ?? 'feature-adr';
|
|
1887
|
+
if (kindRaw !== 'feature-adr' && kindRaw !== 'loop') {
|
|
1888
|
+
write(`dz statusline --fa-record: --kind must be feature-adr or loop (got "${kindRaw}")`);
|
|
1889
|
+
write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 0" --kind feature-adr --recalled 5 --stored 2');
|
|
1890
|
+
return 1;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1658
1893
|
const mode = options.get('mode');
|
|
1659
1894
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1660
1895
|
const state = writeFeatureAdrState(projectRoot, {
|
|
1661
|
-
slug, step, recalled, stored,
|
|
1896
|
+
kind: kindRaw, slug, step, recalled, stored,
|
|
1662
1897
|
...(reinforced > 0 ? { reinforced } : {}),
|
|
1663
1898
|
...(mode !== undefined && mode.trim() !== '' ? { mode: mode.trim() } : {}),
|
|
1664
1899
|
});
|
|
@@ -1709,7 +1944,12 @@ function cmdStatusline(
|
|
|
1709
1944
|
// Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
|
|
1710
1945
|
const fa = data.featureAdr;
|
|
1711
1946
|
if (fa !== undefined) {
|
|
1712
|
-
|
|
1947
|
+
// The producer marker reached data and arbitration in a previous round but not this label, so the bar asserted a pipeline that was not running.
|
|
1948
|
+
if (fa.kind === 'loop') {
|
|
1949
|
+
line = `🔁 loop ${fa.step} · ${line}`;
|
|
1950
|
+
} else {
|
|
1951
|
+
line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
1952
|
+
}
|
|
1713
1953
|
}
|
|
1714
1954
|
|
|
1715
1955
|
write(line);
|
|
@@ -2040,7 +2280,16 @@ function cmdUsage(
|
|
|
2040
2280
|
const s = u.sessionPct === null ? 'n/a' : '~' + u.sessionPct + '%';
|
|
2041
2281
|
const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
|
|
2042
2282
|
const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
|
|
2043
|
-
|
|
2283
|
+
// The weekly reset is WEEKLY: print the anchor verbatim (weekday + offset), not a bare clock
|
|
2284
|
+
// time — 'resets 08:59' reads as daily and hides the weekday (idea c8513be9: the bare form
|
|
2285
|
+
// misread a Monday reading as '41 minutes after the boundary' when the boundary was Wednesday's).
|
|
2286
|
+
const weeklyAnchorLabel = typeof lim.weeklyResetAnchor === 'string' && lim.weeklyResetAnchor !== ''
|
|
2287
|
+
? lim.weeklyResetAnchor
|
|
2288
|
+
: clock(u.weeklyResetsAt);
|
|
2289
|
+
write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + weeklyAnchorLabel + ') · estimated');
|
|
2290
|
+
if (typeof lim.weeklyResetAnchor === 'string' && parseWeeklyResetAnchor(lim.weeklyResetAnchor)?.offsetMinutes === undefined) {
|
|
2291
|
+
write(' ⚠ weeklyResetAnchor has NO utc offset — the boundary follows the SERVER timezone, not your account\'s true reset instant (measured: the same moment lands a week apart under UTC vs +03:00). Pin it: "' + lim.weeklyResetAnchor + ' +03:00" (your offset) in .dz/config.json');
|
|
2292
|
+
}
|
|
2044
2293
|
// re-QE debt surfacing (backlog 6b40e667): the moment someone checks usage is the moment a
|
|
2045
2294
|
// usage-switched self-review debt should be visible. Best-effort — never breaks the contract.
|
|
2046
2295
|
try {
|
|
@@ -8563,6 +8812,45 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
8563
8812
|
return report.ok ? 0 : 1;
|
|
8564
8813
|
}
|
|
8565
8814
|
|
|
8815
|
+
// ── edit — replace/extend ONE idea's text (idea 1fde7bf6). The verb exists because a hand-edit
|
|
8816
|
+
// does not re-embed: the dedup vector keeps describing the OLD text. editIdea owns the text change
|
|
8817
|
+
// and MARKS the record embedStale; this layer owns the async re-embed and clears the mark ONLY
|
|
8818
|
+
// after the vector tier confirms. A failed re-embed is loud here AND enforced at the harm point:
|
|
8819
|
+
// classifyDedup refuses vector candidacy for a marked record (ADR-001).
|
|
8820
|
+
if (sub === 'edit') {
|
|
8821
|
+
const id = options.get('_positional_1');
|
|
8822
|
+
if (id === undefined) return emitErr('an idea id is required: dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run]');
|
|
8823
|
+
const report = editIdea(projectRoot, id, {
|
|
8824
|
+
...(options.get('text') !== undefined ? { text: options.get('text')! } : {}),
|
|
8825
|
+
...(options.get('append') !== undefined ? { append: options.get('append')! } : {}),
|
|
8826
|
+
dryRun: flags.has('dry-run'),
|
|
8827
|
+
});
|
|
8828
|
+
let embed: 'ok' | 'stale' | 'skipped' = 'skipped';
|
|
8829
|
+
if (report.ok && report.written && report.id !== undefined) {
|
|
8830
|
+
const updated = readIdeas(projectRoot).find((i) => i.id === report.id);
|
|
8831
|
+
if (updated !== undefined) {
|
|
8832
|
+
const mirror = await mirrorIdeaVector(projectRoot, updated);
|
|
8833
|
+
if (mirror.mirrored > 0 && mirror.error === undefined && clearEmbedStale(projectRoot, report.id)) embed = 'ok';
|
|
8834
|
+
else embed = 'stale';
|
|
8835
|
+
} else embed = 'stale';
|
|
8836
|
+
}
|
|
8837
|
+
if (json) {
|
|
8838
|
+
write(JSON.stringify({ verb: 'edit', ...report, embed, exitCode: report.ok ? (embed === 'stale' ? 1 : 0) : 1 }, null, 2));
|
|
8839
|
+
return report.ok ? (embed === 'stale' ? 1 : 0) : 1;
|
|
8840
|
+
}
|
|
8841
|
+
for (const e of report.errors) write(`dz backlog edit: ${e}`);
|
|
8842
|
+
if (report.ok && report.id !== undefined) {
|
|
8843
|
+
if (!report.written && !report.dryRun) write(`dz backlog edit: ${report.id} — text unchanged, nothing to do`);
|
|
8844
|
+
else write(`dz backlog edit${report.dryRun ? ' (dry-run)' : ''}: ${report.id}\n was: ${report.previousText?.slice(0, 100)}\n now: ${report.newText?.slice(0, 100)}`);
|
|
8845
|
+
if (report.written) {
|
|
8846
|
+
if (embed === 'ok') write(' vector re-embedded (dedup form) — the record is fully consistent');
|
|
8847
|
+
else write(' ⚠ vector NOT re-embedded — the record is MARKED embedStale: dedup will refuse to trust its similarity until `dz vector reindex` repairs it (the text edit itself landed)');
|
|
8848
|
+
}
|
|
8849
|
+
if (report.dryRun) write(' (dry-run — nothing written; re-run without --dry-run to apply)');
|
|
8850
|
+
}
|
|
8851
|
+
return report.ok ? (embed === 'stale' ? 1 : 0) : 1;
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8566
8854
|
if (sub === 'enrich') {
|
|
8567
8855
|
const id = options.get('_positional_1');
|
|
8568
8856
|
if (id === undefined) return emitErr('an idea id is required: dz backlog enrich <id>');
|
|
@@ -8653,6 +8941,55 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
8653
8941
|
let repoRoot = cwd;
|
|
8654
8942
|
try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd; } catch { /* not git */ }
|
|
8655
8943
|
|
|
8944
|
+
// ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
|
|
8945
|
+
// store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
|
|
8946
|
+
if (options.get('_positional_0') === 'recommend') {
|
|
8947
|
+
const records: unknown[] = readHarnessRecords(repoRoot);
|
|
8948
|
+
// imported runs (trace-bundle ADR-001 D4): run-meta sidecars carry runMeta.records of the SAME shape
|
|
8949
|
+
for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
|
|
8950
|
+
if (!existsSync(base)) continue;
|
|
8951
|
+
let names: string[] = [];
|
|
8952
|
+
try { names = readdirSync(base); } catch { continue; }
|
|
8953
|
+
for (const name of names) {
|
|
8954
|
+
const sidecar = join(base, name, 'run-meta.json');
|
|
8955
|
+
if (!existsSync(sidecar)) continue;
|
|
8956
|
+
try {
|
|
8957
|
+
const meta = JSON.parse(readFileSync(sidecar, 'utf-8')) as { runMeta?: { resolved?: boolean; records?: unknown[] } };
|
|
8958
|
+
if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records)) records.push(...meta.runMeta.records);
|
|
8959
|
+
} catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
const harvest = harvestStageOutcomes(records);
|
|
8963
|
+
const rec = recommendModels(harvest, { ...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}) });
|
|
8964
|
+
if (flags.has('apply')) {
|
|
8965
|
+
const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
|
|
8966
|
+
let alreadyFed: string[] = [];
|
|
8967
|
+
try { alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8')) as string[]; } catch { /* first apply */ }
|
|
8968
|
+
const plan = planFeed(harvest.samples, alreadyFed);
|
|
8969
|
+
for (const sample of plan.toFeed) finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
|
|
8970
|
+
try {
|
|
8971
|
+
mkdirSync(join(repoRoot, '.dz'), { recursive: true });
|
|
8972
|
+
writeFileSync(fedPath, JSON.stringify(plan.fedAfter));
|
|
8973
|
+
} catch (e) {
|
|
8974
|
+
write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${(e as Error).message}`);
|
|
8975
|
+
return 1;
|
|
8976
|
+
}
|
|
8977
|
+
write(`dz routing recommend --apply: fed ${plan.toFeed.length} sample(s) into .dz/routing-outcomes.json${plan.skippedRuns.length > 0 ? `; skipped ${plan.skippedRuns.length} already-fed run(s) (idempotent by runId)` : ''}`);
|
|
8978
|
+
}
|
|
8979
|
+
if (flags.has('json')) {
|
|
8980
|
+
write(JSON.stringify({ recommendation: rec, exitCode: 0 }, null, 2));
|
|
8981
|
+
return 0;
|
|
8982
|
+
}
|
|
8983
|
+
write(`dz routing recommend — args.models suggestion from ${rec.basis.runsUsed} run(s)${rec.basis.window !== null ? ` (${rec.basis.window.min.slice(0, 10)}..${rec.basis.window.max.slice(0, 10)})` : ''}:`);
|
|
8984
|
+
for (const s of rec.perStage) {
|
|
8985
|
+
write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
|
|
8986
|
+
}
|
|
8987
|
+
write(` basis: ${rec.basis.rule}`);
|
|
8988
|
+
write(` basis: skipped records — no result ${rec.basis.skipped.noResult}, no modelsUsed ${rec.basis.skipped.noModels} (predates model routing — history, not an error), no grade ${rec.basis.skipped.noGrade}, unknown model ${rec.basis.skipped.unknownModel}`);
|
|
8989
|
+
write(` basis: ${rec.basis.crossFamilyNote}`);
|
|
8990
|
+
return 0;
|
|
8991
|
+
}
|
|
8992
|
+
|
|
8656
8993
|
const stage = options.get('stage');
|
|
8657
8994
|
const tier = options.get('tier');
|
|
8658
8995
|
const model = options.get('model');
|