@dzhechkov/harness-cli 0.8.16 → 0.8.18

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)
@@ -3187,9 +3193,14 @@ async function cmdStoreGuard(options, flags, cwd, write, writeErr, stdinText, in
3187
3193
  return 1;
3188
3194
  }
3189
3195
  }
3190
- async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
3196
+ async function runTeachGuardReinforcement(projectRoot, dzId, reward, preserveQuarantine = false) {
3191
3197
  const backend = resolveLearningBackend(projectRoot);
3192
- 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
+ });
3193
3204
  return backend.train();
3194
3205
  }
3195
3206
  async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
@@ -3438,6 +3449,15 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
3438
3449
  }
3439
3450
  }
3440
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
+ }
3441
3461
  // Distill pattern into actionable rule (claude-smart inspired)
3442
3462
  // Convert "what happened" into "what to do next time"
3443
3463
  const isRule = pattern.toLowerCase().startsWith('always') || pattern.toLowerCase().startsWith('never') ||
@@ -6396,11 +6416,19 @@ function cmdPublish(options, flags, cwd, write) {
6396
6416
  // A one-line preview inline; on error the FULL captured npm error prints below (truncating a publish
6397
6417
  // failure to 60 chars is how a release stays undiagnosable — the reason lives past char 60).
6398
6418
  const detail = pkg.error && pkg.status !== 'error' ? ` (${pkg.error.slice(0, 60)})` : '';
6399
- 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}`);
6400
6423
  if (pkg.status === 'error' && pkg.error) {
6401
6424
  for (const line of pkg.error.split('\n'))
6402
6425
  write(` ${line}`);
6403
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
+ }
6404
6432
  // Surface warn-mode findings that did not block the publish.
6405
6433
  if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
6406
6434
  write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
@@ -13292,6 +13320,258 @@ function cmdLedgerBackfill(options, flags, cwd, write) {
13292
13320
  write(` not filled: ${n} row(s) — ${reason}`);
13293
13321
  return 0;
13294
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
+ }
13295
13575
  function cmdFeatureAdrRecord(options, flags, cwd, write) {
13296
13576
  const json = flags.has('json');
13297
13577
  // `--backfill` is a different verb on the same store: it fills the ledger's null cost fields from
@@ -13477,15 +13757,28 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13477
13757
  catch {
13478
13758
  return {
13479
13759
  ok: false,
13760
+ reason: 'unreadable',
13480
13761
  diagnostic: { code: 'repository-unreadable', message: 'repository root cannot be resolved', artifact: '.' },
13481
13762
  };
13482
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
+ }
13483
13775
  try {
13484
13776
  targetReal = realpathSync(absolute);
13485
13777
  }
13486
13778
  catch {
13487
13779
  return {
13488
13780
  ok: false,
13781
+ reason: 'unreadable',
13489
13782
  diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory cannot be resolved: ${artifact}`, artifact },
13490
13783
  };
13491
13784
  }
@@ -13493,6 +13786,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13493
13786
  if (rel === '' || rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) {
13494
13787
  return {
13495
13788
  ok: false,
13789
+ reason: 'unreadable',
13496
13790
  diagnostic: { code: 'artifact-outside-repository', message: `artifact resolves outside the repository: ${artifact}`, artifact },
13497
13791
  };
13498
13792
  }
