@dzhechkov/harness-cli 0.8.5 → 0.8.9
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 +128 -19
- 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 +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1220 -130
- 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 +24 -21
- package/sbom.json +24 -24
- package/src/boolean-flags.ts +1 -0
- package/src/cli.ts +1246 -120
- 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
|
@@ -14,11 +14,12 @@ import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_p
|
|
|
14
14
|
import { createHash, randomBytes } from 'node:crypto';
|
|
15
15
|
import { homedir, tmpdir } from 'node:os';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
|
-
import {
|
|
17
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
18
|
+
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
18
19
|
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
|
|
19
|
-
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, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
20
21
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
21
|
-
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, 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,
|
|
22
23
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
23
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
25
|
// Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
|
|
@@ -39,7 +40,7 @@ export const DZ_COMMANDS = [
|
|
|
39
40
|
'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
|
|
40
41
|
'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
|
|
41
42
|
'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
|
|
42
|
-
'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
|
|
43
|
+
'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
|
|
43
44
|
'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
|
|
44
45
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
45
46
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
@@ -51,7 +52,7 @@ export const DZ_COMMANDS = [
|
|
|
51
52
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
52
53
|
|
|
53
54
|
Usage:
|
|
54
|
-
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)
|
|
55
56
|
dz verify [--skills-dir <dir>] [--target <name>]
|
|
56
57
|
dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
|
|
57
58
|
dz update (alias of sync)
|
|
@@ -80,7 +81,7 @@ Usage:
|
|
|
80
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)
|
|
81
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)
|
|
82
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)
|
|
83
|
-
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)
|
|
84
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)
|
|
85
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)
|
|
86
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)
|
|
@@ -115,13 +116,13 @@ Usage:
|
|
|
115
116
|
dz backlog ship <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (mark work DONE: new|enriched|in-progress → shipped, removing it from the roulette pool — run it after finishing a task; short id prefixes ok, ambiguous = loud error)
|
|
116
117
|
dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
|
|
117
118
|
dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run] [--project <dir>] [--json] (rewrite ONE idea's text, preserving every other field; re-embeds the dedup vector, and on a failed re-embed MARKS the record embedStale so dedup refuses to trust it — previous text preserved in .dz/backlog/edits.jsonl)
|
|
118
|
-
dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis
|
|
119
|
+
dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis + current/STALE/UNFED store receipt; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
|
|
119
120
|
dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
|
|
120
121
|
dz backlog enrich <id> [--project <dir>] [--json] (stage the idea2prd input scaffold in features/<slug>/ and hand off to the idea2prd-manual skill — the CLI never fabricates a PRD)
|
|
121
122
|
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)
|
|
122
123
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
123
124
|
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)
|
|
124
|
-
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
|
|
125
|
+
dz teach "<pattern>" [--class-form "<template with :slot>"] [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (class form is optional; rejection never blocks the specific write; --project pins the learned store to <dir>/.dz)
|
|
125
126
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
126
127
|
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)
|
|
127
128
|
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)
|
|
@@ -160,6 +161,7 @@ Usage:
|
|
|
160
161
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
161
162
|
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)
|
|
162
163
|
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)
|
|
164
|
+
dz integrations-verify --target <name> --component <mcp|hooks> [--project <dir>] [--json] (non-executing exact-version registration probe; exit 0 observed / 1 refused)
|
|
163
165
|
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)
|
|
164
166
|
dz plugin [--version <ver>]
|
|
165
167
|
dz downloads
|
|
@@ -271,8 +273,8 @@ async function installSkills(opts) {
|
|
|
271
273
|
// route them through one aggregation.
|
|
272
274
|
if (target === 'agents-md' || target === 'gemini') {
|
|
273
275
|
const report = target === 'gemini'
|
|
274
|
-
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
|
|
275
|
-
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) });
|
|
276
|
+
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) })
|
|
277
|
+
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) });
|
|
276
278
|
const results = report.skills.map((s) => ({
|
|
277
279
|
id: s.id,
|
|
278
280
|
written: s.written.length,
|
|
@@ -284,12 +286,20 @@ async function installSkills(opts) {
|
|
|
284
286
|
written += s.written;
|
|
285
287
|
skipped += s.skipped;
|
|
286
288
|
}
|
|
287
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
|
|
289
|
+
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 } : {}) };
|
|
288
290
|
}
|
|
289
291
|
const results = [];
|
|
290
292
|
const failures = [];
|
|
291
293
|
const applyFailures = [];
|
|
292
|
-
|
|
294
|
+
let integrations = [];
|
|
295
|
+
let integrationDigest;
|
|
296
|
+
const integrationManifestSources = skillsDirs.flatMap((skillsDir) => {
|
|
297
|
+
const discovered = discoverSkillIds(skillsDir);
|
|
298
|
+
return discovered
|
|
299
|
+
.filter((id) => select === undefined || select.includes(id))
|
|
300
|
+
.map((skillId) => ({ skillId, skillDir: skillsDir }));
|
|
301
|
+
});
|
|
302
|
+
for (const [dirIndex, skillsDir] of skillsDirs.entries()) {
|
|
293
303
|
const r = await runInit({
|
|
294
304
|
target,
|
|
295
305
|
skillsDir,
|
|
@@ -297,12 +307,27 @@ async function installSkills(opts) {
|
|
|
297
307
|
force,
|
|
298
308
|
enrich,
|
|
299
309
|
...(select !== undefined ? { select } : {}),
|
|
310
|
+
...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}),
|
|
311
|
+
...(dirIndex === 0
|
|
312
|
+
? {
|
|
313
|
+
integrationManifestSources,
|
|
314
|
+
...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}),
|
|
315
|
+
}
|
|
316
|
+
: { noIntegrations: true }),
|
|
317
|
+
...(opts.noVerify !== undefined ? { noVerify: opts.noVerify } : {}),
|
|
318
|
+
...(opts.allowIntegrations !== undefined ? { allowIntegrations: opts.allowIntegrations } : {}),
|
|
300
319
|
});
|
|
301
320
|
for (const skill of r.skills) {
|
|
302
321
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
303
322
|
}
|
|
304
323
|
failures.push(...r.failures);
|
|
305
324
|
applyFailures.push(...r.applyFailures);
|
|
325
|
+
if (r.integrations.some((row) => row.status !== 'not-requested'))
|
|
326
|
+
integrations = [...r.integrations];
|
|
327
|
+
else if (integrations.length === 0)
|
|
328
|
+
integrations = [...r.integrations];
|
|
329
|
+
if (r.integrationDigest !== undefined)
|
|
330
|
+
integrationDigest = r.integrationDigest;
|
|
306
331
|
}
|
|
307
332
|
let written = 0;
|
|
308
333
|
let skipped = 0;
|
|
@@ -312,7 +337,7 @@ async function installSkills(opts) {
|
|
|
312
337
|
}
|
|
313
338
|
const installed = new Set(results.map((s) => s.id));
|
|
314
339
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
315
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
|
|
340
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures, integrations, ...(integrationDigest !== undefined ? { integrationDigest } : {}) };
|
|
316
341
|
}
|
|
317
342
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
318
343
|
function writeMissingSkillsHint(write, missing, presetName) {
|
|
@@ -375,17 +400,15 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
375
400
|
select,
|
|
376
401
|
force: flags.has('force'),
|
|
377
402
|
enrich: flags.has('enrich'),
|
|
403
|
+
noHooks: flags.has('no-hooks'),
|
|
404
|
+
noIntegrations: flags.has('no-integrations'),
|
|
405
|
+
noVerify: flags.has('no-verify'),
|
|
406
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
378
407
|
});
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
writeMissingSkillsHint(write, r.missing, presetName);
|
|
384
|
-
// Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
|
|
385
|
-
// target's harness: the veto + recall legs are what `--target codex` promises.
|
|
386
|
-
// `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
|
|
387
|
-
// carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
|
|
388
|
-
// must be able to say so — and every test that is about skills says it.
|
|
408
|
+
// Codex keeps its established user-registry writer, but its result is normalized into the same
|
|
409
|
+
// two-outcome contract before JSON/human rendering. A write without a live ready observation is
|
|
410
|
+
// a refusal with applied=true, never a second success channel.
|
|
411
|
+
let integrationOutcomes = r.integrations;
|
|
389
412
|
let codexHooksOk = true;
|
|
390
413
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
391
414
|
const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
|
|
@@ -394,7 +417,30 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
394
417
|
write(line);
|
|
395
418
|
for (const line of delivery.stderr)
|
|
396
419
|
writeErr(line);
|
|
420
|
+
const hookIndex = integrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
421
|
+
if (hookIndex !== -1) {
|
|
422
|
+
const hook = normalizeCodexHookOutcome(integrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
|
|
423
|
+
integrationOutcomes = integrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
424
|
+
}
|
|
397
425
|
}
|
|
426
|
+
if (flags.has('json')) {
|
|
427
|
+
write(JSON.stringify({ target, skills: { count: r.results.length, written: r.written, skipped: r.skipped }, integrations: integrationOutcomes, integrationDigest: r.integrationDigest ?? null }));
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
|
|
431
|
+
if (r.dirsSearched > 1)
|
|
432
|
+
write(` (searched ${r.dirsSearched} skill directories)`);
|
|
433
|
+
for (const outcome of integrationOutcomes) {
|
|
434
|
+
const label = outcome.component === 'mcp' ? 'MCP' : 'Hooks';
|
|
435
|
+
const detail = outcome.status === 'refused'
|
|
436
|
+
? `${outcome.reasonCode ?? 'LIVE_PROBE_FAILED'}${outcome.remediation ? ` — ${outcome.remediation}` : ''}`
|
|
437
|
+
: outcome.status === 'emitted'
|
|
438
|
+
? `${outcome.carrier?.path ?? 'registered'}${outcome.registrations.some((row) => row.approval === 'pending') ? '; Pending approval; ready=false' : ''}`
|
|
439
|
+
: 'explicitly not requested';
|
|
440
|
+
write(`${label}: ${outcome.status.toUpperCase()} (${detail})`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
writeMissingSkillsHint(write, r.missing, presetName);
|
|
398
444
|
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
399
445
|
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
400
446
|
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
@@ -417,7 +463,38 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
417
463
|
writeErr(line);
|
|
418
464
|
return 1;
|
|
419
465
|
}
|
|
420
|
-
|
|
466
|
+
const integrationOk = !integrationOutcomes.some((row) => row.status === 'refused');
|
|
467
|
+
return codexHooksOk && integrationOk ? 0 : 1;
|
|
468
|
+
}
|
|
469
|
+
function cmdIntegrationsVerify(options, flags, cwd, write, writeErr) {
|
|
470
|
+
const targetInput = options.get('target');
|
|
471
|
+
const component = options.get('component');
|
|
472
|
+
if (targetInput === undefined || (component !== 'mcp' && component !== 'hooks')) {
|
|
473
|
+
writeErr('dz integrations-verify: requires --target <name> --component <mcp|hooks>');
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
476
|
+
const resolution = resolveTargetName(targetInput);
|
|
477
|
+
if (resolution.kind === 'unknown') {
|
|
478
|
+
for (const line of formatTargetProblem('dz integrations-verify', resolution))
|
|
479
|
+
writeErr(line);
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
if (resolution.via === 'alias')
|
|
483
|
+
writeErr(formatTargetAliasNote('dz integrations-verify', targetInput, resolution.target));
|
|
484
|
+
const result = runIntegrationsVerify({
|
|
485
|
+
target: resolution.target,
|
|
486
|
+
component,
|
|
487
|
+
projectRoot: resolve(cwd, options.get('project') ?? '.'),
|
|
488
|
+
});
|
|
489
|
+
if (flags.has('json'))
|
|
490
|
+
write(JSON.stringify(result));
|
|
491
|
+
else if (result.ok) {
|
|
492
|
+
write(`dz integrations-verify: ${resolution.target}/${component} registered (runtime ${result.runtimeVersion ?? 'unknown'}; ready=${result.registrations.every((row) => row.ready === true)})`);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
writeErr(`dz integrations-verify: ${resolution.target}/${component} REFUSED (${result.reasonCode ?? 'LIVE_PROBE_FAILED'}) — ${result.remediation ?? 'no qualifying receipt'}`);
|
|
496
|
+
}
|
|
497
|
+
return result.ok ? 0 : 1;
|
|
421
498
|
}
|
|
422
499
|
async function cmdVerify(options, cwd, write, writeErr) {
|
|
423
500
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
@@ -1869,6 +1946,150 @@ function cmdStatuslineFaRecord(options, cwd, write) {
|
|
|
1869
1946
|
write(`dz statusline: recorded /feature-adr learning state for "${slug}" (${step}) — 🎓 ${state.pool} pool · ↑${state.recalled} used · +${state.stored} new · ↻${state.reinforced ?? 0} reinforced`);
|
|
1870
1947
|
return 0;
|
|
1871
1948
|
}
|
|
1949
|
+
function statuslineEtaStage(step) {
|
|
1950
|
+
const match = step.match(/\bStep\s*-?\s*(0|[1-9](?:\.5)?)(?:\b|\s)/i);
|
|
1951
|
+
if (match === null)
|
|
1952
|
+
return undefined;
|
|
1953
|
+
const value = Number(match[1]);
|
|
1954
|
+
if (value === 0)
|
|
1955
|
+
return 'router';
|
|
1956
|
+
if (value > 0 && value < 6)
|
|
1957
|
+
return 'design';
|
|
1958
|
+
if (value === 6)
|
|
1959
|
+
return 'plan';
|
|
1960
|
+
if (value === 7)
|
|
1961
|
+
return 'code';
|
|
1962
|
+
if (value === 8)
|
|
1963
|
+
return 'qe';
|
|
1964
|
+
if (value === 9)
|
|
1965
|
+
return 'fleet';
|
|
1966
|
+
return undefined;
|
|
1967
|
+
}
|
|
1968
|
+
function routerMetadata(router) {
|
|
1969
|
+
const result = router?.result;
|
|
1970
|
+
if (result === null || typeof result !== 'object')
|
|
1971
|
+
return {};
|
|
1972
|
+
const record = result;
|
|
1973
|
+
const tier = typeof record['tier'] === 'string' && record['tier'].length > 0 ? record['tier'] : undefined;
|
|
1974
|
+
const activeSteps = Array.isArray(record['activeSteps'])
|
|
1975
|
+
? record['activeSteps'].filter((value) => typeof value === 'number' && Number.isFinite(value))
|
|
1976
|
+
: undefined;
|
|
1977
|
+
return {
|
|
1978
|
+
...(tier !== undefined ? { tier } : {}),
|
|
1979
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function activeCheckpointStages(tier, activeSteps) {
|
|
1983
|
+
const stages = new Set();
|
|
1984
|
+
if (activeSteps !== undefined) {
|
|
1985
|
+
for (const step of activeSteps) {
|
|
1986
|
+
if (step === 0)
|
|
1987
|
+
stages.add('router');
|
|
1988
|
+
else if (step > 0 && step < 6)
|
|
1989
|
+
stages.add('design');
|
|
1990
|
+
else if (step === 6)
|
|
1991
|
+
stages.add('plan');
|
|
1992
|
+
else if (step === 7)
|
|
1993
|
+
stages.add('code');
|
|
1994
|
+
else if (step === 8)
|
|
1995
|
+
stages.add('qe');
|
|
1996
|
+
else if (step === 9)
|
|
1997
|
+
stages.add('fleet');
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
else if (tier !== undefined) {
|
|
2001
|
+
for (const stage of CHECKPOINT_STAGES) {
|
|
2002
|
+
if (stage !== 'fleet' || tier === 'L' || tier === 'XL')
|
|
2003
|
+
stages.add(stage);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return CHECKPOINT_STAGES.filter((stage) => stages.has(stage));
|
|
2007
|
+
}
|
|
2008
|
+
/** All checkpoint I/O for ETA lives in this CLI-only wrapper and degrades per file. */
|
|
2009
|
+
function readEtaCorpus(projectRoot, currentSlug) {
|
|
2010
|
+
const trace = process.env['DZ_ETA_TRACE'];
|
|
2011
|
+
if (trace !== undefined && trace.length > 0) {
|
|
2012
|
+
try {
|
|
2013
|
+
appendFileSync(trace, `${JSON.stringify({ projectRoot, currentSlug, ts: new Date().toISOString() })}\n`);
|
|
2014
|
+
}
|
|
2015
|
+
catch { /* a test/debug receipt can never take down the statusline */ }
|
|
2016
|
+
}
|
|
2017
|
+
const samples = [];
|
|
2018
|
+
let currentSegments = [];
|
|
2019
|
+
let hasCurrentCheckpoints = false;
|
|
2020
|
+
let entries = [];
|
|
2021
|
+
try {
|
|
2022
|
+
entries = readdirSync(join(projectRoot, 'features'), { withFileTypes: true });
|
|
2023
|
+
}
|
|
2024
|
+
catch {
|
|
2025
|
+
return {
|
|
2026
|
+
history: [], currentRunSamples: [], currentTier: undefined, activeStages: [],
|
|
2027
|
+
lastCheckpointTsMs: undefined, hasCurrentCheckpoints: false,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
for (const entry of entries) {
|
|
2031
|
+
if (!entry.isDirectory())
|
|
2032
|
+
continue;
|
|
2033
|
+
const checkpointPath = join(projectRoot, 'features', entry.name, '.fa-state', 'checkpoints.jsonl');
|
|
2034
|
+
try {
|
|
2035
|
+
const text = readFileSync(checkpointPath, 'utf8');
|
|
2036
|
+
const observations = parseCheckpointLines(text, entry.name);
|
|
2037
|
+
const segments = segmentRun(observations);
|
|
2038
|
+
samples.push(...extractStageSamples(segments));
|
|
2039
|
+
if (entry.name === currentSlug) {
|
|
2040
|
+
hasCurrentCheckpoints = observations.length > 0;
|
|
2041
|
+
currentSegments = segments;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
catch {
|
|
2045
|
+
// Missing, unreadable, or racing append: this run contributes no evidence.
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const lastSegment = currentSegments.at(-1);
|
|
2049
|
+
const currentRouter = lastSegment?.router;
|
|
2050
|
+
const invocationSegments = currentRouter === undefined
|
|
2051
|
+
? []
|
|
2052
|
+
: currentSegments.filter((segment) => segment.router === currentRouter);
|
|
2053
|
+
const invocationRunIds = new Set(invocationSegments.map((segment) => segment.runId));
|
|
2054
|
+
const currentRunSamples = extractStageSamples(invocationSegments);
|
|
2055
|
+
const metadata = routerMetadata(currentRouter);
|
|
2056
|
+
const currentTimestamps = invocationSegments
|
|
2057
|
+
.flatMap((segment) => segment.observations)
|
|
2058
|
+
.flatMap((observation) => observation.tsMs === undefined ? [] : [observation.tsMs]);
|
|
2059
|
+
return {
|
|
2060
|
+
history: samples.filter((sample) => !invocationRunIds.has(sample.runId)),
|
|
2061
|
+
currentRunSamples,
|
|
2062
|
+
currentTier: metadata.tier,
|
|
2063
|
+
activeStages: activeCheckpointStages(metadata.tier, metadata.activeSteps),
|
|
2064
|
+
lastCheckpointTsMs: currentTimestamps.length > 0 ? Math.max(...currentTimestamps) : undefined,
|
|
2065
|
+
hasCurrentCheckpoints,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
function statuslineEta(projectRoot, state, nowMs) {
|
|
2069
|
+
const currentStage = statuslineEtaStage(state.step);
|
|
2070
|
+
if (currentStage === undefined)
|
|
2071
|
+
return undefined;
|
|
2072
|
+
const corpus = readEtaCorpus(projectRoot, state.slug);
|
|
2073
|
+
const currentIndex = CHECKPOINT_STAGES.indexOf(currentStage);
|
|
2074
|
+
const remainingStages = corpus.activeStages.filter((stage) => CHECKPOINT_STAGES.indexOf(stage) >= currentIndex && stage !== 'router');
|
|
2075
|
+
// Preserve honest absence as a machine-readable union member. The text formatter intentionally
|
|
2076
|
+
// omits both variants, but `--json` must still distinguish no file from a file with no tier.
|
|
2077
|
+
const stagesForEstimate = remainingStages.length > 0
|
|
2078
|
+
? remainingStages
|
|
2079
|
+
: (currentStage === 'router' ? [] : [currentStage]);
|
|
2080
|
+
if (stagesForEstimate.length === 0 && corpus.hasCurrentCheckpoints && corpus.currentTier !== undefined)
|
|
2081
|
+
return undefined;
|
|
2082
|
+
return estimateEta({
|
|
2083
|
+
samples: corpus.history,
|
|
2084
|
+
currentTier: corpus.currentTier,
|
|
2085
|
+
currentStage,
|
|
2086
|
+
remainingStages: stagesForEstimate,
|
|
2087
|
+
currentRunSamples: corpus.currentRunSamples,
|
|
2088
|
+
nowMs,
|
|
2089
|
+
...(corpus.lastCheckpointTsMs !== undefined ? { lastCheckpointTsMs: corpus.lastCheckpointTsMs } : {}),
|
|
2090
|
+
hasCurrentCheckpoints: corpus.hasCurrentCheckpoints,
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
1872
2093
|
/**
|
|
1873
2094
|
* `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
|
|
1874
2095
|
* Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
|
|
@@ -1888,8 +2109,23 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
1888
2109
|
try {
|
|
1889
2110
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
1890
2111
|
const data = statuslineData(projectRoot);
|
|
2112
|
+
const fa = data.featureAdr;
|
|
2113
|
+
let eta;
|
|
2114
|
+
let etaFragment;
|
|
2115
|
+
if (fa !== undefined && fa.kind !== 'loop') {
|
|
2116
|
+
try {
|
|
2117
|
+
eta = statuslineEta(projectRoot, fa, Date.now());
|
|
2118
|
+
if (eta !== undefined)
|
|
2119
|
+
etaFragment = formatEta(eta);
|
|
2120
|
+
}
|
|
2121
|
+
catch {
|
|
2122
|
+
// ETA is advisory: any corpus/resource failure omits only this fragment.
|
|
2123
|
+
eta = undefined;
|
|
2124
|
+
etaFragment = undefined;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
1891
2127
|
if (flags.has('json')) {
|
|
1892
|
-
write(JSON.stringify(data));
|
|
2128
|
+
write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
|
|
1893
2129
|
return 0;
|
|
1894
2130
|
}
|
|
1895
2131
|
let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
|
|
@@ -1899,14 +2135,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
|
|
|
1899
2135
|
if (data.consolidatedAgeH !== undefined)
|
|
1900
2136
|
line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
1901
2137
|
// Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
|
|
1902
|
-
const fa = data.featureAdr;
|
|
1903
2138
|
if (fa !== undefined) {
|
|
1904
2139
|
// 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.
|
|
1905
2140
|
if (fa.kind === 'loop') {
|
|
1906
2141
|
line = `🔁 loop ${fa.step} · ${line}`;
|
|
1907
2142
|
}
|
|
1908
2143
|
else {
|
|
1909
|
-
line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2144
|
+
line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
1910
2145
|
}
|
|
1911
2146
|
}
|
|
1912
2147
|
write(line);
|
|
@@ -2493,7 +2728,12 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
|
|
|
2493
2728
|
const reason = teachReasonPhrase(target.reason);
|
|
2494
2729
|
return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
|
|
2495
2730
|
}
|
|
2496
|
-
async function
|
|
2731
|
+
async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
|
|
2732
|
+
const backend = resolveLearningBackend(projectRoot);
|
|
2733
|
+
backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
|
|
2734
|
+
return backend.train();
|
|
2735
|
+
}
|
|
2736
|
+
async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
|
|
2497
2737
|
// WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
|
|
2498
2738
|
// `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
|
|
2499
2739
|
// per-session choice; for a CLI every invocation is a fresh process, so the only honest session
|
|
@@ -2584,26 +2824,39 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2584
2824
|
write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
|
|
2585
2825
|
return 1;
|
|
2586
2826
|
}
|
|
2587
|
-
const
|
|
2827
|
+
const importedKey = (p) => p.lessonForm !== undefined && p.lessonPairId !== undefined
|
|
2828
|
+
? `${p.pattern}\u0000${p.lessonForm}\u0000${p.lessonPairId}`
|
|
2829
|
+
: `legacy\u0000${p.pattern}`;
|
|
2830
|
+
const existing = new Set(loadStorePatternsSync(storeRoot).map(importedKey));
|
|
2588
2831
|
let imported = 0;
|
|
2589
2832
|
let skipped = 0;
|
|
2590
2833
|
const importedRecs = [];
|
|
2591
2834
|
for (const item of parsed) {
|
|
2592
2835
|
const p = item;
|
|
2593
|
-
if (!p || typeof p
|
|
2836
|
+
if (!p || typeof p !== 'object') {
|
|
2594
2837
|
skipped += 1;
|
|
2595
2838
|
continue;
|
|
2596
2839
|
}
|
|
2597
|
-
const
|
|
2598
|
-
|
|
2840
|
+
const pair = (p.lessonForm === 'specific' || p.lessonForm === 'class')
|
|
2841
|
+
&& typeof p.lessonPairId === 'string' && p.lessonPairId !== ''
|
|
2842
|
+
? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
|
|
2843
|
+
: {};
|
|
2844
|
+
const candidate = {
|
|
2845
|
+
pattern: typeof p.pattern === 'string' ? p.pattern : '',
|
|
2599
2846
|
type: (typeof p.type === 'string' ? p.type : 'lesson-learned'),
|
|
2600
2847
|
reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
|
|
2601
2848
|
domain: typeof p.domain === 'string' ? p.domain : 'general',
|
|
2602
2849
|
ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
|
|
2603
2850
|
source: 'dz-teach-import',
|
|
2851
|
+
...pair,
|
|
2604
2852
|
};
|
|
2853
|
+
if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
|
|
2854
|
+
skipped += 1;
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2857
|
+
const rec = candidate;
|
|
2605
2858
|
await recordPattern(storeRoot, rec);
|
|
2606
|
-
existing.add(
|
|
2859
|
+
existing.add(importedKey(rec));
|
|
2607
2860
|
importedRecs.push(rec);
|
|
2608
2861
|
imported += 1;
|
|
2609
2862
|
}
|
|
@@ -2686,23 +2939,32 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2686
2939
|
}
|
|
2687
2940
|
const reward = parseFloat(options.get('reward') ?? '0.8');
|
|
2688
2941
|
const domain = options.get('domain') ?? 'general';
|
|
2942
|
+
const classWasRequested = options.has('class-form') || flags.has('class-form');
|
|
2943
|
+
const lessonForms = normalizeLessonForms(pattern, options.get('class-form'));
|
|
2944
|
+
let guardedExisting;
|
|
2689
2945
|
if (flags.has('guard')) {
|
|
2690
|
-
const verdict = await
|
|
2946
|
+
const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
|
|
2691
2947
|
if (verdict.action === 'reinforce') {
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2948
|
+
if (lessonForms.classForm !== undefined) {
|
|
2949
|
+
const existing = loadStoreRecords(storeRoot).find((record) => record.id === verdict.dzId);
|
|
2950
|
+
if (existing !== undefined) {
|
|
2951
|
+
guardedExisting = { dzId: verdict.dzId, cosine: verdict.cosine, pattern: recordToPattern(existing) };
|
|
2952
|
+
}
|
|
2953
|
+
else {
|
|
2954
|
+
write(`dz teach --guard: matched pattern ${verdict.dzId} was not found in the lexical store — teaching the lesson normally`);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
else {
|
|
2958
|
+
const trained = await reinforceRunner(storeRoot, verdict.dzId, Math.max(0, Math.min(1, reward)));
|
|
2959
|
+
if (trained.flushed > 0) {
|
|
2960
|
+
write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
|
|
2961
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
|
|
2962
|
+
if (clearedQ.cleared > 0)
|
|
2963
|
+
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
2964
|
+
return 0;
|
|
2965
|
+
}
|
|
2966
|
+
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
2704
2967
|
}
|
|
2705
|
-
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
2706
2968
|
}
|
|
2707
2969
|
}
|
|
2708
2970
|
// Distill pattern into actionable rule (claude-smart inspired)
|
|
@@ -2715,8 +2977,8 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2715
2977
|
// Typed through the shared schema (harness-core owns PatternRecord) so the
|
|
2716
2978
|
// write side and the read side (recommend's loadPatterns) can never drift —
|
|
2717
2979
|
// a field rename here is a compile error, not a silently re-muted loop (audit #2).
|
|
2718
|
-
const entry = {
|
|
2719
|
-
pattern,
|
|
2980
|
+
const entry = guardedExisting?.pattern ?? {
|
|
2981
|
+
pattern: lessonForms.specific,
|
|
2720
2982
|
type: type,
|
|
2721
2983
|
reward: Math.max(0, Math.min(1, reward)),
|
|
2722
2984
|
domain,
|
|
@@ -2728,9 +2990,47 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2728
2990
|
// total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
|
|
2729
2991
|
// lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
|
|
2730
2992
|
const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2993
|
+
const stored = await recordLessonForms(storeRoot, entry, lessonForms.classForm, quarantineOn ? { quarantine: true } : {});
|
|
2994
|
+
let recordsToMirror = stored.records;
|
|
2995
|
+
let commandFailed = stored.class === 'failed';
|
|
2996
|
+
let reinforced = false;
|
|
2997
|
+
if (guardedExisting !== undefined) {
|
|
2998
|
+
let reinforceId = guardedExisting.dzId;
|
|
2999
|
+
if (stored.class === 'stored') {
|
|
3000
|
+
const specificRow = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3001
|
+
if (specificRow !== undefined)
|
|
3002
|
+
reinforceId = patternRecordId(specificRow);
|
|
3003
|
+
if (reinforceId !== guardedExisting.dzId) {
|
|
3004
|
+
const removed = removePatternsByIds(storeRoot, new Set([guardedExisting.dzId]));
|
|
3005
|
+
if (removed.error !== undefined || removed.removed === 0) {
|
|
3006
|
+
commandFailed = true;
|
|
3007
|
+
writeErr(`dz teach --guard: class pair stored, but old pattern cleanup failed${removed.error === undefined ? '' : ` — ${removed.error}`}`);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
else if (stored.class === 'failed') {
|
|
3012
|
+
const partialSpecific = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3013
|
+
if (partialSpecific !== undefined && patternRecordId(partialSpecific) !== guardedExisting.dzId) {
|
|
3014
|
+
const rolledBack = removePatternsByIds(storeRoot, new Set([patternRecordId(partialSpecific)]));
|
|
3015
|
+
if (rolledBack.error !== undefined)
|
|
3016
|
+
writeErr(`dz teach --guard: partial enrichment rollback failed — ${rolledBack.error}`);
|
|
3017
|
+
}
|
|
3018
|
+
recordsToMirror = [];
|
|
3019
|
+
reinforceId = guardedExisting.dzId;
|
|
3020
|
+
}
|
|
3021
|
+
if (!commandFailed) {
|
|
3022
|
+
const trained = await reinforceRunner(storeRoot, reinforceId, Math.max(0, Math.min(1, reward)));
|
|
3023
|
+
reinforced = trained.flushed > 0;
|
|
3024
|
+
if (reinforced) {
|
|
3025
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [reinforceId]);
|
|
3026
|
+
if (clearedQ.cleared > 0)
|
|
3027
|
+
write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
const count = guardedExisting === undefined ? stored.count : loadStorePatternsSync(storeRoot).length;
|
|
3032
|
+
write(`${guardedExisting !== undefined && stored.class === 'stored' ? 'Enriched' : 'Learned'}: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
|
|
3033
|
+
write(` Domain: ${entry.domain} Reward: ${entry.reward} Backend: memory (@dzhechkov/memory)`);
|
|
2734
3034
|
write(` Total patterns: ${count}`);
|
|
2735
3035
|
// WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
|
|
2736
3036
|
// domain, the reward and the backend — and not one word about the path, so a store written to
|
|
@@ -2740,6 +3040,24 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2740
3040
|
// sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
|
|
2741
3041
|
// nothing, so the line stays byte-identical for everyone who set nothing.
|
|
2742
3042
|
write(storeLine('written'));
|
|
3043
|
+
if (stored.class === 'stored')
|
|
3044
|
+
write(' Class form: stored separately and linked to the specific lesson');
|
|
3045
|
+
if (classWasRequested && stored.class === 'absent')
|
|
3046
|
+
write(' Class form: skipped; the specific lesson was saved');
|
|
3047
|
+
if (stored.class === 'rejected')
|
|
3048
|
+
writeErr(`dz teach: class form rejected — ${stored.reason ?? 'invalid class form'}; the specific lesson was saved`);
|
|
3049
|
+
if (stored.class === 'failed')
|
|
3050
|
+
writeErr(`dz teach: specific stored; class failed — ${stored.reason ?? 'unknown storage failure'}`);
|
|
3051
|
+
if (guardedExisting !== undefined && stored.class === 'stored') {
|
|
3052
|
+
write(commandFailed
|
|
3053
|
+
? ' Guard: class enrichment stored; reinforcement skipped because old-pattern cleanup failed'
|
|
3054
|
+
: reinforced
|
|
3055
|
+
? ` Guard: enriched existing pattern (cos=${guardedExisting.cosine.toFixed(2)}), then reinforced the linked specific form`
|
|
3056
|
+
: ' Guard: class enrichment stored; reinforcement did not flush');
|
|
3057
|
+
}
|
|
3058
|
+
if (!classWasRequested && interactive) {
|
|
3059
|
+
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.');
|
|
3060
|
+
}
|
|
2743
3061
|
// ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
|
|
2744
3062
|
// directories and this binary; refusing would be defending a user against themselves,
|
|
2745
3063
|
// which this design does not attempt. Making the choice INFORMED is the part that is
|
|
@@ -2758,8 +3076,8 @@ async function cmdTeach(options, flags, cwd, write) {
|
|
|
2758
3076
|
write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
|
|
2759
3077
|
}
|
|
2760
3078
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
2761
|
-
await emitMirrorQ(storeRoot,
|
|
2762
|
-
return 0;
|
|
3079
|
+
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3080
|
+
return commandFailed ? 1 : 0;
|
|
2763
3081
|
}
|
|
2764
3082
|
async function cmdConsolidate(options, flags, cwd, write) {
|
|
2765
3083
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
@@ -3119,7 +3437,7 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
|
|
|
3119
3437
|
write(` not found: ${res.notFound.join(', ')}`);
|
|
3120
3438
|
return 0;
|
|
3121
3439
|
}
|
|
3122
|
-
async function cmdRecall(options, flags, cwd, write) {
|
|
3440
|
+
async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
|
|
3123
3441
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3124
3442
|
const asJson = flags.has('json');
|
|
3125
3443
|
const all = flags.has('all');
|
|
@@ -3140,7 +3458,15 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3140
3458
|
// review could not make correct: a tag set by the writer is decidable, prose is not.
|
|
3141
3459
|
const allPatterns = loadStorePatternsSync(projectRoot);
|
|
3142
3460
|
const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
|
|
3143
|
-
const
|
|
3461
|
+
const classByPair = new Map(holdout.exported
|
|
3462
|
+
.filter((row) => row.lessonForm === 'class' && row.lessonPairId !== undefined)
|
|
3463
|
+
.map((row) => [row.lessonPairId, row.pattern]));
|
|
3464
|
+
const patterns = holdout.exported.map((row) => {
|
|
3465
|
+
const classForm = row.lessonForm === 'specific' && row.lessonPairId !== undefined
|
|
3466
|
+
? classByPair.get(row.lessonPairId)
|
|
3467
|
+
: undefined;
|
|
3468
|
+
return classForm === undefined ? row : { ...row, classForm };
|
|
3469
|
+
});
|
|
3144
3470
|
const holdoutNote = renderHoldoutNote(holdout);
|
|
3145
3471
|
// The opt-in is honoured without argument — and named out loud. A flag that silently
|
|
3146
3472
|
// includes medical lessons in a portable export is a flag whose consequence the user
|
|
@@ -3170,7 +3496,15 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3170
3496
|
const rawStats = storeStats(projectRoot);
|
|
3171
3497
|
const topHoldout = applyExportHoldout(rawStats.topUses, heldOutAfterOptIn(options.get('include-domain')));
|
|
3172
3498
|
const perDomain = Object.fromEntries(Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))));
|
|
3173
|
-
const
|
|
3499
|
+
const generalizedPairs = new Set(patterns.flatMap((p) => p.lessonPairId === undefined ? [] : [p.lessonPairId]));
|
|
3500
|
+
const unpairedLessons = patterns.filter((p) => p.lessonPairId === undefined).length;
|
|
3501
|
+
const stats = {
|
|
3502
|
+
...rawStats,
|
|
3503
|
+
topUses: topHoldout.exported,
|
|
3504
|
+
perDomain,
|
|
3505
|
+
generalized: generalizedPairs.size,
|
|
3506
|
+
logicalLessons: generalizedPairs.size + unpairedLessons,
|
|
3507
|
+
};
|
|
3174
3508
|
const backendStats = resolveLearningBackend(projectRoot).getStats();
|
|
3175
3509
|
if (asJson) {
|
|
3176
3510
|
write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
|
|
@@ -3184,6 +3518,7 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3184
3518
|
write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
|
|
3185
3519
|
write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
|
|
3186
3520
|
write(` exact-dup groups: ${stats.exactDupGroups}`);
|
|
3521
|
+
write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
|
|
3187
3522
|
write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
|
|
3188
3523
|
write(' top uses:');
|
|
3189
3524
|
for (const row of stats.topUses)
|
|
@@ -3311,6 +3646,10 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3311
3646
|
? 'lexical'
|
|
3312
3647
|
: 'hybrid';
|
|
3313
3648
|
const wantedDomain = options.get('domain');
|
|
3649
|
+
const classRecallOptions = {
|
|
3650
|
+
onClassDegraded: writeErr,
|
|
3651
|
+
...(classMatcher === undefined ? {} : { classMatcher }),
|
|
3652
|
+
};
|
|
3314
3653
|
// OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
|
|
3315
3654
|
// truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
|
|
3316
3655
|
// never receive its promised lift — the feature was weakest in exactly the case it
|
|
@@ -3333,8 +3672,8 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3333
3672
|
// second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
|
|
3334
3673
|
// own output is written after it returns.
|
|
3335
3674
|
const result = asJson
|
|
3336
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3337
|
-
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3675
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3676
|
+
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3338
3677
|
if (mode === 'semantic' && result.vectorEngine === 'none') {
|
|
3339
3678
|
// --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
|
|
3340
3679
|
const why = result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb';
|
|
@@ -3368,13 +3707,27 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3368
3707
|
let globalHits = [];
|
|
3369
3708
|
if (readGlobal) {
|
|
3370
3709
|
const g = asJson
|
|
3371
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3372
|
-
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3710
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3711
|
+
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3373
3712
|
globalHits = g.hits;
|
|
3374
3713
|
}
|
|
3714
|
+
const projectHits = boost ? boost.hits : result.hits;
|
|
3715
|
+
// TWO different keys on purpose, and they must not be unified. The cross-store MERGE key is the
|
|
3716
|
+
// lesson TEXT: the same lesson taught into the project store and the global store is one lesson
|
|
3717
|
+
// shown once, labelled `both`, and those two records legitimately differ in timestamp, reward and
|
|
3718
|
+
// domain. The matchedForm LOOKUP key may be narrower. Folding the merge key into
|
|
3719
|
+
// patternIdentityOf (which includes ts) made two `dz teach` calls of identical text stop
|
|
3720
|
+
// collapsing — it reddened the pre-existing P3 case in recall-reads-both-stores.test.ts.
|
|
3721
|
+
const mergeKey = (hit) => hit.pattern.pattern;
|
|
3722
|
+
const globalByIdentity = new Map(globalHits.map((hit) => [mergeKey(hit), hit]));
|
|
3375
3723
|
const merged = readGlobal
|
|
3376
|
-
? mergeStoreHits(
|
|
3377
|
-
|
|
3724
|
+
? mergeStoreHits(projectHits, globalHits, mergeKey).map((hit) => {
|
|
3725
|
+
if (hit.origin !== 'both')
|
|
3726
|
+
return hit;
|
|
3727
|
+
const matchedForm = mergeLessonMatchedForms(hit.matchedForm, globalByIdentity.get(mergeKey(hit))?.matchedForm);
|
|
3728
|
+
return matchedForm === undefined ? hit : { ...hit, matchedForm };
|
|
3729
|
+
})
|
|
3730
|
+
: projectHits;
|
|
3378
3731
|
const hits = merged.slice(0, limit);
|
|
3379
3732
|
// Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
|
|
3380
3733
|
// paths that return earlier — `--json` and the zero-hits branch — still reported success. That
|
|
@@ -3421,6 +3774,7 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3421
3774
|
// version emitted the number while its own comment promised null (found by independent review).
|
|
3422
3775
|
write(JSON.stringify(hits.map((h) => ({
|
|
3423
3776
|
...h.pattern,
|
|
3777
|
+
...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
|
|
3424
3778
|
relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
|
|
3425
3779
|
// The TRUE cosine, as a second companion key — the design named it and the first ship missed
|
|
3426
3780
|
// it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
|
|
@@ -3487,6 +3841,10 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3487
3841
|
// guessing which second one answered.
|
|
3488
3842
|
write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
|
|
3489
3843
|
}
|
|
3844
|
+
const classMatches = hits.filter((h) => h.matchedForm === 'class' || h.matchedForm === 'both').length;
|
|
3845
|
+
if (hits.some((h) => h.pattern.classForm !== undefined)) {
|
|
3846
|
+
write(` class-form matches: ${classMatches} of ${hits.length}`);
|
|
3847
|
+
}
|
|
3490
3848
|
let sawQuarantined = false;
|
|
3491
3849
|
for (const h of hits) {
|
|
3492
3850
|
const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
|
|
@@ -3512,9 +3870,23 @@ async function cmdRecall(options, flags, cwd, write) {
|
|
|
3512
3870
|
// 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
|
|
3513
3871
|
// hidden remainder, and the cosine then appears to describe the visible fragment rather than
|
|
3514
3872
|
// the whole lesson. `--full` prints it all, still on one line.
|
|
3515
|
-
const
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3873
|
+
const specificText = oneLine(h.pattern.pattern);
|
|
3874
|
+
const classText = h.pattern.classForm === undefined ? undefined : oneLine(h.pattern.classForm);
|
|
3875
|
+
let shown = specificText;
|
|
3876
|
+
if (classText !== undefined) {
|
|
3877
|
+
const suffix = ` [match: ${h.matchedForm ?? 'specific'}]`;
|
|
3878
|
+
if (flags.has('full')) {
|
|
3879
|
+
shown = `specific: ${specificText} · class: ${classText}${suffix}`;
|
|
3880
|
+
}
|
|
3881
|
+
else {
|
|
3882
|
+
const contentWidth = Math.max(2, 160 - 'specific: '.length - ' · class: '.length - suffix.length);
|
|
3883
|
+
const specificWidth = Math.floor(contentWidth / 2);
|
|
3884
|
+
shown = `specific: ${specificText.slice(0, specificWidth)} · class: ${classText.slice(0, contentWidth - specificWidth)}${suffix}`;
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
else if (!flags.has('full')) {
|
|
3888
|
+
shown = shown.slice(0, 160);
|
|
3889
|
+
}
|
|
3518
3890
|
// WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
|
|
3519
3891
|
// blindness the store-location line just removed, one level down: the reader would see more
|
|
3520
3892
|
// results and have no way to tell whether the global store is even connected.
|
|
@@ -4523,7 +4895,13 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4523
4895
|
? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
4524
4896
|
: getPreset(preset)?.skills;
|
|
4525
4897
|
const install = select !== undefined && select.length > 0
|
|
4526
|
-
? await installSkills({
|
|
4898
|
+
? await installSkills({
|
|
4899
|
+
target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select,
|
|
4900
|
+
force: flags.has('force'), enrich: flags.has('enrich'), noHooks: flags.has('no-hooks'),
|
|
4901
|
+
noIntegrations: flags.has('no-integrations'),
|
|
4902
|
+
noVerify: flags.has('no-verify'),
|
|
4903
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
4904
|
+
})
|
|
4527
4905
|
: undefined;
|
|
4528
4906
|
if (install) {
|
|
4529
4907
|
write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
|
|
@@ -4533,6 +4911,7 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4533
4911
|
}
|
|
4534
4912
|
// Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
|
|
4535
4913
|
// setup has already run and the summary still prints; only the exit code carries the failure.
|
|
4914
|
+
let setupIntegrationOutcomes = install?.integrations ?? [];
|
|
4536
4915
|
let codexHooksOk = true;
|
|
4537
4916
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
4538
4917
|
write(`║ 5. Delivering codex hooks (live verify)... ║`);
|
|
@@ -4542,6 +4921,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4542
4921
|
write(line);
|
|
4543
4922
|
for (const line of delivery.stderr)
|
|
4544
4923
|
writeErr(line);
|
|
4924
|
+
const hookIndex = setupIntegrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
4925
|
+
if (hookIndex !== -1) {
|
|
4926
|
+
const hook = normalizeCodexHookOutcome(setupIntegrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
|
|
4927
|
+
setupIntegrationOutcomes = setupIntegrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
4928
|
+
}
|
|
4545
4929
|
}
|
|
4546
4930
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
4547
4931
|
write(`║ Setup: ${String(setupResult.completed).padStart(2)} done, ${String(setupResult.skipped).padStart(2)} skipped ║`);
|
|
@@ -4559,6 +4943,9 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4559
4943
|
if (install) {
|
|
4560
4944
|
write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
|
|
4561
4945
|
+ (install.dirsSearched > 1 ? ` (searched ${install.dirsSearched} skill dirs)` : ''));
|
|
4946
|
+
for (const outcome of setupIntegrationOutcomes) {
|
|
4947
|
+
write(`dz setup integration ${outcome.component}: ${outcome.status.toUpperCase()}${outcome.reasonCode ? ` (${outcome.reasonCode})` : ''}`);
|
|
4948
|
+
}
|
|
4562
4949
|
writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
|
|
4563
4950
|
}
|
|
4564
4951
|
// A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
|
|
@@ -4573,7 +4960,8 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4573
4960
|
if (erroredSteps.length > 0) {
|
|
4574
4961
|
write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
|
|
4575
4962
|
}
|
|
4576
|
-
|
|
4963
|
+
const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
|
|
4964
|
+
return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
|
|
4577
4965
|
}
|
|
4578
4966
|
function cmdPretrain(options, cwd, write) {
|
|
4579
4967
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
@@ -5227,6 +5615,9 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
5227
5615
|
write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
|
|
5228
5616
|
else
|
|
5229
5617
|
write('dz publish: ✓ guard pre-flight passed');
|
|
5618
|
+
for (const observation of guardResult.observations ?? []) {
|
|
5619
|
+
write(`dz publish: ℹ guard observation — ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`);
|
|
5620
|
+
}
|
|
5230
5621
|
for (const n of guardResult.notes ?? [])
|
|
5231
5622
|
write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
|
|
5232
5623
|
}
|
|
@@ -6903,6 +7294,26 @@ export function codexHooksSyncOptions(input) {
|
|
|
6903
7294
|
verify: input.verify !== false,
|
|
6904
7295
|
};
|
|
6905
7296
|
}
|
|
7297
|
+
/** Map the retained Codex hook writer's one live verdict into the common integration contract. */
|
|
7298
|
+
export function normalizeCodexHookOutcome(base, delivery, noVerify = false) {
|
|
7299
|
+
if (delivery.report.ready) {
|
|
7300
|
+
return {
|
|
7301
|
+
target: 'codex', component: 'hooks', status: 'emitted',
|
|
7302
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: true }],
|
|
7303
|
+
carrier: { scope: 'user', path: '$CODEX_HOME/hooks.json' },
|
|
7304
|
+
...(delivery.report.codexVersion !== null ? { runtimeVersion: delivery.report.codexVersion } : {}),
|
|
7305
|
+
evidenceVersion: 'codex-hooks-live-v1',
|
|
7306
|
+
};
|
|
7307
|
+
}
|
|
7308
|
+
return {
|
|
7309
|
+
...base,
|
|
7310
|
+
target: 'codex', component: 'hooks', status: 'refused',
|
|
7311
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: false }],
|
|
7312
|
+
reasonCode: 'CURRENT_LIVE_CHECK_FAILED',
|
|
7313
|
+
remediation: noVerify ? '--no-verify cannot establish ready; rerun with live verification' : 'approve the managed hooks and rerun the live verification',
|
|
7314
|
+
applied: delivery.report.written || delivery.report.installed,
|
|
7315
|
+
};
|
|
7316
|
+
}
|
|
6906
7317
|
/**
|
|
6907
7318
|
* What the user is told about a sync report — the ONE place the success word can be printed.
|
|
6908
7319
|
*
|
|
@@ -7220,10 +7631,529 @@ function gatherReadmeCounts(root) {
|
|
|
7220
7631
|
sitePair('packages/@dzhechkov/sitedoc/src/content/docs/index.mdx', /^\s*tagline: "[^"]*?(\d+) commands/m, 'commands (docs-site index hero tagline vs cli All Commands)');
|
|
7221
7632
|
return pairs;
|
|
7222
7633
|
}
|
|
7634
|
+
const MAX_VOLUME_FILES_PER_SCOPE = 512;
|
|
7635
|
+
const MAX_VOLUME_BYTES_PER_SCOPE = 32 * 1024 * 1024;
|
|
7636
|
+
const CYRILLIC_CHARACTER = /\p{Script=Cyrillic}/u;
|
|
7637
|
+
function volumePathInside(root, candidate) {
|
|
7638
|
+
return candidate === root || candidate.startsWith(root + sep);
|
|
7639
|
+
}
|
|
7640
|
+
function cyrillicUtf8Bytes(buffer) {
|
|
7641
|
+
let bytes = 0;
|
|
7642
|
+
for (const character of buffer.toString('utf8')) {
|
|
7643
|
+
if (CYRILLIC_CHARACTER.test(character))
|
|
7644
|
+
bytes += Buffer.byteLength(character, 'utf8');
|
|
7645
|
+
}
|
|
7646
|
+
return bytes;
|
|
7647
|
+
}
|
|
7648
|
+
function gatherTemplateVolumeTarget(packageDir, target) {
|
|
7649
|
+
const templateRoot = join(packageDir, 'templates', '.claude');
|
|
7650
|
+
if (!existsSync(templateRoot))
|
|
7651
|
+
return undefined;
|
|
7652
|
+
const files = [];
|
|
7653
|
+
const realFiles = new Set();
|
|
7654
|
+
let totalBytes = 0;
|
|
7655
|
+
let failure;
|
|
7656
|
+
let packageReal;
|
|
7657
|
+
let templateReal;
|
|
7658
|
+
try {
|
|
7659
|
+
packageReal = realpathSync(packageDir);
|
|
7660
|
+
templateReal = realpathSync(templateRoot);
|
|
7661
|
+
if (!volumePathInside(packageReal, templateReal)) {
|
|
7662
|
+
failure = { reason: 'template-root-escape', detail: templateRoot };
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
catch (error) {
|
|
7666
|
+
return {
|
|
7667
|
+
target,
|
|
7668
|
+
files,
|
|
7669
|
+
collection: { complete: false, reason: 'template-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
7670
|
+
};
|
|
7671
|
+
}
|
|
7672
|
+
const fail = (reason, detail) => {
|
|
7673
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
7674
|
+
};
|
|
7675
|
+
const addFile = (absolute, kind) => {
|
|
7676
|
+
if (failure !== undefined)
|
|
7677
|
+
return;
|
|
7678
|
+
if (files.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
7679
|
+
fail('template-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
7680
|
+
return;
|
|
7681
|
+
}
|
|
7682
|
+
try {
|
|
7683
|
+
const stat = lstatSync(absolute);
|
|
7684
|
+
if (!stat.isFile()) {
|
|
7685
|
+
fail('template-input-not-regular', absolute);
|
|
7686
|
+
return;
|
|
7687
|
+
}
|
|
7688
|
+
const real = realpathSync(absolute);
|
|
7689
|
+
if (!volumePathInside(templateReal, real)) {
|
|
7690
|
+
fail('template-input-escape', absolute);
|
|
7691
|
+
return;
|
|
7692
|
+
}
|
|
7693
|
+
if (realFiles.has(real)) {
|
|
7694
|
+
fail('template-input-duplicate', absolute);
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7698
|
+
fail(stat.size <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
const content = readFileSync(absolute);
|
|
7702
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7703
|
+
fail(content.byteLength <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
7704
|
+
return;
|
|
7705
|
+
}
|
|
7706
|
+
realFiles.add(real);
|
|
7707
|
+
totalBytes += content.byteLength;
|
|
7708
|
+
files.push({
|
|
7709
|
+
path: relative(packageDir, absolute).split(sep).join('/'),
|
|
7710
|
+
kind,
|
|
7711
|
+
bytes: content.byteLength,
|
|
7712
|
+
cyrillicUtf8Bytes: cyrillicUtf8Bytes(content),
|
|
7713
|
+
});
|
|
7714
|
+
}
|
|
7715
|
+
catch (error) {
|
|
7716
|
+
fail('template-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7717
|
+
}
|
|
7718
|
+
};
|
|
7719
|
+
const scanFlat = (name) => {
|
|
7720
|
+
const dir = join(templateRoot, name);
|
|
7721
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
7722
|
+
return;
|
|
7723
|
+
try {
|
|
7724
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7725
|
+
if (!entry.name.endsWith('.md'))
|
|
7726
|
+
continue;
|
|
7727
|
+
addFile(join(dir, entry.name), name);
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
catch (error) {
|
|
7731
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7732
|
+
}
|
|
7733
|
+
};
|
|
7734
|
+
const scanSkills = (dir) => {
|
|
7735
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
7736
|
+
return;
|
|
7737
|
+
let entries;
|
|
7738
|
+
try {
|
|
7739
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
7740
|
+
}
|
|
7741
|
+
catch (error) {
|
|
7742
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7743
|
+
return;
|
|
7744
|
+
}
|
|
7745
|
+
for (const entry of entries) {
|
|
7746
|
+
if (failure !== undefined)
|
|
7747
|
+
return;
|
|
7748
|
+
const absolute = join(dir, entry.name);
|
|
7749
|
+
if (entry.isSymbolicLink()) {
|
|
7750
|
+
fail('template-input-not-regular', absolute);
|
|
7751
|
+
return;
|
|
7752
|
+
}
|
|
7753
|
+
if (entry.isDirectory())
|
|
7754
|
+
scanSkills(absolute);
|
|
7755
|
+
else if (entry.name === 'SKILL.md')
|
|
7756
|
+
addFile(absolute, 'skills');
|
|
7757
|
+
}
|
|
7758
|
+
};
|
|
7759
|
+
scanFlat('rules');
|
|
7760
|
+
scanFlat('commands');
|
|
7761
|
+
scanSkills(join(templateRoot, 'skills'));
|
|
7762
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
7763
|
+
return {
|
|
7764
|
+
target,
|
|
7765
|
+
files,
|
|
7766
|
+
collection: failure === undefined
|
|
7767
|
+
? { complete: true }
|
|
7768
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
7769
|
+
};
|
|
7770
|
+
}
|
|
7771
|
+
function volumeGitText(root, args, allowedStatuses = [0]) {
|
|
7772
|
+
const result = spawnSync('git', [...args], {
|
|
7773
|
+
cwd: root,
|
|
7774
|
+
encoding: 'utf8',
|
|
7775
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
7776
|
+
});
|
|
7777
|
+
if (result.status === null || !allowedStatuses.includes(result.status)) {
|
|
7778
|
+
throw result.error ?? new Error(`git ${args[0] ?? ''} exited ${String(result.status)}`);
|
|
7779
|
+
}
|
|
7780
|
+
return result.stdout ?? '';
|
|
7781
|
+
}
|
|
7782
|
+
function parseGuardStatusPaths(root) {
|
|
7783
|
+
const text = volumeGitText(root, ['status', '--porcelain', '-uall']);
|
|
7784
|
+
return text.split('\n').flatMap((line) => {
|
|
7785
|
+
if (line.length < 4)
|
|
7786
|
+
return [];
|
|
7787
|
+
const code = line.slice(0, 2);
|
|
7788
|
+
const raw = line.slice(3).trim();
|
|
7789
|
+
const path = (raw.includes(' -> ') ? raw.split(' -> ')[1] : raw)?.trim();
|
|
7790
|
+
return path ? [{ code, path }] : [];
|
|
7791
|
+
});
|
|
7792
|
+
}
|
|
7793
|
+
function parseFeatureTier(text) {
|
|
7794
|
+
const match = text.match(/^##\s+Tier:\s*(S|M|L|XL)\s*$/m);
|
|
7795
|
+
return match?.[1];
|
|
7796
|
+
}
|
|
7797
|
+
function parseFeatureActiveSteps(text) {
|
|
7798
|
+
const section = text.match(/^## Active steps[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/mi)?.[1];
|
|
7799
|
+
if (section === undefined)
|
|
7800
|
+
return undefined;
|
|
7801
|
+
const steps = [...section.matchAll(/\b(?:10|[0-9](?:\.5)?)\b/g)].map((match) => match[0]);
|
|
7802
|
+
return [...new Set(steps)];
|
|
7803
|
+
}
|
|
7804
|
+
function featureLifecycle(root, slug, assessment) {
|
|
7805
|
+
if (assessment !== undefined && /^Lifecycle:\s*complete\s*$/mi.test(assessment))
|
|
7806
|
+
return { phase: 'complete' };
|
|
7807
|
+
try {
|
|
7808
|
+
const state = JSON.parse(readFileSync(join(root, '.dz', 'feature-adr', 'learning-state', `${slug}.json`), 'utf8'));
|
|
7809
|
+
const match = typeof state.step === 'string' ? state.step.match(/\bStep\s+(\d+(?:\.5)?)/i) : null;
|
|
7810
|
+
if (!match?.[1])
|
|
7811
|
+
return undefined;
|
|
7812
|
+
const current = Number(match[1]);
|
|
7813
|
+
if (!Number.isFinite(current))
|
|
7814
|
+
return undefined;
|
|
7815
|
+
return { phase: 'in-progress', completedThroughStep: current <= 0 ? 0 : Math.ceil(current) - 1 };
|
|
7816
|
+
}
|
|
7817
|
+
catch {
|
|
7818
|
+
return undefined;
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
function gatherFeatureVolumeFact(root, slug) {
|
|
7822
|
+
const featureDir = join(root, 'features', slug);
|
|
7823
|
+
const artifacts = [];
|
|
7824
|
+
const contents = new Map();
|
|
7825
|
+
let totalBytes = 0;
|
|
7826
|
+
let failure;
|
|
7827
|
+
let rootReal;
|
|
7828
|
+
let featureReal;
|
|
7829
|
+
try {
|
|
7830
|
+
rootReal = realpathSync(root);
|
|
7831
|
+
featureReal = realpathSync(featureDir);
|
|
7832
|
+
if (!volumePathInside(rootReal, featureReal))
|
|
7833
|
+
failure = { reason: 'feature-root-escape', detail: featureDir };
|
|
7834
|
+
}
|
|
7835
|
+
catch (error) {
|
|
7836
|
+
return { fact: {
|
|
7837
|
+
slug,
|
|
7838
|
+
artifacts,
|
|
7839
|
+
collection: { complete: false, reason: 'feature-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
7840
|
+
} };
|
|
7841
|
+
}
|
|
7842
|
+
const fail = (reason, detail) => {
|
|
7843
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
7844
|
+
};
|
|
7845
|
+
const addArtifact = (absolute) => {
|
|
7846
|
+
if (failure !== undefined)
|
|
7847
|
+
return;
|
|
7848
|
+
if (artifacts.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
7849
|
+
fail('feature-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
7850
|
+
return;
|
|
7851
|
+
}
|
|
7852
|
+
try {
|
|
7853
|
+
const stat = lstatSync(absolute);
|
|
7854
|
+
if (!stat.isFile()) {
|
|
7855
|
+
fail('feature-input-not-regular', absolute);
|
|
7856
|
+
return;
|
|
7857
|
+
}
|
|
7858
|
+
const real = realpathSync(absolute);
|
|
7859
|
+
if (!volumePathInside(featureReal, real)) {
|
|
7860
|
+
fail('feature-input-escape', absolute);
|
|
7861
|
+
return;
|
|
7862
|
+
}
|
|
7863
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7864
|
+
fail(stat.size <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
7865
|
+
return;
|
|
7866
|
+
}
|
|
7867
|
+
const content = readFileSync(absolute);
|
|
7868
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
7869
|
+
fail(content.byteLength <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
7870
|
+
return;
|
|
7871
|
+
}
|
|
7872
|
+
const path = relative(featureDir, absolute).split(sep).join('/');
|
|
7873
|
+
totalBytes += content.byteLength;
|
|
7874
|
+
contents.set(path, content);
|
|
7875
|
+
artifacts.push({ path, bytes: content.byteLength });
|
|
7876
|
+
}
|
|
7877
|
+
catch (error) {
|
|
7878
|
+
fail('feature-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7879
|
+
}
|
|
7880
|
+
};
|
|
7881
|
+
const scanNested = (dir) => {
|
|
7882
|
+
if (!existsSync(dir) || failure !== undefined)
|
|
7883
|
+
return;
|
|
7884
|
+
let entries;
|
|
7885
|
+
try {
|
|
7886
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
7887
|
+
}
|
|
7888
|
+
catch (error) {
|
|
7889
|
+
fail('feature-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7890
|
+
return;
|
|
7891
|
+
}
|
|
7892
|
+
for (const entry of entries) {
|
|
7893
|
+
if (failure !== undefined)
|
|
7894
|
+
return;
|
|
7895
|
+
const absolute = join(dir, entry.name);
|
|
7896
|
+
if (entry.isSymbolicLink()) {
|
|
7897
|
+
fail('feature-input-not-regular', absolute);
|
|
7898
|
+
return;
|
|
7899
|
+
}
|
|
7900
|
+
if (entry.isDirectory())
|
|
7901
|
+
scanNested(absolute);
|
|
7902
|
+
else
|
|
7903
|
+
addArtifact(absolute);
|
|
7904
|
+
}
|
|
7905
|
+
};
|
|
7906
|
+
try {
|
|
7907
|
+
const entries = readdirSync(featureDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
7908
|
+
for (const entry of entries) {
|
|
7909
|
+
if (failure !== undefined)
|
|
7910
|
+
break;
|
|
7911
|
+
const absolute = join(featureDir, entry.name);
|
|
7912
|
+
if (entry.name === '03_adr' || entry.name === '07_code_changes') {
|
|
7913
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
7914
|
+
fail('feature-input-not-regular', absolute);
|
|
7915
|
+
else
|
|
7916
|
+
scanNested(absolute);
|
|
7917
|
+
}
|
|
7918
|
+
else if (entry.name === 'README.md' || /^0[0-9](?:[_.-][^/]*)?\.md$/.test(entry.name)) {
|
|
7919
|
+
addArtifact(absolute);
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
catch (error) {
|
|
7924
|
+
fail('feature-directory-unreadable', `${featureDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7925
|
+
}
|
|
7926
|
+
artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
7927
|
+
const assessment = contents.get('00_complexity_assessment.md')?.toString('utf8');
|
|
7928
|
+
const tier = assessment !== undefined ? parseFeatureTier(assessment) : undefined;
|
|
7929
|
+
const activeSteps = assessment !== undefined ? parseFeatureActiveSteps(assessment) : undefined;
|
|
7930
|
+
const lifecycle = featureLifecycle(root, slug, assessment);
|
|
7931
|
+
const fact = {
|
|
7932
|
+
slug,
|
|
7933
|
+
...(tier !== undefined ? { tier } : {}),
|
|
7934
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
7935
|
+
namedConsumers: [],
|
|
7936
|
+
...(lifecycle !== undefined ? { lifecycle } : {}),
|
|
7937
|
+
artifacts,
|
|
7938
|
+
collection: failure === undefined
|
|
7939
|
+
? { complete: true }
|
|
7940
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
7941
|
+
};
|
|
7942
|
+
const manifestText = contents.get('07_code_changes/change_manifest.md')?.toString('utf8');
|
|
7943
|
+
return { fact, ...(manifestText !== undefined ? { manifestText } : {}) };
|
|
7944
|
+
}
|
|
7945
|
+
function manifestPaths(text, slug) {
|
|
7946
|
+
const paths = new Set();
|
|
7947
|
+
for (const line of text.split('\n')) {
|
|
7948
|
+
const match = line.match(/^\s*-\s+`?([^`]+?)`?(?:\s+[—-]\s+.*)?$/);
|
|
7949
|
+
if (!match?.[1])
|
|
7950
|
+
continue;
|
|
7951
|
+
const path = match[1].trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7952
|
+
if (path === '' || path.startsWith('/') || path.split('/').some((part) => part === '..'))
|
|
7953
|
+
continue;
|
|
7954
|
+
if (path.startsWith(`features/${slug}/`))
|
|
7955
|
+
continue;
|
|
7956
|
+
paths.add(path);
|
|
7957
|
+
}
|
|
7958
|
+
return [...paths].sort();
|
|
7959
|
+
}
|
|
7960
|
+
function attributableDiff(root, slug, feature, status, changedSlugs) {
|
|
7961
|
+
if (changedSlugs.length !== 1 || changedSlugs[0] !== slug) {
|
|
7962
|
+
return { attributable: false, reason: changedSlugs.length > 1 ? 'ambiguous-feature-attribution' : 'feature-not-attributable' };
|
|
7963
|
+
}
|
|
7964
|
+
if (feature.manifestText === undefined)
|
|
7965
|
+
return { attributable: false, reason: 'change-manifest-unavailable' };
|
|
7966
|
+
const listed = manifestPaths(feature.manifestText, slug);
|
|
7967
|
+
const changedByPath = new Map(status.map((item) => [item.path, item.code]));
|
|
7968
|
+
const changed = listed.filter((path) => changedByPath.has(path));
|
|
7969
|
+
if (changed.length === 0)
|
|
7970
|
+
return { attributable: true, bytes: 0, base: 'HEAD', head: 'working-tree', method: 'git-unified-diff-bytes/v1', excludedFeaturePath: `features/${slug}/**` };
|
|
7971
|
+
const tracked = [];
|
|
7972
|
+
const untracked = [];
|
|
7973
|
+
for (const path of changed) {
|
|
7974
|
+
const absolute = resolve(root, path);
|
|
7975
|
+
if (!volumePathInside(root, absolute))
|
|
7976
|
+
return { attributable: false, reason: 'manifest-path-escape' };
|
|
7977
|
+
if (changedByPath.get(path) === '??') {
|
|
7978
|
+
try {
|
|
7979
|
+
const stat = lstatSync(absolute);
|
|
7980
|
+
const real = realpathSync(absolute);
|
|
7981
|
+
if (!stat.isFile() || !volumePathInside(root, real) || stat.size > MAX_VOLUME_BYTES_PER_SCOPE) {
|
|
7982
|
+
return { attributable: false, reason: 'untracked-diff-input-refused' };
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
catch {
|
|
7986
|
+
return { attributable: false, reason: 'untracked-diff-input-unreadable' };
|
|
7987
|
+
}
|
|
7988
|
+
untracked.push(path);
|
|
7989
|
+
}
|
|
7990
|
+
else {
|
|
7991
|
+
tracked.push(path);
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
let bytes = 0;
|
|
7995
|
+
try {
|
|
7996
|
+
if (tracked.length > 0) {
|
|
7997
|
+
const diff = volumeGitText(root, ['diff', '--binary', '--no-ext-diff', 'HEAD', '--', ...tracked]);
|
|
7998
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
7999
|
+
}
|
|
8000
|
+
for (const path of untracked) {
|
|
8001
|
+
const diff = volumeGitText(root, ['diff', '--no-index', '--binary', '--', '/dev/null', resolve(root, path)], [0, 1]);
|
|
8002
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
8003
|
+
if (bytes > MAX_VOLUME_BYTES_PER_SCOPE)
|
|
8004
|
+
return { attributable: false, reason: 'diff-byte-cap-exceeded' };
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
catch {
|
|
8008
|
+
return { attributable: false, reason: 'git-diff-failed' };
|
|
8009
|
+
}
|
|
8010
|
+
return {
|
|
8011
|
+
attributable: true,
|
|
8012
|
+
bytes,
|
|
8013
|
+
base: 'HEAD',
|
|
8014
|
+
head: 'working-tree',
|
|
8015
|
+
method: 'git-unified-diff-bytes/v1',
|
|
8016
|
+
excludedFeaturePath: `features/${slug}/**`,
|
|
8017
|
+
};
|
|
8018
|
+
}
|
|
8019
|
+
function gatherVolumeShadowFacts(root, packages) {
|
|
8020
|
+
const templates = packages
|
|
8021
|
+
.filter((item) => !item.privateFlag)
|
|
8022
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.dir.localeCompare(b.dir))
|
|
8023
|
+
.flatMap((item) => {
|
|
8024
|
+
const fact = gatherTemplateVolumeTarget(join(root, item.dir), item.name);
|
|
8025
|
+
return fact === undefined ? [] : [fact];
|
|
8026
|
+
});
|
|
8027
|
+
let status;
|
|
8028
|
+
try {
|
|
8029
|
+
status = parseGuardStatusPaths(root);
|
|
8030
|
+
}
|
|
8031
|
+
catch {
|
|
8032
|
+
return templates.length > 0 ? { templates } : {};
|
|
8033
|
+
}
|
|
8034
|
+
const slugs = [...new Set(status.flatMap((item) => {
|
|
8035
|
+
const match = item.path.match(/^features\/([^/]+)\//);
|
|
8036
|
+
return match?.[1] ? [match[1]] : [];
|
|
8037
|
+
}))].sort();
|
|
8038
|
+
const gathered = slugs.slice(0, 32).map((slug) => gatherFeatureVolumeFact(root, slug));
|
|
8039
|
+
const measuredFeatures = gathered.map((feature) => ({
|
|
8040
|
+
...feature.fact,
|
|
8041
|
+
diff: attributableDiff(root, feature.fact.slug, feature, status, slugs),
|
|
8042
|
+
}));
|
|
8043
|
+
const cappedFeatures = slugs.slice(32).map((slug) => ({
|
|
8044
|
+
slug,
|
|
8045
|
+
artifacts: [],
|
|
8046
|
+
collection: {
|
|
8047
|
+
complete: false,
|
|
8048
|
+
reason: 'feature-scope-cap-exceeded',
|
|
8049
|
+
detail: 'only the first 32 lexicographically sorted changed feature scopes were traversed',
|
|
8050
|
+
},
|
|
8051
|
+
}));
|
|
8052
|
+
const features = [...measuredFeatures, ...cappedFeatures];
|
|
8053
|
+
return {
|
|
8054
|
+
...(templates.length > 0 ? { templates } : {}),
|
|
8055
|
+
...(features.length > 0 ? { features } : {}),
|
|
8056
|
+
};
|
|
8057
|
+
}
|
|
7223
8058
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
7224
8059
|
function gatherGuardFacts(op, root, text, storeCap) {
|
|
7225
8060
|
const facts = { op };
|
|
7226
8061
|
if (op === 'publish') {
|
|
8062
|
+
// Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
|
|
8063
|
+
// stale finding and never a publish blocker.
|
|
8064
|
+
try {
|
|
8065
|
+
const routing = readRoutingTelemetry(root);
|
|
8066
|
+
facts['routingFreshness'] = { unfedRunIds: unfedRuns(routing.harvest.samples, routing.alreadyFed) };
|
|
8067
|
+
}
|
|
8068
|
+
catch { /* fail-open: routing-store-stale is SOFT and needs gathered evidence */ }
|
|
8069
|
+
// marketplace-parity: regenerate the two published showcase manifests from the LIVE registry,
|
|
8070
|
+
// but redirect every write to a unique temp root. Version is operator-owned, so pinning the
|
|
8071
|
+
// published value into generatePlugin makes the comparison composition-only by construction.
|
|
8072
|
+
try {
|
|
8073
|
+
const showcaseDir = join(root, '.claude-plugin');
|
|
8074
|
+
if (!existsSync(showcaseDir)) {
|
|
8075
|
+
facts['marketplaceParity'] = { applicable: false };
|
|
8076
|
+
}
|
|
8077
|
+
else {
|
|
8078
|
+
const pluginPath = join(showcaseDir, 'plugin.json');
|
|
8079
|
+
const marketplacePath = join(showcaseDir, 'marketplace.json');
|
|
8080
|
+
const hasPlugin = existsSync(pluginPath);
|
|
8081
|
+
const hasMarketplace = existsSync(marketplacePath);
|
|
8082
|
+
const manifestFailures = [];
|
|
8083
|
+
const readManifest = (path, file) => {
|
|
8084
|
+
try {
|
|
8085
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
8086
|
+
}
|
|
8087
|
+
catch (error) {
|
|
8088
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8089
|
+
manifestFailures.push({ file, error: message.replace(/\s+/g, ' ').slice(0, 240) });
|
|
8090
|
+
return undefined;
|
|
8091
|
+
}
|
|
8092
|
+
};
|
|
8093
|
+
const versionOf = (manifest) => {
|
|
8094
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
8095
|
+
return undefined;
|
|
8096
|
+
const version = manifest['version'];
|
|
8097
|
+
return typeof version === 'string' && version !== '' ? version : undefined;
|
|
8098
|
+
};
|
|
8099
|
+
const publishedPlugin = hasPlugin ? readManifest(pluginPath, 'plugin.json') : undefined;
|
|
8100
|
+
const publishedMarketplace = hasMarketplace ? readManifest(marketplacePath, 'marketplace.json') : undefined;
|
|
8101
|
+
const marketplaceRecord = publishedMarketplace && typeof publishedMarketplace === 'object' && !Array.isArray(publishedMarketplace)
|
|
8102
|
+
? publishedMarketplace
|
|
8103
|
+
: undefined;
|
|
8104
|
+
const marketplacePlugins = marketplaceRecord !== undefined && Array.isArray(marketplaceRecord['plugins'])
|
|
8105
|
+
? marketplaceRecord['plugins']
|
|
8106
|
+
: [];
|
|
8107
|
+
const publishedVersion = versionOf(publishedPlugin)
|
|
8108
|
+
?? versionOf(publishedMarketplace)
|
|
8109
|
+
?? versionOf(marketplacePlugins[0]);
|
|
8110
|
+
if (manifestFailures.length > 0) {
|
|
8111
|
+
facts['marketplaceParity'] = {
|
|
8112
|
+
applicable: true,
|
|
8113
|
+
manifestFailures,
|
|
8114
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8115
|
+
};
|
|
8116
|
+
}
|
|
8117
|
+
else if (hasPlugin !== hasMarketplace) {
|
|
8118
|
+
facts['marketplaceParity'] = {
|
|
8119
|
+
applicable: true,
|
|
8120
|
+
onlyOnePresent: true,
|
|
8121
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8122
|
+
};
|
|
8123
|
+
}
|
|
8124
|
+
else if (!hasPlugin) {
|
|
8125
|
+
facts['marketplaceParity'] = { applicable: false };
|
|
8126
|
+
}
|
|
8127
|
+
else {
|
|
8128
|
+
let scratch;
|
|
8129
|
+
try {
|
|
8130
|
+
const registry = buildRegistry(root);
|
|
8131
|
+
scratch = mkdtempSync(join(tmpdir(), 'dz-guard-marketplace-'));
|
|
8132
|
+
const generated = generatePlugin(scratch, registry, { version: publishedVersion });
|
|
8133
|
+
const freshPlugin = JSON.parse(readFileSync(generated.pluginJsonPath, 'utf8'));
|
|
8134
|
+
const freshMarketplace = JSON.parse(readFileSync(generated.marketplaceJsonPath, 'utf8'));
|
|
8135
|
+
facts['marketplaceParity'] = {
|
|
8136
|
+
applicable: true,
|
|
8137
|
+
diverged: !isDeepStrictEqual(publishedPlugin, freshPlugin)
|
|
8138
|
+
|| !isDeepStrictEqual(publishedMarketplace, freshMarketplace),
|
|
8139
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8140
|
+
};
|
|
8141
|
+
}
|
|
8142
|
+
catch {
|
|
8143
|
+
facts['marketplaceParity'] = {
|
|
8144
|
+
applicable: true,
|
|
8145
|
+
regenerateFailed: true,
|
|
8146
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8147
|
+
};
|
|
8148
|
+
}
|
|
8149
|
+
finally {
|
|
8150
|
+
if (scratch !== undefined)
|
|
8151
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
8152
|
+
}
|
|
8153
|
+
}
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
catch { /* fail-open only on unexpected showcase-discovery failure; manifest read/parse errors are structured violations above */ }
|
|
7227
8157
|
// agents-md-policy-sync: fixed registry, no tree walk. The pure detector
|
|
7228
8158
|
// recomputes every expected hash from current source text; this gatherer
|
|
7229
8159
|
// only supplies bytes. Any unexpected gather failure omits the fact, and
|
|
@@ -7259,7 +8189,7 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
7259
8189
|
const manifests = [];
|
|
7260
8190
|
const located = [];
|
|
7261
8191
|
try {
|
|
7262
|
-
const out =
|
|
8192
|
+
const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
|
|
7263
8193
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
7264
8194
|
try {
|
|
7265
8195
|
const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
|
|
@@ -7288,6 +8218,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
|
|
|
7288
8218
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
7289
8219
|
}
|
|
7290
8220
|
facts['packages'] = packages;
|
|
8221
|
+
facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
|
|
8222
|
+
dir,
|
|
8223
|
+
name: m.name ?? dir,
|
|
8224
|
+
privateFlag: m.private === true,
|
|
8225
|
+
})));
|
|
7291
8226
|
// licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
|
|
7292
8227
|
// `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
|
|
7293
8228
|
// LICENSE reads as absent (null), which the checker treats as a violation for a publishable
|
|
@@ -7565,6 +8500,10 @@ function runGuardEvaluation(root, op, text, overrideReason) {
|
|
|
7565
8500
|
catch { /* audit is best-effort, never blocks the verdict */ }
|
|
7566
8501
|
return result;
|
|
7567
8502
|
}
|
|
8503
|
+
function renderGuardObservation(observation) {
|
|
8504
|
+
const tag = observation.status === 'unknown' ? 'note' : 'observe';
|
|
8505
|
+
return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
|
|
8506
|
+
}
|
|
7568
8507
|
/**
|
|
7569
8508
|
* The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
|
|
7570
8509
|
* lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
|
|
@@ -7939,8 +8878,9 @@ function cmdGuardPromote(options, flags, root, write) {
|
|
|
7939
8878
|
...next,
|
|
7940
8879
|
entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
|
|
7941
8880
|
};
|
|
8881
|
+
const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
|
|
7942
8882
|
try {
|
|
7943
|
-
writeJsonAtomic(join(root, PROMOTION_STATE_FILE),
|
|
8883
|
+
writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence);
|
|
7944
8884
|
}
|
|
7945
8885
|
catch { /* best-effort */ }
|
|
7946
8886
|
}
|
|
@@ -8049,6 +8989,8 @@ function cmdGuard(options, flags, cwd, write) {
|
|
|
8049
8989
|
}
|
|
8050
8990
|
const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
|
|
8051
8991
|
write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
|
|
8992
|
+
for (const observation of result.observations ?? [])
|
|
8993
|
+
write(renderGuardObservation(observation));
|
|
8052
8994
|
for (const v of result.violations)
|
|
8053
8995
|
write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
|
|
8054
8996
|
for (const n of result.notes ?? [])
|
|
@@ -12647,6 +13589,134 @@ function readOptionalText(path) {
|
|
|
12647
13589
|
return '';
|
|
12648
13590
|
}
|
|
12649
13591
|
}
|
|
13592
|
+
function localEvidenceReadReason(resource, error) {
|
|
13593
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
13594
|
+
return code === 'ENOENT' ? `${resource}-missing` : `${resource}-unreadable`;
|
|
13595
|
+
}
|
|
13596
|
+
function unavailablePromotionEvidence(reason) {
|
|
13597
|
+
return { source: { status: 'not-measured', reason }, acceptances: [], truncatedPeriods: [], acceptanceHistoryComplete: false };
|
|
13598
|
+
}
|
|
13599
|
+
function readPromotionRunEvidence(root) {
|
|
13600
|
+
let raw;
|
|
13601
|
+
try {
|
|
13602
|
+
raw = JSON.parse(readFileSync(join(root, PROMOTION_STATE_FILE), 'utf-8'));
|
|
13603
|
+
}
|
|
13604
|
+
catch (error) {
|
|
13605
|
+
const reason = error instanceof SyntaxError
|
|
13606
|
+
? 'promotion-journal-malformed'
|
|
13607
|
+
: localEvidenceReadReason('promotion-journal', error);
|
|
13608
|
+
return unavailablePromotionEvidence(reason);
|
|
13609
|
+
}
|
|
13610
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
13611
|
+
return unavailablePromotionEvidence('promotion-journal-malformed');
|
|
13612
|
+
}
|
|
13613
|
+
const record = raw;
|
|
13614
|
+
if (record['version'] !== 1 || !Object.hasOwn(record, 'runs')) {
|
|
13615
|
+
return unavailablePromotionEvidence(record['version'] === 1 ? 'promotion-history-not-recorded' : 'promotion-journal-malformed');
|
|
13616
|
+
}
|
|
13617
|
+
if (!Array.isArray(record['runs'])) {
|
|
13618
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13619
|
+
}
|
|
13620
|
+
const state = normalizePromotionState(raw);
|
|
13621
|
+
if (state.runs === undefined || state.runs.length !== record['runs'].length) {
|
|
13622
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13623
|
+
}
|
|
13624
|
+
if ((Object.hasOwn(record, 'acceptances') && (!Array.isArray(record['acceptances']) || state.acceptances?.length !== record['acceptances'].length)) ||
|
|
13625
|
+
(Object.hasOwn(record, 'truncatedRunPeriods') && (!Array.isArray(record['truncatedRunPeriods']) || state.truncatedRunPeriods?.length !== record['truncatedRunPeriods'].length)) ||
|
|
13626
|
+
(Object.hasOwn(record, 'acceptanceHistoryComplete') && typeof record['acceptanceHistoryComplete'] !== 'boolean')) {
|
|
13627
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13628
|
+
}
|
|
13629
|
+
const derivedAcceptances = state.runs.flatMap((run) => run.candidates
|
|
13630
|
+
.filter((candidate) => candidate.verdict === 'promote' && candidate.ruleContentAnchor !== null)
|
|
13631
|
+
.map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor, acceptedTs: run.ts })));
|
|
13632
|
+
const claimedAcceptanceComplete = state.acceptanceHistoryComplete ??
|
|
13633
|
+
((state.truncatedRunPeriods?.length ?? 0) === 0 && state.runs.every((run) => run.complete === true));
|
|
13634
|
+
if (claimedAcceptanceComplete && state.acceptances !== undefined && derivedAcceptances.some((derived) => !state.acceptances.some((stored) => stored.ruleContentAnchor === derived.ruleContentAnchor &&
|
|
13635
|
+
Date.parse(stored.acceptedTs) <= Date.parse(derived.acceptedTs)))) {
|
|
13636
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13637
|
+
}
|
|
13638
|
+
const acceptancesByAnchor = new Map();
|
|
13639
|
+
for (const acceptance of [...(state.acceptances ?? []), ...derivedAcceptances]) {
|
|
13640
|
+
const prior = acceptancesByAnchor.get(acceptance.ruleContentAnchor);
|
|
13641
|
+
if (prior === undefined || Date.parse(acceptance.acceptedTs) < Date.parse(prior.acceptedTs)) {
|
|
13642
|
+
acceptancesByAnchor.set(acceptance.ruleContentAnchor, acceptance);
|
|
13643
|
+
}
|
|
13644
|
+
}
|
|
13645
|
+
return {
|
|
13646
|
+
source: { status: 'measured', rows: state.runs },
|
|
13647
|
+
acceptances: [...acceptancesByAnchor.values()],
|
|
13648
|
+
truncatedPeriods: state.truncatedRunPeriods ?? [],
|
|
13649
|
+
acceptanceHistoryComplete: claimedAcceptanceComplete,
|
|
13650
|
+
};
|
|
13651
|
+
}
|
|
13652
|
+
function readGuardAuditEvidence(root) {
|
|
13653
|
+
let text;
|
|
13654
|
+
try {
|
|
13655
|
+
text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
13656
|
+
}
|
|
13657
|
+
catch (error) {
|
|
13658
|
+
return {
|
|
13659
|
+
source: { status: 'not-measured', reason: localEvidenceReadReason('guard-audit', error) },
|
|
13660
|
+
rows: [],
|
|
13661
|
+
text: null,
|
|
13662
|
+
};
|
|
13663
|
+
}
|
|
13664
|
+
const rows = [];
|
|
13665
|
+
let malformed = false;
|
|
13666
|
+
for (const line of text.split('\n')) {
|
|
13667
|
+
if (line.trim() === '')
|
|
13668
|
+
continue;
|
|
13669
|
+
try {
|
|
13670
|
+
const raw = JSON.parse(line);
|
|
13671
|
+
if (!isOffsetIsoTimestamp(raw['ts']) ||
|
|
13672
|
+
!['publish', 'teach', 'consolidate', 'reindex'].includes(String(raw['op'])) ||
|
|
13673
|
+
!['pass', 'warn', 'block'].includes(String(raw['verdict']))) {
|
|
13674
|
+
malformed = true;
|
|
13675
|
+
continue;
|
|
13676
|
+
}
|
|
13677
|
+
const violations = [];
|
|
13678
|
+
if (!Array.isArray(raw['violations'])) {
|
|
13679
|
+
malformed = true;
|
|
13680
|
+
continue;
|
|
13681
|
+
}
|
|
13682
|
+
for (const item of raw['violations']) {
|
|
13683
|
+
const rule = item && typeof item === 'object' ? item.rule : undefined;
|
|
13684
|
+
if (typeof rule !== 'string' || rule === '' || rule.length > 200) {
|
|
13685
|
+
malformed = true;
|
|
13686
|
+
continue;
|
|
13687
|
+
}
|
|
13688
|
+
const anchor = item.contentAnchor;
|
|
13689
|
+
if (anchor !== undefined && !isLessonRuleContentAnchor(anchor)) {
|
|
13690
|
+
malformed = true;
|
|
13691
|
+
continue;
|
|
13692
|
+
}
|
|
13693
|
+
violations.push({ rule, ...(typeof anchor === 'string' ? { contentAnchor: anchor } : {}) });
|
|
13694
|
+
}
|
|
13695
|
+
const verdict = raw['verdict'];
|
|
13696
|
+
if ((verdict === 'pass') !== (violations.length === 0))
|
|
13697
|
+
malformed = true;
|
|
13698
|
+
rows.push({
|
|
13699
|
+
ts: raw['ts'],
|
|
13700
|
+
op: raw['op'],
|
|
13701
|
+
verdict,
|
|
13702
|
+
rules: violations.map((item) => item.rule),
|
|
13703
|
+
violations,
|
|
13704
|
+
});
|
|
13705
|
+
}
|
|
13706
|
+
catch {
|
|
13707
|
+
malformed = true;
|
|
13708
|
+
}
|
|
13709
|
+
}
|
|
13710
|
+
return {
|
|
13711
|
+
source: malformed
|
|
13712
|
+
? { status: 'not-measured', reason: 'guard-audit-malformed' }
|
|
13713
|
+
: !verifyEventChainText(text).ok
|
|
13714
|
+
? { status: 'not-measured', reason: 'guard-audit-chain-corrupt' }
|
|
13715
|
+
: { status: 'measured', rows },
|
|
13716
|
+
rows,
|
|
13717
|
+
text,
|
|
13718
|
+
};
|
|
13719
|
+
}
|
|
12650
13720
|
function deadwoodAllowlistText() {
|
|
12651
13721
|
const require = createRequire(import.meta.url);
|
|
12652
13722
|
const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
|
|
@@ -12727,7 +13797,7 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12727
13797
|
const json = flags.has('json');
|
|
12728
13798
|
if (flags.has('help')) {
|
|
12729
13799
|
write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
|
|
12730
|
-
write(' pool payoff
|
|
13800
|
+
write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
|
|
12731
13801
|
return 0;
|
|
12732
13802
|
}
|
|
12733
13803
|
for (const flag of flags) {
|
|
@@ -12756,36 +13826,14 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12756
13826
|
}));
|
|
12757
13827
|
// apply-leg usage events (read records only; aggregate rows carry no query by construction)
|
|
12758
13828
|
const usage = readRecallUsageEvents(root);
|
|
12759
|
-
|
|
12760
|
-
const
|
|
12761
|
-
|
|
12762
|
-
const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
12763
|
-
for (const line of text.split('\n')) {
|
|
12764
|
-
if (line.trim() === '')
|
|
12765
|
-
continue;
|
|
12766
|
-
try {
|
|
12767
|
-
const o = JSON.parse(line);
|
|
12768
|
-
if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
|
|
12769
|
-
guard.push({
|
|
12770
|
-
ts: o.ts,
|
|
12771
|
-
verdict: o.verdict,
|
|
12772
|
-
rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
|
|
12773
|
-
});
|
|
12774
|
-
}
|
|
12775
|
-
}
|
|
12776
|
-
catch {
|
|
12777
|
-
/* skip */
|
|
12778
|
-
}
|
|
12779
|
-
}
|
|
12780
|
-
}
|
|
12781
|
-
catch {
|
|
12782
|
-
/* no audit yet */
|
|
12783
|
-
}
|
|
13829
|
+
const promotionEvidence = readPromotionRunEvidence(root);
|
|
13830
|
+
const guardEvidence = readGuardAuditEvidence(root);
|
|
13831
|
+
const guard = [...guardEvidence.rows];
|
|
12784
13832
|
// The evidence logs themselves, verbatim: the report verifies their hash chains (feature
|
|
12785
13833
|
// event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
|
|
12786
13834
|
// "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
|
|
12787
13835
|
const evidenceLogs = [];
|
|
12788
|
-
for (const rel of ['.dz/recall-usage.jsonl'
|
|
13836
|
+
for (const rel of ['.dz/recall-usage.jsonl']) {
|
|
12789
13837
|
try {
|
|
12790
13838
|
evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
|
|
12791
13839
|
}
|
|
@@ -12793,6 +13841,9 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12793
13841
|
/* absent log — reported by its own gate above, not invented here */
|
|
12794
13842
|
}
|
|
12795
13843
|
}
|
|
13844
|
+
if (guardEvidence.text !== null) {
|
|
13845
|
+
evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
|
|
13846
|
+
}
|
|
12796
13847
|
const now = new Date();
|
|
12797
13848
|
const report = assembleCompoundingReport({
|
|
12798
13849
|
lessons,
|
|
@@ -12801,6 +13852,13 @@ function cmdCompounding(options, flags, cwd, write) {
|
|
|
12801
13852
|
nowTs: now.toISOString(),
|
|
12802
13853
|
evidenceLogs,
|
|
12803
13854
|
cmdUsageDepthDays: cmdUsageDepthDays(root, now),
|
|
13855
|
+
lessonToRule: {
|
|
13856
|
+
promotionRuns: promotionEvidence.source,
|
|
13857
|
+
guardAudits: guardEvidence.source,
|
|
13858
|
+
promotionAcceptances: promotionEvidence.acceptances,
|
|
13859
|
+
truncatedPromotionPeriods: promotionEvidence.truncatedPeriods,
|
|
13860
|
+
acceptanceHistoryComplete: promotionEvidence.acceptanceHistoryComplete,
|
|
13861
|
+
},
|
|
12804
13862
|
});
|
|
12805
13863
|
// lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
|
|
12806
13864
|
// dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
|
|
@@ -14016,6 +15074,44 @@ async function cmdBacklog(options, flags, cwd, write) {
|
|
|
14016
15074
|
write(' harmonize [--apply][--threshold 0-1] batch semantic dedup of the backlog');
|
|
14017
15075
|
return sub === undefined ? 0 : 1;
|
|
14018
15076
|
}
|
|
15077
|
+
/** Read the recommender's existing telemetry planes plus its idempotency receipt. One adapter is
|
|
15078
|
+
* shared by `recommend` and the publish advisory so their freshness definitions cannot drift. */
|
|
15079
|
+
function readRoutingTelemetry(repoRoot) {
|
|
15080
|
+
const records = readHarnessRecords(repoRoot);
|
|
15081
|
+
for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
|
|
15082
|
+
if (!existsSync(base))
|
|
15083
|
+
continue;
|
|
15084
|
+
let names = [];
|
|
15085
|
+
try {
|
|
15086
|
+
names = readdirSync(base);
|
|
15087
|
+
}
|
|
15088
|
+
catch {
|
|
15089
|
+
continue;
|
|
15090
|
+
}
|
|
15091
|
+
for (const name of names) {
|
|
15092
|
+
const sidecar = join(base, name, 'run-meta.json');
|
|
15093
|
+
if (!existsSync(sidecar))
|
|
15094
|
+
continue;
|
|
15095
|
+
try {
|
|
15096
|
+
const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
|
15097
|
+
if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records))
|
|
15098
|
+
records.push(...meta.runMeta.records);
|
|
15099
|
+
}
|
|
15100
|
+
catch { /* an unreadable sidecar contributes no asserted record */ }
|
|
15101
|
+
}
|
|
15102
|
+
}
|
|
15103
|
+
let alreadyFed = [];
|
|
15104
|
+
const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
|
|
15105
|
+
if (existsSync(fedPath)) {
|
|
15106
|
+
const parsed = JSON.parse(readFileSync(join(repoRoot, '.dz', 'routing-fed.json'), 'utf-8'));
|
|
15107
|
+
if (!Array.isArray(parsed))
|
|
15108
|
+
throw new Error('.dz/routing-fed.json must be a JSON array of runIds');
|
|
15109
|
+
if (parsed.some((v) => typeof v !== 'string'))
|
|
15110
|
+
throw new Error('.dz/routing-fed.json contains a non-string runId');
|
|
15111
|
+
alreadyFed = parsed;
|
|
15112
|
+
}
|
|
15113
|
+
return { harvest: harvestStageOutcomes(records), alreadyFed };
|
|
15114
|
+
}
|
|
14019
15115
|
/**
|
|
14020
15116
|
* `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
|
|
14021
15117
|
* what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
|
|
@@ -14032,40 +15128,22 @@ function cmdRouting(options, flags, cwd, write) {
|
|
|
14032
15128
|
// ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
|
|
14033
15129
|
// store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
|
|
14034
15130
|
if (options.get('_positional_0') === 'recommend') {
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
names = readdirSync(base);
|
|
14043
|
-
}
|
|
14044
|
-
catch {
|
|
14045
|
-
continue;
|
|
14046
|
-
}
|
|
14047
|
-
for (const name of names) {
|
|
14048
|
-
const sidecar = join(base, name, 'run-meta.json');
|
|
14049
|
-
if (!existsSync(sidecar))
|
|
14050
|
-
continue;
|
|
14051
|
-
try {
|
|
14052
|
-
const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
|
14053
|
-
if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records))
|
|
14054
|
-
records.push(...meta.runMeta.records);
|
|
14055
|
-
}
|
|
14056
|
-
catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
|
|
14057
|
-
}
|
|
15131
|
+
let routing;
|
|
15132
|
+
try {
|
|
15133
|
+
routing = readRoutingTelemetry(repoRoot);
|
|
15134
|
+
}
|
|
15135
|
+
catch (e) {
|
|
15136
|
+
write(`dz routing recommend: cannot read routing freshness state — ${e.message}`);
|
|
15137
|
+
return 1;
|
|
14058
15138
|
}
|
|
14059
|
-
const harvest =
|
|
14060
|
-
|
|
15139
|
+
const harvest = routing.harvest;
|
|
15140
|
+
let rec = recommendModels(harvest, {
|
|
15141
|
+
...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}),
|
|
15142
|
+
alreadyFed: routing.alreadyFed,
|
|
15143
|
+
});
|
|
14061
15144
|
if (flags.has('apply')) {
|
|
14062
15145
|
const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
|
|
14063
|
-
|
|
14064
|
-
try {
|
|
14065
|
-
alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8'));
|
|
14066
|
-
}
|
|
14067
|
-
catch { /* first apply */ }
|
|
14068
|
-
const plan = planFeed(harvest.samples, alreadyFed);
|
|
15146
|
+
const plan = planFeed(harvest.samples, routing.alreadyFed);
|
|
14069
15147
|
for (const sample of plan.toFeed)
|
|
14070
15148
|
finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
|
|
14071
15149
|
try {
|
|
@@ -14076,6 +15154,10 @@ function cmdRouting(options, flags, cwd, write) {
|
|
|
14076
15154
|
write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${e.message}`);
|
|
14077
15155
|
return 1;
|
|
14078
15156
|
}
|
|
15157
|
+
rec = recommendModels(harvest, {
|
|
15158
|
+
...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}),
|
|
15159
|
+
alreadyFed: plan.fedAfter,
|
|
15160
|
+
});
|
|
14079
15161
|
write(`dz routing recommend --apply: fed ${plan.toFeed.length} sample(s) into .dz/routing-outcomes.json${plan.skippedRuns.length > 0 ? `; skipped ${plan.skippedRuns.length} already-fed run(s) (idempotent by runId)` : ''}`);
|
|
14080
15162
|
}
|
|
14081
15163
|
if (flags.has('json')) {
|
|
@@ -14087,6 +15169,12 @@ function cmdRouting(options, flags, cwd, write) {
|
|
|
14087
15169
|
write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
|
|
14088
15170
|
}
|
|
14089
15171
|
write(` basis: ${rec.basis.rule}`);
|
|
15172
|
+
if (rec.basis.freshness === 'current')
|
|
15173
|
+
write(' store: current (0 unharvested runs)');
|
|
15174
|
+
else if (rec.basis.freshness === 'stale')
|
|
15175
|
+
write(` store: STALE — ${rec.basis.unfed.count} run(s) harvested but never fed; auto-cost is routing from an older snapshot — run \`dz routing recommend --apply\``);
|
|
15176
|
+
else
|
|
15177
|
+
write(' store: UNFED — auto-cost has no learned rows; picks below are cold-start');
|
|
14090
15178
|
write(` basis: skipped records — no result ${rec.basis.skipped.noResult}, no modelsUsed ${rec.basis.skipped.noModels} (predates model routing — history, not an error), no grade ${rec.basis.skipped.noGrade}, unknown model ${rec.basis.skipped.unknownModel}`);
|
|
14091
15179
|
write(` basis: ${rec.basis.crossFamilyNote}`);
|
|
14092
15180
|
return 0;
|
|
@@ -14795,11 +15883,11 @@ export async function runCli(argv, io = {}) {
|
|
|
14795
15883
|
case 'bundle':
|
|
14796
15884
|
return cmdBundle(options, flags, cwd, write);
|
|
14797
15885
|
case 'teach':
|
|
14798
|
-
return await cmdTeach(options, flags, cwd, write);
|
|
15886
|
+
return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
|
|
14799
15887
|
case 'consolidate':
|
|
14800
15888
|
return await cmdConsolidate(options, flags, cwd, write);
|
|
14801
15889
|
case 'recall':
|
|
14802
|
-
return await cmdRecall(options, flags, cwd, write);
|
|
15890
|
+
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
14803
15891
|
case 'vector':
|
|
14804
15892
|
return await cmdVector(options, flags, cwd, write);
|
|
14805
15893
|
case 'brain':
|
|
@@ -14852,6 +15940,8 @@ export async function runCli(argv, io = {}) {
|
|
|
14852
15940
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
14853
15941
|
case 'hooks-sync':
|
|
14854
15942
|
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
15943
|
+
case 'integrations-verify':
|
|
15944
|
+
return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
|
|
14855
15945
|
case 'agents-sync':
|
|
14856
15946
|
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
14857
15947
|
case 'sync-canonical':
|