@dzhechkov/harness-cli 0.8.15 → 0.8.17

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
@@ -16,9 +16,11 @@ import { createHash, randomBytes } from 'node:crypto';
16
16
  import { homedir, hostname, tmpdir } from 'node:os';
17
17
  import { createRequire } from 'node:module';
18
18
  import { isDeepStrictEqual } from 'node:util';
19
+ import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from '@dzhechkov/harness-core';
20
+ import { appendRunEvent, readRunRegistry, liveParents, liveness, probePid, settleDeadRuns, planRegistryArchive, planWorktreeCleanup, renderCleanupPlan, worktreeRemovalsToApply } from '@dzhechkov/harness-core';
19
21
  import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
20
22
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
21
- TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, 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, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
23
+ TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, findExactLesson, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, 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, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
22
24
  // guard-promotion (feature guard-promotion, scout idea #1)
23
25
  assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listPackFiles, 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, resolveScanTailTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, runRetroTailScan, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
24
26
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
@@ -28,7 +30,7 @@ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGo
28
30
  // qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
29
31
  KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
30
32
  // Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
31
- parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
33
+ parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, readFeatureTier, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
32
34
  // operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
33
35
  renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
34
36
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
@@ -69,11 +71,14 @@ export const DZ_COMMANDS = [
69
71
  'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
70
72
  'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
71
73
  'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
72
- 'name-check', 'brief-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
74
+ 'name-check', 'brief-check', 'provenance-check', 'journal', 'feature-adr-record', 'runs', 'runs-record', 'runs-clean', 'amendment-check', 'contract-check',
73
75
  'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
74
76
  'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
75
77
  ];
