@dzhechkov/harness-cli 0.8.10 → 0.8.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +42 -22
- package/LICENSE +13 -0
- package/README.md +312 -17
- package/dist/boolean-flags.d.ts.map +1 -1
- package/dist/boolean-flags.js +3 -0
- package/dist/boolean-flags.js.map +1 -1
- package/dist/cli.d.ts +33 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1078 -88
- package/dist/cli.js.map +1 -1
- package/dist/command-inventory.d.ts +149 -0
- package/dist/command-inventory.d.ts.map +1 -0
- package/dist/command-inventory.js +405 -0
- package/dist/command-inventory.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +5 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +15 -14
- package/sbom.json +71 -21
- package/src/boolean-flags.ts +3 -0
- package/src/cli.ts +1071 -80
- package/src/command-inventory.ts +342 -0
- package/src/index.ts +8 -0
- package/src/known-flags.ts +5 -0
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { request as httpsRequest } from 'node:https';
|
|
|
10
10
|
import { KNOWN_CLI_FLAGS } from './known-flags.js';
|
|
11
11
|
import { isBooleanFlag } from './boolean-flags.js';
|
|
12
12
|
import { resolveInstallSpec } from './install-spec.js';
|
|
13
|
+
import { dispatchedCommands, documentedCommands } from './command-inventory.js';
|
|
13
14
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
|
|
14
15
|
import { createHash, randomBytes } from 'node:crypto';
|
|
15
16
|
import { homedir, hostname, tmpdir } from 'node:os';
|
|
@@ -17,9 +18,9 @@ import { createRequire } from 'node:module';
|
|
|
17
18
|
import { isDeepStrictEqual } from 'node:util';
|
|
18
19
|
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,
|
|
19
20
|
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
|
|
20
|
-
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, statuslineData, writeFeatureAdrState, 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, 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, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
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,
|
|
21
22
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
22
|
-
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, 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,
|
|
23
|
+
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,
|
|
23
24
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
24
25
|
replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, scoreReceiptToAggregateRow, readScoreAggregateRows, dedupeScoreAggregateRows, buildScoreAggregateReport, renderScoreAggregateReport, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
|
|
25
26
|
// Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
|
|
@@ -27,16 +28,39 @@ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGo
|
|
|
27
28
|
// qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
|
|
28
29
|
KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
|
|
29
30
|
// Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
|
|
30
|
-
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, 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,
|
|
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,
|
|
31
32
|
// operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
|
|
32
33
|
renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
|
|
33
34
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
34
35
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
36
|
+
/**
|
|
37
|
+
* Область, по которой гейт дрейфа собирает факты. ПОЛНАЯ, а не только корни установки.
|
|
38
|
+
*
|
|
39
|
+
* ПОЧЕМУ. Под областью `installs` сравниваются лишь корни установки (`.claude/skills`,
|
|
40
|
+
* `.agents/skills` и далее). Навык, чьи копии лежат в РАЗНЫХ корнях — канон в `packages/`, живая
|
|
41
|
+
* копия в `.claude/skills` — имеет там ОДНУ копию, а одну копию не с чем сравнивать: она
|
|
42
|
+
* отбрасывается как не дублированная. То есть главный класс расхождения был для гейта невидим.
|
|
43
|
+
*
|
|
44
|
+
* ИЗМЕРЕНО 2026-09-03: `brutal-honesty-review` разошёлся ровно так (канон в skills-qe, копия в
|
|
45
|
+
* бандле p-replicator, живая в .claude/skills), и гейт не мог увидеть это В ПРИНЦИПЕ. Я тогда
|
|
46
|
+
* написал в отчёте «гейт разблокирован» — он никогда не был на этом заблокирован.
|
|
47
|
+
*
|
|
48
|
+
* Узкая область давала 19 дублирующихся навыков, полная даёт 211. Безопасность расширения
|
|
49
|
+
* проверена ДО правки: с полной областью и списком исключений дрейфа сегодня НОЛЬ.
|
|
50
|
+
*/
|
|
51
|
+
const DRIFT_SWEEP_SCOPE = 'all';
|
|
52
|
+
/**
|
|
53
|
+
* Базовая дата правила `backlog-covers-features`. Каталоги фич, заведённые ДО неё, правило не
|
|
54
|
+
* трогает: они появились раньше самого правила. ИЗМЕРЕНО 2026-09-03 — без базы правило даёт 236
|
|
55
|
+
* нарушений из 336 каталогов, и проверка, изобретающая полсотни нарушений в первый день, учит
|
|
56
|
+
* людей себя игнорировать. Дата = день, когда правило принято владельцем.
|
|
57
|
+
*/
|
|
58
|
+
const BACKLOG_COVERAGE_BASELINE = '2026-09-03';
|
|
35
59
|
/** Literal command inventory, pinned against the main dispatch switch by a layer-1 test. */
|
|
36
60
|
export const DZ_COMMANDS = [
|
|
37
61
|
'init', 'verify', 'sync', 'update', 'list', 'create-skill', 'info', 'scout',
|
|
38
62
|
'workflow', 'workflow-lint', 'workflow-trace', 'migrate', 'doctor', 'install',
|
|
39
|
-
'bundle', 'teach', 'consolidate', 'recall', 'vector', 'brain', 'statusline',
|
|
63
|
+
'bundle', 'teach', 'consolidate', 'recall', 'vector', 'brain', 'statusline', 'store-guard',
|
|
40
64
|
'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
|
|
41
65
|
'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
|
|
42
66
|
'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
|
|
@@ -45,7 +69,7 @@ export const DZ_COMMANDS = [
|
|
|
45
69
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
46
70
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
47
71
|
'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
|
|
48
|
-
'name-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
|
|
72
|
+
'name-check', 'brief-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
|
|
49
73
|
'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
|
|
50
74
|
'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
|
|
51
75
|
];
|
|
@@ -95,13 +119,14 @@ Usage:
|
|
|
95
119
|
dz restart-advisor --slug <s> [--threshold C|D] [--rounds N] [--json] (read-only advisory decision over features/<slug>/.fa-state/checkpoints.jsonl and .dz/fa-training/<slug>/qe.jsonl. Defaults: threshold D, rounds 2 — both origins are printed. Equal sources corroborate; conflicts, torn/unreadable evidence, gaps, and unsafe paths are NOT ESTABLISHED. RECOMMENDATION ONLY: autoAction=false; never invokes feature-adr, deletes a stage, or writes advisor state. exit 0 established recommendation/no-recommendation / 2 NOT ESTABLISHED or invalid input / 1 unexpected runtime failure)
|
|
96
120
|
dz tg-post --draft <file.html> [--manifest <sources.json>] [--channel <@name|id>] [--send --yes] [--night] [--preview] [--json] (the sender for an APPROVED channel post, per the accepted genai-tweets-channel ADRs: HTML mode only — never MarkdownV2; link preview OFF by default (x.com previews in Telegram are broken); the 00:00-06:00 MSK quiet window refuses without an explicit --night. DEFAULT IS A DRY-RUN: it validates the draft (tag balance, allowed tags, bare &/<, the 4096 visible-character limit with the overshoot counted) and runs the provenance gate over --manifest IN-PROCESS — a draft with no manifest is refused as unchecked, and anything but ALLOWED refuses. A real send needs --send --yes, stating ADR-004's manual-publishing decision out loud each time. The token comes from TELEGRAM_BOT_TOKEN or telegram.tokenFile in .dz/config.json and is never printed. exit 0 sent or clean dry-run / 1 refused or Telegram error / 2 usage)
|
|
97
121
|
dz name-check [--command <n>] [--module <basename>] [--export <a,b>] [--project <dir>] [--json] (is this name free, BEFORE a line of code? Scans workspace SOURCE — never dist, because a stale build answers 'free' confidently. Checks a dz command name against the dispatcher AND the help block, a module basename against every package's src/, and exported identifiers against every declaration in the workspace. exit 0 all free / 1 at least one taken, naming where / 2 nothing asked or the scan did not run — an empty sweep is never a clean bill. Honest limit, printed on the passing path: it reads declarations, so a re-export under a different name stays the build's job)
|
|
122
|
+
dz brief-check <file> [--json] (does a swarm brief declare OUTPUT_DIR, UNITS and ASSEMBLY_UNIT? parsed as DATA, refused by name; verifies the brief DECLARED the contract, not that the agent follows it. exit 0 ok / 1 refused / 2 unreadable)
|
|
98
123
|
dz provenance-check --manifest <sources.json> [--project <dir>] [--json] (nothing goes out citing a source that may not leave this machine. Checks PROVENANCE, not words: every claim names its source, and only a KNOWN kind that resolves safely is cleared. Repo paths go through 'git -C <root> check-ignore' over the RESOLVED path — a symlink into an ignored directory is REFUSED (git classifies the string and never dereferences, MEASURED), and the verdict does not change with your working directory. Store records must be named in the git-TRACKED provenance-public.json, so declaring one public is a reviewable commit rather than a field inside an ignored store. An undeclared kind is refused, never inferred from the path's shape. exit 0 allowed / 1 blocked / 3 NOT ESTABLISHED — an empty manifest, an unreadable one, or an oracle that did not run is never a pass. It proves what was CITED: it cannot see a paraphrase with no citation, nor confidential text pasted by hand into an allowed file)
|
|
99
124
|
dz project-skills [--project <dir>] [--json] [--stages-json] (polymorphic feature-adr: resolve architecture/project-skills.json — fixed roles product-vision/critic/brand/impl-bar plus an open extra[] — into per-stage guidance. READ-ONLY. --project names the root explicitly, so it works from any cwd; without it the manifest is read from the current repo. No manifest ⇒ a byte-identical generic run)
|
|
100
125
|
dz discrimination-check --slug <slug> [--base <ref>] [--json] (does the ADR's named test actually DISCRIMINATE? Re-runs it on a worktree at the pre-feature commit, where it MUST go red. A test that passes with the feature removed proves nothing; dz amendment-check proves the test exists, this proves it bites)
|
|
101
126
|
dz guard [check|promote|init] [--json] [--force] (HARD/SOFT repo rules — readme-first, lockfile-in-sync, claim tagging — run automatically as a pre-flight inside dz publish. HARD blocks, SOFT warns)
|
|
102
127
|
dz architecture [--check --slug <s> --desc <text>] [--project <dir>] [--revise] (the live product map + vision: --check is the soft Step-0 сверка of a new feature against them, reporting {signal,confidence} rather than blocking)
|
|
103
128
|
dz sbom [--pack <name>] [--out <file>] (CycloneDX software bill of materials for the workspace, or for one pack with --pack)
|
|
104
|
-
dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N row must resolve to a test found INSIDE the file the row names; 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. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
|
|
129
|
+
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)
|
|
105
130
|
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)
|
|
106
131
|
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)
|
|
107
132
|
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)
|
|
@@ -109,7 +134,7 @@ Usage:
|
|
|
109
134
|
dz reqe [--slug <feature> [--done --report <f>]] [--json] (the re-QE debt ledger: a usage-switched run whose Step-8 QE ran on the coder's OWN family records a debt; list debts, print the cross-family review brief, settle FAIL-CLOSED against a graded report — the settlement lands in 08_qe_report.md)
|
|
110
135
|
dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 17 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
|
|
111
136
|
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
|
|
112
|
-
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
137
|
+
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--allow-cold-start] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
113
138
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
114
139
|
dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
|
|
115
140
|
dz backlog goals [--validate] [--project <dir>] [--json] (list/validate the compass at .dz/backlog/goals.json)
|
|
@@ -123,7 +148,7 @@ Usage:
|
|
|
123
148
|
dz backlog jira <id> [--project <dir>] [--json] (draft a Jira issue via the configurable adapter (backlog.jira.adapter: jira-mcp|copilot-mcp|none); none writes an auditable jira-outbox/<id>.json stub)
|
|
124
149
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
125
150
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--no-verify] [--install-driver] [--force] [--enrich] (--target codex ALSO installs + LIVE-verifies the codex hooks; an unverified hook exits non-zero WITHOUT aborting the rest of setup)
|
|
126
|
-
dz teach "<pattern>" [--class-form "<template with :slot>"] [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (class form is optional; rejection never blocks the specific write; --project pins the learned store to <dir>/.dz)
|
|
151
|
+
dz teach "<pattern>" [--class-form "<template with :slot>"] [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] [--allow-cold-start] (class form is optional; rejection never blocks the specific write; --project pins the learned store to <dir>/.dz)
|
|
127
152
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
128
153
|
dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] [--prune-quarantine [--apply]] (both prunes: DRY-RUN by default; --apply snapshots then deletes; prune-quarantine = expired unproven lessons ONLY, never coupled to noise)
|
|
129
154
|
dz recall "<query>" [--limit <N>] [--domain <name>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] | dz recall --promote <dzId>[,<dzId>] [--apply] (--domain <name> BOOSTS lessons of that domain without dropping foreign ones — a shared store keeps its cross-domain transfers; forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
|
|
@@ -143,7 +168,8 @@ Usage:
|
|
|
143
168
|
dz brain expand <kuId> [--source <slug>] [--json] (full-content lookup for a citation kuId; --json emits the full KU object)
|
|
144
169
|
dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
|
|
145
170
|
dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
|
|
146
|
-
dz
|
|
171
|
+
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>] [--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)
|
|
147
173
|
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)
|
|
148
174
|
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)
|
|
149
175
|
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)
|
|
@@ -171,6 +197,10 @@ Usage:
|
|
|
171
197
|
dz dashboard
|
|
172
198
|
dz roam [--apply] [--slug <slug>]
|
|
173
199
|
dz import-ecc [--local-path <dir>] [--select id,id,...] [--limit N] [--output <dir>] [--force]
|
|
200
|
+
dz retro [transcript-path] [--json] [--threshold N] [--no-teach] [--project <dir>] [--install-hook] (per-session retrospective + co-learning: mines the session transcript for recurring PROCESS rakes, drills you, and teaches the same lesson to the store)
|
|
201
|
+
dz feature-adr-setup [--plan] [--from-spec <spec.json>] [--guards [--loc-cap <n>]] [--gates [--target <name>]] [--apply] [--json] (scaffold the project-awareness files feature-adr reads — vision / map / testing / project-skills — plus the deterministic project guards and the portable delivery gates; --guards and --gates work STANDALONE or with --from-spec, and --apply writes for all three; without --apply everything is a preview)
|
|
202
|
+
dz mr-rakes [--json] [--candidate N] [--confirmed N] [--teach] [--gen-critic <path> [--apply]] (experimental: mine the review corpus — features' QE reports + REVIEW files — for RECURRING mistakes and close them into self-learning)
|
|
203
|
+
dz bto-optimize --split | --plan | --select | --scope-check | --diff [--json] (experimental: deterministic tune/holdout split, budget plan and holdout-no-regress winner selection behind the /bto-optimize skill)
|
|
174
204
|
dz help
|
|
175
205
|
|
|
176
206
|
Global: --version | -v [--json] (prints this CLI's own semver on one line, exit 0; "unknown" + exit 1 when unresolvable)
|
|
@@ -749,7 +779,7 @@ async function cmdScout(options, flags, cwd, write) {
|
|
|
749
779
|
write('Scanning 9 sources (GitHub + npm + HN + MCP Registry + Glama + OSSInsight + Smithery + Semantic Scholar + arXiv)...\n');
|
|
750
780
|
try {
|
|
751
781
|
const scanTopics = topicsArg ? topicsArg.split(',').map((t) => t.trim()) : undefined;
|
|
752
|
-
const { results: repos, totalBySource } = await scanAllSources({
|
|
782
|
+
const { results: repos, totalBySource, statusBySource } = await scanAllSources({
|
|
753
783
|
token,
|
|
754
784
|
topics: scanTopics,
|
|
755
785
|
since,
|
|
@@ -761,10 +791,17 @@ async function cmdScout(options, flags, cwd, write) {
|
|
|
761
791
|
.map(([src, count]) => `${src}: ${count}`)
|
|
762
792
|
.join(', ');
|
|
763
793
|
write(`Sources: ${sourceLines}`);
|
|
764
|
-
// Memory: diff with previous scan
|
|
794
|
+
// Memory: diff with previous scan.
|
|
795
|
+
//
|
|
796
|
+
// СОСТОЯНИЕ ИСТОЧНИКОВ ПЕРЕДАЁТСЯ ОБЯЗАТЕЛЬНО. Без него разность не выводит исчезновений
|
|
797
|
+
// вообще — и это правильно: источник, ответивший кодом ошибки, раньше делал ВСЕ свои записи
|
|
798
|
+
// «пропавшими» на экране, то есть отчёт печатал факт о нашей сети как факт о мире.
|
|
765
799
|
if (showDiff || memory.size > 0) {
|
|
766
|
-
const
|
|
767
|
-
|
|
800
|
+
const health = {};
|
|
801
|
+
for (const [source, status] of Object.entries(statusBySource))
|
|
802
|
+
health[source] = status.health;
|
|
803
|
+
const diff = memory.diff(repos, health);
|
|
804
|
+
if (diff.newRepos.length > 0 || diff.goneRepos.length > 0 || diff.changedScore.length > 0 || diff.goneOmittedReason !== undefined) {
|
|
768
805
|
write(memory.diffMarkdown(diff));
|
|
769
806
|
}
|
|
770
807
|
else if (memory.size > 0) {
|
|
@@ -1591,6 +1628,47 @@ function cmdBundle(options, flags, cwd, write) {
|
|
|
1591
1628
|
}
|
|
1592
1629
|
return 0;
|
|
1593
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Команда npm для установки пакета В ЦЕЛЕВОЙ КАТАЛОГ, а не куда решит npm.
|
|
1633
|
+
*
|
|
1634
|
+
* ЗАЧЕМ `--prefix`. Без него npm при отсутствии `package.json` в текущем каталоге поднимается по
|
|
1635
|
+
* дереву до первого найденного и мутирует ЕГО — а `dz` потом ищет пакет в
|
|
1636
|
+
* `<цель>/node_modules` и не находит. Место установки и место проверки были двумя независимыми
|
|
1637
|
+
* предположениями, и совпадали они только по удаче.
|
|
1638
|
+
*
|
|
1639
|
+
* ИЗМЕРЕНО 2026-09-03 (полевой случай владельца): установка в каталог без `package.json`
|
|
1640
|
+
* записала в `/home`, где лежит ЧУЖОЙ проект; ручной откат вернул `package.json`, а запись
|
|
1641
|
+
* `extraneous` в `/home/package-lock.json` пережила откат.
|
|
1642
|
+
*
|
|
1643
|
+
* ПОЧЕМУ НЕ ОТКАЗ (ADR-001, вариант A отвергнут). Отказ запретил бы законный сценарий: проект
|
|
1644
|
+
* внутри монорепо, намеренно не имеющий своего `package.json` и опирающийся на родительский
|
|
1645
|
+
* воркспейс. `--prefix` согласует установку с проверкой ПО ПОСТРОЕНИЮ и сценарий сохраняет.
|
|
1646
|
+
*
|
|
1647
|
+
* ЧИСТАЯ: ни файловой системы, ни запуска npm — проверяется без обоих. Путь экранируется, потому
|
|
1648
|
+
* что каталоги с пробелом в имени встречаются в наших же тестах.
|
|
1649
|
+
*/
|
|
1650
|
+
export function buildInstallArgs(npmSpec, projectRoot) {
|
|
1651
|
+
return ['install', npmSpec, '--prefix', projectRoot, '--save-dev', '--no-fund', '--no-audit'];
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Та же команда СТРОКОЙ — только для показа человеку и для тестового шва.
|
|
1655
|
+
*
|
|
1656
|
+
* НЕ ДЛЯ ИСПОЛНЕНИЯ, и это не стилистическая оговорка. `JSON.stringify` НЕ является экранированием
|
|
1657
|
+
* для оболочки: внутри двойных кавычек оболочка по-прежнему выполняет `$(...)` и обратные кавычки.
|
|
1658
|
+
* ИЗМЕРЕНО 2026-09-03 — `execSync('echo ' + JSON.stringify('pkg$(touch ФАЙЛ)'))` создал файл.
|
|
1659
|
+
* Прежняя редакция этого комментария утверждала «путь экранируется»; это было неверно, и находку
|
|
1660
|
+
* предъявило кросс-семейное ревью (gpt-5.6-sol), а я подтвердил её пробой.
|
|
1661
|
+
*
|
|
1662
|
+
* Боевой путь исполняется через `execFileSync` массивом аргументов — оболочки в цепочке нет вовсе,
|
|
1663
|
+
* поэтому подставлять некуда. Это структурное лечение, а не более хитрое экранирование.
|
|
1664
|
+
*/
|
|
1665
|
+
export function buildInstallCommand(npmSpec, projectRoot) {
|
|
1666
|
+
// ЗНАЧЕНИЯ в кавычках, ФЛАГИ без — та же форма, что печаталась до этой фичи, чтобы читатель
|
|
1667
|
+
// (и закреплённые тесты) видели знакомую строку. Кавычки здесь — ЧИТАЕМОСТЬ, а не безопасность:
|
|
1668
|
+
// безопасность даёт отсутствие оболочки на боевом пути.
|
|
1669
|
+
return `npm install ${JSON.stringify(npmSpec)} --prefix ${JSON.stringify(projectRoot)}`
|
|
1670
|
+
+ ' --save-dev --no-fund --no-audit';
|
|
1671
|
+
}
|
|
1594
1672
|
async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
1595
1673
|
const pkg = options.get('_positional_0');
|
|
1596
1674
|
if (!pkg) {
|
|
@@ -1649,17 +1727,38 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
|
1649
1727
|
if (targetResolution.via === 'alias')
|
|
1650
1728
|
writeErr(formatTargetAliasNote('dz install', targetOpt, target));
|
|
1651
1729
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1730
|
+
// ПРЕДПОСЫЛКА НАЗЫВАЕТСЯ ДО ПОБОЧНОГО ЭФФЕКТА, А НЕ ПОСЛЕ (ADR-001, FR-2).
|
|
1731
|
+
//
|
|
1732
|
+
// Проверка стоит ЗДЕСЬ, до развилки installRunner/execSync, и это не стилистика. Поставить её
|
|
1733
|
+
// внутрь ветки execSync значило бы оставить боевой путь непокрытым при зелёных тестах — ровно
|
|
1734
|
+
// то состояние, из которого фича и родилась.
|
|
1735
|
+
const hasOwnManifest = existsSync(join(projectRoot, 'package.json'));
|
|
1736
|
+
if (!hasOwnManifest) {
|
|
1737
|
+
write(`dz install: ${projectRoot} — не npm-проект (нет своего package.json).`);
|
|
1738
|
+
write(` Ставлю ЛОКАЛЬНО в него: npm получит --prefix, package.json и node_modules появятся здесь.`);
|
|
1739
|
+
write(` Без --prefix npm поднялся бы по дереву и записал в ЧУЖОЙ проект выше — измерено 2026-09-03.`);
|
|
1740
|
+
}
|
|
1652
1741
|
// Step 1: npm install the package (installRunner is the CliIo test seam — unset in production)
|
|
1653
1742
|
write(`Installing ${specResolution.npmSpec}${specResolution.kind === 'name' ? '' : ` (${specResolution.kind} → node_modules/${specResolution.dirName})`}...`);
|
|
1654
|
-
const installCmd =
|
|
1743
|
+
const installCmd = buildInstallCommand(specResolution.npmSpec, projectRoot);
|
|
1655
1744
|
try {
|
|
1656
1745
|
if (installRunner)
|
|
1657
1746
|
installRunner(installCmd, projectRoot);
|
|
1747
|
+
// БЕЗ ОБОЛОЧКИ. execFileSync с массивом аргументов не запускает shell, поэтому имя пакета или
|
|
1748
|
+
// путь с `$(...)` подставить нечему. Строка выше — для показа и для тестового шва, не для
|
|
1749
|
+
// исполнения (см. докстринг buildInstallCommand).
|
|
1658
1750
|
else
|
|
1659
|
-
|
|
1751
|
+
execFileSync('npm', [...buildInstallArgs(specResolution.npmSpec, projectRoot)], { cwd: projectRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
1660
1752
|
}
|
|
1661
1753
|
catch (err) {
|
|
1662
1754
|
write(`dz install: npm install failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
1755
|
+
// НЕАТОМАРНЫЙ ОТКАЗ НАЗЫВАЕТСЯ ВСЛУХ (ADR-001, FR-4). Названо кросс-семейной проверкой
|
|
1756
|
+
// 2026-09-03: npm мог успеть изменить package.json, файл замков и node_modules и упасть уже
|
|
1757
|
+
// после этого. Отката у нас нет — и молчать об этом хуже, чем не откатывать: пользователь
|
|
1758
|
+
// считает каталог нетронутым. Полевой случай: ручной откат вернул package.json, а запись
|
|
1759
|
+
// extraneous в файле замков пережила его.
|
|
1760
|
+
write(` npm мог успеть изменить файлы ДО падения — проверьте ${join(projectRoot, 'package.json')},`);
|
|
1761
|
+
write(` ${join(projectRoot, 'package-lock.json')} и ${join(projectRoot, 'node_modules')}: отката dz не делает.`);
|
|
1663
1762
|
return 1;
|
|
1664
1763
|
}
|
|
1665
1764
|
// Step 2: Find SKILL.md files in the installed package — under the RESOLVED dir name, never the
|
|
@@ -1910,7 +2009,7 @@ function cmdStatuslineInstall(options, cwd, write) {
|
|
|
1910
2009
|
* `--kind <feature-adr|loop>` identifies the producer, defaults to `feature-adr`, and rejects any
|
|
1911
2010
|
* other value rather than silently weakening panel arbitration.
|
|
1912
2011
|
*/
|
|
1913
|
-
function cmdStatuslineFaRecord(options, cwd, write) {
|
|
2012
|
+
function cmdStatuslineFaRecord(options, cwd, write, writeErr) {
|
|
1914
2013
|
const slug = (options.get('slug') ?? '').trim();
|
|
1915
2014
|
const step = (options.get('step') ?? '').trim();
|
|
1916
2015
|
// `--recalled auto` derives the count from the recall-usage log for `--run <id>`, replacing the
|
|
@@ -1970,18 +2069,42 @@ function cmdStatuslineFaRecord(options, cwd, write) {
|
|
|
1970
2069
|
write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 0" --kind feature-adr --recalled 5 --stored 2');
|
|
1971
2070
|
return 1;
|
|
1972
2071
|
}
|
|
2072
|
+
// fa-phase-statusline (acid A1): --tier drives done/total on the phase line — an invalid tier is
|
|
2073
|
+
// REJECTED before anything is written (nothing slotted, nothing ledgered), never silently dropped.
|
|
2074
|
+
const tierRaw = options.get('tier');
|
|
2075
|
+
const tier = tierRaw?.trim();
|
|
2076
|
+
if (tier !== undefined && tier !== 'S' && tier !== 'M' && tier !== 'L' && tier !== 'XL') {
|
|
2077
|
+
write(`dz statusline --fa-record: --tier must be S, M, L or XL (got "${tierRaw}")`);
|
|
2078
|
+
write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 7 Code" --tier M');
|
|
2079
|
+
return 1;
|
|
2080
|
+
}
|
|
1973
2081
|
const mode = options.get('mode');
|
|
2082
|
+
const runId = options.get('run-id')?.trim();
|
|
1974
2083
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1975
|
-
const
|
|
2084
|
+
const outcome = writeFeatureAdrStateDetailed(projectRoot, {
|
|
1976
2085
|
kind: kindRaw, slug, step, recalled, stored,
|
|
1977
2086
|
...(reinforced > 0 ? { reinforced } : {}),
|
|
1978
2087
|
...(mode !== undefined && mode.trim() !== '' ? { mode: mode.trim() } : {}),
|
|
2088
|
+
...(tier !== undefined ? { tier } : {}),
|
|
2089
|
+
...(runId !== undefined && runId !== '' ? { runId } : {}),
|
|
1979
2090
|
});
|
|
2091
|
+
const state = outcome.state;
|
|
1980
2092
|
if (state === undefined) {
|
|
2093
|
+
// [AM-5] A REFUSED slot write is LOUD. A held `fa-phase-slot` lock or an unwritable `.dz` used
|
|
2094
|
+
// to return a bare `undefined`, and a caller reading silence as success is exactly the
|
|
2095
|
+
// "absence of a receipt is not success" class. The reason goes to stderr — diagnosis, not
|
|
2096
|
+
// data — and the EXIT CODE is unchanged for a refusal, because the panel must never break the
|
|
2097
|
+
// pipeline that is only reporting to it.
|
|
2098
|
+
if (outcome.refused !== undefined) {
|
|
2099
|
+
writeErr(`fa-record: slot write refused (${slug}): ${outcome.refused}`);
|
|
2100
|
+
return 0;
|
|
2101
|
+
}
|
|
1981
2102
|
write(`dz statusline --fa-record: could not write learning state under ${projectRoot}/.dz/feature-adr/`);
|
|
1982
2103
|
return 1;
|
|
1983
2104
|
}
|
|
1984
|
-
|
|
2105
|
+
// state.step, not the flag: the monotonic guard may have kept a LATER step against a stale
|
|
2106
|
+
// duplicate record (fa-phase-statusline P1) — print what actually stands in the slot.
|
|
2107
|
+
write(`dz statusline: recorded /feature-adr learning state for "${slug}" (${state.step}) — 🎓 ${state.pool} pool · ↑${state.recalled} used · +${state.stored} new · ↻${state.reinforced ?? 0} reinforced`);
|
|
1985
2108
|
return 0;
|
|
1986
2109
|
}
|
|
1987
2110
|
function statuslineEtaStage(step) {
|
|
@@ -2136,16 +2259,21 @@ function statuslineEta(projectRoot, state, nowMs) {
|
|
|
2136
2259
|
* least a minimal `dz` even on total failure.
|
|
2137
2260
|
*
|
|
2138
2261
|
* Flags: `--install` wires it into settings.json; `--fa-record` records a live `/feature-adr`
|
|
2139
|
-
* learning state (WRITES — see {@link cmdStatuslineFaRecord}); `--json` prints the raw data object
|
|
2140
|
-
*
|
|
2262
|
+
* learning state (WRITES — see {@link cmdStatuslineFaRecord}); `--json` prints the raw data object
|
|
2263
|
+
* (plus `featureAdrLine`, the rendered phase line, when a fresh /feature-adr run is in flight);
|
|
2264
|
+
* default prints the status line, with the 📐 phase panel as its OWN SECOND LINE (format B —
|
|
2265
|
+
* fa-phase-statusline ADR-001 D1; Claude Code renders every stdout line of a statusline command).
|
|
2266
|
+
* The ETA fragment main shipped for that panel rides the SECOND line with it (fa-phase-statusline ADR-001 D1) — the panel
|
|
2267
|
+
* moved, the estimate was not dropped.
|
|
2141
2268
|
*/
|
|
2142
|
-
function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
2269
|
+
function cmdStatusline(options, flags, cwd, write, readStdin, writeErr) {
|
|
2143
2270
|
if (flags.has('install'))
|
|
2144
2271
|
return cmdStatuslineInstall(options, cwd, write);
|
|
2145
2272
|
if (flags.has('fa-record'))
|
|
2146
|
-
return cmdStatuslineFaRecord(options, cwd, write);
|
|
2273
|
+
return cmdStatuslineFaRecord(options, cwd, write, writeErr);
|
|
2147
2274
|
try {
|
|
2148
2275
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
2276
|
+
warnLearningStoreRead(projectRoot, writeErr, 'dz statusline');
|
|
2149
2277
|
const data = statuslineData(projectRoot);
|
|
2150
2278
|
const fa = data.featureAdr;
|
|
2151
2279
|
let eta;
|
|
@@ -2162,27 +2290,67 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
2162
2290
|
etaFragment = undefined;
|
|
2163
2291
|
}
|
|
2164
2292
|
}
|
|
2293
|
+
// fa-phase-statusline (ADR-001 D1): the phase line renders from the slot ALONE — a pure
|
|
2294
|
+
// function over data.featureAdr, computed once here for both the plain and --json surfaces.
|
|
2295
|
+
const phaseLine = fa !== undefined ? renderFeatureAdrPhaseLine(fa) : undefined;
|
|
2165
2296
|
if (flags.has('json')) {
|
|
2166
|
-
write(JSON.stringify({
|
|
2297
|
+
write(JSON.stringify({
|
|
2298
|
+
...data,
|
|
2299
|
+
...(eta !== undefined ? { eta } : {}),
|
|
2300
|
+
...(phaseLine !== undefined ? { featureAdrLine: phaseLine } : {}),
|
|
2301
|
+
}));
|
|
2167
2302
|
return 0;
|
|
2168
2303
|
}
|
|
2169
|
-
|
|
2304
|
+
// ТЕКСТ ПАНЕЛИ — ПО-АНГЛИЙСКИ по просьбе владельца 2026-09-09: строка статуса узкая,
|
|
2305
|
+
// английские слова в ней короче русских при той же ясности. Комментарии остаются русскими.
|
|
2306
|
+
const breakdown = data.patternBreakdown;
|
|
2307
|
+
let line = breakdown === undefined
|
|
2308
|
+
? `🎓 dz: ${data.patterns} patterns`
|
|
2309
|
+
: `🎓 dz: ${data.patterns} (${breakdown.active} active${breakdown.quarantined > 0
|
|
2310
|
+
? ` · ${breakdown.quarantined} quarantined${breakdown.attention ? ' ⚠' : ''}`
|
|
2311
|
+
: ''})${breakdown.tierDelta !== undefined ? ` ⚠ tiers Δ${breakdown.tierDelta}` : ''}`;
|
|
2312
|
+
// Показатель зеркала печатается и здесь: `dz statusline` — та же панель, и показатель,
|
|
2313
|
+
// живущий только во вспомогательном скрипте, для этой поверхности просто не существовал.
|
|
2314
|
+
if (data.patternMirror?.state === 'unavailable')
|
|
2315
|
+
line += ' (mirror unreadable ⚠)';
|
|
2316
|
+
else if (data.patternMirror?.state === 'different')
|
|
2317
|
+
line += ` (mirror ${data.patternMirror.vector} ⚠)`;
|
|
2318
|
+
if (data.storeHealth?.verdict === 'collapsed') {
|
|
2319
|
+
line += ` ⛔ COLLAPSE: was ${data.storeHealth.previousMax ?? '?'} · dz store-guard --reset`;
|
|
2320
|
+
}
|
|
2321
|
+
else if (data.storeHealth?.verdict === 'cold-start-over-existing') {
|
|
2322
|
+
line += ` ⛔ STORE EMPTY: was ${data.storeHealth.previousMax ?? '?'} · restore from snapshots ${data.storeHealth.snapshotPath ?? ''}`.trimEnd();
|
|
2323
|
+
}
|
|
2324
|
+
else if (data.storeHealth?.verdict === 'unreadable') {
|
|
2325
|
+
line += ` ⛔ STORE UNREADABLE${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
|
|
2326
|
+
? `: ${data.storeHealth.unreadableFiles.join(', ')}` : ''}`;
|
|
2327
|
+
}
|
|
2328
|
+
else if (data.storeHealth?.verdict === 'source-changed') {
|
|
2329
|
+
line += ' ⚠ store source changed';
|
|
2330
|
+
}
|
|
2331
|
+
// Рядом с числом источников — объём каждого через «/» (просьба владельца 2026-09-09):
|
|
2332
|
+
// четыре источника по 300 единиц и четыре по три — разные корпуса, а число одно и то же.
|
|
2333
|
+
const ku = data.brainKuCounts.length > 0 ? ` (${data.brainKuCounts.join('/')})` : '';
|
|
2334
|
+
line += `${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources${ku}`;
|
|
2170
2335
|
const branch = statuslineGitBranch(projectRoot);
|
|
2171
2336
|
if (branch !== undefined)
|
|
2172
2337
|
line += ` · ⎇ ${branch}`;
|
|
2173
2338
|
if (data.consolidatedAgeH !== undefined)
|
|
2174
2339
|
line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
2175
|
-
// Live
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
}
|
|
2181
|
-
else {
|
|
2182
|
-
line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2183
|
-
}
|
|
2340
|
+
// Live loop run in flight → PREPEND its segment to the base dz line (unchanged). A live
|
|
2341
|
+
// /feature-adr run no longer glues into line 1: its 📐 segment IS the second line (format B) —
|
|
2342
|
+
// Claude Code renders every stdout line of a statusline command (fa-phase-statusline ADR-001 D1).
|
|
2343
|
+
if (fa !== undefined && fa.kind === 'loop') {
|
|
2344
|
+
line = `🔁 loop ${fa.step} · ${line}`;
|
|
2184
2345
|
}
|
|
2185
2346
|
write(line);
|
|
2347
|
+
// fa-phase-statusline ADR-001 D1: the phase panel moved to line 2 and TOOK main's ETA fragment with it. The move is
|
|
2348
|
+
// the point of format B (ADR-001 D1); dropping the estimate would have been a silent
|
|
2349
|
+
// regression of a feature `main` shipped while this branch was stranded, so it rides here
|
|
2350
|
+
// instead. A phase line that renders (fresh, non-terminal, non-loop slot) is the only gate.
|
|
2351
|
+
if (phaseLine !== undefined) {
|
|
2352
|
+
write(`${phaseLine}${etaFragment !== undefined ? ` · ${etaFragment}` : ''}`);
|
|
2353
|
+
}
|
|
2186
2354
|
return 0;
|
|
2187
2355
|
}
|
|
2188
2356
|
catch {
|
|
@@ -2766,6 +2934,259 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
|
|
|
2766
2934
|
const reason = teachReasonPhrase(target.reason);
|
|
2767
2935
|
return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
|
|
2768
2936
|
}
|
|
2937
|
+
function inspectLearningStore(projectRoot) {
|
|
2938
|
+
const mark = readStoreMark(projectRoot);
|
|
2939
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
2940
|
+
return { mark, rows, health: checkStoreHealth({ projectRoot, ...rows, mark }) };
|
|
2941
|
+
}
|
|
2942
|
+
function shellQuote(value) {
|
|
2943
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2944
|
+
}
|
|
2945
|
+
function storeGuardResetCommand(projectRoot) {
|
|
2946
|
+
return `dz store-guard --reset --project ${shellQuote(projectRoot)}`;
|
|
2947
|
+
}
|
|
2948
|
+
function lexicalSourceLines(rows) {
|
|
2949
|
+
return [
|
|
2950
|
+
` lexical selected: ${rows.lexicalSourcePath} (${rows.lexicalSource}, ${rows.lexicalRows} rows)`,
|
|
2951
|
+
...(rows.lexicalIgnoredSourcePath === undefined ? [] : [
|
|
2952
|
+
` lexical ignored: ${rows.lexicalIgnoredSourcePath} (${rows.lexicalIgnoredRows} rows)`,
|
|
2953
|
+
]),
|
|
2954
|
+
];
|
|
2955
|
+
}
|
|
2956
|
+
function storeGuardRecoveryLines(projectRoot, mark, health, rows) {
|
|
2957
|
+
const snapshots = storeSnapshotPath(projectRoot);
|
|
2958
|
+
const markPath = storeGuardPath(projectRoot);
|
|
2959
|
+
let lexicalSnapshot;
|
|
2960
|
+
let vectorSnapshot = false;
|
|
2961
|
+
try {
|
|
2962
|
+
const names = existsSync(snapshots) ? readdirSync(snapshots) : [];
|
|
2963
|
+
const hasSqlite = names.some((name) => /^lexical\..+\.sqlite$/.test(name));
|
|
2964
|
+
const hasJsonl = names.some((name) => /^lexical\..+\.jsonl$/.test(name));
|
|
2965
|
+
const preferred = mark?.lexicalSource === 'sqlite' || mark?.lexicalSource === 'jsonl'
|
|
2966
|
+
? mark.lexicalSource
|
|
2967
|
+
: rows?.lexicalSource;
|
|
2968
|
+
if (preferred === 'jsonl' && hasJsonl)
|
|
2969
|
+
lexicalSnapshot = 'jsonl';
|
|
2970
|
+
else if (preferred === 'sqlite' && hasSqlite)
|
|
2971
|
+
lexicalSnapshot = 'sqlite';
|
|
2972
|
+
else if (hasSqlite)
|
|
2973
|
+
lexicalSnapshot = 'sqlite';
|
|
2974
|
+
else if (hasJsonl)
|
|
2975
|
+
lexicalSnapshot = 'jsonl';
|
|
2976
|
+
vectorSnapshot = names.some((name) => /^vector\..+\.sqlite$/.test(name));
|
|
2977
|
+
}
|
|
2978
|
+
catch {
|
|
2979
|
+
lexicalSnapshot = undefined;
|
|
2980
|
+
vectorSnapshot = false;
|
|
2981
|
+
}
|
|
2982
|
+
const lines = [
|
|
2983
|
+
`dz store guard: REFUSED — ${health.reason}`,
|
|
2984
|
+
` mark: ${markPath}`,
|
|
2985
|
+
...(mark === undefined ? [] : [` recorded rows: lexical=${mark.lexicalMax} (${mark.lexicalSource}), vector=${mark.vectorMax}`]),
|
|
2986
|
+
...(rows === undefined ? [] : lexicalSourceLines(rows)),
|
|
2987
|
+
];
|
|
2988
|
+
if (lexicalSnapshot !== undefined && vectorSnapshot) {
|
|
2989
|
+
const lexicalDestination = lexicalSnapshot === 'sqlite'
|
|
2990
|
+
? join(projectRoot, '.dz', 'memory', 'patterns.sqlite')
|
|
2991
|
+
: join(projectRoot, '.dz', 'patterns.jsonl');
|
|
2992
|
+
lines.push(` snapshots: ${snapshots}/`, ` restore: mkdir -p ${shellQuote(dirname(lexicalDestination))} && cp ${shellQuote(join(snapshots, `lexical.<timestamp>.${lexicalSnapshot}`))} ${shellQuote(lexicalDestination)} && cp ${shellQuote(join(snapshots, 'vector.<timestamp>.sqlite'))} ${shellQuote(join(projectRoot, '.dz', 'agentdb.db'))}`);
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
lines.push(` snapshots: none found in ${snapshots}/`, ` create one manually: scripts/dz-store-snapshot.sh --project ${shellQuote(projectRoot)}`);
|
|
2996
|
+
}
|
|
2997
|
+
lines.push(` accept current counts: ${storeGuardResetCommand(projectRoot)}`, ' continue intentionally: set DZ_ALLOW_COLD_START=1 or pass --allow-cold-start');
|
|
2998
|
+
return lines;
|
|
2999
|
+
}
|
|
3000
|
+
function observedRows(rows) {
|
|
3001
|
+
return typeof rows.lexicalRows === 'number' && typeof rows.vectorRows === 'number'
|
|
3002
|
+
? { lexicalRows: rows.lexicalRows, vectorRows: rows.vectorRows, lexicalSource: rows.lexicalSource }
|
|
3003
|
+
: undefined;
|
|
3004
|
+
}
|
|
3005
|
+
/** Mark maintenance is diagnostic: the store operation already completed and must keep its exit code. */
|
|
3006
|
+
function refreshLearningStoreMark(projectRoot, writeErr, command, options = {}) {
|
|
3007
|
+
try {
|
|
3008
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
3009
|
+
const counts = observedRows(rows);
|
|
3010
|
+
if (counts === undefined) {
|
|
3011
|
+
writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark was not updated — a store tier is unreadable`);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
writeStoreMark(projectRoot, {
|
|
3015
|
+
...counts,
|
|
3016
|
+
observedAt: new Date().toISOString(),
|
|
3017
|
+
command,
|
|
3018
|
+
}, options.reader ? { timeoutMs: 0 } : {});
|
|
3019
|
+
}
|
|
3020
|
+
catch (error) {
|
|
3021
|
+
if (options.reader && error instanceof NamedLockTimeoutError)
|
|
3022
|
+
return;
|
|
3023
|
+
writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark could not be updated — ${error instanceof Error ? error.message : String(error)}`);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
function storeGuardResetReminder(projectRoot, writeErr, command) {
|
|
3027
|
+
try {
|
|
3028
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
3029
|
+
const mark = readStoreMark(projectRoot);
|
|
3030
|
+
writeErr(`⚠ DZ STORE GUARD — ${command}: store now lexical=${rows.lexicalRows}, vector=${rows.vectorRows}; maximum remains lexical=${mark?.lexicalMax ?? 'none'}, vector=${mark?.vectorMax ?? 'none'}; reconcile explicitly: ${storeGuardResetCommand(projectRoot)}`);
|
|
3031
|
+
for (const line of lexicalSourceLines(rows))
|
|
3032
|
+
writeErr(line);
|
|
3033
|
+
}
|
|
3034
|
+
catch (error) {
|
|
3035
|
+
writeErr(`⚠ DZ STORE GUARD — ${command}: store changed; inspect it and reconcile explicitly with ${storeGuardResetCommand(projectRoot)} (${error instanceof Error ? error.message : String(error)})`);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
/** Fail closed for store writers, except for an explicit per-process/per-command override. */
|
|
3039
|
+
function allowLearningStoreWrite(projectRoot, flags, writeErr, command) {
|
|
3040
|
+
let inspection;
|
|
3041
|
+
try {
|
|
3042
|
+
inspection = inspectLearningStore(projectRoot);
|
|
3043
|
+
}
|
|
3044
|
+
catch (error) {
|
|
3045
|
+
const health = {
|
|
3046
|
+
verdict: 'unreadable',
|
|
3047
|
+
reason: `cannot read the external mark (${error instanceof Error ? error.message : String(error)})`,
|
|
3048
|
+
};
|
|
3049
|
+
for (const line of storeGuardRecoveryLines(projectRoot, undefined, health))
|
|
3050
|
+
writeErr(line);
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
if (inspection.health.verdict === 'no-mark' || inspection.health.verdict === 'ok') {
|
|
3054
|
+
// The successful write path records the resulting counts. Refreshing here as
|
|
3055
|
+
// well would emit the same telemetry failure twice when the external mark is
|
|
3056
|
+
// unavailable, and would receipt a source transition before the command's
|
|
3057
|
+
// own row had landed.
|
|
3058
|
+
return true;
|
|
3059
|
+
}
|
|
3060
|
+
if (inspection.health.verdict === 'source-changed') {
|
|
3061
|
+
const counts = observedRows(inspection.rows);
|
|
3062
|
+
if (counts === undefined)
|
|
3063
|
+
return false;
|
|
3064
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — SOURCE CHANGED: ${inspection.health.reason}; ${storeGuardResetCommand(projectRoot)}`);
|
|
3065
|
+
for (const line of lexicalSourceLines(inspection.rows))
|
|
3066
|
+
writeErr(line);
|
|
3067
|
+
try {
|
|
3068
|
+
// Consume the single migration allowance BEFORE the store write. If the
|
|
3069
|
+
// following command fails, the safe result is a consumed allowance that
|
|
3070
|
+
// requires an explicit reset, never a silently reusable permission.
|
|
3071
|
+
writeStoreMark(projectRoot, { ...counts, observedAt: new Date().toISOString(), command }, {
|
|
3072
|
+
expectedPreviousLexicalSource: inspection.mark?.lexicalSource ?? 'unknown',
|
|
3073
|
+
});
|
|
3074
|
+
return true;
|
|
3075
|
+
}
|
|
3076
|
+
catch (error) {
|
|
3077
|
+
writeErr(`dz store guard: REFUSED — source-change allowance could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
3078
|
+
return false;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const allowed = process.env.DZ_ALLOW_COLD_START === '1' || flags.has('allow-cold-start');
|
|
3082
|
+
if (allowed) {
|
|
3083
|
+
const label = inspection.health.verdict.replaceAll('-', ' ').toUpperCase();
|
|
3084
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — ${label}: ${inspection.health.reason}; explicit cold-start override accepted`);
|
|
3085
|
+
return true;
|
|
3086
|
+
}
|
|
3087
|
+
for (const line of storeGuardRecoveryLines(projectRoot, inspection.mark, inspection.health, inspection.rows))
|
|
3088
|
+
writeErr(line);
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
/** Readers warn on damage and bootstrap/refresh a healthy non-empty store mark. */
|
|
3092
|
+
function warnLearningStoreRead(projectRoot, writeErr, command) {
|
|
3093
|
+
try {
|
|
3094
|
+
const { health, rows, mark } = inspectLearningStore(projectRoot);
|
|
3095
|
+
if (health.verdict === 'collapsed' || health.verdict === 'cold-start-over-existing' || health.verdict === 'unreadable'
|
|
3096
|
+
|| health.verdict === 'source-changed') {
|
|
3097
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — ${health.verdict.replaceAll('-', ' ').toUpperCase()}: ${health.reason}`);
|
|
3098
|
+
for (const line of lexicalSourceLines(rows))
|
|
3099
|
+
writeErr(line);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
const counts = observedRows(rows);
|
|
3103
|
+
if (counts !== undefined && counts.lexicalRows + counts.vectorRows > 0
|
|
3104
|
+
&& (mark === undefined || mark.lexicalLast !== counts.lexicalRows || mark.vectorLast !== counts.vectorRows
|
|
3105
|
+
|| mark.lexicalSource !== counts.lexicalSource
|
|
3106
|
+
|| mark.lexicalMax < counts.lexicalRows || mark.vectorMax < counts.vectorRows)) {
|
|
3107
|
+
refreshLearningStoreMark(projectRoot, writeErr, command, { reader: true });
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
catch (error) {
|
|
3111
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — external mark unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
async function cmdStoreGuard(options, flags, cwd, write, writeErr, stdinText, interactive) {
|
|
3115
|
+
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3116
|
+
const path = storeGuardPath(projectRoot);
|
|
3117
|
+
const reset = flags.has('reset');
|
|
3118
|
+
const status = flags.has('status') || options.has('status');
|
|
3119
|
+
if (reset && status) {
|
|
3120
|
+
writeErr('dz store-guard: --status and --reset are mutually exclusive');
|
|
3121
|
+
return 2;
|
|
3122
|
+
}
|
|
3123
|
+
if (reset) {
|
|
3124
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
3125
|
+
const counts = observedRows(rows);
|
|
3126
|
+
if (counts === undefined) {
|
|
3127
|
+
writeErr(`dz store-guard: REFUSED — cannot reset from an unreadable store; mark: ${path}`);
|
|
3128
|
+
return 1;
|
|
3129
|
+
}
|
|
3130
|
+
try {
|
|
3131
|
+
const previous = readStoreMark(projectRoot);
|
|
3132
|
+
const beforeLexical = previous?.lexicalMax ?? counts.lexicalRows;
|
|
3133
|
+
const beforeVector = previous?.vectorMax ?? counts.vectorRows;
|
|
3134
|
+
writeErr('⚠ dz store-guard --reset: manual operator decision required; this lowers the recorded high-water evidence');
|
|
3135
|
+
writeErr(` old maximum: lexical=${beforeLexical}, vector=${beforeVector}`);
|
|
3136
|
+
writeErr(` new observed: lexical=${counts.lexicalRows} (${counts.lexicalSource}), vector=${counts.vectorRows}`);
|
|
3137
|
+
for (const line of lexicalSourceLines(rows))
|
|
3138
|
+
writeErr(line);
|
|
3139
|
+
let answer = stdinText?.trim().split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? '';
|
|
3140
|
+
if (!flags.has('yes') && answer === '' && interactive && process.stdin.isTTY) {
|
|
3141
|
+
const { createInterface } = await import('node:readline/promises');
|
|
3142
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3143
|
+
try {
|
|
3144
|
+
answer = (await rl.question(' type yes to continue: ')).trim().toLowerCase();
|
|
3145
|
+
}
|
|
3146
|
+
finally {
|
|
3147
|
+
rl.close();
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
if (!flags.has('yes') && !['y', 'yes', 'да'].includes(answer)) {
|
|
3151
|
+
writeErr(`dz store-guard: REFUSED — reset was not confirmed; re-run with --yes or answer yes`);
|
|
3152
|
+
return 1;
|
|
3153
|
+
}
|
|
3154
|
+
const current = countLearningStoreRowsReadonly(projectRoot);
|
|
3155
|
+
const currentCounts = observedRows(current);
|
|
3156
|
+
if (currentCounts === undefined || !isDeepStrictEqual(currentCounts, counts)) {
|
|
3157
|
+
writeErr('dz store-guard: REFUSED — store counts changed after confirmation; inspect and confirm again');
|
|
3158
|
+
return 1;
|
|
3159
|
+
}
|
|
3160
|
+
const mark = resetStoreMark(projectRoot, {
|
|
3161
|
+
...counts,
|
|
3162
|
+
observedAt: new Date().toISOString(),
|
|
3163
|
+
command: 'dz store-guard --reset',
|
|
3164
|
+
});
|
|
3165
|
+
write(`dz store-guard: RESET — accepted lexical=${mark.lexicalMax}, vector=${mark.vectorMax}`);
|
|
3166
|
+
write(` mark: ${path}`);
|
|
3167
|
+
write(` receipt: ${mark.resetAt?.at} — ${mark.resetAt?.reason}`);
|
|
3168
|
+
return 0;
|
|
3169
|
+
}
|
|
3170
|
+
catch (error) {
|
|
3171
|
+
writeErr(`dz store-guard: reset failed — ${error instanceof Error ? error.message : String(error)}; mark: ${path}`);
|
|
3172
|
+
return 1;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
try {
|
|
3176
|
+
const inspection = inspectLearningStore(projectRoot);
|
|
3177
|
+
write(`dz store-guard: ${inspection.health.verdict.toUpperCase()} — ${inspection.health.reason}`);
|
|
3178
|
+
write(` mark: ${path}`);
|
|
3179
|
+
write(` current rows: lexical=${inspection.rows.lexicalRows}, vector=${inspection.rows.vectorRows}`);
|
|
3180
|
+
for (const line of lexicalSourceLines(inspection.rows))
|
|
3181
|
+
write(line);
|
|
3182
|
+
write(` recorded: ${inspection.mark === undefined ? 'none' : JSON.stringify(inspection.mark)}`);
|
|
3183
|
+
return 0;
|
|
3184
|
+
}
|
|
3185
|
+
catch (error) {
|
|
3186
|
+
writeErr(`dz store-guard: mark unreadable — ${error instanceof Error ? error.message : String(error)}; mark: ${path}`);
|
|
3187
|
+
return 1;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
2769
3190
|
async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
|
|
2770
3191
|
const backend = resolveLearningBackend(projectRoot);
|
|
2771
3192
|
backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
|
|
@@ -2793,6 +3214,13 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2793
3214
|
// repo's own store holds 361 records written under that behaviour, and every other user's store
|
|
2794
3215
|
// is the same. Only an explicit choice moves it.
|
|
2795
3216
|
const { storeRoot, target: teachTarget } = resolved;
|
|
3217
|
+
const teachWillWrite = flags.has('harmonize')
|
|
3218
|
+
? false
|
|
3219
|
+
: options.has('from-json')
|
|
3220
|
+
|| (options.get('reinforce') ?? '').trim() !== ''
|
|
3221
|
+
|| (options.get('_positional_0') ?? '').trim() !== '';
|
|
3222
|
+
if (teachWillWrite && !allowLearningStoreWrite(storeRoot, flags, writeErr, 'dz teach'))
|
|
3223
|
+
return 1;
|
|
2796
3224
|
// The verb is per OUTCOME, not per command: a harmonize dry-run and a failed --reinforce READ
|
|
2797
3225
|
// the store and change nothing, so saying "written" there is a false claim about what happened
|
|
2798
3226
|
// (cross-family QE round 2, 2026-08-27).
|
|
@@ -2831,10 +3259,11 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2831
3259
|
// worse defect than the invisibility it was closing (measured live, cross-family QE round 2).
|
|
2832
3260
|
if (!flags.has('json'))
|
|
2833
3261
|
write(storeLine(flags.has('apply') ? 'written' : 'read'));
|
|
2834
|
-
|
|
3262
|
+
const code = await runHarmonize(storeRoot, options, flags, write, writeErr, {
|
|
2835
3263
|
store: join(storeRoot, '.dz'),
|
|
2836
3264
|
storeChosenBy: teachTarget.reason,
|
|
2837
3265
|
});
|
|
3266
|
+
return code;
|
|
2838
3267
|
}
|
|
2839
3268
|
// Bulk import: `dz teach --from-json <file>` ingests a `dz recall --all --json`
|
|
2840
3269
|
// export (an array of learned patterns) — the portable way to SHARE a learned
|
|
@@ -2932,6 +3361,8 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2932
3361
|
const report = await harmonizeVectorStore(storeRoot, {});
|
|
2933
3362
|
write(` ℹ ${imported} imported — ${report.clusters.length} near-duplicate cluster(s): review with dz vector harmonize (dry-run); merge with dz vector harmonize --apply after backup`);
|
|
2934
3363
|
}
|
|
3364
|
+
if (imported > 0)
|
|
3365
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --from-json');
|
|
2935
3366
|
return 0;
|
|
2936
3367
|
}
|
|
2937
3368
|
const reinforce = options.get('reinforce');
|
|
@@ -2952,6 +3383,7 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2952
3383
|
if (clearedQ.cleared > 0)
|
|
2953
3384
|
write(` ↳ promoted out of quarantine (mirror updated)`);
|
|
2954
3385
|
write(storeLine('written'));
|
|
3386
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --reinforce');
|
|
2955
3387
|
return 0;
|
|
2956
3388
|
}
|
|
2957
3389
|
// HIGH-fix: a no-match must NOT auto-teach the raw argument — callers pass dzIds or truncated
|
|
@@ -2999,6 +3431,7 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2999
3431
|
const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
|
|
3000
3432
|
if (clearedQ.cleared > 0)
|
|
3001
3433
|
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3434
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --guard');
|
|
3002
3435
|
return 0;
|
|
3003
3436
|
}
|
|
3004
3437
|
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
@@ -3115,9 +3548,13 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
3115
3548
|
}
|
|
3116
3549
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
3117
3550
|
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3551
|
+
if (stored.records.length > 0) {
|
|
3552
|
+
write(` ID: ${stored.records.map((record) => patternRecordId(record)).join(', ')}`);
|
|
3553
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach');
|
|
3554
|
+
}
|
|
3118
3555
|
return commandFailed ? 1 : 0;
|
|
3119
3556
|
}
|
|
3120
|
-
async function cmdConsolidate(options, flags, cwd, write) {
|
|
3557
|
+
async function cmdConsolidate(options, flags, cwd, write, writeErr) {
|
|
3121
3558
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3122
3559
|
const sessionsDirOpt = options.get('sessions-dir');
|
|
3123
3560
|
const pruneNoise = flags.has('prune-noise');
|
|
@@ -3141,6 +3578,8 @@ async function cmdConsolidate(options, flags, cwd, write) {
|
|
|
3141
3578
|
write(`dz consolidate --prune-quarantine: removed ${res.removed} expired quarantined lesson(s)`);
|
|
3142
3579
|
if (res.snapshot !== undefined)
|
|
3143
3580
|
write(` snapshot: ${res.snapshot}`);
|
|
3581
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz consolidate --prune-quarantine --apply');
|
|
3582
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz consolidate --prune-quarantine --apply');
|
|
3144
3583
|
return 0;
|
|
3145
3584
|
}
|
|
3146
3585
|
// --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
|
|
@@ -3250,6 +3689,10 @@ async function cmdConsolidate(options, flags, cwd, write) {
|
|
|
3250
3689
|
}
|
|
3251
3690
|
}
|
|
3252
3691
|
catch { /* best-effort — the ranking is advisory, never fails the consolidate */ }
|
|
3692
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz consolidate');
|
|
3693
|
+
if (pruneNoise && applyPrune) {
|
|
3694
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz consolidate --prune-noise --apply');
|
|
3695
|
+
}
|
|
3253
3696
|
return 0;
|
|
3254
3697
|
}
|
|
3255
3698
|
function recallUsagePatternRefs(projectRoot) {
|
|
@@ -3360,7 +3803,7 @@ function cmdRecallUsage(options, flags, projectRoot, write) {
|
|
|
3360
3803
|
* DRY-RUN BY DEFAULT, and it snapshots before deleting: `.dz/` is git-ignored and unversioned, so a
|
|
3361
3804
|
* mistaken id is unrecoverable. Use `dz recall --all --json` to find ids.
|
|
3362
3805
|
*/
|
|
3363
|
-
async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
3806
|
+
async function cmdRecallForget(options, flags, projectRoot, write, writeErr) {
|
|
3364
3807
|
const raw = options.get('forget') ?? '';
|
|
3365
3808
|
const ids = new Set(raw.split(',').map((s) => s.trim()).filter((s) => s !== ''));
|
|
3366
3809
|
if (ids.size === 0) {
|
|
@@ -3386,7 +3829,7 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
|
3386
3829
|
write(`dz recall --forget: DRY RUN — ${found.length} record(s) would be removed. Re-run with --apply.`);
|
|
3387
3830
|
return 0;
|
|
3388
3831
|
}
|
|
3389
|
-
const dest = join(projectRoot,
|
|
3832
|
+
const dest = join(storeSnapshotPath(projectRoot), `forget-${Date.now()}.json`);
|
|
3390
3833
|
const snap = snapshotStore(projectRoot, dest);
|
|
3391
3834
|
if (snap.error !== undefined) {
|
|
3392
3835
|
write(`dz recall --forget: snapshot failed (${snap.error}) — nothing removed; the store is not versioned`);
|
|
@@ -3398,6 +3841,8 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
|
3398
3841
|
if (result.error !== undefined)
|
|
3399
3842
|
write(` ⚠ ${result.error}`);
|
|
3400
3843
|
write(' the vector mirror still holds them — run `dz vector reindex` to resync');
|
|
3844
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz recall --forget --apply');
|
|
3845
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz recall --forget --apply');
|
|
3401
3846
|
return 0;
|
|
3402
3847
|
}
|
|
3403
3848
|
/**
|
|
@@ -3477,12 +3922,17 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
|
|
|
3477
3922
|
}
|
|
3478
3923
|
async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
|
|
3479
3924
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3925
|
+
warnLearningStoreRead(projectRoot, writeErr, 'dz recall');
|
|
3926
|
+
const globalRootForGuard = globalStoreRoot();
|
|
3927
|
+
if (!sameStore(projectRoot, globalRootForGuard) && existsSync(join(globalRootForGuard, '.dz', 'memory'))) {
|
|
3928
|
+
warnLearningStoreRead(globalRootForGuard, writeErr, 'dz recall');
|
|
3929
|
+
}
|
|
3480
3930
|
const asJson = flags.has('json');
|
|
3481
3931
|
const all = flags.has('all');
|
|
3482
3932
|
if (flags.has('usage'))
|
|
3483
3933
|
return cmdRecallUsage(options, flags, projectRoot, write);
|
|
3484
3934
|
if (options.has('forget'))
|
|
3485
|
-
return cmdRecallForget(options, flags, projectRoot, write);
|
|
3935
|
+
return cmdRecallForget(options, flags, projectRoot, write, writeErr);
|
|
3486
3936
|
if (options.has('promote'))
|
|
3487
3937
|
return cmdRecallPromote(options, flags, projectRoot, write);
|
|
3488
3938
|
// --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
|
|
@@ -4047,7 +4497,7 @@ function renderHarmonize(report, write) {
|
|
|
4047
4497
|
* and `dz teach --harmonize` — ONE implementation, no bespoke second path (QR-6). Guards (AC-6):
|
|
4048
4498
|
* `--apply` + `--dry-run` together is rejected; `--threshold` must be in `(0, 1]`; no flag ⇒ dry-run.
|
|
4049
4499
|
*/
|
|
4050
|
-
async function runHarmonize(projectRoot, options, flags, write,
|
|
4500
|
+
async function runHarmonize(projectRoot, options, flags, write, writeErr,
|
|
4051
4501
|
/**
|
|
4052
4502
|
* Where this harmonize is pointed and what chose it. Under `--json` the human store line is
|
|
4053
4503
|
* suppressed to keep stdout ONE document, so the destination has to travel INSIDE that document
|
|
@@ -4070,6 +4520,10 @@ storeAnnotation) {
|
|
|
4070
4520
|
}
|
|
4071
4521
|
}
|
|
4072
4522
|
const report = await harmonizeVectorStore(projectRoot, { apply, ...(threshold !== undefined ? { threshold } : {}) });
|
|
4523
|
+
if (apply && report.error === undefined) {
|
|
4524
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector harmonize --apply');
|
|
4525
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector harmonize --apply');
|
|
4526
|
+
}
|
|
4073
4527
|
if (flags.has('json')) {
|
|
4074
4528
|
write(JSON.stringify(storeAnnotation !== undefined ? { ...report, ...storeAnnotation } : report));
|
|
4075
4529
|
return report.error !== undefined ? 1 : 0;
|
|
@@ -4077,7 +4531,7 @@ storeAnnotation) {
|
|
|
4077
4531
|
renderHarmonize(report, write);
|
|
4078
4532
|
return report.error !== undefined ? 1 : 0;
|
|
4079
4533
|
}
|
|
4080
|
-
async function cmdVector(options, flags, cwd, write) {
|
|
4534
|
+
async function cmdVector(options, flags, cwd, write, writeErr) {
|
|
4081
4535
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4082
4536
|
const sub = options.get('_positional_0');
|
|
4083
4537
|
if (sub === 'status') {
|
|
@@ -4131,6 +4585,10 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4131
4585
|
if (sub === 'reindex') {
|
|
4132
4586
|
const report = await reindexVectorStore(projectRoot);
|
|
4133
4587
|
if (flags.has('json')) {
|
|
4588
|
+
if (report.error === undefined) {
|
|
4589
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector reindex');
|
|
4590
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector reindex');
|
|
4591
|
+
}
|
|
4134
4592
|
write(JSON.stringify(report));
|
|
4135
4593
|
return report.error !== undefined ? 1 : 0;
|
|
4136
4594
|
}
|
|
@@ -4150,6 +4608,8 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4150
4608
|
if (report.staleTaskTypes.includes('book-knowledge'))
|
|
4151
4609
|
write(' run \`dz brain reindex\` to rebuild the brain\'s book vectors');
|
|
4152
4610
|
}
|
|
4611
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector reindex');
|
|
4612
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector reindex');
|
|
4153
4613
|
return 0;
|
|
4154
4614
|
}
|
|
4155
4615
|
if (sub === 'export') {
|
|
@@ -4201,7 +4661,7 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4201
4661
|
// harmonize (alias: dz teach --harmonize) — SEMANTIC dedup of the learned store, NON-DESTRUCTIVE:
|
|
4202
4662
|
// dry-run by default (previews clusters, writes nothing); --apply drops after a restorable backup.
|
|
4203
4663
|
if (sub === 'harmonize') {
|
|
4204
|
-
return runHarmonize(projectRoot, options, flags, write);
|
|
4664
|
+
return runHarmonize(projectRoot, options, flags, write, writeErr);
|
|
4205
4665
|
}
|
|
4206
4666
|
// import <file.rvf> — the missing HALF of the RVF cycle: UPSERT-BY-dzId, never overwrites.
|
|
4207
4667
|
if (sub === 'import') {
|
|
@@ -4212,6 +4672,8 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4212
4672
|
}
|
|
4213
4673
|
const report = await importRvfCheckpoint(projectRoot, resolve(cwd, src), {});
|
|
4214
4674
|
if (flags.has('json')) {
|
|
4675
|
+
if (report.error === undefined)
|
|
4676
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector import');
|
|
4215
4677
|
write(JSON.stringify(report));
|
|
4216
4678
|
return report.error !== undefined ? 1 : 0;
|
|
4217
4679
|
}
|
|
@@ -4224,6 +4686,7 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4224
4686
|
if (report.skippedOrphans > 0) {
|
|
4225
4687
|
write(' ↳ orphan vectors have no local pattern — import the text first: dz teach --from-json <recall-export.json>, then re-run dz vector import');
|
|
4226
4688
|
}
|
|
4689
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector import');
|
|
4227
4690
|
return 0;
|
|
4228
4691
|
}
|
|
4229
4692
|
write('dz vector — semantic (vector) tier of the learned-pattern store');
|
|
@@ -5679,6 +6142,18 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5679
6142
|
return 1;
|
|
5680
6143
|
}
|
|
5681
6144
|
}
|
|
6145
|
+
const filterStr = options.get('filter');
|
|
6146
|
+
// SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
|
|
6147
|
+
// Parse before the guard pre-flight so its packed-secret scan uses the SAME scoped package set
|
|
6148
|
+
// that publishPackages receives below; an empty resulting list remains an explicit error.
|
|
6149
|
+
let filter;
|
|
6150
|
+
if (filterStr !== undefined) {
|
|
6151
|
+
filter = filterStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6152
|
+
if (filter.length === 0) {
|
|
6153
|
+
write('dz publish: --filter requires a non-empty comma-separated list of package-name substrings');
|
|
6154
|
+
return 1;
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
5682
6157
|
// dz guard pre-flight (ADR-002 option A): publish is the most dangerous, least-reversible self-mutation, so
|
|
5683
6158
|
// it ALWAYS runs the declarative guard first. A HARD violation refuses the publish; `--no-guard "<reason>"`
|
|
5684
6159
|
// is the logged escape hatch (the override lands in .dz/guard-audit.jsonl — visible, never silent).
|
|
@@ -5693,7 +6168,7 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5693
6168
|
write('dz publish: --no-guard requires a reason (it is logged): --no-guard "hotfix, guard re-run after"');
|
|
5694
6169
|
return 1;
|
|
5695
6170
|
}
|
|
5696
|
-
const guardResult = runGuardEvaluation(guardRoot, 'publish', undefined, noGuard);
|
|
6171
|
+
const guardResult = runGuardEvaluation(guardRoot, 'publish', undefined, noGuard, filter);
|
|
5697
6172
|
if (guardResult.verdict === 'block' && noGuard === undefined) {
|
|
5698
6173
|
write('dz publish: ✗ BLOCKED by dz guard (HARD invariant violated):');
|
|
5699
6174
|
for (const v of guardResult.violations.filter((x) => x.severity === 'hard'))
|
|
@@ -5738,21 +6213,6 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5738
6213
|
}
|
|
5739
6214
|
const claimCheckOpt = claimCheckRaw ?? 'warn';
|
|
5740
6215
|
const bumpOnly = flags.has('bump-only');
|
|
5741
|
-
const filterStr = options.get('filter');
|
|
5742
|
-
// SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
|
|
5743
|
-
// Without this, `--filter ""` (e.g. an unset shell var) or a stray comma yields
|
|
5744
|
-
// [''] / ['', 'core'], and publishPackages matches with name.includes(''), which
|
|
5745
|
-
// is true for EVERY package — silently turning a scoped publish into a
|
|
5746
|
-
// whole-monorepo publish. An empty resulting list is an explicit error, never
|
|
5747
|
-
// "match all".
|
|
5748
|
-
let filter;
|
|
5749
|
-
if (filterStr !== undefined) {
|
|
5750
|
-
filter = filterStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5751
|
-
if (filter.length === 0) {
|
|
5752
|
-
write('dz publish: --filter requires a non-empty comma-separated list of package-name substrings');
|
|
5753
|
-
return 1;
|
|
5754
|
-
}
|
|
5755
|
-
}
|
|
5756
6216
|
// SAFETY: dry-run is the DEFAULT. A real publish requires an EXPLICIT opt-in
|
|
5757
6217
|
// via --yes, --confirm, or --no-dry-run. Without one, we never bump or publish.
|
|
5758
6218
|
const wantsLive = flags.has('yes') || flags.has('confirm') || flags.has('no-dry-run');
|
|
@@ -5945,6 +6405,14 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5945
6405
|
if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
|
|
5946
6406
|
write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
|
|
5947
6407
|
}
|
|
6408
|
+
// A dry run stops before build/sign/pack, so it says NOTHING about the gates below that line.
|
|
6409
|
+
// Printing what it did not check is what keeps a clean preview from reading as a clean publish
|
|
6410
|
+
// (measured 2026-09-02: a clean dry run preceded a RED real gate).
|
|
6411
|
+
if (pkg.notVerified && pkg.notVerified.length > 0) {
|
|
6412
|
+
write(` ⓘ холостой прогон НЕ проверял (${pkg.notVerified.length}):`);
|
|
6413
|
+
for (const item of pkg.notVerified)
|
|
6414
|
+
write(` · ${item}`);
|
|
6415
|
+
}
|
|
5948
6416
|
}
|
|
5949
6417
|
return report.errors > 0 ? 1 : 0;
|
|
5950
6418
|
}
|
|
@@ -7667,7 +8135,19 @@ function cmdAgentsSync(options, flags, cwd, write, writeErr) {
|
|
|
7667
8135
|
writeErr(`dz agents-sync: DRIFT — ${drifted.length} stale/missing section(s); ${effect}`);
|
|
7668
8136
|
for (const finding of drifted)
|
|
7669
8137
|
writeErr(` ${finding.id}: ${finding.file} (${finding.status})`);
|
|
7670
|
-
|
|
8138
|
+
const unregistered = drifted.filter((finding) => finding.status === 'unregistered-section');
|
|
8139
|
+
if (unregistered.length > 0) {
|
|
8140
|
+
// ПОДСКАЗКА, ВЕДУЩАЯ НЕ ТУДА, ХУЖЕ ОТСУТСТВУЮЩЕЙ. Повторный `dz agents-sync` эту находку
|
|
8141
|
+
// НЕ лечит: реестр POLICY_SOURCES ведётся руками, и секция, не вписанная в него, не
|
|
8142
|
+
// попадёт в проекцию сколько ни синхронизируй. Раньше здесь печаталась общая подсказка —
|
|
8143
|
+
// читатель прогнал бы её и снова увидел ту же ошибку.
|
|
8144
|
+
writeErr('→ heal with: объяви секцию в POLICY_SOURCES (packages/@dzhechkov/harness-core/src/agents-policy.ts):');
|
|
8145
|
+
for (const finding of unregistered) {
|
|
8146
|
+
writeErr(` { id: '${finding.id}', file: '${finding.file}', heading: '…', why: '…', operativeClause: '…' }`);
|
|
8147
|
+
}
|
|
8148
|
+
writeErr(' затем: dz agents-sync');
|
|
8149
|
+
}
|
|
8150
|
+
else if (drifted.some((finding) => finding.id === 'dz:policies')) {
|
|
7671
8151
|
writeErr('→ heal with: repair duplicate/unmatched dz:policies markers, then run dz agents-sync');
|
|
7672
8152
|
}
|
|
7673
8153
|
else {
|
|
@@ -7752,7 +8232,9 @@ const DEFAULT_STORE_CAP = 5000;
|
|
|
7752
8232
|
* the rule reports nothing for them — a pre-flight must never become a filesystem sweep.
|
|
7753
8233
|
*/
|
|
7754
8234
|
const MAX_STUB_SCAN_FILES = 400;
|
|
7755
|
-
/**
|
|
8235
|
+
/** Maximum packed-file size read by the publish secret scan. */
|
|
8236
|
+
const SECRET_SCAN_MAX_BYTES = 512 * 1024;
|
|
8237
|
+
/** Read the optional `.dz/guard.json` — `{ rules?, storeCap?, stubWaivers?, secretWaivers? }`. Missing/broken ⇒ defaults. */
|
|
7756
8238
|
function loadGuardConfig(root) {
|
|
7757
8239
|
const p = join(root, '.dz', 'guard.json');
|
|
7758
8240
|
if (!existsSync(p))
|
|
@@ -7789,6 +8271,54 @@ function gatherReadmeCounts(root) {
|
|
|
7789
8271
|
// (target repo without sitedoc — missing-evidence contract). But a file that EXISTS and no longer
|
|
7790
8272
|
// matches its anchored pattern emits a MISMATCH pair (a: -1) — silent non-extraction is the exact
|
|
7791
8273
|
// disease this contour cures (AM-1 applies to the guard path too, not only the CI test).
|
|
8274
|
+
// ЗНАЧКИ ПРОТИВ ДЕРЕВА, а не только README против README (бэклог 9cb30764).
|
|
8275
|
+
//
|
|
8276
|
+
// Прежде правило сверяло только числа МЕЖДУ документами: два согласованных документа могли
|
|
8277
|
+
// хором утверждать одно и то же неверное число, и правило молчало. Значок «пакетов: 52» стоял
|
|
8278
|
+
// при 55 публикуемых на диске — ИЗМЕРЕНО 2026-09-03. Меню обещает 32 блюда, официант
|
|
8279
|
+
// перечисляет 30, кухня готовит 38, и никто в ресторане не знает правду.
|
|
8280
|
+
//
|
|
8281
|
+
// Сверяются только числа, ВЫЧИСЛИМЫЕ ИЗ ДЕРЕВА. Значок «опубликовано в npm» сюда НЕ входит и
|
|
8282
|
+
// это сказано вслух: его источник — реестр, а не рабочая копия, и пара, которая делает вид, что
|
|
8283
|
+
// проверила его, была бы хуже отсутствующей.
|
|
8284
|
+
const badge = (s, name) => {
|
|
8285
|
+
const m = s.match(new RegExp(`img\\.shields\\.io/badge/${name}-(\\d+)`));
|
|
8286
|
+
return m && m[1] ? Number(m[1]) : null;
|
|
8287
|
+
};
|
|
8288
|
+
const publishablePackages = (() => {
|
|
8289
|
+
const dir = join(root, 'packages', '@dzhechkov');
|
|
8290
|
+
if (!existsSync(dir))
|
|
8291
|
+
return null;
|
|
8292
|
+
let n = 0;
|
|
8293
|
+
for (const name of readdirSync(dir)) {
|
|
8294
|
+
const pj = join(dir, name, 'package.json');
|
|
8295
|
+
if (!existsSync(pj))
|
|
8296
|
+
continue;
|
|
8297
|
+
try {
|
|
8298
|
+
const j = JSON.parse(readFileSync(pj, 'utf8'));
|
|
8299
|
+
if (j.private !== true)
|
|
8300
|
+
n += 1;
|
|
8301
|
+
}
|
|
8302
|
+
catch { /* нечитаемый манифест — не считается ни в одну сторону */ }
|
|
8303
|
+
}
|
|
8304
|
+
return n;
|
|
8305
|
+
})();
|
|
8306
|
+
const pkgBadge = badge(rootMd, 'packages');
|
|
8307
|
+
if (pkgBadge !== null && publishablePackages !== null) {
|
|
8308
|
+
pairs.push({ label: 'packages (root badge vs publishable package.json on disk)', a: pkgBadge, b: publishablePackages });
|
|
8309
|
+
}
|
|
8310
|
+
const presetBadge = badge(rootMd, 'presets');
|
|
8311
|
+
if (presetBadge !== null) {
|
|
8312
|
+
pairs.push({ label: 'presets (root badge vs PRESET_NAMES in the build)', a: presetBadge, b: PRESET_NAMES.length });
|
|
8313
|
+
}
|
|
8314
|
+
const targetBadge = badge(rootMd, 'targets');
|
|
8315
|
+
if (targetBadge !== null) {
|
|
8316
|
+
pairs.push({ label: 'targets (root badge vs TARGET_NAMES in the build)', a: targetBadge, b: TARGET_NAMES.length });
|
|
8317
|
+
}
|
|
8318
|
+
const cmdBadge = badge(rootMd, 'CLI%20commands');
|
|
8319
|
+
if (cmdBadge !== null && cliAll !== null) {
|
|
8320
|
+
pairs.push({ label: 'commands (root badge vs cli All Commands)', a: cmdBadge, b: cliAll });
|
|
8321
|
+
}
|
|
7792
8322
|
const sitePair = (rel, re, label) => {
|
|
7793
8323
|
if (cliAll === null || !existsSync(join(root, rel)))
|
|
7794
8324
|
return;
|
|
@@ -8224,8 +8754,9 @@ function gatherVolumeShadowFacts(root, packages) {
|
|
|
8224
8754
|
};
|
|
8225
8755
|
}
|
|
8226
8756
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
8227
|
-
function gatherGuardFacts(op, root, text, storeCap) {
|
|
8757
|
+
function gatherGuardFacts(op, root, text, storeCap, publishFilter) {
|
|
8228
8758
|
const facts = { op };
|
|
8759
|
+
const publishPackageRoots = [];
|
|
8229
8760
|
if (op === 'publish') {
|
|
8230
8761
|
// Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
|
|
8231
8762
|
// stale finding and never a publish blocker.
|
|
@@ -8372,6 +8903,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8372
8903
|
for (const m of manifests)
|
|
8373
8904
|
if (m.name && typeof m.version === 'string')
|
|
8374
8905
|
versionByName.set(m.name, m.version);
|
|
8906
|
+
publishPackageRoots.push(...located
|
|
8907
|
+
.filter(({ dir, m }) => m.private !== true && (publishFilter === undefined
|
|
8908
|
+
|| publishFilter.length === 0
|
|
8909
|
+
|| publishFilter.some((filter) => (m.name ?? '').includes(filter) || dir.includes(filter))))
|
|
8910
|
+
.map(({ dir }) => dir));
|
|
8375
8911
|
const pnpmWorkspace = existsSync(join(root, 'pnpm-workspace.yaml'));
|
|
8376
8912
|
const packages = [];
|
|
8377
8913
|
for (const m of manifests) {
|
|
@@ -8386,6 +8922,95 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8386
8922
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
8387
8923
|
}
|
|
8388
8924
|
facts['packages'] = packages;
|
|
8925
|
+
// sibling-dep-protocol: сырые спеки, БЕЗ подстановки версии. Подставленная версия выглядела бы
|
|
8926
|
+
// как обычный диапазон, и правило потеряло бы ровно то, что проверяет.
|
|
8927
|
+
const siblingDeps = [];
|
|
8928
|
+
for (const m of manifests) {
|
|
8929
|
+
if (m.private === true)
|
|
8930
|
+
continue;
|
|
8931
|
+
const fields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
8932
|
+
for (const field of fields) {
|
|
8933
|
+
const table = m[field];
|
|
8934
|
+
if (typeof table !== 'object' || table === null)
|
|
8935
|
+
continue;
|
|
8936
|
+
for (const [dep, spec] of Object.entries(table)) {
|
|
8937
|
+
if (!dep.startsWith('@dzhechkov/') || typeof spec !== 'string')
|
|
8938
|
+
continue;
|
|
8939
|
+
siblingDeps.push({ name: m.name ?? '(unnamed)', field, dep, spec });
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
facts['siblingDeps'] = siblingDeps;
|
|
8944
|
+
// plugin-manifest-audit: каждый `.claude-plugin/plugin.json` в дереве. Обход ограничен по
|
|
8945
|
+
// глубине и не заходит в node_modules/dist — чужие манифесты не наши, и краснеть на них
|
|
8946
|
+
// значило бы отчитываться о том, чего мы не публикуем.
|
|
8947
|
+
const pluginManifests = [];
|
|
8948
|
+
const walkPlugins = (dir, depth) => {
|
|
8949
|
+
if (depth > 4)
|
|
8950
|
+
return;
|
|
8951
|
+
let entries;
|
|
8952
|
+
try {
|
|
8953
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
8954
|
+
}
|
|
8955
|
+
catch {
|
|
8956
|
+
return;
|
|
8957
|
+
}
|
|
8958
|
+
for (const e of entries) {
|
|
8959
|
+
// `out/` — СГЕНЕРИРОВАННОЕ публичное зеркало: те же манифесты, скопированные. Дефект в нём
|
|
8960
|
+
// есть дефект генератора, и он уже сообщается по источнику; вторая копия только удвоила бы
|
|
8961
|
+
// одну и ту же находку.
|
|
8962
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name === 'dist' || e.name === 'out')
|
|
8963
|
+
continue;
|
|
8964
|
+
const full = join(dir, e.name);
|
|
8965
|
+
if (e.name === '.claude-plugin') {
|
|
8966
|
+
const manifest = join(full, 'plugin.json');
|
|
8967
|
+
if (!existsSync(manifest))
|
|
8968
|
+
continue;
|
|
8969
|
+
try {
|
|
8970
|
+
const j = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
8971
|
+
const pick = (k) => (typeof j[k] === 'string' ? j[k] : undefined);
|
|
8972
|
+
// Состав навыков — ОБЕ стороны инвентаризации, и ТОЛЬКО для коробки ОДНОГО ПАКЕТА.
|
|
8973
|
+
//
|
|
8974
|
+
// Корневая витрина сюда НЕ входит, и это не упрощение: её состав собирается из всего
|
|
8975
|
+
// монорепозитория через реестр, «что лежит на складе» для неё — не обход одного дерева,
|
|
8976
|
+
// а весь реестр, и ровно это уже проверяет `marketplace-parity` регенерацией. Первая
|
|
8977
|
+
// редакция этой проверки обошла корень с ограничением глубины и выдала 27 ЛОЖНЫХ
|
|
8978
|
+
// «объявлено, но не найдено» — навыки лежали глубже границы обхода (ИЗМЕРЕНО 2026-09-04,
|
|
8979
|
+
// поймано до коммита прогоном стража на этом же дереве).
|
|
8980
|
+
//
|
|
8981
|
+
// Сравниваются ПУТИ, как их объявил манифест, а не имена: два навыка с одинаковым
|
|
8982
|
+
// именем в разных подкаталогах — законная вещь, и сведение к имени их бы склеило.
|
|
8983
|
+
const boxRoot = dirname(full);
|
|
8984
|
+
const isRepoRoot = resolve(boxRoot) === resolve(root);
|
|
8985
|
+
const declaredSkills = Array.isArray(j['skills']) && !isRepoRoot
|
|
8986
|
+
? j['skills'].filter((x) => typeof x === 'string')
|
|
8987
|
+
.map((rel) => rel.replace(/^\.\//, '').replace(/\/+$/, '')).filter(Boolean)
|
|
8988
|
+
: undefined;
|
|
8989
|
+
const skillsOnDisk = declaredSkills === undefined ? undefined : findSkillDirs(boxRoot);
|
|
8990
|
+
pluginManifests.push({
|
|
8991
|
+
path: relative(root, manifest),
|
|
8992
|
+
...(pick('name') !== undefined ? { name: pick('name') } : {}),
|
|
8993
|
+
...(pick('version') !== undefined ? { version: pick('version') } : {}),
|
|
8994
|
+
...(pick('description') !== undefined ? { description: pick('description') } : {}),
|
|
8995
|
+
...(declaredSkills !== undefined ? { declaredSkills } : {}),
|
|
8996
|
+
...(skillsOnDisk !== undefined ? { skillsOnDisk } : {}),
|
|
8997
|
+
});
|
|
8998
|
+
}
|
|
8999
|
+
catch (error) {
|
|
9000
|
+
pluginManifests.push({
|
|
9001
|
+
path: relative(root, manifest),
|
|
9002
|
+
parseError: (error instanceof Error ? error.message : String(error)).replace(/\s+/g, ' ').slice(0, 200),
|
|
9003
|
+
});
|
|
9004
|
+
}
|
|
9005
|
+
continue;
|
|
9006
|
+
}
|
|
9007
|
+
if (e.name.startsWith('.'))
|
|
9008
|
+
continue;
|
|
9009
|
+
walkPlugins(full, depth + 1);
|
|
9010
|
+
}
|
|
9011
|
+
};
|
|
9012
|
+
walkPlugins(root, 0);
|
|
9013
|
+
facts['pluginManifests'] = pluginManifests;
|
|
8389
9014
|
facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
|
|
8390
9015
|
dir,
|
|
8391
9016
|
name: m.name ?? dir,
|
|
@@ -8420,9 +9045,57 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8420
9045
|
}
|
|
8421
9046
|
catch { /* unreadable tree — the rule reports nothing rather than inventing a violation */ }
|
|
8422
9047
|
try {
|
|
8423
|
-
facts['drift'] = sweepSkillDrift(root, { scope:
|
|
9048
|
+
facts['drift'] = sweepSkillDrift(root, { scope: DRIFT_SWEEP_SCOPE, allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
|
|
8424
9049
|
}
|
|
8425
9050
|
catch { /* skip */ }
|
|
9051
|
+
// backlog-covers-features: каталоги фич, дата их ПОЯВЛЕНИЯ В ИСТОРИИ (не mtime — его двигает
|
|
9052
|
+
// любой посторонний процесс), оговорки из README фичи и тексты записей бэклога. Базовая дата
|
|
9053
|
+
// делает правило зелёным на приходе: 336 существующих каталогов заведены до правила.
|
|
9054
|
+
// Нечитаемое дерево ⇒ факт НЕ выставляется ⇒ правило молчит, а не выдумывает вердикт.
|
|
9055
|
+
try {
|
|
9056
|
+
const featDir = join(root, 'features');
|
|
9057
|
+
if (existsSync(featDir)) {
|
|
9058
|
+
const slugs = readdirSync(featDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9059
|
+
const features = slugs.map((slug) => {
|
|
9060
|
+
let createdIso = '';
|
|
9061
|
+
try {
|
|
9062
|
+
createdIso = execSync(`git log --diff-filter=A --format=%aI -1 -- ${JSON.stringify('features/' + slug)}`, { cwd: root, encoding: 'utf-8' }).trim().split('\n').filter(Boolean).pop() ?? '';
|
|
9063
|
+
}
|
|
9064
|
+
catch { /* нет в истории — функция засчитает как новый, это верный дефолт */ }
|
|
9065
|
+
let waiver;
|
|
9066
|
+
for (const f of ['README.md', '07_code_changes/change_manifest.md']) {
|
|
9067
|
+
try {
|
|
9068
|
+
const m = readFileSync(join(featDir, slug, f), 'utf-8')
|
|
9069
|
+
.match(/^\s*Backlog:\s*не заведено\s*[—-]\s*(.+)$/m);
|
|
9070
|
+
if (m && m[1] && m[1].trim() !== '') {
|
|
9071
|
+
waiver = m[1].trim();
|
|
9072
|
+
break;
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
catch { /* нет файла — не оговорка */ }
|
|
9076
|
+
}
|
|
9077
|
+
return { slug, createdIso, waiver };
|
|
9078
|
+
});
|
|
9079
|
+
const backlogTexts = [];
|
|
9080
|
+
try {
|
|
9081
|
+
for (const line of readFileSync(join(root, '.dz', 'backlog', 'ideas.jsonl'), 'utf-8').split('\n')) {
|
|
9082
|
+
if (line.trim() === '')
|
|
9083
|
+
continue;
|
|
9084
|
+
try {
|
|
9085
|
+
const o = JSON.parse(line);
|
|
9086
|
+
if (typeof o.text === 'string')
|
|
9087
|
+
backlogTexts.push(o.text);
|
|
9088
|
+
}
|
|
9089
|
+
catch { /* рваная строка */ }
|
|
9090
|
+
}
|
|
9091
|
+
}
|
|
9092
|
+
catch { /* стора нет */ }
|
|
9093
|
+
if (backlogTexts.length > 0) {
|
|
9094
|
+
facts['featureBacklog'] = { baseline: BACKLOG_COVERAGE_BASELINE, features, backlogTexts };
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
catch { /* нечитаемо — правило молчит */ }
|
|
8426
9099
|
facts['counts'] = gatherReadmeCounts(root);
|
|
8427
9100
|
// readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: does the
|
|
8428
9101
|
// change set contain its package.json (the version-bump signal) without its README.md?
|
|
@@ -8617,16 +9290,61 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8617
9290
|
catch { /* not a git repo — every template rule stays silent (fail-open) */ }
|
|
8618
9291
|
// no-stubs config waivers: `.dz/guard.json` `stubWaivers: [{path, reason}]` — path-keyed, reason
|
|
8619
9292
|
// MANDATORY (the feature-adr-setup --guards shape; the pure checker refuses a reasonless entry).
|
|
8620
|
-
const
|
|
9293
|
+
const guardConfig = loadGuardConfig(root);
|
|
9294
|
+
const stubWaivers = guardConfig.stubWaivers;
|
|
8621
9295
|
if (Array.isArray(stubWaivers))
|
|
8622
9296
|
facts['stubWaivers'] = stubWaivers;
|
|
9297
|
+
const secretWaivers = guardConfig.secretWaivers;
|
|
9298
|
+
if (Array.isArray(secretWaivers))
|
|
9299
|
+
facts['secretWaivers'] = secretWaivers;
|
|
8623
9300
|
}
|
|
8624
9301
|
if (op === 'consolidate') {
|
|
8625
9302
|
try {
|
|
8626
|
-
facts['drift'] = sweepSkillDrift(root, { scope:
|
|
9303
|
+
facts['drift'] = sweepSkillDrift(root, { scope: DRIFT_SWEEP_SCOPE, allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
|
|
8627
9304
|
}
|
|
8628
9305
|
catch { /* skip */ }
|
|
8629
9306
|
}
|
|
9307
|
+
if (op === 'publish') {
|
|
9308
|
+
const secretTargets = [];
|
|
9309
|
+
let skipped = 0;
|
|
9310
|
+
for (const dir of publishPackageRoots) {
|
|
9311
|
+
const packageRoot = join(root, dir);
|
|
9312
|
+
let packed;
|
|
9313
|
+
try {
|
|
9314
|
+
packed = listPackFiles(packageRoot);
|
|
9315
|
+
}
|
|
9316
|
+
catch {
|
|
9317
|
+
skipped++;
|
|
9318
|
+
continue;
|
|
9319
|
+
}
|
|
9320
|
+
for (const rel of packed) {
|
|
9321
|
+
const absolute = join(packageRoot, rel);
|
|
9322
|
+
try {
|
|
9323
|
+
const stat = lstatSync(absolute);
|
|
9324
|
+
if (!stat.isFile() || stat.size > SECRET_SCAN_MAX_BYTES) {
|
|
9325
|
+
skipped++;
|
|
9326
|
+
continue;
|
|
9327
|
+
}
|
|
9328
|
+
const content = readFileSync(absolute);
|
|
9329
|
+
if (content.subarray(0, 8 * 1024).includes(0)) {
|
|
9330
|
+
skipped++;
|
|
9331
|
+
continue;
|
|
9332
|
+
}
|
|
9333
|
+
secretTargets.push({
|
|
9334
|
+
label: relative(root, absolute).split(sep).join('/'),
|
|
9335
|
+
text: content.toString('utf8'),
|
|
9336
|
+
});
|
|
9337
|
+
}
|
|
9338
|
+
catch {
|
|
9339
|
+
skipped++;
|
|
9340
|
+
}
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9343
|
+
if (secretTargets.length > 0)
|
|
9344
|
+
facts['secretTargets'] = secretTargets;
|
|
9345
|
+
if (skipped > 0)
|
|
9346
|
+
facts['secretScan'] = { skipped };
|
|
9347
|
+
}
|
|
8630
9348
|
if (op === 'teach' || op === 'consolidate') {
|
|
8631
9349
|
if (op === 'teach' && text)
|
|
8632
9350
|
facts['secretTargets'] = [{ label: 'lesson', text }];
|
|
@@ -8646,13 +9364,13 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8646
9364
|
* shared by `dz guard check` and the `dz publish` pre-flight (ADR-002 option A) so they can never disagree.
|
|
8647
9365
|
* `overrideReason` (when the caller forces through a block) is logged, never silent.
|
|
8648
9366
|
*/
|
|
8649
|
-
function runGuardEvaluation(root, op, text, overrideReason) {
|
|
9367
|
+
function runGuardEvaluation(root, op, text, overrideReason, publishFilter) {
|
|
8650
9368
|
const cfg = loadGuardConfig(root);
|
|
8651
9369
|
// Number.isFinite, not just > 0: a config `storeCap: 1e400` parses to Infinity, passes `> 0`, and would
|
|
8652
9370
|
// silently DISABLE the cap (count <= Infinity always). Non-finite ⇒ fall back to the default.
|
|
8653
9371
|
const storeCap = typeof cfg.storeCap === 'number' && Number.isFinite(cfg.storeCap) && cfg.storeCap > 0 ? cfg.storeCap : DEFAULT_STORE_CAP;
|
|
8654
9372
|
const rules = resolveRules(Array.isArray(cfg.rules) ? cfg.rules : undefined);
|
|
8655
|
-
const facts = gatherGuardFacts(op, root, text, storeCap);
|
|
9373
|
+
const facts = gatherGuardFacts(op, root, text, storeCap, publishFilter);
|
|
8656
9374
|
const result = evaluateGuard(facts, rules);
|
|
8657
9375
|
// audit (append-only). ts is real time here (a CLI, not the sandboxed workflow).
|
|
8658
9376
|
try {
|
|
@@ -9488,17 +10206,57 @@ async function cmdMrRakes(options, flags, cwd, write) {
|
|
|
9488
10206
|
* --threshold N drill threshold (default 2 — anti-noise: a first-seen rake accrues, never drills)
|
|
9489
10207
|
* --no-teach drill only; do NOT write the store (skip the agent side)
|
|
9490
10208
|
* --project <dir> pin the teach ledger
|
|
9491
|
-
* --install-hook print the opt-in
|
|
10209
|
+
* --install-hook print the opt-in hook set to add (non-destructive): Stop scan-tail +
|
|
10210
|
+
* PreCompact/SessionEnd full retro (feature narrated-error-must-be-taught)
|
|
10211
|
+
* --scan-tail per-turn Stop-hook mode: incremental admission-debt scan, O(new bytes) —
|
|
10212
|
+
* writes/clears .dz/retro-pending.json; no ledger, no teach, no git subprocess
|
|
10213
|
+
* --transcript <p> the transcript --scan-tail must read. Without it the Stop hook's own stdin
|
|
10214
|
+
* payload (`transcript_path`) is used; with neither, the scan REFUSES
|
|
10215
|
+
* (NOT-ESTABLISHED) rather than guessing the newest file on disk
|
|
9492
10216
|
*/
|
|
9493
|
-
async function cmdRetro(options, flags, cwd, write) {
|
|
10217
|
+
async function cmdRetro(options, flags, cwd, write, readStdin = () => '') {
|
|
10218
|
+
if (flags.has('scan-tail')) {
|
|
10219
|
+
// Per-turn primary detector (ADR-001 D2). Cost budget IS the design: no `git rev-parse`
|
|
10220
|
+
// subprocess (hooks run with cwd = project root; CLAUDE_PROJECT_DIR pins it), no store open,
|
|
10221
|
+
// O(new bytes) via the persisted offset. Always exit 0 — a Stop hook must never fail a turn.
|
|
10222
|
+
const root = process.env['CLAUDE_PROJECT_DIR'] ?? cwd;
|
|
10223
|
+
// WHICH transcript. Round 3, P1-3: this used to fall back to `findLatestTranscript(root)`, so with
|
|
10224
|
+
// several sessions and their subagents alive at once it scanned whichever file had the newest
|
|
10225
|
+
// mtime — routinely another session's, advancing that session's offset and never seeing this
|
|
10226
|
+
// turn's admission. The Stop hook hands the exact path on stdin; with no path from any source the
|
|
10227
|
+
// answer is a stated refusal, never a guess. Still exit 0: a Stop hook must never fail a turn.
|
|
10228
|
+
const picked = resolveScanTailTranscript({
|
|
10229
|
+
flag: options.get('transcript'),
|
|
10230
|
+
positional: options.get('_positional_0'),
|
|
10231
|
+
stdin: readStdin(),
|
|
10232
|
+
});
|
|
10233
|
+
if (picked.path === null) {
|
|
10234
|
+
const reason = picked.reason ?? 'no transcript path';
|
|
10235
|
+
if (flags.has('json'))
|
|
10236
|
+
write(JSON.stringify({ status: 'not-established', source: 'none', reason, scannedBytes: 0, offset: 0 }));
|
|
10237
|
+
else
|
|
10238
|
+
write(`retro scan-tail: NOT-ESTABLISHED — ${reason}. Wire the hook as \`dz retro --scan-tail\` (Claude Code pipes the payload on stdin) or pass \`--transcript <path>\`.`);
|
|
10239
|
+
return 0;
|
|
10240
|
+
}
|
|
10241
|
+
const outcome = runRetroTailScan(join(root, '.dz'), picked.path);
|
|
10242
|
+
if (flags.has('json'))
|
|
10243
|
+
write(JSON.stringify({ ...outcome, source: picked.source }));
|
|
10244
|
+
else if (outcome.status === 'pending')
|
|
10245
|
+
write(`retro scan-tail: unpaid admission — .dz/retro-pending.json armed («${(outcome.snippet ?? '').slice(0, 60)}…»)`);
|
|
10246
|
+
return 0;
|
|
10247
|
+
}
|
|
9494
10248
|
let repoRoot = cwd;
|
|
9495
10249
|
try {
|
|
9496
10250
|
repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
|
|
9497
10251
|
}
|
|
9498
10252
|
catch { /* not git */ }
|
|
9499
10253
|
if (flags.has('install-hook')) {
|
|
9500
|
-
write('Add
|
|
9501
|
-
write(JSON.stringify({ hooks: {
|
|
10254
|
+
write('Add these opt-in hooks to .claude/settings.json (per-turn debt scan + retro at compaction AND session end — PreCompact covers the crash/disconnect sessions SessionEnd never sees):');
|
|
10255
|
+
write(JSON.stringify({ hooks: {
|
|
10256
|
+
Stop: [{ hooks: [{ type: 'command', command: 'dz retro --scan-tail', timeout: 10000, continueOnError: true }] }],
|
|
10257
|
+
PreCompact: [{ hooks: [{ type: 'command', command: 'dz retro', timeout: 60000, continueOnError: true }] }],
|
|
10258
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: 'dz retro', timeout: 60000, continueOnError: true }] }],
|
|
10259
|
+
} }, null, 2));
|
|
9502
10260
|
return 0;
|
|
9503
10261
|
}
|
|
9504
10262
|
const transcript = options.get('_positional_0') ?? findLatestTranscript(repoRoot);
|
|
@@ -9761,6 +10519,50 @@ function cmdChallenge(options, flags, cwd, write) {
|
|
|
9761
10519
|
* decide (dz's rule — a false gate kills trust). Exit code is 0 on a clean run regardless of verdict; 2 only on
|
|
9762
10520
|
* a usage/setup error, so a caller distinguishes "gate ran" from "gate could not run".
|
|
9763
10521
|
*/
|
|
10522
|
+
/**
|
|
10523
|
+
* Есть ли в этом каталоге НЕЗАКОММИЧЕННЫЕ правки. Это и отличает «фича ещё в рабочем дереве»
|
|
10524
|
+
* (тогда `HEAD` — законная предфичевая база) от «фича уже закоммичена» (тогда `HEAD` её содержит).
|
|
10525
|
+
*
|
|
10526
|
+
* Не удалось спросить git — возвращается null, и вызывающий обязан считать положение НЕ
|
|
10527
|
+
* УСТАНОВЛЕННЫМ, а не выбрать удобный ответ.
|
|
10528
|
+
*/
|
|
10529
|
+
/**
|
|
10530
|
+
* Каталоги с `SKILL.md` внутри коробки ОДНОГО пакета — то, что РЕАЛЬНО лежит на складе.
|
|
10531
|
+
* Возвращаются ПУТИ относительно коробки, как их объявляет манифест, а не имена.
|
|
10532
|
+
*/
|
|
10533
|
+
function findSkillDirs(boxRoot) {
|
|
10534
|
+
const found = new Set();
|
|
10535
|
+
const walk = (dir, depth) => {
|
|
10536
|
+
if (depth > 4)
|
|
10537
|
+
return;
|
|
10538
|
+
let entries;
|
|
10539
|
+
try {
|
|
10540
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10541
|
+
}
|
|
10542
|
+
catch {
|
|
10543
|
+
return;
|
|
10544
|
+
}
|
|
10545
|
+
for (const e of entries) {
|
|
10546
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.'))
|
|
10547
|
+
continue;
|
|
10548
|
+
const full = join(dir, e.name);
|
|
10549
|
+
if (existsSync(join(full, 'SKILL.md')))
|
|
10550
|
+
found.add(relative(boxRoot, full));
|
|
10551
|
+
walk(full, depth + 1);
|
|
10552
|
+
}
|
|
10553
|
+
};
|
|
10554
|
+
walk(boxRoot, 0);
|
|
10555
|
+
return [...found];
|
|
10556
|
+
}
|
|
10557
|
+
function hasUncommittedChangesIn(repoRoot, dir) {
|
|
10558
|
+
try {
|
|
10559
|
+
const out = execSync(`git status --porcelain -- ${JSON.stringify(dir)}`, { cwd: repoRoot, encoding: 'utf-8' });
|
|
10560
|
+
return out.split('\n').some((line) => line.trim() !== '');
|
|
10561
|
+
}
|
|
10562
|
+
catch {
|
|
10563
|
+
return null;
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
9764
10566
|
function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
9765
10567
|
let repoRoot = cwd;
|
|
9766
10568
|
try {
|
|
@@ -9774,7 +10576,22 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9774
10576
|
}
|
|
9775
10577
|
const nameFilter = options.get('name');
|
|
9776
10578
|
const propertyTests = testArg.split(',').map((s) => s.trim()).filter(Boolean).map((file) => nameFilter !== undefined && nameFilter.trim() !== '' ? { file, name: nameFilter.trim() } : { file });
|
|
9777
|
-
|
|
10579
|
+
/**
|
|
10580
|
+
* УМОЛЧАНИЕ БАЗЫ РАЗЛИЧАЕТ ДВА РАЗНЫХ ПОЛОЖЕНИЯ, потому что они и правда разные.
|
|
10581
|
+
*
|
|
10582
|
+
* В штатном ходе конвейера правка Шага 7 ЕЩЁ НЕ ЗАКОММИЧЕНА, и тогда `HEAD` — настоящая
|
|
10583
|
+
* предфичевая база; так конвейер и зовёт гейт (`--base HEAD`, причина записана в его тексте).
|
|
10584
|
+
*
|
|
10585
|
+
* Но если фича УЖЕ ЗАКОММИЧЕНА, `HEAD` её содержит, и гейт сравнивает фичу С САМОЙ СОБОЙ.
|
|
10586
|
+
* ИЗМЕРЕНО 2026-09-04: такой прогон дал уверенное `NON_DISCRIMINATING` с советом «усилить тест»
|
|
10587
|
+
* на тестах, которые дискриминируют; тот же прогон с верной базой дал `DISCRIMINATES_VIA_ERROR`.
|
|
10588
|
+
* Находка выглядела как утверждение О ТЕСТАХ, и автор пошёл бы чинить исправное.
|
|
10589
|
+
*
|
|
10590
|
+
* Различить эти положения можно ДЕТЕРМИНИРОВАННО: есть ли незакоммиченные правки в пакете, чьи
|
|
10591
|
+
* тесты названы. Есть — `HEAD` законен и используется как раньше. Нет — база не установлена, и
|
|
10592
|
+
* гейт ОТКАЗЫВАЕТСЯ (exit 3), вместо того чтобы измерять относительно самого себя.
|
|
10593
|
+
*/
|
|
10594
|
+
const explicitBase = options.get('base');
|
|
9778
10595
|
const runnerOpt = options.get('runner');
|
|
9779
10596
|
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
9780
10597
|
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
@@ -9819,9 +10636,34 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9819
10636
|
// The pure half's path sanitation expects a REPO-RELATIVE package dir ('.'-rooted), not an
|
|
9820
10637
|
// absolute one — an absolute path is refused as unsafe-package-dir by design.
|
|
9821
10638
|
const packageDirRel = relative(repoRoot, packageDir) || '.';
|
|
10639
|
+
// Умолчание базы решается ЗДЕСЬ, потому что только здесь известен пакет, чьи тесты названы.
|
|
10640
|
+
let baseRef;
|
|
10641
|
+
if (explicitBase !== undefined && explicitBase.trim() !== '') {
|
|
10642
|
+
baseRef = explicitBase.trim();
|
|
10643
|
+
}
|
|
10644
|
+
else {
|
|
10645
|
+
const dirty = hasUncommittedChangesIn(repoRoot, packageDirRel);
|
|
10646
|
+
if (dirty !== true) {
|
|
10647
|
+
write('dz discrimination-check: NOT-ESTABLISHED — предфичевая база не установлена.');
|
|
10648
|
+
write(dirty === null
|
|
10649
|
+
? ` Не удалось спросить git о состоянии ${packageDirRel}; выбирать удобный ответ вместо этого нельзя.`
|
|
10650
|
+
: ` В ${packageDirRel} нет незакоммиченных правок, значит фича УЖЕ в HEAD, и сравнение шло бы с самой собой.`);
|
|
10651
|
+
write(' Гейт сравнивает поведение ДО и ПОСЛЕ фичи; без базы сравнивать не с чем, а HEAD здесь');
|
|
10652
|
+
write(' дал бы уверенное NON_DISCRIMINATING на исправных тестах (ИЗМЕРЕНО 2026-09-04).');
|
|
10653
|
+
write(' Передайте --base <коммит перед фичей>, например `<sha коммита фичи>^`.');
|
|
10654
|
+
return 3;
|
|
10655
|
+
}
|
|
10656
|
+
// Штатный ход конвейера: правка Шага 7 ещё в рабочем дереве, HEAD — настоящая предфичевая база.
|
|
10657
|
+
baseRef = 'HEAD';
|
|
10658
|
+
write(`dz discrimination-check: база не задана; в ${packageDirRel} есть незакоммиченные правки, беру HEAD как предфичевую базу.`);
|
|
10659
|
+
}
|
|
10660
|
+
// Провенанс базы едет в квитанцию вместе с самой базой: «явно указано» — утверждение о действии
|
|
10661
|
+
// человека, и писать его для HEAD, выбранного инструментом, значит подделывать происхождение
|
|
10662
|
+
// доказательства, на которое сошлются позже.
|
|
10663
|
+
const baseRefSupplied = explicitBase !== undefined && explicitBase.trim() !== '';
|
|
9822
10664
|
const planInput = runnerOpt !== undefined
|
|
9823
|
-
? { baseRef, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
|
|
9824
|
-
: { baseRef, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
|
|
10665
|
+
? { baseRef, baseRefSupplied, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
|
|
10666
|
+
: { baseRef, baseRefSupplied, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
|
|
9825
10667
|
const plan = planDiscriminationCheck(planInput);
|
|
9826
10668
|
if (!plan.runnable) {
|
|
9827
10669
|
// QE-2 (instrument-honesty, MEDIUM): a runner REFUSE used to be reported through the generic
|
|
@@ -10183,14 +11025,34 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
10183
11025
|
return fail(`registry ${registryPath} is invalid:\n - ${parsed.errors.join('\n - ')}`);
|
|
10184
11026
|
}
|
|
10185
11027
|
let entries = parsed.registry.entries;
|
|
11028
|
+
let entryResults = parsed.entryResults;
|
|
10186
11029
|
const only = options.get('only');
|
|
10187
11030
|
if (only !== undefined) {
|
|
10188
11031
|
const ids = only.split(',').map((s) => s.trim()).filter(Boolean);
|
|
10189
|
-
const known = new Set(
|
|
11032
|
+
const known = new Set([
|
|
11033
|
+
...entries.map((entry) => entry.id),
|
|
11034
|
+
...entryResults.map((result) => result.id),
|
|
11035
|
+
]);
|
|
10190
11036
|
const unknown = ids.filter((id) => !known.has(id));
|
|
10191
11037
|
if (unknown.length > 0)
|
|
10192
11038
|
return fail(`--only names unknown entry id(s): ${unknown.join(', ')}`);
|
|
10193
11039
|
entries = entries.filter((e) => ids.includes(e.id));
|
|
11040
|
+
entryResults = entryResults.filter((result) => ids.includes(result.id));
|
|
11041
|
+
}
|
|
11042
|
+
if (entries.length === 0) {
|
|
11043
|
+
const scope = only === undefined ? 'registry' : 'selected registry entries';
|
|
11044
|
+
const error = `${scope} has no runnable entries after validation — nothing can be run; the registry is unusable`;
|
|
11045
|
+
const summary = summarizeMutationResults(entryResults);
|
|
11046
|
+
if (json) {
|
|
11047
|
+
write(JSON.stringify({ error, registryPath, results: entryResults, summary, exitCode: 2 }, null, 2));
|
|
11048
|
+
}
|
|
11049
|
+
else {
|
|
11050
|
+
write(`dz mutation-gate: ${error}`);
|
|
11051
|
+
for (const result of entryResults) {
|
|
11052
|
+
write(` - ${result.id}: ${result.verdict} — ${result.detail}`);
|
|
11053
|
+
}
|
|
11054
|
+
}
|
|
11055
|
+
return 2;
|
|
10194
11056
|
}
|
|
10195
11057
|
const testCmdRaw = options.get('test-cmd') ?? parsed.registry.testCommand ?? 'npm test';
|
|
10196
11058
|
if (/[\0\n\r]/.test(testCmdRaw))
|
|
@@ -10226,7 +11088,7 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
10226
11088
|
}
|
|
10227
11089
|
catch { /* not in a git repo */ }
|
|
10228
11090
|
let copyDir = join(scratchParent, 'pkg');
|
|
10229
|
-
const results = [];
|
|
11091
|
+
const results = [...entryResults];
|
|
10230
11092
|
const observations = [];
|
|
10231
11093
|
const warnings = [];
|
|
10232
11094
|
const internalRetries = [];
|
|
@@ -10358,10 +11220,10 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
10358
11220
|
: undefined);
|
|
10359
11221
|
if (!baseline.ok) {
|
|
10360
11222
|
if (json) {
|
|
10361
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results
|
|
11223
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results, internalRetries, exitCode: 1 }, null, 2));
|
|
10362
11224
|
return 1;
|
|
10363
11225
|
}
|
|
10364
|
-
write(renderMutationReport(
|
|
11226
|
+
write(renderMutationReport(results, baseline, pkgDir));
|
|
10365
11227
|
return 1;
|
|
10366
11228
|
}
|
|
10367
11229
|
for (const entry of entries) {
|
|
@@ -11804,12 +12666,30 @@ function nameCheckScan(repoRoot) {
|
|
|
11804
12666
|
// Command names come from the dispatcher AND from the help block: a name that dispatches but
|
|
11805
12667
|
// is undocumented is still taken, and so is the reverse.
|
|
11806
12668
|
if (f.name === 'cli.ts') {
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
12669
|
+
// ONE enumeration, every consumer derives (ADR-001, feature command-count-triad). This
|
|
12670
|
+
// scan's question is "is the name TAKEN?", so taken = dispatched ∪ documented, which is
|
|
12671
|
+
// legitimately LARGER than the canonical command count — but it must be the SAME parse
|
|
12672
|
+
// the layer-1 parity test uses, not a second private regex that agrees by coincidence.
|
|
12673
|
+
// The any-indent `dispatchedCommandsIn` fallback stays for a cli.ts WITHOUT a main
|
|
12674
|
+
// `switch (command)` (none in this workspace today): a partial sweep would answer "free"
|
|
12675
|
+
// about a taken name, and that is the one answer this command may never give.
|
|
12676
|
+
let mainSwitchParsed = false;
|
|
12677
|
+
try {
|
|
12678
|
+
for (const c of dispatchedCommands(text))
|
|
12679
|
+
commands.add(c);
|
|
12680
|
+
for (const c of documentedCommands(text))
|
|
12681
|
+
commands.add(c);
|
|
12682
|
+
mainSwitchParsed = true;
|
|
12683
|
+
}
|
|
12684
|
+
catch { /* no main switch here — fall back to the broad regexes below */ }
|
|
12685
|
+
if (!mainSwitchParsed) {
|
|
12686
|
+
for (const c of dispatchedCommandsIn(text))
|
|
12687
|
+
commands.add(c);
|
|
12688
|
+
const help = /^\s{2}dz ([a-z][a-z0-9-]*)/gm;
|
|
12689
|
+
for (let m = help.exec(text); m !== null; m = help.exec(text))
|
|
12690
|
+
if (m[1] !== undefined)
|
|
12691
|
+
commands.add(m[1]);
|
|
12692
|
+
}
|
|
11813
12693
|
}
|
|
11814
12694
|
}
|
|
11815
12695
|
}
|
|
@@ -11824,6 +12704,75 @@ function nameCheckScan(repoRoot) {
|
|
|
11824
12704
|
return { commands: new Set(), modules: new Map(), exports: new Map(), scanFailed: true };
|
|
11825
12705
|
return { commands, modules, exports: exportsFound, scanned: { packages, files, exports: exportsFound.size, commands: commands.size } };
|
|
11826
12706
|
}
|
|
12707
|
+
/**
|
|
12708
|
+
* `dz brief-check <файл>` — проверить бриф роя на контракт вывода (ADR-001 swarm-brief-output-contract).
|
|
12709
|
+
*
|
|
12710
|
+
* Разбирает объявления брифа как ДАННЫЕ и отказывает поимённо: «бриф неверен» не говорит автору,
|
|
12711
|
+
* что чинить, поэтому каждое нарушение называет ключ и причину.
|
|
12712
|
+
*
|
|
12713
|
+
* ЧЕСТНЫЙ ПРЕДЕЛ печатается ВМЕСТЕ С ЗЕЛЁНЫМ ответом: проверено, что бриф ОБЪЯВИЛ каталог и
|
|
12714
|
+
* единицы, а не что агент им последует. Зелёная проверка, читаемая как гарантия поведения, хуже
|
|
12715
|
+
* её отсутствия.
|
|
12716
|
+
*/
|
|
12717
|
+
function cmdBriefCheck(options, flags, cwd, write) {
|
|
12718
|
+
const json = flags.has('json');
|
|
12719
|
+
/**
|
|
12720
|
+
* ВЕТКА «ПРОВЕРИТЬ НЕ УДАЛОСЬ» ТОЖЕ ОБЯЗАНА ОТВЕТИТЬ JSON-ом (находка 11).
|
|
12721
|
+
*
|
|
12722
|
+
* `--json` печатал человеческую строку на несуществующем файле, и потребитель, читающий вывод как
|
|
12723
|
+
* JSON, получал ошибку разбора вместо структурного «не проверено». Признак `checked` — тот же
|
|
12724
|
+
* трихотомический вердикт, что и коды выхода, только для машины: не «бриф плох», а «мы про него
|
|
12725
|
+
* ничего не установили».
|
|
12726
|
+
*/
|
|
12727
|
+
const unchecked = (reason, detail) => {
|
|
12728
|
+
if (json)
|
|
12729
|
+
write(JSON.stringify({ ok: false, checked: false, reason, detail }));
|
|
12730
|
+
else
|
|
12731
|
+
write(detail);
|
|
12732
|
+
return 2;
|
|
12733
|
+
};
|
|
12734
|
+
// Только позиционный аргумент: `--file` был необъявленным псевдонимом, и страж дрейфа флагов
|
|
12735
|
+
// справедливо на него указал — лишняя поверхность, которой нет в справке.
|
|
12736
|
+
const file = options.get('_positional_0');
|
|
12737
|
+
if (file === undefined || file.trim() === '') {
|
|
12738
|
+
const code = unchecked('no-file', 'dz brief-check: name the brief file — dz brief-check <file> [--json]');
|
|
12739
|
+
if (!json)
|
|
12740
|
+
write(' A brief must declare: ' + SWARM_BRIEF_CONTRACT.map((c) => c.key).join(', '));
|
|
12741
|
+
return code;
|
|
12742
|
+
}
|
|
12743
|
+
let text;
|
|
12744
|
+
try {
|
|
12745
|
+
text = readFileSync(resolve(cwd, file), 'utf-8');
|
|
12746
|
+
}
|
|
12747
|
+
catch {
|
|
12748
|
+
// Нечитаемый файл — НЕ «бриф плох»: мы про него ничего не установили. Отдельный код выхода,
|
|
12749
|
+
// чтобы отказ прибора не смешивался с отказом брифа.
|
|
12750
|
+
return unchecked('unreadable', `dz brief-check: cannot read ${visibleText(file)}`);
|
|
12751
|
+
}
|
|
12752
|
+
const result = checkSwarmBrief(text);
|
|
12753
|
+
if (json) {
|
|
12754
|
+
// `checked: true` — вторая половина того же различителя: потребитель отличает «проверено и
|
|
12755
|
+
// отвергнуто» от «проверить не удалось» полем, а не отсутствием поля.
|
|
12756
|
+
write(JSON.stringify({ ...result, checked: true }));
|
|
12757
|
+
return result.ok ? 0 : 1;
|
|
12758
|
+
}
|
|
12759
|
+
if (result.ok) {
|
|
12760
|
+
// ЗНАЧЕНИЯ ИЗ БРИФА ОБЕЗВРЕЖИВАЮТСЯ И В ЗЕЛЁНОЙ СТРОКЕ (находка 10). Отказ их уже обезвредил,
|
|
12761
|
+
// но подделывается ровно эта строка: управляющая последовательность в имени каталога стирает
|
|
12762
|
+
// предыдущий вывод и печатает поверх него подделку.
|
|
12763
|
+
write(`dz brief-check: OK — dir ${visibleText(result.outputDir ?? '')}, ${result.units.length} unit(s), assembly "${visibleText(result.assemblyUnit ?? '')}"`);
|
|
12764
|
+
write(' LIMIT: this verifies the brief DECLARED the contract, not that the agent will follow it —');
|
|
12765
|
+
write(' only comparing the directory against the unit list on an orchestrator tick can show that.');
|
|
12766
|
+
write(' A filled-in template and an unedited one both pass: the parse cannot tell them apart.');
|
|
12767
|
+
return 0;
|
|
12768
|
+
}
|
|
12769
|
+
write(`dz brief-check: REFUSED — ${result.violations.length} violation(s)`);
|
|
12770
|
+
// Ядро уже обезвредило значения в причинах; повтор на слое печати — не суеверие, а граница:
|
|
12771
|
+
// печатающий слой не обязан знать, кто именно из его источников уже почистил текст.
|
|
12772
|
+
for (const v of result.violations)
|
|
12773
|
+
write(` ${v.rule}: ${visibleText(v.detail)}`);
|
|
12774
|
+
return 1;
|
|
12775
|
+
}
|
|
11827
12776
|
function cmdNameCheck(options, flags, cwd, write) {
|
|
11828
12777
|
const repoRoot = resolve(options.get('project') ?? cwd);
|
|
11829
12778
|
const json = flags.has('json');
|
|
@@ -12754,12 +13703,21 @@ function cmdAmendmentCheck(options, flags, cwd, write) {
|
|
|
12754
13703
|
// Paths in an amendment row are repo-relative, so they resolve against the repo root — not
|
|
12755
13704
|
// against the feature directory, and not against wherever the caller happened to stand.
|
|
12756
13705
|
const resolutions = resolveAmendments(rows, { readFile: (rel) => readOr(resolve(cwd, rel)) });
|
|
13706
|
+
// A document that opens `## Amendments` twice cannot have its first heading answer for the
|
|
13707
|
+
// rest (Codex round 6, P2). Counted per document and taken at its worst — one contradictory
|
|
13708
|
+
// input is enough to make the run inconclusive.
|
|
13709
|
+
const sectionCount = Math.max(ideation === null ? 0 : amendmentSectionCount(ideation), plan === null ? 0 : amendmentSectionCount(plan));
|
|
12757
13710
|
const decision = decideAmendmentOutcome({
|
|
12758
13711
|
sectionPresent,
|
|
12759
13712
|
rows,
|
|
12760
13713
|
resolutions,
|
|
12761
13714
|
planSaysNone: plan !== null && planSaysNoAmendments(plan),
|
|
12762
13715
|
missingFromPlan,
|
|
13716
|
+
sectionCount,
|
|
13717
|
+
// Fail-closed: measured on the document the rows would have come from (the plan when it has
|
|
13718
|
+
// a section, otherwise the ideation report).
|
|
13719
|
+
ambiguity: (plan !== null ? amendmentDeclarationAmbiguity(plan) : null)
|
|
13720
|
+
?? (ideation !== null ? amendmentDeclarationAmbiguity(ideation) : null),
|
|
12763
13721
|
});
|
|
12764
13722
|
return { slug, decision, resolutions };
|
|
12765
13723
|
};
|
|
@@ -15011,7 +15969,7 @@ function cmdDeliveryCheck(options, flags, cwd, write) {
|
|
|
15011
15969
|
/* backlog — the Smart Backlog idea pipeline (feature smart-backlog) */
|
|
15012
15970
|
/* ------------------------------------------------------------------ */
|
|
15013
15971
|
/** Thin dispatcher — ALL logic lives in harness-core/src/backlog.ts (05 architecture: handlers stay dumb). */
|
|
15014
|
-
async function cmdBacklog(options, flags, cwd, write) {
|
|
15972
|
+
async function cmdBacklog(options, flags, cwd, write, writeErr) {
|
|
15015
15973
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
15016
15974
|
const json = flags.has('json');
|
|
15017
15975
|
const sub = options.get('_positional_0');
|
|
@@ -15029,6 +15987,8 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15029
15987
|
if (eff.adjusted && !json && eff.note !== undefined)
|
|
15030
15988
|
write(`dz backlog: ${eff.note}`);
|
|
15031
15989
|
const dryRun = flags.has('dry-run');
|
|
15990
|
+
if (!dryRun && !allowLearningStoreWrite(projectRoot, flags, writeErr, 'dz backlog add'))
|
|
15991
|
+
return 1;
|
|
15032
15992
|
// Embed-form migration (register-inflation fix): v1 vectors are FULL-TEXT embeds, v2 queries are
|
|
15033
15993
|
// bounded excerpts — comparing across the forms is a query-vs-row space split. Re-mirror once
|
|
15034
15994
|
// (idempotent upsert), before the dedup search. Dry-run writes nothing, so it only WARNS.
|
|
@@ -15059,6 +16019,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15059
16019
|
const ideas = readIdeas(projectRoot);
|
|
15060
16020
|
const match = ideas.find((i) => i.id === verdict.matchedId);
|
|
15061
16021
|
let absorbErr;
|
|
16022
|
+
let didWrite = false;
|
|
15062
16023
|
if (!dryRun && match !== undefined) {
|
|
15063
16024
|
const snap = snapshotIdeas(projectRoot, join(projectRoot, '.dz', 'backlog', `ideas.pre-merge-${Date.now()}.jsonl`));
|
|
15064
16025
|
if (snap.error !== undefined)
|
|
@@ -15075,7 +16036,10 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15075
16036
|
}).error;
|
|
15076
16037
|
match.uses += 1;
|
|
15077
16038
|
writeIdeas(projectRoot, ideas);
|
|
16039
|
+
didWrite = true;
|
|
15078
16040
|
}
|
|
16041
|
+
if (didWrite)
|
|
16042
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog add');
|
|
15079
16043
|
if (json)
|
|
15080
16044
|
write(JSON.stringify({ action: 'duplicate', matchedId: verdict.matchedId, cosine: verdict.cosine, ...(verdict.containment !== undefined ? { containment: verdict.containment } : {}), ...(verdict.subsetMatch === true ? { subsetMatch: true } : {}), ...(topMatch !== undefined ? { topMatch } : {}), ...(eff.note !== undefined ? { effortNote: eff.note } : {}), ...(dryRun ? {} : { absorbedLogged: absorbErr === undefined, ...(absorbErr !== undefined ? { absorbedLogError: absorbErr } : {}) }), exitCode: 0 }, null, 2));
|
|
15081
16045
|
else {
|
|
@@ -15137,6 +16101,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15137
16101
|
ideas.push(rec);
|
|
15138
16102
|
writeIdeas(projectRoot, ideas);
|
|
15139
16103
|
const mirror = await mirrorIdeaVector(projectRoot, rec); // best-effort — never blocks capture
|
|
16104
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog add');
|
|
15140
16105
|
if (json)
|
|
15141
16106
|
write(JSON.stringify({ action: verdict.action, idea: rec, related: verdict.relatedIds, ...(verdict.demoted !== undefined ? { demoted: verdict.demoted } : {}), ...(topMatch !== undefined ? { topMatch } : {}), ...(eff.note !== undefined ? { effortNote: eff.note } : {}), gitignore: ignore, exitCode: 0 }, null, 2));
|
|
15142
16107
|
else {
|
|
@@ -15336,8 +16301,18 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15336
16301
|
if (commitId !== undefined) {
|
|
15337
16302
|
// Validated above (safe id + known id) BEFORE any early return.
|
|
15338
16303
|
const idx = ideas.findIndex((i) => i.id === commitId);
|
|
16304
|
+
// ОТМЕТКА ВРЕМЕНИ И ЖУРНАЛ ставятся ЗДЕСЬ, а не «где-нибудь потом».
|
|
16305
|
+
// Измерено 2026-09-02: `grep -c statusTs cli.ts` давал НОЛЬ — оба пути смены статуса в этом
|
|
16306
|
+
// файле меняли поле и не отмечали, когда. Отсюда 22 терминальные записи без отметки и
|
|
16307
|
+
// невычислимое «сколько идея пробыла в работе».
|
|
16308
|
+
const spinFrom = ideas[idx].status;
|
|
16309
|
+
const spinTs = new Date().toISOString();
|
|
15339
16310
|
ideas[idx].status = 'in-progress';
|
|
16311
|
+
ideas[idx].statusTs = spinTs;
|
|
15340
16312
|
writeIdeas(projectRoot, ideas);
|
|
16313
|
+
if (!appendTransition(projectRoot, { id: commitId, from: spinFrom, to: 'in-progress', ts: spinTs, by: 'backlog roulette --commit' })) {
|
|
16314
|
+
write('dz backlog: переход НЕ записан в журнал — наблюдение потеряно (сам статус изменён)');
|
|
16315
|
+
}
|
|
15341
16316
|
pick = ideas[idx];
|
|
15342
16317
|
committed = true;
|
|
15343
16318
|
}
|
|
@@ -15412,6 +16387,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15412
16387
|
}
|
|
15413
16388
|
else
|
|
15414
16389
|
embed = 'stale';
|
|
16390
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog edit');
|
|
15415
16391
|
}
|
|
15416
16392
|
if (json) {
|
|
15417
16393
|
write(JSON.stringify({ verb: 'edit', ...report, embed, exitCode: report.ok ? (embed === 'stale' ? 1 : 0) : 1 }, null, 2));
|
|
@@ -15446,9 +16422,15 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15446
16422
|
const goalMap = readGoalMap(projectRoot);
|
|
15447
16423
|
const related = ideas.filter((i) => rec.relatedIds.includes(i.id));
|
|
15448
16424
|
const staging = stageEnrichment(projectRoot, rec, related, goalMap);
|
|
16425
|
+
const enrichFrom = rec.status;
|
|
16426
|
+
const enrichTs = new Date().toISOString();
|
|
15449
16427
|
rec.status = 'enriched';
|
|
16428
|
+
rec.statusTs = enrichTs;
|
|
15450
16429
|
rec.enrichedPath = `features/${staging.slug}`;
|
|
15451
16430
|
writeIdeas(projectRoot, ideas);
|
|
16431
|
+
if (!appendTransition(projectRoot, { id: rec.id, from: enrichFrom, to: 'enriched', ts: enrichTs, by: 'backlog enrich' })) {
|
|
16432
|
+
write('dz backlog: переход НЕ записан в журнал — наблюдение потеряно (сам статус изменён)');
|
|
16433
|
+
}
|
|
15452
16434
|
if (json)
|
|
15453
16435
|
write(JSON.stringify({ slug: staging.slug, scaffoldPath: staging.scaffoldPath, handoff: 'idea2prd-manual', exitCode: 0 }, null, 2));
|
|
15454
16436
|
else {
|
|
@@ -15493,6 +16475,10 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15493
16475
|
else if (form.action === 'deferred' && !json)
|
|
15494
16476
|
write(`dz backlog: ⚠ embed-form migration deferred (${form.error ?? 'unknown error'})`);
|
|
15495
16477
|
const report = await harmonizeBacklog(projectRoot, { apply, ...(thr !== undefined ? { threshold: Number(thr) } : {}) });
|
|
16478
|
+
if (apply) {
|
|
16479
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog harmonize --apply');
|
|
16480
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz backlog harmonize --apply');
|
|
16481
|
+
}
|
|
15496
16482
|
if (json) {
|
|
15497
16483
|
write(JSON.stringify({ ...report, exitCode: 0 }, null, 2));
|
|
15498
16484
|
return 0;
|
|
@@ -16357,15 +17343,17 @@ export async function runCli(argv, io = {}) {
|
|
|
16357
17343
|
case 'teach':
|
|
16358
17344
|
return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
|
|
16359
17345
|
case 'consolidate':
|
|
16360
|
-
return await cmdConsolidate(options, flags, cwd, write);
|
|
17346
|
+
return await cmdConsolidate(options, flags, cwd, write, writeErr);
|
|
16361
17347
|
case 'recall':
|
|
16362
17348
|
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
16363
17349
|
case 'vector':
|
|
16364
|
-
return await cmdVector(options, flags, cwd, write);
|
|
17350
|
+
return await cmdVector(options, flags, cwd, write, writeErr);
|
|
16365
17351
|
case 'brain':
|
|
16366
17352
|
return await cmdBrain(options, flags, cwd, write, readStdin);
|
|
16367
17353
|
case 'statusline':
|
|
16368
|
-
return cmdStatusline(options, flags, cwd, write, readStdin);
|
|
17354
|
+
return cmdStatusline(options, flags, cwd, write, readStdin, writeErr);
|
|
17355
|
+
case 'store-guard':
|
|
17356
|
+
return await cmdStoreGuard(options, flags, cwd, write, writeErr, io.stdin, io.interactive ?? process.stdin.isTTY === true);
|
|
16369
17357
|
case 'usage':
|
|
16370
17358
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
16371
17359
|
case 'chain':
|
|
@@ -16433,7 +17421,7 @@ export async function runCli(argv, io = {}) {
|
|
|
16433
17421
|
case 'mr-rakes':
|
|
16434
17422
|
return await cmdMrRakes(options, flags, cwd, write);
|
|
16435
17423
|
case 'retro':
|
|
16436
|
-
return await cmdRetro(options, flags, cwd, write);
|
|
17424
|
+
return await cmdRetro(options, flags, cwd, write, readStdin);
|
|
16437
17425
|
case 'feature-adr-setup':
|
|
16438
17426
|
return cmdFeatureAdrSetup(options, flags, cwd, write, writeErr);
|
|
16439
17427
|
case 'challenge':
|
|
@@ -16480,6 +17468,8 @@ export async function runCli(argv, io = {}) {
|
|
|
16480
17468
|
return cmdTgPost(options, flags, cwd, write);
|
|
16481
17469
|
case 'name-check':
|
|
16482
17470
|
return cmdNameCheck(options, flags, cwd, write);
|
|
17471
|
+
case 'brief-check':
|
|
17472
|
+
return cmdBriefCheck(options, flags, cwd, write);
|
|
16483
17473
|
case 'provenance-check':
|
|
16484
17474
|
return cmdProvenanceCheck(options, flags, cwd, write);
|
|
16485
17475
|
case 'feature-adr-record':
|
|
@@ -16497,7 +17487,7 @@ export async function runCli(argv, io = {}) {
|
|
|
16497
17487
|
case 'qe-bridge':
|
|
16498
17488
|
return await cmdQeBridge(options, flags, cwd, write);
|
|
16499
17489
|
case 'backlog':
|
|
16500
|
-
return await cmdBacklog(options, flags, cwd, write);
|
|
17490
|
+
return await cmdBacklog(options, flags, cwd, write, writeErr);
|
|
16501
17491
|
case 'routing':
|
|
16502
17492
|
return cmdRouting(options, flags, cwd, write);
|
|
16503
17493
|
case 'bto-optimize':
|