@dzhechkov/harness-cli 0.8.9 → 0.8.15
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 +291 -20
- 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 +44 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1635 -191
- 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 +14 -13
- package/sbom.json +71 -21
- package/src/boolean-flags.ts +3 -0
- package/src/cli.ts +1681 -138
- 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,33 +10,57 @@ 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
|
-
import { homedir, tmpdir } from 'node:os';
|
|
16
|
+
import { homedir, hostname, tmpdir } from 'node:os';
|
|
16
17
|
import { createRequire } from 'node:module';
|
|
17
18
|
import { isDeepStrictEqual } from 'node:util';
|
|
18
|
-
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, 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
|
+
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, 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
|
-
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, 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
|
+
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.
|
|
26
27
|
readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, editIdea, clearEmbedStale, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt,
|
|
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, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, 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,9 +69,9 @@ 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
|
-
'bto-optimize', 'dashboard', 'roam', 'import-ecc',
|
|
74
|
+
'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
|
|
51
75
|
];
|
|
52
76
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
53
77
|
|
|
@@ -88,27 +112,29 @@ Usage:
|
|
|
88
112
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
89
113
|
dz epoch-replay --score <judgments.json> --work-order <file> [--slice <name>] [--json] (un-blind against the pre-registered assignment → SUPPORTED only when the two 95% Wilson CIs are DISJOINT, else FALSIFIED / INCONCLUSIVE)
|
|
90
114
|
dz score --slug <feature> [--project <dir>] [--json] (process scorecard for ONE feature-adr run, from its artifacts: ADR confirmation, discrimination, cross-model QE grade, live verification, README-first, learning loop, amendments — descriptive-only, a low score exits 0)
|
|
115
|
+
dz score --all [--project <dir>] [--json] (sweep features/*/.fa-state/score-*.json into the append-only chained scorecards aggregate — descriptive-only, always exits 0)
|
|
91
116
|
dz recap [--day|--week|--month] [--at <ISO date>] [--project <dir>] [--json] (what was done over a window, from records only: deliveries with the grade an independent review STATED — a report naming two grades is reported ambiguous, never guessed — registry publishes, gate verdicts, knowledge reuse. --quarter/--half-year/--year are RECOGNISED and REFUSED with the real span in days: there is one complete quarter and the longest record is 174 days. Every section carries its own data-start date, and "the source was not read" never prints as zero. Contaminated measures — commit count, lines, tokens, learning-event volume, inventory counts, lesson count — are not computed, and the report says so. exit 0 reported / 2 refused)
|
|
92
117
|
dz cadence [--window day|week|month|quarter|halfyear|year] [--json] (the WHAT-SHIPPED aggregator: graded-shipment cadence by ISO week + npm-publish cadence (recap cache) + guard repeat decay on the FIXED rule set + recall reuse; a window deeper than 2× the record is REFUSED with the depth named (ADR: a cadence from one point is scale forgery); exit 0 report / 2 refused-window / 1 usage)
|
|
93
118
|
dz qe-rounds (--slug <feature> | --feature-dir <abs>) [--ceiling <n>] [--project <dir>] [--json] (how many Step-8 review rounds has this feature ALREADY had? Reads what dz qe-bridge already wrote — signoff-<runId>.json and failed-*.json under features/<slug>/.fa-state/qe-bridge — and writes nothing itself, so it can answer for runs already past. A round is a runId, not a file; an attempt with no verdict is counted SEPARATELY and never merged; an unreadable record is NAMED and the count is declared a LOWER BOUND. ONE directory, never a union across checkouts. exit 0 under the ceiling / 1 at-or-over — owner decides, the command does not judge whether the rounds were warranted / 2 NOT ESTABLISHED, which is never "zero rounds")
|
|
94
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)
|
|
95
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)
|
|
96
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)
|
|
97
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)
|
|
98
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)
|
|
99
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)
|
|
100
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)
|
|
101
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)
|
|
102
128
|
dz sbom [--pack <name>] [--out <file>] (CycloneDX software bill of materials for the workspace, or for one pack with --pack)
|
|
103
|
-
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)
|
|
104
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)
|
|
105
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)
|
|
106
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)
|
|
107
133
|
dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
|
|
108
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)
|
|
109
|
-
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
|
|
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)
|
|
110
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)
|
|
111
|
-
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)
|
|
112
138
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
113
139
|
dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
|
|
114
140
|
dz backlog goals [--validate] [--project <dir>] [--json] (list/validate the compass at .dz/backlog/goals.json)
|
|
@@ -122,7 +148,7 @@ Usage:
|
|
|
122
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)
|
|
123
149
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
124
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)
|
|
125
|
-
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)
|
|
126
152
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
127
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)
|
|
128
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)
|
|
@@ -142,9 +168,11 @@ Usage:
|
|
|
142
168
|
dz brain expand <kuId> [--source <slug>] [--json] (full-content lookup for a citation kuId; --json emits the full KU object)
|
|
143
169
|
dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
|
|
144
170
|
dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
|
|
145
|
-
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>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state + phase → 📐 SECOND-LINE phase panel; a plain "Step <n>" label that goes BACKWARDS against a slot younger than 90 min is absorbed as a stale duplicate — prefix the label with ⛔ or ⏸ to record a legitimate regression)
|
|
146
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)
|
|
147
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)
|
|
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)
|
|
148
176
|
dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] (enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = root README.md + every discovered package's README.md + features/*/08_qe_report.md + docs/**/*.md (historical feature artifacts are NOT scanned — pass paths explicitly); exit 1 only at/above --fail-on, default high)
|
|
149
177
|
dz lint [paths...] [--json] [--config <file>] [--registry <file>] [--project <dir>] (advisory EN/RU prose-style lint; findings exit 0, incomplete input/policy exits 1, usage exits 2)
|
|
150
178
|
dz pretrain [--project <dir>]
|
|
@@ -169,6 +197,10 @@ Usage:
|
|
|
169
197
|
dz dashboard
|
|
170
198
|
dz roam [--apply] [--slug <slug>]
|
|
171
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)
|
|
172
204
|
dz help
|
|
173
205
|
|
|
174
206
|
Global: --version | -v [--json] (prints this CLI's own semver on one line, exit 0; "unknown" + exit 1 when unresolvable)
|
|
@@ -266,6 +298,35 @@ function discoverSkillsDirs(cwd, explicitSkillsDir) {
|
|
|
266
298
|
async function installSkills(opts) {
|
|
267
299
|
const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
|
|
268
300
|
const skillsDirs = discoverSkillsDirs(cwd, explicitSkillsDir);
|
|
301
|
+
// PREFLIGHT (backlog 9d15b9b6, PR-A) — resolve the REQUEST once, before anything is written.
|
|
302
|
+
//
|
|
303
|
+
// Two defects lived in asking each root independently instead of resolving the request: a skill
|
|
304
|
+
// present in two roots was installed TWICE and counted twice (the field report's `2 skill(s)` was
|
|
305
|
+
// one skill installed twice), and a skill present in NO root produced a warning and exit 0 —
|
|
306
|
+
// `0 skill(s)` reading as success. Both are gone once the decision happens here.
|
|
307
|
+
//
|
|
308
|
+
// Placement is load-bearing: an exit 1 that arrives after hooks and memory are written leaves a
|
|
309
|
+
// half-configured project, which is worse than either clean outcome. This runs before the loop
|
|
310
|
+
// below and before every target adapter.
|
|
311
|
+
//
|
|
312
|
+
// Dependency closure is deliberately NOT resolved here — that is PR-B. This preflight fixes the
|
|
313
|
+
// count and the exit contract, and gives that work a base it can trust.
|
|
314
|
+
if (select !== undefined) {
|
|
315
|
+
const roots = skillsDirs.map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
316
|
+
const resolution = resolveSelection(select, roots);
|
|
317
|
+
for (const shadow of resolution.shadowed) {
|
|
318
|
+
opts.writeErr?.(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; ` +
|
|
319
|
+
`installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
322
|
+
if (refusal !== null) {
|
|
323
|
+
return {
|
|
324
|
+
selectRefusal: refusal,
|
|
325
|
+
results: [], dirsSearched: skillsDirs.length, written: 0, skipped: 0,
|
|
326
|
+
missing: [...resolution.missing], failures: [], applyFailures: [], integrations: [],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
269
330
|
// agents-md and gemini are FLATTENING single-file targets: each must aggregate
|
|
270
331
|
// every selected skill from ALL discovered dirs into ONE root file (AGENTS.md /
|
|
271
332
|
// GEMINI.md) in a single merge. A per-dir runInit loop (like the tree targets
|
|
@@ -398,6 +459,7 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
398
459
|
cwd,
|
|
399
460
|
explicitSkillsDir,
|
|
400
461
|
select,
|
|
462
|
+
writeErr,
|
|
401
463
|
force: flags.has('force'),
|
|
402
464
|
enrich: flags.has('enrich'),
|
|
403
465
|
noHooks: flags.has('no-hooks'),
|
|
@@ -405,6 +467,12 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
405
467
|
noVerify: flags.has('no-verify'),
|
|
406
468
|
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
407
469
|
});
|
|
470
|
+
// PR-A: an explicit --select that named a skill no root provides is a REFUSAL, not a warning.
|
|
471
|
+
// Printed and returned here, before any target adapter runs — nothing has been written yet.
|
|
472
|
+
if (r.selectRefusal !== undefined) {
|
|
473
|
+
writeErr(r.selectRefusal);
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
408
476
|
// Codex keeps its established user-registry writer, but its result is normalized into the same
|
|
409
477
|
// two-outcome contract before JSON/human rendering. A write without a live ready observation is
|
|
410
478
|
// a refusal with applied=true, never a second success channel.
|
|
@@ -711,7 +779,7 @@ async function cmdScout(options, flags, cwd, write) {
|
|
|
711
779
|
write('Scanning 9 sources (GitHub + npm + HN + MCP Registry + Glama + OSSInsight + Smithery + Semantic Scholar + arXiv)...\n');
|
|
712
780
|
try {
|
|
713
781
|
const scanTopics = topicsArg ? topicsArg.split(',').map((t) => t.trim()) : undefined;
|
|
714
|
-
const { results: repos, totalBySource } = await scanAllSources({
|
|
782
|
+
const { results: repos, totalBySource, statusBySource } = await scanAllSources({
|
|
715
783
|
token,
|
|
716
784
|
topics: scanTopics,
|
|
717
785
|
since,
|
|
@@ -723,10 +791,17 @@ async function cmdScout(options, flags, cwd, write) {
|
|
|
723
791
|
.map(([src, count]) => `${src}: ${count}`)
|
|
724
792
|
.join(', ');
|
|
725
793
|
write(`Sources: ${sourceLines}`);
|
|
726
|
-
// Memory: diff with previous scan
|
|
794
|
+
// Memory: diff with previous scan.
|
|
795
|
+
//
|
|
796
|
+
// СОСТОЯНИЕ ИСТОЧНИКОВ ПЕРЕДАЁТСЯ ОБЯЗАТЕЛЬНО. Без него разность не выводит исчезновений
|
|
797
|
+
// вообще — и это правильно: источник, ответивший кодом ошибки, раньше делал ВСЕ свои записи
|
|
798
|
+
// «пропавшими» на экране, то есть отчёт печатал факт о нашей сети как факт о мире.
|
|
727
799
|
if (showDiff || memory.size > 0) {
|
|
728
|
-
const
|
|
729
|
-
|
|
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) {
|
|
730
805
|
write(memory.diffMarkdown(diff));
|
|
731
806
|
}
|
|
732
807
|
else if (memory.size > 0) {
|
|
@@ -1553,6 +1628,47 @@ function cmdBundle(options, flags, cwd, write) {
|
|
|
1553
1628
|
}
|
|
1554
1629
|
return 0;
|
|
1555
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
|
+
}
|
|
1556
1672
|
async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
1557
1673
|
const pkg = options.get('_positional_0');
|
|
1558
1674
|
if (!pkg) {
|
|
@@ -1611,17 +1727,38 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
|
1611
1727
|
if (targetResolution.via === 'alias')
|
|
1612
1728
|
writeErr(formatTargetAliasNote('dz install', targetOpt, target));
|
|
1613
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
|
+
}
|
|
1614
1741
|
// Step 1: npm install the package (installRunner is the CliIo test seam — unset in production)
|
|
1615
1742
|
write(`Installing ${specResolution.npmSpec}${specResolution.kind === 'name' ? '' : ` (${specResolution.kind} → node_modules/${specResolution.dirName})`}...`);
|
|
1616
|
-
const installCmd =
|
|
1743
|
+
const installCmd = buildInstallCommand(specResolution.npmSpec, projectRoot);
|
|
1617
1744
|
try {
|
|
1618
1745
|
if (installRunner)
|
|
1619
1746
|
installRunner(installCmd, projectRoot);
|
|
1747
|
+
// БЕЗ ОБОЛОЧКИ. execFileSync с массивом аргументов не запускает shell, поэтому имя пакета или
|
|
1748
|
+
// путь с `$(...)` подставить нечему. Строка выше — для показа и для тестового шва, не для
|
|
1749
|
+
// исполнения (см. докстринг buildInstallCommand).
|
|
1620
1750
|
else
|
|
1621
|
-
|
|
1751
|
+
execFileSync('npm', [...buildInstallArgs(specResolution.npmSpec, projectRoot)], { cwd: projectRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
1622
1752
|
}
|
|
1623
1753
|
catch (err) {
|
|
1624
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 не делает.`);
|
|
1625
1762
|
return 1;
|
|
1626
1763
|
}
|
|
1627
1764
|
// Step 2: Find SKILL.md files in the installed package — under the RESOLVED dir name, never the
|
|
@@ -1872,7 +2009,7 @@ function cmdStatuslineInstall(options, cwd, write) {
|
|
|
1872
2009
|
* `--kind <feature-adr|loop>` identifies the producer, defaults to `feature-adr`, and rejects any
|
|
1873
2010
|
* other value rather than silently weakening panel arbitration.
|
|
1874
2011
|
*/
|
|
1875
|
-
function cmdStatuslineFaRecord(options, cwd, write) {
|
|
2012
|
+
function cmdStatuslineFaRecord(options, cwd, write, writeErr) {
|
|
1876
2013
|
const slug = (options.get('slug') ?? '').trim();
|
|
1877
2014
|
const step = (options.get('step') ?? '').trim();
|
|
1878
2015
|
// `--recalled auto` derives the count from the recall-usage log for `--run <id>`, replacing the
|
|
@@ -1932,18 +2069,40 @@ function cmdStatuslineFaRecord(options, cwd, write) {
|
|
|
1932
2069
|
write(' Example: dz statusline --fa-record --slug add-user-auth --step "Step 0" --kind feature-adr --recalled 5 --stored 2');
|
|
1933
2070
|
return 1;
|
|
1934
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
|
+
}
|
|
1935
2081
|
const mode = options.get('mode');
|
|
1936
2082
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1937
|
-
const
|
|
2083
|
+
const outcome = writeFeatureAdrStateDetailed(projectRoot, {
|
|
1938
2084
|
kind: kindRaw, slug, step, recalled, stored,
|
|
1939
2085
|
...(reinforced > 0 ? { reinforced } : {}),
|
|
1940
2086
|
...(mode !== undefined && mode.trim() !== '' ? { mode: mode.trim() } : {}),
|
|
2087
|
+
...(tier !== undefined ? { tier } : {}),
|
|
1941
2088
|
});
|
|
2089
|
+
const state = outcome.state;
|
|
1942
2090
|
if (state === undefined) {
|
|
2091
|
+
// [AM-5] A REFUSED slot write is LOUD. A held `fa-phase-slot` lock or an unwritable `.dz` used
|
|
2092
|
+
// to return a bare `undefined`, and a caller reading silence as success is exactly the
|
|
2093
|
+
// "absence of a receipt is not success" class. The reason goes to stderr — diagnosis, not
|
|
2094
|
+
// data — and the EXIT CODE is unchanged for a refusal, because the panel must never break the
|
|
2095
|
+
// pipeline that is only reporting to it.
|
|
2096
|
+
if (outcome.refused !== undefined) {
|
|
2097
|
+
writeErr(`fa-record: slot write refused (${slug}): ${outcome.refused}`);
|
|
2098
|
+
return 0;
|
|
2099
|
+
}
|
|
1943
2100
|
write(`dz statusline --fa-record: could not write learning state under ${projectRoot}/.dz/feature-adr/`);
|
|
1944
2101
|
return 1;
|
|
1945
2102
|
}
|
|
1946
|
-
|
|
2103
|
+
// state.step, not the flag: the monotonic guard may have kept a LATER step against a stale
|
|
2104
|
+
// duplicate record (fa-phase-statusline P1) — print what actually stands in the slot.
|
|
2105
|
+
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`);
|
|
1947
2106
|
return 0;
|
|
1948
2107
|
}
|
|
1949
2108
|
function statuslineEtaStage(step) {
|
|
@@ -2098,16 +2257,21 @@ function statuslineEta(projectRoot, state, nowMs) {
|
|
|
2098
2257
|
* least a minimal `dz` even on total failure.
|
|
2099
2258
|
*
|
|
2100
2259
|
* Flags: `--install` wires it into settings.json; `--fa-record` records a live `/feature-adr`
|
|
2101
|
-
* learning state (WRITES — see {@link cmdStatuslineFaRecord}); `--json` prints the raw data object
|
|
2102
|
-
*
|
|
2260
|
+
* learning state (WRITES — see {@link cmdStatuslineFaRecord}); `--json` prints the raw data object
|
|
2261
|
+
* (plus `featureAdrLine`, the rendered phase line, when a fresh /feature-adr run is in flight);
|
|
2262
|
+
* default prints the status line, with the 📐 phase panel as its OWN SECOND LINE (format B —
|
|
2263
|
+
* fa-phase-statusline ADR-001 D1; Claude Code renders every stdout line of a statusline command).
|
|
2264
|
+
* The ETA fragment main shipped for that panel rides the SECOND line with it (fa-phase-statusline ADR-001 D1) — the panel
|
|
2265
|
+
* moved, the estimate was not dropped.
|
|
2103
2266
|
*/
|
|
2104
|
-
function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
2267
|
+
function cmdStatusline(options, flags, cwd, write, readStdin, writeErr) {
|
|
2105
2268
|
if (flags.has('install'))
|
|
2106
2269
|
return cmdStatuslineInstall(options, cwd, write);
|
|
2107
2270
|
if (flags.has('fa-record'))
|
|
2108
|
-
return cmdStatuslineFaRecord(options, cwd, write);
|
|
2271
|
+
return cmdStatuslineFaRecord(options, cwd, write, writeErr);
|
|
2109
2272
|
try {
|
|
2110
2273
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
2274
|
+
warnLearningStoreRead(projectRoot, writeErr, 'dz statusline');
|
|
2111
2275
|
const data = statuslineData(projectRoot);
|
|
2112
2276
|
const fa = data.featureAdr;
|
|
2113
2277
|
let eta;
|
|
@@ -2124,27 +2288,56 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
2124
2288
|
etaFragment = undefined;
|
|
2125
2289
|
}
|
|
2126
2290
|
}
|
|
2291
|
+
// fa-phase-statusline (ADR-001 D1): the phase line renders from the slot ALONE — a pure
|
|
2292
|
+
// function over data.featureAdr, computed once here for both the plain and --json surfaces.
|
|
2293
|
+
const phaseLine = fa !== undefined ? renderFeatureAdrPhaseLine(fa) : undefined;
|
|
2127
2294
|
if (flags.has('json')) {
|
|
2128
|
-
write(JSON.stringify({
|
|
2295
|
+
write(JSON.stringify({
|
|
2296
|
+
...data,
|
|
2297
|
+
...(eta !== undefined ? { eta } : {}),
|
|
2298
|
+
...(phaseLine !== undefined ? { featureAdrLine: phaseLine } : {}),
|
|
2299
|
+
}));
|
|
2129
2300
|
return 0;
|
|
2130
2301
|
}
|
|
2131
|
-
|
|
2302
|
+
const breakdown = data.patternBreakdown;
|
|
2303
|
+
let line = breakdown === undefined
|
|
2304
|
+
? `🎓 dz: ${data.patterns} patterns`
|
|
2305
|
+
: `🎓 dz: ${data.patterns} (${breakdown.active} актив${breakdown.quarantined > 0
|
|
2306
|
+
? ` · ${breakdown.quarantined} карантин${breakdown.attention ? ' ⚠' : ''}`
|
|
2307
|
+
: ''})${breakdown.tierDelta !== undefined ? ` ⚠ тиры Δ${breakdown.tierDelta}` : ''}`;
|
|
2308
|
+
if (data.storeHealth?.verdict === 'collapsed') {
|
|
2309
|
+
line += ` ⛔ ОБВАЛ: было ${data.storeHealth.previousMax ?? '?'} · dz store-guard --reset`;
|
|
2310
|
+
}
|
|
2311
|
+
else if (data.storeHealth?.verdict === 'cold-start-over-existing') {
|
|
2312
|
+
line += ` ⛔ СТОР ПУСТ: было ${data.storeHealth.previousMax ?? '?'} · восстановить из снимков ${data.storeHealth.snapshotPath ?? ''}`.trimEnd();
|
|
2313
|
+
}
|
|
2314
|
+
else if (data.storeHealth?.verdict === 'unreadable') {
|
|
2315
|
+
line += ` ⛔ СТОР НЕЧИТАЕМ${data.storeHealth.unreadableFiles !== undefined && data.storeHealth.unreadableFiles.length > 0
|
|
2316
|
+
? `: ${data.storeHealth.unreadableFiles.join(', ')}` : ''}`;
|
|
2317
|
+
}
|
|
2318
|
+
else if (data.storeHealth?.verdict === 'source-changed') {
|
|
2319
|
+
line += ' ⚠ смена источника хранения';
|
|
2320
|
+
}
|
|
2321
|
+
line += `${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
|
|
2132
2322
|
const branch = statuslineGitBranch(projectRoot);
|
|
2133
2323
|
if (branch !== undefined)
|
|
2134
2324
|
line += ` · ⎇ ${branch}`;
|
|
2135
2325
|
if (data.consolidatedAgeH !== undefined)
|
|
2136
2326
|
line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
2137
|
-
// Live
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
}
|
|
2143
|
-
else {
|
|
2144
|
-
line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2145
|
-
}
|
|
2327
|
+
// Live loop run in flight → PREPEND its segment to the base dz line (unchanged). A live
|
|
2328
|
+
// /feature-adr run no longer glues into line 1: its 📐 segment IS the second line (format B) —
|
|
2329
|
+
// Claude Code renders every stdout line of a statusline command (fa-phase-statusline ADR-001 D1).
|
|
2330
|
+
if (fa !== undefined && fa.kind === 'loop') {
|
|
2331
|
+
line = `🔁 loop ${fa.step} · ${line}`;
|
|
2146
2332
|
}
|
|
2147
2333
|
write(line);
|
|
2334
|
+
// fa-phase-statusline ADR-001 D1: the phase panel moved to line 2 and TOOK main's ETA fragment with it. The move is
|
|
2335
|
+
// the point of format B (ADR-001 D1); dropping the estimate would have been a silent
|
|
2336
|
+
// regression of a feature `main` shipped while this branch was stranded, so it rides here
|
|
2337
|
+
// instead. A phase line that renders (fresh, non-terminal, non-loop slot) is the only gate.
|
|
2338
|
+
if (phaseLine !== undefined) {
|
|
2339
|
+
write(`${phaseLine}${etaFragment !== undefined ? ` · ${etaFragment}` : ''}`);
|
|
2340
|
+
}
|
|
2148
2341
|
return 0;
|
|
2149
2342
|
}
|
|
2150
2343
|
catch {
|
|
@@ -2728,6 +2921,259 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
|
|
|
2728
2921
|
const reason = teachReasonPhrase(target.reason);
|
|
2729
2922
|
return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
|
|
2730
2923
|
}
|
|
2924
|
+
function inspectLearningStore(projectRoot) {
|
|
2925
|
+
const mark = readStoreMark(projectRoot);
|
|
2926
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
2927
|
+
return { mark, rows, health: checkStoreHealth({ projectRoot, ...rows, mark }) };
|
|
2928
|
+
}
|
|
2929
|
+
function shellQuote(value) {
|
|
2930
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2931
|
+
}
|
|
2932
|
+
function storeGuardResetCommand(projectRoot) {
|
|
2933
|
+
return `dz store-guard --reset --project ${shellQuote(projectRoot)}`;
|
|
2934
|
+
}
|
|
2935
|
+
function lexicalSourceLines(rows) {
|
|
2936
|
+
return [
|
|
2937
|
+
` lexical selected: ${rows.lexicalSourcePath} (${rows.lexicalSource}, ${rows.lexicalRows} rows)`,
|
|
2938
|
+
...(rows.lexicalIgnoredSourcePath === undefined ? [] : [
|
|
2939
|
+
` lexical ignored: ${rows.lexicalIgnoredSourcePath} (${rows.lexicalIgnoredRows} rows)`,
|
|
2940
|
+
]),
|
|
2941
|
+
];
|
|
2942
|
+
}
|
|
2943
|
+
function storeGuardRecoveryLines(projectRoot, mark, health, rows) {
|
|
2944
|
+
const snapshots = storeSnapshotPath(projectRoot);
|
|
2945
|
+
const markPath = storeGuardPath(projectRoot);
|
|
2946
|
+
let lexicalSnapshot;
|
|
2947
|
+
let vectorSnapshot = false;
|
|
2948
|
+
try {
|
|
2949
|
+
const names = existsSync(snapshots) ? readdirSync(snapshots) : [];
|
|
2950
|
+
const hasSqlite = names.some((name) => /^lexical\..+\.sqlite$/.test(name));
|
|
2951
|
+
const hasJsonl = names.some((name) => /^lexical\..+\.jsonl$/.test(name));
|
|
2952
|
+
const preferred = mark?.lexicalSource === 'sqlite' || mark?.lexicalSource === 'jsonl'
|
|
2953
|
+
? mark.lexicalSource
|
|
2954
|
+
: rows?.lexicalSource;
|
|
2955
|
+
if (preferred === 'jsonl' && hasJsonl)
|
|
2956
|
+
lexicalSnapshot = 'jsonl';
|
|
2957
|
+
else if (preferred === 'sqlite' && hasSqlite)
|
|
2958
|
+
lexicalSnapshot = 'sqlite';
|
|
2959
|
+
else if (hasSqlite)
|
|
2960
|
+
lexicalSnapshot = 'sqlite';
|
|
2961
|
+
else if (hasJsonl)
|
|
2962
|
+
lexicalSnapshot = 'jsonl';
|
|
2963
|
+
vectorSnapshot = names.some((name) => /^vector\..+\.sqlite$/.test(name));
|
|
2964
|
+
}
|
|
2965
|
+
catch {
|
|
2966
|
+
lexicalSnapshot = undefined;
|
|
2967
|
+
vectorSnapshot = false;
|
|
2968
|
+
}
|
|
2969
|
+
const lines = [
|
|
2970
|
+
`dz store guard: REFUSED — ${health.reason}`,
|
|
2971
|
+
` mark: ${markPath}`,
|
|
2972
|
+
...(mark === undefined ? [] : [` recorded rows: lexical=${mark.lexicalMax} (${mark.lexicalSource}), vector=${mark.vectorMax}`]),
|
|
2973
|
+
...(rows === undefined ? [] : lexicalSourceLines(rows)),
|
|
2974
|
+
];
|
|
2975
|
+
if (lexicalSnapshot !== undefined && vectorSnapshot) {
|
|
2976
|
+
const lexicalDestination = lexicalSnapshot === 'sqlite'
|
|
2977
|
+
? join(projectRoot, '.dz', 'memory', 'patterns.sqlite')
|
|
2978
|
+
: join(projectRoot, '.dz', 'patterns.jsonl');
|
|
2979
|
+
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'))}`);
|
|
2980
|
+
}
|
|
2981
|
+
else {
|
|
2982
|
+
lines.push(` snapshots: none found in ${snapshots}/`, ` create one manually: scripts/dz-store-snapshot.sh --project ${shellQuote(projectRoot)}`);
|
|
2983
|
+
}
|
|
2984
|
+
lines.push(` accept current counts: ${storeGuardResetCommand(projectRoot)}`, ' continue intentionally: set DZ_ALLOW_COLD_START=1 or pass --allow-cold-start');
|
|
2985
|
+
return lines;
|
|
2986
|
+
}
|
|
2987
|
+
function observedRows(rows) {
|
|
2988
|
+
return typeof rows.lexicalRows === 'number' && typeof rows.vectorRows === 'number'
|
|
2989
|
+
? { lexicalRows: rows.lexicalRows, vectorRows: rows.vectorRows, lexicalSource: rows.lexicalSource }
|
|
2990
|
+
: undefined;
|
|
2991
|
+
}
|
|
2992
|
+
/** Mark maintenance is diagnostic: the store operation already completed and must keep its exit code. */
|
|
2993
|
+
function refreshLearningStoreMark(projectRoot, writeErr, command, options = {}) {
|
|
2994
|
+
try {
|
|
2995
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
2996
|
+
const counts = observedRows(rows);
|
|
2997
|
+
if (counts === undefined) {
|
|
2998
|
+
writeErr(`⚠ dz store guard: ${options.reader ? 'reader observation' : 'store operation'} completed but the external mark was not updated — a store tier is unreadable`);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
writeStoreMark(projectRoot, {
|
|
3002
|
+
...counts,
|
|
3003
|
+
observedAt: new Date().toISOString(),
|
|
3004
|
+
command,
|
|
3005
|
+
}, options.reader ? { timeoutMs: 0 } : {});
|
|
3006
|
+
}
|
|
3007
|
+
catch (error) {
|
|
3008
|
+
if (options.reader && error instanceof NamedLockTimeoutError)
|
|
3009
|
+
return;
|
|
3010
|
+
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)}`);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
function storeGuardResetReminder(projectRoot, writeErr, command) {
|
|
3014
|
+
try {
|
|
3015
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
3016
|
+
const mark = readStoreMark(projectRoot);
|
|
3017
|
+
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)}`);
|
|
3018
|
+
for (const line of lexicalSourceLines(rows))
|
|
3019
|
+
writeErr(line);
|
|
3020
|
+
}
|
|
3021
|
+
catch (error) {
|
|
3022
|
+
writeErr(`⚠ DZ STORE GUARD — ${command}: store changed; inspect it and reconcile explicitly with ${storeGuardResetCommand(projectRoot)} (${error instanceof Error ? error.message : String(error)})`);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
/** Fail closed for store writers, except for an explicit per-process/per-command override. */
|
|
3026
|
+
function allowLearningStoreWrite(projectRoot, flags, writeErr, command) {
|
|
3027
|
+
let inspection;
|
|
3028
|
+
try {
|
|
3029
|
+
inspection = inspectLearningStore(projectRoot);
|
|
3030
|
+
}
|
|
3031
|
+
catch (error) {
|
|
3032
|
+
const health = {
|
|
3033
|
+
verdict: 'unreadable',
|
|
3034
|
+
reason: `cannot read the external mark (${error instanceof Error ? error.message : String(error)})`,
|
|
3035
|
+
};
|
|
3036
|
+
for (const line of storeGuardRecoveryLines(projectRoot, undefined, health))
|
|
3037
|
+
writeErr(line);
|
|
3038
|
+
return false;
|
|
3039
|
+
}
|
|
3040
|
+
if (inspection.health.verdict === 'no-mark' || inspection.health.verdict === 'ok') {
|
|
3041
|
+
// The successful write path records the resulting counts. Refreshing here as
|
|
3042
|
+
// well would emit the same telemetry failure twice when the external mark is
|
|
3043
|
+
// unavailable, and would receipt a source transition before the command's
|
|
3044
|
+
// own row had landed.
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
if (inspection.health.verdict === 'source-changed') {
|
|
3048
|
+
const counts = observedRows(inspection.rows);
|
|
3049
|
+
if (counts === undefined)
|
|
3050
|
+
return false;
|
|
3051
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — SOURCE CHANGED: ${inspection.health.reason}; ${storeGuardResetCommand(projectRoot)}`);
|
|
3052
|
+
for (const line of lexicalSourceLines(inspection.rows))
|
|
3053
|
+
writeErr(line);
|
|
3054
|
+
try {
|
|
3055
|
+
// Consume the single migration allowance BEFORE the store write. If the
|
|
3056
|
+
// following command fails, the safe result is a consumed allowance that
|
|
3057
|
+
// requires an explicit reset, never a silently reusable permission.
|
|
3058
|
+
writeStoreMark(projectRoot, { ...counts, observedAt: new Date().toISOString(), command }, {
|
|
3059
|
+
expectedPreviousLexicalSource: inspection.mark?.lexicalSource ?? 'unknown',
|
|
3060
|
+
});
|
|
3061
|
+
return true;
|
|
3062
|
+
}
|
|
3063
|
+
catch (error) {
|
|
3064
|
+
writeErr(`dz store guard: REFUSED — source-change allowance could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const allowed = process.env.DZ_ALLOW_COLD_START === '1' || flags.has('allow-cold-start');
|
|
3069
|
+
if (allowed) {
|
|
3070
|
+
const label = inspection.health.verdict.replaceAll('-', ' ').toUpperCase();
|
|
3071
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — ${label}: ${inspection.health.reason}; explicit cold-start override accepted`);
|
|
3072
|
+
return true;
|
|
3073
|
+
}
|
|
3074
|
+
for (const line of storeGuardRecoveryLines(projectRoot, inspection.mark, inspection.health, inspection.rows))
|
|
3075
|
+
writeErr(line);
|
|
3076
|
+
return false;
|
|
3077
|
+
}
|
|
3078
|
+
/** Readers warn on damage and bootstrap/refresh a healthy non-empty store mark. */
|
|
3079
|
+
function warnLearningStoreRead(projectRoot, writeErr, command) {
|
|
3080
|
+
try {
|
|
3081
|
+
const { health, rows, mark } = inspectLearningStore(projectRoot);
|
|
3082
|
+
if (health.verdict === 'collapsed' || health.verdict === 'cold-start-over-existing' || health.verdict === 'unreadable'
|
|
3083
|
+
|| health.verdict === 'source-changed') {
|
|
3084
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — ${health.verdict.replaceAll('-', ' ').toUpperCase()}: ${health.reason}`);
|
|
3085
|
+
for (const line of lexicalSourceLines(rows))
|
|
3086
|
+
writeErr(line);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
const counts = observedRows(rows);
|
|
3090
|
+
if (counts !== undefined && counts.lexicalRows + counts.vectorRows > 0
|
|
3091
|
+
&& (mark === undefined || mark.lexicalLast !== counts.lexicalRows || mark.vectorLast !== counts.vectorRows
|
|
3092
|
+
|| mark.lexicalSource !== counts.lexicalSource
|
|
3093
|
+
|| mark.lexicalMax < counts.lexicalRows || mark.vectorMax < counts.vectorRows)) {
|
|
3094
|
+
refreshLearningStoreMark(projectRoot, writeErr, command, { reader: true });
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
catch (error) {
|
|
3098
|
+
writeErr(`⚠ DZ STORE GUARD WARNING — external mark unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
async function cmdStoreGuard(options, flags, cwd, write, writeErr, stdinText, interactive) {
|
|
3102
|
+
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3103
|
+
const path = storeGuardPath(projectRoot);
|
|
3104
|
+
const reset = flags.has('reset');
|
|
3105
|
+
const status = flags.has('status') || options.has('status');
|
|
3106
|
+
if (reset && status) {
|
|
3107
|
+
writeErr('dz store-guard: --status and --reset are mutually exclusive');
|
|
3108
|
+
return 2;
|
|
3109
|
+
}
|
|
3110
|
+
if (reset) {
|
|
3111
|
+
const rows = countLearningStoreRowsReadonly(projectRoot);
|
|
3112
|
+
const counts = observedRows(rows);
|
|
3113
|
+
if (counts === undefined) {
|
|
3114
|
+
writeErr(`dz store-guard: REFUSED — cannot reset from an unreadable store; mark: ${path}`);
|
|
3115
|
+
return 1;
|
|
3116
|
+
}
|
|
3117
|
+
try {
|
|
3118
|
+
const previous = readStoreMark(projectRoot);
|
|
3119
|
+
const beforeLexical = previous?.lexicalMax ?? counts.lexicalRows;
|
|
3120
|
+
const beforeVector = previous?.vectorMax ?? counts.vectorRows;
|
|
3121
|
+
writeErr('⚠ dz store-guard --reset: manual operator decision required; this lowers the recorded high-water evidence');
|
|
3122
|
+
writeErr(` old maximum: lexical=${beforeLexical}, vector=${beforeVector}`);
|
|
3123
|
+
writeErr(` new observed: lexical=${counts.lexicalRows} (${counts.lexicalSource}), vector=${counts.vectorRows}`);
|
|
3124
|
+
for (const line of lexicalSourceLines(rows))
|
|
3125
|
+
writeErr(line);
|
|
3126
|
+
let answer = stdinText?.trim().split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? '';
|
|
3127
|
+
if (!flags.has('yes') && answer === '' && interactive && process.stdin.isTTY) {
|
|
3128
|
+
const { createInterface } = await import('node:readline/promises');
|
|
3129
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3130
|
+
try {
|
|
3131
|
+
answer = (await rl.question(' type yes to continue: ')).trim().toLowerCase();
|
|
3132
|
+
}
|
|
3133
|
+
finally {
|
|
3134
|
+
rl.close();
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
if (!flags.has('yes') && !['y', 'yes', 'да'].includes(answer)) {
|
|
3138
|
+
writeErr(`dz store-guard: REFUSED — reset was not confirmed; re-run with --yes or answer yes`);
|
|
3139
|
+
return 1;
|
|
3140
|
+
}
|
|
3141
|
+
const current = countLearningStoreRowsReadonly(projectRoot);
|
|
3142
|
+
const currentCounts = observedRows(current);
|
|
3143
|
+
if (currentCounts === undefined || !isDeepStrictEqual(currentCounts, counts)) {
|
|
3144
|
+
writeErr('dz store-guard: REFUSED — store counts changed after confirmation; inspect and confirm again');
|
|
3145
|
+
return 1;
|
|
3146
|
+
}
|
|
3147
|
+
const mark = resetStoreMark(projectRoot, {
|
|
3148
|
+
...counts,
|
|
3149
|
+
observedAt: new Date().toISOString(),
|
|
3150
|
+
command: 'dz store-guard --reset',
|
|
3151
|
+
});
|
|
3152
|
+
write(`dz store-guard: RESET — accepted lexical=${mark.lexicalMax}, vector=${mark.vectorMax}`);
|
|
3153
|
+
write(` mark: ${path}`);
|
|
3154
|
+
write(` receipt: ${mark.resetAt?.at} — ${mark.resetAt?.reason}`);
|
|
3155
|
+
return 0;
|
|
3156
|
+
}
|
|
3157
|
+
catch (error) {
|
|
3158
|
+
writeErr(`dz store-guard: reset failed — ${error instanceof Error ? error.message : String(error)}; mark: ${path}`);
|
|
3159
|
+
return 1;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
try {
|
|
3163
|
+
const inspection = inspectLearningStore(projectRoot);
|
|
3164
|
+
write(`dz store-guard: ${inspection.health.verdict.toUpperCase()} — ${inspection.health.reason}`);
|
|
3165
|
+
write(` mark: ${path}`);
|
|
3166
|
+
write(` current rows: lexical=${inspection.rows.lexicalRows}, vector=${inspection.rows.vectorRows}`);
|
|
3167
|
+
for (const line of lexicalSourceLines(inspection.rows))
|
|
3168
|
+
write(line);
|
|
3169
|
+
write(` recorded: ${inspection.mark === undefined ? 'none' : JSON.stringify(inspection.mark)}`);
|
|
3170
|
+
return 0;
|
|
3171
|
+
}
|
|
3172
|
+
catch (error) {
|
|
3173
|
+
writeErr(`dz store-guard: mark unreadable — ${error instanceof Error ? error.message : String(error)}; mark: ${path}`);
|
|
3174
|
+
return 1;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
2731
3177
|
async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
|
|
2732
3178
|
const backend = resolveLearningBackend(projectRoot);
|
|
2733
3179
|
backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
|
|
@@ -2755,6 +3201,13 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2755
3201
|
// repo's own store holds 361 records written under that behaviour, and every other user's store
|
|
2756
3202
|
// is the same. Only an explicit choice moves it.
|
|
2757
3203
|
const { storeRoot, target: teachTarget } = resolved;
|
|
3204
|
+
const teachWillWrite = flags.has('harmonize')
|
|
3205
|
+
? false
|
|
3206
|
+
: options.has('from-json')
|
|
3207
|
+
|| (options.get('reinforce') ?? '').trim() !== ''
|
|
3208
|
+
|| (options.get('_positional_0') ?? '').trim() !== '';
|
|
3209
|
+
if (teachWillWrite && !allowLearningStoreWrite(storeRoot, flags, writeErr, 'dz teach'))
|
|
3210
|
+
return 1;
|
|
2758
3211
|
// The verb is per OUTCOME, not per command: a harmonize dry-run and a failed --reinforce READ
|
|
2759
3212
|
// the store and change nothing, so saying "written" there is a false claim about what happened
|
|
2760
3213
|
// (cross-family QE round 2, 2026-08-27).
|
|
@@ -2793,10 +3246,11 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2793
3246
|
// worse defect than the invisibility it was closing (measured live, cross-family QE round 2).
|
|
2794
3247
|
if (!flags.has('json'))
|
|
2795
3248
|
write(storeLine(flags.has('apply') ? 'written' : 'read'));
|
|
2796
|
-
|
|
3249
|
+
const code = await runHarmonize(storeRoot, options, flags, write, writeErr, {
|
|
2797
3250
|
store: join(storeRoot, '.dz'),
|
|
2798
3251
|
storeChosenBy: teachTarget.reason,
|
|
2799
3252
|
});
|
|
3253
|
+
return code;
|
|
2800
3254
|
}
|
|
2801
3255
|
// Bulk import: `dz teach --from-json <file>` ingests a `dz recall --all --json`
|
|
2802
3256
|
// export (an array of learned patterns) — the portable way to SHARE a learned
|
|
@@ -2894,6 +3348,8 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2894
3348
|
const report = await harmonizeVectorStore(storeRoot, {});
|
|
2895
3349
|
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`);
|
|
2896
3350
|
}
|
|
3351
|
+
if (imported > 0)
|
|
3352
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --from-json');
|
|
2897
3353
|
return 0;
|
|
2898
3354
|
}
|
|
2899
3355
|
const reinforce = options.get('reinforce');
|
|
@@ -2914,6 +3370,7 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2914
3370
|
if (clearedQ.cleared > 0)
|
|
2915
3371
|
write(` ↳ promoted out of quarantine (mirror updated)`);
|
|
2916
3372
|
write(storeLine('written'));
|
|
3373
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --reinforce');
|
|
2917
3374
|
return 0;
|
|
2918
3375
|
}
|
|
2919
3376
|
// HIGH-fix: a no-match must NOT auto-teach the raw argument — callers pass dzIds or truncated
|
|
@@ -2961,6 +3418,7 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
2961
3418
|
const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
|
|
2962
3419
|
if (clearedQ.cleared > 0)
|
|
2963
3420
|
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3421
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach --guard');
|
|
2964
3422
|
return 0;
|
|
2965
3423
|
}
|
|
2966
3424
|
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
@@ -3077,9 +3535,13 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
3077
3535
|
}
|
|
3078
3536
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
3079
3537
|
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3538
|
+
if (stored.records.length > 0) {
|
|
3539
|
+
write(` ID: ${stored.records.map((record) => patternRecordId(record)).join(', ')}`);
|
|
3540
|
+
refreshLearningStoreMark(storeRoot, writeErr, 'dz teach');
|
|
3541
|
+
}
|
|
3080
3542
|
return commandFailed ? 1 : 0;
|
|
3081
3543
|
}
|
|
3082
|
-
async function cmdConsolidate(options, flags, cwd, write) {
|
|
3544
|
+
async function cmdConsolidate(options, flags, cwd, write, writeErr) {
|
|
3083
3545
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3084
3546
|
const sessionsDirOpt = options.get('sessions-dir');
|
|
3085
3547
|
const pruneNoise = flags.has('prune-noise');
|
|
@@ -3103,6 +3565,8 @@ async function cmdConsolidate(options, flags, cwd, write) {
|
|
|
3103
3565
|
write(`dz consolidate --prune-quarantine: removed ${res.removed} expired quarantined lesson(s)`);
|
|
3104
3566
|
if (res.snapshot !== undefined)
|
|
3105
3567
|
write(` snapshot: ${res.snapshot}`);
|
|
3568
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz consolidate --prune-quarantine --apply');
|
|
3569
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz consolidate --prune-quarantine --apply');
|
|
3106
3570
|
return 0;
|
|
3107
3571
|
}
|
|
3108
3572
|
// --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
|
|
@@ -3212,6 +3676,10 @@ async function cmdConsolidate(options, flags, cwd, write) {
|
|
|
3212
3676
|
}
|
|
3213
3677
|
}
|
|
3214
3678
|
catch { /* best-effort — the ranking is advisory, never fails the consolidate */ }
|
|
3679
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz consolidate');
|
|
3680
|
+
if (pruneNoise && applyPrune) {
|
|
3681
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz consolidate --prune-noise --apply');
|
|
3682
|
+
}
|
|
3215
3683
|
return 0;
|
|
3216
3684
|
}
|
|
3217
3685
|
function recallUsagePatternRefs(projectRoot) {
|
|
@@ -3322,7 +3790,7 @@ function cmdRecallUsage(options, flags, projectRoot, write) {
|
|
|
3322
3790
|
* DRY-RUN BY DEFAULT, and it snapshots before deleting: `.dz/` is git-ignored and unversioned, so a
|
|
3323
3791
|
* mistaken id is unrecoverable. Use `dz recall --all --json` to find ids.
|
|
3324
3792
|
*/
|
|
3325
|
-
async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
3793
|
+
async function cmdRecallForget(options, flags, projectRoot, write, writeErr) {
|
|
3326
3794
|
const raw = options.get('forget') ?? '';
|
|
3327
3795
|
const ids = new Set(raw.split(',').map((s) => s.trim()).filter((s) => s !== ''));
|
|
3328
3796
|
if (ids.size === 0) {
|
|
@@ -3348,7 +3816,7 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
|
3348
3816
|
write(`dz recall --forget: DRY RUN — ${found.length} record(s) would be removed. Re-run with --apply.`);
|
|
3349
3817
|
return 0;
|
|
3350
3818
|
}
|
|
3351
|
-
const dest = join(projectRoot,
|
|
3819
|
+
const dest = join(storeSnapshotPath(projectRoot), `forget-${Date.now()}.json`);
|
|
3352
3820
|
const snap = snapshotStore(projectRoot, dest);
|
|
3353
3821
|
if (snap.error !== undefined) {
|
|
3354
3822
|
write(`dz recall --forget: snapshot failed (${snap.error}) — nothing removed; the store is not versioned`);
|
|
@@ -3360,6 +3828,8 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
|
|
|
3360
3828
|
if (result.error !== undefined)
|
|
3361
3829
|
write(` ⚠ ${result.error}`);
|
|
3362
3830
|
write(' the vector mirror still holds them — run `dz vector reindex` to resync');
|
|
3831
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz recall --forget --apply');
|
|
3832
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz recall --forget --apply');
|
|
3363
3833
|
return 0;
|
|
3364
3834
|
}
|
|
3365
3835
|
/**
|
|
@@ -3439,12 +3909,17 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
|
|
|
3439
3909
|
}
|
|
3440
3910
|
async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
|
|
3441
3911
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3912
|
+
warnLearningStoreRead(projectRoot, writeErr, 'dz recall');
|
|
3913
|
+
const globalRootForGuard = globalStoreRoot();
|
|
3914
|
+
if (!sameStore(projectRoot, globalRootForGuard) && existsSync(join(globalRootForGuard, '.dz', 'memory'))) {
|
|
3915
|
+
warnLearningStoreRead(globalRootForGuard, writeErr, 'dz recall');
|
|
3916
|
+
}
|
|
3442
3917
|
const asJson = flags.has('json');
|
|
3443
3918
|
const all = flags.has('all');
|
|
3444
3919
|
if (flags.has('usage'))
|
|
3445
3920
|
return cmdRecallUsage(options, flags, projectRoot, write);
|
|
3446
3921
|
if (options.has('forget'))
|
|
3447
|
-
return cmdRecallForget(options, flags, projectRoot, write);
|
|
3922
|
+
return cmdRecallForget(options, flags, projectRoot, write, writeErr);
|
|
3448
3923
|
if (options.has('promote'))
|
|
3449
3924
|
return cmdRecallPromote(options, flags, projectRoot, write);
|
|
3450
3925
|
// --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
|
|
@@ -4009,7 +4484,7 @@ function renderHarmonize(report, write) {
|
|
|
4009
4484
|
* and `dz teach --harmonize` — ONE implementation, no bespoke second path (QR-6). Guards (AC-6):
|
|
4010
4485
|
* `--apply` + `--dry-run` together is rejected; `--threshold` must be in `(0, 1]`; no flag ⇒ dry-run.
|
|
4011
4486
|
*/
|
|
4012
|
-
async function runHarmonize(projectRoot, options, flags, write,
|
|
4487
|
+
async function runHarmonize(projectRoot, options, flags, write, writeErr,
|
|
4013
4488
|
/**
|
|
4014
4489
|
* Where this harmonize is pointed and what chose it. Under `--json` the human store line is
|
|
4015
4490
|
* suppressed to keep stdout ONE document, so the destination has to travel INSIDE that document
|
|
@@ -4032,6 +4507,10 @@ storeAnnotation) {
|
|
|
4032
4507
|
}
|
|
4033
4508
|
}
|
|
4034
4509
|
const report = await harmonizeVectorStore(projectRoot, { apply, ...(threshold !== undefined ? { threshold } : {}) });
|
|
4510
|
+
if (apply && report.error === undefined) {
|
|
4511
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector harmonize --apply');
|
|
4512
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector harmonize --apply');
|
|
4513
|
+
}
|
|
4035
4514
|
if (flags.has('json')) {
|
|
4036
4515
|
write(JSON.stringify(storeAnnotation !== undefined ? { ...report, ...storeAnnotation } : report));
|
|
4037
4516
|
return report.error !== undefined ? 1 : 0;
|
|
@@ -4039,7 +4518,7 @@ storeAnnotation) {
|
|
|
4039
4518
|
renderHarmonize(report, write);
|
|
4040
4519
|
return report.error !== undefined ? 1 : 0;
|
|
4041
4520
|
}
|
|
4042
|
-
async function cmdVector(options, flags, cwd, write) {
|
|
4521
|
+
async function cmdVector(options, flags, cwd, write, writeErr) {
|
|
4043
4522
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4044
4523
|
const sub = options.get('_positional_0');
|
|
4045
4524
|
if (sub === 'status') {
|
|
@@ -4093,6 +4572,10 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4093
4572
|
if (sub === 'reindex') {
|
|
4094
4573
|
const report = await reindexVectorStore(projectRoot);
|
|
4095
4574
|
if (flags.has('json')) {
|
|
4575
|
+
if (report.error === undefined) {
|
|
4576
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector reindex');
|
|
4577
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector reindex');
|
|
4578
|
+
}
|
|
4096
4579
|
write(JSON.stringify(report));
|
|
4097
4580
|
return report.error !== undefined ? 1 : 0;
|
|
4098
4581
|
}
|
|
@@ -4112,6 +4595,8 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4112
4595
|
if (report.staleTaskTypes.includes('book-knowledge'))
|
|
4113
4596
|
write(' run \`dz brain reindex\` to rebuild the brain\'s book vectors');
|
|
4114
4597
|
}
|
|
4598
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector reindex');
|
|
4599
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz vector reindex');
|
|
4115
4600
|
return 0;
|
|
4116
4601
|
}
|
|
4117
4602
|
if (sub === 'export') {
|
|
@@ -4163,7 +4648,7 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4163
4648
|
// harmonize (alias: dz teach --harmonize) — SEMANTIC dedup of the learned store, NON-DESTRUCTIVE:
|
|
4164
4649
|
// dry-run by default (previews clusters, writes nothing); --apply drops after a restorable backup.
|
|
4165
4650
|
if (sub === 'harmonize') {
|
|
4166
|
-
return runHarmonize(projectRoot, options, flags, write);
|
|
4651
|
+
return runHarmonize(projectRoot, options, flags, write, writeErr);
|
|
4167
4652
|
}
|
|
4168
4653
|
// import <file.rvf> — the missing HALF of the RVF cycle: UPSERT-BY-dzId, never overwrites.
|
|
4169
4654
|
if (sub === 'import') {
|
|
@@ -4174,6 +4659,8 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4174
4659
|
}
|
|
4175
4660
|
const report = await importRvfCheckpoint(projectRoot, resolve(cwd, src), {});
|
|
4176
4661
|
if (flags.has('json')) {
|
|
4662
|
+
if (report.error === undefined)
|
|
4663
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector import');
|
|
4177
4664
|
write(JSON.stringify(report));
|
|
4178
4665
|
return report.error !== undefined ? 1 : 0;
|
|
4179
4666
|
}
|
|
@@ -4186,6 +4673,7 @@ async function cmdVector(options, flags, cwd, write) {
|
|
|
4186
4673
|
if (report.skippedOrphans > 0) {
|
|
4187
4674
|
write(' ↳ orphan vectors have no local pattern — import the text first: dz teach --from-json <recall-export.json>, then re-run dz vector import');
|
|
4188
4675
|
}
|
|
4676
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz vector import');
|
|
4189
4677
|
return 0;
|
|
4190
4678
|
}
|
|
4191
4679
|
write('dz vector — semantic (vector) tier of the learned-pattern store');
|
|
@@ -4860,6 +5348,28 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4860
5348
|
writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
|
|
4861
5349
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4862
5350
|
const presetName = options.get('preset');
|
|
5351
|
+
// PREFLIGHT BEFORE THE FIRST WRITE (backlog 9d15b9b6, PR-A). Step 3 configures the learning
|
|
5352
|
+
// environment and step 4 installs skills, so refusing at step 4 would leave a project that has
|
|
5353
|
+
// memory and hooks but not the skills the operator asked for — a half-configured state worse than
|
|
5354
|
+
// either clean outcome. The request is therefore resolved HERE, before the banner's first step.
|
|
5355
|
+
//
|
|
5356
|
+
// Only an EXPLICIT --select is refused. A preset names skills the package itself ships, so a gap
|
|
5357
|
+
// there is our packaging defect, not the operator's typo, and it is reported by the existing
|
|
5358
|
+
// missing-list rather than by refusing the whole run.
|
|
5359
|
+
const setupSelectRaw = options.get('select');
|
|
5360
|
+
if (setupSelectRaw !== undefined) {
|
|
5361
|
+
const requested = setupSelectRaw.split(',').map((x) => x.trim()).filter((x) => x.length > 0);
|
|
5362
|
+
const roots = discoverSkillsDirs(cwd, options.get('skills-dir')).map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
5363
|
+
const resolution = resolveSelection(requested, roots);
|
|
5364
|
+
for (const shadow of resolution.shadowed) {
|
|
5365
|
+
writeErr(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
|
|
5366
|
+
}
|
|
5367
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
5368
|
+
if (refusal !== null) {
|
|
5369
|
+
writeErr(refusal);
|
|
5370
|
+
return 1;
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
4863
5373
|
write(`\n╔══════════════════════════════════════════════════════╗`);
|
|
4864
5374
|
write(`║ DZ SETUP — Full Environment ║`);
|
|
4865
5375
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
@@ -5001,7 +5511,7 @@ function cmdPretrain(options, cwd, write) {
|
|
|
5001
5511
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
5002
5512
|
return 0;
|
|
5003
5513
|
}
|
|
5004
|
-
function cmdRecommend(options, cwd, write) {
|
|
5514
|
+
function cmdRecommend(options, flags, cwd, write) {
|
|
5005
5515
|
const task = options.get('_positional_0');
|
|
5006
5516
|
if (!task) {
|
|
5007
5517
|
write('dz recommend: task description required');
|
|
@@ -5010,15 +5520,40 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5010
5520
|
}
|
|
5011
5521
|
const registry = buildRegistry(cwd);
|
|
5012
5522
|
const report = recommend(task, registry, cwd);
|
|
5523
|
+
if (flags.has('json')) {
|
|
5524
|
+
write(JSON.stringify(report, null, 2));
|
|
5525
|
+
return 0;
|
|
5526
|
+
}
|
|
5013
5527
|
write(`\n╔══════════════════════════════════════════════════════════════╗`);
|
|
5014
5528
|
write(`║ DZ RECOMMEND — Task Advisor ║`);
|
|
5015
5529
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5016
5530
|
write(`║ Task: ${report.task.slice(0, 52).padEnd(52)}║`);
|
|
5017
|
-
|
|
5018
|
-
|
|
5531
|
+
if (report.topicSource === 'task') {
|
|
5532
|
+
write(`║ Topics: ${report.topics.join(', ').slice(0, 50).padEnd(50)}║`);
|
|
5533
|
+
}
|
|
5534
|
+
else if (report.topicSource === 'project-stack') {
|
|
5535
|
+
write(`║ Topics: ${'not matched in the question'.padEnd(50)}║`);
|
|
5536
|
+
}
|
|
5537
|
+
else {
|
|
5538
|
+
write(`║ Topics: ${'not recognized — no recommendations'.padEnd(50)}║`);
|
|
5539
|
+
}
|
|
5019
5540
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5541
|
+
if (report.topicSource === 'project-stack') {
|
|
5542
|
+
write(`⚠ Тема запроса не распознана — подбор ниже сделан по СТЕКУ ПРОЕКТА, не по вашему вопросу.`);
|
|
5543
|
+
write(` (topic not recognized — recommendations reflect the project stack, not the question)`);
|
|
5544
|
+
write(`PROJECT-STACK SUGGESTIONS`);
|
|
5545
|
+
}
|
|
5546
|
+
else if (report.topicSource === 'none') {
|
|
5547
|
+
write(`Тема запроса не распознана; рекомендаций нет.`);
|
|
5548
|
+
write(`Переформулируйте задачу или используйте dz registry search <слово> / /skill-advisor.`);
|
|
5549
|
+
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
5550
|
+
return 0;
|
|
5551
|
+
}
|
|
5552
|
+
const stackDerived = report.topicSource === 'project-stack';
|
|
5020
5553
|
if (report.presets.length > 0) {
|
|
5021
|
-
write(
|
|
5554
|
+
write(stackDerived
|
|
5555
|
+
? `║ PROJECT-STACK PRESETS ║`
|
|
5556
|
+
: `║ RECOMMENDED PRESETS ║`);
|
|
5022
5557
|
for (const p of report.presets) {
|
|
5023
5558
|
const matched = p.matchedSkills.length > 0 ? ` (${p.matchedSkills.slice(0, 3).join(', ')})` : '';
|
|
5024
5559
|
write(`║ ${p.name.padEnd(15)} ${String(p.skills).padStart(2)} skills coverage: ${String(p.coverage).padStart(2)} topics${matched.padEnd(15)}║`);
|
|
@@ -5026,7 +5561,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5026
5561
|
write(`║${''.padEnd(62)}║`);
|
|
5027
5562
|
}
|
|
5028
5563
|
if (report.skills.length > 0) {
|
|
5029
|
-
write(
|
|
5564
|
+
write(stackDerived
|
|
5565
|
+
? `║ PROJECT-STACK SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`
|
|
5566
|
+
: `║ RECOMMENDED SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`);
|
|
5030
5567
|
for (const s of report.skills.slice(0, 8)) {
|
|
5031
5568
|
const desc = s.description.length > 35 ? s.description.slice(0, 32) + '...' : s.description;
|
|
5032
5569
|
write(`║ ${s.id.padEnd(24)} ${desc.padEnd(36)}║`);
|
|
@@ -5034,7 +5571,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5034
5571
|
write(`║${''.padEnd(62)}║`);
|
|
5035
5572
|
}
|
|
5036
5573
|
if (report.toolkits.length > 0) {
|
|
5037
|
-
write(
|
|
5574
|
+
write(stackDerived
|
|
5575
|
+
? `║ PROJECT-STACK PIPELINE (npx toolkits) ║`
|
|
5576
|
+
: `║ FULL PIPELINE (npx toolkits) ║`);
|
|
5038
5577
|
for (const tk of report.toolkits) {
|
|
5039
5578
|
const desc = tk.description.length > 44 ? tk.description.slice(0, 41) + '...' : tk.description;
|
|
5040
5579
|
write(`║ ${tk.name.padEnd(16)} ${desc.padEnd(44)}║`);
|
|
@@ -5044,13 +5583,17 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5044
5583
|
write(`║${''.padEnd(62)}║`);
|
|
5045
5584
|
}
|
|
5046
5585
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5047
|
-
write(
|
|
5586
|
+
write(stackDerived
|
|
5587
|
+
? `║ PROJECT-STACK PLAN ║`
|
|
5588
|
+
: `║ STEP-BY-STEP PLAN ║`);
|
|
5048
5589
|
for (const step of report.plan) {
|
|
5049
5590
|
const line = step.length > 60 ? step.slice(0, 57) + '...' : step;
|
|
5050
5591
|
write(`║ ${line.padEnd(58)}║`);
|
|
5051
5592
|
}
|
|
5052
5593
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5053
|
-
write(
|
|
5594
|
+
write(stackDerived
|
|
5595
|
+
? `║ PROJECT-STACK QUICK INSTALL ║`
|
|
5596
|
+
: `║ QUICK INSTALL ║`);
|
|
5054
5597
|
const cmd = report.installCommand.length > 58 ? report.installCommand.slice(0, 55) + '...' : report.installCommand;
|
|
5055
5598
|
write(`║ ${cmd.padEnd(58)}║`);
|
|
5056
5599
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
@@ -5586,6 +6129,18 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5586
6129
|
return 1;
|
|
5587
6130
|
}
|
|
5588
6131
|
}
|
|
6132
|
+
const filterStr = options.get('filter');
|
|
6133
|
+
// SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
|
|
6134
|
+
// Parse before the guard pre-flight so its packed-secret scan uses the SAME scoped package set
|
|
6135
|
+
// that publishPackages receives below; an empty resulting list remains an explicit error.
|
|
6136
|
+
let filter;
|
|
6137
|
+
if (filterStr !== undefined) {
|
|
6138
|
+
filter = filterStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6139
|
+
if (filter.length === 0) {
|
|
6140
|
+
write('dz publish: --filter requires a non-empty comma-separated list of package-name substrings');
|
|
6141
|
+
return 1;
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
5589
6144
|
// dz guard pre-flight (ADR-002 option A): publish is the most dangerous, least-reversible self-mutation, so
|
|
5590
6145
|
// it ALWAYS runs the declarative guard first. A HARD violation refuses the publish; `--no-guard "<reason>"`
|
|
5591
6146
|
// is the logged escape hatch (the override lands in .dz/guard-audit.jsonl — visible, never silent).
|
|
@@ -5600,7 +6155,7 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5600
6155
|
write('dz publish: --no-guard requires a reason (it is logged): --no-guard "hotfix, guard re-run after"');
|
|
5601
6156
|
return 1;
|
|
5602
6157
|
}
|
|
5603
|
-
const guardResult = runGuardEvaluation(guardRoot, 'publish', undefined, noGuard);
|
|
6158
|
+
const guardResult = runGuardEvaluation(guardRoot, 'publish', undefined, noGuard, filter);
|
|
5604
6159
|
if (guardResult.verdict === 'block' && noGuard === undefined) {
|
|
5605
6160
|
write('dz publish: ✗ BLOCKED by dz guard (HARD invariant violated):');
|
|
5606
6161
|
for (const v of guardResult.violations.filter((x) => x.severity === 'hard'))
|
|
@@ -5645,21 +6200,6 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5645
6200
|
}
|
|
5646
6201
|
const claimCheckOpt = claimCheckRaw ?? 'warn';
|
|
5647
6202
|
const bumpOnly = flags.has('bump-only');
|
|
5648
|
-
const filterStr = options.get('filter');
|
|
5649
|
-
// SAFETY: trim + drop empty segments (mirrors --select at the top of cmdInit).
|
|
5650
|
-
// Without this, `--filter ""` (e.g. an unset shell var) or a stray comma yields
|
|
5651
|
-
// [''] / ['', 'core'], and publishPackages matches with name.includes(''), which
|
|
5652
|
-
// is true for EVERY package — silently turning a scoped publish into a
|
|
5653
|
-
// whole-monorepo publish. An empty resulting list is an explicit error, never
|
|
5654
|
-
// "match all".
|
|
5655
|
-
let filter;
|
|
5656
|
-
if (filterStr !== undefined) {
|
|
5657
|
-
filter = filterStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5658
|
-
if (filter.length === 0) {
|
|
5659
|
-
write('dz publish: --filter requires a non-empty comma-separated list of package-name substrings');
|
|
5660
|
-
return 1;
|
|
5661
|
-
}
|
|
5662
|
-
}
|
|
5663
6203
|
// SAFETY: dry-run is the DEFAULT. A real publish requires an EXPLICIT opt-in
|
|
5664
6204
|
// via --yes, --confirm, or --no-dry-run. Without one, we never bump or publish.
|
|
5665
6205
|
const wantsLive = flags.has('yes') || flags.has('confirm') || flags.has('no-dry-run');
|
|
@@ -5852,6 +6392,14 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5852
6392
|
if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
|
|
5853
6393
|
write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
|
|
5854
6394
|
}
|
|
6395
|
+
// A dry run stops before build/sign/pack, so it says NOTHING about the gates below that line.
|
|
6396
|
+
// Printing what it did not check is what keeps a clean preview from reading as a clean publish
|
|
6397
|
+
// (measured 2026-09-02: a clean dry run preceded a RED real gate).
|
|
6398
|
+
if (pkg.notVerified && pkg.notVerified.length > 0) {
|
|
6399
|
+
write(` ⓘ холостой прогон НЕ проверял (${pkg.notVerified.length}):`);
|
|
6400
|
+
for (const item of pkg.notVerified)
|
|
6401
|
+
write(` · ${item}`);
|
|
6402
|
+
}
|
|
5855
6403
|
}
|
|
5856
6404
|
return report.errors > 0 ? 1 : 0;
|
|
5857
6405
|
}
|
|
@@ -6811,6 +7359,81 @@ function looksBinaryText(text) {
|
|
|
6811
7359
|
* per-file findings (each enriched with its `file`), and applies the exit-code contract.
|
|
6812
7360
|
* `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
|
|
6813
7361
|
*/
|
|
7362
|
+
/**
|
|
7363
|
+
* `dz chain` — verify EVERY hash-chained journal in one command (W0-chain, backlog bc4ee35c).
|
|
7364
|
+
*
|
|
7365
|
+
* The machinery to verify a chain has worked for weeks. What was missing is the ABILITY TO ASK:
|
|
7366
|
+
* verification lived inside two consumers, each carrying its own hardcoded list of which files are
|
|
7367
|
+
* chained, so a journal could be given a chain and still be checked by nobody. Coverage here is
|
|
7368
|
+
* DERIVED from CHAINED_JOURNALS, never typed — adding a journal to the registry adds it to this
|
|
7369
|
+
* report by construction.
|
|
7370
|
+
*
|
|
7371
|
+
* An ABSENT journal is reported as `absent`, not omitted. Omission and cleanliness are
|
|
7372
|
+
* indistinguishable in a report, and that indistinguishability is how the original blind spot
|
|
7373
|
+
* survived; the same reason `broken` exits NON-ZERO rather than merely printing — a verifier that
|
|
7374
|
+
* reports damage and exits 0 is one no automation can act on, and this verb exists to run unattended.
|
|
7375
|
+
*
|
|
7376
|
+
* A journal that exists but carries NO chained records is `unchained`, which is legal (a log may
|
|
7377
|
+
* predate the chain) and therefore does not fail the command. Calling it a defect would train the
|
|
7378
|
+
* reader to ignore the output — the failure mode already measured once on the doctor's own line.
|
|
7379
|
+
*/
|
|
7380
|
+
function cmdChain(options, flags, cwd, write) {
|
|
7381
|
+
const root = options.get('project') ?? cwd;
|
|
7382
|
+
const journals = CHAINED_JOURNALS.map((journal) => {
|
|
7383
|
+
const path = join(root, journal.rel);
|
|
7384
|
+
if (!existsSync(path)) {
|
|
7385
|
+
return { rel: journal.rel, decides: journal.decides, status: 'absent', chained: 0, defects: 0, detail: 'file not present' };
|
|
7386
|
+
}
|
|
7387
|
+
let text = '';
|
|
7388
|
+
try {
|
|
7389
|
+
text = readFileSync(path, 'utf-8');
|
|
7390
|
+
}
|
|
7391
|
+
catch {
|
|
7392
|
+
// Unreadable is NOT clean. It is the one outcome that must never be quietly folded into
|
|
7393
|
+
// "nothing to report": we did not look, so we know nothing.
|
|
7394
|
+
return { rel: journal.rel, decides: journal.decides, status: 'unreadable', chained: 0, defects: 0, detail: 'file could not be read' };
|
|
7395
|
+
}
|
|
7396
|
+
const v = verifyEventChainText(text);
|
|
7397
|
+
if (v.chained === 0) {
|
|
7398
|
+
return { rel: journal.rel, decides: journal.decides, status: 'unchained', chained: 0, defects: 0, detail: 'present, but no record carries a chain (legal — the log predates chaining)' };
|
|
7399
|
+
}
|
|
7400
|
+
const total = text.split('\n').filter((l) => l.trim() !== '').length;
|
|
7401
|
+
const age = classifyChainDefects(v, total);
|
|
7402
|
+
if (v.ok) {
|
|
7403
|
+
return { rel: journal.rel, decides: journal.decides, status: 'ok', chained: v.chained, defects: 0, detail: `${v.chained} chained record(s), ${v.resets} recorded restart(s)` };
|
|
7404
|
+
}
|
|
7405
|
+
// A break the current unbroken run has already outlived does not make TODAY's records unsound.
|
|
7406
|
+
// Reporting both alike is what made the doctor's equivalent line permanently red for four weeks.
|
|
7407
|
+
const historical = age.inRun.length === 0 && age.runRecords > 0;
|
|
7408
|
+
return {
|
|
7409
|
+
rel: journal.rel,
|
|
7410
|
+
decides: journal.decides,
|
|
7411
|
+
status: historical ? 'healed' : 'broken',
|
|
7412
|
+
chained: v.chained,
|
|
7413
|
+
defects: v.defects.length,
|
|
7414
|
+
detail: historical
|
|
7415
|
+
? `${v.defects.length} defect(s), all BEFORE the current run — the last ${age.runRecords} record(s) are unbroken, so verdicts over those are sound`
|
|
7416
|
+
: `${v.defects.length} defect(s) with NO sound records after them: verdicts computed from this log are unsafe`,
|
|
7417
|
+
};
|
|
7418
|
+
});
|
|
7419
|
+
const failed = journals.filter((j) => j.status === 'broken' || j.status === 'unreadable');
|
|
7420
|
+
const ok = failed.length === 0;
|
|
7421
|
+
if (flags.has('json')) {
|
|
7422
|
+
write(JSON.stringify({ ok, root, journals }, null, 2));
|
|
7423
|
+
return ok ? 0 : 1;
|
|
7424
|
+
}
|
|
7425
|
+
write(`dz chain — ${journals.length} registered journal(s) under ${root}`);
|
|
7426
|
+
write('');
|
|
7427
|
+
const MARK = { ok: '\u2713', healed: '\u2713', unchained: '\u00b7', absent: '\u00b7', broken: '\u2717', unreadable: '\u2717' };
|
|
7428
|
+
for (const j of journals) {
|
|
7429
|
+
write(` ${MARK[j.status] ?? '?'} ${j.rel} — ${j.status}`);
|
|
7430
|
+
write(` ${j.detail}`);
|
|
7431
|
+
write(` decides: ${j.decides}`);
|
|
7432
|
+
}
|
|
7433
|
+
write('');
|
|
7434
|
+
write(ok ? ' all registered journals are sound for present verdicts' : ` ${failed.length} journal(s) UNSAFE — see above`);
|
|
7435
|
+
return ok ? 0 : 1;
|
|
7436
|
+
}
|
|
6814
7437
|
function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
|
|
6815
7438
|
// Reject (never silently coerce) an invalid --fail-on.
|
|
6816
7439
|
const failOnRaw = options.get('fail-on') ?? 'high';
|
|
@@ -7499,7 +8122,19 @@ function cmdAgentsSync(options, flags, cwd, write, writeErr) {
|
|
|
7499
8122
|
writeErr(`dz agents-sync: DRIFT — ${drifted.length} stale/missing section(s); ${effect}`);
|
|
7500
8123
|
for (const finding of drifted)
|
|
7501
8124
|
writeErr(` ${finding.id}: ${finding.file} (${finding.status})`);
|
|
7502
|
-
|
|
8125
|
+
const unregistered = drifted.filter((finding) => finding.status === 'unregistered-section');
|
|
8126
|
+
if (unregistered.length > 0) {
|
|
8127
|
+
// ПОДСКАЗКА, ВЕДУЩАЯ НЕ ТУДА, ХУЖЕ ОТСУТСТВУЮЩЕЙ. Повторный `dz agents-sync` эту находку
|
|
8128
|
+
// НЕ лечит: реестр POLICY_SOURCES ведётся руками, и секция, не вписанная в него, не
|
|
8129
|
+
// попадёт в проекцию сколько ни синхронизируй. Раньше здесь печаталась общая подсказка —
|
|
8130
|
+
// читатель прогнал бы её и снова увидел ту же ошибку.
|
|
8131
|
+
writeErr('→ heal with: объяви секцию в POLICY_SOURCES (packages/@dzhechkov/harness-core/src/agents-policy.ts):');
|
|
8132
|
+
for (const finding of unregistered) {
|
|
8133
|
+
writeErr(` { id: '${finding.id}', file: '${finding.file}', heading: '…', why: '…', operativeClause: '…' }`);
|
|
8134
|
+
}
|
|
8135
|
+
writeErr(' затем: dz agents-sync');
|
|
8136
|
+
}
|
|
8137
|
+
else if (drifted.some((finding) => finding.id === 'dz:policies')) {
|
|
7503
8138
|
writeErr('→ heal with: repair duplicate/unmatched dz:policies markers, then run dz agents-sync');
|
|
7504
8139
|
}
|
|
7505
8140
|
else {
|
|
@@ -7584,7 +8219,9 @@ const DEFAULT_STORE_CAP = 5000;
|
|
|
7584
8219
|
* the rule reports nothing for them — a pre-flight must never become a filesystem sweep.
|
|
7585
8220
|
*/
|
|
7586
8221
|
const MAX_STUB_SCAN_FILES = 400;
|
|
7587
|
-
/**
|
|
8222
|
+
/** Maximum packed-file size read by the publish secret scan. */
|
|
8223
|
+
const SECRET_SCAN_MAX_BYTES = 512 * 1024;
|
|
8224
|
+
/** Read the optional `.dz/guard.json` — `{ rules?, storeCap?, stubWaivers?, secretWaivers? }`. Missing/broken ⇒ defaults. */
|
|
7588
8225
|
function loadGuardConfig(root) {
|
|
7589
8226
|
const p = join(root, '.dz', 'guard.json');
|
|
7590
8227
|
if (!existsSync(p))
|
|
@@ -7621,6 +8258,54 @@ function gatherReadmeCounts(root) {
|
|
|
7621
8258
|
// (target repo without sitedoc — missing-evidence contract). But a file that EXISTS and no longer
|
|
7622
8259
|
// matches its anchored pattern emits a MISMATCH pair (a: -1) — silent non-extraction is the exact
|
|
7623
8260
|
// disease this contour cures (AM-1 applies to the guard path too, not only the CI test).
|
|
8261
|
+
// ЗНАЧКИ ПРОТИВ ДЕРЕВА, а не только README против README (бэклог 9cb30764).
|
|
8262
|
+
//
|
|
8263
|
+
// Прежде правило сверяло только числа МЕЖДУ документами: два согласованных документа могли
|
|
8264
|
+
// хором утверждать одно и то же неверное число, и правило молчало. Значок «пакетов: 52» стоял
|
|
8265
|
+
// при 55 публикуемых на диске — ИЗМЕРЕНО 2026-09-03. Меню обещает 32 блюда, официант
|
|
8266
|
+
// перечисляет 30, кухня готовит 38, и никто в ресторане не знает правду.
|
|
8267
|
+
//
|
|
8268
|
+
// Сверяются только числа, ВЫЧИСЛИМЫЕ ИЗ ДЕРЕВА. Значок «опубликовано в npm» сюда НЕ входит и
|
|
8269
|
+
// это сказано вслух: его источник — реестр, а не рабочая копия, и пара, которая делает вид, что
|
|
8270
|
+
// проверила его, была бы хуже отсутствующей.
|
|
8271
|
+
const badge = (s, name) => {
|
|
8272
|
+
const m = s.match(new RegExp(`img\\.shields\\.io/badge/${name}-(\\d+)`));
|
|
8273
|
+
return m && m[1] ? Number(m[1]) : null;
|
|
8274
|
+
};
|
|
8275
|
+
const publishablePackages = (() => {
|
|
8276
|
+
const dir = join(root, 'packages', '@dzhechkov');
|
|
8277
|
+
if (!existsSync(dir))
|
|
8278
|
+
return null;
|
|
8279
|
+
let n = 0;
|
|
8280
|
+
for (const name of readdirSync(dir)) {
|
|
8281
|
+
const pj = join(dir, name, 'package.json');
|
|
8282
|
+
if (!existsSync(pj))
|
|
8283
|
+
continue;
|
|
8284
|
+
try {
|
|
8285
|
+
const j = JSON.parse(readFileSync(pj, 'utf8'));
|
|
8286
|
+
if (j.private !== true)
|
|
8287
|
+
n += 1;
|
|
8288
|
+
}
|
|
8289
|
+
catch { /* нечитаемый манифест — не считается ни в одну сторону */ }
|
|
8290
|
+
}
|
|
8291
|
+
return n;
|
|
8292
|
+
})();
|
|
8293
|
+
const pkgBadge = badge(rootMd, 'packages');
|
|
8294
|
+
if (pkgBadge !== null && publishablePackages !== null) {
|
|
8295
|
+
pairs.push({ label: 'packages (root badge vs publishable package.json on disk)', a: pkgBadge, b: publishablePackages });
|
|
8296
|
+
}
|
|
8297
|
+
const presetBadge = badge(rootMd, 'presets');
|
|
8298
|
+
if (presetBadge !== null) {
|
|
8299
|
+
pairs.push({ label: 'presets (root badge vs PRESET_NAMES in the build)', a: presetBadge, b: PRESET_NAMES.length });
|
|
8300
|
+
}
|
|
8301
|
+
const targetBadge = badge(rootMd, 'targets');
|
|
8302
|
+
if (targetBadge !== null) {
|
|
8303
|
+
pairs.push({ label: 'targets (root badge vs TARGET_NAMES in the build)', a: targetBadge, b: TARGET_NAMES.length });
|
|
8304
|
+
}
|
|
8305
|
+
const cmdBadge = badge(rootMd, 'CLI%20commands');
|
|
8306
|
+
if (cmdBadge !== null && cliAll !== null) {
|
|
8307
|
+
pairs.push({ label: 'commands (root badge vs cli All Commands)', a: cmdBadge, b: cliAll });
|
|
8308
|
+
}
|
|
7624
8309
|
const sitePair = (rel, re, label) => {
|
|
7625
8310
|
if (cliAll === null || !existsSync(join(root, rel)))
|
|
7626
8311
|
return;
|
|
@@ -8056,8 +8741,9 @@ function gatherVolumeShadowFacts(root, packages) {
|
|
|
8056
8741
|
};
|
|
8057
8742
|
}
|
|
8058
8743
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
8059
|
-
function gatherGuardFacts(op, root, text, storeCap) {
|
|
8744
|
+
function gatherGuardFacts(op, root, text, storeCap, publishFilter) {
|
|
8060
8745
|
const facts = { op };
|
|
8746
|
+
const publishPackageRoots = [];
|
|
8061
8747
|
if (op === 'publish') {
|
|
8062
8748
|
// Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
|
|
8063
8749
|
// stale finding and never a publish blocker.
|
|
@@ -8204,6 +8890,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8204
8890
|
for (const m of manifests)
|
|
8205
8891
|
if (m.name && typeof m.version === 'string')
|
|
8206
8892
|
versionByName.set(m.name, m.version);
|
|
8893
|
+
publishPackageRoots.push(...located
|
|
8894
|
+
.filter(({ dir, m }) => m.private !== true && (publishFilter === undefined
|
|
8895
|
+
|| publishFilter.length === 0
|
|
8896
|
+
|| publishFilter.some((filter) => (m.name ?? '').includes(filter) || dir.includes(filter))))
|
|
8897
|
+
.map(({ dir }) => dir));
|
|
8207
8898
|
const pnpmWorkspace = existsSync(join(root, 'pnpm-workspace.yaml'));
|
|
8208
8899
|
const packages = [];
|
|
8209
8900
|
for (const m of manifests) {
|
|
@@ -8218,6 +8909,95 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8218
8909
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
8219
8910
|
}
|
|
8220
8911
|
facts['packages'] = packages;
|
|
8912
|
+
// sibling-dep-protocol: сырые спеки, БЕЗ подстановки версии. Подставленная версия выглядела бы
|
|
8913
|
+
// как обычный диапазон, и правило потеряло бы ровно то, что проверяет.
|
|
8914
|
+
const siblingDeps = [];
|
|
8915
|
+
for (const m of manifests) {
|
|
8916
|
+
if (m.private === true)
|
|
8917
|
+
continue;
|
|
8918
|
+
const fields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
8919
|
+
for (const field of fields) {
|
|
8920
|
+
const table = m[field];
|
|
8921
|
+
if (typeof table !== 'object' || table === null)
|
|
8922
|
+
continue;
|
|
8923
|
+
for (const [dep, spec] of Object.entries(table)) {
|
|
8924
|
+
if (!dep.startsWith('@dzhechkov/') || typeof spec !== 'string')
|
|
8925
|
+
continue;
|
|
8926
|
+
siblingDeps.push({ name: m.name ?? '(unnamed)', field, dep, spec });
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
}
|
|
8930
|
+
facts['siblingDeps'] = siblingDeps;
|
|
8931
|
+
// plugin-manifest-audit: каждый `.claude-plugin/plugin.json` в дереве. Обход ограничен по
|
|
8932
|
+
// глубине и не заходит в node_modules/dist — чужие манифесты не наши, и краснеть на них
|
|
8933
|
+
// значило бы отчитываться о том, чего мы не публикуем.
|
|
8934
|
+
const pluginManifests = [];
|
|
8935
|
+
const walkPlugins = (dir, depth) => {
|
|
8936
|
+
if (depth > 4)
|
|
8937
|
+
return;
|
|
8938
|
+
let entries;
|
|
8939
|
+
try {
|
|
8940
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
8941
|
+
}
|
|
8942
|
+
catch {
|
|
8943
|
+
return;
|
|
8944
|
+
}
|
|
8945
|
+
for (const e of entries) {
|
|
8946
|
+
// `out/` — СГЕНЕРИРОВАННОЕ публичное зеркало: те же манифесты, скопированные. Дефект в нём
|
|
8947
|
+
// есть дефект генератора, и он уже сообщается по источнику; вторая копия только удвоила бы
|
|
8948
|
+
// одну и ту же находку.
|
|
8949
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name === 'dist' || e.name === 'out')
|
|
8950
|
+
continue;
|
|
8951
|
+
const full = join(dir, e.name);
|
|
8952
|
+
if (e.name === '.claude-plugin') {
|
|
8953
|
+
const manifest = join(full, 'plugin.json');
|
|
8954
|
+
if (!existsSync(manifest))
|
|
8955
|
+
continue;
|
|
8956
|
+
try {
|
|
8957
|
+
const j = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
8958
|
+
const pick = (k) => (typeof j[k] === 'string' ? j[k] : undefined);
|
|
8959
|
+
// Состав навыков — ОБЕ стороны инвентаризации, и ТОЛЬКО для коробки ОДНОГО ПАКЕТА.
|
|
8960
|
+
//
|
|
8961
|
+
// Корневая витрина сюда НЕ входит, и это не упрощение: её состав собирается из всего
|
|
8962
|
+
// монорепозитория через реестр, «что лежит на складе» для неё — не обход одного дерева,
|
|
8963
|
+
// а весь реестр, и ровно это уже проверяет `marketplace-parity` регенерацией. Первая
|
|
8964
|
+
// редакция этой проверки обошла корень с ограничением глубины и выдала 27 ЛОЖНЫХ
|
|
8965
|
+
// «объявлено, но не найдено» — навыки лежали глубже границы обхода (ИЗМЕРЕНО 2026-09-04,
|
|
8966
|
+
// поймано до коммита прогоном стража на этом же дереве).
|
|
8967
|
+
//
|
|
8968
|
+
// Сравниваются ПУТИ, как их объявил манифест, а не имена: два навыка с одинаковым
|
|
8969
|
+
// именем в разных подкаталогах — законная вещь, и сведение к имени их бы склеило.
|
|
8970
|
+
const boxRoot = dirname(full);
|
|
8971
|
+
const isRepoRoot = resolve(boxRoot) === resolve(root);
|
|
8972
|
+
const declaredSkills = Array.isArray(j['skills']) && !isRepoRoot
|
|
8973
|
+
? j['skills'].filter((x) => typeof x === 'string')
|
|
8974
|
+
.map((rel) => rel.replace(/^\.\//, '').replace(/\/+$/, '')).filter(Boolean)
|
|
8975
|
+
: undefined;
|
|
8976
|
+
const skillsOnDisk = declaredSkills === undefined ? undefined : findSkillDirs(boxRoot);
|
|
8977
|
+
pluginManifests.push({
|
|
8978
|
+
path: relative(root, manifest),
|
|
8979
|
+
...(pick('name') !== undefined ? { name: pick('name') } : {}),
|
|
8980
|
+
...(pick('version') !== undefined ? { version: pick('version') } : {}),
|
|
8981
|
+
...(pick('description') !== undefined ? { description: pick('description') } : {}),
|
|
8982
|
+
...(declaredSkills !== undefined ? { declaredSkills } : {}),
|
|
8983
|
+
...(skillsOnDisk !== undefined ? { skillsOnDisk } : {}),
|
|
8984
|
+
});
|
|
8985
|
+
}
|
|
8986
|
+
catch (error) {
|
|
8987
|
+
pluginManifests.push({
|
|
8988
|
+
path: relative(root, manifest),
|
|
8989
|
+
parseError: (error instanceof Error ? error.message : String(error)).replace(/\s+/g, ' ').slice(0, 200),
|
|
8990
|
+
});
|
|
8991
|
+
}
|
|
8992
|
+
continue;
|
|
8993
|
+
}
|
|
8994
|
+
if (e.name.startsWith('.'))
|
|
8995
|
+
continue;
|
|
8996
|
+
walkPlugins(full, depth + 1);
|
|
8997
|
+
}
|
|
8998
|
+
};
|
|
8999
|
+
walkPlugins(root, 0);
|
|
9000
|
+
facts['pluginManifests'] = pluginManifests;
|
|
8221
9001
|
facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
|
|
8222
9002
|
dir,
|
|
8223
9003
|
name: m.name ?? dir,
|
|
@@ -8252,9 +9032,57 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8252
9032
|
}
|
|
8253
9033
|
catch { /* unreadable tree — the rule reports nothing rather than inventing a violation */ }
|
|
8254
9034
|
try {
|
|
8255
|
-
facts['drift'] = sweepSkillDrift(root, { scope:
|
|
9035
|
+
facts['drift'] = sweepSkillDrift(root, { scope: DRIFT_SWEEP_SCOPE, allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
|
|
8256
9036
|
}
|
|
8257
9037
|
catch { /* skip */ }
|
|
9038
|
+
// backlog-covers-features: каталоги фич, дата их ПОЯВЛЕНИЯ В ИСТОРИИ (не mtime — его двигает
|
|
9039
|
+
// любой посторонний процесс), оговорки из README фичи и тексты записей бэклога. Базовая дата
|
|
9040
|
+
// делает правило зелёным на приходе: 336 существующих каталогов заведены до правила.
|
|
9041
|
+
// Нечитаемое дерево ⇒ факт НЕ выставляется ⇒ правило молчит, а не выдумывает вердикт.
|
|
9042
|
+
try {
|
|
9043
|
+
const featDir = join(root, 'features');
|
|
9044
|
+
if (existsSync(featDir)) {
|
|
9045
|
+
const slugs = readdirSync(featDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9046
|
+
const features = slugs.map((slug) => {
|
|
9047
|
+
let createdIso = '';
|
|
9048
|
+
try {
|
|
9049
|
+
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() ?? '';
|
|
9050
|
+
}
|
|
9051
|
+
catch { /* нет в истории — функция засчитает как новый, это верный дефолт */ }
|
|
9052
|
+
let waiver;
|
|
9053
|
+
for (const f of ['README.md', '07_code_changes/change_manifest.md']) {
|
|
9054
|
+
try {
|
|
9055
|
+
const m = readFileSync(join(featDir, slug, f), 'utf-8')
|
|
9056
|
+
.match(/^\s*Backlog:\s*не заведено\s*[—-]\s*(.+)$/m);
|
|
9057
|
+
if (m && m[1] && m[1].trim() !== '') {
|
|
9058
|
+
waiver = m[1].trim();
|
|
9059
|
+
break;
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
catch { /* нет файла — не оговорка */ }
|
|
9063
|
+
}
|
|
9064
|
+
return { slug, createdIso, waiver };
|
|
9065
|
+
});
|
|
9066
|
+
const backlogTexts = [];
|
|
9067
|
+
try {
|
|
9068
|
+
for (const line of readFileSync(join(root, '.dz', 'backlog', 'ideas.jsonl'), 'utf-8').split('\n')) {
|
|
9069
|
+
if (line.trim() === '')
|
|
9070
|
+
continue;
|
|
9071
|
+
try {
|
|
9072
|
+
const o = JSON.parse(line);
|
|
9073
|
+
if (typeof o.text === 'string')
|
|
9074
|
+
backlogTexts.push(o.text);
|
|
9075
|
+
}
|
|
9076
|
+
catch { /* рваная строка */ }
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
catch { /* стора нет */ }
|
|
9080
|
+
if (backlogTexts.length > 0) {
|
|
9081
|
+
facts['featureBacklog'] = { baseline: BACKLOG_COVERAGE_BASELINE, features, backlogTexts };
|
|
9082
|
+
}
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
catch { /* нечитаемо — правило молчит */ }
|
|
8258
9086
|
facts['counts'] = gatherReadmeCounts(root);
|
|
8259
9087
|
// readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: does the
|
|
8260
9088
|
// change set contain its package.json (the version-bump signal) without its README.md?
|
|
@@ -8449,16 +9277,61 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8449
9277
|
catch { /* not a git repo — every template rule stays silent (fail-open) */ }
|
|
8450
9278
|
// no-stubs config waivers: `.dz/guard.json` `stubWaivers: [{path, reason}]` — path-keyed, reason
|
|
8451
9279
|
// MANDATORY (the feature-adr-setup --guards shape; the pure checker refuses a reasonless entry).
|
|
8452
|
-
const
|
|
9280
|
+
const guardConfig = loadGuardConfig(root);
|
|
9281
|
+
const stubWaivers = guardConfig.stubWaivers;
|
|
8453
9282
|
if (Array.isArray(stubWaivers))
|
|
8454
9283
|
facts['stubWaivers'] = stubWaivers;
|
|
9284
|
+
const secretWaivers = guardConfig.secretWaivers;
|
|
9285
|
+
if (Array.isArray(secretWaivers))
|
|
9286
|
+
facts['secretWaivers'] = secretWaivers;
|
|
8455
9287
|
}
|
|
8456
9288
|
if (op === 'consolidate') {
|
|
8457
9289
|
try {
|
|
8458
|
-
facts['drift'] = sweepSkillDrift(root, { scope:
|
|
9290
|
+
facts['drift'] = sweepSkillDrift(root, { scope: DRIFT_SWEEP_SCOPE, allowlist: readDriftAllowlist(root) }).drifted.map((d) => d.name);
|
|
8459
9291
|
}
|
|
8460
9292
|
catch { /* skip */ }
|
|
8461
9293
|
}
|
|
9294
|
+
if (op === 'publish') {
|
|
9295
|
+
const secretTargets = [];
|
|
9296
|
+
let skipped = 0;
|
|
9297
|
+
for (const dir of publishPackageRoots) {
|
|
9298
|
+
const packageRoot = join(root, dir);
|
|
9299
|
+
let packed;
|
|
9300
|
+
try {
|
|
9301
|
+
packed = listPackFiles(packageRoot);
|
|
9302
|
+
}
|
|
9303
|
+
catch {
|
|
9304
|
+
skipped++;
|
|
9305
|
+
continue;
|
|
9306
|
+
}
|
|
9307
|
+
for (const rel of packed) {
|
|
9308
|
+
const absolute = join(packageRoot, rel);
|
|
9309
|
+
try {
|
|
9310
|
+
const stat = lstatSync(absolute);
|
|
9311
|
+
if (!stat.isFile() || stat.size > SECRET_SCAN_MAX_BYTES) {
|
|
9312
|
+
skipped++;
|
|
9313
|
+
continue;
|
|
9314
|
+
}
|
|
9315
|
+
const content = readFileSync(absolute);
|
|
9316
|
+
if (content.subarray(0, 8 * 1024).includes(0)) {
|
|
9317
|
+
skipped++;
|
|
9318
|
+
continue;
|
|
9319
|
+
}
|
|
9320
|
+
secretTargets.push({
|
|
9321
|
+
label: relative(root, absolute).split(sep).join('/'),
|
|
9322
|
+
text: content.toString('utf8'),
|
|
9323
|
+
});
|
|
9324
|
+
}
|
|
9325
|
+
catch {
|
|
9326
|
+
skipped++;
|
|
9327
|
+
}
|
|
9328
|
+
}
|
|
9329
|
+
}
|
|
9330
|
+
if (secretTargets.length > 0)
|
|
9331
|
+
facts['secretTargets'] = secretTargets;
|
|
9332
|
+
if (skipped > 0)
|
|
9333
|
+
facts['secretScan'] = { skipped };
|
|
9334
|
+
}
|
|
8462
9335
|
if (op === 'teach' || op === 'consolidate') {
|
|
8463
9336
|
if (op === 'teach' && text)
|
|
8464
9337
|
facts['secretTargets'] = [{ label: 'lesson', text }];
|
|
@@ -8478,13 +9351,13 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
8478
9351
|
* shared by `dz guard check` and the `dz publish` pre-flight (ADR-002 option A) so they can never disagree.
|
|
8479
9352
|
* `overrideReason` (when the caller forces through a block) is logged, never silent.
|
|
8480
9353
|
*/
|
|
8481
|
-
function runGuardEvaluation(root, op, text, overrideReason) {
|
|
9354
|
+
function runGuardEvaluation(root, op, text, overrideReason, publishFilter) {
|
|
8482
9355
|
const cfg = loadGuardConfig(root);
|
|
8483
9356
|
// Number.isFinite, not just > 0: a config `storeCap: 1e400` parses to Infinity, passes `> 0`, and would
|
|
8484
9357
|
// silently DISABLE the cap (count <= Infinity always). Non-finite ⇒ fall back to the default.
|
|
8485
9358
|
const storeCap = typeof cfg.storeCap === 'number' && Number.isFinite(cfg.storeCap) && cfg.storeCap > 0 ? cfg.storeCap : DEFAULT_STORE_CAP;
|
|
8486
9359
|
const rules = resolveRules(Array.isArray(cfg.rules) ? cfg.rules : undefined);
|
|
8487
|
-
const facts = gatherGuardFacts(op, root, text, storeCap);
|
|
9360
|
+
const facts = gatherGuardFacts(op, root, text, storeCap, publishFilter);
|
|
8488
9361
|
const result = evaluateGuard(facts, rules);
|
|
8489
9362
|
// audit (append-only). ts is real time here (a CLI, not the sandboxed workflow).
|
|
8490
9363
|
try {
|
|
@@ -9320,17 +10193,57 @@ async function cmdMrRakes(options, flags, cwd, write) {
|
|
|
9320
10193
|
* --threshold N drill threshold (default 2 — anti-noise: a first-seen rake accrues, never drills)
|
|
9321
10194
|
* --no-teach drill only; do NOT write the store (skip the agent side)
|
|
9322
10195
|
* --project <dir> pin the teach ledger
|
|
9323
|
-
* --install-hook print the opt-in
|
|
10196
|
+
* --install-hook print the opt-in hook set to add (non-destructive): Stop scan-tail +
|
|
10197
|
+
* PreCompact/SessionEnd full retro (feature narrated-error-must-be-taught)
|
|
10198
|
+
* --scan-tail per-turn Stop-hook mode: incremental admission-debt scan, O(new bytes) —
|
|
10199
|
+
* writes/clears .dz/retro-pending.json; no ledger, no teach, no git subprocess
|
|
10200
|
+
* --transcript <p> the transcript --scan-tail must read. Without it the Stop hook's own stdin
|
|
10201
|
+
* payload (`transcript_path`) is used; with neither, the scan REFUSES
|
|
10202
|
+
* (NOT-ESTABLISHED) rather than guessing the newest file on disk
|
|
9324
10203
|
*/
|
|
9325
|
-
async function cmdRetro(options, flags, cwd, write) {
|
|
10204
|
+
async function cmdRetro(options, flags, cwd, write, readStdin = () => '') {
|
|
10205
|
+
if (flags.has('scan-tail')) {
|
|
10206
|
+
// Per-turn primary detector (ADR-001 D2). Cost budget IS the design: no `git rev-parse`
|
|
10207
|
+
// subprocess (hooks run with cwd = project root; CLAUDE_PROJECT_DIR pins it), no store open,
|
|
10208
|
+
// O(new bytes) via the persisted offset. Always exit 0 — a Stop hook must never fail a turn.
|
|
10209
|
+
const root = process.env['CLAUDE_PROJECT_DIR'] ?? cwd;
|
|
10210
|
+
// WHICH transcript. Round 3, P1-3: this used to fall back to `findLatestTranscript(root)`, so with
|
|
10211
|
+
// several sessions and their subagents alive at once it scanned whichever file had the newest
|
|
10212
|
+
// mtime — routinely another session's, advancing that session's offset and never seeing this
|
|
10213
|
+
// turn's admission. The Stop hook hands the exact path on stdin; with no path from any source the
|
|
10214
|
+
// answer is a stated refusal, never a guess. Still exit 0: a Stop hook must never fail a turn.
|
|
10215
|
+
const picked = resolveScanTailTranscript({
|
|
10216
|
+
flag: options.get('transcript'),
|
|
10217
|
+
positional: options.get('_positional_0'),
|
|
10218
|
+
stdin: readStdin(),
|
|
10219
|
+
});
|
|
10220
|
+
if (picked.path === null) {
|
|
10221
|
+
const reason = picked.reason ?? 'no transcript path';
|
|
10222
|
+
if (flags.has('json'))
|
|
10223
|
+
write(JSON.stringify({ status: 'not-established', source: 'none', reason, scannedBytes: 0, offset: 0 }));
|
|
10224
|
+
else
|
|
10225
|
+
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>\`.`);
|
|
10226
|
+
return 0;
|
|
10227
|
+
}
|
|
10228
|
+
const outcome = runRetroTailScan(join(root, '.dz'), picked.path);
|
|
10229
|
+
if (flags.has('json'))
|
|
10230
|
+
write(JSON.stringify({ ...outcome, source: picked.source }));
|
|
10231
|
+
else if (outcome.status === 'pending')
|
|
10232
|
+
write(`retro scan-tail: unpaid admission — .dz/retro-pending.json armed («${(outcome.snippet ?? '').slice(0, 60)}…»)`);
|
|
10233
|
+
return 0;
|
|
10234
|
+
}
|
|
9326
10235
|
let repoRoot = cwd;
|
|
9327
10236
|
try {
|
|
9328
10237
|
repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim() || cwd;
|
|
9329
10238
|
}
|
|
9330
10239
|
catch { /* not git */ }
|
|
9331
10240
|
if (flags.has('install-hook')) {
|
|
9332
|
-
write('Add
|
|
9333
|
-
write(JSON.stringify({ hooks: {
|
|
10241
|
+
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):');
|
|
10242
|
+
write(JSON.stringify({ hooks: {
|
|
10243
|
+
Stop: [{ hooks: [{ type: 'command', command: 'dz retro --scan-tail', timeout: 10000, continueOnError: true }] }],
|
|
10244
|
+
PreCompact: [{ hooks: [{ type: 'command', command: 'dz retro', timeout: 60000, continueOnError: true }] }],
|
|
10245
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: 'dz retro', timeout: 60000, continueOnError: true }] }],
|
|
10246
|
+
} }, null, 2));
|
|
9334
10247
|
return 0;
|
|
9335
10248
|
}
|
|
9336
10249
|
const transcript = options.get('_positional_0') ?? findLatestTranscript(repoRoot);
|
|
@@ -9593,6 +10506,50 @@ function cmdChallenge(options, flags, cwd, write) {
|
|
|
9593
10506
|
* decide (dz's rule — a false gate kills trust). Exit code is 0 on a clean run regardless of verdict; 2 only on
|
|
9594
10507
|
* a usage/setup error, so a caller distinguishes "gate ran" from "gate could not run".
|
|
9595
10508
|
*/
|
|
10509
|
+
/**
|
|
10510
|
+
* Есть ли в этом каталоге НЕЗАКОММИЧЕННЫЕ правки. Это и отличает «фича ещё в рабочем дереве»
|
|
10511
|
+
* (тогда `HEAD` — законная предфичевая база) от «фича уже закоммичена» (тогда `HEAD` её содержит).
|
|
10512
|
+
*
|
|
10513
|
+
* Не удалось спросить git — возвращается null, и вызывающий обязан считать положение НЕ
|
|
10514
|
+
* УСТАНОВЛЕННЫМ, а не выбрать удобный ответ.
|
|
10515
|
+
*/
|
|
10516
|
+
/**
|
|
10517
|
+
* Каталоги с `SKILL.md` внутри коробки ОДНОГО пакета — то, что РЕАЛЬНО лежит на складе.
|
|
10518
|
+
* Возвращаются ПУТИ относительно коробки, как их объявляет манифест, а не имена.
|
|
10519
|
+
*/
|
|
10520
|
+
function findSkillDirs(boxRoot) {
|
|
10521
|
+
const found = new Set();
|
|
10522
|
+
const walk = (dir, depth) => {
|
|
10523
|
+
if (depth > 4)
|
|
10524
|
+
return;
|
|
10525
|
+
let entries;
|
|
10526
|
+
try {
|
|
10527
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10528
|
+
}
|
|
10529
|
+
catch {
|
|
10530
|
+
return;
|
|
10531
|
+
}
|
|
10532
|
+
for (const e of entries) {
|
|
10533
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.'))
|
|
10534
|
+
continue;
|
|
10535
|
+
const full = join(dir, e.name);
|
|
10536
|
+
if (existsSync(join(full, 'SKILL.md')))
|
|
10537
|
+
found.add(relative(boxRoot, full));
|
|
10538
|
+
walk(full, depth + 1);
|
|
10539
|
+
}
|
|
10540
|
+
};
|
|
10541
|
+
walk(boxRoot, 0);
|
|
10542
|
+
return [...found];
|
|
10543
|
+
}
|
|
10544
|
+
function hasUncommittedChangesIn(repoRoot, dir) {
|
|
10545
|
+
try {
|
|
10546
|
+
const out = execSync(`git status --porcelain -- ${JSON.stringify(dir)}`, { cwd: repoRoot, encoding: 'utf-8' });
|
|
10547
|
+
return out.split('\n').some((line) => line.trim() !== '');
|
|
10548
|
+
}
|
|
10549
|
+
catch {
|
|
10550
|
+
return null;
|
|
10551
|
+
}
|
|
10552
|
+
}
|
|
9596
10553
|
function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
9597
10554
|
let repoRoot = cwd;
|
|
9598
10555
|
try {
|
|
@@ -9606,13 +10563,124 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9606
10563
|
}
|
|
9607
10564
|
const nameFilter = options.get('name');
|
|
9608
10565
|
const propertyTests = testArg.split(',').map((s) => s.trim()).filter(Boolean).map((file) => nameFilter !== undefined && nameFilter.trim() !== '' ? { file, name: nameFilter.trim() } : { file });
|
|
9609
|
-
|
|
10566
|
+
/**
|
|
10567
|
+
* УМОЛЧАНИЕ БАЗЫ РАЗЛИЧАЕТ ДВА РАЗНЫХ ПОЛОЖЕНИЯ, потому что они и правда разные.
|
|
10568
|
+
*
|
|
10569
|
+
* В штатном ходе конвейера правка Шага 7 ЕЩЁ НЕ ЗАКОММИЧЕНА, и тогда `HEAD` — настоящая
|
|
10570
|
+
* предфичевая база; так конвейер и зовёт гейт (`--base HEAD`, причина записана в его тексте).
|
|
10571
|
+
*
|
|
10572
|
+
* Но если фича УЖЕ ЗАКОММИЧЕНА, `HEAD` её содержит, и гейт сравнивает фичу С САМОЙ СОБОЙ.
|
|
10573
|
+
* ИЗМЕРЕНО 2026-09-04: такой прогон дал уверенное `NON_DISCRIMINATING` с советом «усилить тест»
|
|
10574
|
+
* на тестах, которые дискриминируют; тот же прогон с верной базой дал `DISCRIMINATES_VIA_ERROR`.
|
|
10575
|
+
* Находка выглядела как утверждение О ТЕСТАХ, и автор пошёл бы чинить исправное.
|
|
10576
|
+
*
|
|
10577
|
+
* Различить эти положения можно ДЕТЕРМИНИРОВАННО: есть ли незакоммиченные правки в пакете, чьи
|
|
10578
|
+
* тесты названы. Есть — `HEAD` законен и используется как раньше. Нет — база не установлена, и
|
|
10579
|
+
* гейт ОТКАЗЫВАЕТСЯ (exit 3), вместо того чтобы измерять относительно самого себя.
|
|
10580
|
+
*/
|
|
10581
|
+
const explicitBase = options.get('base');
|
|
9610
10582
|
const runnerOpt = options.get('runner');
|
|
9611
10583
|
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
9612
10584
|
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
9613
10585
|
const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
|
|
9614
|
-
|
|
10586
|
+
// Runner honesty (feature instrument-honesty, ADR-001): the runner is selected from the TARGET
|
|
10587
|
+
// package's own scripts.test, never from a global default. The package dir is the nearest
|
|
10588
|
+
// ancestor of the FIRST named test that carries a package.json — walked here, at the seam,
|
|
10589
|
+
// because the pure half deliberately takes the script text as data and never touches the fs.
|
|
10590
|
+
let packageTestScript = null;
|
|
10591
|
+
let packageDevDependencies = [];
|
|
10592
|
+
let packageDir = repoRoot;
|
|
10593
|
+
{
|
|
10594
|
+
const firstTest = propertyTests[0]?.file;
|
|
10595
|
+
// QE-1 (instrument-honesty, HIGH): this walk runs on the RAW --test argument, BEFORE the
|
|
10596
|
+
// engine's sanitation — a `../` traversal made it read an arbitrary package.json OUTSIDE the
|
|
10597
|
+
// repo and echo its scripts.test verbatim into the JSON output (MEASURED with a planted
|
|
10598
|
+
// marker file). Containment first: a start point outside the repo root never gets walked,
|
|
10599
|
+
// the script stays null, and the engine's own path sanitation then refuses the test path.
|
|
10600
|
+
const walkStart = firstTest !== undefined ? resolve(cwd, dirname(firstTest)) : undefined;
|
|
10601
|
+
if (firstTest !== undefined && walkStart !== undefined
|
|
10602
|
+
&& (walkStart === resolve(repoRoot) || walkStart.startsWith(resolve(repoRoot) + sep))) {
|
|
10603
|
+
let probe = walkStart;
|
|
10604
|
+
// walk up to the repo root looking for package.json (bounded by the fs root either way)
|
|
10605
|
+
for (;;) {
|
|
10606
|
+
if (existsSync(join(probe, 'package.json'))) {
|
|
10607
|
+
packageDir = probe;
|
|
10608
|
+
break;
|
|
10609
|
+
}
|
|
10610
|
+
const parent = dirname(probe);
|
|
10611
|
+
if (parent === probe || probe === repoRoot)
|
|
10612
|
+
break;
|
|
10613
|
+
probe = parent;
|
|
10614
|
+
}
|
|
10615
|
+
try {
|
|
10616
|
+
const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'));
|
|
10617
|
+
packageTestScript = typeof pkg.scripts?.test === 'string' ? pkg.scripts.test : null;
|
|
10618
|
+
packageDevDependencies = Object.keys(pkg.devDependencies ?? {});
|
|
10619
|
+
}
|
|
10620
|
+
catch { /* unreadable package.json → selection falls through to the honest REFUSE */ }
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
// The pure half's path sanitation expects a REPO-RELATIVE package dir ('.'-rooted), not an
|
|
10624
|
+
// absolute one — an absolute path is refused as unsafe-package-dir by design.
|
|
10625
|
+
const packageDirRel = relative(repoRoot, packageDir) || '.';
|
|
10626
|
+
// Умолчание базы решается ЗДЕСЬ, потому что только здесь известен пакет, чьи тесты названы.
|
|
10627
|
+
let baseRef;
|
|
10628
|
+
if (explicitBase !== undefined && explicitBase.trim() !== '') {
|
|
10629
|
+
baseRef = explicitBase.trim();
|
|
10630
|
+
}
|
|
10631
|
+
else {
|
|
10632
|
+
const dirty = hasUncommittedChangesIn(repoRoot, packageDirRel);
|
|
10633
|
+
if (dirty !== true) {
|
|
10634
|
+
write('dz discrimination-check: NOT-ESTABLISHED — предфичевая база не установлена.');
|
|
10635
|
+
write(dirty === null
|
|
10636
|
+
? ` Не удалось спросить git о состоянии ${packageDirRel}; выбирать удобный ответ вместо этого нельзя.`
|
|
10637
|
+
: ` В ${packageDirRel} нет незакоммиченных правок, значит фича УЖЕ в HEAD, и сравнение шло бы с самой собой.`);
|
|
10638
|
+
write(' Гейт сравнивает поведение ДО и ПОСЛЕ фичи; без базы сравнивать не с чем, а HEAD здесь');
|
|
10639
|
+
write(' дал бы уверенное NON_DISCRIMINATING на исправных тестах (ИЗМЕРЕНО 2026-09-04).');
|
|
10640
|
+
write(' Передайте --base <коммит перед фичей>, например `<sha коммита фичи>^`.');
|
|
10641
|
+
return 3;
|
|
10642
|
+
}
|
|
10643
|
+
// Штатный ход конвейера: правка Шага 7 ещё в рабочем дереве, HEAD — настоящая предфичевая база.
|
|
10644
|
+
baseRef = 'HEAD';
|
|
10645
|
+
write(`dz discrimination-check: база не задана; в ${packageDirRel} есть незакоммиченные правки, беру HEAD как предфичевую базу.`);
|
|
10646
|
+
}
|
|
10647
|
+
// Провенанс базы едет в квитанцию вместе с самой базой: «явно указано» — утверждение о действии
|
|
10648
|
+
// человека, и писать его для HEAD, выбранного инструментом, значит подделывать происхождение
|
|
10649
|
+
// доказательства, на которое сошлются позже.
|
|
10650
|
+
const baseRefSupplied = explicitBase !== undefined && explicitBase.trim() !== '';
|
|
10651
|
+
const planInput = runnerOpt !== undefined
|
|
10652
|
+
? { baseRef, baseRefSupplied, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
|
|
10653
|
+
: { baseRef, baseRefSupplied, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
|
|
10654
|
+
const plan = planDiscriminationCheck(planInput);
|
|
9615
10655
|
if (!plan.runnable) {
|
|
10656
|
+
// QE-2 (instrument-honesty, MEDIUM): a runner REFUSE used to be reported through the generic
|
|
10657
|
+
// "no property test to check"/map-a-test framing — the operator-facing surface re-created the
|
|
10658
|
+
// exact "instrument gap misread as test gap" class ADR-001 names as the reason three duplicate
|
|
10659
|
+
// backlog entries existed. The plan's own named reason is the verdict; the generic classify
|
|
10660
|
+
// stays only for the genuinely-empty-target case.
|
|
10661
|
+
const runnerRefusal = typeof plan.reason === 'string' && plan.reason.startsWith('unsupported-runner');
|
|
10662
|
+
if (runnerRefusal) {
|
|
10663
|
+
const refusal = {
|
|
10664
|
+
aggregate: 'CANNOT_ISOLATE',
|
|
10665
|
+
measurementValid: false,
|
|
10666
|
+
primaryAction: plan.primaryAction ?? 'fix-runner-invocation',
|
|
10667
|
+
finding: {
|
|
10668
|
+
severity: 'high',
|
|
10669
|
+
verdict: 'CANNOT_ISOLATE',
|
|
10670
|
+
files: plan.targets.map((t) => t.file),
|
|
10671
|
+
detail: `runner refused: ${plan.reason} — the INSTRUMENT could not run, nothing was measured; `
|
|
10672
|
+
+ `declare scripts.test in the target package (or pass --runner) and re-run. `
|
|
10673
|
+
+ `This is NOT a statement about the tests.`,
|
|
10674
|
+
},
|
|
10675
|
+
};
|
|
10676
|
+
if (flags.has('json')) {
|
|
10677
|
+
write(JSON.stringify({ plan, results: [], perTest: [], ...refusal }, null, 2));
|
|
10678
|
+
return 0;
|
|
10679
|
+
}
|
|
10680
|
+
write(`discrimination-check: REFUSED (${plan.reason})`);
|
|
10681
|
+
write(` → ${refusal.finding.detail}`);
|
|
10682
|
+
return 0;
|
|
10683
|
+
}
|
|
9616
10684
|
// No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
|
|
9617
10685
|
const result = classifyDiscrimination({ propertyTests: [], results: [] });
|
|
9618
10686
|
if (flags.has('json')) {
|
|
@@ -9717,10 +10785,24 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9717
10785
|
}
|
|
9718
10786
|
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
9719
10787
|
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
10788
|
+
// Runner honesty (ADR-001): the run executes FROM the target package dir with a
|
|
10789
|
+
// package-relative path — a root-cwd `npx vitest run packages/...` loads the ROOT config
|
|
10790
|
+
// (none) and reds unclassifiably, which is exactly the CANNOT_ISOLATE artifact this
|
|
10791
|
+
// feature removes. The plan's own commands encode the same cd; this body mirrors it.
|
|
10792
|
+
const pkgRel = plan.packageDir === '.' ? '' : plan.packageDir;
|
|
10793
|
+
const fileInPkg = pkgRel !== '' && t.file.startsWith(pkgRel + '/') ? t.file.slice(pkgRel.length + 1) : t.file;
|
|
10794
|
+
const execDirBase = pkgRel === '' ? worktree : join(worktree, pkgRel);
|
|
10795
|
+
const execDirTip = pkgRel === '' ? repoRoot : join(repoRoot, pkgRel);
|
|
9720
10796
|
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
10797
|
+
// NO `--` before the path: MEASURED 2026-09-02 — `npx vitest run -- 'file'` IGNORES the
|
|
10798
|
+
// filter and runs the whole suite (5269 tests), which is the exact whole-repo artifact
|
|
10799
|
+
// this feature removes (QE ha-intake-archive F5). The path is engine-sanitized (no
|
|
10800
|
+
// leading dash, no metacharacters), so it can never be read as an option.
|
|
10801
|
+
const cmd = `${runner}${nameArg} '${fileInPkg}'`;
|
|
10802
|
+
const base = runCapturedTest(cmd, execDirBase, timeoutMs);
|
|
10803
|
+
// The classifier's targetSeen is a substring probe: the run now prints PACKAGE-relative
|
|
10804
|
+
// paths, so it must be probed with the same form, or every hit reads as target-unseen.
|
|
10805
|
+
const evidence = classifyExecutionEvidence(base.output, base.exitCode, fileInPkg);
|
|
9724
10806
|
const outcome = discriminationOutcomeOf(base.exitCode, evidence);
|
|
9725
10807
|
const row = t.name !== undefined
|
|
9726
10808
|
? { file: t.file, name: t.name, outcome, evidence }
|
|
@@ -9731,8 +10813,8 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9731
10813
|
// base rows per the matrix; running it is cheap and only ever on an already-broken path.
|
|
9732
10814
|
// Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
|
|
9733
10815
|
if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
|
|
9734
|
-
const tip = runCapturedTest(cmd,
|
|
9735
|
-
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode,
|
|
10816
|
+
const tip = runCapturedTest(cmd, execDirTip, timeoutMs);
|
|
10817
|
+
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, fileInPkg);
|
|
9736
10818
|
row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
|
|
9737
10819
|
row['tipEvidence'] = tipEvidence;
|
|
9738
10820
|
// R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
|
|
@@ -9841,51 +10923,6 @@ function readTipTreeConditions(repoRoot) {
|
|
|
9841
10923
|
function nameFor(t, outcome) {
|
|
9842
10924
|
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
9843
10925
|
}
|
|
9844
|
-
/**
|
|
9845
|
-
* `dz mutation-gate` — the mutation gate (feature ha-mutation-gate, SPEC at
|
|
9846
|
-
* features/ha-mutation-gate/SPEC.md). A green test proves the code works; it does NOT prove the
|
|
9847
|
-
* test would notice the protection being DELETED. For each entry in a declarative registry this
|
|
9848
|
-
* gate applies the entry's exact {find, replace} mutation to a SCRATCH COPY of the package, runs
|
|
9849
|
-
* the package's test command there, and REQUIRES a non-zero exit (red). All logic is in
|
|
9850
|
-
* harness-core's pure engine (mutation-gate.ts); this is the I/O executor.
|
|
9851
|
-
*
|
|
9852
|
-
* --package <dir> the package to gate (default: cwd; must contain package.json)
|
|
9853
|
-
* --registry <file> registry path (default: <pkg>/test/mutation-registry.json, then
|
|
9854
|
-
* <pkg>/mutation-registry.json)
|
|
9855
|
-
* --test-cmd '<cmd>' suite command run in the scratch copy (default: registry.testCommand,
|
|
9856
|
-
* then `npm test`)
|
|
9857
|
-
* --only <id[,id]> run a subset of entries (unknown id = usage error, never a silent skip)
|
|
9858
|
-
* --timeout <ms> per-suite-run timeout (default 300000). A timed-out run is INCONCLUSIVE —
|
|
9859
|
-
* a FAILURE, never a pass.
|
|
9860
|
-
* --rebaseline <m> route-b guard mode: 'per-entry' (default — every red entry re-runs the
|
|
9861
|
-
* suite on the restored tree; not green ⇒ that entry is INCONCLUSIVE) or
|
|
9862
|
-
* 'final' (one re-run at the end; not green ⇒ every red-based verdict is
|
|
9863
|
-
* downgraded). The gate's PROVEN now means the redness was ATTRIBUTABLE.
|
|
9864
|
-
* --keep-scratch keep the scratch copy for inspection (default: removed in a finally)
|
|
9865
|
-
* --json machine contract {packageDir, registryPath, testCommand, rebaselineMode,
|
|
9866
|
-
* baseline, results, summary, warnings, exitCode}
|
|
9867
|
-
*
|
|
9868
|
-
* The four rules (SPEC §"Four rules") and where each is enforced:
|
|
9869
|
-
* 1. does-not-apply = FAILURE → core classifyMutationOutcome (occurrences !== 1 ⇒ NOT_APPLIED);
|
|
9870
|
-
* 2. green suite = FAILURE → core (exit 0 ⇒ UNDEFENDED, names the property);
|
|
9871
|
-
* 3. never mutate the working tree → HERE: every write targets the scratch copy under tmpdir();
|
|
9872
|
-
* the repo tree is opened read-only, and a crashed run leaves at worst a stale tmp dir;
|
|
9873
|
-
* 4. the gate's own discrimination proof → harness-cli/test/fixtures/mutation-gate-undefended
|
|
9874
|
-
* (the gate MUST fail on it; asserted by test/mutation-gate-cli.test.ts).
|
|
9875
|
-
*
|
|
9876
|
-
* Exit codes: 0 every entry PROVEN · 1 the gate ran and failed (undefended / not-applied /
|
|
9877
|
-
* below-min / unparseable / load-fatal / over-failing / inconclusive entry) · 2 usage or setup
|
|
9878
|
-
* error (missing registry, red BASELINE — a red unmutated copy proves nothing and must not be
|
|
9879
|
-
* read as a mutation result — or an entry whose file RESOLVES outside the scratch copy: a
|
|
9880
|
-
* symlink escape is refused before anything is written, SPEC rule 3).
|
|
9881
|
-
*/
|
|
9882
|
-
/**
|
|
9883
|
-
* Route-a guard for `dz mutation-gate`: parse-check a MUTATED file as its own language BEFORE the
|
|
9884
|
-
* suite runs. A registry mutation must delete the protection while keeping the file loadable — a
|
|
9885
|
-
* file that no longer parses kills the whole suite (or its import chain), and that STRUCTURAL
|
|
9886
|
-
* redness says nothing about the named protection. Returns `{error}` when a parser ran and the
|
|
9887
|
-
* text does not parse; `{skipped}` (reported loudly, never silently) when no parser is available.
|
|
9888
|
-
*/
|
|
9889
10926
|
function parseCheckMutatedFile(absFile, text) {
|
|
9890
10927
|
const ext = extname(absFile).toLowerCase();
|
|
9891
10928
|
try {
|
|
@@ -9916,18 +10953,36 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9916
10953
|
}
|
|
9917
10954
|
}
|
|
9918
10955
|
if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '') {
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
10956
|
+
const checked = runWithOneInternalRetry(() => {
|
|
10957
|
+
try {
|
|
10958
|
+
// `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
|
|
10959
|
+
execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
|
|
10960
|
+
return {};
|
|
10961
|
+
}
|
|
10962
|
+
catch (e) {
|
|
10963
|
+
const err = e;
|
|
10964
|
+
// A launched parser that exits non-zero with a SyntaxError is a parse verdict. A child
|
|
10965
|
+
// launch/internal error (EPERM, ENOENT, Node's thrown internal) is runner infrastructure
|
|
10966
|
+
// and must take the bounded retry → INCONCLUSIVE route instead of masquerading as bad JS.
|
|
10967
|
+
if (typeof err.code === 'string' || typeof err.status !== 'number')
|
|
10968
|
+
throw e;
|
|
10969
|
+
const stderrLines = String(err.stderr ?? '').split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
10970
|
+
const msg = [...stderrLines].reverse().find((line) => line.includes('Error'))
|
|
10971
|
+
?? stderrLines.at(-1)
|
|
10972
|
+
?? err.message
|
|
10973
|
+
?? 'node --check failed';
|
|
10974
|
+
return { error: msg.slice(0, 200) };
|
|
10975
|
+
}
|
|
10976
|
+
});
|
|
10977
|
+
if (checked.value === null) {
|
|
10978
|
+
return {
|
|
10979
|
+
internalFailureReason: checked.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
10980
|
+
internalAttempts: checked.attempts,
|
|
10981
|
+
};
|
|
9930
10982
|
}
|
|
10983
|
+
return checked.internalRetries === 1
|
|
10984
|
+
? { ...checked.value, internalAttempts: checked.attempts }
|
|
10985
|
+
: checked.value;
|
|
9931
10986
|
}
|
|
9932
10987
|
return { skipped: `no parser for '${ext}' files — parse-check unavailable` };
|
|
9933
10988
|
}
|
|
@@ -9935,7 +10990,7 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9935
10990
|
return { skipped: `parse-check errored: ${String(e.message).slice(0, 120)}` };
|
|
9936
10991
|
}
|
|
9937
10992
|
}
|
|
9938
|
-
function cmdMutationGate(options, flags, cwd, write) {
|
|
10993
|
+
function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
9939
10994
|
const json = flags.has('json');
|
|
9940
10995
|
const fail = (what) => {
|
|
9941
10996
|
write(json ? JSON.stringify({ error: what, exitCode: 2 }) : `dz mutation-gate: ${what}`);
|
|
@@ -10003,6 +11058,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10003
11058
|
const results = [];
|
|
10004
11059
|
const observations = [];
|
|
10005
11060
|
const warnings = [];
|
|
11061
|
+
const internalRetries = [];
|
|
10006
11062
|
let baseline;
|
|
10007
11063
|
try {
|
|
10008
11064
|
if (gitTop !== null && gitTop !== pkgDir && resolve(pkgDir).startsWith(resolve(gitTop) + sep)) {
|
|
@@ -10046,7 +11102,10 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10046
11102
|
// write below is asserted to RESOLVE inside this root before it happens.
|
|
10047
11103
|
const realScratchRoot = realpathSync(copyDir);
|
|
10048
11104
|
const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
|
|
10049
|
-
const
|
|
11105
|
+
const invokeSuite = () => {
|
|
11106
|
+
if (injectedRunner !== undefined) {
|
|
11107
|
+
return injectedRunner(testCmd, { cwd: copyDir, timeoutMs: timeout });
|
|
11108
|
+
}
|
|
10050
11109
|
const run = spawnSync(testCmd, {
|
|
10051
11110
|
cwd: copyDir,
|
|
10052
11111
|
shell: true,
|
|
@@ -10058,6 +11117,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10058
11117
|
const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
|
|
10059
11118
|
? run.error.code
|
|
10060
11119
|
: undefined;
|
|
11120
|
+
// Node may populate both `error` and a numeric `status` for an internal spawn failure. The
|
|
11121
|
+
// error wins except for the two already-named resource observations: a status alongside
|
|
11122
|
+
// EPERM/Unreachable-code is not a suite verdict and takes the one-retry internal-error path.
|
|
11123
|
+
if (run.error !== undefined && errorCode !== 'ETIMEDOUT' && errorCode !== 'ENOBUFS') {
|
|
11124
|
+
throw run.error;
|
|
11125
|
+
}
|
|
10061
11126
|
const signal = typeof run.signal === 'string' ? run.signal : undefined;
|
|
10062
11127
|
let failureReason;
|
|
10063
11128
|
if (typeof run.status !== 'number') {
|
|
@@ -10074,25 +11139,59 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10074
11139
|
}
|
|
10075
11140
|
return {
|
|
10076
11141
|
exitCode: typeof run.status === 'number' ? run.status : null,
|
|
10077
|
-
// Receipt markers may be on stderr. Preserve both streams even on exit 0; stdout-only
|
|
10078
|
-
// collection would silently lose a green-run marker.
|
|
10079
11142
|
output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
|
|
10080
11143
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
10081
11144
|
};
|
|
10082
11145
|
};
|
|
11146
|
+
const runSuite = (phase, entryId) => {
|
|
11147
|
+
const retried = runWithOneInternalRetry(invokeSuite);
|
|
11148
|
+
const loggedAttempts = retried.attempts.map((attempt) => {
|
|
11149
|
+
if (attempt.outcome !== 'completed' || retried.value === null)
|
|
11150
|
+
return attempt;
|
|
11151
|
+
const outcome = retried.value.exitCode === null
|
|
11152
|
+
? `no exit code (${retried.value.failureReason ?? 'unnamed failure'})`
|
|
11153
|
+
: `exit ${retried.value.exitCode}`;
|
|
11154
|
+
return { ...attempt, detail: `attempt ${attempt.attempt}: completed — ${outcome}` };
|
|
11155
|
+
});
|
|
11156
|
+
if (retried.internalRetries === 1) {
|
|
11157
|
+
const record = entryId === undefined
|
|
11158
|
+
? { phase, attempts: loggedAttempts }
|
|
11159
|
+
: { phase, entryId, attempts: loggedAttempts };
|
|
11160
|
+
internalRetries.push(record);
|
|
11161
|
+
if (!json)
|
|
11162
|
+
write(`mutation-gate: internal retry — ${loggedAttempts.map((attempt) => attempt.detail).join('; ')}`);
|
|
11163
|
+
}
|
|
11164
|
+
const internalAttemptLog = retried.internalRetries === 1
|
|
11165
|
+
? loggedAttempts.map((attempt) => attempt.detail).join('; ')
|
|
11166
|
+
: undefined;
|
|
11167
|
+
if (retried.value !== null) {
|
|
11168
|
+
return {
|
|
11169
|
+
...retried.value,
|
|
11170
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
11171
|
+
};
|
|
11172
|
+
}
|
|
11173
|
+
return {
|
|
11174
|
+
exitCode: null,
|
|
11175
|
+
output: '',
|
|
11176
|
+
failureReason: retried.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
11177
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
11178
|
+
};
|
|
11179
|
+
};
|
|
10083
11180
|
// Baseline BEFORE any mutation: a red copy proves nothing, and reading it as a mutation
|
|
10084
11181
|
// result would be this gate shipping the defect class it exists to catch.
|
|
10085
11182
|
if (!json)
|
|
10086
11183
|
write(`mutation-gate: baseline suite in scratch copy of ${pkgDir} …`);
|
|
10087
|
-
const base = runSuite();
|
|
10088
|
-
baseline = classifyBaseline(base.exitCode, base.failureReason
|
|
11184
|
+
const base = runSuite('baseline');
|
|
11185
|
+
baseline = classifyBaseline(base.exitCode, base.failureReason, base.exitCode !== null && base.exitCode !== 0
|
|
11186
|
+
? attributeBaselineRedness(base.output, entries.map((entry) => entry.file))
|
|
11187
|
+
: undefined);
|
|
10089
11188
|
if (!baseline.ok) {
|
|
10090
11189
|
if (json) {
|
|
10091
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], exitCode:
|
|
10092
|
-
return
|
|
11190
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
|
|
11191
|
+
return 1;
|
|
10093
11192
|
}
|
|
10094
11193
|
write(renderMutationReport([], baseline, pkgDir));
|
|
10095
|
-
return
|
|
11194
|
+
return 1;
|
|
10096
11195
|
}
|
|
10097
11196
|
for (const entry of entries) {
|
|
10098
11197
|
const filePath = join(copyDir, entry.file);
|
|
@@ -10135,6 +11234,8 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10135
11234
|
write(`mutation-gate: ${entry.id} — mutating ${entry.file}, running suite …`);
|
|
10136
11235
|
let run = null;
|
|
10137
11236
|
let parseError;
|
|
11237
|
+
let parseInternalFailureReason;
|
|
11238
|
+
let parseInternalAttemptLog;
|
|
10138
11239
|
try {
|
|
10139
11240
|
writeFileSync(filePath, applied.text);
|
|
10140
11241
|
// Route-a guard: the mutated file must still PARSE — a load failure reddens the whole
|
|
@@ -10145,11 +11246,18 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10145
11246
|
if (!json)
|
|
10146
11247
|
write(`mutation-gate: WARNING ${entry.id}: parse-check skipped — ${check.skipped}`);
|
|
10147
11248
|
}
|
|
11249
|
+
if (check.internalAttempts !== undefined) {
|
|
11250
|
+
internalRetries.push({ phase: 'parse-check', entryId: entry.id, attempts: check.internalAttempts });
|
|
11251
|
+
parseInternalAttemptLog = check.internalAttempts.map((attempt) => attempt.detail).join('; ');
|
|
11252
|
+
if (!json)
|
|
11253
|
+
write(`mutation-gate: internal retry — ${parseInternalAttemptLog}`);
|
|
11254
|
+
}
|
|
11255
|
+
parseInternalFailureReason = check.internalFailureReason;
|
|
10148
11256
|
if (check.error !== undefined) {
|
|
10149
11257
|
parseError = check.error; // no suite run: the verdict is MUTATION_UNPARSEABLE regardless
|
|
10150
11258
|
}
|
|
10151
|
-
else {
|
|
10152
|
-
run = runSuite();
|
|
11259
|
+
else if (parseInternalFailureReason === undefined) {
|
|
11260
|
+
run = runSuite('mutation', entry.id);
|
|
10153
11261
|
}
|
|
10154
11262
|
}
|
|
10155
11263
|
finally {
|
|
@@ -10185,14 +11293,24 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10185
11293
|
// those verdicts outrank the rebaseline check, so the extra suite run would buy nothing.
|
|
10186
11294
|
let rebaselineExitCode;
|
|
10187
11295
|
let rebaselineFailureReason;
|
|
11296
|
+
let rebaselineAttribution;
|
|
11297
|
+
let rebaselineInternalAttemptLog;
|
|
10188
11298
|
if (rebaselineMode === 'per-entry' && run !== null && run.exitCode !== null && run.exitCode !== 0
|
|
10189
11299
|
&& fileLoadFailure === undefined && outputUnrecognised === undefined && receiptMismatch === undefined) {
|
|
10190
11300
|
if (!json)
|
|
10191
11301
|
write(`mutation-gate: ${entry.id} — re-baselining the restored tree …`);
|
|
10192
|
-
const rebaselineRun = runSuite();
|
|
11302
|
+
const rebaselineRun = runSuite('rebaseline', entry.id);
|
|
10193
11303
|
rebaselineExitCode = rebaselineRun.exitCode;
|
|
10194
11304
|
rebaselineFailureReason = rebaselineRun.failureReason;
|
|
11305
|
+
rebaselineInternalAttemptLog = rebaselineRun.internalAttemptLog;
|
|
11306
|
+
if (rebaselineRun.exitCode !== null && rebaselineRun.exitCode !== 0) {
|
|
11307
|
+
rebaselineAttribution = attributeBaselineRedness(rebaselineRun.output, entries.map((candidate) => candidate.file));
|
|
11308
|
+
}
|
|
10195
11309
|
}
|
|
11310
|
+
const entryRunFailureReason = run?.failureReason ?? parseInternalFailureReason;
|
|
11311
|
+
const entryInternalAttemptLog = [parseInternalAttemptLog, run?.internalAttemptLog, rebaselineInternalAttemptLog]
|
|
11312
|
+
.filter((log) => log !== undefined)
|
|
11313
|
+
.join('; ');
|
|
10196
11314
|
const obs = {
|
|
10197
11315
|
entry,
|
|
10198
11316
|
occurrences: 1,
|
|
@@ -10202,9 +11320,11 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10202
11320
|
...(fileLoadFailure !== undefined ? { fileLoadFailure } : {}),
|
|
10203
11321
|
...(outputUnrecognised !== undefined ? { outputUnrecognised } : {}),
|
|
10204
11322
|
...(receiptMismatch !== undefined ? { receiptMismatch } : {}),
|
|
10205
|
-
...(
|
|
11323
|
+
...(entryRunFailureReason !== undefined ? { runFailureReason: entryRunFailureReason } : {}),
|
|
11324
|
+
...(entryInternalAttemptLog !== '' ? { internalAttemptLog: entryInternalAttemptLog } : {}),
|
|
10206
11325
|
...(rebaselineExitCode !== undefined ? { rebaselineExitCode } : {}),
|
|
10207
11326
|
...(rebaselineFailureReason !== undefined ? { rebaselineFailureReason } : {}),
|
|
11327
|
+
...(rebaselineAttribution !== undefined ? { rebaselineAttribution } : {}),
|
|
10208
11328
|
};
|
|
10209
11329
|
observations.push(obs);
|
|
10210
11330
|
results.push(classifyMutationOutcome(obs));
|
|
@@ -10217,7 +11337,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10217
11337
|
if (rebaselineMode === 'final') {
|
|
10218
11338
|
if (!json)
|
|
10219
11339
|
write('mutation-gate: final re-baseline of the restored tree …');
|
|
10220
|
-
const finalRun = runSuite();
|
|
11340
|
+
const finalRun = runSuite('final-rebaseline');
|
|
10221
11341
|
const finalExit = finalRun.exitCode;
|
|
10222
11342
|
if (finalExit !== 0) {
|
|
10223
11343
|
const what = finalExit === null ? `no exit code: ${finalRun.failureReason ?? 'unknown timeout / spawn failure'}` : `exit ${finalExit}`;
|
|
@@ -10228,6 +11348,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10228
11348
|
...obs,
|
|
10229
11349
|
rebaselineExitCode: finalExit,
|
|
10230
11350
|
...(finalRun.failureReason !== undefined ? { rebaselineFailureReason: finalRun.failureReason } : {}),
|
|
11351
|
+
...(finalRun.internalAttemptLog !== undefined
|
|
11352
|
+
? { internalAttemptLog: [obs.internalAttemptLog, finalRun.internalAttemptLog].filter((log) => log !== undefined).join('; ') }
|
|
11353
|
+
: {}),
|
|
11354
|
+
...(finalExit !== null && finalExit !== 0
|
|
11355
|
+
? { rebaselineAttribution: attributeBaselineRedness(finalRun.output, entries.map((entry) => entry.file)) }
|
|
11356
|
+
: {}),
|
|
10231
11357
|
}));
|
|
10232
11358
|
results.length = 0;
|
|
10233
11359
|
results.push(...reclassified);
|
|
@@ -10247,7 +11373,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10247
11373
|
}
|
|
10248
11374
|
const exitCode = mutationGateExitCode(results, baseline.ok);
|
|
10249
11375
|
if (json) {
|
|
10250
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, exitCode }, null, 2));
|
|
11376
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
|
|
10251
11377
|
return exitCode;
|
|
10252
11378
|
}
|
|
10253
11379
|
write(renderMutationReport(results, baseline, pkgDir));
|
|
@@ -11507,12 +12633,30 @@ function nameCheckScan(repoRoot) {
|
|
|
11507
12633
|
// Command names come from the dispatcher AND from the help block: a name that dispatches but
|
|
11508
12634
|
// is undocumented is still taken, and so is the reverse.
|
|
11509
12635
|
if (f.name === 'cli.ts') {
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
12636
|
+
// ONE enumeration, every consumer derives (ADR-001, feature command-count-triad). This
|
|
12637
|
+
// scan's question is "is the name TAKEN?", so taken = dispatched ∪ documented, which is
|
|
12638
|
+
// legitimately LARGER than the canonical command count — but it must be the SAME parse
|
|
12639
|
+
// the layer-1 parity test uses, not a second private regex that agrees by coincidence.
|
|
12640
|
+
// The any-indent `dispatchedCommandsIn` fallback stays for a cli.ts WITHOUT a main
|
|
12641
|
+
// `switch (command)` (none in this workspace today): a partial sweep would answer "free"
|
|
12642
|
+
// about a taken name, and that is the one answer this command may never give.
|
|
12643
|
+
let mainSwitchParsed = false;
|
|
12644
|
+
try {
|
|
12645
|
+
for (const c of dispatchedCommands(text))
|
|
12646
|
+
commands.add(c);
|
|
12647
|
+
for (const c of documentedCommands(text))
|
|
12648
|
+
commands.add(c);
|
|
12649
|
+
mainSwitchParsed = true;
|
|
12650
|
+
}
|
|
12651
|
+
catch { /* no main switch here — fall back to the broad regexes below */ }
|
|
12652
|
+
if (!mainSwitchParsed) {
|
|
12653
|
+
for (const c of dispatchedCommandsIn(text))
|
|
12654
|
+
commands.add(c);
|
|
12655
|
+
const help = /^\s{2}dz ([a-z][a-z0-9-]*)/gm;
|
|
12656
|
+
for (let m = help.exec(text); m !== null; m = help.exec(text))
|
|
12657
|
+
if (m[1] !== undefined)
|
|
12658
|
+
commands.add(m[1]);
|
|
12659
|
+
}
|
|
11516
12660
|
}
|
|
11517
12661
|
}
|
|
11518
12662
|
}
|
|
@@ -11527,6 +12671,75 @@ function nameCheckScan(repoRoot) {
|
|
|
11527
12671
|
return { commands: new Set(), modules: new Map(), exports: new Map(), scanFailed: true };
|
|
11528
12672
|
return { commands, modules, exports: exportsFound, scanned: { packages, files, exports: exportsFound.size, commands: commands.size } };
|
|
11529
12673
|
}
|
|
12674
|
+
/**
|
|
12675
|
+
* `dz brief-check <файл>` — проверить бриф роя на контракт вывода (ADR-001 swarm-brief-output-contract).
|
|
12676
|
+
*
|
|
12677
|
+
* Разбирает объявления брифа как ДАННЫЕ и отказывает поимённо: «бриф неверен» не говорит автору,
|
|
12678
|
+
* что чинить, поэтому каждое нарушение называет ключ и причину.
|
|
12679
|
+
*
|
|
12680
|
+
* ЧЕСТНЫЙ ПРЕДЕЛ печатается ВМЕСТЕ С ЗЕЛЁНЫМ ответом: проверено, что бриф ОБЪЯВИЛ каталог и
|
|
12681
|
+
* единицы, а не что агент им последует. Зелёная проверка, читаемая как гарантия поведения, хуже
|
|
12682
|
+
* её отсутствия.
|
|
12683
|
+
*/
|
|
12684
|
+
function cmdBriefCheck(options, flags, cwd, write) {
|
|
12685
|
+
const json = flags.has('json');
|
|
12686
|
+
/**
|
|
12687
|
+
* ВЕТКА «ПРОВЕРИТЬ НЕ УДАЛОСЬ» ТОЖЕ ОБЯЗАНА ОТВЕТИТЬ JSON-ом (находка 11).
|
|
12688
|
+
*
|
|
12689
|
+
* `--json` печатал человеческую строку на несуществующем файле, и потребитель, читающий вывод как
|
|
12690
|
+
* JSON, получал ошибку разбора вместо структурного «не проверено». Признак `checked` — тот же
|
|
12691
|
+
* трихотомический вердикт, что и коды выхода, только для машины: не «бриф плох», а «мы про него
|
|
12692
|
+
* ничего не установили».
|
|
12693
|
+
*/
|
|
12694
|
+
const unchecked = (reason, detail) => {
|
|
12695
|
+
if (json)
|
|
12696
|
+
write(JSON.stringify({ ok: false, checked: false, reason, detail }));
|
|
12697
|
+
else
|
|
12698
|
+
write(detail);
|
|
12699
|
+
return 2;
|
|
12700
|
+
};
|
|
12701
|
+
// Только позиционный аргумент: `--file` был необъявленным псевдонимом, и страж дрейфа флагов
|
|
12702
|
+
// справедливо на него указал — лишняя поверхность, которой нет в справке.
|
|
12703
|
+
const file = options.get('_positional_0');
|
|
12704
|
+
if (file === undefined || file.trim() === '') {
|
|
12705
|
+
const code = unchecked('no-file', 'dz brief-check: name the brief file — dz brief-check <file> [--json]');
|
|
12706
|
+
if (!json)
|
|
12707
|
+
write(' A brief must declare: ' + SWARM_BRIEF_CONTRACT.map((c) => c.key).join(', '));
|
|
12708
|
+
return code;
|
|
12709
|
+
}
|
|
12710
|
+
let text;
|
|
12711
|
+
try {
|
|
12712
|
+
text = readFileSync(resolve(cwd, file), 'utf-8');
|
|
12713
|
+
}
|
|
12714
|
+
catch {
|
|
12715
|
+
// Нечитаемый файл — НЕ «бриф плох»: мы про него ничего не установили. Отдельный код выхода,
|
|
12716
|
+
// чтобы отказ прибора не смешивался с отказом брифа.
|
|
12717
|
+
return unchecked('unreadable', `dz brief-check: cannot read ${visibleText(file)}`);
|
|
12718
|
+
}
|
|
12719
|
+
const result = checkSwarmBrief(text);
|
|
12720
|
+
if (json) {
|
|
12721
|
+
// `checked: true` — вторая половина того же различителя: потребитель отличает «проверено и
|
|
12722
|
+
// отвергнуто» от «проверить не удалось» полем, а не отсутствием поля.
|
|
12723
|
+
write(JSON.stringify({ ...result, checked: true }));
|
|
12724
|
+
return result.ok ? 0 : 1;
|
|
12725
|
+
}
|
|
12726
|
+
if (result.ok) {
|
|
12727
|
+
// ЗНАЧЕНИЯ ИЗ БРИФА ОБЕЗВРЕЖИВАЮТСЯ И В ЗЕЛЁНОЙ СТРОКЕ (находка 10). Отказ их уже обезвредил,
|
|
12728
|
+
// но подделывается ровно эта строка: управляющая последовательность в имени каталога стирает
|
|
12729
|
+
// предыдущий вывод и печатает поверх него подделку.
|
|
12730
|
+
write(`dz brief-check: OK — dir ${visibleText(result.outputDir ?? '')}, ${result.units.length} unit(s), assembly "${visibleText(result.assemblyUnit ?? '')}"`);
|
|
12731
|
+
write(' LIMIT: this verifies the brief DECLARED the contract, not that the agent will follow it —');
|
|
12732
|
+
write(' only comparing the directory against the unit list on an orchestrator tick can show that.');
|
|
12733
|
+
write(' A filled-in template and an unedited one both pass: the parse cannot tell them apart.');
|
|
12734
|
+
return 0;
|
|
12735
|
+
}
|
|
12736
|
+
write(`dz brief-check: REFUSED — ${result.violations.length} violation(s)`);
|
|
12737
|
+
// Ядро уже обезвредило значения в причинах; повтор на слое печати — не суеверие, а граница:
|
|
12738
|
+
// печатающий слой не обязан знать, кто именно из его источников уже почистил текст.
|
|
12739
|
+
for (const v of result.violations)
|
|
12740
|
+
write(` ${v.rule}: ${visibleText(v.detail)}`);
|
|
12741
|
+
return 1;
|
|
12742
|
+
}
|
|
11530
12743
|
function cmdNameCheck(options, flags, cwd, write) {
|
|
11531
12744
|
const repoRoot = resolve(options.get('project') ?? cwd);
|
|
11532
12745
|
const json = flags.has('json');
|
|
@@ -12091,6 +13304,18 @@ function cmdFeatureAdrRecord(options, flags, cwd, write) {
|
|
|
12091
13304
|
targetExists: existsSync(target),
|
|
12092
13305
|
targetHasPair: flags.has('once') && existsSync(target),
|
|
12093
13306
|
timestamp: new Date().toISOString(),
|
|
13307
|
+
// WHO ran it: `--runner <id>` when the caller knows, otherwise this host. The workflow cannot
|
|
13308
|
+
// supply it — it has no host inside its sandbox — so the identity is resolved here, at the one
|
|
13309
|
+
// seam that runs outside. hostname() can throw on an exotic setup; an unresolvable runner stays
|
|
13310
|
+
// ABSENT rather than becoming the string 'unknown', which would later join as if it were one.
|
|
13311
|
+
runnerId: (options.get('runner') ?? '').trim() !== ''
|
|
13312
|
+
? (options.get('runner') ?? '').trim()
|
|
13313
|
+
: (() => { try {
|
|
13314
|
+
return hostname();
|
|
13315
|
+
}
|
|
13316
|
+
catch {
|
|
13317
|
+
return null;
|
|
13318
|
+
} })(),
|
|
12094
13319
|
});
|
|
12095
13320
|
if (decision.line === null)
|
|
12096
13321
|
return emit(decision);
|
|
@@ -12445,12 +13670,21 @@ function cmdAmendmentCheck(options, flags, cwd, write) {
|
|
|
12445
13670
|
// Paths in an amendment row are repo-relative, so they resolve against the repo root — not
|
|
12446
13671
|
// against the feature directory, and not against wherever the caller happened to stand.
|
|
12447
13672
|
const resolutions = resolveAmendments(rows, { readFile: (rel) => readOr(resolve(cwd, rel)) });
|
|
13673
|
+
// A document that opens `## Amendments` twice cannot have its first heading answer for the
|
|
13674
|
+
// rest (Codex round 6, P2). Counted per document and taken at its worst — one contradictory
|
|
13675
|
+
// input is enough to make the run inconclusive.
|
|
13676
|
+
const sectionCount = Math.max(ideation === null ? 0 : amendmentSectionCount(ideation), plan === null ? 0 : amendmentSectionCount(plan));
|
|
12448
13677
|
const decision = decideAmendmentOutcome({
|
|
12449
13678
|
sectionPresent,
|
|
12450
13679
|
rows,
|
|
12451
13680
|
resolutions,
|
|
12452
13681
|
planSaysNone: plan !== null && planSaysNoAmendments(plan),
|
|
12453
13682
|
missingFromPlan,
|
|
13683
|
+
sectionCount,
|
|
13684
|
+
// Fail-closed: measured on the document the rows would have come from (the plan when it has
|
|
13685
|
+
// a section, otherwise the ideation report).
|
|
13686
|
+
ambiguity: (plan !== null ? amendmentDeclarationAmbiguity(plan) : null)
|
|
13687
|
+
?? (ideation !== null ? amendmentDeclarationAmbiguity(ideation) : null),
|
|
12454
13688
|
});
|
|
12455
13689
|
return { slug, decision, resolutions };
|
|
12456
13690
|
};
|
|
@@ -13443,6 +14677,143 @@ async function cmdQeBridge(options, flags, cwd, write) {
|
|
|
13443
14677
|
cleanupIsolated();
|
|
13444
14678
|
}
|
|
13445
14679
|
}
|
|
14680
|
+
function scoreReceiptFiles(root) {
|
|
14681
|
+
const featuresDir = join(root, 'features');
|
|
14682
|
+
let features;
|
|
14683
|
+
try {
|
|
14684
|
+
features = readdirSync(featuresDir, { withFileTypes: true });
|
|
14685
|
+
}
|
|
14686
|
+
catch {
|
|
14687
|
+
return [];
|
|
14688
|
+
}
|
|
14689
|
+
const receipts = [];
|
|
14690
|
+
for (const feature of features) {
|
|
14691
|
+
if (!feature.isDirectory())
|
|
14692
|
+
continue;
|
|
14693
|
+
const stateDir = join(featuresDir, feature.name, '.fa-state');
|
|
14694
|
+
let entries;
|
|
14695
|
+
try {
|
|
14696
|
+
if (lstatSync(stateDir).isSymbolicLink())
|
|
14697
|
+
continue;
|
|
14698
|
+
entries = readdirSync(stateDir, { withFileTypes: true });
|
|
14699
|
+
}
|
|
14700
|
+
catch {
|
|
14701
|
+
continue;
|
|
14702
|
+
}
|
|
14703
|
+
for (const entry of entries) {
|
|
14704
|
+
if (!entry.isFile())
|
|
14705
|
+
continue;
|
|
14706
|
+
const match = /^score-(.+)\.json$/.exec(entry.name);
|
|
14707
|
+
if (match === null || match[1] === undefined || match[1] === '')
|
|
14708
|
+
continue;
|
|
14709
|
+
const path = join(stateDir, entry.name);
|
|
14710
|
+
receipts.push({ path, displayPath: relative(root, path), qeHash: match[1] });
|
|
14711
|
+
}
|
|
14712
|
+
}
|
|
14713
|
+
return receipts.sort((a, b) => a.displayPath < b.displayPath ? -1 : a.displayPath > b.displayPath ? 1 : 0);
|
|
14714
|
+
}
|
|
14715
|
+
function scoreAggregateChainLine(text) {
|
|
14716
|
+
if (text === '') {
|
|
14717
|
+
return { line: 'chain: NOT_PRESENT — no aggregate evidence file was created', verification: null, defectAges: null };
|
|
14718
|
+
}
|
|
14719
|
+
const verification = verifyEventChainText(text);
|
|
14720
|
+
const defectAges = classifyChainDefects(verification, verification.lines);
|
|
14721
|
+
const kinds = new Map();
|
|
14722
|
+
for (const defect of verification.defects)
|
|
14723
|
+
kinds.set(defect.kind, (kinds.get(defect.kind) ?? 0) + 1);
|
|
14724
|
+
const kindText = [...kinds.entries()].map(([kind, count]) => `${kind}: ${count}`).join(' · ');
|
|
14725
|
+
const line = `chain: ${verification.ok ? 'OK' : 'FAILED'} · ${verification.chained} chained · ` +
|
|
14726
|
+
`${verification.resets} recorded restart(s) · before-run defects ${defectAges.beforeRun.length} · ` +
|
|
14727
|
+
`in-run defects ${defectAges.inRun.length} · current run ${defectAges.runRecords} record(s)` +
|
|
14728
|
+
(kindText === '' ? '' : ` · ${kindText}`) +
|
|
14729
|
+
` — ${verification.scope}`;
|
|
14730
|
+
return { line, verification, defectAges };
|
|
14731
|
+
}
|
|
14732
|
+
function cmdScoreAll(options, flags, cwd, write) {
|
|
14733
|
+
const json = flags.has('json');
|
|
14734
|
+
if (options.has('slug')) {
|
|
14735
|
+
write(json
|
|
14736
|
+
? JSON.stringify({ error: '--all and --slug are mutually exclusive', exitCode: 1 })
|
|
14737
|
+
: 'dz score: --all and --slug are mutually exclusive');
|
|
14738
|
+
return 1;
|
|
14739
|
+
}
|
|
14740
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
14741
|
+
const receiptFiles = scoreReceiptFiles(root);
|
|
14742
|
+
if (receiptFiles.length === 0) {
|
|
14743
|
+
const report = buildScoreAggregateReport([], [], 0);
|
|
14744
|
+
const chain = scoreAggregateChainLine('');
|
|
14745
|
+
if (json)
|
|
14746
|
+
write(JSON.stringify({ ...report, chain: null, aggregatePath: '.dz/feature-adr/scorecards.jsonl', exitCode: 0 }, null, 2));
|
|
14747
|
+
else {
|
|
14748
|
+
write(renderScoreAggregateReport(report));
|
|
14749
|
+
write(chain.line);
|
|
14750
|
+
}
|
|
14751
|
+
return 0;
|
|
14752
|
+
}
|
|
14753
|
+
const ts = new Date().toISOString();
|
|
14754
|
+
const rows = [];
|
|
14755
|
+
const unreadableReceipts = [];
|
|
14756
|
+
for (const receipt of receiptFiles) {
|
|
14757
|
+
try {
|
|
14758
|
+
rows.push(scoreReceiptToAggregateRow({
|
|
14759
|
+
content: readFileSync(receipt.path, 'utf8'),
|
|
14760
|
+
qeHash: receipt.qeHash,
|
|
14761
|
+
ts,
|
|
14762
|
+
}));
|
|
14763
|
+
}
|
|
14764
|
+
catch {
|
|
14765
|
+
unreadableReceipts.push(receipt.displayPath);
|
|
14766
|
+
}
|
|
14767
|
+
}
|
|
14768
|
+
const storeDir = join(root, '.dz', 'feature-adr');
|
|
14769
|
+
const aggregatePath = join(storeDir, 'scorecards.jsonl');
|
|
14770
|
+
let finalText = '';
|
|
14771
|
+
let finalRows = rows;
|
|
14772
|
+
let appended = 0;
|
|
14773
|
+
let storeError = null;
|
|
14774
|
+
try {
|
|
14775
|
+
const result = withNamedLockSync(storeDir, 'scorecards', () => {
|
|
14776
|
+
let existingText = '';
|
|
14777
|
+
try {
|
|
14778
|
+
existingText = readFileSync(aggregatePath, 'utf8');
|
|
14779
|
+
}
|
|
14780
|
+
catch (error) {
|
|
14781
|
+
if (error.code !== 'ENOENT')
|
|
14782
|
+
throw error;
|
|
14783
|
+
}
|
|
14784
|
+
const fresh = dedupeScoreAggregateRows(rows, readScoreAggregateRows(existingText));
|
|
14785
|
+
const appendText = appendChainedLines(fresh, readTailInfo(existingText));
|
|
14786
|
+
if (appendText !== '')
|
|
14787
|
+
appendFileSync(aggregatePath, appendText, { encoding: 'utf8', mode: 0o600 });
|
|
14788
|
+
const settledText = existingText + appendText;
|
|
14789
|
+
return { text: settledText, rows: readScoreAggregateRows(settledText), appended: fresh.length };
|
|
14790
|
+
});
|
|
14791
|
+
finalText = result.text;
|
|
14792
|
+
finalRows = result.rows;
|
|
14793
|
+
appended = result.appended;
|
|
14794
|
+
}
|
|
14795
|
+
catch (error) {
|
|
14796
|
+
storeError = error instanceof Error ? error.message : String(error);
|
|
14797
|
+
}
|
|
14798
|
+
const report = buildScoreAggregateReport(finalRows, unreadableReceipts, appended);
|
|
14799
|
+
const chain = scoreAggregateChainLine(finalText);
|
|
14800
|
+
if (json) {
|
|
14801
|
+
write(JSON.stringify({
|
|
14802
|
+
...report,
|
|
14803
|
+
aggregatePath: '.dz/feature-adr/scorecards.jsonl',
|
|
14804
|
+
chain: chain.verification === null ? null : { verification: chain.verification, defectAges: chain.defectAges },
|
|
14805
|
+
storeError,
|
|
14806
|
+
exitCode: 0,
|
|
14807
|
+
}, null, 2));
|
|
14808
|
+
}
|
|
14809
|
+
else {
|
|
14810
|
+
write(renderScoreAggregateReport(report));
|
|
14811
|
+
write(chain.line);
|
|
14812
|
+
if (storeError !== null)
|
|
14813
|
+
write(`store error (nothing was claimed appended): ${storeError}`);
|
|
14814
|
+
}
|
|
14815
|
+
return 0;
|
|
14816
|
+
}
|
|
13446
14817
|
function cmdScore(options, flags, cwd, write) {
|
|
13447
14818
|
const json = flags.has('json');
|
|
13448
14819
|
if (flags.has('help')) {
|
|
@@ -13451,13 +14822,14 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
13451
14822
|
write(JSON.stringify({ help: usage, exitCode: 0 })); // --json stays ONE document even for help
|
|
13452
14823
|
else {
|
|
13453
14824
|
write(usage);
|
|
14825
|
+
write('dz score --all [--project <dir>] [--json] — sweep immutable score receipts into the append-only chained aggregate');
|
|
13454
14826
|
write(' disciplines: ADR confirmation · discrimination · cross-model QE · live verification · README-first · learning loop · amendments');
|
|
13455
14827
|
write(' descriptive-only, never a gate: a low score exits 0');
|
|
13456
14828
|
}
|
|
13457
14829
|
return 0;
|
|
13458
14830
|
}
|
|
13459
14831
|
for (const flag of flags) {
|
|
13460
|
-
if (!new Set(['json', 'help']).has(flag)) {
|
|
14832
|
+
if (!new Set(['json', 'help', 'all']).has(flag)) {
|
|
13461
14833
|
write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz score: unknown option --${flag}\n allowed: --slug <feature>, --project <dir>, --json`);
|
|
13462
14834
|
return 1;
|
|
13463
14835
|
}
|
|
@@ -13469,6 +14841,8 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
13469
14841
|
return 1;
|
|
13470
14842
|
}
|
|
13471
14843
|
}
|
|
14844
|
+
if (flags.has('all'))
|
|
14845
|
+
return cmdScoreAll(options, flags, cwd, write);
|
|
13472
14846
|
const slug = options.get('slug') ?? '';
|
|
13473
14847
|
// The delivery-check traversal lesson, upgraded to a WHITELIST: `.` slipped the blacklist and
|
|
13474
14848
|
// silently aggregated the entire features/ tree as one "run" (Codex QE #2).
|
|
@@ -14562,7 +15936,7 @@ function cmdDeliveryCheck(options, flags, cwd, write) {
|
|
|
14562
15936
|
/* backlog — the Smart Backlog idea pipeline (feature smart-backlog) */
|
|
14563
15937
|
/* ------------------------------------------------------------------ */
|
|
14564
15938
|
/** Thin dispatcher — ALL logic lives in harness-core/src/backlog.ts (05 architecture: handlers stay dumb). */
|
|
14565
|
-
async function cmdBacklog(options, flags, cwd, write) {
|
|
15939
|
+
async function cmdBacklog(options, flags, cwd, write, writeErr) {
|
|
14566
15940
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
14567
15941
|
const json = flags.has('json');
|
|
14568
15942
|
const sub = options.get('_positional_0');
|
|
@@ -14580,6 +15954,8 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14580
15954
|
if (eff.adjusted && !json && eff.note !== undefined)
|
|
14581
15955
|
write(`dz backlog: ${eff.note}`);
|
|
14582
15956
|
const dryRun = flags.has('dry-run');
|
|
15957
|
+
if (!dryRun && !allowLearningStoreWrite(projectRoot, flags, writeErr, 'dz backlog add'))
|
|
15958
|
+
return 1;
|
|
14583
15959
|
// Embed-form migration (register-inflation fix): v1 vectors are FULL-TEXT embeds, v2 queries are
|
|
14584
15960
|
// bounded excerpts — comparing across the forms is a query-vs-row space split. Re-mirror once
|
|
14585
15961
|
// (idempotent upsert), before the dedup search. Dry-run writes nothing, so it only WARNS.
|
|
@@ -14610,6 +15986,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14610
15986
|
const ideas = readIdeas(projectRoot);
|
|
14611
15987
|
const match = ideas.find((i) => i.id === verdict.matchedId);
|
|
14612
15988
|
let absorbErr;
|
|
15989
|
+
let didWrite = false;
|
|
14613
15990
|
if (!dryRun && match !== undefined) {
|
|
14614
15991
|
const snap = snapshotIdeas(projectRoot, join(projectRoot, '.dz', 'backlog', `ideas.pre-merge-${Date.now()}.jsonl`));
|
|
14615
15992
|
if (snap.error !== undefined)
|
|
@@ -14626,7 +16003,10 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14626
16003
|
}).error;
|
|
14627
16004
|
match.uses += 1;
|
|
14628
16005
|
writeIdeas(projectRoot, ideas);
|
|
16006
|
+
didWrite = true;
|
|
14629
16007
|
}
|
|
16008
|
+
if (didWrite)
|
|
16009
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog add');
|
|
14630
16010
|
if (json)
|
|
14631
16011
|
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));
|
|
14632
16012
|
else {
|
|
@@ -14688,6 +16068,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14688
16068
|
ideas.push(rec);
|
|
14689
16069
|
writeIdeas(projectRoot, ideas);
|
|
14690
16070
|
const mirror = await mirrorIdeaVector(projectRoot, rec); // best-effort — never blocks capture
|
|
16071
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog add');
|
|
14691
16072
|
if (json)
|
|
14692
16073
|
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));
|
|
14693
16074
|
else {
|
|
@@ -14887,8 +16268,18 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14887
16268
|
if (commitId !== undefined) {
|
|
14888
16269
|
// Validated above (safe id + known id) BEFORE any early return.
|
|
14889
16270
|
const idx = ideas.findIndex((i) => i.id === commitId);
|
|
16271
|
+
// ОТМЕТКА ВРЕМЕНИ И ЖУРНАЛ ставятся ЗДЕСЬ, а не «где-нибудь потом».
|
|
16272
|
+
// Измерено 2026-09-02: `grep -c statusTs cli.ts` давал НОЛЬ — оба пути смены статуса в этом
|
|
16273
|
+
// файле меняли поле и не отмечали, когда. Отсюда 22 терминальные записи без отметки и
|
|
16274
|
+
// невычислимое «сколько идея пробыла в работе».
|
|
16275
|
+
const spinFrom = ideas[idx].status;
|
|
16276
|
+
const spinTs = new Date().toISOString();
|
|
14890
16277
|
ideas[idx].status = 'in-progress';
|
|
16278
|
+
ideas[idx].statusTs = spinTs;
|
|
14891
16279
|
writeIdeas(projectRoot, ideas);
|
|
16280
|
+
if (!appendTransition(projectRoot, { id: commitId, from: spinFrom, to: 'in-progress', ts: spinTs, by: 'backlog roulette --commit' })) {
|
|
16281
|
+
write('dz backlog: переход НЕ записан в журнал — наблюдение потеряно (сам статус изменён)');
|
|
16282
|
+
}
|
|
14892
16283
|
pick = ideas[idx];
|
|
14893
16284
|
committed = true;
|
|
14894
16285
|
}
|
|
@@ -14963,6 +16354,7 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14963
16354
|
}
|
|
14964
16355
|
else
|
|
14965
16356
|
embed = 'stale';
|
|
16357
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog edit');
|
|
14966
16358
|
}
|
|
14967
16359
|
if (json) {
|
|
14968
16360
|
write(JSON.stringify({ verb: 'edit', ...report, embed, exitCode: report.ok ? (embed === 'stale' ? 1 : 0) : 1 }, null, 2));
|
|
@@ -14997,9 +16389,15 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14997
16389
|
const goalMap = readGoalMap(projectRoot);
|
|
14998
16390
|
const related = ideas.filter((i) => rec.relatedIds.includes(i.id));
|
|
14999
16391
|
const staging = stageEnrichment(projectRoot, rec, related, goalMap);
|
|
16392
|
+
const enrichFrom = rec.status;
|
|
16393
|
+
const enrichTs = new Date().toISOString();
|
|
15000
16394
|
rec.status = 'enriched';
|
|
16395
|
+
rec.statusTs = enrichTs;
|
|
15001
16396
|
rec.enrichedPath = `features/${staging.slug}`;
|
|
15002
16397
|
writeIdeas(projectRoot, ideas);
|
|
16398
|
+
if (!appendTransition(projectRoot, { id: rec.id, from: enrichFrom, to: 'enriched', ts: enrichTs, by: 'backlog enrich' })) {
|
|
16399
|
+
write('dz backlog: переход НЕ записан в журнал — наблюдение потеряно (сам статус изменён)');
|
|
16400
|
+
}
|
|
15003
16401
|
if (json)
|
|
15004
16402
|
write(JSON.stringify({ slug: staging.slug, scaffoldPath: staging.scaffoldPath, handoff: 'idea2prd-manual', exitCode: 0 }, null, 2));
|
|
15005
16403
|
else {
|
|
@@ -15044,6 +16442,10 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
15044
16442
|
else if (form.action === 'deferred' && !json)
|
|
15045
16443
|
write(`dz backlog: ⚠ embed-form migration deferred (${form.error ?? 'unknown error'})`);
|
|
15046
16444
|
const report = await harmonizeBacklog(projectRoot, { apply, ...(thr !== undefined ? { threshold: Number(thr) } : {}) });
|
|
16445
|
+
if (apply) {
|
|
16446
|
+
refreshLearningStoreMark(projectRoot, writeErr, 'dz backlog harmonize --apply');
|
|
16447
|
+
storeGuardResetReminder(projectRoot, writeErr, 'dz backlog harmonize --apply');
|
|
16448
|
+
}
|
|
15047
16449
|
if (json) {
|
|
15048
16450
|
write(JSON.stringify({ ...report, exitCode: 0 }, null, 2));
|
|
15049
16451
|
return 0;
|
|
@@ -15342,17 +16744,19 @@ function cmdStats(cwd, write) {
|
|
|
15342
16744
|
}
|
|
15343
16745
|
const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
15344
16746
|
const packages = dirs.length;
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
16747
|
+
// Backlog e160aeee. This used to walk the tree ITSELF, and was wrong in two independent ways:
|
|
16748
|
+
// it counted only packages whose NAME starts with `skills-` (health-advisor, p-replicator,
|
|
16749
|
+
// keysarium and trip-planner were therefore invisible), and it knew only ONE of the three skill
|
|
16750
|
+
// layouts. Result: 203 here against 250 from `dz registry` on the same tree — two counters of one
|
|
16751
|
+
// quantity, each unable to refute the other because neither knew the other existed.
|
|
16752
|
+
//
|
|
16753
|
+
// The fix is structural, not arithmetic: there is now ONE enumerator, and both commands ask it.
|
|
16754
|
+
// Pinned by test/stats-registry-parity.test.ts, whose red half is this exact divergence.
|
|
16755
|
+
// The registry already PUBLISHES these totals; recomputing them from `entries` here would be a
|
|
16756
|
+
// third implementation of the same count, which is the very defect being fixed.
|
|
16757
|
+
const registry = buildRegistry(cwd);
|
|
16758
|
+
const totalSkills = registry.totalSkills;
|
|
16759
|
+
const skillPacks = registry.totalPacks;
|
|
15356
16760
|
const targets = TARGET_NAMES.length;
|
|
15357
16761
|
const presets = PRESET_NAMES.length;
|
|
15358
16762
|
write(`dz stats — DZ Harness Hub`);
|
|
@@ -15837,7 +17241,28 @@ export async function runCli(argv, io = {}) {
|
|
|
15837
17241
|
// let a guard treat "I could not tell you" as "I answered you".
|
|
15838
17242
|
return version === 'unknown' ? 1 : 0;
|
|
15839
17243
|
}
|
|
15840
|
-
|
|
17244
|
+
// `-h` is the most-typed help flag and is NOT a command: before the unknown-command contract
|
|
17245
|
+
// landed it fell through to the switch and still printed usage; afterwards it would have died
|
|
17246
|
+
// with exit 2 and an empty stdout (measured regression, cross-model QE M1). It belongs beside
|
|
17247
|
+
// `-v` above — an argv-level flag, resolved before command dispatch.
|
|
17248
|
+
if (argv[0] === '-h') {
|
|
17249
|
+
write(USAGE);
|
|
17250
|
+
return 0;
|
|
17251
|
+
}
|
|
17252
|
+
// A bare `--typo` leaves the command empty, so the usage branch reported SUCCESS on a misspelled
|
|
17253
|
+
// FLAG exactly as it used to on a misspelled VERB (cross-model QE M2): `dz --frobnicate` exited 0
|
|
17254
|
+
// with 30 KB of usage. The refusal is deliberately narrowed to the no-command case, because the
|
|
17255
|
+
// warn-don't-refuse decision above is measured and still stands: with a command present, an
|
|
17256
|
+
// unrecognised name may simply be missing from KNOWN_CLI_FLAGS and refusing would break working
|
|
17257
|
+
// invocations. With NO command there is nothing the flag could belong to, so it is a usage error.
|
|
17258
|
+
if (command === '') {
|
|
17259
|
+
const strayNames = unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS).map((n) => n.name);
|
|
17260
|
+
if (strayNames.length > 0) {
|
|
17261
|
+
writeErr(`dz: unknown option --${strayNames[0]} — run 'dz help' for usage`);
|
|
17262
|
+
return 2;
|
|
17263
|
+
}
|
|
17264
|
+
}
|
|
17265
|
+
if (command === '' || command === 'help' || (flags.has('help') && DZ_COMMANDS.includes(command))) {
|
|
15841
17266
|
write(USAGE);
|
|
15842
17267
|
return 0;
|
|
15843
17268
|
}
|
|
@@ -15885,17 +17310,21 @@ export async function runCli(argv, io = {}) {
|
|
|
15885
17310
|
case 'teach':
|
|
15886
17311
|
return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
|
|
15887
17312
|
case 'consolidate':
|
|
15888
|
-
return await cmdConsolidate(options, flags, cwd, write);
|
|
17313
|
+
return await cmdConsolidate(options, flags, cwd, write, writeErr);
|
|
15889
17314
|
case 'recall':
|
|
15890
17315
|
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
15891
17316
|
case 'vector':
|
|
15892
|
-
return await cmdVector(options, flags, cwd, write);
|
|
17317
|
+
return await cmdVector(options, flags, cwd, write, writeErr);
|
|
15893
17318
|
case 'brain':
|
|
15894
17319
|
return await cmdBrain(options, flags, cwd, write, readStdin);
|
|
15895
17320
|
case 'statusline':
|
|
15896
|
-
return cmdStatusline(options, flags, cwd, write, readStdin);
|
|
17321
|
+
return cmdStatusline(options, flags, cwd, write, readStdin, writeErr);
|
|
17322
|
+
case 'store-guard':
|
|
17323
|
+
return await cmdStoreGuard(options, flags, cwd, write, writeErr, io.stdin, io.interactive ?? process.stdin.isTTY === true);
|
|
15897
17324
|
case 'usage':
|
|
15898
17325
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
17326
|
+
case 'chain':
|
|
17327
|
+
return cmdChain(options, flags, cwd, write);
|
|
15899
17328
|
case 'claim-check':
|
|
15900
17329
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
15901
17330
|
case 'lint':
|
|
@@ -15917,7 +17346,7 @@ export async function runCli(argv, io = {}) {
|
|
|
15917
17346
|
case 'diff':
|
|
15918
17347
|
return cmdDiff(options, cwd, write);
|
|
15919
17348
|
case 'recommend':
|
|
15920
|
-
return cmdRecommend(options, cwd, write);
|
|
17349
|
+
return cmdRecommend(options, flags, cwd, write);
|
|
15921
17350
|
case 'upgrade':
|
|
15922
17351
|
return cmdUpgrade(options, flags, cwd, write, writeErr);
|
|
15923
17352
|
case 'auto-canonicalize':
|
|
@@ -15959,15 +17388,29 @@ export async function runCli(argv, io = {}) {
|
|
|
15959
17388
|
case 'mr-rakes':
|
|
15960
17389
|
return await cmdMrRakes(options, flags, cwd, write);
|
|
15961
17390
|
case 'retro':
|
|
15962
|
-
return await cmdRetro(options, flags, cwd, write);
|
|
17391
|
+
return await cmdRetro(options, flags, cwd, write, readStdin);
|
|
15963
17392
|
case 'feature-adr-setup':
|
|
15964
17393
|
return cmdFeatureAdrSetup(options, flags, cwd, write, writeErr);
|
|
15965
17394
|
case 'challenge':
|
|
15966
17395
|
return cmdChallenge(options, flags, cwd, write);
|
|
15967
17396
|
case 'discrimination-check':
|
|
15968
17397
|
return cmdDiscriminationCheck(options, flags, cwd, write);
|
|
15969
|
-
case 'mutation-gate':
|
|
15970
|
-
|
|
17398
|
+
case 'mutation-gate': {
|
|
17399
|
+
try {
|
|
17400
|
+
return cmdMutationGate(options, flags, cwd, write, io.mutationGateRunner);
|
|
17401
|
+
}
|
|
17402
|
+
catch (error) {
|
|
17403
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
17404
|
+
const head = Array.from(raw.split(/\r?\n/, 1)[0]?.trim() || 'unknown internal error').slice(0, 160).join('');
|
|
17405
|
+
if (flags.has('json')) {
|
|
17406
|
+
write(JSON.stringify({ verdict: 'INCONCLUSIVE', reason: 'runner-internal-error', error: head, exitCode: 1 }));
|
|
17407
|
+
}
|
|
17408
|
+
else {
|
|
17409
|
+
write(`mutation-gate: INTERNAL ERROR (${head}) — verdict INCONCLUSIVE, exit 1`);
|
|
17410
|
+
}
|
|
17411
|
+
return 1;
|
|
17412
|
+
}
|
|
17413
|
+
}
|
|
15971
17414
|
case 'delivery-check':
|
|
15972
17415
|
return cmdDeliveryCheck(options, flags, cwd, write);
|
|
15973
17416
|
case 'skills-verify':
|
|
@@ -15992,6 +17435,8 @@ export async function runCli(argv, io = {}) {
|
|
|
15992
17435
|
return cmdTgPost(options, flags, cwd, write);
|
|
15993
17436
|
case 'name-check':
|
|
15994
17437
|
return cmdNameCheck(options, flags, cwd, write);
|
|
17438
|
+
case 'brief-check':
|
|
17439
|
+
return cmdBriefCheck(options, flags, cwd, write);
|
|
15995
17440
|
case 'provenance-check':
|
|
15996
17441
|
return cmdProvenanceCheck(options, flags, cwd, write);
|
|
15997
17442
|
case 'feature-adr-record':
|
|
@@ -16009,7 +17454,7 @@ export async function runCli(argv, io = {}) {
|
|
|
16009
17454
|
case 'qe-bridge':
|
|
16010
17455
|
return await cmdQeBridge(options, flags, cwd, write);
|
|
16011
17456
|
case 'backlog':
|
|
16012
|
-
return await cmdBacklog(options, flags, cwd, write);
|
|
17457
|
+
return await cmdBacklog(options, flags, cwd, write, writeErr);
|
|
16013
17458
|
case 'routing':
|
|
16014
17459
|
return cmdRouting(options, flags, cwd, write);
|
|
16015
17460
|
case 'bto-optimize':
|
|
@@ -16021,9 +17466,8 @@ export async function runCli(argv, io = {}) {
|
|
|
16021
17466
|
case 'import-ecc':
|
|
16022
17467
|
return await cmdImportEcc(options, flags, cwd, write);
|
|
16023
17468
|
default:
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
return 1;
|
|
17469
|
+
writeErr(`dz: unknown command ${JSON.stringify(command)} — run 'dz help' for the command list`);
|
|
17470
|
+
return 2;
|
|
16027
17471
|
}
|
|
16028
17472
|
}
|
|
16029
17473
|
catch (error) {
|