@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.4.1",
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": "357bb891e287a2af120a66914085b3b7dec24092aa99f46a8767c096741d713f"
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": "a8cff82f9f31b06b6a5f1934747631fcc551945ee3b7ac2c620b7c25076303cc"
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": "7afd2be99a3b9581b6fd8bfae58df6c608db346f741263c219d2140a5e8300ce"
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": "481e6ddd256869585b3450ebe901057398a30bb8292e6f93f80a18d5c89ad0c0"
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": "5a2d203ef20d2b6179595627b4fa1856fd7e17a31b19690324f589487874f9e0"
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": "1aba996c4e5dd717a496074baa83778e8f2e04927689fc2cfc315033b8982b38"
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": "e63d24934506b3d6c6ec7f67072273ed9c9dabb86e8a24a7838819217ca6035d"
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": "36276fa51f9e5301ddb317687d0dbc839641b86f47f203e5d392bab31f4f178e"
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.json` (via harness-core's best-effort
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
- line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
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
- write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
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');