@dzhechkov/harness-cli 0.8.25 → 0.8.27
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 +12 -12
- package/README.md +178 -10
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +386 -34
- package/dist/cli.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +7 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +11 -11
- package/src/cli.ts +386 -31
- package/src/known-flags.ts +7 -0
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import { parseNpmPackInventory } from '@dzhechkov/harness-core';
|
|
7
|
+
// Fix-round 1 (Codex HIGH-1c, feature recall-short-terms): the ONE place `dz recall` prints an
|
|
8
|
+
// empty result must name WHY — via the shared helper, not by re-deriving the decision. Routed
|
|
9
|
+
// through harness-core's re-export (lead correction) rather than a new direct dependency on
|
|
10
|
+
// `@dzhechkov/memory`: a new package-graph edge is a publishing-surface change outside this
|
|
11
|
+
// feature's scope, and harness-core already depends on memory.
|
|
12
|
+
import { noSearchableTermsReason } from '@dzhechkov/harness-core';
|
|
7
13
|
import { appendFileSync, chmodSync, closeSync, constants as fsConstants, cpSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
8
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, posix as nodePosixPath } from 'node:path';
|
|
15
|
+
const posixNormalize = nodePosixPath.normalize;
|
|
9
16
|
import { fileURLToPath } from 'node:url';
|
|
10
17
|
import { request as httpsRequest } from 'node:https';
|
|
11
18
|
import { KNOWN_CLI_FLAGS } from './known-flags.js';
|
|
@@ -14,15 +21,15 @@ import { resolveInstallSpec } from './install-spec.js';
|
|
|
14
21
|
import { dispatchedCommands, documentedCommands } from './command-inventory.js';
|
|
15
22
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
|
|
16
23
|
import { createHash, randomBytes } from 'node:crypto';
|
|
17
|
-
import { homedir, hostname, tmpdir } from 'node:os';
|
|
24
|
+
import { cpus, homedir, hostname, tmpdir } from 'node:os';
|
|
18
25
|
import { createRequire } from 'node:module';
|
|
19
26
|
import { isDeepStrictEqual } from 'node:util';
|
|
20
27
|
import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from '@dzhechkov/harness-core';
|
|
21
28
|
import { appendRunEvent, readRunRegistry, liveParents, liveness, probePid, settleDeadRuns, planRegistryArchive, planWorktreeCleanup, renderCleanupPlan, worktreeRemovalsToApply } from '@dzhechkov/harness-core';
|
|
22
29
|
import { openRound, closeRound, listRounds, parseCodexTokens, classifyRoundExecOutcome, buildRoundExecRow, } from '@dzhechkov/harness-core';
|
|
23
|
-
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, memoryBackendSourceLabel, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
30
|
+
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, loadSkillFromDir, 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, memoryBackendSourceLabel, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
24
31
|
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
|
|
25
|
-
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, computeParity, PARITY_FEATURES, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, applyLegStatus, applyLegReasonMessage, resolveAgentdbPath, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeSpendReport, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, spendReport, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, findExactLesson, recordToPattern, bundleSkills, brainHome, brainAgentdbPath, listPreReindexSnapshots, rotatePreReindexSnapshots, scanSnapshotDir, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
32
|
+
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, computeParity, PARITY_FEATURES, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, applyLegStatus, applyLegReasonMessage, probeApplyLeg, resolveAgentdbPath, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeSpendReport, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, spendReport, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, findExactLesson, recordToPattern, bundleSkills, brainHome, brainAgentdbPath, listPreReindexSnapshots, rotatePreReindexSnapshots, scanSnapshotDir, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
26
33
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
27
34
|
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, hashPackBytes, rewriteWorkspaceSpecs, detectSiblingDrift, planPackedInstallSmoke, judgePackedInstallSmoke, listPackFiles, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, resolveScanTailTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, runRetroTailScan, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
|
|
28
35
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
@@ -32,7 +39,7 @@ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGo
|
|
|
32
39
|
// qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
|
|
33
40
|
KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
|
|
34
41
|
// Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
|
|
35
|
-
REGISTRY_SELFCHECK_TESTS, buildMutationTestCommand, parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, readFeatureTier, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
42
|
+
REGISTRY_SELFCHECK_TESTS, buildMutationTestCommand, parseMutationRegistry, registryEntriesAddedSince, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, readFeatureTier, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
36
43
|
// operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
|
|
37
44
|
renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
|
|
38
45
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
@@ -142,7 +149,7 @@ Usage:
|
|
|
142
149
|
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)
|
|
143
150
|
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)
|
|
144
151
|
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)
|
|
145
|
-
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)
|
|
152
|
+
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--touched <path[,path]>] [--added-since <git-ref>] [--timeout <ms>] [--max-workers <n>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: '--max-workers' resolves flag > the registry's own 'maxWorkers' field > 'min(4, max(1, floor(cpus/2)))' (an invalid flag value — 0, negative, fractional, non-numeric — is a usage error, exit 2, never a silent default; fix-round 1), injects '--maxWorkers=<n>' right after 'vitest run' inside its own compound-command segment (unless that segment already names the flag; fix-round 1 — scoped detection, not a whole-command substring check) and sets 'VITEST_MAX_WORKERS=<n>' in the env regardless — an uncapped full-suite baseline/mutant run at vitest's default worker count (= cpu cores) has measured load 62-358 and <2GB free on an 8-core/16GB box under embedding-daemon tests, killing full overnight gate runs (0bb74d66); printed as 'mutation-gate: workers: <n> (<flag|registry|default>)', or 'mutation-gate: workers: n/a — test command is not vitest' when the command is not recognised as vitest. '--touched' selects entries whose 'file' matches one of the given paths, accepted in ANY of package-relative, './'-prefixed, absolute-inside-the-package, repo-relative, or backslash-separated form — all normalized to package-relative POSIX before matching (fix-round 1, AM-1); a path that resolves OUTSIDE the package is counted, never silently dropped, as '<K> outside package' in the 'selected N of M' line; '--added-since <ref>' selects entries whose id is not present in the registry as it read at that ref ('git show <ref>:<registry path>'): a registry genuinely ABSENT at that ref means every current entry counts as added (said explicitly); an unresolvable ref, any OTHER git failure, or an invalid/malformed base registry at that ref is a usage error (exit 2), never folded into "absent" (AM-3) — a feature scopes the gate to its own touched files and any entries it just added instead of the whole registry (MEASURED: an unscoped run over 358 entries on this repo's core package ran 30-40 minutes and hit the timeout wall, INCONCLUSIVE every time). The two selectors UNION and the result INTERSECTS with '--only' when both are given; an empty selection prints 'selected 0 of M entries (…)' and exits 0 — never a silent skip; '--json' always carries a 'selection' object ({selected, total, touched, addedSince, base, outsidePackage}) on every scoped run (AM-4). 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)
|
|
146
153
|
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--allow-cold-start] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
147
154
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
148
155
|
dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
|
|
@@ -1808,6 +1815,43 @@ async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
|
1808
1815
|
if (root.layout === 'npx-template' && root.hasCompanionAssets) {
|
|
1809
1816
|
write(` note: ${pkg} also ships commands/hooks/agents — \`npx -y ${pkg} init\` installs the full kit.`);
|
|
1810
1817
|
}
|
|
1818
|
+
// Junk-skip summary (feature skills-walk-symlinks-and-junk, FR-2/AC-3). `report.skills[].skipped`
|
|
1819
|
+
// above is a WRITE outcome (an existing file not overwritten without --force) — a different
|
|
1820
|
+
// question from "did this skill directory contain build/cache junk that never became an asset at
|
|
1821
|
+
// all". Re-walk each discovered skill (a cheap second READ — `loadSkillFromDir` already did this
|
|
1822
|
+
// once inside `runInit`'s `adapter.compile`, this adds no write) to surface that count without
|
|
1823
|
+
// reshaping `InitReport`. Silent at N=0 (AC-3): most packages ship no junk and must print nothing.
|
|
1824
|
+
// fix-round 1 MEDIUM-4: this counts ENTRIES, not files. A skipped junk DIRECTORY (e.g.
|
|
1825
|
+
// `__pycache__`) is exactly one entry here even though it may hold many files underneath —
|
|
1826
|
+
// `walkFiles` never descends into a skipped junk directory to count those (see skills.ts),
|
|
1827
|
+
// so a per-file count would be a number this code cannot honestly produce. The wording and
|
|
1828
|
+
// the trailing `/` on directory paths say so, instead of implying "file" for something that
|
|
1829
|
+
// may be a whole tree.
|
|
1830
|
+
let junkSkippedCount = 0;
|
|
1831
|
+
const junkSkippedPaths = [];
|
|
1832
|
+
for (const id of discoverSkillIds(root.dir)) {
|
|
1833
|
+
let skill;
|
|
1834
|
+
try {
|
|
1835
|
+
skill = loadSkillFromDir(root.dir, id);
|
|
1836
|
+
}
|
|
1837
|
+
catch {
|
|
1838
|
+
continue; // unparseable skills are reported separately via report.failures below
|
|
1839
|
+
}
|
|
1840
|
+
for (const entry of skill.skipped ?? []) {
|
|
1841
|
+
if (!entry.reason.startsWith('junk'))
|
|
1842
|
+
continue; // broken-symlink/cycle/escape skips are a different concern (FR-1/AM-8)
|
|
1843
|
+
junkSkippedCount += 1;
|
|
1844
|
+
const shownPath = relative(pkgDir, entry.path).split('\\').join('/');
|
|
1845
|
+
const isDir = entry.reason.startsWith('junk directory');
|
|
1846
|
+
junkSkippedPaths.push(isDir ? `${shownPath}/` : shownPath);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (junkSkippedCount > 0) {
|
|
1850
|
+
const shown = junkSkippedPaths.slice(0, 5).join(', ');
|
|
1851
|
+
const more = junkSkippedPaths.length > 5 ? `, … (+${junkSkippedPaths.length - 5} more)` : '';
|
|
1852
|
+
const noun = junkSkippedCount === 1 ? 'entry' : 'entries';
|
|
1853
|
+
write(`skills: skipped ${junkSkippedCount} junk ${noun} (${shown}${more})`);
|
|
1854
|
+
}
|
|
1811
1855
|
// Skip-and-collect at install time (D1 / the report's D2 amendment): the offending
|
|
1812
1856
|
// SKILL.md came out of the DOWNLOADED TARBALL, so the path is rendered relative to
|
|
1813
1857
|
// the package root (a `node_modules/**` absolute path is not actionable) and the
|
|
@@ -4244,7 +4288,19 @@ async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
|
|
|
4244
4288
|
}
|
|
4245
4289
|
if (hits.length === 0) {
|
|
4246
4290
|
write(`dz recall "${shownQuery}"`);
|
|
4247
|
-
|
|
4291
|
+
// Fix-round 1 (Codex HIGH-1c): a query that tokenizes to literally nothing (pure
|
|
4292
|
+
// punctuation/whitespace) is a DIFFERENT empty result than a query with real terms that
|
|
4293
|
+
// simply matched no record — FR-3 requires the reason to be named, not folded into the same
|
|
4294
|
+
// generic "no matching patterns" line. `noSearchableTermsReason` is the single source of
|
|
4295
|
+
// truth for the decision (memory/src/tokenize.ts); this print site calls it rather than
|
|
4296
|
+
// re-deriving "no searchable terms" from the query text itself.
|
|
4297
|
+
const noTermsReason = noSearchableTermsReason(query);
|
|
4298
|
+
if (noTermsReason !== undefined) {
|
|
4299
|
+
write(` no searchable terms in "${shownQuery}" (only punctuation/whitespace) — reason: ${noTermsReason}`);
|
|
4300
|
+
}
|
|
4301
|
+
else {
|
|
4302
|
+
write(` No matching patterns (teach some with \`dz teach\`, or consolidate sessions).`);
|
|
4303
|
+
}
|
|
4248
4304
|
// The domain note must print here too (Codex QE #10): a --domain run with zero hits
|
|
4249
4305
|
// silently said nothing about the domain, so the reader could not tell whether the
|
|
4250
4306
|
// boost had been applied and found nothing, or had not run at all.
|
|
@@ -7189,7 +7245,7 @@ npmPackRunner) {
|
|
|
7189
7245
|
/* dz parity — the honest feature×target map (target-parity-matrix, */
|
|
7190
7246
|
/* ADR-001): computed from the declarative model, never hand-written */
|
|
7191
7247
|
/* ------------------------------------------------------------------ */
|
|
7192
|
-
function cmdParity(options, flags, write, writeErr, cwd) {
|
|
7248
|
+
async function cmdParity(options, flags, write, writeErr, cwd) {
|
|
7193
7249
|
const json = flags.has('json');
|
|
7194
7250
|
if (flags.has('help')) {
|
|
7195
7251
|
write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
|
|
@@ -7217,32 +7273,56 @@ function cmdParity(options, flags, write, writeErr, cwd) {
|
|
|
7217
7273
|
return 1;
|
|
7218
7274
|
}
|
|
7219
7275
|
}
|
|
7220
|
-
// ADR-001 Decision 3 (feature setup-installs-apply-leg)
|
|
7221
|
-
// MEASURED, not declared — `hooks-prompt`
|
|
7222
|
-
//
|
|
7223
|
-
//
|
|
7224
|
-
// `applyLegStatus`
|
|
7225
|
-
//
|
|
7276
|
+
// ADR-001 Decision 3 (feature setup-installs-apply-leg), extended by `apply-leg-never-silent`
|
|
7277
|
+
// Decision 3 (FR-4): `learning-apply` on `claude-code` is MEASURED, not declared — `hooks-prompt`
|
|
7278
|
+
// is present for that ONE target only when the leg is OBSERVED to inject, not merely installed.
|
|
7279
|
+
// Issue #2 was exactly this cell reading `full` while the leg injected nothing in every session
|
|
7280
|
+
// but one: `applyLegStatus(root).installed` alone (file presence + structural wiring) is
|
|
7281
|
+
// necessary but not sufficient — `probeApplyLeg` is the live end-to-end proof. `computeParity`
|
|
7282
|
+
// itself is untouched (FR-5 of the earlier feature); only the capability SET fed into it for this
|
|
7283
|
+
// one cell differs from the static `TARGET_CAPABILITIES`, exactly as before this feature.
|
|
7284
|
+
// `applyLegStatus` never throws (fix round 1, Q3 finding); `probeApplyLeg` is only invoked when
|
|
7285
|
+
// `installed` is true — a leg with missing/stale/unreadable helpers has nothing live to probe,
|
|
7286
|
+
// and the pre-existing remedy for that state is unchanged.
|
|
7226
7287
|
const applyLegStatusVal = applyLegStatus(cwd);
|
|
7227
7288
|
const applyLegInstalled = applyLegStatusVal.installed;
|
|
7289
|
+
// Codex round-2: `probeApplyLeg` may REJECT (temp dir, dynamic import, a throwing remover) — doctor
|
|
7290
|
+
// catches that; parity must too, or a probe crash would crash `dz parity` instead of reading as
|
|
7291
|
+
// "installed but silent: probe-error".
|
|
7292
|
+
let applyLegProbe;
|
|
7293
|
+
if (applyLegInstalled) {
|
|
7294
|
+
try {
|
|
7295
|
+
applyLegProbe = await probeApplyLeg(cwd);
|
|
7296
|
+
}
|
|
7297
|
+
catch (err) {
|
|
7298
|
+
applyLegProbe = { ok: false, reason: `probe-error: ${err instanceof Error ? err.message : String(err)}`, elapsedMs: 0 };
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
const applyLegWorking = applyLegProbe?.ok === true;
|
|
7228
7302
|
const matrix = buildParityMatrix().map((row) => {
|
|
7229
|
-
if (row.feature.id !== 'learning-apply' ||
|
|
7303
|
+
if (row.feature.id !== 'learning-apply' || applyLegWorking)
|
|
7230
7304
|
return row;
|
|
7231
7305
|
const claudeCodeCaps = TARGET_CAPABILITIES['claude-code'].filter((c) => c !== 'hooks-prompt');
|
|
7232
7306
|
return { feature: row.feature, cells: { ...row.cells, 'claude-code': computeParity(row.feature, claudeCodeCaps) } };
|
|
7233
7307
|
});
|
|
7234
|
-
// The
|
|
7235
|
-
//
|
|
7236
|
-
//
|
|
7237
|
-
// function `dz doctor` uses for those two
|
|
7238
|
-
// the two instruments cannot disagree about WHY
|
|
7308
|
+
// The remedy — named ONLY for the one cell whose grant is a live measurement, never a blanket
|
|
7309
|
+
// note for every `manual` cell (most targets are manual by DESIGN, not absence). Three distinct
|
|
7310
|
+
// states, never conflated: NOT installed (stale-version/unreadable route through
|
|
7311
|
+
// `applyLegReasonMessage`, the SAME text-producing function `dz doctor` uses for those two
|
|
7312
|
+
// reasons, so the two instruments cannot disagree about WHY); installed but the live probe did
|
|
7313
|
+
// NOT observe injection (FR-4's new "installed but silent" case, reason from `probeApplyLeg`
|
|
7314
|
+
// itself — the SAME reason `dz doctor`'s live-probe row prints, so doctor and parity cannot
|
|
7315
|
+
// disagree about a dead leg either); and the working case, where this function returns ''.
|
|
7239
7316
|
const applyLegRemedy = (featureId, t) => {
|
|
7240
|
-
if (featureId !== 'learning-apply' || t !== 'claude-code' ||
|
|
7317
|
+
if (featureId !== 'learning-apply' || t !== 'claude-code' || applyLegWorking)
|
|
7241
7318
|
return '';
|
|
7242
|
-
if (
|
|
7243
|
-
|
|
7319
|
+
if (!applyLegInstalled) {
|
|
7320
|
+
if (applyLegStatusVal.reason === 'stale-version' || applyLegStatusVal.reason === 'unreadable') {
|
|
7321
|
+
return ` — ${applyLegReasonMessage(applyLegStatusVal)}`;
|
|
7322
|
+
}
|
|
7323
|
+
return ' — not installed — run dz setup --target claude-code --memory agentdb';
|
|
7244
7324
|
}
|
|
7245
|
-
return
|
|
7325
|
+
return ` — installed but silent: ${applyLegProbe?.reason ?? 'unknown'}`;
|
|
7246
7326
|
};
|
|
7247
7327
|
// EVIDENCE staleness, folded into the report (fix round 2, R2-3). Derived from the records
|
|
7248
7328
|
// themselves — no `codex --version`, no subprocess, so `dz parity` stays a deterministic function
|
|
@@ -12320,6 +12400,43 @@ function splitMutationGateOutputWrite(result) {
|
|
|
12320
12400
|
return {};
|
|
12321
12401
|
return 'path' in result ? { outputPath: result.path } : { outputError: result.error };
|
|
12322
12402
|
}
|
|
12403
|
+
/** Fix-round 1 (AM-1, feature qe-step-gate-scoped-to-feature): a `--touched` path arrives in one of
|
|
12404
|
+
* several shapes — package-relative POSIX (the common case, already correct as-is), with a leading
|
|
12405
|
+
* `./`, as an ABSOLUTE path inside the package, or REPO-relative (e.g.
|
|
12406
|
+
* `packages/@dzhechkov/harness-cli/src/x.ts`) — and on a POSIX host a caller may hand a backslash
|
|
12407
|
+
* path too (a Windows-authored change list). MEASURED (Codex round-1 review): the pre-fix
|
|
12408
|
+
* normalization only converted the native path separator, so any of the other shapes matched ZERO
|
|
12409
|
+
* registry entries and the gate silently reported `selected 0` / exit 0 — the run looked clean while
|
|
12410
|
+
* defending nothing. This resolves EVERY shape to the package-relative POSIX candidate(s) a registry
|
|
12411
|
+
* entry's `file` is written in, and — when a path genuinely resolves outside `pkgDir` (an absolute
|
|
12412
|
+
* path elsewhere, or a `../` that still escapes the package after a repo-relative reinterpretation)
|
|
12413
|
+
* — reports it as `outside`, never a silent non-match indistinguishable from "the path doesn't
|
|
12414
|
+
* exist".
|
|
12415
|
+
*/
|
|
12416
|
+
function normalizeTouchedPath(raw, pkgDir, repoTop) {
|
|
12417
|
+
// Codex round-2: normalize lexically FIRST so `lib/../../x` is seen as the traversal it is, and treat a
|
|
12418
|
+
// Windows-drive path (`C:/…`) on a POSIX host as outside the package (counted, said) rather than as a
|
|
12419
|
+
// package-relative name that silently matches nothing. Symlinked package dirs stay a named limit: the
|
|
12420
|
+
// containment check is lexical, not realpath-based.
|
|
12421
|
+
const p = posixNormalize(raw.replace(/\\/g, '/')).replace(/^(?:\.\/)+/, '');
|
|
12422
|
+
if (p === '' || p === '.')
|
|
12423
|
+
return { candidates: [], outside: false };
|
|
12424
|
+
if (/^[A-Za-z]:\//.test(p) && !isAbsolute(p))
|
|
12425
|
+
return { candidates: [], outside: true };
|
|
12426
|
+
if (isAbsolute(p)) {
|
|
12427
|
+
const rel = relative(pkgDir, resolve(p)).split(sep).join('/');
|
|
12428
|
+
return rel === '' || rel.startsWith('..') ? { candidates: [], outside: true } : { candidates: [rel], outside: false };
|
|
12429
|
+
}
|
|
12430
|
+
const candidates = new Set();
|
|
12431
|
+
if (p !== '..' && !p.startsWith('../'))
|
|
12432
|
+
candidates.add(p); // already package-relative, as given
|
|
12433
|
+
if (repoTop !== null) {
|
|
12434
|
+
const rel = relative(pkgDir, resolve(repoTop, p)).split(sep).join('/');
|
|
12435
|
+
if (rel !== '' && !rel.startsWith('..'))
|
|
12436
|
+
candidates.add(rel);
|
|
12437
|
+
}
|
|
12438
|
+
return candidates.size > 0 ? { candidates: [...candidates], outside: false } : { candidates: [], outside: true };
|
|
12439
|
+
}
|
|
12323
12440
|
function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
12324
12441
|
const json = flags.has('json');
|
|
12325
12442
|
const fail = (what) => {
|
|
@@ -12343,7 +12460,9 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12343
12460
|
}
|
|
12344
12461
|
let entries = parsed.registry.entries;
|
|
12345
12462
|
let entryResults = parsed.entryResults;
|
|
12463
|
+
const totalRegistryEntries = entries.length;
|
|
12346
12464
|
const only = options.get('only');
|
|
12465
|
+
let onlyIds = null;
|
|
12347
12466
|
if (only !== undefined) {
|
|
12348
12467
|
const ids = only.split(',').map((s) => s.trim()).filter(Boolean);
|
|
12349
12468
|
const known = new Set([
|
|
@@ -12353,15 +12472,139 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12353
12472
|
const unknown = ids.filter((id) => !known.has(id));
|
|
12354
12473
|
if (unknown.length > 0)
|
|
12355
12474
|
return fail(`--only names unknown entry id(s): ${unknown.join(', ')}`);
|
|
12475
|
+
onlyIds = new Set(ids);
|
|
12356
12476
|
entries = entries.filter((e) => ids.includes(e.id));
|
|
12357
12477
|
entryResults = entryResults.filter((result) => ids.includes(result.id));
|
|
12358
12478
|
}
|
|
12479
|
+
// Feature qe-step-gate-scoped-to-feature (FR-1/FR-2/FR-3): scope the gate to the files a FEATURE
|
|
12480
|
+
// actually touched and/or entries added since a base ref, instead of the whole registry — MEASURED
|
|
12481
|
+
// 2026-09-12, 358 entries on this repo's core package ran 30-40 minutes and hit the timeout wall,
|
|
12482
|
+
// INCONCLUSIVE every time, though a feature owns only its own touched files. `--touched` and
|
|
12483
|
+
// `--added-since` UNION (a file changed by the feature OR an entry it newly added is in scope);
|
|
12484
|
+
// that union then INTERSECTS with `--only` when both are given, same algebra as an ordinary filter
|
|
12485
|
+
// chain. Both selectors are computed against the FULL (pre-`--only`) registry so their reported
|
|
12486
|
+
// counts describe what THEY matched, independent of any `--only` narrowing applied on top.
|
|
12487
|
+
const touchedRaw = options.get('touched');
|
|
12488
|
+
const addedSinceRaw = options.get('added-since');
|
|
12489
|
+
let selectionMeta = null;
|
|
12490
|
+
if (touchedRaw !== undefined || addedSinceRaw !== undefined) {
|
|
12491
|
+
let repoTop = null;
|
|
12492
|
+
try {
|
|
12493
|
+
repoTop = execSync('git rev-parse --show-toplevel', { cwd: pkgDir, stdio: 'pipe', encoding: 'utf-8' }).trim() || null;
|
|
12494
|
+
}
|
|
12495
|
+
catch { /* not in a git repo */ }
|
|
12496
|
+
const touchedIds = new Set();
|
|
12497
|
+
let outsidePackageCount = 0;
|
|
12498
|
+
if (touchedRaw !== undefined) {
|
|
12499
|
+
const touchedPaths = touchedRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
12500
|
+
const normalizedCandidates = new Set();
|
|
12501
|
+
for (const raw of touchedPaths) {
|
|
12502
|
+
const { candidates, outside } = normalizeTouchedPath(raw, pkgDir, repoTop);
|
|
12503
|
+
if (outside)
|
|
12504
|
+
outsidePackageCount++;
|
|
12505
|
+
for (const c of candidates)
|
|
12506
|
+
normalizedCandidates.add(c);
|
|
12507
|
+
}
|
|
12508
|
+
for (const entry of parsed.registry.entries) {
|
|
12509
|
+
if (normalizedCandidates.has(entry.file))
|
|
12510
|
+
touchedIds.add(entry.id);
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
const addedSinceIds = new Set();
|
|
12514
|
+
let baseAbsentMessage = null;
|
|
12515
|
+
if (addedSinceRaw !== undefined) {
|
|
12516
|
+
if (repoTop === null)
|
|
12517
|
+
return fail(`--added-since requires ${pkgDir} to be inside a git repository`);
|
|
12518
|
+
try {
|
|
12519
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', `${addedSinceRaw}^{commit}`], { cwd: repoTop, stdio: 'pipe' });
|
|
12520
|
+
}
|
|
12521
|
+
catch {
|
|
12522
|
+
return fail(`--added-since names an unknown git ref: ${addedSinceRaw}`);
|
|
12523
|
+
}
|
|
12524
|
+
const registryRelPath = relative(repoTop, registryPath).split(sep).join('/');
|
|
12525
|
+
let baseRegistry = null;
|
|
12526
|
+
// Fix-round 1 (AM-3): only a genuinely ABSENT registry at that ref means "all entries are
|
|
12527
|
+
// new". Any OTHER `git show` failure (a bad path that isn't a missing-registry case, git
|
|
12528
|
+
// itself missing, a timeout) is a usage error, and a registry that parses to invalid JSON /
|
|
12529
|
+
// an invalid registry shape at that ref is a distinct usage error too — collapsing all three
|
|
12530
|
+
// into "absent" used to hide real failures behind a silently-too-generous selection.
|
|
12531
|
+
try {
|
|
12532
|
+
// stdio: registry-absent-at-ref is an EXPECTED outcome (git's own "fatal: path … does not
|
|
12533
|
+
// exist" on stderr would otherwise leak to the terminal by node's default inherit-stderr
|
|
12534
|
+
// behaviour) — pipe it into the caught error instead of printing it at the user.
|
|
12535
|
+
const baseText = execFileSync('git', ['show', `${addedSinceRaw}:${registryRelPath}`], { cwd: repoTop, encoding: 'utf-8', env: { ...process.env, LC_ALL: 'C', LANG: 'C' }, maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
12536
|
+
const baseParsed = parseMutationRegistry(baseText);
|
|
12537
|
+
if (baseParsed.registry === null) {
|
|
12538
|
+
return fail(`base registry at ${addedSinceRaw} is not a valid registry: ${baseParsed.errors.join('; ')}`);
|
|
12539
|
+
}
|
|
12540
|
+
baseRegistry = baseParsed.registry;
|
|
12541
|
+
}
|
|
12542
|
+
catch (e) {
|
|
12543
|
+
const stderr = typeof e?.stderr === 'string'
|
|
12544
|
+
? e.stderr
|
|
12545
|
+
: String(e?.message ?? e);
|
|
12546
|
+
if (/does not exist in|exists on disk, but not in|Path .* does not exist/i.test(stderr)) {
|
|
12547
|
+
baseRegistry = null; // registry absent at that ref → every current entry counts as added
|
|
12548
|
+
baseAbsentMessage = `base registry absent at ${addedSinceRaw} — all ${totalRegistryEntries} entries count as added`;
|
|
12549
|
+
}
|
|
12550
|
+
else {
|
|
12551
|
+
return fail(`git show ${addedSinceRaw}:${registryRelPath} failed: ${stderr.trim()}`);
|
|
12552
|
+
}
|
|
12553
|
+
}
|
|
12554
|
+
if (baseAbsentMessage !== null && !json)
|
|
12555
|
+
write(`mutation-gate: ${baseAbsentMessage}`);
|
|
12556
|
+
for (const id of registryEntriesAddedSince(baseRegistry, parsed.registry))
|
|
12557
|
+
addedSinceIds.add(id);
|
|
12558
|
+
}
|
|
12559
|
+
let unionIds = new Set([...touchedIds, ...addedSinceIds]);
|
|
12560
|
+
if (onlyIds !== null) {
|
|
12561
|
+
const only2 = onlyIds;
|
|
12562
|
+
unionIds = new Set([...unionIds].filter((id) => only2.has(id)));
|
|
12563
|
+
}
|
|
12564
|
+
const selectorParts = [];
|
|
12565
|
+
if (touchedRaw !== undefined)
|
|
12566
|
+
selectorParts.push(`touched: ${touchedIds.size}${outsidePackageCount > 0 ? ` (${outsidePackageCount} outside package)` : ''}`);
|
|
12567
|
+
if (addedSinceRaw !== undefined)
|
|
12568
|
+
selectorParts.push(`added-since ${addedSinceRaw}: ${addedSinceIds.size}`);
|
|
12569
|
+
if (!json)
|
|
12570
|
+
write(`mutation-gate: selected ${unionIds.size} of ${totalRegistryEntries} entries (${selectorParts.join('; ')})`);
|
|
12571
|
+
entries = entries.filter((e) => unionIds.has(e.id));
|
|
12572
|
+
entryResults = entryResults.filter((result) => unionIds.has(result.id));
|
|
12573
|
+
// AM-4: every scoped run — empty or not — carries a machine-readable `selection` object in the
|
|
12574
|
+
// JSON contract, not just the empty-selection early return.
|
|
12575
|
+
selectionMeta = {
|
|
12576
|
+
selected: unionIds.size,
|
|
12577
|
+
total: totalRegistryEntries,
|
|
12578
|
+
touched: touchedRaw !== undefined ? [...touchedIds].sort() : null,
|
|
12579
|
+
addedSince: addedSinceRaw !== undefined ? [...addedSinceIds].sort() : null,
|
|
12580
|
+
base: addedSinceRaw ?? null,
|
|
12581
|
+
baseAbsent: baseAbsentMessage !== null,
|
|
12582
|
+
outsidePackage: outsidePackageCount,
|
|
12583
|
+
};
|
|
12584
|
+
if (unionIds.size === 0) {
|
|
12585
|
+
const msg = `0 entries match ${selectorParts.join(', ')} — nothing to run`;
|
|
12586
|
+
if (json) {
|
|
12587
|
+
write(JSON.stringify({
|
|
12588
|
+
packageDir: pkgDir,
|
|
12589
|
+
registryPath,
|
|
12590
|
+
selection: selectionMeta,
|
|
12591
|
+
results: [],
|
|
12592
|
+
summary: summarizeMutationResults([]),
|
|
12593
|
+
exitCode: 0,
|
|
12594
|
+
}, null, 2));
|
|
12595
|
+
}
|
|
12596
|
+
else {
|
|
12597
|
+
write(`mutation-gate: ${msg}`);
|
|
12598
|
+
}
|
|
12599
|
+
return 0;
|
|
12600
|
+
}
|
|
12601
|
+
}
|
|
12359
12602
|
if (entries.length === 0) {
|
|
12360
12603
|
const scope = only === undefined ? 'registry' : 'selected registry entries';
|
|
12361
12604
|
const error = `${scope} has no runnable entries after validation — nothing can be run; the registry is unusable`;
|
|
12362
12605
|
const summary = summarizeMutationResults(entryResults);
|
|
12363
12606
|
if (json) {
|
|
12364
|
-
write(JSON.stringify({ error, registryPath, results: entryResults, summary, exitCode: 2 }, null, 2));
|
|
12607
|
+
write(JSON.stringify({ error, registryPath, ...(selectionMeta !== null ? { selection: selectionMeta } : {}), results: entryResults, summary, exitCode: 2 }, null, 2));
|
|
12365
12608
|
}
|
|
12366
12609
|
else {
|
|
12367
12610
|
write(`dz mutation-gate: ${error}`);
|
|
@@ -12374,13 +12617,98 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12374
12617
|
const testCmdRaw = options.get('test-cmd') ?? parsed.registry.testCommand ?? 'npm test';
|
|
12375
12618
|
if (/[\0\n\r]/.test(testCmdRaw))
|
|
12376
12619
|
return fail('--test-cmd may not contain NUL or newline characters');
|
|
12377
|
-
|
|
12620
|
+
let testCmd = testCmdRaw;
|
|
12378
12621
|
const excludedSelfChecks = REGISTRY_SELFCHECK_TESTS.filter((testFile) => entries.some((entry) => buildMutationTestCommand(testCmd, entry).excluded.includes(testFile)));
|
|
12379
12622
|
if (!json) {
|
|
12380
12623
|
write(`mutation-gate: self-check excluded from mutant runs: ${excludedSelfChecks.join(', ') || '(none)'}`);
|
|
12381
12624
|
}
|
|
12382
|
-
|
|
12383
|
-
|
|
12625
|
+
// mutation-gate-timeout-verdict FR-3/FR-4: precedence is the `--timeout` flag > the registry's
|
|
12626
|
+
// own `timeoutMs` field > the 300000ms default. A package whose real baseline run is longer than
|
|
12627
|
+
// the default (this repo's core package, MEASURED ≈5-8 min) declares its floor in the registry so
|
|
12628
|
+
// a bare `dz mutation-gate` — no flag — still succeeds (AC-5).
|
|
12629
|
+
const timeoutFlagRaw = options.get('timeout');
|
|
12630
|
+
let timeout;
|
|
12631
|
+
let timeoutSource;
|
|
12632
|
+
if (timeoutFlagRaw !== undefined) {
|
|
12633
|
+
const timeoutOpt = Number(timeoutFlagRaw);
|
|
12634
|
+
if (Number.isFinite(timeoutOpt) && timeoutOpt > 0) {
|
|
12635
|
+
timeout = timeoutOpt;
|
|
12636
|
+
timeoutSource = 'flag';
|
|
12637
|
+
}
|
|
12638
|
+
else {
|
|
12639
|
+
timeout = 300000;
|
|
12640
|
+
timeoutSource = 'default';
|
|
12641
|
+
}
|
|
12642
|
+
}
|
|
12643
|
+
else if (parsed.registry.timeoutMs !== undefined) {
|
|
12644
|
+
timeout = parsed.registry.timeoutMs;
|
|
12645
|
+
timeoutSource = 'registry';
|
|
12646
|
+
}
|
|
12647
|
+
else {
|
|
12648
|
+
timeout = 300000;
|
|
12649
|
+
timeoutSource = 'default';
|
|
12650
|
+
}
|
|
12651
|
+
if (!json)
|
|
12652
|
+
write(`mutation-gate: timeout: ${timeout} ms (${timeoutSource})`);
|
|
12653
|
+
// mutation-gate-baseline-honesty FR-2/FR-3: precedence is the `--max-workers` flag > the
|
|
12654
|
+
// registry's own `maxWorkers` field > `min(4, max(1, floor(cpus/2)))`. Baseline and mutant runs
|
|
12655
|
+
// spawn the package's FULL testCommand at vitest's DEFAULT worker count (= cpu cores) unless
|
|
12656
|
+
// capped — MEASURED: under embedding-daemon tests (0.7-3.5 GB/process) this repo's core package
|
|
12657
|
+
// hit load 62-358 and 0.4-1.8 GB free on an 8-core/16GB box, killing three full overnight runs
|
|
12658
|
+
// (0bb74d66); the same suite at `--maxWorkers=2` passed 6909/6909.
|
|
12659
|
+
const maxWorkersFlagRaw = options.get('max-workers');
|
|
12660
|
+
const defaultMaxWorkers = Math.min(4, Math.max(1, Math.floor(cpus().length / 2)));
|
|
12661
|
+
let maxWorkers;
|
|
12662
|
+
let maxWorkersSource;
|
|
12663
|
+
if (maxWorkersFlagRaw !== undefined) {
|
|
12664
|
+
// fix-round 1, AM-1: an invalid `--max-workers` value (0, negative, fractional, `NaN`,
|
|
12665
|
+
// non-numeric) is a USAGE ERROR (exit 2) — mirroring the `--only` unknown-id fail() style —
|
|
12666
|
+
// never a silent fallback to the default. A silent fallback would make a typo'd flag run
|
|
12667
|
+
// uncapped-by-mistake while LOOKING capped (the source line still says "(default)").
|
|
12668
|
+
const maxWorkersOpt = Number(maxWorkersFlagRaw);
|
|
12669
|
+
if (!Number.isInteger(maxWorkersOpt) || maxWorkersOpt < 1) {
|
|
12670
|
+
return fail(`--max-workers must be a positive integer, got '${maxWorkersFlagRaw}'`);
|
|
12671
|
+
}
|
|
12672
|
+
maxWorkers = maxWorkersOpt;
|
|
12673
|
+
maxWorkersSource = 'flag';
|
|
12674
|
+
}
|
|
12675
|
+
else if (parsed.registry.maxWorkers !== undefined) {
|
|
12676
|
+
maxWorkers = parsed.registry.maxWorkers;
|
|
12677
|
+
maxWorkersSource = 'registry';
|
|
12678
|
+
}
|
|
12679
|
+
else {
|
|
12680
|
+
maxWorkers = defaultMaxWorkers;
|
|
12681
|
+
maxWorkersSource = 'default';
|
|
12682
|
+
}
|
|
12683
|
+
// FR-3: inject the ceiling into the command ONLY when it is (detectably) a vitest run and does
|
|
12684
|
+
// not already name the flag itself — an arbitrary testCommand cannot be assumed to accept
|
|
12685
|
+
// `--maxWorkers`. VITEST_MAX_WORKERS is set in the env unconditionally (below, at spawn time)
|
|
12686
|
+
// regardless of this detection, so a vitest command reached indirectly (e.g. through a package
|
|
12687
|
+
// script) is still capped.
|
|
12688
|
+
// fix-round 1, AM-3: detection AND injection are scoped to the VITEST SEGMENT — from the first
|
|
12689
|
+
// `vitest run` token to the next `&&`/`||`/`;`/`|` (or end of string) — and the flag lands right
|
|
12690
|
+
// after `vitest run`, never appended to the tail of a whole (possibly compound) command. A
|
|
12691
|
+
// raw-substring append over the FULL command turned `vitest run … && cleanup` into
|
|
12692
|
+
// `vitest run … && cleanup --maxWorkers=2` (silently handed to `cleanup`, not vitest), and the
|
|
12693
|
+
// existing-flag check could be suppressed by an unrelated `--maxWorkers` substring living outside
|
|
12694
|
+
// the vitest segment entirely (e.g. inside `cleanup`'s own args, or before `vitest run` in the
|
|
12695
|
+
// same command).
|
|
12696
|
+
const vitestRunIdx = testCmd.indexOf('vitest run');
|
|
12697
|
+
const isVitestCommand = vitestRunIdx !== -1;
|
|
12698
|
+
if (isVitestCommand) {
|
|
12699
|
+
const tailFromRun = testCmd.slice(vitestRunIdx);
|
|
12700
|
+
const terminator = /&&|\|\||;|\|/.exec(tailFromRun);
|
|
12701
|
+
const vitestSegment = terminator !== null ? tailFromRun.slice(0, terminator.index) : tailFromRun;
|
|
12702
|
+
if (!vitestSegment.includes('--maxWorkers')) {
|
|
12703
|
+
const insertAt = vitestRunIdx + 'vitest run'.length;
|
|
12704
|
+
testCmd = `${testCmd.slice(0, insertAt)} --maxWorkers=${maxWorkers}${testCmd.slice(insertAt)}`;
|
|
12705
|
+
}
|
|
12706
|
+
}
|
|
12707
|
+
if (!json) {
|
|
12708
|
+
write(isVitestCommand
|
|
12709
|
+
? `mutation-gate: workers: ${maxWorkers} (${maxWorkersSource})`
|
|
12710
|
+
: 'mutation-gate: workers: n/a — test command is not vitest');
|
|
12711
|
+
}
|
|
12384
12712
|
// Route-b guard mode: `per-entry` (default, strongest — each red entry re-baselines the restored
|
|
12385
12713
|
// tree, so a flaky neighbour flips THAT entry to INCONCLUSIVE) or `final` (cheap — one re-run at
|
|
12386
12714
|
// the end; if it is not green, every red-based verdict of the run is downgraded, because any of
|
|
@@ -12457,22 +12785,29 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12457
12785
|
const realScratchRoot = realpathSync(copyDir);
|
|
12458
12786
|
const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
|
|
12459
12787
|
const invokeSuite = (suiteCommand, phase, entryId) => {
|
|
12788
|
+
// FR-3: VITEST_MAX_WORKERS is set in any case — regardless of whether the command was
|
|
12789
|
+
// recognised as vitest and got the `--maxWorkers=<n>` flag injected — so a vitest command
|
|
12790
|
+
// reached indirectly (a wrapper script) is still capped.
|
|
12791
|
+
const extraEnv = { VITEST_MAX_WORKERS: String(maxWorkers) };
|
|
12460
12792
|
if (injectedRunner !== undefined) {
|
|
12461
12793
|
return injectedRunner(suiteCommand, {
|
|
12462
12794
|
cwd: copyDir,
|
|
12463
12795
|
timeoutMs: timeout,
|
|
12464
12796
|
phase,
|
|
12797
|
+
env: extraEnv,
|
|
12465
12798
|
...(entryId !== undefined ? { entryId } : {}),
|
|
12466
12799
|
});
|
|
12467
12800
|
}
|
|
12801
|
+
const startedAt = performance.now();
|
|
12468
12802
|
const run = spawnSync(suiteCommand, {
|
|
12469
12803
|
cwd: copyDir,
|
|
12470
12804
|
shell: true,
|
|
12471
12805
|
encoding: 'utf-8',
|
|
12472
12806
|
timeout,
|
|
12473
12807
|
maxBuffer: 64 * 1024 * 1024,
|
|
12474
|
-
env: { ...process.env, FORCE_COLOR: '0' },
|
|
12808
|
+
env: { ...process.env, FORCE_COLOR: '0', ...extraEnv },
|
|
12475
12809
|
});
|
|
12810
|
+
const elapsedMs = Math.round(performance.now() - startedAt);
|
|
12476
12811
|
const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
|
|
12477
12812
|
? run.error.code
|
|
12478
12813
|
: undefined;
|
|
@@ -12483,11 +12818,28 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12483
12818
|
throw run.error;
|
|
12484
12819
|
}
|
|
12485
12820
|
const signal = typeof run.signal === 'string' ? run.signal : undefined;
|
|
12821
|
+
// mutation-gate-timeout-verdict FR-1/FR-2: a genuine ETIMEDOUT must NEVER be read as a real
|
|
12822
|
+
// suite verdict, even when the killed child intercepted the kill signal and exited with its
|
|
12823
|
+
// OWN status (e.g. a SIGTERM handler calling `process.exit(1)`) — MEASURED: node still sets
|
|
12824
|
+
// `run.error.code === 'ETIMEDOUT'` in that case, but a numeric `run.status` used to win the
|
|
12825
|
+
// `typeof run.status !== 'number'` check below, so the timeout was silently reported as
|
|
12826
|
+
// "baseline suite RED (exit 1)", indistinguishable from a real red suite. ETIMEDOUT now short
|
|
12827
|
+
// -circuits to `exitCode: null` unconditionally, before that check runs.
|
|
12828
|
+
if (errorCode === 'ETIMEDOUT') {
|
|
12829
|
+
// Codex round-1 (2026-09-14): report BOTH facts when both exist — a trapped SIGTERM that
|
|
12830
|
+
// exits 1 shows `child exit 1; signal SIGTERM`, a plain kill shows `no exit code; signal …`.
|
|
12831
|
+
const exitPart = typeof run.status === 'number' ? `child exit ${run.status}` : 'child produced no exit code';
|
|
12832
|
+
const childExit = signal === undefined ? exitPart : `${exitPart}; signal ${signal}`;
|
|
12833
|
+
const suggestedMs = Math.max(timeout + 1, Math.ceil(elapsedMs * 2));
|
|
12834
|
+
return {
|
|
12835
|
+
exitCode: null,
|
|
12836
|
+
output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
|
|
12837
|
+
failureReason: `timeout after ${timeout}ms (elapsed ${elapsedMs}ms); ${childExit}; try --timeout ${suggestedMs} or the registry's timeoutMs field`,
|
|
12838
|
+
};
|
|
12839
|
+
}
|
|
12486
12840
|
let failureReason;
|
|
12487
12841
|
if (typeof run.status !== 'number') {
|
|
12488
|
-
if (errorCode === '
|
|
12489
|
-
failureReason = `timeout after ${timeout}ms${signal === undefined ? '' : `; signal=${signal}`}`;
|
|
12490
|
-
else if (errorCode === 'ENOBUFS')
|
|
12842
|
+
if (errorCode === 'ENOBUFS')
|
|
12491
12843
|
failureReason = 'maxBuffer exceeded (ENOBUFS; 67108864-byte output ceiling)';
|
|
12492
12844
|
else if (signal !== undefined)
|
|
12493
12845
|
failureReason = `child killed by signal ${signal}`;
|
|
@@ -12749,7 +13101,7 @@ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
|
12749
13101
|
}
|
|
12750
13102
|
const exitCode = mutationGateExitCode(results, baseline.ok);
|
|
12751
13103
|
if (json) {
|
|
12752
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
|
|
13104
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, ...(selectionMeta !== null ? { selection: selectionMeta } : {}), exitCode }, null, 2));
|
|
12753
13105
|
return exitCode;
|
|
12754
13106
|
}
|
|
12755
13107
|
write(renderMutationReport(results, baseline, pkgDir));
|
|
@@ -20126,7 +20478,7 @@ export async function runCli(argv, io = {}) {
|
|
|
20126
20478
|
case 'release':
|
|
20127
20479
|
return cmdRelease(options, flags, cwd, write, io.releaseRunner);
|
|
20128
20480
|
case 'parity':
|
|
20129
|
-
return cmdParity(options, flags, write, writeErr, cwd);
|
|
20481
|
+
return await cmdParity(options, flags, write, writeErr, cwd);
|
|
20130
20482
|
case 'registry':
|
|
20131
20483
|
return cmdRegistry(options, cwd, write);
|
|
20132
20484
|
case 'benchmark':
|