@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/dist/cli.js CHANGED
@@ -10,15 +10,15 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { homedir, tmpdir } from 'node:os';
12
12
  import { createRequire } from 'node:module';
13
- import { createSkill, getSkillInfo, isTargetName, listSkills, runDoctor, runInit, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, renderTimelineHtml, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
13
+ import { createSkill, getSkillInfo, isTargetName, listSkills, runDoctor, runInit, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, renderTimelineHtml, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
14
14
  // guard-promotion (feature guard-promotion, scout idea #1)
15
- assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport,
15
+ assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport,
16
16
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
17
17
  replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
18
18
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
19
- readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
19
+ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, editIdea, clearEmbedStale, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
20
20
  // Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
21
- parseMutationRegistry, applyMutationToText, countFailingTests, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, } from '@dzhechkov/harness-core';
21
+ parseMutationRegistry, applyMutationToText, countFailingTests, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, } from '@dzhechkov/harness-core';
22
22
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
23
23
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
24
24
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -39,6 +39,8 @@ Usage:
39
39
  dz workflow blobs [--check] (list/self-check the subsystem blob registry)
40
40
  dz workflow-lint <script.js> [--plan <plan.json>] [--require-plan|--legacy] [--json] (layer-1 gate; exit 0/1/3 — inconclusive is never a pass)
41
41
  dz workflow-trace <runDir|--slug <s>|--run <id>> [--invariants <plan.json>] [--html <out.html>] [--json] (timeline + SEQ invariant runner)
42
+ 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)
43
+ 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)
42
44
  dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
43
45
  dz bundle [--preset <name> | --select id,id,...] [--out <dir>] [--skills-dir <dir>] [--force] (portable self-contained skill bundles for a generic/LangGraph consumer)
44
46
  dz doctor [--project <dir>] [--pubkey <path>] [--require-signing] (health + signature check of installed packs)
@@ -65,6 +67,8 @@ Usage:
65
67
  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)
66
68
  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)
67
69
  dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
70
+ 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)
71
+ 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)
68
72
  dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
69
73
  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)
70
74
  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)
@@ -90,7 +94,7 @@ Usage:
90
94
  dz brain expand <kuId> [--source <slug>] [--json] (full-content lookup for a citation kuId; --json emits the full KU object)
91
95
  dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
92
96
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
93
- dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
97
+ 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)
94
98
  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)
95
99
  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)
96
100
  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)