76
78
  const USAGE = `dz - DZ cross-platform harness CLI
79
+ dz runs [--settle] [--stall-minutes N] [--json] [--project <dir>] [--probe-pid <pid>] (run registry: live, stalled, orphaned, inconclusive or finished; PID probe prints true|false|unknown)
80
+ dz runs-record --event started|heartbeat|finished [--run-id <id>] [--kind <kind>] [--slug <slug>] [--pid <pid|host>] [--parent-run-id <id>] [--outcome <outcome>] [--reason <text>] [--project <dir>] [--json] (append run event; started can allocate a run ID; host resolves the Claude ancestor PID)
81
+ dz runs-clean [--apply] [--retention-days N] [--project <dir>] [--json] (plan cleanup of old clean merged worktrees and completed/dead registry events; apply explicitly)
77
82
 
78
83
  Usage:
79
84
  dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--allow-integrations <sha256:digest>] [--no-integrations] [--no-hooks] [--no-verify] (integration manifests require exact digest consent; --no-integrations = explicit skills-only)
@@ -128,6 +133,7 @@ Usage:
128
133
  dz sbom [--pack <name>] [--out <file>] (CycloneDX software bill of materials for the workspace, or for one pack with --pack)
129
134
  dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N / AM-CP-N row must resolve to a test found INSIDE the file the row names (the challenge-panel prefix is part of the id: AM-CP-1 is never AM-1); the PLAN is authoritative when it carries rows, and an ideation amendment the plan drops is a failure. exit 0 pass/skip, 1 fail, 3 NOT-ESTABLISHED — a section that parsed ZERO rows is never a pass, UNLESS the plan explicitly declares \"None\"/\"нет\", which is an answer and reports skip. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
130
135
  dz contract-check --slug <s> [--json] (read-only retrospective feature contract gate: extracts canonical AC-N + ADR Confirmation items, requires one artifact-anchored met|unmet|not-testable verdict per CC-N, and rejects A/B with unmet. exit 0 pass / 1 readable contract or verdict violation / 2 invalid invocation or unreadable/not-established artifacts)
136
+ dz journal add --kind decision|verdict|run|error|block "<text>" [--ref <trace>] [--at <ISO>] [--quote <file>] [--commit-quote]; dz journal show [--day|--week] [--at <date>] [--kind <kind>] [--json] (UTC day files, witnessed append; quotes stay local unless explicitly staged)
131
137
  dz feature-adr-record --kind ledger|training-pair --stage <s> [--slug <s>] [--row|--pair <json>] [--mark <n>] [--once] [--json] (the witnessed writer for the run-cost ledger and training pairs: the payload arrives as an ARGUMENT, never as shell; a malformed or wrong-kind payload is REFUSED before any write; the timestamp is stamped before serialising; the append is verified by re-reading the tail. exit 0 written|duplicate|skipped, 2 refused, 3 not-verified — a record failure is never blocking)
132
138
  dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
133
139
  dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
@@ -169,7 +175,7 @@ Usage:
169
175
  dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
170
176
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
171
177
  dz store-guard [--status|--reset] [--yes] [--project <dir>] (show the monotonic external high-water mark; --reset is the only lowering path and requires confirmation or --yes)
172
- dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--tier <S|M|L|XL>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state + phase → 📐 SECOND-LINE phase panel; a plain "Step <n>" label that goes BACKWARDS against a slot younger than 90 min is absorbed as a stale duplicate — prefix the label with ⛔ or ⏸ to record a legitimate regression)
178
+ dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--tier <S|M|L|XL>] [--run-id <id>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state + phase → 📐 SECOND-LINE phase panel; the monotone guard absorbs a backwards plain "Step <n>" only within the same non-empty run id, while an absent/empty id retains legacy fresh-slot behavior — prefix the label with ⛔ or ⏸ to record a legitimate regression)
173
179
  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)
174
180
  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)
175
181
  dz chain [--project <dir>] [--json] (verify EVERY hash-chained journal in ONE command: coverage is DERIVED from the CHAINED_JOURNALS registry, never typed, so a journal cannot be given a chain and checked by nobody. An ABSENT journal is NAMED absent, never omitted — omission and cleanliness are indistinguishable in a report. Statuses: ok | healed (defects the current unbroken run has outlived — verdicts over present records are sound) | unchained (present, no chained record yet — legal) | absent | broken | unreadable. Exit 1 on broken/unreadable: a verifier that reports damage and exits 0 is one no automation can act on)
@@ -2079,12 +2085,14 @@ function cmdStatuslineFaRecord(options, cwd, write, writeErr) {
2079
2085
  return 1;
2080
2086
  }
2081
2087
  const mode = options.get('mode');
2088
+ const runId = options.get('run-id')?.trim();
2082
2089
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
2083
2090
  const outcome = writeFeatureAdrStateDetailed(projectRoot, {
2084
2091
  kind: kindRaw, slug, step, recalled, stored,
2085
2092
  ...(reinforced > 0 ? { reinforced } : {}),
2086
2093
  ...(mode !== undefined && mode.trim() !== '' ? { mode: mode.trim() } : {}),
2087
2094
  ...(tier !== undefined ? { tier } : {}),
2095
+ ...(runId !== undefined && runId !== '' ? { runId } : {}),
2088
2096
  });
2089
2097
  const state = outcome.state;
2090
2098
  if (state === undefined) {
@@ -2299,26 +2307,37 @@ function cmdStatusline(options, flags, cwd, write, readStdin, writeErr) {
2299
2307
  }));
2300
2308
  return 0;
2301
2309
  }
2310
+ // ТЕКСТ ПАНЕЛИ — ПО-АНГЛИЙСКИ по просьбе владельца 2026-09-09: строка статуса узкая,
2311
+ // английские слова в ней короче русских при той же ясности. Комментарии остаются русскими.
2302
2312
  const breakdown = data.patternBreakdown;
2303
2313
  let line = breakdown === undefined
2304
2314
  ? `🎓 dz: ${data.patterns} patterns`
2305
- : `🎓 dz: ${data.patterns} (${breakdown.active} актив${breakdown.quarantined > 0
2306
- ? ` · ${breakdown.quarantined} карантин${breakdown.attention ? ' ⚠' : ''}`
2307
- : ''})${breakdown.tierDelta !== undefined ? ` ⚠ тиры Δ${breakdown.tierDelta}` : ''}`;
2315
+ : `🎓 dz: ${data.patterns} (${breakdown.active} active${breakdown.quarantined > 0
2316
+ ? ` · ${breakdown.quarantined} quarantined${breakdown.attention ? ' ⚠' : ''}`
2317
+ : ''})${breakdown.tierDelta !== undefined ? ` ⚠ tiers Δ${breakdown.tierDelta}` : ''}`;
2318
+ // Показатель зеркала печатается и здесь: `dz statusline` — та же панель, и показатель,
2319
+ // живущий только во вспомогательном скрипте, для этой поверхности просто не существовал.
2320
+ if (data.patternMirror?.state === 'unavailable')
2321
+ line += ' (mirror unreadable ⚠)';
2322
+ else if (data.patternMirror?.state === 'different')
2323
+ line += ` (mirror ${data.patternMirror.vector} ⚠)`;
2308
2324
  if (data.storeHealth?.verdict === 'collapsed') {
2309
- line += ` ⛔ ОБВАЛ: было ${data.storeHealth.previousMax ?? '?'} · dz store-guard --reset`;
2325
+ line += ` ⛔ COLLAPSE: was ${data.storeHealth.previousMax ?? '?'} · dz store-guard --reset`;
2310
2326
  }
2311
2327
  else if (data.storeHealth?.verdict === 'cold-start-over-existing') {
2312
- line += ` ⛔ СТОР ПУСТ: было ${data.storeHealth.previousMax ?? '?'} · восстановить из снимков ${data.storeHealth.snapshotPath ?? ''}`.trimEnd();
2328
+ line += ` ⛔ STORE EMPTY: was ${data.storeHealth.previousMax ?? '?'} · restore from snapshots ${data.storeHealth.snapshotPath ?? ''}`.trimEnd();
2313
2329
  }
2314
2330
  else if (data.storeHealth?.verdict === 'unreadable') {
2315
- line += ` ⛔ СТОР НЕЧИТАЕМ${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
2331
+ line += ` ⛔ STORE UNREADABLE${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
2316
2332
  ? `: ${data.storeHealth.unreadableFiles.join(', ')}` : ''}`;
2317
2333
  }
2318
2334
  else if (data.storeHealth?.verdict === 'source-changed') {
2319
- line += ' ⚠ смена источника хранения';
2335
+ line += ' ⚠ store source changed';
2320
2336
  }
