@dzhechkov/harness-cli 0.8.6 → 0.8.10
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 +25 -25
- package/README.md +130 -12
- package/dist/boolean-flags.d.ts.map +1 -1
- package/dist/boolean-flags.js +1 -0
- package/dist/boolean-flags.js.map +1 -1
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1657 -206
- package/dist/cli.js.map +1 -1
- package/dist/core-compat.d.ts +1 -1
- package/dist/core-compat.js +4 -4
- package/dist/core-compat.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +10 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +10 -10
- package/sbom.json +24 -24
- package/src/boolean-flags.ts +1 -0
- package/src/cli.ts +1758 -162
- package/src/core-compat.ts +4 -4
- package/src/index.ts +1 -1
- package/src/known-flags.ts +10 -0
package/dist/cli.js
CHANGED
|
@@ -12,22 +12,22 @@ import { isBooleanFlag } from './boolean-flags.js';
|
|
|
12
12
|
import { resolveInstallSpec } from './install-spec.js';
|
|
13
13
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
|
|
14
14
|
import { createHash, randomBytes } from 'node:crypto';
|
|
15
|
-
import { homedir, tmpdir } from 'node:os';
|
|
15
|
+
import { homedir, hostname, tmpdir } from 'node:os';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { isDeepStrictEqual } from 'node:util';
|
|
18
|
-
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, 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,
|
|
18
|
+
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
19
|
// 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, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, 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, 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,
|
|
20
|
+
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
21
21
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
22
|
-
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, 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,
|
|
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
23
|
// 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,
|
|
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, 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
25
|
// Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
|
|
26
26
|
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
27
|
// qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
|
|
28
28
|
KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
|
|
29
29
|
// 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,
|
|
30
|
+
parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
31
31
|
// operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
|
|
32
32
|
renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
|
|
33
33
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
@@ -40,19 +40,19 @@ export const DZ_COMMANDS = [
|
|
|
40
40
|
'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
|
|
41
41
|
'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
|
|
42
42
|
'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
|
|
43
|
-
'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
|
|
43
|
+
'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
|
|
44
44
|
'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
|
|
45
45
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
46
46
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
47
47
|
'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
|
|
48
48
|
'name-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
|
|
49
49
|
'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
|
|
50
|
-
'bto-optimize', 'dashboard', 'roam', 'import-ecc',
|
|
50
|
+
'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
|
|
51
51
|
];
|
|
52
52
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
53
53
|
|
|
54
54
|
Usage:
|
|
55
|
-
dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--no-hooks] [--no-verify] (
|
|
55
|
+
dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--allow-integrations <sha256:digest>] [--no-integrations] [--no-hooks] [--no-verify] (integration manifests require exact digest consent; --no-integrations = explicit skills-only)
|
|
56
56
|
dz verify [--skills-dir <dir>] [--target <name>]
|
|
57
57
|
dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
|
|
58
58
|
dz update (alias of sync)
|
|
@@ -81,13 +81,14 @@ Usage:
|
|
|
81
81
|
dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
|
|
82
82
|
dz challenge --plan <plan.md> [--author <model>] (the deterministic cartridge behind the challenge-panel adversarial plan-gate (R6): assembles the wide brief — plan + architecture/vision.md + testing.md + map.json + degradations.md — and prints the C1-C8 adversary prompt naming the cross-family reviewer to dispatch. exit 0 brief printed / 1 plan missing or empty)
|
|
83
83
|
dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--json] (does .claude/skills/ actually REGISTER? --static = instant layout scan for CI; default reads the authoritative system/init listing from a real session. exit 0 pass / 1 fail / 2 inconclusive — never a false pass)
|
|
84
|
-
dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool
|
|
84
|
+
dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool/replay/guard instrumentation + monthly eligible→attempted→accepted→executions; unavailable is NOT MEASURED, and only a named empty stage after a non-empty predecessor for 3 measured months is a funnel finding)
|
|
85
85
|
dz deadwood [--weeks <n>] [--json] (advisory zero-usage candidates for human deprecation review; safety-excluded surfaces carry reasons; never deletes or deprecates anything; shallow history says INSUFFICIENT_DATA)
|
|
86
86
|
dz epoch-replay --mock [--n <N>] [--effect <-1..1>] [--tie-rate <0..1>] [--seed <N>] [--slice <name>] [--json] ($0 synthetic run — exercises the verdict math, NOT evidence)
|
|
87
87
|
dz epoch-replay --emit [--project <dir>] [--limit <N>] [--seed <N>] [--out <file>] (cold-vs-warm work order: instances + PRE-REGISTERED blind A/B assignment; the runner never calls a model)
|
|
88
88
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
89
89
|
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
90
|
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)
|
|
91
|
+
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
92
|
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
93
|
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
94
|
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")
|
|
@@ -106,7 +107,7 @@ Usage:
|
|
|
106
107
|
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
108
|
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
109
|
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
|
|
110
|
+
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
111
|
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
112
|
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
112
113
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
@@ -122,7 +123,7 @@ Usage:
|
|
|
122
123
|
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
124
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
124
125
|
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>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz
|
|
126
|
+
dz teach "<pattern>" [--class-form "<template with :slot>"] [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (class form is optional; rejection never blocks the specific write; --project pins the learned store to <dir>/.dz)
|
|
126
127
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
127
128
|
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
129
|
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)
|
|
@@ -145,6 +146,7 @@ Usage:
|
|
|
145
146
|
dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
|
|
146
147
|
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
148
|
dz usage --by-stage [--run <runId> | --slug <slug>] [--epsilon <0..1>] [--write <file.jsonl>] [--json] (per-stage cost ledger for ONE feature-adr run + the reconciliation invariant: accounted + unaccounted = run total; verdict BALANCED | DEFECT | INSUFFICIENT_DATA; local transcript ESTIMATES — catches ATTRIBUTION errors, not pricing errors)
|
|
149
|
+
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
150
|
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
151
|
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
152
|
dz pretrain [--project <dir>]
|
|
@@ -161,6 +163,7 @@ Usage:
|
|
|
161
163
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
162
164
|
dz agents-sync [--project <dir>] [--check] [--json] (sync/verify the always-on policy fence in root AGENTS.md; exit 0 synced/written, 1 drift, 3 inconclusive)
|
|
163
165
|
dz hooks-sync --target codex [--check] [--verify] [--remove] [--json] (install/verify the dz veto + recall hooks in $CODEX_HOME/hooks.json; exit 0 armed+trusted, 1 not armed/drift, 3 inconclusive)
|
|
166
|
+
dz integrations-verify --target <name> --component <mcp|hooks> [--project <dir>] [--json] (non-executing exact-version registration probe; exit 0 observed / 1 refused)
|
|
164
167
|
dz sync-canonical <skill> [--check] [--from <dir>] [--auto] [--project <dir>] (heal every copy from skills-meta/<skill> or --from; no canonical + --check = compare copies to each other (exit 1 on drift); no canonical + write = refuse unless --auto (LOUD, picks most-complete copy); --check writes nothing)
|
|
165
168
|
dz plugin [--version <ver>]
|
|
166
169
|
dz downloads
|
|
@@ -265,6 +268,35 @@ function discoverSkillsDirs(cwd, explicitSkillsDir) {
|
|
|
265
268
|
async function installSkills(opts) {
|
|
266
269
|
const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
|
|
267
270
|
const skillsDirs = discoverSkillsDirs(cwd, explicitSkillsDir);
|
|
271
|
+
// PREFLIGHT (backlog 9d15b9b6, PR-A) — resolve the REQUEST once, before anything is written.
|
|
272
|
+
//
|
|
273
|
+
// Two defects lived in asking each root independently instead of resolving the request: a skill
|
|
274
|
+
// present in two roots was installed TWICE and counted twice (the field report's `2 skill(s)` was
|
|
275
|
+
// one skill installed twice), and a skill present in NO root produced a warning and exit 0 —
|
|
276
|
+
// `0 skill(s)` reading as success. Both are gone once the decision happens here.
|
|
277
|
+
//
|
|
278
|
+
// Placement is load-bearing: an exit 1 that arrives after hooks and memory are written leaves a
|
|
279
|
+
// half-configured project, which is worse than either clean outcome. This runs before the loop
|
|
280
|
+
// below and before every target adapter.
|
|
281
|
+
//
|
|
282
|
+
// Dependency closure is deliberately NOT resolved here — that is PR-B. This preflight fixes the
|
|
283
|
+
// count and the exit contract, and gives that work a base it can trust.
|
|
284
|
+
if (select !== undefined) {
|
|
285
|
+
const roots = skillsDirs.map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
286
|
+
const resolution = resolveSelection(select, roots);
|
|
287
|
+
for (const shadow of resolution.shadowed) {
|
|
288
|
+
opts.writeErr?.(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; ` +
|
|
289
|
+
`installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
292
|
+
if (refusal !== null) {
|
|
293
|
+
return {
|
|
294
|
+
selectRefusal: refusal,
|
|
295
|
+
results: [], dirsSearched: skillsDirs.length, written: 0, skipped: 0,
|
|
296
|
+
missing: [...resolution.missing], failures: [], applyFailures: [], integrations: [],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
268
300
|
// agents-md and gemini are FLATTENING single-file targets: each must aggregate
|
|
269
301
|
// every selected skill from ALL discovered dirs into ONE root file (AGENTS.md /
|
|
270
302
|
// GEMINI.md) in a single merge. A per-dir runInit loop (like the tree targets
|
|
@@ -272,8 +304,8 @@ async function installSkills(opts) {
|
|
|
272
304
|
// route them through one aggregation.
|
|
273
305
|
if (target === 'agents-md' || target === 'gemini') {
|
|
274
306
|
const report = target === 'gemini'
|
|
275
|
-
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
|
|
276
|
-
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) });
|
|
307
|
+
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) })
|
|
308
|
+
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) });
|
|
277
309
|
const results = report.skills.map((s) => ({
|
|
278
310
|
id: s.id,
|
|
279
311
|
written: s.written.length,
|
|
@@ -285,12 +317,20 @@ async function installSkills(opts) {
|
|
|
285
317
|
written += s.written;
|
|
286
318
|
skipped += s.skipped;
|
|
287
319
|
}
|
|
288
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
|
|
320
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures], integrations: [...report.integrations], ...(report.integrationDigest !== undefined ? { integrationDigest: report.integrationDigest } : {}) };
|
|
289
321
|
}
|
|
290
322
|
const results = [];
|
|
291
323
|
const failures = [];
|
|
292
324
|
const applyFailures = [];
|
|
293
|
-
|
|
325
|
+
let integrations = [];
|
|
326
|
+
let integrationDigest;
|
|
327
|
+
const integrationManifestSources = skillsDirs.flatMap((skillsDir) => {
|
|
328
|
+
const discovered = discoverSkillIds(skillsDir);
|
|
329
|
+
return discovered
|
|
330
|
+
.filter((id) => select === undefined || select.includes(id))
|
|
331
|
+
.map((skillId) => ({ skillId, skillDir: skillsDir }));
|
|
332
|
+
});
|
|
333
|
+
for (const [dirIndex, skillsDir] of skillsDirs.entries()) {
|
|
294
334
|
const r = await runInit({
|
|
295
335
|
target,
|
|
296
336
|
skillsDir,
|
|
@@ -298,12 +338,27 @@ async function installSkills(opts) {
|
|
|
298
338
|
force,
|
|
299
339
|
enrich,
|
|
300
340
|
...(select !== undefined ? { select } : {}),
|
|
341
|
+
...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}),
|
|
342
|
+
...(dirIndex === 0
|
|
343
|
+
? {
|
|
344
|
+
integrationManifestSources,
|
|
345
|
+
...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}),
|
|
346
|
+
}
|
|
347
|
+
: { noIntegrations: true }),
|
|
348
|
+
...(opts.noVerify !== undefined ? { noVerify: opts.noVerify } : {}),
|
|
349
|
+
...(opts.allowIntegrations !== undefined ? { allowIntegrations: opts.allowIntegrations } : {}),
|
|
301
350
|
});
|
|
302
351
|
for (const skill of r.skills) {
|
|
303
352
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
304
353
|
}
|
|
305
354
|
failures.push(...r.failures);
|
|
306
355
|
applyFailures.push(...r.applyFailures);
|
|
356
|
+
if (r.integrations.some((row) => row.status !== 'not-requested'))
|
|
357
|
+
integrations = [...r.integrations];
|
|
358
|
+
else if (integrations.length === 0)
|
|
359
|
+
integrations = [...r.integrations];
|
|
360
|
+
if (r.integrationDigest !== undefined)
|
|
361
|
+
integrationDigest = r.integrationDigest;
|
|
307
362
|
}
|
|
308
363
|
let written = 0;
|
|
309
364
|
let skipped = 0;
|
|
@@ -313,7 +368,7 @@ async function installSkills(opts) {
|
|
|
313
368
|
}
|
|
314
369
|
const installed = new Set(results.map((s) => s.id));
|
|
315
370
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
316
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
|
|
371
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures, integrations, ...(integrationDigest !== undefined ? { integrationDigest } : {}) };
|
|
317
372
|
}
|
|
318
373
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
319
374
|
function writeMissingSkillsHint(write, missing, presetName) {
|
|
@@ -374,19 +429,24 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
374
429
|
cwd,
|
|
375
430
|
explicitSkillsDir,
|
|
376
431
|
select,
|
|
432
|
+
writeErr,
|
|
377
433
|
force: flags.has('force'),
|
|
378
434
|
enrich: flags.has('enrich'),
|
|
435
|
+
noHooks: flags.has('no-hooks'),
|
|
436
|
+
noIntegrations: flags.has('no-integrations'),
|
|
437
|
+
noVerify: flags.has('no-verify'),
|
|
438
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
379
439
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
440
|
+
// PR-A: an explicit --select that named a skill no root provides is a REFUSAL, not a warning.
|
|
441
|
+
// Printed and returned here, before any target adapter runs — nothing has been written yet.
|
|
442
|
+
if (r.selectRefusal !== undefined) {
|
|
443
|
+
writeErr(r.selectRefusal);
|
|
444
|
+
return 1;
|
|
383
445
|
}
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
// carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
|
|
389
|
-
// must be able to say so — and every test that is about skills says it.
|
|
446
|
+
// Codex keeps its established user-registry writer, but its result is normalized into the same
|
|
447
|
+
// two-outcome contract before JSON/human rendering. A write without a live ready observation is
|
|
448
|
+
// a refusal with applied=true, never a second success channel.
|
|
449
|
+
let integrationOutcomes = r.integrations;
|
|
390
450
|
let codexHooksOk = true;
|
|
391
451
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
392
452
|
const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
|
|
@@ -395,7 +455,30 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
395
455
|
write(line);
|
|
396
456
|
for (const line of delivery.stderr)
|
|
397
457
|
writeErr(line);
|
|
458
|
+
const hookIndex = integrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
459
|
+
if (hookIndex !== -1) {
|
|
460
|
+
const hook = normalizeCodexHookOutcome(integrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
|
|
461
|
+
integrationOutcomes = integrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (flags.has('json')) {
|
|
465
|
+
write(JSON.stringify({ target, skills: { count: r.results.length, written: r.written, skipped: r.skipped }, integrations: integrationOutcomes, integrationDigest: r.integrationDigest ?? null }));
|
|
398
466
|
}
|
|
467
|
+
else {
|
|
468
|
+
write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
|
|
469
|
+
if (r.dirsSearched > 1)
|
|
470
|
+
write(` (searched ${r.dirsSearched} skill directories)`);
|
|
471
|
+
for (const outcome of integrationOutcomes) {
|
|
472
|
+
const label = outcome.component === 'mcp' ? 'MCP' : 'Hooks';
|
|
473
|
+
const detail = outcome.status === 'refused'
|
|
474
|
+
? `${outcome.reasonCode ?? 'LIVE_PROBE_FAILED'}${outcome.remediation ? ` — ${outcome.remediation}` : ''}`
|
|
475
|
+
: outcome.status === 'emitted'
|
|
476
|
+
? `${outcome.carrier?.path ?? 'registered'}${outcome.registrations.some((row) => row.approval === 'pending') ? '; Pending approval; ready=false' : ''}`
|
|
477
|
+
: 'explicitly not requested';
|
|
478
|
+
write(`${label}: ${outcome.status.toUpperCase()} (${detail})`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
writeMissingSkillsHint(write, r.missing, presetName);
|
|
399
482
|
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
400
483
|
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
401
484
|
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
@@ -418,7 +501,38 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
418
501
|
writeErr(line);
|
|
419
502
|
return 1;
|
|
420
503
|
}
|
|
421
|
-
|
|
504
|
+
const integrationOk = !integrationOutcomes.some((row) => row.status === 'refused');
|
|
505
|
+
return codexHooksOk && integrationOk ? 0 : 1;
|
|
506
|
+
}
|
|
507
|
+
function cmdIntegrationsVerify(options, flags, cwd, write, writeErr) {
|
|
508
|
+
const targetInput = options.get('target');
|
|
509
|
+
const component = options.get('component');
|
|
510
|
+
if (targetInput === undefined || (component !== 'mcp' && component !== 'hooks')) {
|
|
511
|
+
writeErr('dz integrations-verify: requires --target <name> --component <mcp|hooks>');
|
|
512
|
+
return 1;
|
|
513
|
+
}
|
|
514
|
+
const resolution = resolveTargetName(targetInput);
|
|
515
|
+
if (resolution.kind === 'unknown') {
|
|
516
|
+
for (const line of formatTargetProblem('dz integrations-verify', resolution))
|
|
517
|
+
writeErr(line);
|
|
518
|
+
return 1;
|
|
519
|
+
}
|
|
520
|
+
if (resolution.via === 'alias')
|
|
521
|
+
writeErr(formatTargetAliasNote('dz integrations-verify', targetInput, resolution.target));
|
|
522
|
+
const result = runIntegrationsVerify({
|
|
523
|
+
target: resolution.target,
|
|
524
|
+
component,
|
|
525
|
+
projectRoot: resolve(cwd, options.get('project') ?? '.'),
|
|
526
|
+
});
|
|
527
|
+
if (flags.has('json'))
|
|
528
|
+
write(JSON.stringify(result));
|
|
529
|
+
else if (result.ok) {
|
|
530
|
+
write(`dz integrations-verify: ${resolution.target}/${component} registered (runtime ${result.runtimeVersion ?? 'unknown'}; ready=${result.registrations.every((row) => row.ready === true)})`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
writeErr(`dz integrations-verify: ${resolution.target}/${component} REFUSED (${result.reasonCode ?? 'LIVE_PROBE_FAILED'}) — ${result.remediation ?? 'no qualifying receipt'}`);
|
|
534
|
+
}
|
|
535
|
+
return result.ok ? 0 : 1;
|
|
422
536
|
}
|
|
423
537
|
async function cmdVerify(options, cwd, write, writeErr) {
|
|
424
538
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
@@ -1870,6 +1984,150 @@ function cmdStatuslineFaRecord(options, cwd, write) {
|
|
|
1870
1984
|
write(`dz statusline: recorded /feature-adr learning state for "${slug}" (${step}) — 🎓 ${state.pool} pool · ↑${state.recalled} used · +${state.stored} new · ↻${state.reinforced ?? 0} reinforced`);
|
|
1871
1985
|
return 0;
|
|
1872
1986
|
}
|
|
1987
|
+
function statuslineEtaStage(step) {
|
|
1988
|
+
const match = step.match(/\bStep\s*-?\s*(0|[1-9](?:\.5)?)(?:\b|\s)/i);
|
|
1989
|
+
if (match === null)
|
|
1990
|
+
return undefined;
|
|
1991
|
+
const value = Number(match[1]);
|
|
1992
|
+
if (value === 0)
|
|
1993
|
+
return 'router';
|
|
1994
|
+
if (value > 0 && value < 6)
|
|
1995
|
+
return 'design';
|
|
1996
|
+
if (value === 6)
|
|
1997
|
+
return 'plan';
|
|
1998
|
+
if (value === 7)
|
|
1999
|
+
return 'code';
|
|
2000
|
+
if (value === 8)
|
|
2001
|
+
return 'qe';
|
|
2002
|
+
if (value === 9)
|
|
2003
|
+
return 'fleet';
|
|
2004
|
+
return undefined;
|
|
2005
|
+
}
|
|
2006
|
+
function routerMetadata(router) {
|
|
2007
|
+
const result = router?.result;
|
|
2008
|
+
if (result === null || typeof result !== 'object')
|
|
2009
|
+
return {};
|
|
2010
|
+
const record = result;
|
|
2011
|
+
const tier = typeof record['tier'] === 'string' && record['tier'].length > 0 ? record['tier'] : undefined;
|
|
2012
|
+
const activeSteps = Array.isArray(record['activeSteps'])
|
|
2013
|
+
? record['activeSteps'].filter((value) => typeof value === 'number' && Number.isFinite(value))
|
|
2014
|
+
: undefined;
|
|
2015
|
+
return {
|
|
2016
|
+
...(tier !== undefined ? { tier } : {}),
|
|
2017
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function activeCheckpointStages(tier, activeSteps) {
|
|
2021
|
+
const stages = new Set();
|
|
2022
|
+
if (activeSteps !== undefined) {
|
|
2023
|
+
for (const step of activeSteps) {
|
|
2024
|
+
if (step === 0)
|
|
2025
|
+
stages.add('router');
|
|
2026
|
+
else if (step > 0 && step < 6)
|
|
2027
|
+
stages.add('design');
|
|
2028
|
+
else if (step === 6)
|
|
2029
|
+
stages.add('plan');
|
|
2030
|
+
else if (step === 7)
|
|
2031
|
+
stages.add('code');
|
|
2032
|
+
else if (step === 8)
|
|
2033
|
+
stages.add('qe');
|
|
2034
|
+
else if (step === 9)
|
|
2035
|
+
stages.add('fleet');
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
else if (tier !== undefined) {
|
|
2039
|
+
for (const stage of CHECKPOINT_STAGES) {
|
|
2040
|
+
if (stage !== 'fleet' || tier === 'L' || tier === 'XL')
|
|
2041
|
+
stages.add(stage);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return CHECKPOINT_STAGES.filter((stage) => stages.has(stage));
|
|
2045
|
+
}
|
|
2046
|
+
/** All checkpoint I/O for ETA lives in this CLI-only wrapper and degrades per file. */
|
|
2047
|
+
function readEtaCorpus(projectRoot, currentSlug) {
|
|
2048
|
+
const trace = process.env['DZ_ETA_TRACE'];
|
|
2049
|
+
if (trace !== undefined && trace.length > 0) {
|
|
2050
|
+
try {
|
|
2051
|
+
appendFileSync(trace, `${JSON.stringify({ projectRoot, currentSlug, ts: new Date().toISOString() })}\n`);
|
|
2052
|
+
}
|
|
2053
|
+
catch { /* a test/debug receipt can never take down the statusline */ }
|
|
2054
|
+
}
|
|
2055
|
+
const samples = [];
|
|
2056
|
+
let currentSegments = [];
|
|
2057
|
+
let hasCurrentCheckpoints = false;
|
|
2058
|
+
let entries = [];
|
|
2059
|
+
try {
|
|
2060
|
+
entries = readdirSync(join(projectRoot, 'features'), { withFileTypes: true });
|
|
2061
|
+
}
|
|
2062
|
+
catch {
|
|
2063
|
+
return {
|
|
2064
|
+
history: [], currentRunSamples: [], currentTier: undefined, activeStages: [],
|
|
2065
|
+
lastCheckpointTsMs: undefined, hasCurrentCheckpoints: false,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
for (const entry of entries) {
|
|
2069
|
+
if (!entry.isDirectory())
|
|
2070
|
+
continue;
|
|
2071
|
+
const checkpointPath = join(projectRoot, 'features', entry.name, '.fa-state', 'checkpoints.jsonl');
|
|
2072
|
+
try {
|
|
2073
|
+
const text = readFileSync(checkpointPath, 'utf8');
|
|
2074
|
+
const observations = parseCheckpointLines(text, entry.name);
|
|
2075
|
+
const segments = segmentRun(observations);
|
|
2076
|
+
samples.push(...extractStageSamples(segments));
|
|
2077
|
+
if (entry.name === currentSlug) {
|
|
2078
|
+
hasCurrentCheckpoints = observations.length > 0;
|
|
2079
|
+
currentSegments = segments;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
catch {
|
|
2083
|
+
// Missing, unreadable, or racing append: this run contributes no evidence.
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const lastSegment = currentSegments.at(-1);
|
|
2087
|
+
const currentRouter = lastSegment?.router;
|
|
2088
|
+
const invocationSegments = currentRouter === undefined
|
|
2089
|
+
? []
|
|
2090
|
+
: currentSegments.filter((segment) => segment.router === currentRouter);
|
|
2091
|
+
const invocationRunIds = new Set(invocationSegments.map((segment) => segment.runId));
|
|
2092
|
+
const currentRunSamples = extractStageSamples(invocationSegments);
|
|
2093
|
+
const metadata = routerMetadata(currentRouter);
|
|
2094
|
+
const currentTimestamps = invocationSegments
|
|
2095
|
+
.flatMap((segment) => segment.observations)
|
|
2096
|
+
.flatMap((observation) => observation.tsMs === undefined ? [] : [observation.tsMs]);
|
|
2097
|
+
return {
|
|
2098
|
+
history: samples.filter((sample) => !invocationRunIds.has(sample.runId)),
|
|
2099
|
+
currentRunSamples,
|
|
2100
|
+
currentTier: metadata.tier,
|
|
2101
|
+
activeStages: activeCheckpointStages(metadata.tier, metadata.activeSteps),
|
|
2102
|
+
lastCheckpointTsMs: currentTimestamps.length > 0 ? Math.max(...currentTimestamps) : undefined,
|
|
2103
|
+
hasCurrentCheckpoints,
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function statuslineEta(projectRoot, state, nowMs) {
|
|
2107
|
+
const currentStage = statuslineEtaStage(state.step);
|
|
2108
|
+
if (currentStage === undefined)
|
|
2109
|
+
return undefined;
|
|
2110
|
+
const corpus = readEtaCorpus(projectRoot, state.slug);
|
|
2111
|
+
const currentIndex = CHECKPOINT_STAGES.indexOf(currentStage);
|
|
2112
|
+
const remainingStages = corpus.activeStages.filter((stage) => CHECKPOINT_STAGES.indexOf(stage) >= currentIndex && stage !== 'router');
|
|
2113
|
+
// Preserve honest absence as a machine-readable union member. The text formatter intentionally
|
|
2114
|
+
// omits both variants, but `--json` must still distinguish no file from a file with no tier.
|
|
2115
|
+
const stagesForEstimate = remainingStages.length > 0
|
|
2116
|
+
? remainingStages
|
|
2117
|
+
: (currentStage === 'router' ? [] : [currentStage]);
|
|
2118
|
+
if (stagesForEstimate.length === 0 && corpus.hasCurrentCheckpoints && corpus.currentTier !== undefined)
|
|
2119
|
+
return undefined;
|
|
2120
|
+
return estimateEta({
|
|
2121
|
+
samples: corpus.history,
|
|
2122
|
+
currentTier: corpus.currentTier,
|
|
2123
|
+
currentStage,
|
|
2124
|
+
remainingStages: stagesForEstimate,
|
|
2125
|
+
currentRunSamples: corpus.currentRunSamples,
|
|
2126
|
+
nowMs,
|
|
2127
|
+
...(corpus.lastCheckpointTsMs !== undefined ? { lastCheckpointTsMs: corpus.lastCheckpointTsMs } : {}),
|
|
2128
|
+
hasCurrentCheckpoints: corpus.hasCurrentCheckpoints,
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
1873
2131
|
/**
|
|
1874
2132
|
* `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
|
|
1875
2133
|
* Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
|
|
@@ -1889,8 +2147,23 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
1889
2147
|
try {
|
|
1890
2148
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
1891
2149
|
const data = statuslineData(projectRoot);
|
|
2150
|
+
const fa = data.featureAdr;
|
|
2151
|
+
let eta;
|
|
2152
|
+
let etaFragment;
|
|
2153
|
+
if (fa !== undefined && fa.kind !== 'loop') {
|
|
2154
|
+
try {
|
|
2155
|
+
eta = statuslineEta(projectRoot, fa, Date.now());
|
|
2156
|
+
if (eta !== undefined)
|
|
2157
|
+
etaFragment = formatEta(eta);
|
|
2158
|
+
}
|
|
2159
|
+
catch {
|
|
2160
|
+
// ETA is advisory: any corpus/resource failure omits only this fragment.
|
|
2161
|
+
eta = undefined;
|
|
2162
|
+
etaFragment = undefined;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
1892
2165
|
if (flags.has('json')) {
|
|
1893
|
-
write(JSON.stringify(data));
|
|
2166
|
+
write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
|
|
1894
2167
|
return 0;
|
|
1895
2168
|
}
|
|
1896
2169
|
let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
|
|
@@ -1900,14 +2173,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
1900
2173
|
if (data.consolidatedAgeH !== undefined)
|
|
1901
2174
|
line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
1902
2175
|
// Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
|
|
1903
|
-
const fa = data.featureAdr;
|
|
1904
2176
|
if (fa !== undefined) {
|
|
1905
2177
|
// The producer marker reached data and arbitration in a previous round but not this label, so the bar asserted a pipeline that was not running.
|
|
1906
2178
|
if (fa.kind === 'loop') {
|
|
1907
2179
|
line = `🔁 loop ${fa.step} · ${line}`;
|
|
1908
2180
|
}
|
|
1909
2181
|
else {
|
|
1910
|
-
line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2182
|
+
line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
1911
2183
|
}
|
|
1912
2184
|
}
|
|
1913
2185
|
write(line);
|
|
@@ -2494,7 +2766,12 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
|
|
|
2494
2766
|
const reason = teachReasonPhrase(target.reason);
|
|
2495
2767
|
return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
|
|
2496
2768
|
}
|
|
2497
|
-
async function
|
|
2769
|
+
async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
|
|
2770
|
+
const backend = resolveLearningBackend(projectRoot);
|
|
2771
|
+
backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
|
|
2772
|
+
return backend.train();
|
|
2773
|
+
}
|
|
2774
|
+
async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
|
|
2498
2775
|
// WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
|
|
2499
2776
|
// `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
|
|
2500
2777
|
// per-session choice; for a CLI every invocation is a fresh process, so the only honest session
|
|
@@ -2585,26 +2862,39 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2585
2862
|
write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
|
|
2586
2863
|
return 1;
|
|
2587
2864
|
}
|
|
2588
|
-
const
|
|
2865
|
+
const importedKey = (p) => p.lessonForm !== undefined && p.lessonPairId !== undefined
|
|
2866
|
+
? `${p.pattern}\u0000${p.lessonForm}\u0000${p.lessonPairId}`
|
|
2867
|
+
: `legacy\u0000${p.pattern}`;
|
|
2868
|
+
const existing = new Set(loadStorePatternsSync(storeRoot).map(importedKey));
|
|
2589
2869
|
let imported = 0;
|
|
2590
2870
|
let skipped = 0;
|
|
2591
2871
|
const importedRecs = [];
|
|
2592
2872
|
for (const item of parsed) {
|
|
2593
2873
|
const p = item;
|
|
2594
|
-
if (!p || typeof p
|
|
2874
|
+
if (!p || typeof p !== 'object') {
|
|
2595
2875
|
skipped += 1;
|
|
2596
2876
|
continue;
|
|
2597
2877
|
}
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2878
|
+
const pair = (p.lessonForm === 'specific' || p.lessonForm === 'class')
|
|
2879
|
+
&& typeof p.lessonPairId === 'string' && p.lessonPairId !== ''
|
|
2880
|
+
? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
|
|
2881
|
+
: {};
|
|
2882
|
+
const candidate = {
|
|
2883
|
+
pattern: typeof p.pattern === 'string' ? p.pattern : '',
|
|
2600
2884
|
type: (typeof p.type === 'string' ? p.type : 'lesson-learned'),
|
|
2601
2885
|
reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
|
|
2602
2886
|
domain: typeof p.domain === 'string' ? p.domain : 'general',
|
|
2603
2887
|
ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
|
|
2604
2888
|
source: 'dz-teach-import',
|
|
2889
|
+
...pair,
|
|
2605
2890
|
};
|
|
2891
|
+
if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
|
|
2892
|
+
skipped += 1;
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
const rec = candidate;
|
|
2606
2896
|
await recordPattern(storeRoot, rec);
|
|
2607
|
-
existing.add(
|
|
2897
|
+
existing.add(importedKey(rec));
|
|
2608
2898
|
importedRecs.push(rec);
|
|
2609
2899
|
imported += 1;
|
|
2610
2900
|
}
|
|
@@ -2687,23 +2977,32 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2687
2977
|
}
|
|
2688
2978
|
const reward = parseFloat(options.get('reward') ?? '0.8');
|
|
2689
2979
|
const domain = options.get('domain') ?? 'general';
|
|
2980
|
+
const classWasRequested = options.has('class-form') || flags.has('class-form');
|
|
2981
|
+
const lessonForms = normalizeLessonForms(pattern, options.get('class-form'));
|
|
2982
|
+
let guardedExisting;
|
|
2690
2983
|
if (flags.has('guard')) {
|
|
2691
|
-
const verdict = await
|
|
2984
|
+
const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
|
|
2692
2985
|
if (verdict.action === 'reinforce') {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2986
|
+
if (lessonForms.classForm !== undefined) {
|
|
2987
|
+
const existing = loadStoreRecords(storeRoot).find((record) => record.id === verdict.dzId);
|
|
2988
|
+
if (existing !== undefined) {
|
|
2989
|
+
guardedExisting = { dzId: verdict.dzId, cosine: verdict.cosine, pattern: recordToPattern(existing) };
|
|
2990
|
+
}
|
|
2991
|
+
else {
|
|
2992
|
+
write(`dz teach --guard: matched pattern ${verdict.dzId} was not found in the lexical store — teaching the lesson normally`);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
else {
|
|
2996
|
+
const trained = await reinforceRunner(storeRoot, verdict.dzId, Math.max(0, Math.min(1, reward)));
|
|
2997
|
+
if (trained.flushed > 0) {
|
|
2998
|
+
write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
|
|
2999
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
|
|
3000
|
+
if (clearedQ.cleared > 0)
|
|
3001
|
+
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3002
|
+
return 0;
|
|
3003
|
+
}
|
|
3004
|
+
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
2705
3005
|
}
|
|
2706
|
-
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
2707
3006
|
}
|
|
2708
3007
|
}
|
|
2709
3008
|
// Distill pattern into actionable rule (claude-smart inspired)
|
|
@@ -2716,8 +3015,8 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2716
3015
|
// Typed through the shared schema (harness-core owns PatternRecord) so the
|
|
2717
3016
|
// write side and the read side (recommend's loadPatterns) can never drift —
|
|
2718
3017
|
// a field rename here is a compile error, not a silently re-muted loop (audit #2).
|
|
2719
|
-
const entry = {
|
|
2720
|
-
pattern,
|
|
3018
|
+
const entry = guardedExisting?.pattern ?? {
|
|
3019
|
+
pattern: lessonForms.specific,
|
|
2721
3020
|
type: type,
|
|
2722
3021
|
reward: Math.max(0, Math.min(1, reward)),
|
|
2723
3022
|
domain,
|
|
@@ -2729,9 +3028,47 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2729
3028
|
// total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
|
|
2730
3029
|
// lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
|
|
2731
3030
|
const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2734
|
-
|
|
3031
|
+
const stored = await recordLessonForms(storeRoot, entry, lessonForms.classForm, quarantineOn ? { quarantine: true } : {});
|
|
3032
|
+
let recordsToMirror = stored.records;
|
|
3033
|
+
let commandFailed = stored.class === 'failed';
|
|
3034
|
+
let reinforced = false;
|
|
3035
|
+
if (guardedExisting !== undefined) {
|
|
3036
|
+
let reinforceId = guardedExisting.dzId;
|
|
3037
|
+
if (stored.class === 'stored') {
|
|
3038
|
+
const specificRow = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3039
|
+
if (specificRow !== undefined)
|
|
3040
|
+
reinforceId = patternRecordId(specificRow);
|
|
3041
|
+
if (reinforceId !== guardedExisting.dzId) {
|
|
3042
|
+
const removed = removePatternsByIds(storeRoot, new Set([guardedExisting.dzId]));
|
|
3043
|
+
if (removed.error !== undefined || removed.removed === 0) {
|
|
3044
|
+
commandFailed = true;
|
|
3045
|
+
writeErr(`dz teach --guard: class pair stored, but old pattern cleanup failed${removed.error === undefined ? '' : ` — ${removed.error}`}`);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
else if (stored.class === 'failed') {
|
|
3050
|
+
const partialSpecific = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3051
|
+
if (partialSpecific !== undefined && patternRecordId(partialSpecific) !== guardedExisting.dzId) {
|
|
3052
|
+
const rolledBack = removePatternsByIds(storeRoot, new Set([patternRecordId(partialSpecific)]));
|
|
3053
|
+
if (rolledBack.error !== undefined)
|
|
3054
|
+
writeErr(`dz teach --guard: partial enrichment rollback failed — ${rolledBack.error}`);
|
|
3055
|
+
}
|
|
3056
|
+
recordsToMirror = [];
|
|
3057
|
+
reinforceId = guardedExisting.dzId;
|
|
3058
|
+
}
|
|
3059
|
+
if (!commandFailed) {
|
|
3060
|
+
const trained = await reinforceRunner(storeRoot, reinforceId, Math.max(0, Math.min(1, reward)));
|
|
3061
|
+
reinforced = trained.flushed > 0;
|
|
3062
|
+
if (reinforced) {
|
|
3063
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [reinforceId]);
|
|
3064
|
+
if (clearedQ.cleared > 0)
|
|
3065
|
+
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
const count = guardedExisting === undefined ? stored.count : loadStorePatternsSync(storeRoot).length;
|
|
3070
|
+
write(`${guardedExisting !== undefined && stored.class === 'stored' ? 'Enriched' : 'Learned'}: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
|
|
3071
|
+
write(` Domain: ${entry.domain} Reward: ${entry.reward} Backend: memory (@dzhechkov/memory)`);
|
|
2735
3072
|
write(` Total patterns: ${count}`);
|
|
2736
3073
|
// WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
|
|
2737
3074
|
// domain, the reward and the backend — and not one word about the path, so a store written to
|
|
@@ -2741,6 +3078,24 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2741
3078
|
// sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
|
|
2742
3079
|
// nothing, so the line stays byte-identical for everyone who set nothing.
|
|
2743
3080
|
write(storeLine('written'));
|
|
3081
|
+
if (stored.class === 'stored')
|
|
3082
|
+
write(' Class form: stored separately and linked to the specific lesson');
|
|
3083
|
+
if (classWasRequested && stored.class === 'absent')
|
|
3084
|
+
write(' Class form: skipped; the specific lesson was saved');
|
|
3085
|
+
if (stored.class === 'rejected')
|
|
3086
|
+
writeErr(`dz teach: class form rejected — ${stored.reason ?? 'invalid class form'}; the specific lesson was saved`);
|
|
3087
|
+
if (stored.class === 'failed')
|
|
3088
|
+
writeErr(`dz teach: specific stored; class failed — ${stored.reason ?? 'unknown storage failure'}`);
|
|
3089
|
+
if (guardedExisting !== undefined && stored.class === 'stored') {
|
|
3090
|
+
write(commandFailed
|
|
3091
|
+
? ' Guard: class enrichment stored; reinforcement skipped because old-pattern cleanup failed'
|
|
3092
|
+
: reinforced
|
|
3093
|
+
? ` Guard: enriched existing pattern (cos=${guardedExisting.cosine.toFixed(2)}), then reinforced the linked specific form`
|
|
3094
|
+
: ' Guard: class enrichment stored; reinforcement did not flush');
|
|
3095
|
+
}
|
|
3096
|
+
if (!classWasRequested && interactive) {
|
|
3097
|
+
write(' Rule of one place or of a class? Optional: add --class-form when a future reader needs the why for structure, interfaces, or maintainability; skip when scope, risk, time, and cost are low or standards, policy, or documentation already cover it.');
|
|
3098
|
+
}
|
|
2744
3099
|
// ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
|
|
2745
3100
|
// directories and this binary; refusing would be defending a user against themselves,
|
|
2746
3101
|
// which this design does not attempt. Making the choice INFORMED is the part that is
|
|
@@ -2759,8 +3114,8 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2759
3114
|
write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
|
|
2760
3115
|
}
|
|
2761
3116
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
2762
|
-
await emitMirrorQ(storeRoot,
|
|
2763
|
-
return 0;
|
|
3117
|
+
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3118
|
+
return commandFailed ? 1 : 0;
|
|
2764
3119
|
}
|
|
2765
3120
|
async function cmdConsolidate(options, flags, cwd, write) {
|
|
2766
3121
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
@@ -3120,7 +3475,7 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
|
|
|
3120
3475
|
write(` not found: ${res.notFound.join(', ')}`);
|
|
3121
3476
|
return 0;
|
|
3122
3477
|
}
|
|
3123
|
-
async function cmdRecall(options, flags, cwd, write) {
|
|
3478
|
+
async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
|
|
3124
3479
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3125
3480
|
const asJson = flags.has('json');
|
|
3126
3481
|
const all = flags.has('all');
|
|
@@ -3141,7 +3496,15 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3141
3496
|
// review could not make correct: a tag set by the writer is decidable, prose is not.
|
|
3142
3497
|
const allPatterns = loadStorePatternsSync(projectRoot);
|
|
3143
3498
|
const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
|
|
3144
|
-
const
|
|
3499
|
+
const classByPair = new Map(holdout.exported
|
|
3500
|
+
.filter((row) => row.lessonForm === 'class' && row.lessonPairId !== undefined)
|
|
3501
|
+
.map((row) => [row.lessonPairId, row.pattern]));
|
|
3502
|
+
const patterns = holdout.exported.map((row) => {
|
|
3503
|
+
const classForm = row.lessonForm === 'specific' && row.lessonPairId !== undefined
|
|
3504
|
+
? classByPair.get(row.lessonPairId)
|
|
3505
|
+
: undefined;
|
|
3506
|
+
return classForm === undefined ? row : { ...row, classForm };
|
|
3507
|
+
});
|
|
3145
3508
|
const holdoutNote = renderHoldoutNote(holdout);
|
|
3146
3509
|
// The opt-in is honoured without argument — and named out loud. A flag that silently
|
|
3147
3510
|
// includes medical lessons in a portable export is a flag whose consequence the user
|
|
@@ -3171,7 +3534,15 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3171
3534
|
const rawStats = storeStats(projectRoot);
|
|
3172
3535
|
const topHoldout = applyExportHoldout(rawStats.topUses, heldOutAfterOptIn(options.get('include-domain')));
|
|
3173
3536
|
const perDomain = Object.fromEntries(Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))));
|
|
3174
|
-
const
|
|
3537
|
+
const generalizedPairs = new Set(patterns.flatMap((p) => p.lessonPairId === undefined ? [] : [p.lessonPairId]));
|
|
3538
|
+
const unpairedLessons = patterns.filter((p) => p.lessonPairId === undefined).length;
|
|
3539
|
+
const stats = {
|
|
3540
|
+
...rawStats,
|
|
3541
|
+
topUses: topHoldout.exported,
|
|
3542
|
+
perDomain,
|
|
3543
|
+
generalized: generalizedPairs.size,
|
|
3544
|
+
logicalLessons: generalizedPairs.size + unpairedLessons,
|
|
3545
|
+
};
|
|
3175
3546
|
const backendStats = resolveLearningBackend(projectRoot).getStats();
|
|
3176
3547
|
if (asJson) {
|
|
3177
3548
|
write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
|
|
@@ -3185,6 +3556,7 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3185
3556
|
write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
|
|
3186
3557
|
write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
|
|
3187
3558
|
write(` exact-dup groups: ${stats.exactDupGroups}`);
|
|
3559
|
+
write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
|
|
3188
3560
|
write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
|
|
3189
3561
|
write(' top uses:');
|
|
3190
3562
|
for (const row of stats.topUses)
|
|
@@ -3312,6 +3684,10 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3312
3684
|
? 'lexical'
|
|
3313
3685
|
: 'hybrid';
|
|
3314
3686
|
const wantedDomain = options.get('domain');
|
|
3687
|
+
const classRecallOptions = {
|
|
3688
|
+
onClassDegraded: writeErr,
|
|
3689
|
+
...(classMatcher === undefined ? {} : { classMatcher }),
|
|
3690
|
+
};
|
|
3315
3691
|
// OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
|
|
3316
3692
|
// truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
|
|
3317
3693
|
// never receive its promised lift — the feature was weakest in exactly the case it
|
|
@@ -3334,8 +3710,8 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3334
3710
|
// second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
|
|
3335
3711
|
// own output is written after it returns.
|
|
3336
3712
|
const result = asJson
|
|
3337
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3338
|
-
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3713
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3714
|
+
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3339
3715
|
if (mode === 'semantic' && result.vectorEngine === 'none') {
|
|
3340
3716
|
// --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
|
|
3341
3717
|
const why = result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb';
|
|
@@ -3369,13 +3745,27 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3369
3745
|
let globalHits = [];
|
|
3370
3746
|
if (readGlobal) {
|
|
3371
3747
|
const g = asJson
|
|
3372
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3373
|
-
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3748
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3749
|
+
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3374
3750
|
globalHits = g.hits;
|
|
3375
3751
|
}
|
|
3752
|
+
const projectHits = boost ? boost.hits : result.hits;
|
|
3753
|
+
// TWO different keys on purpose, and they must not be unified. The cross-store MERGE key is the
|
|
3754
|
+
// lesson TEXT: the same lesson taught into the project store and the global store is one lesson
|
|
3755
|
+
// shown once, labelled `both`, and those two records legitimately differ in timestamp, reward and
|
|
3756
|
+
// domain. The matchedForm LOOKUP key may be narrower. Folding the merge key into
|
|
3757
|
+
// patternIdentityOf (which includes ts) made two `dz teach` calls of identical text stop
|
|
3758
|
+
// collapsing — it reddened the pre-existing P3 case in recall-reads-both-stores.test.ts.
|
|
3759
|
+
const mergeKey = (hit) => hit.pattern.pattern;
|
|
3760
|
+
const globalByIdentity = new Map(globalHits.map((hit) => [mergeKey(hit), hit]));
|
|
3376
3761
|
const merged = readGlobal
|
|
3377
|
-
? mergeStoreHits(
|
|
3378
|
-
|
|
3762
|
+
? mergeStoreHits(projectHits, globalHits, mergeKey).map((hit) => {
|
|
3763
|
+
if (hit.origin !== 'both')
|
|
3764
|
+
return hit;
|
|
3765
|
+
const matchedForm = mergeLessonMatchedForms(hit.matchedForm, globalByIdentity.get(mergeKey(hit))?.matchedForm);
|
|
3766
|
+
return matchedForm === undefined ? hit : { ...hit, matchedForm };
|
|
3767
|
+
})
|
|
3768
|
+
: projectHits;
|
|
3379
3769
|
const hits = merged.slice(0, limit);
|
|
3380
3770
|
// Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
|
|
3381
3771
|
// paths that return earlier — `--json` and the zero-hits branch — still reported success. That
|
|
@@ -3422,6 +3812,7 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3422
3812
|
// version emitted the number while its own comment promised null (found by independent review).
|
|
3423
3813
|
write(JSON.stringify(hits.map((h) => ({
|
|
3424
3814
|
...h.pattern,
|
|
3815
|
+
...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
|
|
3425
3816
|
relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
|
|
3426
3817
|
// The TRUE cosine, as a second companion key — the design named it and the first ship missed
|
|
3427
3818
|
// it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
|
|
@@ -3488,6 +3879,10 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3488
3879
|
// guessing which second one answered.
|
|
3489
3880
|
write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
|
|
3490
3881
|
}
|
|
3882
|
+
const classMatches = hits.filter((h) => h.matchedForm === 'class' || h.matchedForm === 'both').length;
|
|
3883
|
+
if (hits.some((h) => h.pattern.classForm !== undefined)) {
|
|
3884
|
+
write(` class-form matches: ${classMatches} of ${hits.length}`);
|
|
3885
|
+
}
|
|
3491
3886
|
let sawQuarantined = false;
|
|
3492
3887
|
for (const h of hits) {
|
|
3493
3888
|
const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
|
|
@@ -3513,9 +3908,23 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3513
3908
|
// 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
|
|
3514
3909
|
// hidden remainder, and the cosine then appears to describe the visible fragment rather than
|
|
3515
3910
|
// the whole lesson. `--full` prints it all, still on one line.
|
|
3516
|
-
const
|
|
3517
|
-
const
|
|
3518
|
-
|
|
3911
|
+
const specificText = oneLine(h.pattern.pattern);
|
|
3912
|
+
const classText = h.pattern.classForm === undefined ? undefined : oneLine(h.pattern.classForm);
|
|
3913
|
+
let shown = specificText;
|
|
3914
|
+
if (classText !== undefined) {
|
|
3915
|
+
const suffix = ` [match: ${h.matchedForm ?? 'specific'}]`;
|
|
3916
|
+
if (flags.has('full')) {
|
|
3917
|
+
shown = `specific: ${specificText} · class: ${classText}${suffix}`;
|
|
3918
|
+
}
|
|
3919
|
+
else {
|
|
3920
|
+
const contentWidth = Math.max(2, 160 - 'specific: '.length - ' · class: '.length - suffix.length);
|
|
3921
|
+
const specificWidth = Math.floor(contentWidth / 2);
|
|
3922
|
+
shown = `specific: ${specificText.slice(0, specificWidth)} · class: ${classText.slice(0, contentWidth - specificWidth)}${suffix}`;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
else if (!flags.has('full')) {
|
|
3926
|
+
shown = shown.slice(0, 160);
|
|
3927
|
+
}
|
|
3519
3928
|
// WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
|
|
3520
3929
|
// blindness the store-location line just removed, one level down: the reader would see more
|
|
3521
3930
|
// results and have no way to tell whether the global store is even connected.
|
|
@@ -4489,6 +4898,28 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4489
4898
|
writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
|
|
4490
4899
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4491
4900
|
const presetName = options.get('preset');
|
|
4901
|
+
// PREFLIGHT BEFORE THE FIRST WRITE (backlog 9d15b9b6, PR-A). Step 3 configures the learning
|
|
4902
|
+
// environment and step 4 installs skills, so refusing at step 4 would leave a project that has
|
|
4903
|
+
// memory and hooks but not the skills the operator asked for — a half-configured state worse than
|
|
4904
|
+
// either clean outcome. The request is therefore resolved HERE, before the banner's first step.
|
|
4905
|
+
//
|
|
4906
|
+
// Only an EXPLICIT --select is refused. A preset names skills the package itself ships, so a gap
|
|
4907
|
+
// there is our packaging defect, not the operator's typo, and it is reported by the existing
|
|
4908
|
+
// missing-list rather than by refusing the whole run.
|
|
4909
|
+
const setupSelectRaw = options.get('select');
|
|
4910
|
+
if (setupSelectRaw !== undefined) {
|
|
4911
|
+
const requested = setupSelectRaw.split(',').map((x) => x.trim()).filter((x) => x.length > 0);
|
|
4912
|
+
const roots = discoverSkillsDirs(cwd, options.get('skills-dir')).map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
4913
|
+
const resolution = resolveSelection(requested, roots);
|
|
4914
|
+
for (const shadow of resolution.shadowed) {
|
|
4915
|
+
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(', ')}`);
|
|
4916
|
+
}
|
|
4917
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
4918
|
+
if (refusal !== null) {
|
|
4919
|
+
writeErr(refusal);
|
|
4920
|
+
return 1;
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4492
4923
|
write(`\n╔══════════════════════════════════════════════════════╗`);
|
|
4493
4924
|
write(`║ DZ SETUP — Full Environment ║`);
|
|
4494
4925
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
@@ -4524,7 +4955,13 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4524
4955
|
? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
4525
4956
|
: getPreset(preset)?.skills;
|
|
4526
4957
|
const install = select !== undefined && select.length > 0
|
|
4527
|
-
? await installSkills({
|
|
4958
|
+
? await installSkills({
|
|
4959
|
+
target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select,
|
|
4960
|
+
force: flags.has('force'), enrich: flags.has('enrich'), noHooks: flags.has('no-hooks'),
|
|
4961
|
+
noIntegrations: flags.has('no-integrations'),
|
|
4962
|
+
noVerify: flags.has('no-verify'),
|
|
4963
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
4964
|
+
})
|
|
4528
4965
|
: undefined;
|
|
4529
4966
|
if (install) {
|
|
4530
4967
|
write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
|
|
@@ -4534,6 +4971,7 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4534
4971
|
}
|
|
4535
4972
|
// Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
|
|
4536
4973
|
// setup has already run and the summary still prints; only the exit code carries the failure.
|
|
4974
|
+
let setupIntegrationOutcomes = install?.integrations ?? [];
|
|
4537
4975
|
let codexHooksOk = true;
|
|
4538
4976
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
4539
4977
|
write(`║ 5. Delivering codex hooks (live verify)... ║`);
|
|
@@ -4543,6 +4981,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4543
4981
|
write(line);
|
|
4544
4982
|
for (const line of delivery.stderr)
|
|
4545
4983
|
writeErr(line);
|
|
4984
|
+
const hookIndex = setupIntegrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
4985
|
+
if (hookIndex !== -1) {
|
|
4986
|
+
const hook = normalizeCodexHookOutcome(setupIntegrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
|
|
4987
|
+
setupIntegrationOutcomes = setupIntegrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
4988
|
+
}
|
|
4546
4989
|
}
|
|
4547
4990
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
4548
4991
|
write(`║ Setup: ${String(setupResult.completed).padStart(2)} done, ${String(setupResult.skipped).padStart(2)} skipped ║`);
|
|
@@ -4560,6 +5003,9 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4560
5003
|
if (install) {
|
|
4561
5004
|
write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
|
|
4562
5005
|
+ (install.dirsSearched > 1 ? ` (searched ${install.dirsSearched} skill dirs)` : ''));
|
|
5006
|
+
for (const outcome of setupIntegrationOutcomes) {
|
|
5007
|
+
write(`dz setup integration ${outcome.component}: ${outcome.status.toUpperCase()}${outcome.reasonCode ? ` (${outcome.reasonCode})` : ''}`);
|
|
5008
|
+
}
|
|
4563
5009
|
writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
|
|
4564
5010
|
}
|
|
4565
5011
|
// A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
|
|
@@ -4574,7 +5020,8 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4574
5020
|
if (erroredSteps.length > 0) {
|
|
4575
5021
|
write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
|
|
4576
5022
|
}
|
|
4577
|
-
|
|
5023
|
+
const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
|
|
5024
|
+
return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
|
|
4578
5025
|
}
|
|
4579
5026
|
function cmdPretrain(options, cwd, write) {
|
|
4580
5027
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
@@ -4614,7 +5061,7 @@ function cmdPretrain(options, cwd, write) {
|
|
|
4614
5061
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
4615
5062
|
return 0;
|
|
4616
5063
|
}
|
|
4617
|
-
function cmdRecommend(options, cwd, write) {
|
|
5064
|
+
function cmdRecommend(options, flags, cwd, write) {
|
|
4618
5065
|
const task = options.get('_positional_0');
|
|
4619
5066
|
if (!task) {
|
|
4620
5067
|
write('dz recommend: task description required');
|
|
@@ -4623,15 +5070,40 @@ function cmdRecommend(options, cwd, write) {
|
|
|
4623
5070
|
}
|
|
4624
5071
|
const registry = buildRegistry(cwd);
|
|
4625
5072
|
const report = recommend(task, registry, cwd);
|
|
5073
|
+
if (flags.has('json')) {
|
|
5074
|
+
write(JSON.stringify(report, null, 2));
|
|
5075
|
+
return 0;
|
|
5076
|
+
}
|
|
4626
5077
|
write(`\n╔══════════════════════════════════════════════════════════════╗`);
|
|
4627
5078
|
write(`║ DZ RECOMMEND — Task Advisor ║`);
|
|
4628
5079
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
4629
5080
|
write(`║ Task: ${report.task.slice(0, 52).padEnd(52)}║`);
|
|
4630
|
-
|
|
4631
|
-
|
|
5081
|
+
if (report.topicSource === 'task') {
|
|
5082
|
+
write(`║ Topics: ${report.topics.join(', ').slice(0, 50).padEnd(50)}║`);
|
|
5083
|
+
}
|
|
5084
|
+
else if (report.topicSource === 'project-stack') {
|
|
5085
|
+
write(`║ Topics: ${'not matched in the question'.padEnd(50)}║`);
|
|
5086
|
+
}
|
|
5087
|
+
else {
|
|
5088
|
+
write(`║ Topics: ${'not recognized — no recommendations'.padEnd(50)}║`);
|
|
5089
|
+
}
|
|
4632
5090
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5091
|
+
if (report.topicSource === 'project-stack') {
|
|
5092
|
+
write(`⚠ Тема запроса не распознана — подбор ниже сделан по СТЕКУ ПРОЕКТА, не по вашему вопросу.`);
|
|
5093
|
+
write(` (topic not recognized — recommendations reflect the project stack, not the question)`);
|
|
5094
|
+
write(`PROJECT-STACK SUGGESTIONS`);
|
|
5095
|
+
}
|
|
5096
|
+
else if (report.topicSource === 'none') {
|
|
5097
|
+
write(`Тема запроса не распознана; рекомендаций нет.`);
|
|
5098
|
+
write(`Переформулируйте задачу или используйте dz registry search <слово> / /skill-advisor.`);
|
|
5099
|
+
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
5100
|
+
return 0;
|
|
5101
|
+
}
|
|
5102
|
+
const stackDerived = report.topicSource === 'project-stack';
|
|
4633
5103
|
if (report.presets.length > 0) {
|
|
4634
|
-
write(
|
|
5104
|
+
write(stackDerived
|
|
5105
|
+
? `║ PROJECT-STACK PRESETS ║`
|
|
5106
|
+
: `║ RECOMMENDED PRESETS ║`);
|
|
4635
5107
|
for (const p of report.presets) {
|
|
4636
5108
|
const matched = p.matchedSkills.length > 0 ? ` (${p.matchedSkills.slice(0, 3).join(', ')})` : '';
|
|
4637
5109
|
write(`║ ${p.name.padEnd(15)} ${String(p.skills).padStart(2)} skills coverage: ${String(p.coverage).padStart(2)} topics${matched.padEnd(15)}║`);
|
|
@@ -4639,7 +5111,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
4639
5111
|
write(`║${''.padEnd(62)}║`);
|
|
4640
5112
|
}
|
|
4641
5113
|
if (report.skills.length > 0) {
|
|
4642
|
-
write(
|
|
5114
|
+
write(stackDerived
|
|
5115
|
+
? `║ PROJECT-STACK SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`
|
|
5116
|
+
: `║ RECOMMENDED SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`);
|
|
4643
5117
|
for (const s of report.skills.slice(0, 8)) {
|
|
4644
5118
|
const desc = s.description.length > 35 ? s.description.slice(0, 32) + '...' : s.description;
|
|
4645
5119
|
write(`║ ${s.id.padEnd(24)} ${desc.padEnd(36)}║`);
|
|
@@ -4647,7 +5121,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
4647
5121
|
write(`║${''.padEnd(62)}║`);
|
|
4648
5122
|
}
|
|
4649
5123
|
if (report.toolkits.length > 0) {
|
|
4650
|
-
write(
|
|
5124
|
+
write(stackDerived
|
|
5125
|
+
? `║ PROJECT-STACK PIPELINE (npx toolkits) ║`
|
|
5126
|
+
: `║ FULL PIPELINE (npx toolkits) ║`);
|
|
4651
5127
|
for (const tk of report.toolkits) {
|
|
4652
5128
|
const desc = tk.description.length > 44 ? tk.description.slice(0, 41) + '...' : tk.description;
|
|
4653
5129
|
write(`║ ${tk.name.padEnd(16)} ${desc.padEnd(44)}║`);
|
|
@@ -4657,13 +5133,17 @@ function cmdRecommend(options, cwd, write) {
|
|
|
4657
5133
|
write(`║${''.padEnd(62)}║`);
|
|
4658
5134
|
}
|
|
4659
5135
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
4660
|
-
write(
|
|
5136
|
+
write(stackDerived
|
|
5137
|
+
? `║ PROJECT-STACK PLAN ║`
|
|
5138
|
+
: `║ STEP-BY-STEP PLAN ║`);
|
|
4661
5139
|
for (const step of report.plan) {
|
|
4662
5140
|
const line = step.length > 60 ? step.slice(0, 57) + '...' : step;
|
|
4663
5141
|
write(`║ ${line.padEnd(58)}║`);
|
|
4664
5142
|
}
|
|
4665
5143
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
4666
|
-
write(
|
|
5144
|
+
write(stackDerived
|
|
5145
|
+
? `║ PROJECT-STACK QUICK INSTALL ║`
|
|
5146
|
+
: `║ QUICK INSTALL ║`);
|
|
4667
5147
|
const cmd = report.installCommand.length > 58 ? report.installCommand.slice(0, 55) + '...' : report.installCommand;
|
|
4668
5148
|
write(`║ ${cmd.padEnd(58)}║`);
|
|
4669
5149
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
@@ -5228,6 +5708,9 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5228
5708
|
write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
|
|
5229
5709
|
else
|
|
5230
5710
|
write('dz publish: ✓ guard pre-flight passed');
|
|
5711
|
+
for (const observation of guardResult.observations ?? []) {
|
|
5712
|
+
write(`dz publish: ℹ guard observation — ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`);
|
|
5713
|
+
}
|
|
5231
5714
|
for (const n of guardResult.notes ?? [])
|
|
5232
5715
|
write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
|
|
5233
5716
|
}
|
|
@@ -6421,6 +6904,81 @@ function looksBinaryText(text) {
|
|
|
6421
6904
|
* per-file findings (each enriched with its `file`), and applies the exit-code contract.
|
|
6422
6905
|
* `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
|
|
6423
6906
|
*/
|
|
6907
|
+
/**
|
|
6908
|
+
* `dz chain` — verify EVERY hash-chained journal in one command (W0-chain, backlog bc4ee35c).
|
|
6909
|
+
*
|
|
6910
|
+
* The machinery to verify a chain has worked for weeks. What was missing is the ABILITY TO ASK:
|
|
6911
|
+
* verification lived inside two consumers, each carrying its own hardcoded list of which files are
|
|
6912
|
+
* chained, so a journal could be given a chain and still be checked by nobody. Coverage here is
|
|
6913
|
+
* DERIVED from CHAINED_JOURNALS, never typed — adding a journal to the registry adds it to this
|
|
6914
|
+
* report by construction.
|
|
6915
|
+
*
|
|
6916
|
+
* An ABSENT journal is reported as `absent`, not omitted. Omission and cleanliness are
|
|
6917
|
+
* indistinguishable in a report, and that indistinguishability is how the original blind spot
|
|
6918
|
+
* survived; the same reason `broken` exits NON-ZERO rather than merely printing — a verifier that
|
|
6919
|
+
* reports damage and exits 0 is one no automation can act on, and this verb exists to run unattended.
|
|
6920
|
+
*
|
|
6921
|
+
* A journal that exists but carries NO chained records is `unchained`, which is legal (a log may
|
|
6922
|
+
* predate the chain) and therefore does not fail the command. Calling it a defect would train the
|
|
6923
|
+
* reader to ignore the output — the failure mode already measured once on the doctor's own line.
|
|
6924
|
+
*/
|
|
6925
|
+
function cmdChain(options, flags, cwd, write) {
|
|
6926
|
+
const root = options.get('project') ?? cwd;
|
|
6927
|
+
const journals = CHAINED_JOURNALS.map((journal) => {
|
|
6928
|
+
const path = join(root, journal.rel);
|
|
6929
|
+
if (!existsSync(path)) {
|
|
6930
|
+
return { rel: journal.rel, decides: journal.decides, status: 'absent', chained: 0, defects: 0, detail: 'file not present' };
|
|
6931
|
+
}
|
|
6932
|
+
let text = '';
|
|
6933
|
+
try {
|
|
6934
|
+
text = readFileSync(path, 'utf-8');
|
|
6935
|
+
}
|
|
6936
|
+
catch {
|
|
6937
|
+
// Unreadable is NOT clean. It is the one outcome that must never be quietly folded into
|
|
6938
|
+
// "nothing to report": we did not look, so we know nothing.
|
|
6939
|
+
return { rel: journal.rel, decides: journal.decides, status: 'unreadable', chained: 0, defects: 0, detail: 'file could not be read' };
|
|
6940
|
+
}
|
|
6941
|
+
const v = verifyEventChainText(text);
|
|
6942
|
+
if (v.chained === 0) {
|
|
6943
|
+
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)' };
|
|
6944
|
+
}
|
|
6945
|
+
const total = text.split('\n').filter((l) => l.trim() !== '').length;
|
|
6946
|
+
const age = classifyChainDefects(v, total);
|
|
6947
|
+
if (v.ok) {
|
|
6948
|
+
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)` };
|
|
6949
|
+
}
|
|
6950
|
+
// A break the current unbroken run has already outlived does not make TODAY's records unsound.
|
|
6951
|
+
// Reporting both alike is what made the doctor's equivalent line permanently red for four weeks.
|
|
6952
|
+
const historical = age.inRun.length === 0 && age.runRecords > 0;
|
|
6953
|
+
return {
|
|
6954
|
+
rel: journal.rel,
|
|
6955
|
+
decides: journal.decides,
|
|
6956
|
+
status: historical ? 'healed' : 'broken',
|
|
6957
|
+
chained: v.chained,
|
|
6958
|
+
defects: v.defects.length,
|
|
6959
|
+
detail: historical
|
|
6960
|
+
? `${v.defects.length} defect(s), all BEFORE the current run — the last ${age.runRecords} record(s) are unbroken, so verdicts over those are sound`
|
|
6961
|
+
: `${v.defects.length} defect(s) with NO sound records after them: verdicts computed from this log are unsafe`,
|
|
6962
|
+
};
|
|
6963
|
+
});
|
|
6964
|
+
const failed = journals.filter((j) => j.status === 'broken' || j.status === 'unreadable');
|
|
6965
|
+
const ok = failed.length === 0;
|
|
6966
|
+
if (flags.has('json')) {
|
|
6967
|
+
write(JSON.stringify({ ok, root, journals }, null, 2));
|
|
6968
|
+
return ok ? 0 : 1;
|
|
6969
|
+
}
|
|
6970
|
+
write(`dz chain — ${journals.length} registered journal(s) under ${root}`);
|
|
6971
|
+
write('');
|
|
6972
|
+
const MARK = { ok: '\u2713', healed: '\u2713', unchained: '\u00b7', absent: '\u00b7', broken: '\u2717', unreadable: '\u2717' };
|
|
6973
|
+
for (const j of journals) {
|
|
6974
|
+
write(` ${MARK[j.status] ?? '?'} ${j.rel} — ${j.status}`);
|
|
6975
|
+
write(` ${j.detail}`);
|
|
6976
|
+
write(` decides: ${j.decides}`);
|
|
6977
|
+
}
|
|
6978
|
+
write('');
|
|
6979
|
+
write(ok ? ' all registered journals are sound for present verdicts' : ` ${failed.length} journal(s) UNSAFE — see above`);
|
|
6980
|
+
return ok ? 0 : 1;
|
|
6981
|
+
}
|
|
6424
6982
|
function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
|
|
6425
6983
|
// Reject (never silently coerce) an invalid --fail-on.
|
|
6426
6984
|
const failOnRaw = options.get('fail-on') ?? 'high';
|
|
@@ -6904,6 +7462,26 @@ export function codexHooksSyncOptions(input) {
|
|
|
6904
7462
|
verify: input.verify !== false,
|
|
6905
7463
|
};
|
|
6906
7464
|
}
|
|
7465
|
+
/** Map the retained Codex hook writer's one live verdict into the common integration contract. */
|
|
7466
|
+
export function normalizeCodexHookOutcome(base, delivery, noVerify = false) {
|
|
7467
|
+
if (delivery.report.ready) {
|
|
7468
|
+
return {
|
|
7469
|
+
target: 'codex', component: 'hooks', status: 'emitted',
|
|
7470
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: true }],
|
|
7471
|
+
carrier: { scope: 'user', path: '$CODEX_HOME/hooks.json' },
|
|
7472
|
+
...(delivery.report.codexVersion !== null ? { runtimeVersion: delivery.report.codexVersion } : {}),
|
|
7473
|
+
evidenceVersion: 'codex-hooks-live-v1',
|
|
7474
|
+
};
|
|
7475
|
+
}
|
|
7476
|
+
return {
|
|
7477
|
+
...base,
|
|
7478
|
+
target: 'codex', component: 'hooks', status: 'refused',
|
|
7479
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: false }],
|
|
7480
|
+
reasonCode: 'CURRENT_LIVE_CHECK_FAILED',
|
|
7481
|
+
remediation: noVerify ? '--no-verify cannot establish ready; rerun with live verification' : 'approve the managed hooks and rerun the live verification',
|
|
7482
|
+
applied: delivery.report.written || delivery.report.installed,
|
|
7483
|
+
};
|
|
7484
|
+
}
|
|
6907
7485
|
/**
|
|
6908
7486
|
* What the user is told about a sync report — the ONE place the success word can be printed.
|
|
6909
7487
|
*
|
|
@@ -7221,6 +7799,430 @@ function gatherReadmeCounts(root) {
|
|
|
7221
7799
|
sitePair('packages/@dzhechkov/sitedoc/src/content/docs/index.mdx', /^\s*tagline: "[^"]*?(\d+) commands/m, 'commands (docs-site index hero tagline vs cli All Commands)');
|
|
7222
7800
|
return pairs;
|
|
7223
7801
|
}
|
|
7802
|
+
const MAX_VOLUME_FILES_PER_SCOPE = 512;
|
|
7803
|
+
const MAX_VOLUME_BYTES_PER_SCOPE = 32 * 1024 * 1024;
|
|
7804
|
+
const CYRILLIC_CHARACTER = /\p{Script=Cyrillic}/u;
|
|
7805
|
+
function volumePathInside(root, candidate) {
|
|
7806
|
+
return candidate === root || candidate.startsWith(root + sep);
|
|
7807
|
+
}
|
|
7808
|
+
function cyrillicUtf8Bytes(buffer) {
|
|
7809
|
+
let bytes = 0;
|
|
7810
|
+
for (const character of buffer.toString('utf8')) {
|
|
7811
|
+
if (CYRILLIC_CHARACTER.test(character))
|
|
7812
|
+
bytes += Buffer.byteLength(character, 'utf8');
|
|
7813
|
+
}
|
|
7814
|
+
return bytes;
|
|
7815
|
+
}
|
|
7816
|
+
function gatherTemplateVolumeTarget(packageDir, target) {
|
|
7817
|
+
const templateRoot = join(packageDir, 'templates', '.claude');
|
|
7818
|
+
if (!existsSync(templateRoot))
|
|
7819
|
+
return undefined;
|
|
7820
|
+
const files = [];
|
|
7821
|
+
const realFiles = new Set();
|
|
7822
|
+
let totalBytes = 0;
|
|
7823
|
+
let failure;
|
|
7824
|
+
let packageReal;
|
|
7825
|
+
let templateReal;
|
|
7826
|
+
try {
|
|
7827
|
+
packageReal = realpathSync(packageDir);
|
|
7828
|
+
templateReal = realpathSync(templateRoot);
|
|
7829
|
+
if (!volumePathInside(packageReal, templateReal)) {
|
|
7830
|
+
failure = { reason: 'template-root-escape', detail: templateRoot };
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
catch (error) {
|
|
7834
|
+
return {
|
|
7835
|
+
target,
|
|
7836
|
+
files,
|
|
7837
|
+
collection: { complete: false, reason: 'template-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
7838
|
+
};
|
|
7839
|
+
}
|
|
7840
|
+
const fail = (reason, detail) => {
|
|
7841
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
7842
|
+
};
|
|
7843
|
+
const addFile = (absolute, kind) => {
|
|
7844
|
+
if (failure !== undefined)
|
|
7845
|
+
return;
|
|
7846
|
+
if (files.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
7847
|
+
fail('template-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
try {
|
|
7851
|
+
const stat = lstatSync(absolute);
|
|
7852
|
+
if (!stat.isFile()) {
|
|
7853
|
+
fail('template-input-not-regular', absolute);
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
const real = realpathSync(absolute);
|
|
7857
|
+
if (!volumePathInside(templateReal, real)) {
|
|
7858
|
+
fail('template-input-escape', absolute);
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7861
|
+
if (realFiles.has(real)) {
|
|
7862
|
+
fail('template-input-duplicate', absolute);
|
|
7863
|
+
return;
|
|
7864
|
+
}
|
|
7865
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7866
|
+
fail(stat.size <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
const content = readFileSync(absolute);
|
|
7870
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7871
|
+
fail(content.byteLength <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
7872
|
+
return;
|
|
7873
|
+
}
|
|
7874
|
+
realFiles.add(real);
|
|
7875
|
+
totalBytes += content.byteLength;
|
|
7876
|
+
files.push({
|
|
7877
|
+
path: relative(packageDir, absolute).split(sep).join('/'),
|
|
7878
|
+
kind,
|
|
7879
|
+
bytes: content.byteLength,
|
|
7880
|
+
cyrillicUtf8Bytes: cyrillicUtf8Bytes(content),
|
|
7881
|
+
});
|
|
7882
|
+
}
|
|
7883
|
+
catch (error) {
|
|
7884
|
+
fail('template-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7885
|
+
}
|
|
7886
|
+
};
|
|
7887
|
+
const scanFlat = (name) => {
|
|
7888
|
+
const dir = join(templateRoot, name);
|
|
7889
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
7890
|
+
return;
|
|
7891
|
+
try {
|
|
7892
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7893
|
+
if (!entry.name.endsWith('.md'))
|
|
7894
|
+
continue;
|
|
7895
|
+
addFile(join(dir, entry.name), name);
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
catch (error) {
|
|
7899
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7900
|
+
}
|
|
7901
|
+
};
|
|
7902
|
+
const scanSkills = (dir) => {
|
|
7903
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
7904
|
+
return;
|
|
7905
|
+
let entries;
|
|
7906
|
+
try {
|
|
7907
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
7908
|
+
}
|
|
7909
|
+
catch (error) {
|
|
7910
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7911
|
+
return;
|
|
7912
|
+
}
|
|
7913
|
+
for (const entry of entries) {
|
|
7914
|
+
if (failure !== undefined)
|
|
7915
|
+
return;
|
|
7916
|
+
const absolute = join(dir, entry.name);
|
|
7917
|
+
if (entry.isSymbolicLink()) {
|
|
7918
|
+
fail('template-input-not-regular', absolute);
|
|
7919
|
+
return;
|
|
7920
|
+
}
|
|
7921
|
+
if (entry.isDirectory())
|
|
7922
|
+
scanSkills(absolute);
|
|
7923
|
+
else if (entry.name === 'SKILL.md')
|
|
7924
|
+
addFile(absolute, 'skills');
|
|
7925
|
+
}
|
|
7926
|
+
};
|
|
7927
|
+
scanFlat('rules');
|
|
7928
|
+
scanFlat('commands');
|
|
7929
|
+
scanSkills(join(templateRoot, 'skills'));
|
|
7930
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
7931
|
+
return {
|
|
7932
|
+
target,
|
|
7933
|
+
files,
|
|
7934
|
+
collection: failure === undefined
|
|
7935
|
+
? { complete: true }
|
|
7936
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
7937
|
+
};
|
|
7938
|
+
}
|
|
7939
|
+
function volumeGitText(root, args, allowedStatuses = [0]) {
|
|
7940
|
+
const result = spawnSync('git', [...args], {
|
|
7941
|
+
cwd: root,
|
|
7942
|
+
encoding: 'utf8',
|
|
7943
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
7944
|
+
});
|
|
7945
|
+
if (result.status === null || !allowedStatuses.includes(result.status)) {
|
|
7946
|
+
throw result.error ?? new Error(`git ${args[0] ?? ''} exited ${String(result.status)}`);
|
|
7947
|
+
}
|
|
7948
|
+
return result.stdout ?? '';
|
|
7949
|
+
}
|
|
7950
|
+
function parseGuardStatusPaths(root) {
|
|
7951
|
+
const text = volumeGitText(root, ['status', '--porcelain', '-uall']);
|
|
7952
|
+
return text.split('\n').flatMap((line) => {
|
|
7953
|
+
if (line.length < 4)
|
|
7954
|
+
return [];
|
|
7955
|
+
const code = line.slice(0, 2);
|
|
7956
|
+
const raw = line.slice(3).trim();
|
|
7957
|
+
const path = (raw.includes(' -> ') ? raw.split(' -> ')[1] : raw)?.trim();
|
|
7958
|
+
return path ? [{ code, path }] : [];
|
|
7959
|
+
});
|
|
7960
|
+
}
|
|
7961
|
+
function parseFeatureTier(text) {
|
|
7962
|
+
const match = text.match(/^##\s+Tier:\s*(S|M|L|XL)\s*$/m);
|
|
7963
|
+
return match?.[1];
|
|
7964
|
+
}
|
|
7965
|
+
function parseFeatureActiveSteps(text) {
|
|
7966
|
+
const section = text.match(/^## Active steps[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/mi)?.[1];
|
|
7967
|
+
if (section === undefined)
|
|
7968
|
+
return undefined;
|
|
7969
|
+
const steps = [...section.matchAll(/\b(?:10|[0-9](?:\.5)?)\b/g)].map((match) => match[0]);
|
|
7970
|
+
return [...new Set(steps)];
|
|
7971
|
+
}
|
|
7972
|
+
function featureLifecycle(root, slug, assessment) {
|
|
7973
|
+
if (assessment !== undefined && /^Lifecycle:\s*complete\s*$/mi.test(assessment))
|
|
7974
|
+
return { phase: 'complete' };
|
|
7975
|
+
try {
|
|
7976
|
+
const state = JSON.parse(readFileSync(join(root, '.dz', 'feature-adr', 'learning-state', `${slug}.json`), 'utf8'));
|
|
7977
|
+
const match = typeof state.step === 'string' ? state.step.match(/\bStep\s+(\d+(?:\.5)?)/i) : null;
|
|
7978
|
+
if (!match?.[1])
|
|
7979
|
+
return undefined;
|
|
7980
|
+
const current = Number(match[1]);
|
|
7981
|
+
if (!Number.isFinite(current))
|
|
7982
|
+
return undefined;
|
|
7983
|
+
return { phase: 'in-progress', completedThroughStep: current <= 0 ? 0 : Math.ceil(current) - 1 };
|
|
7984
|
+
}
|
|
7985
|
+
catch {
|
|
7986
|
+
return undefined;
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
function gatherFeatureVolumeFact(root, slug) {
|
|
7990
|
+
const featureDir = join(root, 'features', slug);
|
|
7991
|
+
const artifacts = [];
|
|
7992
|
+
const contents = new Map();
|
|
7993
|
+
let totalBytes = 0;
|
|
7994
|
+
let failure;
|
|
7995
|
+
let rootReal;
|
|
7996
|
+
let featureReal;
|
|
7997
|
+
try {
|
|
7998
|
+
rootReal = realpathSync(root);
|
|
7999
|
+
featureReal = realpathSync(featureDir);
|
|
8000
|
+
if (!volumePathInside(rootReal, featureReal))
|
|
8001
|
+
failure = { reason: 'feature-root-escape', detail: featureDir };
|
|
8002
|
+
}
|
|
8003
|
+
catch (error) {
|
|
8004
|
+
return { fact: {
|
|
8005
|
+
slug,
|
|
8006
|
+
artifacts,
|
|
8007
|
+
collection: { complete: false, reason: 'feature-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
8008
|
+
} };
|
|
8009
|
+
}
|
|
8010
|
+
const fail = (reason, detail) => {
|
|
8011
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
8012
|
+
};
|
|
8013
|
+
const addArtifact = (absolute) => {
|
|
8014
|
+
if (failure !== undefined)
|
|
8015
|
+
return;
|
|
8016
|
+
if (artifacts.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
8017
|
+
fail('feature-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
8018
|
+
return;
|
|
8019
|
+
}
|
|
8020
|
+
try {
|
|
8021
|
+
const stat = lstatSync(absolute);
|
|
8022
|
+
if (!stat.isFile()) {
|
|
8023
|
+
fail('feature-input-not-regular', absolute);
|
|
8024
|
+
return;
|
|
8025
|
+
}
|
|
8026
|
+
const real = realpathSync(absolute);
|
|
8027
|
+
if (!volumePathInside(featureReal, real)) {
|
|
8028
|
+
fail('feature-input-escape', absolute);
|
|
8029
|
+
return;
|
|
8030
|
+
}
|
|
8031
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8032
|
+
fail(stat.size <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
8033
|
+
return;
|
|
8034
|
+
}
|
|
8035
|
+
const content = readFileSync(absolute);
|
|
8036
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8037
|
+
fail(content.byteLength <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
8038
|
+
return;
|
|
8039
|
+
}
|
|
8040
|
+
const path = relative(featureDir, absolute).split(sep).join('/');
|
|
8041
|
+
totalBytes += content.byteLength;
|
|
8042
|
+
contents.set(path, content);
|
|
8043
|
+
artifacts.push({ path, bytes: content.byteLength });
|
|
8044
|
+
}
|
|
8045
|
+
catch (error) {
|
|
8046
|
+
fail('feature-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8047
|
+
}
|
|
8048
|
+
};
|
|
8049
|
+
const scanNested = (dir) => {
|
|
8050
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
8051
|
+
return;
|
|
8052
|
+
let entries;
|
|
8053
|
+
try {
|
|
8054
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
8055
|
+
}
|
|
8056
|
+
catch (error) {
|
|
8057
|
+
fail('feature-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8058
|
+
return;
|
|
8059
|
+
}
|
|
8060
|
+
for (const entry of entries) {
|
|
8061
|
+
if (failure !== undefined)
|
|
8062
|
+
return;
|
|
8063
|
+
const absolute = join(dir, entry.name);
|
|
8064
|
+
if (entry.isSymbolicLink()) {
|
|
8065
|
+
fail('feature-input-not-regular', absolute);
|
|
8066
|
+
return;
|
|
8067
|
+
}
|
|
8068
|
+
if (entry.isDirectory())
|
|
8069
|
+
scanNested(absolute);
|
|
8070
|
+
else
|
|
8071
|
+
addArtifact(absolute);
|
|
8072
|
+
}
|
|
8073
|
+
};
|
|
8074
|
+
try {
|
|
8075
|
+
const entries = readdirSync(featureDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
8076
|
+
for (const entry of entries) {
|
|
8077
|
+
if (failure !== undefined)
|
|
8078
|
+
break;
|
|
8079
|
+
const absolute = join(featureDir, entry.name);
|
|
8080
|
+
if (entry.name === '03_adr' || entry.name === '07_code_changes') {
|
|
8081
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
8082
|
+
fail('feature-input-not-regular', absolute);
|
|
8083
|
+
else
|
|
8084
|
+
scanNested(absolute);
|
|
8085
|
+
}
|
|
8086
|
+
else if (entry.name === 'README.md' || /^0[0-9](?:[_.-][^/]*)?\.md$/.test(entry.name)) {
|
|
8087
|
+
addArtifact(absolute);
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
catch (error) {
|
|
8092
|
+
fail('feature-directory-unreadable', `${featureDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8093
|
+
}
|
|
8094
|
+
artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
8095
|
+
const assessment = contents.get('00_complexity_assessment.md')?.toString('utf8');
|
|
8096
|
+
const tier = assessment !== undefined ? parseFeatureTier(assessment) : undefined;
|
|
8097
|
+
const activeSteps = assessment !== undefined ? parseFeatureActiveSteps(assessment) : undefined;
|
|
8098
|
+
const lifecycle = featureLifecycle(root, slug, assessment);
|
|
8099
|
+
const fact = {
|
|
8100
|
+
slug,
|
|
8101
|
+
...(tier !== undefined ? { tier } : {}),
|
|
8102
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
8103
|
+
namedConsumers: [],
|
|
8104
|
+
...(lifecycle !== undefined ? { lifecycle } : {}),
|
|
8105
|
+
artifacts,
|
|
8106
|
+
collection: failure === undefined
|
|
8107
|
+
? { complete: true }
|
|
8108
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
8109
|
+
};
|
|
8110
|
+
const manifestText = contents.get('07_code_changes/change_manifest.md')?.toString('utf8');
|
|
8111
|
+
return { fact, ...(manifestText !== undefined ? { manifestText } : {}) };
|
|
8112
|
+
}
|
|
8113
|
+
function manifestPaths(text, slug) {
|
|
8114
|
+
const paths = new Set();
|
|
8115
|
+
for (const line of text.split('\n')) {
|
|
8116
|
+
const match = line.match(/^\s*-\s+`?([^`]+?)`?(?:\s+[—-]\s+.*)?$/);
|
|
8117
|
+
if (!match?.[1])
|
|
8118
|
+
continue;
|
|
8119
|
+
const path = match[1].trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
8120
|
+
if (path === '' || path.startsWith('/') || path.split('/').some((part) => part === '..'))
|
|
8121
|
+
continue;
|
|
8122
|
+
if (path.startsWith(`features/${slug}/`))
|
|
8123
|
+
continue;
|
|
8124
|
+
paths.add(path);
|
|
8125
|
+
}
|
|
8126
|
+
return [...paths].sort();
|
|
8127
|
+
}
|
|
8128
|
+
function attributableDiff(root, slug, feature, status, changedSlugs) {
|
|
8129
|
+
if (changedSlugs.length !== 1 || changedSlugs[0] !== slug) {
|
|
8130
|
+
return { attributable: false, reason: changedSlugs.length > 1 ? 'ambiguous-feature-attribution' : 'feature-not-attributable' };
|
|
8131
|
+
}
|
|
8132
|
+
if (feature.manifestText === undefined)
|
|
8133
|
+
return { attributable: false, reason: 'change-manifest-unavailable' };
|
|
8134
|
+
const listed = manifestPaths(feature.manifestText, slug);
|
|
8135
|
+
const changedByPath = new Map(status.map((item) => [item.path, item.code]));
|
|
8136
|
+
const changed = listed.filter((path) => changedByPath.has(path));
|
|
8137
|
+
if (changed.length === 0)
|
|
8138
|
+
return { attributable: true, bytes: 0, base: 'HEAD', head: 'working-tree', method: 'git-unified-diff-bytes/v1', excludedFeaturePath: `features/${slug}/**` };
|
|
8139
|
+
const tracked = [];
|
|
8140
|
+
const untracked = [];
|
|
8141
|
+
for (const path of changed) {
|
|
8142
|
+
const absolute = resolve(root, path);
|
|
8143
|
+
if (!volumePathInside(root, absolute))
|
|
8144
|
+
return { attributable: false, reason: 'manifest-path-escape' };
|
|
8145
|
+
if (changedByPath.get(path) === '??') {
|
|
8146
|
+
try {
|
|
8147
|
+
const stat = lstatSync(absolute);
|
|
8148
|
+
const real = realpathSync(absolute);
|
|
8149
|
+
if (!stat.isFile() || !volumePathInside(root, real) || stat.size > MAX_VOLUME_BYTES_PER_SCOPE) {
|
|
8150
|
+
return { attributable: false, reason: 'untracked-diff-input-refused' };
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
catch {
|
|
8154
|
+
return { attributable: false, reason: 'untracked-diff-input-unreadable' };
|
|
8155
|
+
}
|
|
8156
|
+
untracked.push(path);
|
|
8157
|
+
}
|
|
8158
|
+
else {
|
|
8159
|
+
tracked.push(path);
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
let bytes = 0;
|
|
8163
|
+
try {
|
|
8164
|
+
if (tracked.length > 0) {
|
|
8165
|
+
const diff = volumeGitText(root, ['diff', '--binary', '--no-ext-diff', 'HEAD', '--', ...tracked]);
|
|
8166
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
8167
|
+
}
|
|
8168
|
+
for (const path of untracked) {
|
|
8169
|
+
const diff = volumeGitText(root, ['diff', '--no-index', '--binary', '--', '/dev/null', resolve(root, path)], [0, 1]);
|
|
8170
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
8171
|
+
if (bytes > MAX_VOLUME_BYTES_PER_SCOPE)
|
|
8172
|
+
return { attributable: false, reason: 'diff-byte-cap-exceeded' };
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
catch {
|
|
8176
|
+
return { attributable: false, reason: 'git-diff-failed' };
|
|
8177
|
+
}
|
|
8178
|
+
return {
|
|
8179
|
+
attributable: true,
|
|
8180
|
+
bytes,
|
|
8181
|
+
base: 'HEAD',
|
|
8182
|
+
head: 'working-tree',
|
|
8183
|
+
method: 'git-unified-diff-bytes/v1',
|
|
8184
|
+
excludedFeaturePath: `features/${slug}/**`,
|
|
8185
|
+
};
|
|
8186
|
+
}
|
|
8187
|
+
function gatherVolumeShadowFacts(root, packages) {
|
|
8188
|
+
const templates = packages
|
|
8189
|
+
.filter((item) => !item.privateFlag)
|
|
8190
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.dir.localeCompare(b.dir))
|
|
8191
|
+
.flatMap((item) => {
|
|
8192
|
+
const fact = gatherTemplateVolumeTarget(join(root, item.dir), item.name);
|
|
8193
|
+
return fact === undefined ? [] : [fact];
|
|
8194
|
+
});
|
|
8195
|
+
let status;
|
|
8196
|
+
try {
|
|
8197
|
+
status = parseGuardStatusPaths(root);
|
|
8198
|
+
}
|
|
8199
|
+
catch {
|
|
8200
|
+
return templates.length > 0 ? { templates } : {};
|
|
8201
|
+
}
|
|
8202
|
+
const slugs = [...new Set(status.flatMap((item) => {
|
|
8203
|
+
const match = item.path.match(/^features\/([^/]+)\//);
|
|
8204
|
+
return match?.[1] ? [match[1]] : [];
|
|
8205
|
+
}))].sort();
|
|
8206
|
+
const gathered = slugs.slice(0, 32).map((slug) => gatherFeatureVolumeFact(root, slug));
|
|
8207
|
+
const measuredFeatures = gathered.map((feature) => ({
|
|
8208
|
+
...feature.fact,
|
|
8209
|
+
diff: attributableDiff(root, feature.fact.slug, feature, status, slugs),
|
|
8210
|
+
}));
|
|
8211
|
+
const cappedFeatures = slugs.slice(32).map((slug) => ({
|
|
8212
|
+
slug,
|
|
8213
|
+
artifacts: [],
|
|
8214
|
+
collection: {
|
|
8215
|
+
complete: false,
|
|
8216
|
+
reason: 'feature-scope-cap-exceeded',
|
|
8217
|
+
detail: 'only the first 32 lexicographically sorted changed feature scopes were traversed',
|
|
8218
|
+
},
|
|
8219
|
+
}));
|
|
8220
|
+
const features = [...measuredFeatures, ...cappedFeatures];
|
|
8221
|
+
return {
|
|
8222
|
+
...(templates.length > 0 ? { templates } : {}),
|
|
8223
|
+
...(features.length > 0 ? { features } : {}),
|
|
8224
|
+
};
|
|
8225
|
+
}
|
|
7224
8226
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
7225
8227
|
function gatherGuardFacts(op, root, text, storeCap) {
|
|
7226
8228
|
const facts = { op };
|
|
@@ -7355,7 +8357,7 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
7355
8357
|
const manifests = [];
|
|
7356
8358
|
const located = [];
|
|
7357
8359
|
try {
|
|
7358
|
-
const out =
|
|
8360
|
+
const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
|
|
7359
8361
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
7360
8362
|
try {
|
|
7361
8363
|
const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
|
|
@@ -7384,6 +8386,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
7384
8386
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
7385
8387
|
}
|
|
7386
8388
|
facts['packages'] = packages;
|
|
8389
|
+
facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
|
|
8390
|
+
dir,
|
|
8391
|
+
name: m.name ?? dir,
|
|
8392
|
+
privateFlag: m.private === true,
|
|
8393
|
+
})));
|
|
7387
8394
|
// licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
|
|
7388
8395
|
// `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
|
|
7389
8396
|
// LICENSE reads as absent (null), which the checker treats as a violation for a publishable
|
|
@@ -7661,6 +8668,10 @@ function runGuardEvaluation(root, op, text, overrideReason) {
|
|
|
7661
8668
|
catch { /* audit is best-effort, never blocks the verdict */ }
|
|
7662
8669
|
return result;
|
|
7663
8670
|
}
|
|
8671
|
+
function renderGuardObservation(observation) {
|
|
8672
|
+
const tag = observation.status === 'unknown' ? 'note' : 'observe';
|
|
8673
|
+
return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
|
|
8674
|
+
}
|
|
7664
8675
|
/**
|
|
7665
8676
|
* The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
|
|
7666
8677
|
* lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
|
|
@@ -8035,8 +9046,9 @@ function cmdGuardPromote(options, flags, root, write) {
|
|
|
8035
9046
|
...next,
|
|
8036
9047
|
entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
|
|
8037
9048
|
};
|
|
9049
|
+
const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
|
|
8038
9050
|
try {
|
|
8039
|
-
writeJsonAtomic(join(root, PROMOTION_STATE_FILE),
|
|
9051
|
+
writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence);
|
|
8040
9052
|
}
|
|
8041
9053
|
catch { /* best-effort */ }
|
|
8042
9054
|
}
|
|
@@ -8145,6 +9157,8 @@ function cmdGuard(options, flags, cwd, write) {
|
|
|
8145
9157
|
}
|
|
8146
9158
|
const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
|
|
8147
9159
|
write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
|
|
9160
|
+
for (const observation of result.observations ?? [])
|
|
9161
|
+
write(renderGuardObservation(observation));
|
|
8148
9162
|
for (const v of result.violations)
|
|
8149
9163
|
write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
|
|
8150
9164
|
for (const n of result.notes ?? [])
|
|
@@ -8765,8 +9779,79 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
8765
9779
|
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
8766
9780
|
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
8767
9781
|
const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
|
|
8768
|
-
|
|
9782
|
+
// Runner honesty (feature instrument-honesty, ADR-001): the runner is selected from the TARGET
|
|
9783
|
+
// package's own scripts.test, never from a global default. The package dir is the nearest
|
|
9784
|
+
// ancestor of the FIRST named test that carries a package.json — walked here, at the seam,
|
|
9785
|
+
// because the pure half deliberately takes the script text as data and never touches the fs.
|
|
9786
|
+
let packageTestScript = null;
|
|
9787
|
+
let packageDevDependencies = [];
|
|
9788
|
+
let packageDir = repoRoot;
|
|
9789
|
+
{
|
|
9790
|
+
const firstTest = propertyTests[0]?.file;
|
|
9791
|
+
// QE-1 (instrument-honesty, HIGH): this walk runs on the RAW --test argument, BEFORE the
|
|
9792
|
+
// engine's sanitation — a `../` traversal made it read an arbitrary package.json OUTSIDE the
|
|
9793
|
+
// repo and echo its scripts.test verbatim into the JSON output (MEASURED with a planted
|
|
9794
|
+
// marker file). Containment first: a start point outside the repo root never gets walked,
|
|
9795
|
+
// the script stays null, and the engine's own path sanitation then refuses the test path.
|
|
9796
|
+
const walkStart = firstTest !== undefined ? resolve(cwd, dirname(firstTest)) : undefined;
|
|
9797
|
+
if (firstTest !== undefined && walkStart !== undefined
|
|
9798
|
+
&& (walkStart === resolve(repoRoot) || walkStart.startsWith(resolve(repoRoot) + sep))) {
|
|
9799
|
+
let probe = walkStart;
|
|
9800
|
+
// walk up to the repo root looking for package.json (bounded by the fs root either way)
|
|
9801
|
+
for (;;) {
|
|
9802
|
+
if (existsSync(join(probe, 'package.json'))) {
|
|
9803
|
+
packageDir = probe;
|
|
9804
|
+
break;
|
|
9805
|
+
}
|
|
9806
|
+
const parent = dirname(probe);
|
|
9807
|
+
if (parent === probe || probe === repoRoot)
|
|
9808
|
+
break;
|
|
9809
|
+
probe = parent;
|
|
9810
|
+
}
|
|
9811
|
+
try {
|
|
9812
|
+
const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'));
|
|
9813
|
+
packageTestScript = typeof pkg.scripts?.test === 'string' ? pkg.scripts.test : null;
|
|
9814
|
+
packageDevDependencies = Object.keys(pkg.devDependencies ?? {});
|
|
9815
|
+
}
|
|
9816
|
+
catch { /* unreadable package.json → selection falls through to the honest REFUSE */ }
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
// The pure half's path sanitation expects a REPO-RELATIVE package dir ('.'-rooted), not an
|
|
9820
|
+
// absolute one — an absolute path is refused as unsafe-package-dir by design.
|
|
9821
|
+
const packageDirRel = relative(repoRoot, packageDir) || '.';
|
|
9822
|
+
const planInput = runnerOpt !== undefined
|
|
9823
|
+
? { baseRef, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
|
|
9824
|
+
: { baseRef, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
|
|
9825
|
+
const plan = planDiscriminationCheck(planInput);
|
|
8769
9826
|
if (!plan.runnable) {
|
|
9827
|
+
// QE-2 (instrument-honesty, MEDIUM): a runner REFUSE used to be reported through the generic
|
|
9828
|
+
// "no property test to check"/map-a-test framing — the operator-facing surface re-created the
|
|
9829
|
+
// exact "instrument gap misread as test gap" class ADR-001 names as the reason three duplicate
|
|
9830
|
+
// backlog entries existed. The plan's own named reason is the verdict; the generic classify
|
|
9831
|
+
// stays only for the genuinely-empty-target case.
|
|
9832
|
+
const runnerRefusal = typeof plan.reason === 'string' && plan.reason.startsWith('unsupported-runner');
|
|
9833
|
+
if (runnerRefusal) {
|
|
9834
|
+
const refusal = {
|
|
9835
|
+
aggregate: 'CANNOT_ISOLATE',
|
|
9836
|
+
measurementValid: false,
|
|
9837
|
+
primaryAction: plan.primaryAction ?? 'fix-runner-invocation',
|
|
9838
|
+
finding: {
|
|
9839
|
+
severity: 'high',
|
|
9840
|
+
verdict: 'CANNOT_ISOLATE',
|
|
9841
|
+
files: plan.targets.map((t) => t.file),
|
|
9842
|
+
detail: `runner refused: ${plan.reason} — the INSTRUMENT could not run, nothing was measured; `
|
|
9843
|
+
+ `declare scripts.test in the target package (or pass --runner) and re-run. `
|
|
9844
|
+
+ `This is NOT a statement about the tests.`,
|
|
9845
|
+
},
|
|
9846
|
+
};
|
|
9847
|
+
if (flags.has('json')) {
|
|
9848
|
+
write(JSON.stringify({ plan, results: [], perTest: [], ...refusal }, null, 2));
|
|
9849
|
+
return 0;
|
|
9850
|
+
}
|
|
9851
|
+
write(`discrimination-check: REFUSED (${plan.reason})`);
|
|
9852
|
+
write(` → ${refusal.finding.detail}`);
|
|
9853
|
+
return 0;
|
|
9854
|
+
}
|
|
8770
9855
|
// No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
|
|
8771
9856
|
const result = classifyDiscrimination({ propertyTests: [], results: [] });
|
|
8772
9857
|
if (flags.has('json')) {
|
|
@@ -8871,10 +9956,24 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
8871
9956
|
}
|
|
8872
9957
|
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
8873
9958
|
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
9959
|
+
// Runner honesty (ADR-001): the run executes FROM the target package dir with a
|
|
9960
|
+
// package-relative path — a root-cwd `npx vitest run packages/...` loads the ROOT config
|
|
9961
|
+
// (none) and reds unclassifiably, which is exactly the CANNOT_ISOLATE artifact this
|
|
9962
|
+
// feature removes. The plan's own commands encode the same cd; this body mirrors it.
|
|
9963
|
+
const pkgRel = plan.packageDir === '.' ? '' : plan.packageDir;
|
|
9964
|
+
const fileInPkg = pkgRel !== '' && t.file.startsWith(pkgRel + '/') ? t.file.slice(pkgRel.length + 1) : t.file;
|
|
9965
|
+
const execDirBase = pkgRel === '' ? worktree : join(worktree, pkgRel);
|
|
9966
|
+
const execDirTip = pkgRel === '' ? repoRoot : join(repoRoot, pkgRel);
|
|
8874
9967
|
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
9968
|
+
// NO `--` before the path: MEASURED 2026-09-02 — `npx vitest run -- 'file'` IGNORES the
|
|
9969
|
+
// filter and runs the whole suite (5269 tests), which is the exact whole-repo artifact
|
|
9970
|
+
// this feature removes (QE ha-intake-archive F5). The path is engine-sanitized (no
|
|
9971
|
+
// leading dash, no metacharacters), so it can never be read as an option.
|
|
9972
|
+
const cmd = `${runner}${nameArg} '${fileInPkg}'`;
|
|
9973
|
+
const base = runCapturedTest(cmd, execDirBase, timeoutMs);
|
|
9974
|
+
// The classifier's targetSeen is a substring probe: the run now prints PACKAGE-relative
|
|
9975
|
+
// paths, so it must be probed with the same form, or every hit reads as target-unseen.
|
|
9976
|
+
const evidence = classifyExecutionEvidence(base.output, base.exitCode, fileInPkg);
|
|
8878
9977
|
const outcome = discriminationOutcomeOf(base.exitCode, evidence);
|
|
8879
9978
|
const row = t.name !== undefined
|
|
8880
9979
|
? { file: t.file, name: t.name, outcome, evidence }
|
|
@@ -8885,8 +9984,8 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
8885
9984
|
// base rows per the matrix; running it is cheap and only ever on an already-broken path.
|
|
8886
9985
|
// Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
|
|
8887
9986
|
if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
|
|
8888
|
-
const tip = runCapturedTest(cmd,
|
|
8889
|
-
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode,
|
|
9987
|
+
const tip = runCapturedTest(cmd, execDirTip, timeoutMs);
|
|
9988
|
+
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, fileInPkg);
|
|
8890
9989
|
row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
|
|
8891
9990
|
row['tipEvidence'] = tipEvidence;
|
|
8892
9991
|
// R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
|
|
@@ -8995,51 +10094,6 @@ function readTipTreeConditions(repoRoot) {
|
|
|
8995
10094
|
function nameFor(t, outcome) {
|
|
8996
10095
|
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
8997
10096
|
}
|
|
8998
|
-
/**
|
|
8999
|
-
* `dz mutation-gate` — the mutation gate (feature ha-mutation-gate, SPEC at
|
|
9000
|
-
* features/ha-mutation-gate/SPEC.md). A green test proves the code works; it does NOT prove the
|
|
9001
|
-
* test would notice the protection being DELETED. For each entry in a declarative registry this
|
|
9002
|
-
* gate applies the entry's exact {find, replace} mutation to a SCRATCH COPY of the package, runs
|
|
9003
|
-
* the package's test command there, and REQUIRES a non-zero exit (red). All logic is in
|
|
9004
|
-
* harness-core's pure engine (mutation-gate.ts); this is the I/O executor.
|
|
9005
|
-
*
|
|
9006
|
-
* --package <dir> the package to gate (default: cwd; must contain package.json)
|
|
9007
|
-
* --registry <file> registry path (default: <pkg>/test/mutation-registry.json, then
|
|
9008
|
-
* <pkg>/mutation-registry.json)
|
|
9009
|
-
* --test-cmd '<cmd>' suite command run in the scratch copy (default: registry.testCommand,
|
|
9010
|
-
* then `npm test`)
|
|
9011
|
-
* --only <id[,id]> run a subset of entries (unknown id = usage error, never a silent skip)
|
|
9012
|
-
* --timeout <ms> per-suite-run timeout (default 300000). A timed-out run is INCONCLUSIVE —
|
|
9013
|
-
* a FAILURE, never a pass.
|
|
9014
|
-
* --rebaseline <m> route-b guard mode: 'per-entry' (default — every red entry re-runs the
|
|
9015
|
-
* suite on the restored tree; not green ⇒ that entry is INCONCLUSIVE) or
|
|
9016
|
-
* 'final' (one re-run at the end; not green ⇒ every red-based verdict is
|
|
9017
|
-
* downgraded). The gate's PROVEN now means the redness was ATTRIBUTABLE.
|
|
9018
|
-
* --keep-scratch keep the scratch copy for inspection (default: removed in a finally)
|
|
9019
|
-
* --json machine contract {packageDir, registryPath, testCommand, rebaselineMode,
|
|
9020
|
-
* baseline, results, summary, warnings, exitCode}
|
|
9021
|
-
*
|
|
9022
|
-
* The four rules (SPEC §"Four rules") and where each is enforced:
|
|
9023
|
-
* 1. does-not-apply = FAILURE → core classifyMutationOutcome (occurrences !== 1 ⇒ NOT_APPLIED);
|
|
9024
|
-
* 2. green suite = FAILURE → core (exit 0 ⇒ UNDEFENDED, names the property);
|
|
9025
|
-
* 3. never mutate the working tree → HERE: every write targets the scratch copy under tmpdir();
|
|
9026
|
-
* the repo tree is opened read-only, and a crashed run leaves at worst a stale tmp dir;
|
|
9027
|
-
* 4. the gate's own discrimination proof → harness-cli/test/fixtures/mutation-gate-undefended
|
|
9028
|
-
* (the gate MUST fail on it; asserted by test/mutation-gate-cli.test.ts).
|
|
9029
|
-
*
|
|
9030
|
-
* Exit codes: 0 every entry PROVEN · 1 the gate ran and failed (undefended / not-applied /
|
|
9031
|
-
* below-min / unparseable / load-fatal / over-failing / inconclusive entry) · 2 usage or setup
|
|
9032
|
-
* error (missing registry, red BASELINE — a red unmutated copy proves nothing and must not be
|
|
9033
|
-
* read as a mutation result — or an entry whose file RESOLVES outside the scratch copy: a
|
|
9034
|
-
* symlink escape is refused before anything is written, SPEC rule 3).
|
|
9035
|
-
*/
|
|
9036
|
-
/**
|
|
9037
|
-
* Route-a guard for `dz mutation-gate`: parse-check a MUTATED file as its own language BEFORE the
|
|
9038
|
-
* suite runs. A registry mutation must delete the protection while keeping the file loadable — a
|
|
9039
|
-
* file that no longer parses kills the whole suite (or its import chain), and that STRUCTURAL
|
|
9040
|
-
* redness says nothing about the named protection. Returns `{error}` when a parser ran and the
|
|
9041
|
-
* text does not parse; `{skipped}` (reported loudly, never silently) when no parser is available.
|
|
9042
|
-
*/
|
|
9043
10097
|
function parseCheckMutatedFile(absFile, text) {
|
|
9044
10098
|
const ext = extname(absFile).toLowerCase();
|
|
9045
10099
|
try {
|
|
@@ -9070,18 +10124,36 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9070
10124
|
}
|
|
9071
10125
|
}
|
|
9072
10126
|
if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '') {
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
10127
|
+
const checked = runWithOneInternalRetry(() => {
|
|
10128
|
+
try {
|
|
10129
|
+
// `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
|
|
10130
|
+
execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
|
|
10131
|
+
return {};
|
|
10132
|
+
}
|
|
10133
|
+
catch (e) {
|
|
10134
|
+
const err = e;
|
|
10135
|
+
// A launched parser that exits non-zero with a SyntaxError is a parse verdict. A child
|
|
10136
|
+
// launch/internal error (EPERM, ENOENT, Node's thrown internal) is runner infrastructure
|
|
10137
|
+
// and must take the bounded retry → INCONCLUSIVE route instead of masquerading as bad JS.
|
|
10138
|
+
if (typeof err.code === 'string' || typeof err.status !== 'number')
|
|
10139
|
+
throw e;
|
|
10140
|
+
const stderrLines = String(err.stderr ?? '').split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
10141
|
+
const msg = [...stderrLines].reverse().find((line) => line.includes('Error'))
|
|
10142
|
+
?? stderrLines.at(-1)
|
|
10143
|
+
?? err.message
|
|
10144
|
+
?? 'node --check failed';
|
|
10145
|
+
return { error: msg.slice(0, 200) };
|
|
10146
|
+
}
|
|
10147
|
+
});
|
|
10148
|
+
if (checked.value === null) {
|
|
10149
|
+
return {
|
|
10150
|
+
internalFailureReason: checked.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
10151
|
+
internalAttempts: checked.attempts,
|
|
10152
|
+
};
|
|
9084
10153
|
}
|
|
10154
|
+
return checked.internalRetries === 1
|
|
10155
|
+
? { ...checked.value, internalAttempts: checked.attempts }
|
|
10156
|
+
: checked.value;
|
|
9085
10157
|
}
|
|
9086
10158
|
return { skipped: `no parser for '${ext}' files — parse-check unavailable` };
|
|
9087
10159
|
}
|
|
@@ -9089,7 +10161,7 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9089
10161
|
return { skipped: `parse-check errored: ${String(e.message).slice(0, 120)}` };
|
|
9090
10162
|
}
|
|
9091
10163
|
}
|
|
9092
|
-
function cmdMutationGate(options, flags, cwd, write) {
|
|
10164
|
+
function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
9093
10165
|
const json = flags.has('json');
|
|
9094
10166
|
const fail = (what) => {
|
|
9095
10167
|
write(json ? JSON.stringify({ error: what, exitCode: 2 }) : `dz mutation-gate: ${what}`);
|
|
@@ -9157,6 +10229,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9157
10229
|
const results = [];
|
|
9158
10230
|
const observations = [];
|
|
9159
10231
|
const warnings = [];
|
|
10232
|
+
const internalRetries = [];
|
|
9160
10233
|
let baseline;
|
|
9161
10234
|
try {
|
|
9162
10235
|
if (gitTop !== null && gitTop !== pkgDir && resolve(pkgDir).startsWith(resolve(gitTop) + sep)) {
|
|
@@ -9200,7 +10273,10 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9200
10273
|
// write below is asserted to RESOLVE inside this root before it happens.
|
|
9201
10274
|
const realScratchRoot = realpathSync(copyDir);
|
|
9202
10275
|
const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
|
|
9203
|
-
const
|
|
10276
|
+
const invokeSuite = () => {
|
|
10277
|
+
if (injectedRunner !== undefined) {
|
|
10278
|
+
return injectedRunner(testCmd, { cwd: copyDir, timeoutMs: timeout });
|
|
10279
|
+
}
|
|
9204
10280
|
const run = spawnSync(testCmd, {
|
|
9205
10281
|
cwd: copyDir,
|
|
9206
10282
|
shell: true,
|
|
@@ -9212,6 +10288,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9212
10288
|
const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
|
|
9213
10289
|
? run.error.code
|
|
9214
10290
|
: undefined;
|
|
10291
|
+
// Node may populate both `error` and a numeric `status` for an internal spawn failure. The
|
|
10292
|
+
// error wins except for the two already-named resource observations: a status alongside
|
|
10293
|
+
// EPERM/Unreachable-code is not a suite verdict and takes the one-retry internal-error path.
|
|
10294
|
+
if (run.error !== undefined && errorCode !== 'ETIMEDOUT' && errorCode !== 'ENOBUFS') {
|
|
10295
|
+
throw run.error;
|
|
10296
|
+
}
|
|
9215
10297
|
const signal = typeof run.signal === 'string' ? run.signal : undefined;
|
|
9216
10298
|
let failureReason;
|
|
9217
10299
|
if (typeof run.status !== 'number') {
|
|
@@ -9228,25 +10310,59 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9228
10310
|
}
|
|
9229
10311
|
return {
|
|
9230
10312
|
exitCode: typeof run.status === 'number' ? run.status : null,
|
|
9231
|
-
// Receipt markers may be on stderr. Preserve both streams even on exit 0; stdout-only
|
|
9232
|
-
// collection would silently lose a green-run marker.
|
|
9233
10313
|
output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
|
|
9234
10314
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
9235
10315
|
};
|
|
9236
10316
|
};
|
|
10317
|
+
const runSuite = (phase, entryId) => {
|
|
10318
|
+
const retried = runWithOneInternalRetry(invokeSuite);
|
|
10319
|
+
const loggedAttempts = retried.attempts.map((attempt) => {
|
|
10320
|
+
if (attempt.outcome !== 'completed' || retried.value === null)
|
|
10321
|
+
return attempt;
|
|
10322
|
+
const outcome = retried.value.exitCode === null
|
|
10323
|
+
? `no exit code (${retried.value.failureReason ?? 'unnamed failure'})`
|
|
10324
|
+
: `exit ${retried.value.exitCode}`;
|
|
10325
|
+
return { ...attempt, detail: `attempt ${attempt.attempt}: completed — ${outcome}` };
|
|
10326
|
+
});
|
|
10327
|
+
if (retried.internalRetries === 1) {
|
|
10328
|
+
const record = entryId === undefined
|
|
10329
|
+
? { phase, attempts: loggedAttempts }
|
|
10330
|
+
: { phase, entryId, attempts: loggedAttempts };
|
|
10331
|
+
internalRetries.push(record);
|
|
10332
|
+
if (!json)
|
|
10333
|
+
write(`mutation-gate: internal retry — ${loggedAttempts.map((attempt) => attempt.detail).join('; ')}`);
|
|
10334
|
+
}
|
|
10335
|
+
const internalAttemptLog = retried.internalRetries === 1
|
|
10336
|
+
? loggedAttempts.map((attempt) => attempt.detail).join('; ')
|
|
10337
|
+
: undefined;
|
|
10338
|
+
if (retried.value !== null) {
|
|
10339
|
+
return {
|
|
10340
|
+
...retried.value,
|
|
10341
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
10342
|
+
};
|
|
10343
|
+
}
|
|
10344
|
+
return {
|
|
10345
|
+
exitCode: null,
|
|
10346
|
+
output: '',
|
|
10347
|
+
failureReason: retried.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
10348
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
10349
|
+
};
|
|
10350
|
+
};
|
|
9237
10351
|
// Baseline BEFORE any mutation: a red copy proves nothing, and reading it as a mutation
|
|
9238
10352
|
// result would be this gate shipping the defect class it exists to catch.
|
|
9239
10353
|
if (!json)
|
|
9240
10354
|
write(`mutation-gate: baseline suite in scratch copy of ${pkgDir} …`);
|
|
9241
|
-
const base = runSuite();
|
|
9242
|
-
baseline = classifyBaseline(base.exitCode, base.failureReason
|
|
10355
|
+
const base = runSuite('baseline');
|
|
10356
|
+
baseline = classifyBaseline(base.exitCode, base.failureReason, base.exitCode !== null && base.exitCode !== 0
|
|
10357
|
+
? attributeBaselineRedness(base.output, entries.map((entry) => entry.file))
|
|
10358
|
+
: undefined);
|
|
9243
10359
|
if (!baseline.ok) {
|
|
9244
10360
|
if (json) {
|
|
9245
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], exitCode:
|
|
9246
|
-
return
|
|
10361
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
|
|
10362
|
+
return 1;
|
|
9247
10363
|
}
|
|
9248
10364
|
write(renderMutationReport([], baseline, pkgDir));
|
|
9249
|
-
return
|
|
10365
|
+
return 1;
|
|
9250
10366
|
}
|
|
9251
10367
|
for (const entry of entries) {
|
|
9252
10368
|
const filePath = join(copyDir, entry.file);
|
|
@@ -9289,6 +10405,8 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9289
10405
|
write(`mutation-gate: ${entry.id} — mutating ${entry.file}, running suite …`);
|
|
9290
10406
|
let run = null;
|
|
9291
10407
|
let parseError;
|
|
10408
|
+
let parseInternalFailureReason;
|
|
10409
|
+
let parseInternalAttemptLog;
|
|
9292
10410
|
try {
|
|
9293
10411
|
writeFileSync(filePath, applied.text);
|
|
9294
10412
|
// Route-a guard: the mutated file must still PARSE — a load failure reddens the whole
|
|
@@ -9299,11 +10417,18 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9299
10417
|
if (!json)
|
|
9300
10418
|
write(`mutation-gate: WARNING ${entry.id}: parse-check skipped — ${check.skipped}`);
|
|
9301
10419
|
}
|
|
10420
|
+
if (check.internalAttempts !== undefined) {
|
|
10421
|
+
internalRetries.push({ phase: 'parse-check', entryId: entry.id, attempts: check.internalAttempts });
|
|
10422
|
+
parseInternalAttemptLog = check.internalAttempts.map((attempt) => attempt.detail).join('; ');
|
|
10423
|
+
if (!json)
|
|
10424
|
+
write(`mutation-gate: internal retry — ${parseInternalAttemptLog}`);
|
|
10425
|
+
}
|
|
10426
|
+
parseInternalFailureReason = check.internalFailureReason;
|
|
9302
10427
|
if (check.error !== undefined) {
|
|
9303
10428
|
parseError = check.error; // no suite run: the verdict is MUTATION_UNPARSEABLE regardless
|
|
9304
10429
|
}
|
|
9305
|
-
else {
|
|
9306
|
-
run = runSuite();
|
|
10430
|
+
else if (parseInternalFailureReason === undefined) {
|
|
10431
|
+
run = runSuite('mutation', entry.id);
|
|
9307
10432
|
}
|
|
9308
10433
|
}
|
|
9309
10434
|
finally {
|
|
@@ -9339,14 +10464,24 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9339
10464
|
// those verdicts outrank the rebaseline check, so the extra suite run would buy nothing.
|
|
9340
10465
|
let rebaselineExitCode;
|
|
9341
10466
|
let rebaselineFailureReason;
|
|
10467
|
+
let rebaselineAttribution;
|
|
10468
|
+
let rebaselineInternalAttemptLog;
|
|
9342
10469
|
if (rebaselineMode === 'per-entry' && run !== null && run.exitCode !== null && run.exitCode !== 0
|
|
9343
10470
|
&& fileLoadFailure === undefined && outputUnrecognised === undefined && receiptMismatch === undefined) {
|
|
9344
10471
|
if (!json)
|
|
9345
10472
|
write(`mutation-gate: ${entry.id} — re-baselining the restored tree …`);
|
|
9346
|
-
const rebaselineRun = runSuite();
|
|
10473
|
+
const rebaselineRun = runSuite('rebaseline', entry.id);
|
|
9347
10474
|
rebaselineExitCode = rebaselineRun.exitCode;
|
|
9348
10475
|
rebaselineFailureReason = rebaselineRun.failureReason;
|
|
10476
|
+
rebaselineInternalAttemptLog = rebaselineRun.internalAttemptLog;
|
|
10477
|
+
if (rebaselineRun.exitCode !== null && rebaselineRun.exitCode !== 0) {
|
|
10478
|
+
rebaselineAttribution = attributeBaselineRedness(rebaselineRun.output, entries.map((candidate) => candidate.file));
|
|
10479
|
+
}
|
|
9349
10480
|
}
|
|
10481
|
+
const entryRunFailureReason = run?.failureReason ?? parseInternalFailureReason;
|
|
10482
|
+
const entryInternalAttemptLog = [parseInternalAttemptLog, run?.internalAttemptLog, rebaselineInternalAttemptLog]
|
|
10483
|
+
.filter((log) => log !== undefined)
|
|
10484
|
+
.join('; ');
|
|
9350
10485
|
const obs = {
|
|
9351
10486
|
entry,
|
|
9352
10487
|
occurrences: 1,
|
|
@@ -9356,9 +10491,11 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9356
10491
|
...(fileLoadFailure !== undefined ? { fileLoadFailure } : {}),
|
|
9357
10492
|
...(outputUnrecognised !== undefined ? { outputUnrecognised } : {}),
|
|
9358
10493
|
...(receiptMismatch !== undefined ? { receiptMismatch } : {}),
|
|
9359
|
-
...(
|
|
10494
|
+
...(entryRunFailureReason !== undefined ? { runFailureReason: entryRunFailureReason } : {}),
|
|
10495
|
+
...(entryInternalAttemptLog !== '' ? { internalAttemptLog: entryInternalAttemptLog } : {}),
|
|
9360
10496
|
...(rebaselineExitCode !== undefined ? { rebaselineExitCode } : {}),
|
|
9361
10497
|
...(rebaselineFailureReason !== undefined ? { rebaselineFailureReason } : {}),
|
|
10498
|
+
...(rebaselineAttribution !== undefined ? { rebaselineAttribution } : {}),
|
|
9362
10499
|
};
|
|
9363
10500
|
observations.push(obs);
|
|
9364
10501
|
results.push(classifyMutationOutcome(obs));
|
|
@@ -9371,7 +10508,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9371
10508
|
if (rebaselineMode === 'final') {
|
|
9372
10509
|
if (!json)
|
|
9373
10510
|
write('mutation-gate: final re-baseline of the restored tree …');
|
|
9374
|
-
const finalRun = runSuite();
|
|
10511
|
+
const finalRun = runSuite('final-rebaseline');
|
|
9375
10512
|
const finalExit = finalRun.exitCode;
|
|
9376
10513
|
if (finalExit !== 0) {
|
|
9377
10514
|
const what = finalExit === null ? `no exit code: ${finalRun.failureReason ?? 'unknown timeout / spawn failure'}` : `exit ${finalExit}`;
|
|
@@ -9382,6 +10519,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9382
10519
|
...obs,
|
|
9383
10520
|
rebaselineExitCode: finalExit,
|
|
9384
10521
|
...(finalRun.failureReason !== undefined ? { rebaselineFailureReason: finalRun.failureReason } : {}),
|
|
10522
|
+
...(finalRun.internalAttemptLog !== undefined
|
|
10523
|
+
? { internalAttemptLog: [obs.internalAttemptLog, finalRun.internalAttemptLog].filter((log) => log !== undefined).join('; ') }
|
|
10524
|
+
: {}),
|
|
10525
|
+
...(finalExit !== null && finalExit !== 0
|
|
10526
|
+
? { rebaselineAttribution: attributeBaselineRedness(finalRun.output, entries.map((entry) => entry.file)) }
|
|
10527
|
+
: {}),
|
|
9385
10528
|
}));
|
|
9386
10529
|
results.length = 0;
|
|
9387
10530
|
results.push(...reclassified);
|
|
@@ -9401,7 +10544,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
9401
10544
|
}
|
|
9402
10545
|
const exitCode = mutationGateExitCode(results, baseline.ok);
|
|
9403
10546
|
if (json) {
|
|
9404
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, exitCode }, null, 2));
|
|
10547
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
|
|
9405
10548
|
return exitCode;
|
|
9406
10549
|
}
|
|
9407
10550
|
write(renderMutationReport(results, baseline, pkgDir));
|
|
@@ -11245,6 +12388,18 @@ function cmdFeatureAdrRecord(options, flags, cwd, write) {
|
|
|
11245
12388
|
targetExists: existsSync(target),
|
|
11246
12389
|
targetHasPair: flags.has('once') && existsSync(target),
|
|
11247
12390
|
timestamp: new Date().toISOString(),
|
|
12391
|
+
// WHO ran it: `--runner <id>` when the caller knows, otherwise this host. The workflow cannot
|
|
12392
|
+
// supply it — it has no host inside its sandbox — so the identity is resolved here, at the one
|
|
12393
|
+
// seam that runs outside. hostname() can throw on an exotic setup; an unresolvable runner stays
|
|
12394
|
+
// ABSENT rather than becoming the string 'unknown', which would later join as if it were one.
|
|
12395
|
+
runnerId: (options.get('runner') ?? '').trim() !== ''
|
|
12396
|
+
? (options.get('runner') ?? '').trim()
|
|
12397
|
+
: (() => { try {
|
|
12398
|
+
return hostname();
|
|
12399
|
+
}
|
|
12400
|
+
catch {
|
|
12401
|
+
return null;
|
|
12402
|
+
} })(),
|
|
11248
12403
|
});
|
|
11249
12404
|
if (decision.line === null)
|
|
11250
12405
|
return emit(decision);
|
|
@@ -12597,6 +13752,143 @@ async function cmdQeBridge(options, flags, cwd, write) {
|
|
|
12597
13752
|
cleanupIsolated();
|
|
12598
13753
|
}
|
|
12599
13754
|
}
|
|
13755
|
+
function scoreReceiptFiles(root) {
|
|
13756
|
+
const featuresDir = join(root, 'features');
|
|
13757
|
+
let features;
|
|
13758
|
+
try {
|
|
13759
|
+
features = readdirSync(featuresDir, { withFileTypes: true });
|
|
13760
|
+
}
|
|
13761
|
+
catch {
|
|
13762
|
+
return [];
|
|
13763
|
+
}
|
|
13764
|
+
const receipts = [];
|
|
13765
|
+
for (const feature of features) {
|
|
13766
|
+
if (!feature.isDirectory())
|
|
13767
|
+
continue;
|
|
13768
|
+
const stateDir = join(featuresDir, feature.name, '.fa-state');
|
|
13769
|
+
let entries;
|
|
13770
|
+
try {
|
|
13771
|
+
if (lstatSync(stateDir).isSymbolicLink())
|
|
13772
|
+
continue;
|
|
13773
|
+
entries = readdirSync(stateDir, { withFileTypes: true });
|
|
13774
|
+
}
|
|
13775
|
+
catch {
|
|
13776
|
+
continue;
|
|
13777
|
+
}
|
|
13778
|
+
for (const entry of entries) {
|
|
13779
|
+
if (!entry.isFile())
|
|
13780
|
+
continue;
|
|
13781
|
+
const match = /^score-(.+)\.json$/.exec(entry.name);
|
|
13782
|
+
if (match === null || match[1] === undefined || match[1] === '')
|
|
13783
|
+
continue;
|
|
13784
|
+
const path = join(stateDir, entry.name);
|
|
13785
|
+
receipts.push({ path, displayPath: relative(root, path), qeHash: match[1] });
|
|
13786
|
+
}
|
|
13787
|
+
}
|
|
13788
|
+
return receipts.sort((a, b) => a.displayPath < b.displayPath ? -1 : a.displayPath > b.displayPath ? 1 : 0);
|
|
13789
|
+
}
|
|
13790
|
+
function scoreAggregateChainLine(text) {
|
|
13791
|
+
if (text === '') {
|
|
13792
|
+
return { line: 'chain: NOT_PRESENT — no aggregate evidence file was created', verification: null, defectAges: null };
|
|
13793
|
+
}
|
|
13794
|
+
const verification = verifyEventChainText(text);
|
|
13795
|
+
const defectAges = classifyChainDefects(verification, verification.lines);
|
|
13796
|
+
const kinds = new Map();
|
|
13797
|
+
for (const defect of verification.defects)
|
|
13798
|
+
kinds.set(defect.kind, (kinds.get(defect.kind) ?? 0) + 1);
|
|
13799
|
+
const kindText = [...kinds.entries()].map(([kind, count]) => `${kind}: ${count}`).join(' · ');
|
|
13800
|
+
const line = `chain: ${verification.ok ? 'OK' : 'FAILED'} · ${verification.chained} chained · ` +
|
|
13801
|
+
`${verification.resets} recorded restart(s) · before-run defects ${defectAges.beforeRun.length} · ` +
|
|
13802
|
+
`in-run defects ${defectAges.inRun.length} · current run ${defectAges.runRecords} record(s)` +
|
|
13803
|
+
(kindText === '' ? '' : ` · ${kindText}`) +
|
|
13804
|
+
` — ${verification.scope}`;
|
|
13805
|
+
return { line, verification, defectAges };
|
|
13806
|
+
}
|
|
13807
|
+
function cmdScoreAll(options, flags, cwd, write) {
|
|
13808
|
+
const json = flags.has('json');
|
|
13809
|
+
if (options.has('slug')) {
|
|
13810
|
+
write(json
|
|
13811
|
+
? JSON.stringify({ error: '--all and --slug are mutually exclusive', exitCode: 1 })
|
|
13812
|
+
: 'dz score: --all and --slug are mutually exclusive');
|
|
13813
|
+
return 1;
|
|
13814
|
+
}
|
|
13815
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
13816
|
+
const receiptFiles = scoreReceiptFiles(root);
|
|
13817
|
+
if (receiptFiles.length === 0) {
|
|
13818
|
+
const report = buildScoreAggregateReport([], [], 0);
|
|
13819
|
+
const chain = scoreAggregateChainLine('');
|
|
13820
|
+
if (json)
|
|
13821
|
+
write(JSON.stringify({ ...report, chain: null, aggregatePath: '.dz/feature-adr/scorecards.jsonl', exitCode: 0 }, null, 2));
|
|
13822
|
+
else {
|
|
13823
|
+
write(renderScoreAggregateReport(report));
|
|
13824
|
+
write(chain.line);
|
|
13825
|
+
}
|
|
13826
|
+
return 0;
|
|
13827
|
+
}
|
|
13828
|
+
const ts = new Date().toISOString();
|
|
13829
|
+
const rows = [];
|
|
13830
|
+
const unreadableReceipts = [];
|
|
13831
|
+
for (const receipt of receiptFiles) {
|
|
13832
|
+
try {
|
|
13833
|
+
rows.push(scoreReceiptToAggregateRow({
|
|
13834
|
+
content: readFileSync(receipt.path, 'utf8'),
|
|
13835
|
+
qeHash: receipt.qeHash,
|
|
13836
|
+
ts,
|
|
13837
|
+
}));
|
|
13838
|
+
}
|
|
13839
|
+
catch {
|
|
13840
|
+
unreadableReceipts.push(receipt.displayPath);
|
|
13841
|
+
}
|
|
13842
|
+
}
|
|
13843
|
+
const storeDir = join(root, '.dz', 'feature-adr');
|
|
13844
|
+
const aggregatePath = join(storeDir, 'scorecards.jsonl');
|
|
13845
|
+
let finalText = '';
|
|
13846
|
+
let finalRows = rows;
|
|
13847
|
+
let appended = 0;
|
|
13848
|
+
let storeError = null;
|
|
13849
|
+
try {
|
|
13850
|
+
const result = withNamedLockSync(storeDir, 'scorecards', () => {
|
|
13851
|
+
let existingText = '';
|
|
13852
|
+
try {
|
|
13853
|
+
existingText = readFileSync(aggregatePath, 'utf8');
|
|
13854
|
+
}
|
|
13855
|
+
catch (error) {
|
|
13856
|
+
if (error.code !== 'ENOENT')
|
|
13857
|
+
throw error;
|
|
13858
|
+
}
|
|
13859
|
+
const fresh = dedupeScoreAggregateRows(rows, readScoreAggregateRows(existingText));
|
|
13860
|
+
const appendText = appendChainedLines(fresh, readTailInfo(existingText));
|
|
13861
|
+
if (appendText !== '')
|
|
13862
|
+
appendFileSync(aggregatePath, appendText, { encoding: 'utf8', mode: 0o600 });
|
|
13863
|
+
const settledText = existingText + appendText;
|
|
13864
|
+
return { text: settledText, rows: readScoreAggregateRows(settledText), appended: fresh.length };
|
|
13865
|
+
});
|
|
13866
|
+
finalText = result.text;
|
|
13867
|
+
finalRows = result.rows;
|
|
13868
|
+
appended = result.appended;
|
|
13869
|
+
}
|
|
13870
|
+
catch (error) {
|
|
13871
|
+
storeError = error instanceof Error ? error.message : String(error);
|
|
13872
|
+
}
|
|
13873
|
+
const report = buildScoreAggregateReport(finalRows, unreadableReceipts, appended);
|
|
13874
|
+
const chain = scoreAggregateChainLine(finalText);
|
|
13875
|
+
if (json) {
|
|
13876
|
+
write(JSON.stringify({
|
|
13877
|
+
...report,
|
|
13878
|
+
aggregatePath: '.dz/feature-adr/scorecards.jsonl',
|
|
13879
|
+
chain: chain.verification === null ? null : { verification: chain.verification, defectAges: chain.defectAges },
|
|
13880
|
+
storeError,
|
|
13881
|
+
exitCode: 0,
|
|
13882
|
+
}, null, 2));
|
|
13883
|
+
}
|
|
13884
|
+
else {
|
|
13885
|
+
write(renderScoreAggregateReport(report));
|
|
13886
|
+
write(chain.line);
|
|
13887
|
+
if (storeError !== null)
|
|
13888
|
+
write(`store error (nothing was claimed appended): ${storeError}`);
|
|
13889
|
+
}
|
|
13890
|
+
return 0;
|
|
13891
|
+
}
|
|
12600
13892
|
function cmdScore(options, flags, cwd, write) {
|
|
12601
13893
|
const json = flags.has('json');
|
|
12602
13894
|
if (flags.has('help')) {
|
|
@@ -12605,13 +13897,14 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
12605
13897
|
write(JSON.stringify({ help: usage, exitCode: 0 })); // --json stays ONE document even for help
|
|
12606
13898
|
else {
|
|
12607
13899
|
write(usage);
|
|
13900
|
+
write('dz score --all [--project <dir>] [--json] — sweep immutable score receipts into the append-only chained aggregate');
|
|
12608
13901
|
write(' disciplines: ADR confirmation · discrimination · cross-model QE · live verification · README-first · learning loop · amendments');
|
|
12609
13902
|
write(' descriptive-only, never a gate: a low score exits 0');
|
|
12610
13903
|
}
|
|
12611
13904
|
return 0;
|
|
12612
13905
|
}
|
|
12613
13906
|
for (const flag of flags) {
|
|
12614
|
-
if (!new Set(['json', 'help']).has(flag)) {
|
|
13907
|
+
if (!new Set(['json', 'help', 'all']).has(flag)) {
|
|
12615
13908
|
write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz score: unknown option --${flag}\n allowed: --slug <feature>, --project <dir>, --json`);
|
|
12616
13909
|
return 1;
|
|
12617
13910
|
}
|
|
@@ -12623,6 +13916,8 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
12623
13916
|
return 1;
|
|
12624
13917
|
}
|
|
12625
13918
|
}
|
|
13919
|
+
if (flags.has('all'))
|
|
13920
|
+
return cmdScoreAll(options, flags, cwd, write);
|
|
12626
13921
|
const slug = options.get('slug') ?? '';
|
|
12627
13922
|
// The delivery-check traversal lesson, upgraded to a WHITELIST: `.` slipped the blacklist and
|
|
12628
13923
|
// silently aggregated the entire features/ tree as one "run" (Codex QE #2).
|
|
@@ -12743,6 +14038,134 @@ function readOptionalText(path) {
|
|
|
12743
14038
|
return '';
|
|
12744
14039
|
}
|
|
12745
14040
|
}
|
|
14041
|
+
function localEvidenceReadReason(resource, error) {
|
|
14042
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
14043
|
+
return code === 'ENOENT' ? `${resource}-missing` : `${resource}-unreadable`;
|
|
14044
|
+
}
|
|
14045
|
+
function unavailablePromotionEvidence(reason) {
|
|
14046
|
+
return { source: { status: 'not-measured', reason }, acceptances: [], truncatedPeriods: [], acceptanceHistoryComplete: false };
|
|
14047
|
+
}
|
|
14048
|
+
function readPromotionRunEvidence(root) {
|
|
14049
|
+
let raw;
|
|
14050
|
+
try {
|
|
14051
|
+
raw = JSON.parse(readFileSync(join(root, PROMOTION_STATE_FILE), 'utf-8'));
|
|
14052
|
+
}
|
|
14053
|
+
catch (error) {
|
|
14054
|
+
const reason = error instanceof SyntaxError
|
|
14055
|
+
? 'promotion-journal-malformed'
|
|
14056
|
+
: localEvidenceReadReason('promotion-journal', error);
|
|
14057
|
+
return unavailablePromotionEvidence(reason);
|
|
14058
|
+
}
|
|
14059
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
14060
|
+
return unavailablePromotionEvidence('promotion-journal-malformed');
|
|
14061
|
+
}
|
|
14062
|
+
const record = raw;
|
|
14063
|
+
if (record['version'] !== 1 || !Object.hasOwn(record, 'runs')) {
|
|
14064
|
+
return unavailablePromotionEvidence(record['version'] === 1 ? 'promotion-history-not-recorded' : 'promotion-journal-malformed');
|
|
14065
|
+
}
|
|
14066
|
+
if (!Array.isArray(record['runs'])) {
|
|
14067
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
14068
|
+
}
|
|
14069
|
+
const state = normalizePromotionState(raw);
|
|
14070
|
+
if (state.runs === undefined || state.runs.length !== record['runs'].length) {
|
|
14071
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
14072
|
+
}
|
|
14073
|
+
if ((Object.hasOwn(record, 'acceptances') && (!Array.isArray(record['acceptances']) || state.acceptances?.length !== record['acceptances'].length)) ||
|
|
14074
|
+
(Object.hasOwn(record, 'truncatedRunPeriods') && (!Array.isArray(record['truncatedRunPeriods']) || state.truncatedRunPeriods?.length !== record['truncatedRunPeriods'].length)) ||
|
|
14075
|
+
(Object.hasOwn(record, 'acceptanceHistoryComplete') && typeof record['acceptanceHistoryComplete'] !== 'boolean')) {
|
|
14076
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
14077
|
+
}
|
|
14078
|
+
const derivedAcceptances = state.runs.flatMap((run) => run.candidates
|
|
14079
|
+
.filter((candidate) => candidate.verdict === 'promote' && candidate.ruleContentAnchor !== null)
|
|
14080
|
+
.map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor, acceptedTs: run.ts })));
|
|
14081
|
+
const claimedAcceptanceComplete = state.acceptanceHistoryComplete ??
|
|
14082
|
+
((state.truncatedRunPeriods?.length ?? 0) === 0 && state.runs.every((run) => run.complete === true));
|
|
14083
|
+
if (claimedAcceptanceComplete && state.acceptances !== undefined && derivedAcceptances.some((derived) => !state.acceptances.some((stored) => stored.ruleContentAnchor === derived.ruleContentAnchor &&
|
|
14084
|
+
Date.parse(stored.acceptedTs) <= Date.parse(derived.acceptedTs)))) {
|
|
14085
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
14086
|
+
}
|
|
14087
|
+
const acceptancesByAnchor = new Map();
|
|
14088
|
+
for (const acceptance of [...(state.acceptances ?? []), ...derivedAcceptances]) {
|
|
14089
|
+
const prior = acceptancesByAnchor.get(acceptance.ruleContentAnchor);
|
|
14090
|
+
if (prior === undefined || Date.parse(acceptance.acceptedTs) < Date.parse(prior.acceptedTs)) {
|
|
14091
|
+
acceptancesByAnchor.set(acceptance.ruleContentAnchor, acceptance);
|
|
14092
|
+
}
|
|
14093
|
+
}
|
|
14094
|
+
return {
|
|
14095
|
+
source: { status: 'measured', rows: state.runs },
|
|
14096
|
+
acceptances: [...acceptancesByAnchor.values()],
|
|
14097
|
+
truncatedPeriods: state.truncatedRunPeriods ?? [],
|
|
14098
|
+
acceptanceHistoryComplete: claimedAcceptanceComplete,
|
|
14099
|
+
};
|
|
14100
|
+
}
|
|
14101
|
+
function readGuardAuditEvidence(root) {
|
|
14102
|
+
let text;
|
|
14103
|
+
try {
|
|
14104
|
+
text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
14105
|
+
}
|
|
14106
|
+
catch (error) {
|
|
14107
|
+
return {
|
|
14108
|
+
source: { status: 'not-measured', reason: localEvidenceReadReason('guard-audit', error) },
|
|
14109
|
+
rows: [],
|
|
14110
|
+
text: null,
|
|
14111
|
+
};
|
|
14112
|
+
}
|
|
14113
|
+
const rows = [];
|
|
14114
|
+
let malformed = false;
|
|
14115
|
+
for (const line of text.split('\n')) {
|
|
14116
|
+
if (line.trim() === '')
|
|
14117
|
+
continue;
|
|
14118
|
+
try {
|
|
14119
|
+
const raw = JSON.parse(line);
|
|
14120
|
+
if (!isOffsetIsoTimestamp(raw['ts']) ||
|
|
14121
|
+
!['publish', 'teach', 'consolidate', 'reindex'].includes(String(raw['op'])) ||
|
|
14122
|
+
!['pass', 'warn', 'block'].includes(String(raw['verdict']))) {
|
|
14123
|
+
malformed = true;
|
|
14124
|
+
continue;
|
|
14125
|
+
}
|
|
14126
|
+
const violations = [];
|
|
14127
|
+
if (!Array.isArray(raw['violations'])) {
|
|
14128
|
+
malformed = true;
|
|
14129
|
+
continue;
|
|
14130
|
+
}
|
|
14131
|
+
for (const item of raw['violations']) {
|
|
14132
|
+
const rule = item && typeof item === 'object' ? item.rule : undefined;
|
|
14133
|
+
if (typeof rule !== 'string' || rule === '' || rule.length > 200) {
|
|
14134
|
+
malformed = true;
|
|
14135
|
+
continue;
|
|
14136
|
+
}
|
|
14137
|
+
const anchor = item.contentAnchor;
|
|
14138
|
+
if (anchor !== undefined && !isLessonRuleContentAnchor(anchor)) {
|
|
14139
|
+
malformed = true;
|
|
14140
|
+
continue;
|
|
14141
|
+
}
|
|
14142
|
+
violations.push({ rule, ...(typeof anchor === 'string' ? { contentAnchor: anchor } : {}) });
|
|
14143
|
+
}
|
|
14144
|
+
const verdict = raw['verdict'];
|
|
14145
|
+
if ((verdict === 'pass') !== (violations.length === 0))
|
|
14146
|
+
malformed = true;
|
|
14147
|
+
rows.push({
|
|
14148
|
+
ts: raw['ts'],
|
|
14149
|
+
op: raw['op'],
|
|
14150
|
+
verdict,
|
|
14151
|
+
rules: violations.map((item) => item.rule),
|
|
14152
|
+
violations,
|
|
14153
|
+
});
|
|
14154
|
+
}
|
|
14155
|
+
catch {
|
|
14156
|
+
malformed = true;
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
return {
|
|
14160
|
+
source: malformed
|
|
14161
|
+
? { status: 'not-measured', reason: 'guard-audit-malformed' }
|
|
14162
|
+
: !verifyEventChainText(text).ok
|
|
14163
|
+
? { status: 'not-measured', reason: 'guard-audit-chain-corrupt' }
|
|
14164
|
+
: { status: 'measured', rows },
|
|
14165
|
+
rows,
|
|
14166
|
+
text,
|
|
14167
|
+
};
|
|
14168
|
+
}
|
|
12746
14169
|
function deadwoodAllowlistText() {
|
|
12747
14170
|
const require = createRequire(import.meta.url);
|
|
12748
14171
|
const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
|
|
@@ -12823,7 +14246,7 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12823
14246
|
const json = flags.has('json');
|
|
12824
14247
|
if (flags.has('help')) {
|
|
12825
14248
|
write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
|
|
12826
|
-
write(' pool payoff
|
|
14249
|
+
write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
|
|
12827
14250
|
return 0;
|
|
12828
14251
|
}
|
|
12829
14252
|
for (const flag of flags) {
|
|
@@ -12852,36 +14275,14 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12852
14275
|
}));
|
|
12853
14276
|
// apply-leg usage events (read records only; aggregate rows carry no query by construction)
|
|
12854
14277
|
const usage = readRecallUsageEvents(root);
|
|
12855
|
-
|
|
12856
|
-
const
|
|
12857
|
-
|
|
12858
|
-
const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
12859
|
-
for (const line of text.split('\n')) {
|
|
12860
|
-
if (line.trim() === '')
|
|
12861
|
-
continue;
|
|
12862
|
-
try {
|
|
12863
|
-
const o = JSON.parse(line);
|
|
12864
|
-
if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
|
|
12865
|
-
guard.push({
|
|
12866
|
-
ts: o.ts,
|
|
12867
|
-
verdict: o.verdict,
|
|
12868
|
-
rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
|
|
12869
|
-
});
|
|
12870
|
-
}
|
|
12871
|
-
}
|
|
12872
|
-
catch {
|
|
12873
|
-
/* skip */
|
|
12874
|
-
}
|
|
12875
|
-
}
|
|
12876
|
-
}
|
|
12877
|
-
catch {
|
|
12878
|
-
/* no audit yet */
|
|
12879
|
-
}
|
|
14278
|
+
const promotionEvidence = readPromotionRunEvidence(root);
|
|
14279
|
+
const guardEvidence = readGuardAuditEvidence(root);
|
|
14280
|
+
const guard = [...guardEvidence.rows];
|
|
12880
14281
|
// The evidence logs themselves, verbatim: the report verifies their hash chains (feature
|
|
12881
14282
|
// event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
|
|
12882
14283
|
// "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
|
|
12883
14284
|
const evidenceLogs = [];
|
|
12884
|
-
for (const rel of ['.dz/recall-usage.jsonl'
|
|
14285
|
+
for (const rel of ['.dz/recall-usage.jsonl']) {
|
|
12885
14286
|
try {
|
|
12886
14287
|
evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
|
|
12887
14288
|
}
|
|
@@ -12889,6 +14290,9 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12889
14290
|
/* absent log — reported by its own gate above, not invented here */
|
|
12890
14291
|
}
|
|
12891
14292
|
}
|
|
14293
|
+
if (guardEvidence.text !== null) {
|
|
14294
|
+
evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
|
|
14295
|
+
}
|
|
12892
14296
|
const now = new Date();
|
|
12893
14297
|
const report = assembleCompoundingReport({
|
|
12894
14298
|
lessons,
|
|
@@ -12897,6 +14301,13 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12897
14301
|
nowTs: now.toISOString(),
|
|
12898
14302
|
evidenceLogs,
|
|
12899
14303
|
cmdUsageDepthDays: cmdUsageDepthDays(root, now),
|
|
14304
|
+
lessonToRule: {
|
|
14305
|
+
promotionRuns: promotionEvidence.source,
|
|
14306
|
+
guardAudits: guardEvidence.source,
|
|
14307
|
+
promotionAcceptances: promotionEvidence.acceptances,
|
|
14308
|
+
truncatedPromotionPeriods: promotionEvidence.truncatedPeriods,
|
|
14309
|
+
acceptanceHistoryComplete: promotionEvidence.acceptanceHistoryComplete,
|
|
14310
|
+
},
|
|
12900
14311
|
});
|
|
12901
14312
|
// lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
|
|
12902
14313
|
// dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
|
|
@@ -14380,17 +15791,19 @@ function cmdStats(cwd, write) {
|
|
|
14380
15791
|
}
|
|
14381
15792
|
const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
14382
15793
|
const packages = dirs.length;
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
15794
|
+
// Backlog e160aeee. This used to walk the tree ITSELF, and was wrong in two independent ways:
|
|
15795
|
+
// it counted only packages whose NAME starts with `skills-` (health-advisor, p-replicator,
|
|
15796
|
+
// keysarium and trip-planner were therefore invisible), and it knew only ONE of the three skill
|
|
15797
|
+
// layouts. Result: 203 here against 250 from `dz registry` on the same tree — two counters of one
|
|
15798
|
+
// quantity, each unable to refute the other because neither knew the other existed.
|
|
15799
|
+
//
|
|
15800
|
+
// The fix is structural, not arithmetic: there is now ONE enumerator, and both commands ask it.
|
|
15801
|
+
// Pinned by test/stats-registry-parity.test.ts, whose red half is this exact divergence.
|
|
15802
|
+
// The registry already PUBLISHES these totals; recomputing them from `entries` here would be a
|
|
15803
|
+
// third implementation of the same count, which is the very defect being fixed.
|
|
15804
|
+
const registry = buildRegistry(cwd);
|
|
15805
|
+
const totalSkills = registry.totalSkills;
|
|
15806
|
+
const skillPacks = registry.totalPacks;
|
|
14394
15807
|
const targets = TARGET_NAMES.length;
|
|
14395
15808
|
const presets = PRESET_NAMES.length;
|
|
14396
15809
|
write(`dz stats — DZ Harness Hub`);
|
|
@@ -14875,7 +16288,28 @@ export async function runCli(argv, io = {}) {
|
|
|
14875
16288
|
// let a guard treat "I could not tell you" as "I answered you".
|
|
14876
16289
|
return version === 'unknown' ? 1 : 0;
|
|
14877
16290
|
}
|
|
14878
|
-
|
|
16291
|
+
// `-h` is the most-typed help flag and is NOT a command: before the unknown-command contract
|
|
16292
|
+
// landed it fell through to the switch and still printed usage; afterwards it would have died
|
|
16293
|
+
// with exit 2 and an empty stdout (measured regression, cross-model QE M1). It belongs beside
|
|
16294
|
+
// `-v` above — an argv-level flag, resolved before command dispatch.
|
|
16295
|
+
if (argv[0] === '-h') {
|
|
16296
|
+
write(USAGE);
|
|
16297
|
+
return 0;
|
|
16298
|
+
}
|
|
16299
|
+
// A bare `--typo` leaves the command empty, so the usage branch reported SUCCESS on a misspelled
|
|
16300
|
+
// FLAG exactly as it used to on a misspelled VERB (cross-model QE M2): `dz --frobnicate` exited 0
|
|
16301
|
+
// with 30 KB of usage. The refusal is deliberately narrowed to the no-command case, because the
|
|
16302
|
+
// warn-don't-refuse decision above is measured and still stands: with a command present, an
|
|
16303
|
+
// unrecognised name may simply be missing from KNOWN_CLI_FLAGS and refusing would break working
|
|
16304
|
+
// invocations. With NO command there is nothing the flag could belong to, so it is a usage error.
|
|
16305
|
+
if (command === '') {
|
|
16306
|
+
const strayNames = unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS).map((n) => n.name);
|
|
16307
|
+
if (strayNames.length > 0) {
|
|
16308
|
+
writeErr(`dz: unknown option --${strayNames[0]} — run 'dz help' for usage`);
|
|
16309
|
+
return 2;
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
if (command === '' || command === 'help' || (flags.has('help') && DZ_COMMANDS.includes(command))) {
|
|
14879
16313
|
write(USAGE);
|
|
14880
16314
|
return 0;
|
|
14881
16315
|
}
|
|
@@ -14921,11 +16355,11 @@ export async function runCli(argv, io = {}) {
|
|
|
14921
16355
|
case 'bundle':
|
|
14922
16356
|
return cmdBundle(options, flags, cwd, write);
|
|
14923
16357
|
case 'teach':
|
|
14924
|
-
return await cmdTeach(options, flags, cwd, write);
|
|
16358
|
+
return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
|
|
14925
16359
|
case 'consolidate':
|
|
14926
16360
|
return await cmdConsolidate(options, flags, cwd, write);
|
|
14927
16361
|
case 'recall':
|
|
14928
|
-
return await cmdRecall(options, flags, cwd, write);
|
|
16362
|
+
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
14929
16363
|
case 'vector':
|
|
14930
16364
|
return await cmdVector(options, flags, cwd, write);
|
|
14931
16365
|
case 'brain':
|
|
@@ -14934,6 +16368,8 @@ export async function runCli(argv, io = {}) {
|
|
|
14934
16368
|
return cmdStatusline(options, flags, cwd, write, readStdin);
|
|
14935
16369
|
case 'usage':
|
|
14936
16370
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
16371
|
+
case 'chain':
|
|
16372
|
+
return cmdChain(options, flags, cwd, write);
|
|
14937
16373
|
case 'claim-check':
|
|
14938
16374
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
14939
16375
|
case 'lint':
|
|
@@ -14955,7 +16391,7 @@ export async function runCli(argv, io = {}) {
|
|
|
14955
16391
|
case 'diff':
|
|
14956
16392
|
return cmdDiff(options, cwd, write);
|
|
14957
16393
|
case 'recommend':
|
|
14958
|
-
return cmdRecommend(options, cwd, write);
|
|
16394
|
+
return cmdRecommend(options, flags, cwd, write);
|
|
14959
16395
|
case 'upgrade':
|
|
14960
16396
|
return cmdUpgrade(options, flags, cwd, write, writeErr);
|
|
14961
16397
|
case 'auto-canonicalize':
|
|
@@ -14978,6 +16414,8 @@ export async function runCli(argv, io = {}) {
|
|
|
14978
16414
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
14979
16415
|
case 'hooks-sync':
|
|
14980
16416
|
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
16417
|
+
case 'integrations-verify':
|
|
16418
|
+
return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
|
|
14981
16419
|
case 'agents-sync':
|
|
14982
16420
|
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
14983
16421
|
case 'sync-canonical':
|
|
@@ -15002,8 +16440,22 @@ export async function runCli(argv, io = {}) {
|
|
|
15002
16440
|
return cmdChallenge(options, flags, cwd, write);
|
|
15003
16441
|
case 'discrimination-check':
|
|
15004
16442
|
return cmdDiscriminationCheck(options, flags, cwd, write);
|
|
15005
|
-
case 'mutation-gate':
|
|
15006
|
-
|
|
16443
|
+
case 'mutation-gate': {
|
|
16444
|
+
try {
|
|
16445
|
+
return cmdMutationGate(options, flags, cwd, write, io.mutationGateRunner);
|
|
16446
|
+
}
|
|
16447
|
+
catch (error) {
|
|
16448
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
16449
|
+
const head = Array.from(raw.split(/\r?\n/, 1)[0]?.trim() || 'unknown internal error').slice(0, 160).join('');
|
|
16450
|
+
if (flags.has('json')) {
|
|
16451
|
+
write(JSON.stringify({ verdict: 'INCONCLUSIVE', reason: 'runner-internal-error', error: head, exitCode: 1 }));
|
|
16452
|
+
}
|
|
16453
|
+
else {
|
|
16454
|
+
write(`mutation-gate: INTERNAL ERROR (${head}) — verdict INCONCLUSIVE, exit 1`);
|
|
16455
|
+
}
|
|
16456
|
+
return 1;
|
|
16457
|
+
}
|
|
16458
|
+
}
|
|
15007
16459
|
case 'delivery-check':
|
|
15008
16460
|
return cmdDeliveryCheck(options, flags, cwd, write);
|
|
15009
16461
|
case 'skills-verify':
|
|
@@ -15057,9 +16509,8 @@ export async function runCli(argv, io = {}) {
|
|
|
15057
16509
|
case 'import-ecc':
|
|
15058
16510
|
return await cmdImportEcc(options, flags, cwd, write);
|
|
15059
16511
|
default:
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
return 1;
|
|
16512
|
+
writeErr(`dz: unknown command ${JSON.stringify(command)} — run 'dz help' for the command list`);
|
|
16513
|
+
return 2;
|
|
15063
16514
|
}
|
|
15064
16515
|
}
|
|
15065
16516
|
catch (error) {
|