@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/src/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { execFile, execFileSync, execSync, spawn, spawnSync, type ChildProcess }
|
|
|
15
15
|
import { createHash, randomBytes } from 'node:crypto';
|
|
16
16
|
import { homedir, tmpdir } from 'node:os';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
18
19
|
|
|
19
20
|
import {
|
|
20
21
|
createSkill,
|
|
@@ -28,6 +29,8 @@ import {
|
|
|
28
29
|
TARGET_NAMES_SORTED,
|
|
29
30
|
runDoctor,
|
|
30
31
|
runInit,
|
|
32
|
+
discoverSkillIds,
|
|
33
|
+
runIntegrationsVerify,
|
|
31
34
|
resolvePackageSkillRoots,
|
|
32
35
|
PACKAGE_SKILL_LAYOUTS,
|
|
33
36
|
benchmarkSkill,
|
|
@@ -80,6 +83,7 @@ import {
|
|
|
80
83
|
NamedLockTimeoutError,
|
|
81
84
|
NamedLockCompromisedError,
|
|
82
85
|
type CodexHooksSyncReport,
|
|
86
|
+
type IntegrationManifestSource,
|
|
83
87
|
type ParityCell,
|
|
84
88
|
type ParityFeature,
|
|
85
89
|
type ParityReportCell,
|
|
@@ -115,6 +119,8 @@ import {
|
|
|
115
119
|
renderTimelineHtml,
|
|
116
120
|
importEcc,
|
|
117
121
|
recordPattern,
|
|
122
|
+
recordLessonForms,
|
|
123
|
+
normalizeLessonForms,
|
|
118
124
|
resolveLearningBackend,
|
|
119
125
|
storeStats,
|
|
120
126
|
consolidateSessions,
|
|
@@ -140,6 +146,17 @@ import {
|
|
|
140
146
|
importRvfCheckpoint,
|
|
141
147
|
statuslineData,
|
|
142
148
|
writeFeatureAdrState,
|
|
149
|
+
CHECKPOINT_STAGES,
|
|
150
|
+
estimateEta,
|
|
151
|
+
extractStageSamples,
|
|
152
|
+
formatEta,
|
|
153
|
+
parseCheckpointLines,
|
|
154
|
+
segmentRun,
|
|
155
|
+
type CheckpointStage,
|
|
156
|
+
type EtaEstimate,
|
|
157
|
+
type FeatureAdrState,
|
|
158
|
+
type RunSegment,
|
|
159
|
+
type StageSample,
|
|
143
160
|
computeUsage,
|
|
144
161
|
deriveCostLedger,
|
|
145
162
|
planLedgerBackfill,
|
|
@@ -166,6 +183,8 @@ import {
|
|
|
166
183
|
queryBookKnowledge,
|
|
167
184
|
loadStorePatternsSync,
|
|
168
185
|
patternRecordId,
|
|
186
|
+
patternIdentityOf,
|
|
187
|
+
mergeLessonMatchedForms,
|
|
169
188
|
loadStoreRecords,
|
|
170
189
|
recordToPattern,
|
|
171
190
|
bundleSkills,
|
|
@@ -203,12 +222,22 @@ import {
|
|
|
203
222
|
DEFAULT_RULES,
|
|
204
223
|
parsePnpmLockImporters,
|
|
205
224
|
scannableStubPath,
|
|
225
|
+
type FeatureArtifactFact,
|
|
226
|
+
type FeatureTier,
|
|
227
|
+
type FeatureVolumeFact,
|
|
228
|
+
type GuardObservation,
|
|
229
|
+
type TemplateVolumeFileFact,
|
|
230
|
+
type TemplateVolumeTargetFact,
|
|
231
|
+
type VolumeShadowInput,
|
|
206
232
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
207
233
|
assembleCandidates,
|
|
208
234
|
renderPromotionReport,
|
|
209
235
|
renderPromotionAdr,
|
|
210
236
|
normalizePromotionState,
|
|
211
237
|
nextPromotionState,
|
|
238
|
+
recordPromotionRunEvidence,
|
|
239
|
+
isLessonRuleContentAnchor,
|
|
240
|
+
isOffsetIsoTimestamp,
|
|
212
241
|
globMatch,
|
|
213
242
|
promotionAdrRelPath,
|
|
214
243
|
DEFAULT_WINDOW_DAYS,
|
|
@@ -218,6 +247,10 @@ import {
|
|
|
218
247
|
type ChangeSet,
|
|
219
248
|
type ExistingRuleView,
|
|
220
249
|
type PromotionReport,
|
|
250
|
+
type PromotionAcceptanceEvidence,
|
|
251
|
+
type PromotionRunEvidence,
|
|
252
|
+
type FunnelEvidenceSource,
|
|
253
|
+
type GuardEvent,
|
|
221
254
|
decideProvenance,
|
|
222
255
|
isInsideTree,
|
|
223
256
|
signManifest,
|
|
@@ -283,6 +316,7 @@ import {
|
|
|
283
316
|
harvestStageOutcomes,
|
|
284
317
|
recommendModels,
|
|
285
318
|
planFeed,
|
|
319
|
+
unfedRuns,
|
|
286
320
|
GRADE_SUCCESS_FLOOR,
|
|
287
321
|
COST_LADDER,
|
|
288
322
|
splitScenarios,
|
|
@@ -520,7 +554,7 @@ import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
|
520
554
|
import type { LogTail } from '@dzhechkov/harness-core';
|
|
521
555
|
import type { DeadwoodInventoryItem } from '@dzhechkov/harness-core';
|
|
522
556
|
import type { ContractDiagnostic, ContractEvidenceReader } from '@dzhechkov/harness-core';
|
|
523
|
-
import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow, GateExecution, GateStep, SlopFinding, SlopLintConfig, SlopRegistry } from '@dzhechkov/harness-core';
|
|
557
|
+
import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, RecallPatternsOptions, TeachGuardResult, TargetName, IntegrationOutcome, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow, GateExecution, GateStep, SlopFinding, SlopLintConfig, SlopRegistry } from '@dzhechkov/harness-core';
|
|
524
558
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
525
559
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
526
560
|
|
|
@@ -532,7 +566,7 @@ export const DZ_COMMANDS: readonly string[] = [
|
|
|
532
566
|
'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
|
|
533
567
|
'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
|
|
534
568
|
'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
|
|
535
|
-
'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
|
|
569
|
+
'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
|
|
536
570
|
'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
|
|
537
571
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
538
572
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
@@ -545,7 +579,7 @@ export const DZ_COMMANDS: readonly string[] = [
|
|
|
545
579
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
546
580
|
|
|
547
581
|
Usage:
|
|
548
|
-
dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--no-hooks] [--no-verify] (
|
|
582
|
+
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)
|
|
549
583
|
dz verify [--skills-dir <dir>] [--target <name>]
|
|
550
584
|
dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
|
|
551
585
|
dz update (alias of sync)
|
|
@@ -574,7 +608,7 @@ Usage:
|
|
|
574
608
|
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)
|
|
575
609
|
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)
|
|
576
610
|
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)
|
|
577
|
-
dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool
|
|
611
|
+
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)
|
|
578
612
|
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)
|
|
579
613
|
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)
|
|
580
614
|
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)
|
|
@@ -609,13 +643,13 @@ Usage:
|
|
|
609
643
|
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)
|
|
610
644
|
dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
|
|
611
645
|
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)
|
|
612
|
-
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
|
|
646
|
+
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)
|
|
613
647
|
dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
|
|
614
648
|
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)
|
|
615
649
|
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)
|
|
616
650
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
617
651
|
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)
|
|
618
|
-
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
|
|
652
|
+
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)
|
|
619
653
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
620
654
|
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)
|
|
621
655
|
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)
|
|
@@ -654,6 +688,7 @@ Usage:
|
|
|
654
688
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
655
689
|
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)
|
|
656
690
|
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)
|
|
691
|
+
dz integrations-verify --target <name> --component <mcp|hooks> [--project <dir>] [--json] (non-executing exact-version registration probe; exit 0 observed / 1 refused)
|
|
657
692
|
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)
|
|
658
693
|
dz plugin [--version <ver>]
|
|
659
694
|
dz downloads
|
|
@@ -704,6 +739,14 @@ export interface CliIo {
|
|
|
704
739
|
* command that needs it (`brain ground`), and never when stdin is a TTY (nothing piped).
|
|
705
740
|
*/
|
|
706
741
|
readonly stdin?: string;
|
|
742
|
+
/** Human-terminal rendering seam; production defaults to stdout TTY detection. */
|
|
743
|
+
readonly interactive?: boolean;
|
|
744
|
+
/** Fault seam proving that class-form recall degrades to specific recall with a stderr receipt. */
|
|
745
|
+
readonly classMatcher?: RecallPatternsOptions['classMatcher'];
|
|
746
|
+
/** Guard decision seam; production always uses the real vector-backed teach guard. */
|
|
747
|
+
readonly teachGuardRunner?: (projectRoot: string, text: string, opts: { readonly reward?: number }) => Promise<TeachGuardResult>;
|
|
748
|
+
/** Reinforcement flush seam paired with `teachGuardRunner`; production uses the configured backend. */
|
|
749
|
+
readonly teachReinforceRunner?: (projectRoot: string, dzId: string, reward: number) => Promise<{ readonly flushed: number }>;
|
|
707
750
|
/**
|
|
708
751
|
* Test seam for `dz release`: overrides subprocess execution for gate steps and the
|
|
709
752
|
* gh/git side channels (production leaves it unset → real `execSync`, stdio piped).
|
|
@@ -821,6 +864,8 @@ interface InstallSkillsResult {
|
|
|
821
864
|
* the source `SKILL.md`.
|
|
822
865
|
*/
|
|
823
866
|
readonly applyFailures: SkillApplyFailure[];
|
|
867
|
+
readonly integrations: readonly IntegrationOutcome[];
|
|
868
|
+
readonly integrationDigest?: string;
|
|
824
869
|
}
|
|
825
870
|
|
|
826
871
|
/**
|
|
@@ -836,6 +881,10 @@ async function installSkills(opts: {
|
|
|
836
881
|
select?: readonly string[] | undefined;
|
|
837
882
|
force: boolean;
|
|
838
883
|
enrich: boolean;
|
|
884
|
+
noHooks?: boolean;
|
|
885
|
+
noIntegrations?: boolean;
|
|
886
|
+
noVerify?: boolean;
|
|
887
|
+
allowIntegrations?: string;
|
|
839
888
|
}): Promise<InstallSkillsResult> {
|
|
840
889
|
const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
|
|
841
890
|
|
|
@@ -848,8 +897,8 @@ async function installSkills(opts: {
|
|
|
848
897
|
// route them through one aggregation.
|
|
849
898
|
if (target === 'agents-md' || target === 'gemini') {
|
|
850
899
|
const report = target === 'gemini'
|
|
851
|
-
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
|
|
852
|
-
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) });
|
|
900
|
+
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) })
|
|
901
|
+
: runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) });
|
|
853
902
|
const results = report.skills.map((s) => ({
|
|
854
903
|
id: s.id,
|
|
855
904
|
written: s.written.length,
|
|
@@ -858,13 +907,21 @@ async function installSkills(opts: {
|
|
|
858
907
|
let written = 0;
|
|
859
908
|
let skipped = 0;
|
|
860
909
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
861
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
|
|
910
|
+
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 } : {}) };
|
|
862
911
|
}
|
|
863
912
|
|
|
864
913
|
const results: { id: string; written: number; skipped: number }[] = [];
|
|
865
914
|
const failures: SkillLoadFailure[] = [];
|
|
866
915
|
const applyFailures: SkillApplyFailure[] = [];
|
|
867
|
-
|
|
916
|
+
let integrations: readonly IntegrationOutcome[] = [];
|
|
917
|
+
let integrationDigest: string | undefined;
|
|
918
|
+
const integrationManifestSources: readonly IntegrationManifestSource[] = skillsDirs.flatMap((skillsDir) => {
|
|
919
|
+
const discovered = discoverSkillIds(skillsDir);
|
|
920
|
+
return discovered
|
|
921
|
+
.filter((id) => select === undefined || select.includes(id))
|
|
922
|
+
.map((skillId) => ({ skillId, skillDir: skillsDir }));
|
|
923
|
+
});
|
|
924
|
+
for (const [dirIndex, skillsDir] of skillsDirs.entries()) {
|
|
868
925
|
const r = await runInit({
|
|
869
926
|
target,
|
|
870
927
|
skillsDir,
|
|
@@ -872,12 +929,24 @@ async function installSkills(opts: {
|
|
|
872
929
|
force,
|
|
873
930
|
enrich,
|
|
874
931
|
...(select !== undefined ? { select } : {}),
|
|
932
|
+
...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}),
|
|
933
|
+
...(dirIndex === 0
|
|
934
|
+
? {
|
|
935
|
+
integrationManifestSources,
|
|
936
|
+
...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}),
|
|
937
|
+
}
|
|
938
|
+
: { noIntegrations: true }),
|
|
939
|
+
...(opts.noVerify !== undefined ? { noVerify: opts.noVerify } : {}),
|
|
940
|
+
...(opts.allowIntegrations !== undefined ? { allowIntegrations: opts.allowIntegrations } : {}),
|
|
875
941
|
});
|
|
876
942
|
for (const skill of r.skills) {
|
|
877
943
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
878
944
|
}
|
|
879
945
|
failures.push(...r.failures);
|
|
880
946
|
applyFailures.push(...r.applyFailures);
|
|
947
|
+
if (r.integrations.some((row) => row.status !== 'not-requested')) integrations = [...r.integrations];
|
|
948
|
+
else if (integrations.length === 0) integrations = [...r.integrations];
|
|
949
|
+
if (r.integrationDigest !== undefined) integrationDigest = r.integrationDigest;
|
|
881
950
|
}
|
|
882
951
|
|
|
883
952
|
let written = 0;
|
|
@@ -885,7 +954,7 @@ async function installSkills(opts: {
|
|
|
885
954
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
886
955
|
const installed = new Set(results.map((s) => s.id));
|
|
887
956
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
888
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
|
|
957
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures, integrations, ...(integrationDigest !== undefined ? { integrationDigest } : {}) };
|
|
889
958
|
}
|
|
890
959
|
|
|
891
960
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
@@ -947,26 +1016,46 @@ async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
947
1016
|
select,
|
|
948
1017
|
force: flags.has('force'),
|
|
949
1018
|
enrich: flags.has('enrich'),
|
|
1019
|
+
noHooks: flags.has('no-hooks'),
|
|
1020
|
+
noIntegrations: flags.has('no-integrations'),
|
|
1021
|
+
noVerify: flags.has('no-verify'),
|
|
1022
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations')! } : {}),
|
|
950
1023
|
});
|
|
951
1024
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
writeMissingSkillsHint(write, r.missing, presetName);
|
|
957
|
-
|
|
958
|
-
// Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
|
|
959
|
-
// target's harness: the veto + recall legs are what `--target codex` promises.
|
|
960
|
-
// `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
|
|
961
|
-
// carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
|
|
962
|
-
// must be able to say so — and every test that is about skills says it.
|
|
1025
|
+
// Codex keeps its established user-registry writer, but its result is normalized into the same
|
|
1026
|
+
// two-outcome contract before JSON/human rendering. A write without a live ready observation is
|
|
1027
|
+
// a refusal with applied=true, never a second success channel.
|
|
1028
|
+
let integrationOutcomes: readonly IntegrationOutcome[] = r.integrations;
|
|
963
1029
|
let codexHooksOk = true;
|
|
964
1030
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
965
1031
|
const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
|
|
966
1032
|
codexHooksOk = delivery.ok;
|
|
967
1033
|
for (const line of delivery.stdout) write(line);
|
|
968
1034
|
for (const line of delivery.stderr) writeErr(line);
|
|
1035
|
+
const hookIndex = integrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
1036
|
+
if (hookIndex !== -1) {
|
|
1037
|
+
const hook = normalizeCodexHookOutcome(integrationOutcomes[hookIndex]!, delivery, flags.has('no-verify'));
|
|
1038
|
+
integrationOutcomes = integrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (flags.has('json')) {
|
|
1043
|
+
write(JSON.stringify({ target, skills: { count: r.results.length, written: r.written, skipped: r.skipped }, integrations: integrationOutcomes, integrationDigest: r.integrationDigest ?? null }));
|
|
1044
|
+
} else {
|
|
1045
|
+
write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
|
|
1046
|
+
if (r.dirsSearched > 1) write(` (searched ${r.dirsSearched} skill directories)`);
|
|
1047
|
+
for (const outcome of integrationOutcomes) {
|
|
1048
|
+
const label = outcome.component === 'mcp' ? 'MCP' : 'Hooks';
|
|
1049
|
+
const detail = outcome.status === 'refused'
|
|
1050
|
+
? `${outcome.reasonCode ?? 'LIVE_PROBE_FAILED'}${outcome.remediation ? ` — ${outcome.remediation}` : ''}`
|
|
1051
|
+
: outcome.status === 'emitted'
|
|
1052
|
+
? `${outcome.carrier?.path ?? 'registered'}${outcome.registrations.some((row) => row.approval === 'pending') ? '; Pending approval; ready=false' : ''}`
|
|
1053
|
+
: 'explicitly not requested';
|
|
1054
|
+
write(`${label}: ${outcome.status.toUpperCase()} (${detail})`);
|
|
1055
|
+
}
|
|
969
1056
|
}
|
|
1057
|
+
writeMissingSkillsHint(write, r.missing, presetName);
|
|
1058
|
+
|
|
970
1059
|
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
971
1060
|
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
972
1061
|
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
@@ -985,7 +1074,35 @@ async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
985
1074
|
for (const line of formatSkillApplyFailures(r.applyFailures)) writeErr(line);
|
|
986
1075
|
return 1;
|
|
987
1076
|
}
|
|
988
|
-
|
|
1077
|
+
const integrationOk = !integrationOutcomes.some((row) => row.status === 'refused');
|
|
1078
|
+
return codexHooksOk && integrationOk ? 0 : 1;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function cmdIntegrationsVerify(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): number {
|
|
1082
|
+
const targetInput = options.get('target');
|
|
1083
|
+
const component = options.get('component');
|
|
1084
|
+
if (targetInput === undefined || (component !== 'mcp' && component !== 'hooks')) {
|
|
1085
|
+
writeErr('dz integrations-verify: requires --target <name> --component <mcp|hooks>');
|
|
1086
|
+
return 1;
|
|
1087
|
+
}
|
|
1088
|
+
const resolution = resolveTargetName(targetInput);
|
|
1089
|
+
if (resolution.kind === 'unknown') {
|
|
1090
|
+
for (const line of formatTargetProblem('dz integrations-verify', resolution)) writeErr(line);
|
|
1091
|
+
return 1;
|
|
1092
|
+
}
|
|
1093
|
+
if (resolution.via === 'alias') writeErr(formatTargetAliasNote('dz integrations-verify', targetInput, resolution.target));
|
|
1094
|
+
const result = runIntegrationsVerify({
|
|
1095
|
+
target: resolution.target,
|
|
1096
|
+
component,
|
|
1097
|
+
projectRoot: resolve(cwd, options.get('project') ?? '.'),
|
|
1098
|
+
});
|
|
1099
|
+
if (flags.has('json')) write(JSON.stringify(result));
|
|
1100
|
+
else if (result.ok) {
|
|
1101
|
+
write(`dz integrations-verify: ${resolution.target}/${component} registered (runtime ${result.runtimeVersion ?? 'unknown'}; ready=${result.registrations.every((row) => row.ready === true)})`);
|
|
1102
|
+
} else {
|
|
1103
|
+
writeErr(`dz integrations-verify: ${resolution.target}/${component} REFUSED (${result.reasonCode ?? 'LIVE_PROBE_FAILED'}) — ${result.remediation ?? 'no qualifying receipt'}`);
|
|
1104
|
+
}
|
|
1105
|
+
return result.ok ? 0 : 1;
|
|
989
1106
|
}
|
|
990
1107
|
|
|
991
1108
|
async function cmdVerify(options: Map<string, string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
@@ -2421,6 +2538,146 @@ function cmdStatuslineFaRecord(options: Map<string, string>, cwd: string, write:
|
|
|
2421
2538
|
return 0;
|
|
2422
2539
|
}
|
|
2423
2540
|
|
|
2541
|
+
interface EtaCorpusRead {
|
|
2542
|
+
readonly history: StageSample[];
|
|
2543
|
+
readonly currentRunSamples: StageSample[];
|
|
2544
|
+
readonly currentTier: string | undefined;
|
|
2545
|
+
readonly activeStages: CheckpointStage[];
|
|
2546
|
+
readonly lastCheckpointTsMs: number | undefined;
|
|
2547
|
+
readonly hasCurrentCheckpoints: boolean;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function statuslineEtaStage(step: string): CheckpointStage | undefined {
|
|
2551
|
+
const match = step.match(/\bStep\s*-?\s*(0|[1-9](?:\.5)?)(?:\b|\s)/i);
|
|
2552
|
+
if (match === null) return undefined;
|
|
2553
|
+
const value = Number(match[1]);
|
|
2554
|
+
if (value === 0) return 'router';
|
|
2555
|
+
if (value > 0 && value < 6) return 'design';
|
|
2556
|
+
if (value === 6) return 'plan';
|
|
2557
|
+
if (value === 7) return 'code';
|
|
2558
|
+
if (value === 8) return 'qe';
|
|
2559
|
+
if (value === 9) return 'fleet';
|
|
2560
|
+
return undefined;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
function routerMetadata(router: RunSegment['router']): { tier?: string; activeSteps?: number[] } {
|
|
2564
|
+
const result = router?.result;
|
|
2565
|
+
if (result === null || typeof result !== 'object') return {};
|
|
2566
|
+
const record = result as Record<string, unknown>;
|
|
2567
|
+
const tier = typeof record['tier'] === 'string' && record['tier'].length > 0 ? record['tier'] : undefined;
|
|
2568
|
+
const activeSteps = Array.isArray(record['activeSteps'])
|
|
2569
|
+
? record['activeSteps'].filter((value): value is number => typeof value === 'number' && Number.isFinite(value))
|
|
2570
|
+
: undefined;
|
|
2571
|
+
return {
|
|
2572
|
+
...(tier !== undefined ? { tier } : {}),
|
|
2573
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function activeCheckpointStages(tier: string | undefined, activeSteps: readonly number[] | undefined): CheckpointStage[] {
|
|
2578
|
+
const stages = new Set<CheckpointStage>();
|
|
2579
|
+
if (activeSteps !== undefined) {
|
|
2580
|
+
for (const step of activeSteps) {
|
|
2581
|
+
if (step === 0) stages.add('router');
|
|
2582
|
+
else if (step > 0 && step < 6) stages.add('design');
|
|
2583
|
+
else if (step === 6) stages.add('plan');
|
|
2584
|
+
else if (step === 7) stages.add('code');
|
|
2585
|
+
else if (step === 8) stages.add('qe');
|
|
2586
|
+
else if (step === 9) stages.add('fleet');
|
|
2587
|
+
}
|
|
2588
|
+
} else if (tier !== undefined) {
|
|
2589
|
+
for (const stage of CHECKPOINT_STAGES) {
|
|
2590
|
+
if (stage !== 'fleet' || tier === 'L' || tier === 'XL') stages.add(stage);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return CHECKPOINT_STAGES.filter((stage) => stages.has(stage));
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/** All checkpoint I/O for ETA lives in this CLI-only wrapper and degrades per file. */
|
|
2597
|
+
function readEtaCorpus(projectRoot: string, currentSlug: string): EtaCorpusRead {
|
|
2598
|
+
const trace = process.env['DZ_ETA_TRACE'];
|
|
2599
|
+
if (trace !== undefined && trace.length > 0) {
|
|
2600
|
+
try {
|
|
2601
|
+
appendFileSync(trace, `${JSON.stringify({ projectRoot, currentSlug, ts: new Date().toISOString() })}\n`);
|
|
2602
|
+
} catch { /* a test/debug receipt can never take down the statusline */ }
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const samples: StageSample[] = [];
|
|
2606
|
+
let currentSegments: RunSegment[] = [];
|
|
2607
|
+
let hasCurrentCheckpoints = false;
|
|
2608
|
+
let entries: Dirent[] = [];
|
|
2609
|
+
try {
|
|
2610
|
+
entries = readdirSync(join(projectRoot, 'features'), { withFileTypes: true });
|
|
2611
|
+
} catch {
|
|
2612
|
+
return {
|
|
2613
|
+
history: [], currentRunSamples: [], currentTier: undefined, activeStages: [],
|
|
2614
|
+
lastCheckpointTsMs: undefined, hasCurrentCheckpoints: false,
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
for (const entry of entries) {
|
|
2619
|
+
if (!entry.isDirectory()) continue;
|
|
2620
|
+
const checkpointPath = join(projectRoot, 'features', entry.name, '.fa-state', 'checkpoints.jsonl');
|
|
2621
|
+
try {
|
|
2622
|
+
const text = readFileSync(checkpointPath, 'utf8');
|
|
2623
|
+
const observations = parseCheckpointLines(text, entry.name);
|
|
2624
|
+
const segments = segmentRun(observations);
|
|
2625
|
+
samples.push(...extractStageSamples(segments));
|
|
2626
|
+
if (entry.name === currentSlug) {
|
|
2627
|
+
hasCurrentCheckpoints = observations.length > 0;
|
|
2628
|
+
currentSegments = segments;
|
|
2629
|
+
}
|
|
2630
|
+
} catch {
|
|
2631
|
+
// Missing, unreadable, or racing append: this run contributes no evidence.
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
const lastSegment = currentSegments.at(-1);
|
|
2636
|
+
const currentRouter = lastSegment?.router;
|
|
2637
|
+
const invocationSegments = currentRouter === undefined
|
|
2638
|
+
? []
|
|
2639
|
+
: currentSegments.filter((segment) => segment.router === currentRouter);
|
|
2640
|
+
const invocationRunIds = new Set(invocationSegments.map((segment) => segment.runId));
|
|
2641
|
+
const currentRunSamples = extractStageSamples(invocationSegments);
|
|
2642
|
+
const metadata = routerMetadata(currentRouter);
|
|
2643
|
+
const currentTimestamps = invocationSegments
|
|
2644
|
+
.flatMap((segment) => segment.observations)
|
|
2645
|
+
.flatMap((observation) => observation.tsMs === undefined ? [] : [observation.tsMs]);
|
|
2646
|
+
|
|
2647
|
+
return {
|
|
2648
|
+
history: samples.filter((sample) => !invocationRunIds.has(sample.runId)),
|
|
2649
|
+
currentRunSamples,
|
|
2650
|
+
currentTier: metadata.tier,
|
|
2651
|
+
activeStages: activeCheckpointStages(metadata.tier, metadata.activeSteps),
|
|
2652
|
+
lastCheckpointTsMs: currentTimestamps.length > 0 ? Math.max(...currentTimestamps) : undefined,
|
|
2653
|
+
hasCurrentCheckpoints,
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function statuslineEta(projectRoot: string, state: FeatureAdrState, nowMs: number): EtaEstimate | undefined {
|
|
2658
|
+
const currentStage = statuslineEtaStage(state.step);
|
|
2659
|
+
if (currentStage === undefined) return undefined;
|
|
2660
|
+
const corpus = readEtaCorpus(projectRoot, state.slug);
|
|
2661
|
+
const currentIndex = CHECKPOINT_STAGES.indexOf(currentStage);
|
|
2662
|
+
const remainingStages = corpus.activeStages.filter((stage) => CHECKPOINT_STAGES.indexOf(stage) >= currentIndex && stage !== 'router');
|
|
2663
|
+
// Preserve honest absence as a machine-readable union member. The text formatter intentionally
|
|
2664
|
+
// omits both variants, but `--json` must still distinguish no file from a file with no tier.
|
|
2665
|
+
const stagesForEstimate = remainingStages.length > 0
|
|
2666
|
+
? remainingStages
|
|
2667
|
+
: (currentStage === 'router' ? [] : [currentStage]);
|
|
2668
|
+
if (stagesForEstimate.length === 0 && corpus.hasCurrentCheckpoints && corpus.currentTier !== undefined) return undefined;
|
|
2669
|
+
return estimateEta({
|
|
2670
|
+
samples: corpus.history,
|
|
2671
|
+
currentTier: corpus.currentTier,
|
|
2672
|
+
currentStage,
|
|
2673
|
+
remainingStages: stagesForEstimate,
|
|
2674
|
+
currentRunSamples: corpus.currentRunSamples,
|
|
2675
|
+
nowMs,
|
|
2676
|
+
...(corpus.lastCheckpointTsMs !== undefined ? { lastCheckpointTsMs: corpus.lastCheckpointTsMs } : {}),
|
|
2677
|
+
hasCurrentCheckpoints: corpus.hasCurrentCheckpoints,
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2424
2681
|
/**
|
|
2425
2682
|
* `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
|
|
2426
2683
|
* Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
|
|
@@ -2445,9 +2702,22 @@ function cmdStatusline(
|
|
|
2445
2702
|
try {
|
|
2446
2703
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
2447
2704
|
const data = statuslineData(projectRoot);
|
|
2705
|
+
const fa = data.featureAdr;
|
|
2706
|
+
let eta: EtaEstimate | undefined;
|
|
2707
|
+
let etaFragment: string | undefined;
|
|
2708
|
+
if (fa !== undefined && fa.kind !== 'loop') {
|
|
2709
|
+
try {
|
|
2710
|
+
eta = statuslineEta(projectRoot, fa, Date.now());
|
|
2711
|
+
if (eta !== undefined) etaFragment = formatEta(eta);
|
|
2712
|
+
} catch {
|
|
2713
|
+
// ETA is advisory: any corpus/resource failure omits only this fragment.
|
|
2714
|
+
eta = undefined;
|
|
2715
|
+
etaFragment = undefined;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2448
2718
|
|
|
2449
2719
|
if (flags.has('json')) {
|
|
2450
|
-
write(JSON.stringify(data));
|
|
2720
|
+
write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
|
|
2451
2721
|
return 0;
|
|
2452
2722
|
}
|
|
2453
2723
|
|
|
@@ -2457,13 +2727,12 @@ function cmdStatusline(
|
|
|
2457
2727
|
if (data.consolidatedAgeH !== undefined) line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
2458
2728
|
|
|
2459
2729
|
// Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
|
|
2460
|
-
const fa = data.featureAdr;
|
|
2461
2730
|
if (fa !== undefined) {
|
|
2462
2731
|
// 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.
|
|
2463
2732
|
if (fa.kind === 'loop') {
|
|
2464
2733
|
line = `🔁 loop ${fa.step} · ${line}`;
|
|
2465
2734
|
} else {
|
|
2466
|
-
line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2735
|
+
line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
|
|
2467
2736
|
}
|
|
2468
2737
|
}
|
|
2469
2738
|
|
|
@@ -3090,7 +3359,22 @@ function learningStoreLine(
|
|
|
3090
3359
|
) + (reason ? ' [' + reason + ']' : '');
|
|
3091
3360
|
}
|
|
3092
3361
|
|
|
3093
|
-
async function
|
|
3362
|
+
async function runTeachGuardReinforcement(
|
|
3363
|
+
projectRoot: string,
|
|
3364
|
+
dzId: string,
|
|
3365
|
+
reward: number,
|
|
3366
|
+
): Promise<{ readonly flushed: number }> {
|
|
3367
|
+
const backend = resolveLearningBackend(projectRoot);
|
|
3368
|
+
backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
|
|
3369
|
+
return backend.train();
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
async function cmdTeach(
|
|
3373
|
+
options: Map<string, string>, flags: Set<string>, cwd: string, write: Write,
|
|
3374
|
+
writeErr: WriteErr = (line) => { console.error(line); }, interactive = false,
|
|
3375
|
+
guardRunner: (projectRoot: string, text: string, opts: { readonly reward?: number }) => Promise<TeachGuardResult> = teachGuard,
|
|
3376
|
+
reinforceRunner: (projectRoot: string, dzId: string, reward: number) => Promise<{ readonly flushed: number }> = runTeachGuardReinforcement,
|
|
3377
|
+
): Promise<number> {
|
|
3094
3378
|
// WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
|
|
3095
3379
|
// `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
|
|
3096
3380
|
// per-session choice; for a CLI every invocation is a fresh process, so the only honest session
|
|
@@ -3174,26 +3458,39 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3174
3458
|
write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
|
|
3175
3459
|
return 1;
|
|
3176
3460
|
}
|
|
3177
|
-
const
|
|
3461
|
+
const importedKey = (p: PatternRecord): string => p.lessonForm !== undefined && p.lessonPairId !== undefined
|
|
3462
|
+
? `${p.pattern}\u0000${p.lessonForm}\u0000${p.lessonPairId}`
|
|
3463
|
+
: `legacy\u0000${p.pattern}`;
|
|
3464
|
+
const existing = new Set(loadStorePatternsSync(storeRoot).map(importedKey));
|
|
3178
3465
|
let imported = 0;
|
|
3179
3466
|
let skipped = 0;
|
|
3180
3467
|
const importedRecs: PatternRecord[] = [];
|
|
3181
3468
|
for (const item of parsed) {
|
|
3182
3469
|
const p = item as Partial<PatternRecord>;
|
|
3183
|
-
if (!p || typeof p
|
|
3470
|
+
if (!p || typeof p !== 'object') {
|
|
3184
3471
|
skipped += 1;
|
|
3185
3472
|
continue;
|
|
3186
3473
|
}
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3474
|
+
const pair = (p.lessonForm === 'specific' || p.lessonForm === 'class')
|
|
3475
|
+
&& typeof p.lessonPairId === 'string' && p.lessonPairId !== ''
|
|
3476
|
+
? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
|
|
3477
|
+
: {};
|
|
3478
|
+
const candidate = {
|
|
3479
|
+
pattern: typeof p.pattern === 'string' ? p.pattern : '',
|
|
3189
3480
|
type: (typeof p.type === 'string' ? p.type : 'lesson-learned') as PatternRecord['type'],
|
|
3190
3481
|
reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
|
|
3191
3482
|
domain: typeof p.domain === 'string' ? p.domain : 'general',
|
|
3192
3483
|
ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
|
|
3193
3484
|
source: 'dz-teach-import',
|
|
3194
|
-
|
|
3485
|
+
...pair,
|
|
3486
|
+
} satisfies PatternRecord;
|
|
3487
|
+
if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
|
|
3488
|
+
skipped += 1;
|
|
3489
|
+
continue;
|
|
3490
|
+
}
|
|
3491
|
+
const rec: PatternRecord = candidate;
|
|
3195
3492
|
await recordPattern(storeRoot, rec);
|
|
3196
|
-
existing.add(
|
|
3493
|
+
existing.add(importedKey(rec));
|
|
3197
3494
|
importedRecs.push(rec);
|
|
3198
3495
|
imported += 1;
|
|
3199
3496
|
}
|
|
@@ -3277,23 +3574,30 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3277
3574
|
|
|
3278
3575
|
const reward = parseFloat(options.get('reward') ?? '0.8');
|
|
3279
3576
|
const domain = options.get('domain') ?? 'general';
|
|
3577
|
+
const classWasRequested = options.has('class-form') || flags.has('class-form');
|
|
3578
|
+
const lessonForms = normalizeLessonForms(pattern, options.get('class-form'));
|
|
3579
|
+
let guardedExisting: { readonly dzId: string; readonly cosine: number; readonly pattern: PatternRecord } | undefined;
|
|
3280
3580
|
|
|
3281
3581
|
if (flags.has('guard')) {
|
|
3282
|
-
const verdict = await
|
|
3582
|
+
const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
|
|
3283
3583
|
if (verdict.action === 'reinforce') {
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
const
|
|
3293
|
-
if (
|
|
3294
|
-
|
|
3584
|
+
if (lessonForms.classForm !== undefined) {
|
|
3585
|
+
const existing = loadStoreRecords(storeRoot).find((record) => record.id === verdict.dzId);
|
|
3586
|
+
if (existing !== undefined) {
|
|
3587
|
+
guardedExisting = { dzId: verdict.dzId, cosine: verdict.cosine, pattern: recordToPattern(existing) };
|
|
3588
|
+
} else {
|
|
3589
|
+
write(`dz teach --guard: matched pattern ${verdict.dzId} was not found in the lexical store — teaching the lesson normally`);
|
|
3590
|
+
}
|
|
3591
|
+
} else {
|
|
3592
|
+
const trained = await reinforceRunner(storeRoot, verdict.dzId, Math.max(0, Math.min(1, reward)));
|
|
3593
|
+
if (trained.flushed > 0) {
|
|
3594
|
+
write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
|
|
3595
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
|
|
3596
|
+
if (clearedQ.cleared > 0) write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3597
|
+
return 0;
|
|
3598
|
+
}
|
|
3599
|
+
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
3295
3600
|
}
|
|
3296
|
-
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
3297
3601
|
}
|
|
3298
3602
|
}
|
|
3299
3603
|
|
|
@@ -3308,24 +3612,63 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3308
3612
|
// Typed through the shared schema (harness-core owns PatternRecord) so the
|
|
3309
3613
|
// write side and the read side (recommend's loadPatterns) can never drift —
|
|
3310
3614
|
// a field rename here is a compile error, not a silently re-muted loop (audit #2).
|
|
3311
|
-
const entry: PatternRecord = {
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3615
|
+
const entry: PatternRecord = guardedExisting?.pattern ?? {
|
|
3616
|
+
pattern: lessonForms.specific,
|
|
3617
|
+
type: type as PatternRecord['type'],
|
|
3618
|
+
reward: Math.max(0, Math.min(1, reward)),
|
|
3619
|
+
domain,
|
|
3620
|
+
ts: new Date().toISOString(),
|
|
3621
|
+
source: 'dz-teach',
|
|
3622
|
+
};
|
|
3319
3623
|
|
|
3320
3624
|
// Tier-2 (ADR-005): persist through the unified @dzhechkov/memory store. recordPattern
|
|
3321
3625
|
// folds any legacy .dz/patterns.jsonl into the backend (idempotent) and returns the
|
|
3322
3626
|
// total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
|
|
3323
3627
|
// lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
|
|
3324
3628
|
const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
|
|
3325
|
-
const
|
|
3629
|
+
const stored = await recordLessonForms(
|
|
3630
|
+
storeRoot,
|
|
3631
|
+
entry,
|
|
3632
|
+
lessonForms.classForm,
|
|
3633
|
+
quarantineOn ? { quarantine: true } : {},
|
|
3634
|
+
);
|
|
3635
|
+
let recordsToMirror = stored.records;
|
|
3636
|
+
let commandFailed = stored.class === 'failed';
|
|
3637
|
+
let reinforced = false;
|
|
3638
|
+
if (guardedExisting !== undefined) {
|
|
3639
|
+
let reinforceId = guardedExisting.dzId;
|
|
3640
|
+
if (stored.class === 'stored') {
|
|
3641
|
+
const specificRow = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3642
|
+
if (specificRow !== undefined) reinforceId = patternRecordId(specificRow);
|
|
3643
|
+
if (reinforceId !== guardedExisting.dzId) {
|
|
3644
|
+
const removed = removePatternsByIds(storeRoot, new Set([guardedExisting.dzId]));
|
|
3645
|
+
if (removed.error !== undefined || removed.removed === 0) {
|
|
3646
|
+
commandFailed = true;
|
|
3647
|
+
writeErr(`dz teach --guard: class pair stored, but old pattern cleanup failed${removed.error === undefined ? '' : ` — ${removed.error}`}`);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
} else if (stored.class === 'failed') {
|
|
3651
|
+
const partialSpecific = stored.records.find((row) => row.lessonForm === 'specific');
|
|
3652
|
+
if (partialSpecific !== undefined && patternRecordId(partialSpecific) !== guardedExisting.dzId) {
|
|
3653
|
+
const rolledBack = removePatternsByIds(storeRoot, new Set([patternRecordId(partialSpecific)]));
|
|
3654
|
+
if (rolledBack.error !== undefined) writeErr(`dz teach --guard: partial enrichment rollback failed — ${rolledBack.error}`);
|
|
3655
|
+
}
|
|
3656
|
+
recordsToMirror = [];
|
|
3657
|
+
reinforceId = guardedExisting.dzId;
|
|
3658
|
+
}
|
|
3659
|
+
if (!commandFailed) {
|
|
3660
|
+
const trained = await reinforceRunner(storeRoot, reinforceId, Math.max(0, Math.min(1, reward)));
|
|
3661
|
+
reinforced = trained.flushed > 0;
|
|
3662
|
+
if (reinforced) {
|
|
3663
|
+
const clearedQ = clearAgentdbQuarantine(storeRoot, [reinforceId]);
|
|
3664
|
+
if (clearedQ.cleared > 0) write(' ↳ promoted out of quarantine (mirror updated)');
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
const count = guardedExisting === undefined ? stored.count : loadStorePatternsSync(storeRoot).length;
|
|
3326
3669
|
|
|
3327
|
-
write(
|
|
3328
|
-
write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
|
|
3670
|
+
write(`${guardedExisting !== undefined && stored.class === 'stored' ? 'Enriched' : 'Learned'}: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
|
|
3671
|
+
write(` Domain: ${entry.domain} Reward: ${entry.reward} Backend: memory (@dzhechkov/memory)`);
|
|
3329
3672
|
write(` Total patterns: ${count}`);
|
|
3330
3673
|
// WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
|
|
3331
3674
|
// domain, the reward and the backend — and not one word about the path, so a store written to
|
|
@@ -3335,6 +3678,20 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3335
3678
|
// sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
|
|
3336
3679
|
// nothing, so the line stays byte-identical for everyone who set nothing.
|
|
3337
3680
|
write(storeLine('written'));
|
|
3681
|
+
if (stored.class === 'stored') write(' Class form: stored separately and linked to the specific lesson');
|
|
3682
|
+
if (classWasRequested && stored.class === 'absent') write(' Class form: skipped; the specific lesson was saved');
|
|
3683
|
+
if (stored.class === 'rejected') writeErr(`dz teach: class form rejected — ${stored.reason ?? 'invalid class form'}; the specific lesson was saved`);
|
|
3684
|
+
if (stored.class === 'failed') writeErr(`dz teach: specific stored; class failed — ${stored.reason ?? 'unknown storage failure'}`);
|
|
3685
|
+
if (guardedExisting !== undefined && stored.class === 'stored') {
|
|
3686
|
+
write(commandFailed
|
|
3687
|
+
? ' Guard: class enrichment stored; reinforcement skipped because old-pattern cleanup failed'
|
|
3688
|
+
: reinforced
|
|
3689
|
+
? ` Guard: enriched existing pattern (cos=${guardedExisting.cosine.toFixed(2)}), then reinforced the linked specific form`
|
|
3690
|
+
: ' Guard: class enrichment stored; reinforcement did not flush');
|
|
3691
|
+
}
|
|
3692
|
+
if (!classWasRequested && interactive) {
|
|
3693
|
+
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.');
|
|
3694
|
+
}
|
|
3338
3695
|
// ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
|
|
3339
3696
|
// directories and this binary; refusing would be defending a user against themselves,
|
|
3340
3697
|
// which this design does not attempt. Making the choice INFORMED is the part that is
|
|
@@ -3349,8 +3706,8 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3349
3706
|
write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
|
|
3350
3707
|
}
|
|
3351
3708
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
3352
|
-
await emitMirrorQ(storeRoot,
|
|
3353
|
-
return 0;
|
|
3709
|
+
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3710
|
+
return commandFailed ? 1 : 0;
|
|
3354
3711
|
}
|
|
3355
3712
|
|
|
3356
3713
|
async function cmdConsolidate(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
@@ -3702,7 +4059,14 @@ async function cmdRecallPromote(
|
|
|
3702
4059
|
return 0;
|
|
3703
4060
|
}
|
|
3704
4061
|
|
|
3705
|
-
async function cmdRecall(
|
|
4062
|
+
async function cmdRecall(
|
|
4063
|
+
options: Map<string, string>,
|
|
4064
|
+
flags: Set<string>,
|
|
4065
|
+
cwd: string,
|
|
4066
|
+
write: Write,
|
|
4067
|
+
writeErr: WriteErr,
|
|
4068
|
+
classMatcher?: RecallPatternsOptions['classMatcher'],
|
|
4069
|
+
): Promise<number> {
|
|
3706
4070
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3707
4071
|
const asJson = flags.has('json');
|
|
3708
4072
|
const all = flags.has('all');
|
|
@@ -3721,7 +4085,17 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3721
4085
|
// review could not make correct: a tag set by the writer is decidable, prose is not.
|
|
3722
4086
|
const allPatterns = loadStorePatternsSync(projectRoot);
|
|
3723
4087
|
const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
|
|
3724
|
-
const
|
|
4088
|
+
const classByPair = new Map(
|
|
4089
|
+
holdout.exported
|
|
4090
|
+
.filter((row) => row.lessonForm === 'class' && row.lessonPairId !== undefined)
|
|
4091
|
+
.map((row) => [row.lessonPairId as string, row.pattern]),
|
|
4092
|
+
);
|
|
4093
|
+
const patterns = holdout.exported.map((row) => {
|
|
4094
|
+
const classForm = row.lessonForm === 'specific' && row.lessonPairId !== undefined
|
|
4095
|
+
? classByPair.get(row.lessonPairId)
|
|
4096
|
+
: undefined;
|
|
4097
|
+
return classForm === undefined ? row : { ...row, classForm };
|
|
4098
|
+
});
|
|
3725
4099
|
const holdoutNote = renderHoldoutNote(holdout);
|
|
3726
4100
|
// The opt-in is honoured without argument — and named out loud. A flag that silently
|
|
3727
4101
|
// includes medical lessons in a portable export is a flag whose consequence the user
|
|
@@ -3755,7 +4129,15 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3755
4129
|
const perDomain = Object.fromEntries(
|
|
3756
4130
|
Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))),
|
|
3757
4131
|
);
|
|
3758
|
-
const
|
|
4132
|
+
const generalizedPairs = new Set(patterns.flatMap((p) => p.lessonPairId === undefined ? [] : [p.lessonPairId]));
|
|
4133
|
+
const unpairedLessons = patterns.filter((p) => p.lessonPairId === undefined).length;
|
|
4134
|
+
const stats = {
|
|
4135
|
+
...rawStats,
|
|
4136
|
+
topUses: topHoldout.exported,
|
|
4137
|
+
perDomain,
|
|
4138
|
+
generalized: generalizedPairs.size,
|
|
4139
|
+
logicalLessons: generalizedPairs.size + unpairedLessons,
|
|
4140
|
+
};
|
|
3759
4141
|
const backendStats = resolveLearningBackend(projectRoot).getStats();
|
|
3760
4142
|
if (asJson) {
|
|
3761
4143
|
write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
|
|
@@ -3767,6 +4149,7 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3767
4149
|
write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
|
|
3768
4150
|
write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
|
|
3769
4151
|
write(` exact-dup groups: ${stats.exactDupGroups}`);
|
|
4152
|
+
write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
|
|
3770
4153
|
write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
|
|
3771
4154
|
write(' top uses:');
|
|
3772
4155
|
for (const row of stats.topUses) write(` ${row.uses}× [${row.reward.toFixed(2)}] (${row.domain}) ${row.pattern.slice(0, 80)}`);
|
|
@@ -3887,6 +4270,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3887
4270
|
? ('lexical' as const)
|
|
3888
4271
|
: ('hybrid' as const);
|
|
3889
4272
|
const wantedDomain = options.get('domain');
|
|
4273
|
+
const classRecallOptions = {
|
|
4274
|
+
onClassDegraded: writeErr,
|
|
4275
|
+
...(classMatcher === undefined ? {} : { classMatcher }),
|
|
4276
|
+
};
|
|
3890
4277
|
// OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
|
|
3891
4278
|
// truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
|
|
3892
4279
|
// never receive its promised lift — the feature was weakest in exactly the case it
|
|
@@ -3909,8 +4296,8 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3909
4296
|
// second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
|
|
3910
4297
|
// own output is written after it returns.
|
|
3911
4298
|
const result = asJson
|
|
3912
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3913
|
-
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
4299
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
4300
|
+
: await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3914
4301
|
|
|
3915
4302
|
if (mode === 'semantic' && result.vectorEngine === 'none') {
|
|
3916
4303
|
// --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
|
|
@@ -3946,13 +4333,26 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3946
4333
|
let globalHits: typeof result.hits = [];
|
|
3947
4334
|
if (readGlobal) {
|
|
3948
4335
|
const g = asJson
|
|
3949
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3950
|
-
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
4336
|
+
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
4337
|
+
: await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
|
|
3951
4338
|
globalHits = g.hits;
|
|
3952
4339
|
}
|
|
4340
|
+
const projectHits = boost ? boost.hits : result.hits;
|
|
4341
|
+
// TWO different keys on purpose, and they must not be unified. The cross-store MERGE key is the
|
|
4342
|
+
// lesson TEXT: the same lesson taught into the project store and the global store is one lesson
|
|
4343
|
+
// shown once, labelled `both`, and those two records legitimately differ in timestamp, reward and
|
|
4344
|
+
// domain. The matchedForm LOOKUP key may be narrower. Folding the merge key into
|
|
4345
|
+
// patternIdentityOf (which includes ts) made two `dz teach` calls of identical text stop
|
|
4346
|
+
// collapsing — it reddened the pre-existing P3 case in recall-reads-both-stores.test.ts.
|
|
4347
|
+
const mergeKey = (hit: (typeof projectHits)[number]): string => hit.pattern.pattern;
|
|
4348
|
+
const globalByIdentity = new Map(globalHits.map((hit) => [mergeKey(hit), hit]));
|
|
3953
4349
|
const merged = readGlobal
|
|
3954
|
-
? mergeStoreHits(
|
|
3955
|
-
|
|
4350
|
+
? mergeStoreHits(projectHits, globalHits, mergeKey).map((hit) => {
|
|
4351
|
+
if (hit.origin !== 'both') return hit;
|
|
4352
|
+
const matchedForm = mergeLessonMatchedForms(hit.matchedForm, globalByIdentity.get(mergeKey(hit))?.matchedForm);
|
|
4353
|
+
return matchedForm === undefined ? hit : { ...hit, matchedForm };
|
|
4354
|
+
})
|
|
4355
|
+
: projectHits;
|
|
3956
4356
|
const hits = merged.slice(0, limit);
|
|
3957
4357
|
// Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
|
|
3958
4358
|
// paths that return earlier — `--json` and the zero-hits branch — still reported success. That
|
|
@@ -4000,6 +4400,7 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4000
4400
|
// version emitted the number while its own comment promised null (found by independent review).
|
|
4001
4401
|
write(JSON.stringify(hits.map((h) => ({
|
|
4002
4402
|
...h.pattern,
|
|
4403
|
+
...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
|
|
4003
4404
|
relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
|
|
4004
4405
|
// The TRUE cosine, as a second companion key — the design named it and the first ship missed
|
|
4005
4406
|
// it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
|
|
@@ -4064,6 +4465,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4064
4465
|
// guessing which second one answered.
|
|
4065
4466
|
write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
|
|
4066
4467
|
}
|
|
4468
|
+
const classMatches = hits.filter((h) => h.matchedForm === 'class' || h.matchedForm === 'both').length;
|
|
4469
|
+
if (hits.some((h) => h.pattern.classForm !== undefined)) {
|
|
4470
|
+
write(` class-form matches: ${classMatches} of ${hits.length}`);
|
|
4471
|
+
}
|
|
4067
4472
|
let sawQuarantined = false;
|
|
4068
4473
|
for (const h of hits) {
|
|
4069
4474
|
const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
|
|
@@ -4088,9 +4493,21 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4088
4493
|
// 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
|
|
4089
4494
|
// hidden remainder, and the cosine then appears to describe the visible fragment rather than
|
|
4090
4495
|
// the whole lesson. `--full` prints it all, still on one line.
|
|
4091
|
-
const
|
|
4092
|
-
const
|
|
4093
|
-
|
|
4496
|
+
const specificText = oneLine(h.pattern.pattern);
|
|
4497
|
+
const classText = h.pattern.classForm === undefined ? undefined : oneLine(h.pattern.classForm);
|
|
4498
|
+
let shown = specificText;
|
|
4499
|
+
if (classText !== undefined) {
|
|
4500
|
+
const suffix = ` [match: ${h.matchedForm ?? 'specific'}]`;
|
|
4501
|
+
if (flags.has('full')) {
|
|
4502
|
+
shown = `specific: ${specificText} · class: ${classText}${suffix}`;
|
|
4503
|
+
} else {
|
|
4504
|
+
const contentWidth = Math.max(2, 160 - 'specific: '.length - ' · class: '.length - suffix.length);
|
|
4505
|
+
const specificWidth = Math.floor(contentWidth / 2);
|
|
4506
|
+
shown = `specific: ${specificText.slice(0, specificWidth)} · class: ${classText.slice(0, contentWidth - specificWidth)}${suffix}`;
|
|
4507
|
+
}
|
|
4508
|
+
} else if (!flags.has('full')) {
|
|
4509
|
+
shown = shown.slice(0, 160);
|
|
4510
|
+
}
|
|
4094
4511
|
// WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
|
|
4095
4512
|
// blindness the store-location line just removed, one level down: the reader would see more
|
|
4096
4513
|
// results and have no way to tell whether the global store is even connected.
|
|
@@ -5094,7 +5511,13 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5094
5511
|
? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
5095
5512
|
: getPreset(preset)?.skills;
|
|
5096
5513
|
const install = select !== undefined && select.length > 0
|
|
5097
|
-
? await installSkills({
|
|
5514
|
+
? await installSkills({
|
|
5515
|
+
target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select,
|
|
5516
|
+
force: flags.has('force'), enrich: flags.has('enrich'), noHooks: flags.has('no-hooks'),
|
|
5517
|
+
noIntegrations: flags.has('no-integrations'),
|
|
5518
|
+
noVerify: flags.has('no-verify'),
|
|
5519
|
+
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations')! } : {}),
|
|
5520
|
+
})
|
|
5098
5521
|
: undefined;
|
|
5099
5522
|
if (install) {
|
|
5100
5523
|
write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
|
|
@@ -5104,6 +5527,7 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5104
5527
|
|
|
5105
5528
|
// Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
|
|
5106
5529
|
// setup has already run and the summary still prints; only the exit code carries the failure.
|
|
5530
|
+
let setupIntegrationOutcomes: readonly IntegrationOutcome[] = install?.integrations ?? [];
|
|
5107
5531
|
let codexHooksOk = true;
|
|
5108
5532
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
5109
5533
|
write(`║ 5. Delivering codex hooks (live verify)... ║`);
|
|
@@ -5111,6 +5535,11 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5111
5535
|
codexHooksOk = delivery.ok;
|
|
5112
5536
|
for (const line of delivery.stdout) write(line);
|
|
5113
5537
|
for (const line of delivery.stderr) writeErr(line);
|
|
5538
|
+
const hookIndex = setupIntegrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
|
|
5539
|
+
if (hookIndex !== -1) {
|
|
5540
|
+
const hook = normalizeCodexHookOutcome(setupIntegrationOutcomes[hookIndex]!, delivery, flags.has('no-verify'));
|
|
5541
|
+
setupIntegrationOutcomes = setupIntegrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
|
|
5542
|
+
}
|
|
5114
5543
|
}
|
|
5115
5544
|
|
|
5116
5545
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
@@ -5130,6 +5559,9 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5130
5559
|
if (install) {
|
|
5131
5560
|
write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
|
|
5132
5561
|
+ (install.dirsSearched > 1 ? ` (searched ${install.dirsSearched} skill dirs)` : ''));
|
|
5562
|
+
for (const outcome of setupIntegrationOutcomes) {
|
|
5563
|
+
write(`dz setup integration ${outcome.component}: ${outcome.status.toUpperCase()}${outcome.reasonCode ? ` (${outcome.reasonCode})` : ''}`);
|
|
5564
|
+
}
|
|
5133
5565
|
writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
|
|
5134
5566
|
}
|
|
5135
5567
|
// A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
|
|
@@ -5144,7 +5576,8 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5144
5576
|
if (erroredSteps.length > 0) {
|
|
5145
5577
|
write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
|
|
5146
5578
|
}
|
|
5147
|
-
|
|
5579
|
+
const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
|
|
5580
|
+
return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
|
|
5148
5581
|
}
|
|
5149
5582
|
|
|
5150
5583
|
function cmdPretrain(options: Map<string, string>, cwd: string, write: Write): number {
|
|
@@ -5802,6 +6235,9 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
5802
6235
|
if (guardResult.verdict === 'block') write(`dz publish: ⚠ guard BLOCK overridden via --no-guard: ${noGuard} (logged)`);
|
|
5803
6236
|
else if (guardResult.verdict === 'warn') for (const v of guardResult.violations) write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
|
|
5804
6237
|
else write('dz publish: ✓ guard pre-flight passed');
|
|
6238
|
+
for (const observation of guardResult.observations ?? []) {
|
|
6239
|
+
write(`dz publish: ℹ guard observation — ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`);
|
|
6240
|
+
}
|
|
5805
6241
|
for (const n of guardResult.notes ?? []) write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
|
|
5806
6242
|
}
|
|
5807
6243
|
|
|
@@ -7501,6 +7937,31 @@ export interface CodexHooksSummary {
|
|
|
7501
7937
|
readonly stderr: readonly string[];
|
|
7502
7938
|
}
|
|
7503
7939
|
|
|
7940
|
+
/** Map the retained Codex hook writer's one live verdict into the common integration contract. */
|
|
7941
|
+
export function normalizeCodexHookOutcome(
|
|
7942
|
+
base: IntegrationOutcome,
|
|
7943
|
+
delivery: CodexHooksSummary & { readonly report: CodexHooksSyncReport },
|
|
7944
|
+
noVerify = false,
|
|
7945
|
+
): IntegrationOutcome {
|
|
7946
|
+
if (delivery.report.ready) {
|
|
7947
|
+
return {
|
|
7948
|
+
target: 'codex', component: 'hooks', status: 'emitted',
|
|
7949
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: true }],
|
|
7950
|
+
carrier: { scope: 'user', path: '$CODEX_HOME/hooks.json' },
|
|
7951
|
+
...(delivery.report.codexVersion !== null ? { runtimeVersion: delivery.report.codexVersion } : {}),
|
|
7952
|
+
evidenceVersion: 'codex-hooks-live-v1',
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
return {
|
|
7956
|
+
...base,
|
|
7957
|
+
target: 'codex', component: 'hooks', status: 'refused',
|
|
7958
|
+
registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: false }],
|
|
7959
|
+
reasonCode: 'CURRENT_LIVE_CHECK_FAILED',
|
|
7960
|
+
remediation: noVerify ? '--no-verify cannot establish ready; rerun with live verification' : 'approve the managed hooks and rerun the live verification',
|
|
7961
|
+
applied: delivery.report.written || delivery.report.installed,
|
|
7962
|
+
};
|
|
7963
|
+
}
|
|
7964
|
+
|
|
7504
7965
|
/**
|
|
7505
7966
|
* What the user is told about a sync report — the ONE place the success word can be printed.
|
|
7506
7967
|
*
|
|
@@ -7829,10 +8290,480 @@ function gatherReadmeCounts(root: string): { label: string; a: number; b: number
|
|
|
7829
8290
|
return pairs;
|
|
7830
8291
|
}
|
|
7831
8292
|
|
|
8293
|
+
const MAX_VOLUME_FILES_PER_SCOPE = 512;
|
|
8294
|
+
const MAX_VOLUME_BYTES_PER_SCOPE = 32 * 1024 * 1024;
|
|
8295
|
+
const CYRILLIC_CHARACTER = /\p{Script=Cyrillic}/u;
|
|
8296
|
+
|
|
8297
|
+
function volumePathInside(root: string, candidate: string): boolean {
|
|
8298
|
+
return candidate === root || candidate.startsWith(root + sep);
|
|
8299
|
+
}
|
|
8300
|
+
|
|
8301
|
+
function cyrillicUtf8Bytes(buffer: Buffer): number {
|
|
8302
|
+
let bytes = 0;
|
|
8303
|
+
for (const character of buffer.toString('utf8')) {
|
|
8304
|
+
if (CYRILLIC_CHARACTER.test(character)) bytes += Buffer.byteLength(character, 'utf8');
|
|
8305
|
+
}
|
|
8306
|
+
return bytes;
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
function gatherTemplateVolumeTarget(
|
|
8310
|
+
packageDir: string,
|
|
8311
|
+
target: string,
|
|
8312
|
+
): TemplateVolumeTargetFact | undefined {
|
|
8313
|
+
const templateRoot = join(packageDir, 'templates', '.claude');
|
|
8314
|
+
if (!existsSync(templateRoot)) return undefined;
|
|
8315
|
+
const files: TemplateVolumeFileFact[] = [];
|
|
8316
|
+
const realFiles = new Set<string>();
|
|
8317
|
+
let totalBytes = 0;
|
|
8318
|
+
let failure: { reason: string; detail: string } | undefined;
|
|
8319
|
+
let packageReal: string;
|
|
8320
|
+
let templateReal: string;
|
|
8321
|
+
try {
|
|
8322
|
+
packageReal = realpathSync(packageDir);
|
|
8323
|
+
templateReal = realpathSync(templateRoot);
|
|
8324
|
+
if (!volumePathInside(packageReal, templateReal)) {
|
|
8325
|
+
failure = { reason: 'template-root-escape', detail: templateRoot };
|
|
8326
|
+
}
|
|
8327
|
+
} catch (error) {
|
|
8328
|
+
return {
|
|
8329
|
+
target,
|
|
8330
|
+
files,
|
|
8331
|
+
collection: { complete: false, reason: 'template-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
8332
|
+
};
|
|
8333
|
+
}
|
|
8334
|
+
|
|
8335
|
+
const fail = (reason: string, detail: string): void => {
|
|
8336
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
8337
|
+
};
|
|
8338
|
+
const addFile = (absolute: string, kind: 'rules' | 'commands' | 'skills'): void => {
|
|
8339
|
+
if (failure !== undefined) return;
|
|
8340
|
+
if (files.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
8341
|
+
fail('template-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
try {
|
|
8345
|
+
const stat = lstatSync(absolute);
|
|
8346
|
+
if (!stat.isFile()) { fail('template-input-not-regular', absolute); return; }
|
|
8347
|
+
const real = realpathSync(absolute);
|
|
8348
|
+
if (!volumePathInside(templateReal, real)) { fail('template-input-escape', absolute); return; }
|
|
8349
|
+
if (realFiles.has(real)) { fail('template-input-duplicate', absolute); return; }
|
|
8350
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8351
|
+
fail(stat.size <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
8352
|
+
return;
|
|
8353
|
+
}
|
|
8354
|
+
const content = readFileSync(absolute);
|
|
8355
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8356
|
+
fail(content.byteLength <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
|
|
8357
|
+
return;
|
|
8358
|
+
}
|
|
8359
|
+
realFiles.add(real);
|
|
8360
|
+
totalBytes += content.byteLength;
|
|
8361
|
+
files.push({
|
|
8362
|
+
path: relative(packageDir, absolute).split(sep).join('/'),
|
|
8363
|
+
kind,
|
|
8364
|
+
bytes: content.byteLength,
|
|
8365
|
+
cyrillicUtf8Bytes: cyrillicUtf8Bytes(content),
|
|
8366
|
+
});
|
|
8367
|
+
} catch (error) {
|
|
8368
|
+
fail('template-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
const scanFlat = (name: 'rules' | 'commands'): void => {
|
|
8372
|
+
const dir = join(templateRoot, name);
|
|
8373
|
+
if (!existsSync(dir) || failure !== undefined) return;
|
|
8374
|
+
try {
|
|
8375
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8376
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
8377
|
+
addFile(join(dir, entry.name), name);
|
|
8378
|
+
}
|
|
8379
|
+
} catch (error) {
|
|
8380
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8381
|
+
}
|
|
8382
|
+
};
|
|
8383
|
+
const scanSkills = (dir: string): void => {
|
|
8384
|
+
if (!existsSync(dir) || failure !== undefined) return;
|
|
8385
|
+
let entries: Dirent[];
|
|
8386
|
+
try {
|
|
8387
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
8388
|
+
} catch (error) {
|
|
8389
|
+
fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8390
|
+
return;
|
|
8391
|
+
}
|
|
8392
|
+
for (const entry of entries) {
|
|
8393
|
+
if (failure !== undefined) return;
|
|
8394
|
+
const absolute = join(dir, entry.name);
|
|
8395
|
+
if (entry.isSymbolicLink()) { fail('template-input-not-regular', absolute); return; }
|
|
8396
|
+
if (entry.isDirectory()) scanSkills(absolute);
|
|
8397
|
+
else if (entry.name === 'SKILL.md') addFile(absolute, 'skills');
|
|
8398
|
+
}
|
|
8399
|
+
};
|
|
8400
|
+
|
|
8401
|
+
scanFlat('rules');
|
|
8402
|
+
scanFlat('commands');
|
|
8403
|
+
scanSkills(join(templateRoot, 'skills'));
|
|
8404
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
8405
|
+
return {
|
|
8406
|
+
target,
|
|
8407
|
+
files,
|
|
8408
|
+
collection: failure === undefined
|
|
8409
|
+
? { complete: true }
|
|
8410
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
8411
|
+
};
|
|
8412
|
+
}
|
|
8413
|
+
|
|
8414
|
+
function volumeGitText(root: string, args: readonly string[], allowedStatuses: readonly number[] = [0]): string {
|
|
8415
|
+
const result = spawnSync('git', [...args], {
|
|
8416
|
+
cwd: root,
|
|
8417
|
+
encoding: 'utf8',
|
|
8418
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
8419
|
+
});
|
|
8420
|
+
if (result.status === null || !allowedStatuses.includes(result.status)) {
|
|
8421
|
+
throw result.error ?? new Error(`git ${args[0] ?? ''} exited ${String(result.status)}`);
|
|
8422
|
+
}
|
|
8423
|
+
return result.stdout ?? '';
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
function parseGuardStatusPaths(root: string): { readonly code: string; readonly path: string }[] {
|
|
8427
|
+
const text = volumeGitText(root, ['status', '--porcelain', '-uall']);
|
|
8428
|
+
return text.split('\n').flatMap((line) => {
|
|
8429
|
+
if (line.length < 4) return [];
|
|
8430
|
+
const code = line.slice(0, 2);
|
|
8431
|
+
const raw = line.slice(3).trim();
|
|
8432
|
+
const path = (raw.includes(' -> ') ? raw.split(' -> ')[1] : raw)?.trim();
|
|
8433
|
+
return path ? [{ code, path }] : [];
|
|
8434
|
+
});
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
function parseFeatureTier(text: string): FeatureTier | undefined {
|
|
8438
|
+
const match = text.match(/^##\s+Tier:\s*(S|M|L|XL)\s*$/m);
|
|
8439
|
+
return match?.[1] as FeatureTier | undefined;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
function parseFeatureActiveSteps(text: string): (number | string)[] | undefined {
|
|
8443
|
+
const section = text.match(/^## Active steps[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/mi)?.[1];
|
|
8444
|
+
if (section === undefined) return undefined;
|
|
8445
|
+
const steps = [...section.matchAll(/\b(?:10|[0-9](?:\.5)?)\b/g)].map((match) => match[0]!);
|
|
8446
|
+
return [...new Set(steps)];
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8449
|
+
function featureLifecycle(root: string, slug: string, assessment: string | undefined): FeatureVolumeFact['lifecycle'] {
|
|
8450
|
+
if (assessment !== undefined && /^Lifecycle:\s*complete\s*$/mi.test(assessment)) return { phase: 'complete' };
|
|
8451
|
+
try {
|
|
8452
|
+
const state = JSON.parse(readFileSync(join(root, '.dz', 'feature-adr', 'learning-state', `${slug}.json`), 'utf8')) as { step?: unknown };
|
|
8453
|
+
const match = typeof state.step === 'string' ? state.step.match(/\bStep\s+(\d+(?:\.5)?)/i) : null;
|
|
8454
|
+
if (!match?.[1]) return undefined;
|
|
8455
|
+
const current = Number(match[1]);
|
|
8456
|
+
if (!Number.isFinite(current)) return undefined;
|
|
8457
|
+
return { phase: 'in-progress', completedThroughStep: current <= 0 ? 0 : Math.ceil(current) - 1 };
|
|
8458
|
+
} catch {
|
|
8459
|
+
return undefined;
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
|
|
8463
|
+
interface GatheredFeatureVolume {
|
|
8464
|
+
readonly fact: FeatureVolumeFact;
|
|
8465
|
+
readonly manifestText?: string;
|
|
8466
|
+
}
|
|
8467
|
+
|
|
8468
|
+
function gatherFeatureVolumeFact(root: string, slug: string): GatheredFeatureVolume {
|
|
8469
|
+
const featureDir = join(root, 'features', slug);
|
|
8470
|
+
const artifacts: FeatureArtifactFact[] = [];
|
|
8471
|
+
const contents = new Map<string, Buffer>();
|
|
8472
|
+
let totalBytes = 0;
|
|
8473
|
+
let failure: { reason: string; detail: string } | undefined;
|
|
8474
|
+
let rootReal: string;
|
|
8475
|
+
let featureReal: string;
|
|
8476
|
+
try {
|
|
8477
|
+
rootReal = realpathSync(root);
|
|
8478
|
+
featureReal = realpathSync(featureDir);
|
|
8479
|
+
if (!volumePathInside(rootReal, featureReal)) failure = { reason: 'feature-root-escape', detail: featureDir };
|
|
8480
|
+
} catch (error) {
|
|
8481
|
+
return { fact: {
|
|
8482
|
+
slug,
|
|
8483
|
+
artifacts,
|
|
8484
|
+
collection: { complete: false, reason: 'feature-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
|
|
8485
|
+
} };
|
|
8486
|
+
}
|
|
8487
|
+
const fail = (reason: string, detail: string): void => {
|
|
8488
|
+
failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
|
|
8489
|
+
};
|
|
8490
|
+
const addArtifact = (absolute: string): void => {
|
|
8491
|
+
if (failure !== undefined) return;
|
|
8492
|
+
if (artifacts.length >= MAX_VOLUME_FILES_PER_SCOPE) {
|
|
8493
|
+
fail('feature-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
try {
|
|
8497
|
+
const stat = lstatSync(absolute);
|
|
8498
|
+
if (!stat.isFile()) { fail('feature-input-not-regular', absolute); return; }
|
|
8499
|
+
const real = realpathSync(absolute);
|
|
8500
|
+
if (!volumePathInside(featureReal, real)) { fail('feature-input-escape', absolute); return; }
|
|
8501
|
+
if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8502
|
+
fail(stat.size <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
8503
|
+
return;
|
|
8504
|
+
}
|
|
8505
|
+
const content = readFileSync(absolute);
|
|
8506
|
+
if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
|
|
8507
|
+
fail(content.byteLength <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
|
|
8508
|
+
return;
|
|
8509
|
+
}
|
|
8510
|
+
const path = relative(featureDir, absolute).split(sep).join('/');
|
|
8511
|
+
totalBytes += content.byteLength;
|
|
8512
|
+
contents.set(path, content);
|
|
8513
|
+
artifacts.push({ path, bytes: content.byteLength });
|
|
8514
|
+
} catch (error) {
|
|
8515
|
+
fail('feature-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8516
|
+
}
|
|
8517
|
+
};
|
|
8518
|
+
const scanNested = (dir: string): void => {
|
|
8519
|
+
if (!existsSync(dir) || failure !== undefined) return;
|
|
8520
|
+
let entries: Dirent[];
|
|
8521
|
+
try {
|
|
8522
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
8523
|
+
} catch (error) {
|
|
8524
|
+
fail('feature-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8525
|
+
return;
|
|
8526
|
+
}
|
|
8527
|
+
for (const entry of entries) {
|
|
8528
|
+
if (failure !== undefined) return;
|
|
8529
|
+
const absolute = join(dir, entry.name);
|
|
8530
|
+
if (entry.isSymbolicLink()) { fail('feature-input-not-regular', absolute); return; }
|
|
8531
|
+
if (entry.isDirectory()) scanNested(absolute);
|
|
8532
|
+
else addArtifact(absolute);
|
|
8533
|
+
}
|
|
8534
|
+
};
|
|
8535
|
+
try {
|
|
8536
|
+
const entries = readdirSync(featureDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
8537
|
+
for (const entry of entries) {
|
|
8538
|
+
if (failure !== undefined) break;
|
|
8539
|
+
const absolute = join(featureDir, entry.name);
|
|
8540
|
+
if (entry.name === '03_adr' || entry.name === '07_code_changes') {
|
|
8541
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) fail('feature-input-not-regular', absolute);
|
|
8542
|
+
else scanNested(absolute);
|
|
8543
|
+
} else if (entry.name === 'README.md' || /^0[0-9](?:[_.-][^/]*)?\.md$/.test(entry.name)) {
|
|
8544
|
+
addArtifact(absolute);
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
} catch (error) {
|
|
8548
|
+
fail('feature-directory-unreadable', `${featureDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8549
|
+
}
|
|
8550
|
+
artifacts.sort((a, b) => a.path.localeCompare(b.path));
|
|
8551
|
+
const assessment = contents.get('00_complexity_assessment.md')?.toString('utf8');
|
|
8552
|
+
const tier = assessment !== undefined ? parseFeatureTier(assessment) : undefined;
|
|
8553
|
+
const activeSteps = assessment !== undefined ? parseFeatureActiveSteps(assessment) : undefined;
|
|
8554
|
+
const lifecycle = featureLifecycle(root, slug, assessment);
|
|
8555
|
+
const fact: FeatureVolumeFact = {
|
|
8556
|
+
slug,
|
|
8557
|
+
...(tier !== undefined ? { tier } : {}),
|
|
8558
|
+
...(activeSteps !== undefined ? { activeSteps } : {}),
|
|
8559
|
+
namedConsumers: [],
|
|
8560
|
+
...(lifecycle !== undefined ? { lifecycle } : {}),
|
|
8561
|
+
artifacts,
|
|
8562
|
+
collection: failure === undefined
|
|
8563
|
+
? { complete: true }
|
|
8564
|
+
: { complete: false, reason: failure.reason, detail: failure.detail },
|
|
8565
|
+
};
|
|
8566
|
+
const manifestText = contents.get('07_code_changes/change_manifest.md')?.toString('utf8');
|
|
8567
|
+
return { fact, ...(manifestText !== undefined ? { manifestText } : {}) };
|
|
8568
|
+
}
|
|
8569
|
+
|
|
8570
|
+
function manifestPaths(text: string, slug: string): string[] {
|
|
8571
|
+
const paths = new Set<string>();
|
|
8572
|
+
for (const line of text.split('\n')) {
|
|
8573
|
+
const match = line.match(/^\s*-\s+`?([^`]+?)`?(?:\s+[—-]\s+.*)?$/);
|
|
8574
|
+
if (!match?.[1]) continue;
|
|
8575
|
+
const path = match[1].trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
8576
|
+
if (path === '' || path.startsWith('/') || path.split('/').some((part) => part === '..')) continue;
|
|
8577
|
+
if (path.startsWith(`features/${slug}/`)) continue;
|
|
8578
|
+
paths.add(path);
|
|
8579
|
+
}
|
|
8580
|
+
return [...paths].sort();
|
|
8581
|
+
}
|
|
8582
|
+
|
|
8583
|
+
function attributableDiff(
|
|
8584
|
+
root: string,
|
|
8585
|
+
slug: string,
|
|
8586
|
+
feature: GatheredFeatureVolume,
|
|
8587
|
+
status: readonly { readonly code: string; readonly path: string }[],
|
|
8588
|
+
changedSlugs: readonly string[],
|
|
8589
|
+
): NonNullable<FeatureVolumeFact['diff']> {
|
|
8590
|
+
if (changedSlugs.length !== 1 || changedSlugs[0] !== slug) {
|
|
8591
|
+
return { attributable: false, reason: changedSlugs.length > 1 ? 'ambiguous-feature-attribution' : 'feature-not-attributable' };
|
|
8592
|
+
}
|
|
8593
|
+
if (feature.manifestText === undefined) return { attributable: false, reason: 'change-manifest-unavailable' };
|
|
8594
|
+
const listed = manifestPaths(feature.manifestText, slug);
|
|
8595
|
+
const changedByPath = new Map(status.map((item) => [item.path, item.code]));
|
|
8596
|
+
const changed = listed.filter((path) => changedByPath.has(path));
|
|
8597
|
+
if (changed.length === 0) return { attributable: true, bytes: 0, base: 'HEAD', head: 'working-tree', method: 'git-unified-diff-bytes/v1', excludedFeaturePath: `features/${slug}/**` };
|
|
8598
|
+
const tracked: string[] = [];
|
|
8599
|
+
const untracked: string[] = [];
|
|
8600
|
+
for (const path of changed) {
|
|
8601
|
+
const absolute = resolve(root, path);
|
|
8602
|
+
if (!volumePathInside(root, absolute)) return { attributable: false, reason: 'manifest-path-escape' };
|
|
8603
|
+
if (changedByPath.get(path) === '??') {
|
|
8604
|
+
try {
|
|
8605
|
+
const stat = lstatSync(absolute);
|
|
8606
|
+
const real = realpathSync(absolute);
|
|
8607
|
+
if (!stat.isFile() || !volumePathInside(root, real) || stat.size > MAX_VOLUME_BYTES_PER_SCOPE) {
|
|
8608
|
+
return { attributable: false, reason: 'untracked-diff-input-refused' };
|
|
8609
|
+
}
|
|
8610
|
+
} catch { return { attributable: false, reason: 'untracked-diff-input-unreadable' }; }
|
|
8611
|
+
untracked.push(path);
|
|
8612
|
+
} else {
|
|
8613
|
+
tracked.push(path);
|
|
8614
|
+
}
|
|
8615
|
+
}
|
|
8616
|
+
let bytes = 0;
|
|
8617
|
+
try {
|
|
8618
|
+
if (tracked.length > 0) {
|
|
8619
|
+
const diff = volumeGitText(root, ['diff', '--binary', '--no-ext-diff', 'HEAD', '--', ...tracked]);
|
|
8620
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
8621
|
+
}
|
|
8622
|
+
for (const path of untracked) {
|
|
8623
|
+
const diff = volumeGitText(root, ['diff', '--no-index', '--binary', '--', '/dev/null', resolve(root, path)], [0, 1]);
|
|
8624
|
+
bytes += Buffer.byteLength(diff, 'utf8');
|
|
8625
|
+
if (bytes > MAX_VOLUME_BYTES_PER_SCOPE) return { attributable: false, reason: 'diff-byte-cap-exceeded' };
|
|
8626
|
+
}
|
|
8627
|
+
} catch {
|
|
8628
|
+
return { attributable: false, reason: 'git-diff-failed' };
|
|
8629
|
+
}
|
|
8630
|
+
return {
|
|
8631
|
+
attributable: true,
|
|
8632
|
+
bytes,
|
|
8633
|
+
base: 'HEAD',
|
|
8634
|
+
head: 'working-tree',
|
|
8635
|
+
method: 'git-unified-diff-bytes/v1',
|
|
8636
|
+
excludedFeaturePath: `features/${slug}/**`,
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
function gatherVolumeShadowFacts(
|
|
8641
|
+
root: string,
|
|
8642
|
+
packages: readonly { readonly dir: string; readonly name: string; readonly privateFlag: boolean }[],
|
|
8643
|
+
): VolumeShadowInput {
|
|
8644
|
+
const templates = packages
|
|
8645
|
+
.filter((item) => !item.privateFlag)
|
|
8646
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.dir.localeCompare(b.dir))
|
|
8647
|
+
.flatMap((item) => {
|
|
8648
|
+
const fact = gatherTemplateVolumeTarget(join(root, item.dir), item.name);
|
|
8649
|
+
return fact === undefined ? [] : [fact];
|
|
8650
|
+
});
|
|
8651
|
+
let status: { code: string; path: string }[];
|
|
8652
|
+
try { status = parseGuardStatusPaths(root); }
|
|
8653
|
+
catch { return templates.length > 0 ? { templates } : {}; }
|
|
8654
|
+
const slugs = [...new Set(status.flatMap((item) => {
|
|
8655
|
+
const match = item.path.match(/^features\/([^/]+)\//);
|
|
8656
|
+
return match?.[1] ? [match[1]] : [];
|
|
8657
|
+
}))].sort();
|
|
8658
|
+
const gathered = slugs.slice(0, 32).map((slug) => gatherFeatureVolumeFact(root, slug));
|
|
8659
|
+
const measuredFeatures = gathered.map((feature) => ({
|
|
8660
|
+
...feature.fact,
|
|
8661
|
+
diff: attributableDiff(root, feature.fact.slug, feature, status, slugs),
|
|
8662
|
+
}));
|
|
8663
|
+
const cappedFeatures: FeatureVolumeFact[] = slugs.slice(32).map((slug) => ({
|
|
8664
|
+
slug,
|
|
8665
|
+
artifacts: [],
|
|
8666
|
+
collection: {
|
|
8667
|
+
complete: false,
|
|
8668
|
+
reason: 'feature-scope-cap-exceeded',
|
|
8669
|
+
detail: 'only the first 32 lexicographically sorted changed feature scopes were traversed',
|
|
8670
|
+
},
|
|
8671
|
+
}));
|
|
8672
|
+
const features = [...measuredFeatures, ...cappedFeatures];
|
|
8673
|
+
return {
|
|
8674
|
+
...(templates.length > 0 ? { templates } : {}),
|
|
8675
|
+
...(features.length > 0 ? { features } : {}),
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
|
|
7832
8679
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
7833
8680
|
function gatherGuardFacts(op: string, root: string, text: string | undefined, storeCap: number): Record<string, unknown> {
|
|
7834
8681
|
const facts: Record<string, unknown> = { op };
|
|
7835
8682
|
if (op === 'publish') {
|
|
8683
|
+
// Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
|
|
8684
|
+
// stale finding and never a publish blocker.
|
|
8685
|
+
try {
|
|
8686
|
+
const routing = readRoutingTelemetry(root);
|
|
8687
|
+
facts['routingFreshness'] = { unfedRunIds: unfedRuns(routing.harvest.samples, routing.alreadyFed) };
|
|
8688
|
+
} catch { /* fail-open: routing-store-stale is SOFT and needs gathered evidence */ }
|
|
8689
|
+
// marketplace-parity: regenerate the two published showcase manifests from the LIVE registry,
|
|
8690
|
+
// but redirect every write to a unique temp root. Version is operator-owned, so pinning the
|
|
8691
|
+
// published value into generatePlugin makes the comparison composition-only by construction.
|
|
8692
|
+
try {
|
|
8693
|
+
const showcaseDir = join(root, '.claude-plugin');
|
|
8694
|
+
if (!existsSync(showcaseDir)) {
|
|
8695
|
+
facts['marketplaceParity'] = { applicable: false };
|
|
8696
|
+
} else {
|
|
8697
|
+
const pluginPath = join(showcaseDir, 'plugin.json');
|
|
8698
|
+
const marketplacePath = join(showcaseDir, 'marketplace.json');
|
|
8699
|
+
const hasPlugin = existsSync(pluginPath);
|
|
8700
|
+
const hasMarketplace = existsSync(marketplacePath);
|
|
8701
|
+
const manifestFailures: { file: 'plugin.json' | 'marketplace.json'; error: string }[] = [];
|
|
8702
|
+
const readManifest = (path: string, file: 'plugin.json' | 'marketplace.json'): unknown => {
|
|
8703
|
+
try {
|
|
8704
|
+
return JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
8705
|
+
} catch (error) {
|
|
8706
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8707
|
+
manifestFailures.push({ file, error: message.replace(/\s+/g, ' ').slice(0, 240) });
|
|
8708
|
+
return undefined;
|
|
8709
|
+
}
|
|
8710
|
+
};
|
|
8711
|
+
const versionOf = (manifest: unknown): string | undefined => {
|
|
8712
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return undefined;
|
|
8713
|
+
const version = (manifest as Record<string, unknown>)['version'];
|
|
8714
|
+
return typeof version === 'string' && version !== '' ? version : undefined;
|
|
8715
|
+
};
|
|
8716
|
+
const publishedPlugin = hasPlugin ? readManifest(pluginPath, 'plugin.json') : undefined;
|
|
8717
|
+
const publishedMarketplace = hasMarketplace ? readManifest(marketplacePath, 'marketplace.json') : undefined;
|
|
8718
|
+
const marketplaceRecord = publishedMarketplace && typeof publishedMarketplace === 'object' && !Array.isArray(publishedMarketplace)
|
|
8719
|
+
? publishedMarketplace as Record<string, unknown>
|
|
8720
|
+
: undefined;
|
|
8721
|
+
const marketplacePlugins = marketplaceRecord !== undefined && Array.isArray(marketplaceRecord['plugins'])
|
|
8722
|
+
? marketplaceRecord['plugins']
|
|
8723
|
+
: [];
|
|
8724
|
+
const publishedVersion = versionOf(publishedPlugin)
|
|
8725
|
+
?? versionOf(publishedMarketplace)
|
|
8726
|
+
?? versionOf(marketplacePlugins[0]);
|
|
8727
|
+
if (manifestFailures.length > 0) {
|
|
8728
|
+
facts['marketplaceParity'] = {
|
|
8729
|
+
applicable: true,
|
|
8730
|
+
manifestFailures,
|
|
8731
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8732
|
+
};
|
|
8733
|
+
} else if (hasPlugin !== hasMarketplace) {
|
|
8734
|
+
facts['marketplaceParity'] = {
|
|
8735
|
+
applicable: true,
|
|
8736
|
+
onlyOnePresent: true,
|
|
8737
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8738
|
+
};
|
|
8739
|
+
} else if (!hasPlugin) {
|
|
8740
|
+
facts['marketplaceParity'] = { applicable: false };
|
|
8741
|
+
} else {
|
|
8742
|
+
let scratch: string | undefined;
|
|
8743
|
+
try {
|
|
8744
|
+
const registry = buildRegistry(root);
|
|
8745
|
+
scratch = mkdtempSync(join(tmpdir(), 'dz-guard-marketplace-'));
|
|
8746
|
+
const generated = generatePlugin(scratch, registry, { version: publishedVersion });
|
|
8747
|
+
const freshPlugin = JSON.parse(readFileSync(generated.pluginJsonPath, 'utf8')) as unknown;
|
|
8748
|
+
const freshMarketplace = JSON.parse(readFileSync(generated.marketplaceJsonPath, 'utf8')) as unknown;
|
|
8749
|
+
facts['marketplaceParity'] = {
|
|
8750
|
+
applicable: true,
|
|
8751
|
+
diverged: !isDeepStrictEqual(publishedPlugin, freshPlugin)
|
|
8752
|
+
|| !isDeepStrictEqual(publishedMarketplace, freshMarketplace),
|
|
8753
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8754
|
+
};
|
|
8755
|
+
} catch {
|
|
8756
|
+
facts['marketplaceParity'] = {
|
|
8757
|
+
applicable: true,
|
|
8758
|
+
regenerateFailed: true,
|
|
8759
|
+
...(publishedVersion !== undefined ? { publishedVersion } : {}),
|
|
8760
|
+
};
|
|
8761
|
+
} finally {
|
|
8762
|
+
if (scratch !== undefined) rmSync(scratch, { recursive: true, force: true });
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
} catch { /* fail-open only on unexpected showcase-discovery failure; manifest read/parse errors are structured violations above */ }
|
|
7836
8767
|
// agents-md-policy-sync: fixed registry, no tree walk. The pure detector
|
|
7837
8768
|
// recomputes every expected hash from current source text; this gatherer
|
|
7838
8769
|
// only supplies bytes. Any unexpected gather failure omits the fact, and
|
|
@@ -7868,7 +8799,7 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
7868
8799
|
const manifests: Manifest[] = [];
|
|
7869
8800
|
const located: { dir: string; m: Manifest }[] = [];
|
|
7870
8801
|
try {
|
|
7871
|
-
const out =
|
|
8802
|
+
const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
|
|
7872
8803
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
7873
8804
|
try {
|
|
7874
8805
|
const m = JSON.parse(readFileSync(join(root, rel), 'utf8')) as Manifest;
|
|
@@ -7892,6 +8823,11 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
7892
8823
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
7893
8824
|
}
|
|
7894
8825
|
facts['packages'] = packages;
|
|
8826
|
+
facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
|
|
8827
|
+
dir,
|
|
8828
|
+
name: m.name ?? dir,
|
|
8829
|
+
privateFlag: m.private === true,
|
|
8830
|
+
})));
|
|
7895
8831
|
|
|
7896
8832
|
// licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
|
|
7897
8833
|
// `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
|
|
@@ -8116,6 +9052,11 @@ function runGuardEvaluation(root: string, op: string, text: string | undefined,
|
|
|
8116
9052
|
return result;
|
|
8117
9053
|
}
|
|
8118
9054
|
|
|
9055
|
+
function renderGuardObservation(observation: GuardObservation): string {
|
|
9056
|
+
const tag = observation.status === 'unknown' ? 'note' : 'observe';
|
|
9057
|
+
return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
|
|
9058
|
+
}
|
|
9059
|
+
|
|
8119
9060
|
/**
|
|
8120
9061
|
* The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
|
|
8121
9062
|
* lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
|
|
@@ -8456,7 +9397,8 @@ function cmdGuardPromote(options: Map<string, string>, flags: Set<string>, root:
|
|
|
8456
9397
|
...next,
|
|
8457
9398
|
entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
|
|
8458
9399
|
};
|
|
8459
|
-
|
|
9400
|
+
const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
|
|
9401
|
+
try { writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence); } catch { /* best-effort */ }
|
|
8460
9402
|
}
|
|
8461
9403
|
|
|
8462
9404
|
// A refused conflict means the requested apply did NOT fully happen — exit non-zero rather than
|
|
@@ -8548,6 +9490,7 @@ function cmdGuard(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
8548
9490
|
|
|
8549
9491
|
const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
|
|
8550
9492
|
write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
|
|
9493
|
+
for (const observation of result.observations ?? []) write(renderGuardObservation(observation));
|
|
8551
9494
|
for (const v of result.violations) write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
|
|
8552
9495
|
for (const n of result.notes ?? []) write(` [note] ${n}`); // information, never a verdict input (FN-7)
|
|
8553
9496
|
if (result.verdict === 'block' && forced) write(` → forced through: ${force} (logged to .dz/guard-audit.jsonl)`);
|
|
@@ -12972,6 +13915,160 @@ function readOptionalText(path: string): string {
|
|
|
12972
13915
|
}
|
|
12973
13916
|
}
|
|
12974
13917
|
|
|
13918
|
+
function localEvidenceReadReason(resource: 'promotion-journal' | 'guard-audit', error: unknown): string {
|
|
13919
|
+
const code = error && typeof error === 'object' && 'code' in error ? String((error as { code?: unknown }).code) : '';
|
|
13920
|
+
return code === 'ENOENT' ? `${resource}-missing` : `${resource}-unreadable`;
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
interface PromotionEvidenceRead {
|
|
13924
|
+
readonly source: FunnelEvidenceSource<PromotionRunEvidence>;
|
|
13925
|
+
readonly acceptances: readonly PromotionAcceptanceEvidence[];
|
|
13926
|
+
readonly truncatedPeriods: readonly string[];
|
|
13927
|
+
readonly acceptanceHistoryComplete: boolean;
|
|
13928
|
+
}
|
|
13929
|
+
|
|
13930
|
+
function unavailablePromotionEvidence(reason: string): PromotionEvidenceRead {
|
|
13931
|
+
return { source: { status: 'not-measured', reason }, acceptances: [], truncatedPeriods: [], acceptanceHistoryComplete: false };
|
|
13932
|
+
}
|
|
13933
|
+
|
|
13934
|
+
function readPromotionRunEvidence(root: string): PromotionEvidenceRead {
|
|
13935
|
+
let raw: unknown;
|
|
13936
|
+
try {
|
|
13937
|
+
raw = JSON.parse(readFileSync(join(root, PROMOTION_STATE_FILE), 'utf-8')) as unknown;
|
|
13938
|
+
} catch (error) {
|
|
13939
|
+
const reason = error instanceof SyntaxError
|
|
13940
|
+
? 'promotion-journal-malformed'
|
|
13941
|
+
: localEvidenceReadReason('promotion-journal', error);
|
|
13942
|
+
return unavailablePromotionEvidence(reason);
|
|
13943
|
+
}
|
|
13944
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
13945
|
+
return unavailablePromotionEvidence('promotion-journal-malformed');
|
|
13946
|
+
}
|
|
13947
|
+
const record = raw as Record<string, unknown>;
|
|
13948
|
+
if (record['version'] !== 1 || !Object.hasOwn(record, 'runs')) {
|
|
13949
|
+
return unavailablePromotionEvidence(
|
|
13950
|
+
record['version'] === 1 ? 'promotion-history-not-recorded' : 'promotion-journal-malformed',
|
|
13951
|
+
);
|
|
13952
|
+
}
|
|
13953
|
+
if (!Array.isArray(record['runs'])) {
|
|
13954
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13955
|
+
}
|
|
13956
|
+
const state = normalizePromotionState(raw);
|
|
13957
|
+
if (state.runs === undefined || state.runs.length !== record['runs'].length) {
|
|
13958
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13959
|
+
}
|
|
13960
|
+
if (
|
|
13961
|
+
(Object.hasOwn(record, 'acceptances') && (
|
|
13962
|
+
!Array.isArray(record['acceptances']) || state.acceptances?.length !== record['acceptances'].length
|
|
13963
|
+
)) ||
|
|
13964
|
+
(Object.hasOwn(record, 'truncatedRunPeriods') && (
|
|
13965
|
+
!Array.isArray(record['truncatedRunPeriods']) || state.truncatedRunPeriods?.length !== record['truncatedRunPeriods'].length
|
|
13966
|
+
)) ||
|
|
13967
|
+
(Object.hasOwn(record, 'acceptanceHistoryComplete') && typeof record['acceptanceHistoryComplete'] !== 'boolean')
|
|
13968
|
+
) {
|
|
13969
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13970
|
+
}
|
|
13971
|
+
const derivedAcceptances = state.runs.flatMap((run) => run.candidates
|
|
13972
|
+
.filter((candidate) => candidate.verdict === 'promote' && candidate.ruleContentAnchor !== null)
|
|
13973
|
+
.map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor!, acceptedTs: run.ts })));
|
|
13974
|
+
const claimedAcceptanceComplete = state.acceptanceHistoryComplete ??
|
|
13975
|
+
((state.truncatedRunPeriods?.length ?? 0) === 0 && state.runs.every((run) => run.complete === true));
|
|
13976
|
+
if (claimedAcceptanceComplete && state.acceptances !== undefined && derivedAcceptances.some((derived) =>
|
|
13977
|
+
!state.acceptances!.some((stored) =>
|
|
13978
|
+
stored.ruleContentAnchor === derived.ruleContentAnchor &&
|
|
13979
|
+
Date.parse(stored.acceptedTs) <= Date.parse(derived.acceptedTs),
|
|
13980
|
+
),
|
|
13981
|
+
)) {
|
|
13982
|
+
return unavailablePromotionEvidence('promotion-history-malformed');
|
|
13983
|
+
}
|
|
13984
|
+
const acceptancesByAnchor = new Map<string, PromotionAcceptanceEvidence>();
|
|
13985
|
+
for (const acceptance of [...(state.acceptances ?? []), ...derivedAcceptances]) {
|
|
13986
|
+
const prior = acceptancesByAnchor.get(acceptance.ruleContentAnchor);
|
|
13987
|
+
if (prior === undefined || Date.parse(acceptance.acceptedTs) < Date.parse(prior.acceptedTs)) {
|
|
13988
|
+
acceptancesByAnchor.set(acceptance.ruleContentAnchor, acceptance);
|
|
13989
|
+
}
|
|
13990
|
+
}
|
|
13991
|
+
return {
|
|
13992
|
+
source: { status: 'measured', rows: state.runs },
|
|
13993
|
+
acceptances: [...acceptancesByAnchor.values()],
|
|
13994
|
+
truncatedPeriods: state.truncatedRunPeriods ?? [],
|
|
13995
|
+
acceptanceHistoryComplete: claimedAcceptanceComplete,
|
|
13996
|
+
};
|
|
13997
|
+
}
|
|
13998
|
+
|
|
13999
|
+
interface GuardAuditEvidenceRead {
|
|
14000
|
+
readonly source: FunnelEvidenceSource<GuardEvent>;
|
|
14001
|
+
readonly rows: readonly GuardEvent[];
|
|
14002
|
+
readonly text: string | null;
|
|
14003
|
+
}
|
|
14004
|
+
|
|
14005
|
+
function readGuardAuditEvidence(root: string): GuardAuditEvidenceRead {
|
|
14006
|
+
let text: string;
|
|
14007
|
+
try {
|
|
14008
|
+
text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
14009
|
+
} catch (error) {
|
|
14010
|
+
return {
|
|
14011
|
+
source: { status: 'not-measured', reason: localEvidenceReadReason('guard-audit', error) },
|
|
14012
|
+
rows: [],
|
|
14013
|
+
text: null,
|
|
14014
|
+
};
|
|
14015
|
+
}
|
|
14016
|
+
const rows: GuardEvent[] = [];
|
|
14017
|
+
let malformed = false;
|
|
14018
|
+
for (const line of text.split('\n')) {
|
|
14019
|
+
if (line.trim() === '') continue;
|
|
14020
|
+
try {
|
|
14021
|
+
const raw = JSON.parse(line) as Record<string, unknown>;
|
|
14022
|
+
if (
|
|
14023
|
+
!isOffsetIsoTimestamp(raw['ts']) ||
|
|
14024
|
+
!['publish', 'teach', 'consolidate', 'reindex'].includes(String(raw['op'])) ||
|
|
14025
|
+
!['pass', 'warn', 'block'].includes(String(raw['verdict']))
|
|
14026
|
+
) {
|
|
14027
|
+
malformed = true;
|
|
14028
|
+
continue;
|
|
14029
|
+
}
|
|
14030
|
+
const violations: { rule: string; contentAnchor?: string }[] = [];
|
|
14031
|
+
if (!Array.isArray(raw['violations'])) {
|
|
14032
|
+
malformed = true;
|
|
14033
|
+
continue;
|
|
14034
|
+
}
|
|
14035
|
+
for (const item of raw['violations']) {
|
|
14036
|
+
const rule = item && typeof item === 'object' ? (item as { rule?: unknown }).rule : undefined;
|
|
14037
|
+
if (typeof rule !== 'string' || rule === '' || rule.length > 200) {
|
|
14038
|
+
malformed = true;
|
|
14039
|
+
continue;
|
|
14040
|
+
}
|
|
14041
|
+
const anchor = (item as { contentAnchor?: unknown }).contentAnchor;
|
|
14042
|
+
if (anchor !== undefined && !isLessonRuleContentAnchor(anchor)) {
|
|
14043
|
+
malformed = true;
|
|
14044
|
+
continue;
|
|
14045
|
+
}
|
|
14046
|
+
violations.push({ rule, ...(typeof anchor === 'string' ? { contentAnchor: anchor } : {}) });
|
|
14047
|
+
}
|
|
14048
|
+
const verdict = raw['verdict'] as 'pass' | 'warn' | 'block';
|
|
14049
|
+
if ((verdict === 'pass') !== (violations.length === 0)) malformed = true;
|
|
14050
|
+
rows.push({
|
|
14051
|
+
ts: raw['ts'],
|
|
14052
|
+
op: raw['op'] as 'publish' | 'teach' | 'consolidate' | 'reindex',
|
|
14053
|
+
verdict,
|
|
14054
|
+
rules: violations.map((item) => item.rule),
|
|
14055
|
+
violations,
|
|
14056
|
+
});
|
|
14057
|
+
} catch {
|
|
14058
|
+
malformed = true;
|
|
14059
|
+
}
|
|
14060
|
+
}
|
|
14061
|
+
return {
|
|
14062
|
+
source: malformed
|
|
14063
|
+
? { status: 'not-measured', reason: 'guard-audit-malformed' }
|
|
14064
|
+
: !verifyEventChainText(text).ok
|
|
14065
|
+
? { status: 'not-measured', reason: 'guard-audit-chain-corrupt' }
|
|
14066
|
+
: { status: 'measured', rows },
|
|
14067
|
+
rows,
|
|
14068
|
+
text,
|
|
14069
|
+
};
|
|
14070
|
+
}
|
|
14071
|
+
|
|
12975
14072
|
function deadwoodAllowlistText(): string {
|
|
12976
14073
|
const require = createRequire(import.meta.url);
|
|
12977
14074
|
const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
|
|
@@ -13057,7 +14154,7 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13057
14154
|
const json = flags.has('json');
|
|
13058
14155
|
if (flags.has('help')) {
|
|
13059
14156
|
write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
|
|
13060
|
-
write(' pool payoff
|
|
14157
|
+
write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
|
|
13061
14158
|
return 0;
|
|
13062
14159
|
}
|
|
13063
14160
|
for (const flag of flags) {
|
|
@@ -13089,40 +14186,24 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13089
14186
|
// apply-leg usage events (read records only; aggregate rows carry no query by construction)
|
|
13090
14187
|
const usage = readRecallUsageEvents(root);
|
|
13091
14188
|
|
|
13092
|
-
|
|
13093
|
-
const
|
|
13094
|
-
|
|
13095
|
-
const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
13096
|
-
for (const line of text.split('\n')) {
|
|
13097
|
-
if (line.trim() === '') continue;
|
|
13098
|
-
try {
|
|
13099
|
-
const o = JSON.parse(line) as { ts?: unknown; verdict?: unknown; violations?: { rule?: unknown }[] };
|
|
13100
|
-
if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
|
|
13101
|
-
guard.push({
|
|
13102
|
-
ts: o.ts,
|
|
13103
|
-
verdict: o.verdict,
|
|
13104
|
-
rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
|
|
13105
|
-
});
|
|
13106
|
-
}
|
|
13107
|
-
} catch {
|
|
13108
|
-
/* skip */
|
|
13109
|
-
}
|
|
13110
|
-
}
|
|
13111
|
-
} catch {
|
|
13112
|
-
/* no audit yet */
|
|
13113
|
-
}
|
|
14189
|
+
const promotionEvidence = readPromotionRunEvidence(root);
|
|
14190
|
+
const guardEvidence = readGuardAuditEvidence(root);
|
|
14191
|
+
const guard = [...guardEvidence.rows];
|
|
13114
14192
|
|
|
13115
14193
|
// The evidence logs themselves, verbatim: the report verifies their hash chains (feature
|
|
13116
14194
|
// event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
|
|
13117
14195
|
// "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
|
|
13118
14196
|
const evidenceLogs: { log: string; text: string }[] = [];
|
|
13119
|
-
for (const rel of ['.dz/recall-usage.jsonl'
|
|
14197
|
+
for (const rel of ['.dz/recall-usage.jsonl']) {
|
|
13120
14198
|
try {
|
|
13121
14199
|
evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
|
|
13122
14200
|
} catch {
|
|
13123
14201
|
/* absent log — reported by its own gate above, not invented here */
|
|
13124
14202
|
}
|
|
13125
14203
|
}
|
|
14204
|
+
if (guardEvidence.text !== null) {
|
|
14205
|
+
evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
|
|
14206
|
+
}
|
|
13126
14207
|
|
|
13127
14208
|
const now = new Date();
|
|
13128
14209
|
const report = assembleCompoundingReport({
|
|
@@ -13132,6 +14213,13 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13132
14213
|
nowTs: now.toISOString(),
|
|
13133
14214
|
evidenceLogs,
|
|
13134
14215
|
cmdUsageDepthDays: cmdUsageDepthDays(root, now),
|
|
14216
|
+
lessonToRule: {
|
|
14217
|
+
promotionRuns: promotionEvidence.source,
|
|
14218
|
+
guardAudits: guardEvidence.source,
|
|
14219
|
+
promotionAcceptances: promotionEvidence.acceptances,
|
|
14220
|
+
truncatedPromotionPeriods: promotionEvidence.truncatedPeriods,
|
|
14221
|
+
acceptanceHistoryComplete: promotionEvidence.acceptanceHistoryComplete,
|
|
14222
|
+
},
|
|
13135
14223
|
});
|
|
13136
14224
|
// lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
|
|
13137
14225
|
// dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
|
|
@@ -14272,6 +15360,34 @@ async function cmdBacklog(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
14272
15360
|
return sub === undefined ? 0 : 1;
|
|
14273
15361
|
}
|
|
14274
15362
|
|
|
15363
|
+
/** Read the recommender's existing telemetry planes plus its idempotency receipt. One adapter is
|
|
15364
|
+
* shared by `recommend` and the publish advisory so their freshness definitions cannot drift. */
|
|
15365
|
+
function readRoutingTelemetry(repoRoot: string): { harvest: ReturnType<typeof harvestStageOutcomes>; alreadyFed: string[] } {
|
|
15366
|
+
const records: unknown[] = readHarnessRecords(repoRoot);
|
|
15367
|
+
for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
|
|
15368
|
+
if (!existsSync(base)) continue;
|
|
15369
|
+
let names: string[] = [];
|
|
15370
|
+
try { names = readdirSync(base); } catch { continue; }
|
|
15371
|
+
for (const name of names) {
|
|
15372
|
+
const sidecar = join(base, name, 'run-meta.json');
|
|
15373
|
+
if (!existsSync(sidecar)) continue;
|
|
15374
|
+
try {
|
|
15375
|
+
const meta = JSON.parse(readFileSync(sidecar, 'utf-8')) as { runMeta?: { resolved?: boolean; records?: unknown[] } };
|
|
15376
|
+
if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records)) records.push(...meta.runMeta.records);
|
|
15377
|
+
} catch { /* an unreadable sidecar contributes no asserted record */ }
|
|
15378
|
+
}
|
|
15379
|
+
}
|
|
15380
|
+
let alreadyFed: string[] = [];
|
|
15381
|
+
const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
|
|
15382
|
+
if (existsSync(fedPath)) {
|
|
15383
|
+
const parsed = JSON.parse(readFileSync(join(repoRoot, '.dz', 'routing-fed.json'), 'utf-8')) as unknown;
|
|
15384
|
+
if (!Array.isArray(parsed)) throw new Error('.dz/routing-fed.json must be a JSON array of runIds');
|
|
15385
|
+
if (parsed.some((v) => typeof v !== 'string')) throw new Error('.dz/routing-fed.json contains a non-string runId');
|
|
15386
|
+
alreadyFed = parsed as string[];
|
|
15387
|
+
}
|
|
15388
|
+
return { harvest: harvestStageOutcomes(records), alreadyFed };
|
|
15389
|
+
}
|
|
15390
|
+
|
|
14275
15391
|
/**
|
|
14276
15392
|
* `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
|
|
14277
15393
|
* what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
|
|
@@ -14286,28 +15402,20 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
14286
15402
|
// ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
|
|
14287
15403
|
// store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
|
|
14288
15404
|
if (options.get('_positional_0') === 'recommend') {
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
try { names = readdirSync(base); } catch { continue; }
|
|
14295
|
-
for (const name of names) {
|
|
14296
|
-
const sidecar = join(base, name, 'run-meta.json');
|
|
14297
|
-
if (!existsSync(sidecar)) continue;
|
|
14298
|
-
try {
|
|
14299
|
-
const meta = JSON.parse(readFileSync(sidecar, 'utf-8')) as { runMeta?: { resolved?: boolean; records?: unknown[] } };
|
|
14300
|
-
if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records)) records.push(...meta.runMeta.records);
|
|
14301
|
-
} catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
|
|
14302
|
-
}
|
|
15405
|
+
let routing: ReturnType<typeof readRoutingTelemetry>;
|
|
15406
|
+
try { routing = readRoutingTelemetry(repoRoot); }
|
|
15407
|
+
catch (e) {
|
|
15408
|
+
write(`dz routing recommend: cannot read routing freshness state — ${(e as Error).message}`);
|
|
15409
|
+
return 1;
|
|
14303
15410
|
}
|
|
14304
|
-
const harvest =
|
|
14305
|
-
|
|
15411
|
+
const harvest = routing.harvest;
|
|
15412
|
+
let rec = recommendModels(harvest, {
|
|
15413
|
+
...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}),
|
|
15414
|
+
alreadyFed: routing.alreadyFed,
|
|
15415
|
+
});
|
|
14306
15416
|
if (flags.has('apply')) {
|
|
14307
15417
|
const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
|
|
14308
|
-
|
|
14309
|
-
try { alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8')) as string[]; } catch { /* first apply */ }
|
|
14310
|
-
const plan = planFeed(harvest.samples, alreadyFed);
|
|
15418
|
+
const plan = planFeed(harvest.samples, routing.alreadyFed);
|
|
14311
15419
|
for (const sample of plan.toFeed) finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
|
|
14312
15420
|
try {
|
|
14313
15421
|
mkdirSync(join(repoRoot, '.dz'), { recursive: true });
|
|
@@ -14316,6 +15424,10 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
14316
15424
|
write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${(e as Error).message}`);
|
|
14317
15425
|
return 1;
|
|
14318
15426
|
}
|
|
15427
|
+
rec = recommendModels(harvest, {
|
|
15428
|
+
...(options.get('tier') !== undefined ? { tier: options.get('tier')! } : {}),
|
|
15429
|
+
alreadyFed: plan.fedAfter,
|
|
15430
|
+
});
|
|
14319
15431
|
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)` : ''}`);
|
|
14320
15432
|
}
|
|
14321
15433
|
if (flags.has('json')) {
|
|
@@ -14327,6 +15439,9 @@ function cmdRouting(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
14327
15439
|
write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
|
|
14328
15440
|
}
|
|
14329
15441
|
write(` basis: ${rec.basis.rule}`);
|
|
15442
|
+
if (rec.basis.freshness === 'current') write(' store: current (0 unharvested runs)');
|
|
15443
|
+
else if (rec.basis.freshness === 'stale') 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\``);
|
|
15444
|
+
else write(' store: UNFED — auto-cost has no learned rows; picks below are cold-start');
|
|
14330
15445
|
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}`);
|
|
14331
15446
|
write(` basis: ${rec.basis.crossFamilyNote}`);
|
|
14332
15447
|
return 0;
|
|
@@ -14997,11 +16112,20 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
14997
16112
|
case 'bundle':
|
|
14998
16113
|
return cmdBundle(options, flags, cwd, write);
|
|
14999
16114
|
case 'teach':
|
|
15000
|
-
return await cmdTeach(
|
|
16115
|
+
return await cmdTeach(
|
|
16116
|
+
options,
|
|
16117
|
+
flags,
|
|
16118
|
+
cwd,
|
|
16119
|
+
write,
|
|
16120
|
+
writeErr,
|
|
16121
|
+
io.interactive ?? process.stdout.isTTY === true,
|
|
16122
|
+
io.teachGuardRunner ?? teachGuard,
|
|
16123
|
+
io.teachReinforceRunner ?? runTeachGuardReinforcement,
|
|
16124
|
+
);
|
|
15001
16125
|
case 'consolidate':
|
|
15002
16126
|
return await cmdConsolidate(options, flags, cwd, write);
|
|
15003
16127
|
case 'recall':
|
|
15004
|
-
return await cmdRecall(options, flags, cwd, write);
|
|
16128
|
+
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
15005
16129
|
case 'vector':
|
|
15006
16130
|
return await cmdVector(options, flags, cwd, write);
|
|
15007
16131
|
case 'brain':
|
|
@@ -15054,6 +16178,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
15054
16178
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
15055
16179
|
case 'hooks-sync':
|
|
15056
16180
|
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
16181
|
+
case 'integrations-verify':
|
|
16182
|
+
return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
|
|
15057
16183
|
case 'agents-sync':
|
|
15058
16184
|
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
15059
16185
|
case 'sync-canonical':
|