@dzhechkov/harness-cli 0.8.16 → 0.8.17
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 +16 -16
- package/README.md +85 -9
- package/dist/boolean-flags.d.ts.map +1 -1
- package/dist/boolean-flags.js +4 -0
- package/dist/boolean-flags.js.map +1 -1
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +470 -29
- package/dist/cli.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +14 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +8 -8
- package/sbom.json +15 -15
- package/src/boolean-flags.ts +4 -0
- package/src/cli.ts +404 -29
- package/src/known-flags.ts +14 -0
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,11 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
16
16
|
import { homedir, hostname, tmpdir } from 'node:os';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { isDeepStrictEqual } from 'node:util';
|
|
19
|
+
import { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from '@dzhechkov/harness-core';
|
|
20
|
+
import { appendRunEvent, readRunRegistry, liveParents, liveness, probePid, settleDeadRuns, planRegistryArchive, planWorktreeCleanup, renderCleanupPlan, worktreeRemovalsToApply } from '@dzhechkov/harness-core';
|
|
19
21
|
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
20
22
|
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
|
|
21
|
-
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
23
|
+
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, renderFeatureAdrPhaseLine, statuslineData, countLearningStoreRowsReadonly, readStoreMark, writeStoreMark, resetStoreMark, checkStoreHealth, storeGuardPath, storeSnapshotPath, writeFeatureAdrState, writeFeatureAdrStateDetailed, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, SWARM_BRIEF_CONTRACT, checkSwarmBrief, visibleText, loadStoreRecords, findExactLesson, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, appendTransition, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
22
24
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
23
25
|
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listPackFiles, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, resolveScanTailTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, runRetroTailScan, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
|
|
24
26
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
@@ -28,7 +30,7 @@ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGo
|
|
|
28
30
|
// qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
|
|
29
31
|
KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
|
|
30
32
|
// Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
|
|
31
|
-
parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
33
|
+
parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, amendmentSectionCount, amendmentDeclarationAmbiguity, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, readFeatureTier, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
32
34
|
// operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
|
|
33
35
|
renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
|
|
34
36
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
@@ -69,11 +71,14 @@ export const DZ_COMMANDS = [
|
|
|
69
71
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
70
72
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
71
73
|
'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
|
|
72
|
-
'name-check', 'brief-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
|
|
74
|
+
'name-check', 'brief-check', 'provenance-check', 'journal', 'feature-adr-record', 'runs', 'runs-record', 'runs-clean', 'amendment-check', 'contract-check',
|
|
73
75
|
'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
|
|
74
76
|
'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
|
|
75
77
|
];
|
|
76
78
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
79
|
+
dz runs [--settle] [--stall-minutes N] [--json] [--project <dir>] [--probe-pid <pid>] (run registry: live, stalled, orphaned, inconclusive or finished; PID probe prints true|false|unknown)
|
|
80
|
+
dz runs-record --event started|heartbeat|finished [--run-id <id>] [--kind <kind>] [--slug <slug>] [--pid <pid|host>] [--parent-run-id <id>] [--outcome <outcome>] [--reason <text>] [--project <dir>] [--json] (append run event; started can allocate a run ID; host resolves the Claude ancestor PID)
|
|
81
|
+
dz runs-clean [--apply] [--retention-days N] [--project <dir>] [--json] (plan cleanup of old clean merged worktrees and completed/dead registry events; apply explicitly)
|
|
77
82
|
|
|
78
83
|
Usage:
|
|
79
84
|
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)
|
|
@@ -128,6 +133,7 @@ Usage:
|
|
|
128
133
|
dz sbom [--pack <name>] [--out <file>] (CycloneDX software bill of materials for the workspace, or for one pack with --pack)
|
|
129
134
|
dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N / AM-CP-N row must resolve to a test found INSIDE the file the row names (the challenge-panel prefix is part of the id: AM-CP-1 is never AM-1); the PLAN is authoritative when it carries rows, and an ideation amendment the plan drops is a failure. exit 0 pass/skip, 1 fail, 3 NOT-ESTABLISHED — a section that parsed ZERO rows is never a pass, UNLESS the plan explicitly declares \"None\"/\"нет\", which is an answer and reports skip. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
|
|
130
135
|
dz contract-check --slug <s> [--json] (read-only retrospective feature contract gate: extracts canonical AC-N + ADR Confirmation items, requires one artifact-anchored met|unmet|not-testable verdict per CC-N, and rejects A/B with unmet. exit 0 pass / 1 readable contract or verdict violation / 2 invalid invocation or unreadable/not-established artifacts)
|
|
136
|
+
dz journal add --kind decision|verdict|run|error|block "<text>" [--ref <trace>] [--at <ISO>] [--quote <file>] [--commit-quote]; dz journal show [--day|--week] [--at <date>] [--kind <kind>] [--json] (UTC day files, witnessed append; quotes stay local unless explicitly staged)
|
|
131
137
|
dz feature-adr-record --kind ledger|training-pair --stage <s> [--slug <s>] [--row|--pair <json>] [--mark <n>] [--once] [--json] (the witnessed writer for the run-cost ledger and training pairs: the payload arrives as an ARGUMENT, never as shell; a malformed or wrong-kind payload is REFUSED before any write; the timestamp is stamped before serialising; the append is verified by re-reading the tail. exit 0 written|duplicate|skipped, 2 refused, 3 not-verified — a record failure is never blocking)
|
|
132
138
|
dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
|
|
133
139
|
dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
|
|
@@ -3187,9 +3193,14 @@ async function cmdStoreGuard(options, flags, cwd, write, writeErr, stdinText, in
|
|
|
3187
3193
|
return 1;
|
|
3188
3194
|
}
|
|
3189
3195
|
}
|
|
3190
|
-
async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
|
|
3196
|
+
async function runTeachGuardReinforcement(projectRoot, dzId, reward, preserveQuarantine = false) {
|
|
3191
3197
|
const backend = resolveLearningBackend(projectRoot);
|
|
3192
|
-
backend.addSample({
|
|
3198
|
+
backend.addSample({
|
|
3199
|
+
dzId,
|
|
3200
|
+
kind: preserveQuarantine ? 'recall-hit' : 'reinforce',
|
|
3201
|
+
reward,
|
|
3202
|
+
ts: new Date().toISOString(),
|
|
3203
|
+
});
|
|
3193
3204
|
return backend.train();
|
|
3194
3205
|
}
|
|
3195
3206
|
async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
|
|
@@ -3438,6 +3449,15 @@ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { conso
|
|
|
3438
3449
|
}
|
|
3439
3450
|
}
|
|
3440
3451
|
}
|
|
3452
|
+
if (!flags.has('guard')) {
|
|
3453
|
+
const exact = findExactLesson(loadStoreRecords(storeRoot), pattern, domain);
|
|
3454
|
+
if (exact !== null) {
|
|
3455
|
+
await reinforceRunner(storeRoot, exact.id, Math.max(0, Math.min(1, reward)), exact.quarantined);
|
|
3456
|
+
write(`Reinforced existing: ${exact.id} (exact text)${exact.quarantined ? ' [quarantined]' : ''}`);
|
|
3457
|
+
write(` Total patterns: ${loadStorePatternsSync(storeRoot).length}`);
|
|
3458
|
+
return 0;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3441
3461
|
// Distill pattern into actionable rule (claude-smart inspired)
|
|
3442
3462
|
// Convert "what happened" into "what to do next time"
|
|
3443
3463
|
const isRule = pattern.toLowerCase().startsWith('always') || pattern.toLowerCase().startsWith('never') ||
|
|
@@ -6396,11 +6416,19 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
6396
6416
|
// A one-line preview inline; on error the FULL captured npm error prints below (truncating a publish
|
|
6397
6417
|
// failure to 60 chars is how a release stays undiagnosable — the reason lives past char 60).
|
|
6398
6418
|
const detail = pkg.error && pkg.status !== 'error' ? ` (${pkg.error.slice(0, 60)})` : '';
|
|
6399
|
-
|
|
6419
|
+
const receipt = pkg.status === 'published' && pkg.registryProbes !== undefined
|
|
6420
|
+
? ` (confirmed by registry after ${pkg.registryProbes} probes)`
|
|
6421
|
+
: '';
|
|
6422
|
+
write(` ${icon} ${pkg.name.padEnd(35)} ${pkg.oldVersion} → ${pkg.newVersion} ${pkg.status}${receipt}${detail}`);
|
|
6400
6423
|
if (pkg.status === 'error' && pkg.error) {
|
|
6401
6424
|
for (const line of pkg.error.split('\n'))
|
|
6402
6425
|
write(` ${line}`);
|
|
6403
6426
|
}
|
|
6427
|
+
// The rollback branch cannot tell "publish failed" from "publish landed, registry never answered":
|
|
6428
|
+
// both restore package.json. The operator can — one registry query — so say what to check.
|
|
6429
|
+
if (pkg.status === 'error' && pkg.error?.startsWith('registry did not confirm')) {
|
|
6430
|
+
write(` ↳ disk bump rolled back; if \`npm view ${pkg.name}@${pkg.newVersion} version\` answers, the publish DID land — re-apply the bump by hand`);
|
|
6431
|
+
}
|
|
6404
6432
|
// Surface warn-mode findings that did not block the publish.
|
|
6405
6433
|
if (pkg.claimCheck && pkg.claimCheck.findings > 0 && pkg.status !== 'error') {
|
|
6406
6434
|
write(` ⚠ claim-check: ${pkg.claimCheck.findings} finding(s) (${pkg.claimCheck.high} high) in README.md`);
|
|
@@ -13292,6 +13320,258 @@ function cmdLedgerBackfill(options, flags, cwd, write) {
|
|
|
13292
13320
|
write(` not filled: ${n} row(s) — ${reason}`);
|
|
13293
13321
|
return 0;
|
|
13294
13322
|
}
|
|
13323
|
+
const runRegistryIO = {
|
|
13324
|
+
append: (path, line) => withNamedLockSync(dirname(path), 'run-registry-archive', () => appendFileSync(path, line, 'utf8')),
|
|
13325
|
+
read: (path) => readFileSync(path, 'utf8'),
|
|
13326
|
+
mkdir: (dir) => { mkdirSync(dir, { recursive: true }); },
|
|
13327
|
+
};
|
|
13328
|
+
function cmdRuns(options, flags, cwd, write) {
|
|
13329
|
+
if (options.has('probe-pid')) {
|
|
13330
|
+
const result = probePid(Number(options.get('probe-pid')));
|
|
13331
|
+
write(result === null ? 'unknown' : String(result));
|
|
13332
|
+
return 0;
|
|
13333
|
+
}
|
|
13334
|
+
const root = resolve(options.get('project') ?? cwd);
|
|
13335
|
+
const minutes = Number(options.get('stall-minutes') ?? 120);
|
|
13336
|
+
if (!Number.isFinite(minutes) || minutes < 0) {
|
|
13337
|
+
write(flags.has('json') ? JSON.stringify({ status: 'refused', reason: 'stall-minutes must be a finite non-negative number' }) : 'stall-minutes must be a finite non-negative number');
|
|
13338
|
+
return 2;
|
|
13339
|
+
}
|
|
13340
|
+
const opts = { stallMs: minutes * 60_000 };
|
|
13341
|
+
let settled = [];
|
|
13342
|
+
if (flags.has('settle')) {
|
|
13343
|
+
settled = withNamedLockSync(join(root, '.dz', 'runs'), 'run-registry-archive', () => {
|
|
13344
|
+
const events = settleDeadRuns(readRunRegistry(root, runRegistryIO), Date.now(), probePid);
|
|
13345
|
+
// Already holding the shared archive/append lock across read-plan-append.
|
|
13346
|
+
const unlockedIO = { ...runRegistryIO, append: (path, line) => appendFileSync(path, line, 'utf8') };
|
|
13347
|
+
for (const event of events)
|
|
13348
|
+
appendRunEvent(root, event, unlockedIO);
|
|
13349
|
+
return events;
|
|
13350
|
+
});
|
|
13351
|
+
if (!flags.has('json')) {
|
|
13352
|
+
for (const event of settled)
|
|
13353
|
+
write(`settled ${event.runId} — died (${event.reason})`);
|
|
13354
|
+
if (!settled.length)
|
|
13355
|
+
write('nothing to settle');
|
|
13356
|
+
}
|
|
13357
|
+
}
|
|
13358
|
+
const registry = readRunRegistry(root, runRegistryIO);
|
|
13359
|
+
if (registry.status === 'missing') {
|
|
13360
|
+
write(flags.has('json') ? JSON.stringify(registry) : 'нет реестра: .dz/runs/registry.jsonl ещё не создан (ни одного прогона)');
|
|
13361
|
+
return 0;
|
|
13362
|
+
}
|
|
13363
|
+
const now = Date.now();
|
|
13364
|
+
const parents = liveParents(registry, now, probePid, opts);
|
|
13365
|
+
const rows = registry.runs.map((run, i) => {
|
|
13366
|
+
const parent = parents[i].liveness;
|
|
13367
|
+
const own = liveness(run, now, probePid, opts);
|
|
13368
|
+
const decision = registry.status !== 'readable' ? parent :
|
|
13369
|
+
run.finished ? { state: 'finished', reason: run.outcome } :
|
|
13370
|
+
parent.state !== 'live' && parent.state !== 'stalled' ? parent : own;
|
|
13371
|
+
return { ...run, state: decision.state, reason: decision.reason,
|
|
13372
|
+
parentLiveness: parent, heartbeatAgeMs: run.heartbeat ? now - Date.parse(run.heartbeat) : null };
|
|
13373
|
+
});
|
|
13374
|
+
if (flags.has('json'))
|
|
13375
|
+
write(JSON.stringify({ ...registry, runs: rows, ...(flags.has('settle') ? { settled } : {}) }));
|
|
13376
|
+
else {
|
|
13377
|
+
if (registry.status !== 'readable')
|
|
13378
|
+
write('inconclusive — ' + registry.reason);
|
|
13379
|
+
write('runId | kind | slug | pid | parent | liveness | heartbeat age');
|
|
13380
|
+
for (const row of rows)
|
|
13381
|
+
write([row.runId, row.kind, row.slug, row.pid, row.parentRunId ?? '—',
|
|
13382
|
+
row.state, row.heartbeatAgeMs === null ? '—' : `${row.heartbeatAgeMs}ms`].join(' | ') + ' — ' + row.reason);
|
|
13383
|
+
}
|
|
13384
|
+
return registry.status === 'readable' ? 0 : 1;
|
|
13385
|
+
}
|
|
13386
|
+
/** Git effects are injected; the core owns eligibility and the explicit apply boundary. */
|
|
13387
|
+
export function cmdRunsClean(options, flags, cwd, write, exec = execSync) {
|
|
13388
|
+
const root = resolve(options.get('project') ?? cwd);
|
|
13389
|
+
const json = flags.has('json');
|
|
13390
|
+
const messages = [];
|
|
13391
|
+
const say = (line) => { messages.push(line); if (!json)
|
|
13392
|
+
write(line); };
|
|
13393
|
+
try {
|
|
13394
|
+
const days = Number(options.get('retention-days') ?? 2);
|
|
13395
|
+
if (!Number.isFinite(days) || days < 0)
|
|
13396
|
+
throw new Error('retention-days must be a finite non-negative number');
|
|
13397
|
+
const now = Date.now(), retentionMs = days * 86400000;
|
|
13398
|
+
const git = (command, dir = root) => String(exec(command, { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
|
|
13399
|
+
// Git C-quotes paths containing controls/non-ASCII bytes. Decode octal UTF-8 as well.
|
|
13400
|
+
const unquote = (value) => {
|
|
13401
|
+
if (!value.startsWith('"'))
|
|
13402
|
+
return value;
|
|
13403
|
+
const bytes = [];
|
|
13404
|
+
const body = value.slice(1, -1);
|
|
13405
|
+
const escapes = { a: '\x07', b: '\b', t: '\t', n: '\n', v: '\v', f: '\f', r: '\r', '"': '"', '\\': '\\' };
|
|
13406
|
+
for (let i = 0; i < body.length; i++) {
|
|
13407
|
+
if (body[i] === '\\') {
|
|
13408
|
+
const octal = /^[0-7]{1,3}/.exec(body.slice(i + 1));
|
|
13409
|
+
if (octal) {
|
|
13410
|
+
bytes.push(parseInt(octal[0], 8));
|
|
13411
|
+
i += octal[0].length;
|
|
13412
|
+
}
|
|
13413
|
+
else {
|
|
13414
|
+
i++;
|
|
13415
|
+
bytes.push(...Buffer.from(escapes[body[i]] ?? body[i]));
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
else {
|
|
13419
|
+
const cp = String.fromCodePoint(body.codePointAt(i));
|
|
13420
|
+
bytes.push(...Buffer.from(cp));
|
|
13421
|
+
i += cp.length - 1;
|
|
13422
|
+
}
|
|
13423
|
+
}
|
|
13424
|
+
return Buffer.from(bytes).toString('utf8');
|
|
13425
|
+
};
|
|
13426
|
+
const list = () => {
|
|
13427
|
+
const text = git('git worktree list --porcelain');
|
|
13428
|
+
const trees = text.trimEnd().split(/\n\n+/).filter(Boolean).map(block => {
|
|
13429
|
+
const lines = block.split('\n');
|
|
13430
|
+
const path = lines.find(line => line.startsWith('worktree '))?.slice(9);
|
|
13431
|
+
const sha = lines.find(line => line.startsWith('HEAD '))?.slice(5);
|
|
13432
|
+
if (!path || !sha)
|
|
13433
|
+
throw new Error('unreadable worktree entry: ' + block);
|
|
13434
|
+
return { path: unquote(path), sha,
|
|
13435
|
+
branch: lines.find(line => line.startsWith('branch '))?.slice(7) ?? null,
|
|
13436
|
+
detached: lines.includes('detached') };
|
|
13437
|
+
});
|
|
13438
|
+
if (!trees.length)
|
|
13439
|
+
throw new Error('empty git worktree list');
|
|
13440
|
+
return trees;
|
|
13441
|
+
};
|
|
13442
|
+
const trees = list();
|
|
13443
|
+
const facts = trees.map((tree, index) => {
|
|
13444
|
+
// Any unreadable status aborts before removal; unknown cleanliness is never clean.
|
|
13445
|
+
const dirtyFiles = git('git status --short --untracked-files=all', tree.path).split('\n').filter(Boolean)
|
|
13446
|
+
.map(line => unquote(line.slice(3)));
|
|
13447
|
+
let merged = null;
|
|
13448
|
+
try {
|
|
13449
|
+
git(`git merge-base --is-ancestor ${shellQuote(tree.branch ?? tree.sha)} main`);
|
|
13450
|
+
merged = true;
|
|
13451
|
+
}
|
|
13452
|
+
catch (error) {
|
|
13453
|
+
if (error.status === 1)
|
|
13454
|
+
merged = false;
|
|
13455
|
+
}
|
|
13456
|
+
let lastCommitTs = null;
|
|
13457
|
+
try {
|
|
13458
|
+
const raw = git('git log -1 --format=%ct', tree.path).trim();
|
|
13459
|
+
if (/^\d+$/.test(raw) && Number.isFinite(Number(raw) * 1000))
|
|
13460
|
+
lastCommitTs = Number(raw) * 1000;
|
|
13461
|
+
}
|
|
13462
|
+
catch { /* unknown age keeps the worktree */ }
|
|
13463
|
+
return { path: tree.path, branch: tree.branch?.replace(/^refs\/heads\//, '') ?? null,
|
|
13464
|
+
detached: tree.detached, isMain: index === 0 || tree.branch === 'refs/heads/main', merged, dirtyFiles, lastCommitTs };
|
|
13465
|
+
});
|
|
13466
|
+
const plan = planWorktreeCleanup(facts, { now, retentionMs });
|
|
13467
|
+
for (const line of renderCleanupPlan(plan))
|
|
13468
|
+
say(line);
|
|
13469
|
+
let code = 0;
|
|
13470
|
+
for (const fact of worktreeRemovalsToApply(plan, flags.has('apply'))) {
|
|
13471
|
+
let failure;
|
|
13472
|
+
try {
|
|
13473
|
+
git(`git worktree remove ${shellQuote(fact.path)}`);
|
|
13474
|
+
}
|
|
13475
|
+
catch (error) {
|
|
13476
|
+
failure = String(error);
|
|
13477
|
+
}
|
|
13478
|
+
try {
|
|
13479
|
+
if (!list().some(tree => tree.path === fact.path))
|
|
13480
|
+
say(`removed ${fact.path} (confirmed)`);
|
|
13481
|
+
else {
|
|
13482
|
+
say(`NOT removed ${fact.path}` + (failure ? ` — ${failure}` : ''));
|
|
13483
|
+
code = 1;
|
|
13484
|
+
}
|
|
13485
|
+
}
|
|
13486
|
+
catch (error) {
|
|
13487
|
+
say(`NOT removed ${fact.path} — confirmation unavailable: ${String(error)}`);
|
|
13488
|
+
code = 1;
|
|
13489
|
+
}
|
|
13490
|
+
}
|
|
13491
|
+
const archiveRegistry = () => {
|
|
13492
|
+
const registry = readRunRegistry(root, runRegistryIO);
|
|
13493
|
+
if (registry.status === 'inconclusive')
|
|
13494
|
+
throw new Error(registry.reason);
|
|
13495
|
+
const archivePlan = planRegistryArchive(registry, { now, retentionMs, probe: probePid });
|
|
13496
|
+
if (flags.has('apply') && archivePlan.archive.length > 0) {
|
|
13497
|
+
const dir = join(root, '.dz', 'runs');
|
|
13498
|
+
const encode = (events) => events.map(ev => JSON.stringify(ev) + '\n').join('');
|
|
13499
|
+
const temp = join(dir, `registry.${randomBytes(8).toString('hex')}.tmp`);
|
|
13500
|
+
try {
|
|
13501
|
+
writeFileSync(temp, encode(archivePlan.keep), { flag: 'wx', mode: 0o600 });
|
|
13502
|
+
appendFileSync(join(dir, 'registry.archive.jsonl'), encode(archivePlan.archive));
|
|
13503
|
+
renameSync(temp, join(dir, 'registry.jsonl'));
|
|
13504
|
+
}
|
|
13505
|
+
finally {
|
|
13506
|
+
if (existsSync(temp))
|
|
13507
|
+
unlinkSync(temp);
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
return archivePlan;
|
|
13511
|
+
};
|
|
13512
|
+
const archive = flags.has('apply')
|
|
13513
|
+
? withNamedLockSync(join(root, '.dz', 'runs'), 'run-registry-archive', archiveRegistry)
|
|
13514
|
+
: archiveRegistry();
|
|
13515
|
+
say(`${flags.has('apply') ? 'archived' : 'archive'} ${archive.archive.length} registry event(s)`);
|
|
13516
|
+
if (json)
|
|
13517
|
+
write(JSON.stringify({ ...plan, applied: flags.has('apply'), archive, messages, ok: code === 0 }));
|
|
13518
|
+
return code;
|
|
13519
|
+
}
|
|
13520
|
+
catch (error) {
|
|
13521
|
+
const reason = String(error);
|
|
13522
|
+
if (json)
|
|
13523
|
+
write(JSON.stringify({ ok: false, reason, messages }));
|
|
13524
|
+
else
|
|
13525
|
+
write('runs-clean refused: ' + reason);
|
|
13526
|
+
return 1;
|
|
13527
|
+
}
|
|
13528
|
+
}
|
|
13529
|
+
function cmdRunsRecord(options, flags, cwd, write) {
|
|
13530
|
+
try {
|
|
13531
|
+
const event = options.get('event');
|
|
13532
|
+
const runId = options.get('run-id') ?? (event === 'started' ? randomBytes(16).toString('hex') : '');
|
|
13533
|
+
const ev = { event, runId, ts: new Date().toISOString() };
|
|
13534
|
+
if (event === 'started') {
|
|
13535
|
+
const kind = options.get('kind');
|
|
13536
|
+
const slug = options.get('slug');
|
|
13537
|
+
Object.assign(ev, { ...(kind === undefined ? {} : { kind }), ...(slug === undefined ? {} : { slug }) });
|
|
13538
|
+
const rawPid = options.get('pid');
|
|
13539
|
+
if (rawPid === 'host') {
|
|
13540
|
+
// The shell agent is transient. Walk to the nearest Claude host, never guess from age.
|
|
13541
|
+
let candidate = process.ppid;
|
|
13542
|
+
const seen = new Set();
|
|
13543
|
+
while (candidate > 1 && !seen.has(candidate)) {
|
|
13544
|
+
seen.add(candidate);
|
|
13545
|
+
const row = execFileSync('ps', ['-p', String(candidate), '-o', 'ppid=', '-o', 'comm='], { encoding: 'utf8' }).trim();
|
|
13546
|
+
const match = /^(\d+)\s+(.+)$/.exec(row);
|
|
13547
|
+
if (!match)
|
|
13548
|
+
break;
|
|
13549
|
+
if (/^claude(?:$|[- ])/.test(basename(match[2]))) {
|
|
13550
|
+
ev.pid = candidate;
|
|
13551
|
+
break;
|
|
13552
|
+
}
|
|
13553
|
+
candidate = Number(match[1]);
|
|
13554
|
+
}
|
|
13555
|
+
if (ev.pid === undefined)
|
|
13556
|
+
throw new Error('Claude host PID unavailable; pass explicit --pid (Workflow args.runPid)');
|
|
13557
|
+
}
|
|
13558
|
+
else
|
|
13559
|
+
ev.pid = Number(rawPid);
|
|
13560
|
+
const parentRunId = options.get('parent-run-id');
|
|
13561
|
+
Object.assign(ev, { ...(parentRunId === undefined ? {} : { parentRunId }) });
|
|
13562
|
+
}
|
|
13563
|
+
const outcome = event === 'finished' ? options.get('outcome') : undefined;
|
|
13564
|
+
const reason = options.get('reason');
|
|
13565
|
+
Object.assign(ev, { ...(outcome === undefined ? {} : { outcome }), ...(reason === undefined ? {} : { reason }) });
|
|
13566
|
+
appendRunEvent(resolve(options.get('project') ?? cwd), ev, runRegistryIO);
|
|
13567
|
+
write(flags.has('json') ? JSON.stringify({ status: 'written', ...ev }) : `written ${event} ${runId}`);
|
|
13568
|
+
return 0;
|
|
13569
|
+
}
|
|
13570
|
+
catch (error) {
|
|
13571
|
+
write(flags.has('json') ? JSON.stringify({ status: 'refused', reason: String(error) }) : `refused: ${String(error)}`);
|
|
13572
|
+
return 2;
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
13295
13575
|
function cmdFeatureAdrRecord(options, flags, cwd, write) {
|
|
13296
13576
|
const json = flags.has('json');
|
|
13297
13577
|
// `--backfill` is a different verb on the same store: it fills the ledger's null cost fields from
|
|
@@ -13477,15 +13757,28 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
|
|
|
13477
13757
|
catch {
|
|
13478
13758
|
return {
|
|
13479
13759
|
ok: false,
|
|
13760
|
+
reason: 'unreadable',
|
|
13480
13761
|
diagnostic: { code: 'repository-unreadable', message: 'repository root cannot be resolved', artifact: '.' },
|
|
13481
13762
|
};
|
|
13482
13763
|
}
|
|
13764
|
+
try {
|
|
13765
|
+
lstatSync(absolute);
|
|
13766
|
+
}
|
|
13767
|
+
catch (error) {
|
|
13768
|
+
const missing = error.code === 'ENOENT';
|
|
13769
|
+
return {
|
|
13770
|
+
ok: false,
|
|
13771
|
+
reason: missing ? 'missing' : 'unreadable',
|
|
13772
|
+
diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory cannot be resolved: ${artifact}`, artifact },
|
|
13773
|
+
};
|
|
13774
|
+
}
|
|
13483
13775
|
try {
|
|
13484
13776
|
targetReal = realpathSync(absolute);
|
|
13485
13777
|
}
|
|
13486
13778
|
catch {
|
|
13487
13779
|
return {
|
|
13488
13780
|
ok: false,
|
|
13781
|
+
reason: 'unreadable',
|
|
13489
13782
|
diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory cannot be resolved: ${artifact}`, artifact },
|
|
13490
13783
|
};
|
|
13491
13784
|
}
|
|
@@ -13493,6 +13786,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
|
|
|
13493
13786
|
if (rel === '' || rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) {
|
|
13494
13787
|
return {
|
|
13495
13788
|
ok: false,
|
|
13789
|
+
reason: 'unreadable',
|
|
13496
13790
|
diagnostic: { code: 'artifact-outside-repository', message: `artifact resolves outside the repository: ${artifact}`, artifact },
|
|
13497
13791
|
};
|
|
13498
13792
|
}
|
|
@@ -13504,6 +13798,7 @@ function contractDirectoryConfined(repoRoot, absolute, artifact) {
|
|
|
13504
13798
|
catch {
|
|
13505
13799
|
return {
|
|
13506
13800
|
ok: false,
|
|
13801
|
+
reason: 'unreadable',
|
|
13507
13802
|
diagnostic: { code: 'adr-directory-unreadable', message: `required ADR directory is not readable: ${artifact}`, artifact },
|
|
13508
13803
|
};
|
|
13509
13804
|
}
|
|
@@ -13556,30 +13851,63 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
|
|
|
13556
13851
|
return emitEarly('not-established', 2, [requirementsRead.diagnostic]);
|
|
13557
13852
|
const adrDirRel = `features/${slug}/03_adr`;
|
|
13558
13853
|
const adrDirectory = contractDirectoryConfined(repoRoot, join(featureDir, '03_adr'), adrDirRel);
|
|
13559
|
-
|
|
13560
|
-
return emitEarly('not-established', 2, [adrDirectory.diagnostic]);
|
|
13561
|
-
const adrDir = adrDirectory.realPath;
|
|
13854
|
+
const informationalDiagnostics = [];
|
|
13562
13855
|
let adrNames;
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13856
|
+
let adrDir = null;
|
|
13857
|
+
let adrsOptional;
|
|
13858
|
+
if (!adrDirectory.ok) {
|
|
13859
|
+
if (adrDirectory.reason !== 'missing')
|
|
13860
|
+
return emitEarly('not-established', 2, [adrDirectory.diagnostic]);
|
|
13861
|
+
const tier = readFeatureTier((rel) => {
|
|
13862
|
+
const assessment = contractReadConfined(repoRoot, join(repoRoot, rel), rel);
|
|
13863
|
+
return assessment.ok ? assessment.text : null;
|
|
13864
|
+
}, slug);
|
|
13865
|
+
if (tier === 'S') {
|
|
13866
|
+
adrNames = [];
|
|
13867
|
+
adrsOptional = true;
|
|
13868
|
+
informationalDiagnostics.push({
|
|
13869
|
+
code: 'adr-not-required-for-tier',
|
|
13870
|
+
message: 'ADR artifacts are not required for an S-tier feature',
|
|
13573
13871
|
artifact: adrDirRel,
|
|
13574
|
-
}
|
|
13872
|
+
});
|
|
13873
|
+
}
|
|
13874
|
+
else if (tier !== null) {
|
|
13875
|
+
return emitEarly('not-established', 2, [{
|
|
13876
|
+
code: 'adr-directory-missing',
|
|
13877
|
+
message: `required ADR directory is missing for ${tier}-tier feature: ${adrDirRel}`,
|
|
13878
|
+
artifact: adrDirRel,
|
|
13879
|
+
}]);
|
|
13880
|
+
}
|
|
13881
|
+
else {
|
|
13882
|
+
return emitEarly('not-established', 2, [{
|
|
13883
|
+
...adrDirectory.diagnostic,
|
|
13884
|
+
message: `${adrDirectory.diagnostic.message}; feature tier is unreadable`,
|
|
13885
|
+
}]);
|
|
13886
|
+
}
|
|
13575
13887
|
}
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13888
|
+
else {
|
|
13889
|
+
adrDir = adrDirectory.realPath;
|
|
13890
|
+
try {
|
|
13891
|
+
adrNames = readdirSync(adrDir, { withFileTypes: true })
|
|
13892
|
+
.filter((entry) => entry.name.endsWith('.md') && (entry.isFile() || entry.isSymbolicLink()))
|
|
13893
|
+
.map((entry) => entry.name)
|
|
13894
|
+
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
13895
|
+
}
|
|
13896
|
+
catch {
|
|
13897
|
+
return emitEarly('not-established', 2, [{
|
|
13898
|
+
code: 'adr-directory-unreadable',
|
|
13899
|
+
message: `required ADR directory cannot be read: ${adrDirRel}`,
|
|
13900
|
+
artifact: adrDirRel,
|
|
13901
|
+
}]);
|
|
13902
|
+
}
|
|
13903
|
+
if (adrNames.length === 0) {
|
|
13904
|
+
return emitEarly('not-established', 2, [{
|
|
13905
|
+
code: 'adr-artifacts-missing',
|
|
13906
|
+
message: `no direct ADR Markdown artifacts exist under ${adrDirRel}`,
|
|
13907
|
+
artifact: adrDirRel,
|
|
13908
|
+
observed: 0,
|
|
13909
|
+
}]);
|
|
13910
|
+
}
|
|
13583
13911
|
}
|
|
13584
13912
|
const adrs = [];
|
|
13585
13913
|
const adrReadErrors = [];
|
|
@@ -13596,6 +13924,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
|
|
|
13596
13924
|
const extracted = extractContractChecklist({
|
|
13597
13925
|
requirements: { path: requirementsRel, text: requirementsRead.text },
|
|
13598
13926
|
adrs,
|
|
13927
|
+
...(adrsOptional === true ? { adrsOptional: true } : {}),
|
|
13599
13928
|
});
|
|
13600
13929
|
if (!extracted.ok)
|
|
13601
13930
|
return emitEarly('fail', 1, extracted.diagnostics);
|
|
@@ -13639,7 +13968,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
|
|
|
13639
13968
|
});
|
|
13640
13969
|
const verification = {
|
|
13641
13970
|
...rawVerification,
|
|
13642
|
-
diagnostics: rawVerification.diagnostics.map(withReportArtifact),
|
|
13971
|
+
diagnostics: [...informationalDiagnostics, ...rawVerification.diagnostics.map(withReportArtifact)],
|
|
13643
13972
|
items: rawVerification.items.map((item) => ({
|
|
13644
13973
|
...item,
|
|
13645
13974
|
diagnostics: item.diagnostics.map(withReportArtifact),
|
|
@@ -13668,7 +13997,7 @@ function cmdContractCheck(options, flags, cwd, write, writeErr) {
|
|
|
13668
13997
|
writeErr(contractDiagnosticLine(entry));
|
|
13669
13998
|
const summary = verification.outcome === 'pass'
|
|
13670
13999
|
? `${verification.counts.met} contract item(s) met`
|
|
13671
|
-
: `${
|
|
14000
|
+
: `${rawVerification.diagnostics.length} contract or evidence violation(s)`;
|
|
13672
14001
|
write(`contract-check: ${verification.outcome === 'pass' ? 'PASS' : 'FAIL'} — ${summary}`);
|
|
13673
14002
|
return verification.exitCode;
|
|
13674
14003
|
}
|
|
@@ -17222,6 +17551,110 @@ async function cmdProfile(options, flags, write, writeErr) {
|
|
|
17222
17551
|
}
|
|
17223
17552
|
return fail(2, `dz profile: unknown subcommand ${JSON.stringify(sub)} — accepted: init | show | set | sync`);
|
|
17224
17553
|
}
|
|
17554
|
+
function cmdJournal(options, flags, cwd, write, injectedIo) {
|
|
17555
|
+
try {
|
|
17556
|
+
const verb = options.get('_positional_0');
|
|
17557
|
+
if (verb !== 'add' && verb !== 'show')
|
|
17558
|
+
throw new Error('Ожидается journal add или journal show');
|
|
17559
|
+
const kindRaw = options.get('kind');
|
|
17560
|
+
if ((verb === 'add' || kindRaw !== undefined) && !JOURNAL_KINDS.includes(kindRaw)) {
|
|
17561
|
+
write(`Категория должна быть одной из: ${JOURNAL_KINDS.join(', ')}`);
|
|
17562
|
+
return 1;
|
|
17563
|
+
}
|
|
17564
|
+
if (flags.has('day') && flags.has('week'))
|
|
17565
|
+
throw new Error('Выберите --day или --week');
|
|
17566
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
17567
|
+
const dir = join(root, 'docs', 'journal');
|
|
17568
|
+
// A journal symlink must never redirect an append or a quote outside this repository.
|
|
17569
|
+
for (const path of [join(root, 'docs'), dir, join(dir, 'quotes')]) {
|
|
17570
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink())
|
|
17571
|
+
throw new Error(`Символическая ссылка запрещена: ${path}`);
|
|
17572
|
+
}
|
|
17573
|
+
const at = options.get('at') ?? (verb === 'add' ? new Date().toISOString() : new Date().toISOString().slice(0, 10));
|
|
17574
|
+
let date = at;
|
|
17575
|
+
let time = '';
|
|
17576
|
+
if (verb === 'add') {
|
|
17577
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(at))
|
|
17578
|
+
throw new Error('--at ожидает ISO с часовым поясом');
|
|
17579
|
+
selectWindow([], at.slice(0, 10), false);
|
|
17580
|
+
const stamp = new Date(at).toISOString();
|
|
17581
|
+
date = stamp.slice(0, 10);
|
|
17582
|
+
time = stamp.slice(11, 16);
|
|
17583
|
+
}
|
|
17584
|
+
selectWindow([], date, false);
|
|
17585
|
+
if (verb === 'show') {
|
|
17586
|
+
const days = existsSync(dir) ? readdirSync(dir).filter(n => /^\d{4}-\d{2}-\d{2}\.md$/.test(n)).map(n => n.slice(0, 10)) : [];
|
|
17587
|
+
const rows = selectWindow(days, date, flags.has('week')).flatMap(day => {
|
|
17588
|
+
return readFileSync(join(dir, `${day}.md`), 'utf8').split('\n').filter(line => line.startsWith('- '))
|
|
17589
|
+
.map(raw => ({ day, ...parseLine(raw) }));
|
|
17590
|
+
}).filter(row => row.status === 'unparsed' || kindRaw === undefined || row.kind === kindRaw);
|
|
17591
|
+
if (flags.has('json'))
|
|
17592
|
+
write(JSON.stringify(rows));
|
|
17593
|
+
else
|
|
17594
|
+
for (const row of rows)
|
|
17595
|
+
write(row.status === 'unparsed' ? `[unparsed] ${row.raw}` : row.raw);
|
|
17596
|
+
return 0;
|
|
17597
|
+
}
|
|
17598
|
+
const text = options.get('_positional_1') ?? '';
|
|
17599
|
+
let ref = options.get('ref') ?? '';
|
|
17600
|
+
const event = { time, kind: kindRaw, text, ref };
|
|
17601
|
+
formatLine(event); // validate before any journal/quote write
|
|
17602
|
+
if (options.has('_positional_2'))
|
|
17603
|
+
throw new Error('Текст должен быть одним аргументом');
|
|
17604
|
+
if (flags.has('commit-quote') && !options.has('quote'))
|
|
17605
|
+
throw new Error('--commit-quote требует --quote');
|
|
17606
|
+
return withNamedLockSync(root, 'journal', () => {
|
|
17607
|
+
const path = join(dir, `${date}.md`);
|
|
17608
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink())
|
|
17609
|
+
throw new Error(`Символическая ссылка запрещена: ${path}`);
|
|
17610
|
+
const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
17611
|
+
if (previous.split('\n').map(parseLine).some(row => row.status === 'parsed' && row.time === time && row.text === text.trim())) {
|
|
17612
|
+
write('уже записано');
|
|
17613
|
+
return 0;
|
|
17614
|
+
}
|
|
17615
|
+
const quote = options.get('quote');
|
|
17616
|
+
if (quote !== undefined) {
|
|
17617
|
+
const content = readFileSync(resolve(cwd, quote), 'utf8');
|
|
17618
|
+
const quotes = join(dir, 'quotes');
|
|
17619
|
+
const exclude = execFileSync('git', ['rev-parse', '--git-path', 'info/exclude'], { cwd: root, encoding: 'utf8' }).trim();
|
|
17620
|
+
const excludePath = resolve(root, exclude);
|
|
17621
|
+
const ignored = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
|
|
17622
|
+
if (!ignored.split('\n').includes('/docs/journal/quotes/')) {
|
|
17623
|
+
mkdirSync(dirname(excludePath), { recursive: true });
|
|
17624
|
+
appendFileSync(excludePath, `${ignored.endsWith('\n') || !ignored ? '' : '\n'}/docs/journal/quotes/\n`);
|
|
17625
|
+
}
|
|
17626
|
+
mkdirSync(quotes, { recursive: true });
|
|
17627
|
+
const tracked = new Set(execFileSync('git', ['ls-files', '-z', '--', 'docs/journal/quotes/'], { cwd: root, encoding: 'utf8' }).split('\0'));
|
|
17628
|
+
let n = 1;
|
|
17629
|
+
while (existsSync(join(quotes, `${date}-${n}.md`)) || tracked.has(`docs/journal/quotes/${date}-${n}.md`))
|
|
17630
|
+
n++;
|
|
17631
|
+
const quoteRef = `docs/journal/quotes/${date}-${n}.md`;
|
|
17632
|
+
const quotePath = join(root, quoteRef);
|
|
17633
|
+
writeFileSync(quotePath, content, { flag: 'wx' });
|
|
17634
|
+
if (readFileSync(quotePath, 'utf8') !== content)
|
|
17635
|
+
throw new Error('Цитата не засвидетельствована');
|
|
17636
|
+
if (flags.has('commit-quote')) {
|
|
17637
|
+
execFileSync('git', ['add', '-f', '--', quoteRef], { cwd: root, stdio: 'pipe' });
|
|
17638
|
+
}
|
|
17639
|
+
ref = ref ? `${ref}; ${quoteRef}` : quoteRef;
|
|
17640
|
+
}
|
|
17641
|
+
mkdirSync(dir, { recursive: true });
|
|
17642
|
+
if (!existsSync(path))
|
|
17643
|
+
writeFileSync(path, `# Журнал действий — ${date}\n\n## Решения · вердикты · запуски и падения · ошибки · блокировки (UTC)\n\n`, { flag: 'wx' });
|
|
17644
|
+
else if (previous && !previous.endsWith('\n'))
|
|
17645
|
+
appendFileSync(path, '\n');
|
|
17646
|
+
const fs = injectedIo ?? { read: p => readFileSync(p, 'utf8'), append: (p, s) => appendFileSync(p, s) };
|
|
17647
|
+
const line = formatLine({ ...event, ref });
|
|
17648
|
+
appendWitnessed(fs, path, line);
|
|
17649
|
+
write(`записано и засвидетельствовано: ${line}`);
|
|
17650
|
+
return 0;
|
|
17651
|
+
});
|
|
17652
|
+
}
|
|
17653
|
+
catch (error) {
|
|
17654
|
+
write(`dz journal: ${error instanceof Error ? error.message : String(error)}`);
|
|
17655
|
+
return 1;
|
|
17656
|
+
}
|
|
17657
|
+
}
|
|
17225
17658
|
export async function runCli(argv, io = {}) {
|
|
17226
17659
|
const cwd = io.cwd ?? process.cwd();
|
|
17227
17660
|
const write = io.write ?? ((line) => { console.log(line); });
|
|
@@ -17472,8 +17905,16 @@ export async function runCli(argv, io = {}) {
|
|
|
17472
17905
|
return cmdBriefCheck(options, flags, cwd, write);
|
|
17473
17906
|
case 'provenance-check':
|
|
17474
17907
|
return cmdProvenanceCheck(options, flags, cwd, write);
|
|
17908
|
+
case 'journal':
|
|
17909
|
+
return cmdJournal(options, flags, cwd, write, io.journalIo);
|
|
17475
17910
|
case 'feature-adr-record':
|
|
17476
17911
|
return cmdFeatureAdrRecord(options, flags, cwd, write);
|
|
17912
|
+
case 'runs':
|
|
17913
|
+
return cmdRuns(options, flags, cwd, write);
|
|
17914
|
+
case 'runs-clean':
|
|
17915
|
+
return cmdRunsClean(options, flags, cwd, write);
|
|
17916
|
+
case 'runs-record':
|
|
17917
|
+
return cmdRunsRecord(options, flags, cwd, write);
|
|
17477
17918
|
case 'amendment-check':
|
|
17478
17919
|
return cmdAmendmentCheck(options, flags, cwd, write);
|
|
17479
17920
|
case 'contract-check':
|