@@ -13504,6 +13798,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
13504
13798
  catch {
13505
13799
  return {
13506
13800
  ok: false,
13801
+ reason: 'unreadable',
13507
13802
  diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory is not readable: ${artifact}`, artifact },
13508
13803
  };
13509
13804
  }
@@ -13556,30 +13851,63 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13556
13851
  return emitEarly('not-established', 2, [requirementsRead.diagnostic]);
13557
13852
  const adrDirRel = `features/${slug}/03_adr`;
13558
13853
  const adrDirectory = contractDirectoryConfined(repoRoot, join(featureDir, '03_adr'), adrDirRel);
13559
- if (!adrDirectory.ok)
13560
- return emitEarly('not-established', 2, [adrDirectory.diagnostic]);
13561
- const adrDir = adrDirectory.realPath;
13854
+ const informationalDiagnostics = [];
13562
13855
  let adrNames;
13563
- try {
13564
- adrNames = readdirSync(adrDir, { withFileTypes: true })
13565
- .filter((entry) => entry.name.endsWith('.md') && (entry.isFile() || entry.isSymbolicLink()))
13566
- .map((entry) => entry.name)
13567
- .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
13568
- }
13569
- catch {
13570
- return emitEarly('not-established', 2, [{
13571
- code: 'adr-directory-unreadable',
13572
- 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',
13573
13871
  artifact: adrDirRel,
13574
- }]);
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
+ }
13575
13887
  }
13576
- if (adrNames.length === 0) {
13577
- return emitEarly('not-established', 2, [{
13578
- code: 'adr-artifacts-missing',
13579
- message: `no direct ADR Markdown artifacts exist under ${adrDirRel}`,
13580
- artifact: adrDirRel,
13581
- observed: 0,
13582
- }]);
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
+ }
13583
13911
  }
13584
13912
  const adrs = [];
13585
13913
  const adrReadErrors = [];
@@ -13596,6 +13924,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13596
13924
  const extracted = extractContractChecklist({
13597
13925
  requirements: { path: requirementsRel, text: requirementsRead.text },
13598
13926
  adrs,
13927
+ ...(adrsOptional === true ? { adrsOptional: true } : {}),
13599
13928
  });
13600
13929
  if (!extracted.ok)
13601
13930
  return emitEarly('fail', 1, extracted.diagnostics);
@@ -13639,7 +13968,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13639
13968
  });
13640
13969
  const verification = {
13641
13970
  ...rawVerification,
13642
- diagnostics: rawVerification.diagnostics.map(withReportArtifact),
13971
+ diagnostics: [...informationalDiagnostics, ...rawVerification.diagnostics.map(withReportArtifact)],
13643
13972
  items: rawVerification.items.map((item) => ({
13644
13973
  ...item,
13645
13974
  diagnostics: item.diagnostics.map(withReportArtifact),
@@ -13668,7 +13997,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
13668
13997
  writeErr(contractDiagnosticLine(entry));
13669
13998
  const summary = verification.outcome === 'pass'
13670
13999
  ? `${verification.counts.met} contract item(s) met`
13671
- : `${verification.diagnostics.length} contract or evidence violation(s)`;
14000
+ : `${rawVerification.diagnostics.length} contract or evidence violation(s)`;
13672
14001
  write(`contract-check: ${verification.outcome === 'pass' ? 'PASS' : 'FAIL'} — ${summary}`);
13673
14002
  return verification.exitCode;
13674
14003
  }
@@ -17222,6 +17551,110 @@ async function cmdProfile(options, flags, write, writeErr) {
17222
17551
  }
17223
17552
  return fail(2, `dz profile: unknown subcommand ${JSON.stringify(sub)} — accepted: init | show | set | sync`);
17224
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
+ }
17225
17658
  export async function runCli(argv, io = {}) {
17226
17659
  const cwd = io.cwd ?? process.cwd();
17227
17660
  const write = io.write ?? ((line) => { console.log(line); });
@@ -17472,8 +17905,16 @@ export async function runCli(argv, io = {}) {
17472
17905
  return cmdBriefCheck(options, flags, cwd, write);
17473
17906
  case 'provenance-check':
17474
17907
  return cmdProvenanceCheck(options, flags, cwd, write);
17908
+ case 'journal':
17909
+ return cmdJournal(options, flags, cwd, write, io.journalIo);
17475
17910
  case 'feature-adr-record':
17476
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);
17477
17918
  case 'amendment-check':
17478
17919
  return cmdAmendmentCheck(options, flags, cwd, write);
17479
17920
  case 'contract-check':