2321
- line += `${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
2337
+ // Рядом с числом источников объём каждого через «/» (просьба владельца 2026-09-09):
2338
+ // четыре источника по 300 единиц и четыре по три — разные корпуса, а число одно и то же.
2339
+ const ku = data.brainKuCounts.length > 0 ? ` (${data.brainKuCounts.join('/')})` : '';
2340
+ line += `${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources${ku}`;
2322
2341
  const branch = statuslineGitBranch(projectRoot);
2323
2342
  if (branch !== undefined)
2324
2343
  line += ` · ⎇ ${branch}`;
@@ -3174,9 +3193,14 @@ async function cmdStoreGuard(options, flags, cwd, write, writeErr, stdinText, in
3174
3193
  return 1;
3175
3194
  }
3176
3195
  }
3177
- async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
3196
+ async function runTeachGuardReinforcement(projectRoot, dzId, reward, preserveQuarantine = false) {
3178
3197
  const backend = resolveLearningBackend(projectRoot);
3179
- backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
3198
+ backend.addSample({
3199
+ dzId,
3200
+ kind: preserveQuarantine ? 'recall-hit' : 'reinforce',
3201
+ reward,
3202
+ ts: new Date().toISOString(),
3203
+ });
3180
3204
  return backend.train();
3181
3205
  }
3182
3206
  async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
@@ -3425,6 +3449,15 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
3425
3449
  }
3426
3450
  }
3427
3451
  }
3452
+ if (!flags.has('guard')) {
3453
+ const exact = findExactLesson(loadStoreRecords(storeRoot), pattern, domain);
3454
+ if (exact !== null) {
3455
+ await reinforceRunner(storeRoot, exact.id, Math.max(0, Math.min(1, reward)), exact.quarantined);
3456
+ write(`Reinforced existing: ${exact.id} (exact text)${exact.quarantined ? ' [quarantined]' : ''}`);
3457
+ write(` Total patterns: ${loadStorePatternsSync(storeRoot).length}`);
3458
+ return 0;
3459
+ }
3460
+ }
3428
3461
  // Distill pattern into actionable rule (claude-smart inspired)
3429
3462
  // Convert "what happened" into "what to do next time"
3430
3463
  const isRule = pattern.toLowerCase().startsWith('always') || pattern.toLowerCase().startsWith('never') ||
@@ -6383,11 +6416,19 @@ function cmdPublish(options, flags, cwd, write) {
6383
6416
  // A one-line preview inline; on error the FULL captured npm error prints below (truncating a publish
6384
6417
  // failure to 60 chars is how a release stays undiagnosable — the reason lives past char 60).
6385
6418
  const detail = pkg.error && pkg.status !== 'error' ? ` (${pkg.error.slice(0, 60)})` : '';
6386
- write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} ${pkg.newVersion} ${pkg.status}${detail}`);
6419
+ const receipt = pkg.status === 'published' && pkg.registryProbes !== undefined
6420
+ ? ` (confirmed by registry after ${pkg.registryProbes} probes)`
6421
+ : '';
6422
+ write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${receipt}${detail}`);
6387
6423
  if (pkg.status === 'error' && pkg.error) {
6388
6424
  for (const line of pkg.error.split('\n'))
6389
6425
  write(` ${line}`);
6390
6426
  }
6427
+ // The rollback branch cannot tell "publish failed" from "publish landed, registry never answered":
6428
+ // both restore package.json. The operator can — one registry query — so say what to check.
6429
+ if (pkg.status === 'error' && pkg.error?.startsWith('registry did not confirm')) {
6430
+ write(` ↳ disk bump rolled back; if \`npm view ${pkg.name}@${pkg.newVersion} version\` answers, the publish DID land — re-apply the bump by hand`);
6431
+ }
6391
6432
  // Surface warn-mode findings that did not block the publish.
6392
6433
  if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
6393
6434
  write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
@@ -11012,14 +11053,34 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
11012
11053
  return fail(`registry ${registryPath} is invalid:\n - ${parsed.errors.join('\n - ')}`);
11013
11054
  }
11014
11055
  let entries = parsed.registry.entries;
11056
+ let entryResults = parsed.entryResults;
11015
11057
  const only = options.get('only');
11016
11058
  if (only !== undefined) {
11017
11059
  const ids = only.split(',').map((s) => s.trim()).filter(Boolean);
11018
- const known = new Set(entries.map((e) => e.id));
11060
+ const known = new Set([
11061
+ ...entries.map((entry) => entry.id),
11062
+ ...entryResults.map((result) => result.id),
11063
+ ]);
11019
11064
  const unknown = ids.filter((id) => !known.has(id));
11020
11065
  if (unknown.length > 0)
11021
11066
  return fail(`--only names unknown entry id(s): ${unknown.join(', ')}`);
11022
11067
  entries = entries.filter((e) => ids.includes(e.id));
11068
+ entryResults = entryResults.filter((result) => ids.includes(result.id));
11069
+ }
11070
+ if (entries.length === 0) {
11071
+ const scope = only === undefined ? 'registry' : 'selected registry entries';
11072
+ const error = `${scope} has no runnable entries after validation — nothing can be run; the registry is unusable`;
11073
+ const summary = summarizeMutationResults(entryResults);
11074
+ if (json) {
11075
+ write(JSON.stringify({ error, registryPath, results: entryResults, summary, exitCode: 2 }, null, 2));
11076
+ }
11077
+ else {
11078
+ write(`dz mutation-gate: ${error}`);
11079
+ for (const result of entryResults) {
11080
+ write(` - ${result.id}: ${result.verdict} — ${result.detail}`);
11081
+ }
11082
+ }
11083
+ return 2;
11023
11084
  }
11024
11085
  const testCmdRaw = options.get('test-cmd') ?? parsed.registry.testCommand ?? 'npm test';
11025
11086
  if (/[\0\n\r]/.test(testCmdRaw))
@@ -11055,7 +11116,7 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
11055
11116
  }
11056
11117
  catch { /* not in a git repo */ }
11057
11118
  let copyDir = join(scratchParent, 'pkg');
11058
- const results = [];
11119
+ const results = [...entryResults];
11059
11120
  const observations = [];
11060
11121
  const warnings = [];
11061
11122
  const internalRetries = [];
@@ -11187,10 +11248,10 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
11187
11248
  : undefined);
11188
11249
  if (!baseline.ok) {
11189
11250
  if (json) {
11190
- write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
11251
+ write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results, internalRetries, exitCode: 1 }, null, 2));
11191
11252
  return 1;
11192
11253
  }
11193
- write(renderMutationReport([], baseline, pkgDir));
11254
+ write(renderMutationReport(results, baseline, pkgDir));
11194
11255
  return 1;
11195
11256
  }