@@ -758,7 +762,244 @@ function cmdWorkflowLint(options, flags, cwd, write) {
758
762
  /** `dz workflow-trace` — timeline + invariant runner over a run's trace.jsonl. Scope is CAPPED
759
763
  * (AM-8): <runDir|--slug|--run>, --invariants, --html, --json. NO watch/filter/compare/search/
760
764
  * retention/access-control — adding one needs an ADR amendment (the surface test pins this). */
765
+ /** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
766
+ function traceBundleToolVersion() {
767
+ try {
768
+ const req = createRequire(import.meta.url);
769
+ const pkg = req('../package.json');
770
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
771
+ }
772
+ catch {
773
+ return 'unknown';
774
+ }
775
+ }
776
+ /** Resolve a run the SAME three ways the timeline reader does — positional, --slug, --run — so a
777
+ * bundle can never address a run by a scheme the rest of the command does not understand. */
778
+ function resolveTraceRun(options, cwd, positional) {
779
+ const slug = options.get('slug');
780
+ const runId = options.get('run');
781
+ if (positional !== undefined && positional !== '') {
782
+ return { runDir: resolve(cwd, positional), slug: slug ?? null, runId: runId ?? null };
783
+ }
784
+ if (slug !== undefined)
785
+ return { runDir: resolve(cwd, 'features', slug), slug, runId: runId ?? null };
786
+ if (runId !== undefined)
787
+ return { runDir: resolve(cwd, '.dz', 'loop-trace', runId), slug: slug ?? null, runId };
788
+ return null;
789
+ }
790
+ /** Every harness workflow record under the project, already parsed. An unparseable file is SKIPPED
791
+ * here rather than guessed at — the pure half answers `unreadable` from what it is handed. */
792
+ function readHarnessRecords(cwd) {
793
+ const out = [];
794
+ const base = join(cwd, 'roam', 'claude-state');
795
+ if (!existsSync(base))
796
+ return out;
797
+ let sessions = [];
798
+ try {
799
+ sessions = readdirSync(base);
800
+ }
801
+ catch {
802
+ return out;
803
+ }
804
+ for (const session of sessions) {
805
+ const dir = join(base, session, 'workflows');
806
+ if (!existsSync(dir))
807
+ continue;
808
+ let names = [];
809
+ try {
810
+ names = readdirSync(dir);
811
+ }
812
+ catch {
813
+ continue;
814
+ }
815
+ for (const name of names) {
816
+ if (!name.endsWith('.json'))
817
+ continue;
818
+ try {
819
+ out.push(JSON.parse(readFileSync(join(dir, name), 'utf-8')));
820
+ }
821
+ catch { /* skipped, never guessed */ }
822
+ }
823
+ }
824
+ return out;
825
+ }
826
+ /** `dz workflow-trace export` — one run's telemetry as ONE movable file. */
827
+ function cmdWorkflowTraceExport(options, flags, cwd, write) {
828
+ const out = options.get('o') ?? options.get('out');
829
+ const run = resolveTraceRun(options, cwd, options.get('_positional_1'));
830
+ if (run === null || out === undefined || out === '') {
831
+ write('dz workflow-trace export: usage — dz workflow-trace export <runDir|--slug <s>|--run <id>> --o <file> [--include-pairs --yes] [--strict]');
832
+ return 1;
833
+ }
834
+ const wantPairs = flags.has('include-pairs');
835
+ if (wantPairs && !flags.has('yes')) {
836
+ 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');
837
+ return 1;
838
+ }
839
+ const readSlot = (abs, origin) => existsSync(abs)
840
+ ? { present: true, member: { origin, content: readFileSync(abs, 'utf-8') } }
841
+ : { present: false, reason: `absent on disk at ${origin}` };
842
+ const trace = readSlot(join(run.runDir, 'trace.jsonl'), 'trace.jsonl');
843
+ const checkpoints = readSlot(join(run.runDir, '.fa-state', 'checkpoints.jsonl'), '.fa-state/checkpoints.jsonl');
844
+ const ledgerAbs = join(cwd, TRACE_BUNDLE_LEDGER_PATH);
845
+ // An empty trailing line is not a malformed row — hand the pure half only real lines, or the
846
+ // bundle reports a phantom parse failure on every well-formed ledger (MEASURED on first run).
847
+ const ledgerLines = existsSync(ledgerAbs)
848
+ ? readFileSync(ledgerAbs, 'utf-8').split('\n').filter((line) => line.trim() !== '')
849
+ : null;
850
+ const pairFiles = [];
851
+ if (wantPairs && run.slug !== null) {
852
+ const pairDir = join(cwd, '.dz', 'fa-training', run.slug);
853
+ if (existsSync(pairDir)) {
854
+ for (const name of readdirSync(pairDir)) {
855
+ if (!name.endsWith('.jsonl'))
856
+ continue;
857
+ pairFiles.push({ origin: `.dz/fa-training/${run.slug}/${name}`, content: readFileSync(join(pairDir, name), 'utf-8') });
858
+ }
859
+ }
860
+ }
861
+ const bundle = buildBundle({
862
+ sourceRoot: cwd,
863
+ runAddress: options.get('_positional_1') ?? (run.slug !== null ? `--slug ${run.slug}` : `--run ${String(run.runId)}`),
864
+ slug: run.slug,
865
+ runId: run.runId,
866
+ toolVersion: traceBundleToolVersion(),
867
+ createdAt: null,
868
+ trace,
869
+ checkpoints,
870
+ ledgerLines,
871
+ ...(ledgerLines === null ? { ledgerReason: `no ledger at ${TRACE_BUNDLE_LEDGER_PATH}` } : {}),
872
+ includePairs: wantPairs,
873
+ pairFiles: wantPairs ? pairFiles : null,
874
+ records: readHarnessRecords(cwd),
875
+ });
876
+ try {
877
+ mkdirSync(dirname(resolve(cwd, out)), { recursive: true });
878
+ writeFileSync(resolve(cwd, out), serializeBundle(bundle));
879
+ }
880
+ catch (err) {
881
+ write(`dz workflow-trace export: could not write ${out} — ${err instanceof Error ? err.message : String(err)}`);
882
+ return 1;
883
+ }
884
+ // Degradation is NAMED, never silent — and exactly one reason is actionable.
885
+ let degraded = 0;
886
+ if (!bundle.trace.present) {
887
+ degraded++;
888
+ write(`dz workflow-trace export: trace absent — ${bundle.trace.reason}`);
889
+ }
890
+ if (!bundle.checkpoints.present) {
891
+ degraded++;
892
+ write(`dz workflow-trace export: checkpoints absent — ${bundle.checkpoints.reason}`);
893
+ }
894
+ if (!bundle.ledger.present) {
895
+ degraded++;
896
+ write(`dz workflow-trace export: ledger absent — ${String(bundle.ledger.reason)}`);
897
+ }
898
+ if (!bundle.runMeta.resolved) {
899
+ degraded++;
900
+ const reason = bundle.runMeta.reason;
901
+ write(reason === 'layout-unrecognised'
902
+ ? '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'
903
+ : `dz workflow-trace export: runMeta unresolved (${reason}) — no action needed, this is history or absence, not a layout change`);
904
+ }
905
+ write(`dz workflow-trace export: wrote ${out} (${bundle.schema}; ledger rows scanned ${bundle.ledger.scanned}, matched ${bundle.ledger.matched}, malformed ${bundle.ledger.malformed})`);
906
+ if (degraded > 0 && flags.has('strict')) {
907
+ write(`dz workflow-trace export: --strict — ${degraded} member(s) degraded, failing closed`);
908
+ return 1;
909
+ }
910
+ return 0;
911
+ }
912
+ /** `dz workflow-trace import` — reconstruct a run under an explicit root. FAIL-CLOSED. */
913
+ function cmdWorkflowTraceImport(options, flags, cwd, write) {
914
+ const file = options.get('_positional_1');
915
+ const into = options.get('into');
916
+ if (file === undefined || file === '' || into === undefined || into === '') {
917
+ write('dz workflow-trace import: usage — dz workflow-trace import <bundle.json> --into <root> [--force] [--with-pairs]');
918
+ return 1;
919
+ }
920
+ const abs = resolve(cwd, file);
921
+ if (!existsSync(abs)) {
922
+ write(`dz workflow-trace import: no such bundle: ${file}`);
923
+ return 1;
924
+ }
925
+ const parsed = parseBundle(readFileSync(abs, 'utf-8'));
926
+ if (!parsed.ok) {
927
+ write(parsed.reason === 'unknown-schema'
928
+ ? `dz workflow-trace import: REFUSED — unknown bundle schema "${parsed.found}"; this dz reads ${TRACE_BUNDLE_SCHEMA}. A future bundle is never best-effort parsed`
929
+ : parsed.reason === 'member-shape'
930
+ ? `dz workflow-trace import: REFUSED — member "${parsed.member}" fails its shape check; a partial import is never left behind`
931
+ : 'dz workflow-trace import: REFUSED — the bundle is not parseable JSON');
932
+ return 1;
933
+ }
934
+ const bundle = parsed.bundle;
935
+ const root = resolve(cwd, into);
936
+ const runDirRel = bundle.provenance.slug !== null ? join('features', bundle.provenance.slug) : join('.dz', 'loop-trace', String(bundle.provenance.runId ?? 'unknown-run'));
937
+ const runDirAbs = join(root, runDirRel);
938
+ let runDirHasContent = false;
939
+ try {
940
+ runDirHasContent = existsSync(runDirAbs) && readdirSync(runDirAbs).length > 0;
941
+ }
942
+ catch {
943
+ runDirHasContent = false;
944
+ }
945
+ let runIdentity = null;
946
+ const metaAbs = join(runDirAbs, TRACE_BUNDLE_RUN_META_FILE);
947
+ if (existsSync(metaAbs)) {
948
+ try {
949
+ const meta = JSON.parse(readFileSync(metaAbs, 'utf-8'));
950
+ runIdentity = {
951
+ slug: typeof meta.slug === 'string' ? meta.slug : null,
952
+ runId: typeof meta.runId === 'string' ? meta.runId : null,
953
+ };
954
+ }
955
+ catch {
956
+ runIdentity = null;
957
+ }
958
+ }
959
+ const plan = planImport(bundle, {
960
+ runDir: runDirRel,
961
+ existingPaths: [],
962
+ runDirHasContent,
963
+ runIdentity,
964
+ force: flags.has('force'),
965
+ withPairs: flags.has('with-pairs'),
966
+ bundleName: basename(abs),
967
+ });
968
+ if (!plan.ok) {
969
+ for (const refusal of plan.refusals)
970
+ write(`dz workflow-trace import: REFUSED ${refusal.path} — ${refusal.reason}`);
971
+ write('dz workflow-trace import: nothing was written');
972
+ return 1;
973
+ }
974
+ let written = 0;
975
+ const failures = [];
976
+ for (const entry of plan.writes) {
977
+ const target = join(root, entry.path);
978
+ try {
979
+ mkdirSync(dirname(target), { recursive: true });
980
+ writeFileSync(target, entry.content);
981
+ written++;
982
+ }
983
+ catch (err) {
984
+ failures.push(`${entry.path} — ${err instanceof Error ? err.message : String(err)}`);
985
+ }
986
+ }
987
+ // Report what ACTUALLY landed, computed from the writes, not from the plan.
988
+ for (const skipped of plan.refusals)
989
+ write(`dz workflow-trace import: skipped ${skipped.path} — ${skipped.reason}`);
990
+ for (const failure of failures)
991
+ write(`dz workflow-trace import: FAILED ${failure}`);
992
+ write(`dz workflow-trace import: wrote ${written} of ${plan.writes.length} member(s) under ${into}`);
993
+ return failures.length > 0 ? 1 : 0;
994
+ }
761
995
  function cmdWorkflowTrace(options, flags, cwd, write) {
996
+ // Two subcommands ride the SAME run addressing as the timeline reader (AM-8 keeps the surface
997
+ // capped; these are bundle transport, not new query verbs).
998
+ const sub = options.get('_positional_0');
999
+ if (sub === 'export')
1000
+ return cmdWorkflowTraceExport(options, flags, cwd, write);
1001
+ if (sub === 'import')
1002
+ return cmdWorkflowTraceImport(options, flags, cwd, write);
762
1003
  const runDirArg = options.get('_positional_0');
763
1004
  const slug = options.get('slug');
764
1005
  const runId = options.get('run');
@@ -1212,11 +1453,13 @@ function cmdStatuslineInstall(options, cwd, write) {
1212
1453
  /**
1213
1454
  * `dz statusline --fa-record` — the entry point the `/feature-adr` pipeline calls at Steps 0/8/9 to
1214
1455
  * record its LIVE learning state (Pattern memory loop: pool / recalled / stored). Unlike the render
1215
- * path this WRITES `.dz/feature-adr/learning-state.json` (via harness-core's best-effort
1216
- * `writeFeatureAdrState`, which itself computes `pool` from the learned-pattern count).
1456
+ * path this WRITES a per-slug `.dz/feature-adr/learning-state/*.json` slot (via harness-core's
1457
+ * best-effort `writeFeatureAdrState`, which itself computes `pool` from the learned-pattern count).
1217
1458
  *
1218
1459
  * GUARD (feature-adr flag discipline): `--slug` and `--step` are required; a missing one exits 1
1219
1460
  * with a copy-paste example. `--recalled`/`--stored` default to 0 and reject non-numeric input.
1461
+ * `--kind <feature-adr|loop>` identifies the producer, defaults to `feature-adr`, and rejects any
1462
+ * other value rather than silently weakening panel arbitration.
1220
1463
  */
1221
1464
  function cmdStatuslineFaRecord(options, cwd, write) {
1222
1465
  const slug = (options.get('slug') ?? '').trim();
@@ -1248,10 +1491,16 @@ function cmdStatuslineFaRecord(options, cwd, write) {
1248
1491
  const reinforced = parseCount('reinforced');
1249
1492
  if (reinforced === undefined)
1250
1493
  return 1;
1494
+ const kindRaw = options.get('kind') ?? 'feature-adr';
1495
+ if (kindRaw !== 'feature-adr' && kindRaw !== 'loop') {
1496
+ write(`dz statusline --fa-record: --kind must be feature-adr or loop (got "${kindRaw}")`);
1497
+ write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 0" --kind feature-adr --recalled 5 --stored 2');
1498
+ return 1;
1499
+ }
1251
1500
  const mode = options.get('mode');
1252
1501
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1253
1502
  const state = writeFeatureAdrState(projectRoot, {
1254
- slug, step, recalled, stored,
1503
+ kind: kindRaw, slug, step, recalled, stored,
1255
1504
  ...(reinforced > 0 ? { reinforced } : {}),
1256
1505
  ...(mode !== undefined && mode.trim() !== '' ? { mode: mode.trim() } : {}),
1257
1506
  });
@@ -1294,7 +1543,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1294
1543
  // Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
1295
1544
  const fa = data.featureAdr;
1296
1545
  if (fa !== undefined) {
1297
- line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
1546
+ // 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.
1547
+ if (fa.kind === 'loop') {
1548
+ line = `🔁 loop ${fa.step} · ${line}`;
1549
+ }
1550
+ else {
1551
+ line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
1552
+ }
1298
1553
  }
1299
1554
  write(line);
1300
1555
  return 0;
@@ -1585,7 +1840,16 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1585
1840
  const s = u.sessionPct === null ? 'n/a' : '~' + u.sessionPct + '%';
1586
1841
  const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
1587
1842
  const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
1588
- write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
1843
+ // The weekly reset is WEEKLY: print the anchor verbatim (weekday + offset), not a bare clock
1844
+ // time — 'resets 08:59' reads as daily and hides the weekday (idea c8513be9: the bare form
1845
+ // misread a Monday reading as '41 minutes after the boundary' when the boundary was Wednesday's).
1846
+ const weeklyAnchorLabel = typeof lim.weeklyResetAnchor === 'string' && lim.weeklyResetAnchor !== ''
1847
+ ? lim.weeklyResetAnchor
1848
+ : clock(u.weeklyResetsAt);
1849
+ write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + weeklyAnchorLabel + ') · estimated');
1850
+ if (typeof lim.weeklyResetAnchor === 'string' && parseWeeklyResetAnchor(lim.weeklyResetAnchor)?.offsetMinutes === undefined) {
1851
+ 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');
1852
+ }
1589
1853
  // re-QE debt surfacing (backlog 6b40e667): the moment someone checks usage is the moment a
1590
1854
  // usage-switched self-review debt should be visible. Best-effort — never breaks the contract.
1591
1855
  try {
@@ -8371,6 +8635,55 @@ async function cmdBacklog(options, flags, cwd, write) {
8371
8635
  write(' (dry-run — nothing written; re-run without --dry-run to apply)');
8372
8636
  return report.ok ? 0 : 1;
8373
8637
  }
8638
+ // ── edit — replace/extend ONE idea's text (idea 1fde7bf6). The verb exists because a hand-edit
8639
+ // does not re-embed: the dedup vector keeps describing the OLD text. editIdea owns the text change
8640
+ // and MARKS the record embedStale; this layer owns the async re-embed and clears the mark ONLY
8641
+ // after the vector tier confirms. A failed re-embed is loud here AND enforced at the harm point:
8642
+ // classifyDedup refuses vector candidacy for a marked record (ADR-001).
8643
+ if (sub === 'edit') {
8644
+ const id = options.get('_positional_1');
8645
+ if (id === undefined)
8646
+ return emitErr('an idea id is required: dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run]');
8647
+ const report = editIdea(projectRoot, id, {
8648
+ ...(options.get('text') !== undefined ? { text: options.get('text') } : {}),
8649
+ ...(options.get('append') !== undefined ? { append: options.get('append') } : {}),
8650
+ dryRun: flags.has('dry-run'),
8651
+ });
8652
+ let embed = 'skipped';
8653
+ if (report.ok && report.written && report.id !== undefined) {
8654
+ const updated = readIdeas(projectRoot).find((i) => i.id === report.id);
8655
+ if (updated !== undefined) {
8656
+ const mirror = await mirrorIdeaVector(projectRoot, updated);
8657
+ if (mirror.mirrored > 0 && mirror.error === undefined && clearEmbedStale(projectRoot, report.id))
8658
+ embed = 'ok';
8659
+ else
8660
+ embed = 'stale';
8661
+ }
8662
+ else
8663
+ embed = 'stale';
8664
+ }
8665
+ if (json) {
8666
+ write(JSON.stringify({ verb: 'edit', ...report, embed, exitCode: report.ok ? (embed === 'stale' ? 1 : 0) : 1 }, null, 2));
8667
+ return report.ok ? (embed === 'stale' ? 1 : 0) : 1;
8668
+ }
8669
+ for (const e of report.errors)
8670
+ write(`dz backlog edit: ${e}`);
8671
+ if (report.ok && report.id !== undefined) {
8672
+ if (!report.written && !report.dryRun)
8673
+ write(`dz backlog edit: ${report.id} — text unchanged, nothing to do`);
8674
+ else
8675
+ write(`dz backlog edit${report.dryRun ? ' (dry-run)' : ''}: ${report.id}\n was: ${report.previousText?.slice(0, 100)}\n now: ${report.newText?.slice(0, 100)}`);
8676
+ if (report.written) {
8677
+ if (embed === 'ok')
8678
+ write(' vector re-embedded (dedup form) — the record is fully consistent');
8679
+ else
8680
+ 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)');
8681
+ }
8682
+ if (report.dryRun)
8683
+ write(' (dry-run — nothing written; re-run without --dry-run to apply)');
8684
+ }
8685
+ return report.ok ? (embed === 'stale' ? 1 : 0) : 1;
8686
+ }
8374
8687
  if (sub === 'enrich') {
8375
8688
  const id = options.get('_positional_1');
8376
8689
  if (id === undefined)
@@ -8472,6 +8785,68 @@ function cmdRouting(options, flags, cwd, write) {
8472
8785
  repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
8473
8786
  }
8474
8787
  catch { /* not git */ }
8788
+ // ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
8789
+ // store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
8790
+ if (options.get('_positional_0') === 'recommend') {
8791
+ const records = readHarnessRecords(repoRoot);
8792
+ // imported runs (trace-bundle ADR-001 D4): run-meta sidecars carry runMeta.records of the SAME shape
8793
+ for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
8794
+ if (!existsSync(base))
8795
+ continue;
8796
+ let names = [];
8797
+ try {
8798
+ names = readdirSync(base);
8799
+ }
8800
+ catch {
8801
+ continue;
8802
+ }
8803
+ for (const name of names) {
8804
+ const sidecar = join(base, name, 'run-meta.json');
8805
+ if (!existsSync(sidecar))
8806
+ continue;
8807
+ try {
8808
+ const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
8809
+ if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records))
8810
+ records.push(...meta.runMeta.records);
8811
+ }
8812
+ catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
8813
+ }
8814
+ }
8815
+ const harvest = harvestStageOutcomes(records);
8816
+ const rec = recommendModels(harvest, { ...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}) });
8817
+ if (flags.has('apply')) {
8818
+ const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
8819
+ let alreadyFed = [];
8820
+ try {
8821
+ alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8'));
8822
+ }
8823
+ catch { /* first apply */ }
8824
+ const plan = planFeed(harvest.samples, alreadyFed);
8825
+ for (const sample of plan.toFeed)
8826
+ finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
8827
+ try {
8828
+ mkdirSync(join(repoRoot, '.dz'), { recursive: true });
8829
+ writeFileSync(fedPath, JSON.stringify(plan.fedAfter));
8830
+ }
8831
+ catch (e) {
8832
+ write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${e.message}`);
8833
+ return 1;
8834
+ }
8835
+ 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)` : ''}`);
8836
+ }
8837
+ if (flags.has('json')) {
8838
+ write(JSON.stringify({ recommendation: rec, exitCode: 0 }, null, 2));
8839
+ return 0;
8840
+ }
8841
+ 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)})` : ''}:`);
8842
+ for (const s of rec.perStage) {
8843
+ write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
8844
+ }
8845
+ write(` basis: ${rec.basis.rule}`);
8846
+ 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}`);
8847
+ write(` basis: ${rec.basis.crossFamilyNote}`);
8848
+ return 0;
8849
+ }
8475
8850
  const stage = options.get('stage');
8476
8851
  const tier = options.get('tier');
8477
8852
  const model = options.get('model');