11196
11257
  for (const entry of entries) {
@@ -13259,6 +13320,258 @@ function cmdLedgerBackfill(options, flags, cwd, write) {
13259
13320
  write(` not filled: ${n} row(s) — ${reason}`);
13260
13321
  return 0;
13261
13322
  }
13323
+ const runRegistryIO = {
13324
+ append: (path, line) => withNamedLockSync(dirname(path), 'run-registry-archive', () => appendFileSync(path, line, 'utf8')),
13325
+ read: (path) => readFileSync(path, 'utf8'),
13326
+ mkdir: (dir) => { mkdirSync(dir, { recursive: true }); },
13327
+ };
13328
+ function cmdRuns(options, flags, cwd, write) {
13329
+ if (options.has('probe-pid')) {
13330
+ const result = probePid(Number(options.get('probe-pid')));
13331
+ write(result === null ? 'unknown' : String(result));
13332
+ return 0;
13333
+ }
13334
+ const root = resolve(options.get('project') ?? cwd);
13335
+ const minutes = Number(options.get('stall-minutes') ?? 120);
13336
+ if (!Number.isFinite(minutes) || minutes < 0) {
13337
+ write(flags.has('json') ? JSON.stringify({ status: 'refused', reason: 'stall-minutes must be a finite non-negative number' }) : 'stall-minutes must be a finite non-negative number');
13338
+ return 2;
13339
+ }
13340
+ const opts = { stallMs: minutes * 60_000 };
13341
+ let settled = [];
13342
+ if (flags.has('settle')) {
13343
+ settled = withNamedLockSync(join(root, '.dz', 'runs'), 'run-registry-archive', () => {
13344
+ const events = settleDeadRuns(readRunRegistry(root, runRegistryIO), Date.now(), probePid);
13345
+ // Already holding the shared archive/append lock across read-plan-append.
13346
+ const unlockedIO = { ...runRegistryIO, append: (path, line) => appendFileSync(path, line, 'utf8') };
13347
+ for (const event of events)
13348
+ appendRunEvent(root, event, unlockedIO);
13349
+ return events;
13350
+ });
13351
+ if (!flags.has('json')) {
13352
+ for (const event of settled)
13353
+ write(`settled ${event.runId} — died (${event.reason})`);
13354
+ if (!settled.length)
13355
+ write('nothing to settle');
13356
+ }
13357
+ }
13358
+ const registry = readRunRegistry(root, runRegistryIO);
13359
+ if (registry.status === 'missing') {
13360
+ write(flags.has('json') ? JSON.stringify(registry) : 'нет реестра: .dz/runs/registry.jsonl ещё не создан (ни одного прогона)');
13361
+ return 0;
13362
+ }
13363
+ const now = Date.now();
13364
+ const parents = liveParents(registry, now, probePid, opts);
13365
+ const rows = registry.runs.map((run, i) => {
13366
+ const parent = parents[i].liveness;
13367
+ const own = liveness(run, now, probePid, opts);
13368
+ const decision = registry.status !== 'readable' ? parent :
13369
+ run.finished ? { state: 'finished', reason: run.outcome } :
13370
+ parent.state !== 'live' && parent.state !== 'stalled' ? parent : own;
13371
+ return { ...run, state: decision.state, reason: decision.reason,
13372
+ parentLiveness: parent, heartbeatAgeMs: run.heartbeat ? now - Date.parse(run.heartbeat) : null };
13373
+ });
13374
+ if (flags.has('json'))
13375
+ write(JSON.stringify({ ...registry, runs: rows, ...(flags.has('settle') ? { settled } : {}) }));
13376
+ else {
13377
+ if (registry.status !== 'readable')
13378
+ write('inconclusive — ' + registry.reason);
13379
+ write('runId | kind | slug | pid | parent | liveness | heartbeat age');
13380
+ for (const row of rows)
13381
+ write([row.runId, row.kind, row.slug, row.pid, row.parentRunId ?? '—',
13382
+ row.state, row.heartbeatAgeMs === null ? '—' : `${row.heartbeatAgeMs}ms`].join(' | ') + ' — ' + row.reason);
13383
+ }
13384
+ return registry.status === 'readable' ? 0 : 1;
13385
+ }
13386
+ /** Git effects are injected; the core owns eligibility and the explicit apply boundary. */
13387
+ export function cmdRunsClean(options, flags, cwd, write, exec = execSync) {
13388
+ const root = resolve(options.get('project') ?? cwd);
13389
+ const json = flags.has('json');
13390
+ const messages = [];
13391
+ const say = (line) => { messages.push(line); if (!json)
13392
+ write(line); };
13393
+ try {
13394
+ const days = Number(options.get('retention-days') ?? 2);
13395
+ if (!Number.isFinite(days) || days < 0)
13396
+ throw new Error('retention-days must be a finite non-negative number');
13397
+ const now = Date.now(), retentionMs = days * 86400000;
13398
+ const git = (command, dir = root) => String(exec(command, { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
13399
+ // Git C-quotes paths containing controls/non-ASCII bytes. Decode octal UTF-8 as well.
13400
+ const unquote = (value) => {
13401
+ if (!value.startsWith('"'))
13402
+ return value;
13403
+ const bytes = [];
13404
+ const body = value.slice(1, -1);
13405
+ const escapes = { a: '\x07', b: '\b', t: '\t', n: '\n', v: '\v', f: '\f', r: '\r', '"': '"', '\\': '\\' };
13406
+ for (let i = 0; i < body.length; i++) {
13407
+ if (body[i] === '\\') {
13408
+ const octal = /^[0-7]{1,3}/.exec(body.slice(i + 1));
13409
+ if (octal) {
13410
+ bytes.push(parseInt(octal[0], 8));
13411
+ i += octal[0].length;
13412
+ }
13413
+ else {
13414
+ i++;
13415
+ bytes.push(...Buffer.from(escapes[body[i]] ?? body[i]));
13416
+ }
13417
+ }
13418
+ else {
13419
+ const cp = String.fromCodePoint(body.codePointAt(i));
13420
+ bytes.push(...Buffer.from(cp));
13421
+ i += cp.length - 1;
13422
+ }
13423
+ }
13424
+ return Buffer.from(bytes).toString('utf8');
13425
+ };
13426
+ const list = () => {
13427
+ const text = git('git worktree list --porcelain');
13428
+ const trees = text.trimEnd().split(/\n\n+/).filter(Boolean).map(block => {
13429
+ const lines = block.split('\n');
13430
+ const path = lines.find(line => line.startsWith('worktree '))?.slice(9);
13431
+ const sha = lines.find(line => line.startsWith('HEAD '))?.slice(5);
13432
+ if (!path || !sha)
13433
+ throw new Error('unreadable worktree entry: ' + block);
13434
+ return { path: unquote(path), sha,
13435
+ branch: lines.find(line => line.startsWith('branch '))?.slice(7) ?? null,
13436
+ detached: lines.includes('detached') };
13437
+ });
13438
+ if (!trees.length)
13439
+ throw new Error('empty git worktree list');
13440
+ return trees;
13441
+ };
13442
+ const trees = list();
13443
+ const facts = trees.map((tree, index) => {
13444
+ // Any unreadable status aborts before removal; unknown cleanliness is never clean.
13445
+ const dirtyFiles = git('git status --short --untracked-files=all', tree.path).split('\n').filter(Boolean)
13446
+ .map(line => unquote(line.slice(3)));
13447
+ let merged = null;
13448
+ try {
13449
+ git(`git merge-base --is-ancestor ${shellQuote(tree.branch ?? tree.sha)} main`);
13450
+ merged = true;
13451
+ }
13452
+ catch (error) {
13453
+ if (error.status === 1)
13454
+ merged = false;
13455
+ }
13456
+ let lastCommitTs = null;
13457
+ try {
13458
+ const raw = git('git log -1 --format=%ct', tree.path).trim();
13459
+ if (/^\d+$/.test(raw) && Number.isFinite(Number(raw) * 1000))
13460
+ lastCommitTs = Number(raw) * 1000;
13461
+ }
13462
+ catch { /* unknown age keeps the worktree */ }
13463
+ return { path: tree.path, branch: tree.branch?.replace(/^refs\/heads\//, '') ?? null,
13464
+ detached: tree.detached, isMain: index === 0 || tree.branch === 'refs/heads/main', merged, dirtyFiles, lastCommitTs };
13465
+ });
13466
+ const plan = planWorktreeCleanup(facts, { now, retentionMs });
13467
+ for (const line of renderCleanupPlan(plan))
13468
+ say(line);
13469
+ let code = 0;
13470
+ for (const fact of worktreeRemovalsToApply(plan, flags.has('apply'))) {
13471
+ let failure;
13472
+ try {
13473
+ git(`git worktree remove ${shellQuote(fact.path)}`);
13474
+ }
13475
+ catch (error) {
13476
+ failure = String(error);
13477
+ }
13478
+ try {
13479
+ if (!list().some(tree => tree.path === fact.path))
13480
+ say(`removed ${fact.path} (confirmed)`);
13481
+ else {
13482
+ say(`NOT removed ${fact.path}` + (failure ? ` — ${failure}` : ''));
13483
+ code = 1;
13484
+ }
13485
+ }
13486
+ catch (error) {
13487
+ say(`NOT removed ${fact.path} — confirmation unavailable: ${String(error)}`);
13488
+ code = 1;
13489
+ }
13490
+ }
13491
+ const archiveRegistry = () => {
13492
+ const registry = readRunRegistry(root, runRegistryIO);
13493
+ if (registry.status === 'inconclusive')
13494
+ throw new Error(registry.reason);
13495
+ const archivePlan = planRegistryArchive(registry, { now, retentionMs, probe: probePid });
13496
+ if (flags.has('apply') && archivePlan.archive.length > 0) {
13497
+ const dir = join(root, '.dz', 'runs');
13498
+ const encode = (events) => events.map(ev => JSON.stringify(ev) + '\n').join('');
13499
+ const temp = join(dir, `registry.${randomBytes(8).toString('hex')}.tmp`);
13500
+ try {
13501
+ writeFileSync(temp, encode(archivePlan.keep), { flag: 'wx', mode: 0o600 });
13502
+ appendFileSync(join(dir, 'registry.archive.jsonl'), encode(archivePlan.archive));
13503
+ renameSync(temp, join(dir, 'registry.jsonl'));
13504
+ }
13505
+ finally {
13506
+ if (existsSync(temp))
13507
+ unlinkSync(temp);
13508
+ }
13509
+ }
13510
+ return archivePlan;
13511
+ };
13512
+ const archive = flags.has('apply')
13513
+ ? withNamedLockSync(join(root, '.dz', 'runs'), 'run-registry-archive', archiveRegistry)
13514
+ : archiveRegistry();
13515
+ say(`${flags.has('apply') ? 'archived' : 'archive'} ${archive.archive.length} registry event(s)`);
13516
+ if (json)
13517
+ write(JSON.stringify({ ...plan, applied: flags.has('apply'), archive, messages, ok: code === 0 }));
13518
+ return code;
13519
+ }
13520
+ catch (error) {
13521
+ const reason = String(error);
13522
+ if (json)
13523
+ write(JSON.stringify({ ok: false, reason, messages }));
13524
+ else
13525
+ write('runs-clean refused: ' + reason);
13526
+ return 1;
13527
+ }
13528
+ }
13529
+ function cmdRunsRecord(options, flags, cwd, write) {
13530
+ try {
13531
+ const event = options.get('event');
13532
+ const runId = options.get('run-id') ?? (event === 'started' ? randomBytes(16).toString('hex') : '');
13533
+ const ev = { event, runId, ts: new Date().toISOString() };
13534
+ if (event === 'started') {
13535
+ const kind = options.get('kind');
13536
+ const slug = options.get('slug');
13537
+ Object.assign(ev, { ...(kind === undefined ? {} : { kind }), ...(slug === undefined ? {} : { slug }) });
13538
+ const rawPid = options.get('pid');
13539
+ if (rawPid === 'host') {
13540
+ // The shell agent is transient. Walk to the nearest Claude host, never guess from age.
13541
+ let candidate = process.ppid;
13542
+ const seen = new Set();
13543
+ while (candidate > 1 && !seen.has(candidate)) {
13544
+ seen.add(candidate);
13545
+ const row = execFileSync('ps', ['-p', String(candidate), '-o', 'ppid=', '-o', 'comm='], { encoding: 'utf8' }).trim();
13546
+ const match = /^(\d+)\s+(.+)$/.exec(row);
13547
+ if (!match)
13548
+ break;
13549
+ if (/^claude(?:$|[- ])/.test(basename(match[2]))) {
13550
+ ev.pid = candidate;
13551
+ break;
13552
+ }
13553
+ candidate = Number(match[1]);
13554
+ }
13555
+ if (ev.pid === undefined)
13556
+ throw new Error('Claude host PID unavailable; pass explicit --pid (Workflow args.runPid)');
13557
+ }
13558
+ else
13559
+ ev.pid = Number(rawPid);
13560
+ const parentRunId = options.get('parent-run-id');
13561
+ Object.assign(ev, { ...(parentRunId === undefined ? {} : { parentRunId }) });
13562
+ }
13563
+ const outcome = event === 'finished' ? options.get('outcome') : undefined;
13564
+ const reason = options.get('reason');
13565
+ Object.assign(ev, { ...(outcome === undefined ? {} : { outcome }), ...(reason === undefined ? {} : { reason }) });
13566
+ appendRunEvent(resolve(options.get('project') ?? cwd), ev, runRegistryIO);
13567
+ write(flags.has('json') ? JSON.stringify({ status: 'written', ...ev }) : `written ${event} ${runId}`);
13568
+ return 0;
13569
+ }
13570
+ catch (error) {
13571
+ write(flags.has('json') ? JSON.stringify({ status: 'refused', reason: String(error) }) : `refused: ${String(error)}`);
13572
+ return 2;
13573
+ }
13574
+ }
13262
13575
  function cmdFeatureAdrRecord(options, flags, cwd, write) {
13263
13576
  const json = flags.has('json');
13264
13577
  // `--backfill` is a different verb on the same store: it fills the ledger's null cost fields from
@@ -13444,15 +13757,28 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13444
13757
  catch {
13445
13758
  return {
13446
13759
  ok: false,
13760
+ reason: 'unreadable',
13447
13761
  diagnostic: { code: 'repository-unreadable', message: 'repository root cannot be resolved', artifact: '.' },
13448
13762
  };
13449
13763
  }
13764
+ try {
13765
+ lstatSync(absolute);
13766
+ }
13767
+ catch (error) {
13768
+ const missing = error.code === 'ENOENT';
13769
+ return {
13770
+ ok: false,
13771
+ reason: missing ? 'missing' : 'unreadable',
13772
+ diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory cannot be resolved: ${artifact}`, artifact },
13773
+ };
13774
+ }
13450
13775
  try {
13451
13776
  targetReal = realpathSync(absolute);
13452
13777
  }
13453
13778
  catch {
13454
13779
  return {
13455
13780
  ok: false,
13781
+ reason: 'unreadable',
13456
13782
  diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory cannot be resolved: ${artifact}`, artifact },
13457
13783
  };
13458
13784
  }
@@ -13460,6 +13786,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13460
13786
  if (rel === '' || rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) {
13461
13787
  return {
13462
13788
  ok: false,
13789
+ reason: 'unreadable',
13463
13790
  diagnostic: { code: 'artifact-outside-repository', message: `artifact resolves outside the repository: ${artifact}`, artifact },
13464
13791
  };
13465
13792
  }
@@ -13471,6 +13798,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13471
13798
  catch {
13472
13799
  return {
13473
13800
  ok: false,
13801
+ reason: 'unreadable',
13474
13802
  diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory is not readable: ${artifact}`, artifact },
13475
13803
  };
13476
13804
  }
@@ -13523,30 +13851,63 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13523
13851
  return emitEarly('not-established', 2, [requirementsRead.diagnostic]);
13524
13852
  const adrDirRel = `features/${slug}/03_adr`;
13525
13853
  const adrDirectory = contractDirectoryConfined(repoRoot, join(featureDir, '03_adr'), adrDirRel);
13526
- if (!adrDirectory.ok)
13527
- return emitEarly('not-established', 2, [adrDirectory.diagnostic]);
13528
- const adrDir = adrDirectory.realPath;
13854
+ const informationalDiagnostics = [];
13529
13855
  let adrNames;
13530
- try {
13531
- adrNames = readdirSync(adrDir, { withFileTypes: true })
13532
- .filter((entry) => entry.name.endsWith('.md') && (entry.isFile() || entry.isSymbolicLink()))
13533
- .map((entry) => entry.name)
13534
- .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
13535
- }
13536
- catch {
13537
- return emitEarly('not-established', 2, [{
13538
- code: 'adr-directory-unreadable',
13539
- message: `required ADR directory cannot be read: ${adrDirRel}`,
13856
+ let adrDir = null;
13857
+ let adrsOptional;
13858
+ if (!adrDirectory.ok) {
13859
+ if (adrDirectory.reason !== 'missing')
13860
+ return emitEarly('not-established', 2, [adrDirectory.diagnostic]);
13861
+ const tier = readFeatureTier((rel) => {
13862
+ const assessment = contractReadConfined(repoRoot, join(repoRoot, rel), rel);
13863
+ return assessment.ok ? assessment.text : null;
13864
+ }, slug);
13865
+ if (tier === 'S') {
13866
+ adrNames = [];
13867
+ adrsOptional = true;
13868
+ informationalDiagnostics.push({
13869
+ code: 'adr-not-required-for-tier',
13870
+ message: 'ADR artifacts are not required for an S-tier feature',
13540
13871
  artifact: adrDirRel,
13541
- }]);
13872
+ });
13873
+ }
13874
+ else if (tier !== null) {
13875
+ return emitEarly('not-established', 2, [{
13876
+ code: 'adr-directory-missing',
13877
+ message: `required ADR directory is missing for ${tier}-tier feature: ${adrDirRel}`,
13878
+ artifact: adrDirRel,
13879
+ }]);
13880
+ }
13881
+ else {
13882
+ return emitEarly('not-established', 2, [{
13883
+ ...adrDirectory.diagnostic,
13884
+ message: `${adrDirectory.diagnostic.message}; feature tier is unreadable`,
13885
+ }]);
13886
+ }
13542
13887
  }
13543
- if (adrNames.length === 0) {
13544
- return emitEarly('not-established', 2, [{
13545
- code: 'adr-artifacts-missing',
13546
- message: `no direct ADR Markdown artifacts exist under ${adrDirRel}`,
13547
- artifact: adrDirRel,
13548
- observed: 0,
13549
- }]);
13888
+ else {
13889
+ adrDir = adrDirectory.realPath;
13890
+ try {
13891
+ adrNames = readdirSync(adrDir, { withFileTypes: true })
13892
+ .filter((entry) => entry.name.endsWith('.md') && (entry.isFile() || entry.isSymbolicLink()))
13893
+ .map((entry) => entry.name)
13894
+ .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
13895
+ }
13896
+ catch {
13897
+ return emitEarly('not-established', 2, [{
13898
+ code: 'adr-directory-unreadable',
13899
+ message: `required ADR directory cannot be read: ${adrDirRel}`,
13900
+ artifact: adrDirRel,
13901
+ }]);
13902
+ }
13903
+ if (adrNames.length === 0) {
13904
+ return emitEarly('not-established', 2, [{
13905
+ code: 'adr-artifacts-missing',
13906
+ message: `no direct ADR Markdown artifacts exist under ${adrDirRel}`,
13907
+ artifact: adrDirRel,
13908
+ observed: 0,
13909
+ }]);
13910
+ }
13550
13911
  }
13551
13912
  const adrs = [];
13552
13913
  const adrReadErrors = [];
@@ -13563,6 +13924,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13563
13924
  const extracted = extractContractChecklist({
13564
13925
  requirements: { path: requirementsRel, text: requirementsRead.text },
13565
13926
  adrs,
13927
+ ...(adrsOptional === true ? { adrsOptional: true } : {}),
13566
13928
  });
13567
13929
  if (!extracted.ok)
13568
13930
  return emitEarly('fail', 1, extracted.diagnostics);
@@ -13606,7 +13968,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13606
13968
  });
13607
13969
  const verification = {
13608
13970
  ...rawVerification,
13609
- diagnostics: rawVerification.diagnostics.map(withReportArtifact),
13971
+ diagnostics: [...informationalDiagnostics, ...rawVerification.diagnostics.map(withReportArtifact)],
13610
13972
  items: rawVerification.items.map((item) => ({
13611
13973
  ...item,
13612
13974
  diagnostics: item.diagnostics.map(withReportArtifact),
@@ -13635,7 +13997,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13635
13997
  writeErr(contractDiagnosticLine(entry));
13636
13998
  const summary = verification.outcome === 'pass'
13637
13999
  ? `${verification.counts.met} contract item(s) met`
13638
- : `${verification.diagnostics.length} contract or evidence violation(s)`;
14000
+ : `${rawVerification.diagnostics.length} contract or evidence violation(s)`;
13639
14001
  write(`contract-check: ${verification.outcome === 'pass' ? 'PASS' : 'FAIL'} — ${summary}`);
13640
14002
  return verification.exitCode;
13641
14003
  }
@@ -17189,6 +17551,110 @@ async function cmdProfile(options, flags, write, writeErr) {
17189
17551
  }
17190
17552
  return fail(2, `dz profile: unknown subcommand ${JSON.stringify(sub)} — accepted: init | show | set | sync`);
17191
17553
  }
17554
+ function cmdJournal(options, flags, cwd, write, injectedIo) {
17555
+ try {
17556
+ const verb = options.get('_positional_0');
17557
+ if (verb !== 'add' && verb !== 'show')
17558
+ throw new Error('Ожидается journal add или journal show');
17559
+ const kindRaw = options.get('kind');
17560
+ if ((verb === 'add' || kindRaw !== undefined) && !JOURNAL_KINDS.includes(kindRaw)) {
17561
+ write(`Категория должна быть одной из: ${JOURNAL_KINDS.join(', ')}`);
17562
+ return 1;
17563
+ }
17564
+ if (flags.has('day') && flags.has('week'))
17565
+ throw new Error('Выберите --day или --week');
17566
+ const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
17567
+ const dir = join(root, 'docs', 'journal');
17568
+ // A journal symlink must never redirect an append or a quote outside this repository.
17569
+ for (const path of [join(root, 'docs'), dir, join(dir, 'quotes')]) {
17570
+ if (existsSync(path) && lstatSync(path).isSymbolicLink())
17571
+ throw new Error(`Символическая ссылка запрещена: ${path}`);
17572
+ }
17573
+ const at = options.get('at') ?? (verb === 'add' ? new Date().toISOString() : new Date().toISOString().slice(0, 10));
17574
+ let date = at;
17575
+ let time = '';
17576
+ if (verb === 'add') {
17577
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(at))
17578
+ throw new Error('--at ожидает ISO с часовым поясом');
17579
+ selectWindow([], at.slice(0, 10), false);
17580
+ const stamp = new Date(at).toISOString();
17581
+ date = stamp.slice(0, 10);
17582
+ time = stamp.slice(11, 16);
17583
+ }
17584
+ selectWindow([], date, false);
17585
+ if (verb === 'show') {
17586
+ const days = existsSync(dir) ? readdirSync(dir).filter(n => /^\d{4}-\d{2}-\d{2}\.md$/.test(n)).map(n => n.slice(0, 10)) : [];
17587
+ const rows = selectWindow(days, date, flags.has('week')).flatMap(day => {
17588
+ return readFileSync(join(dir, `${day}.md`), 'utf8').split('\n').filter(line => line.startsWith('- '))
17589
+ .map(raw => ({ day, ...parseLine(raw) }));
17590
+ }).filter(row => row.status === 'unparsed' || kindRaw === undefined || row.kind === kindRaw);
17591
+ if (flags.has('json'))
17592
+ write(JSON.stringify(rows));
17593
+ else
17594
+ for (const row of rows)
17595
+ write(row.status === 'unparsed' ? `[unparsed] ${row.raw}` : row.raw);
17596
+ return 0;
17597
+ }
17598
+ const text = options.get('_positional_1') ?? '';
17599
+ let ref = options.get('ref') ?? '';
17600
+ const event = { time, kind: kindRaw, text, ref };
17601
+ formatLine(event); // validate before any journal/quote write
17602
+ if (options.has('_positional_2'))
17603
+ throw new Error('Текст должен быть одним аргументом');
17604
+ if (flags.has('commit-quote') && !options.has('quote'))
17605
+ throw new Error('--commit-quote требует --quote');
17606
+ return withNamedLockSync(root, 'journal', () => {
17607
+ const path = join(dir, `${date}.md`);
17608
+ if (existsSync(path) && lstatSync(path).isSymbolicLink())
17609
+ throw new Error(`Символическая ссылка запрещена: ${path}`);
17610
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
17611
+ if (previous.split('\n').map(parseLine).some(row => row.status === 'parsed' && row.time === time && row.text === text.trim())) {
17612
+ write('уже записано');
17613
+ return 0;
17614
+ }
17615
+ const quote = options.get('quote');
17616
+ if (quote !== undefined) {
17617
+ const content = readFileSync(resolve(cwd, quote), 'utf8');
17618
+ const quotes = join(dir, 'quotes');
17619
+ const exclude = execFileSync('git', ['rev-parse', '--git-path', 'info/exclude'], { cwd: root, encoding: 'utf8' }).trim();
17620
+ const excludePath = resolve(root, exclude);
17621
+ const ignored = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
17622
+ if (!ignored.split('\n').includes('/docs/journal/quotes/')) {
17623
+ mkdirSync(dirname(excludePath), { recursive: true });
17624
+ appendFileSync(excludePath, `${ignored.endsWith('\n') || !ignored ? '' : '\n'}/docs/journal/quotes/\n`);
17625
+ }
17626
+ mkdirSync(quotes, { recursive: true });
17627
+ const tracked = new Set(execFileSync('git', ['ls-files', '-z', '--', 'docs/journal/quotes/'], { cwd: root, encoding: 'utf8' }).split('\0'));
17628
+ let n = 1;
17629
+ while (existsSync(join(quotes, `${date}-${n}.md`)) || tracked.has(`docs/journal/quotes/${date}-${n}.md`))
17630
+ n++;
17631
+ const quoteRef = `docs/journal/quotes/${date}-${n}.md`;
17632
+ const quotePath = join(root, quoteRef);
17633
+ writeFileSync(quotePath, content, { flag: 'wx' });
17634
+ if (readFileSync(quotePath, 'utf8') !== content)
17635
+ throw new Error('Цитата не засвидетельствована');
17636
+ if (flags.has('commit-quote')) {
17637
+ execFileSync('git', ['add', '-f', '--', quoteRef], { cwd: root, stdio: 'pipe' });
17638
+ }
17639
+ ref = ref ? `${ref}; ${quoteRef}` : quoteRef;
17640
+ }
17641
+ mkdirSync(dir, { recursive: true });
17642
+ if (!existsSync(path))
17643
+ writeFileSync(path, `# Журнал действий — ${date}\n\n## Решения · вердикты · запуски и падения · ошибки · блокировки (UTC)\n\n`, { flag: 'wx' });
17644
+ else if (previous && !previous.endsWith('\n'))
17645
+ appendFileSync(path, '\n');
17646
+ const fs = injectedIo ?? { read: p => readFileSync(p, 'utf8'), append: (p, s) => appendFileSync(p, s) };
17647
+ const line = formatLine({ ...event, ref });
17648
+ appendWitnessed(fs, path, line);
17649
+ write(`записано и засвидетельствовано: ${line}`);
17650
+ return 0;
17651
+ });
17652
+ }
17653
+ catch (error) {
17654
+ write(`dz journal: ${error instanceof Error ? error.message : String(error)}`);
17655
+ return 1;
17656
+ }
17657
+ }
17192
17658
  export async function runCli(argv, io = {}) {
17193
17659
  const cwd = io.cwd ?? process.cwd();
17194
17660
  const write = io.write ?? ((line) => { console.log(line); });
@@ -17439,8 +17905,16 @@ export async function runCli(argv, io = {}) {
17439
17905
  return cmdBriefCheck(options, flags, cwd, write);
17440
17906
  case 'provenance-check':
17441
17907
  return cmdProvenanceCheck(options, flags, cwd, write);
17908
+ case 'journal':
17909
+ return cmdJournal(options, flags, cwd, write, io.journalIo);
17442
17910
  case 'feature-adr-record':
17443
17911
  return cmdFeatureAdrRecord(options, flags, cwd, write);
17912
+ case 'runs':
17913
+ return cmdRuns(options, flags, cwd, write);
17914
+ case 'runs-clean':
17915
+ return cmdRunsClean(options, flags, cwd, write);
17916
+ case 'runs-record':
17917
+ return cmdRunsRecord(options, flags, cwd, write);
17444
17918
  case 'amendment-check':
17445
17919
  return cmdAmendmentCheck(options, flags, cwd, write);
17446
17920
  case 'contract-check':