@dzhechkov/harness-cli 0.8.6 → 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 +106 -9
- 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 +1062 -98
- 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 +8 -8
- package/sbom.json +24 -24
- package/src/boolean-flags.ts +1 -0
- package/src/cli.ts +1113 -100
- 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
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
TARGET_NAMES_SORTED,
|
|
30
30
|
runDoctor,
|
|
31
31
|
runInit,
|
|
32
|
+
discoverSkillIds,
|
|
33
|
+
runIntegrationsVerify,
|
|
32
34
|
resolvePackageSkillRoots,
|
|
33
35
|
PACKAGE_SKILL_LAYOUTS,
|
|
34
36
|
benchmarkSkill,
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
NamedLockTimeoutError,
|
|
82
84
|
NamedLockCompromisedError,
|
|
83
85
|
type CodexHooksSyncReport,
|
|
86
|
+
type IntegrationManifestSource,
|
|
84
87
|
type ParityCell,
|
|
85
88
|
type ParityFeature,
|
|
86
89
|
type ParityReportCell,
|
|
@@ -116,6 +119,8 @@ import {
|
|
|
116
119
|
renderTimelineHtml,
|
|
117
120
|
importEcc,
|
|
118
121
|
recordPattern,
|
|
122
|
+
recordLessonForms,
|
|
123
|
+
normalizeLessonForms,
|
|
119
124
|
resolveLearningBackend,
|
|
120
125
|
storeStats,
|
|
121
126
|
consolidateSessions,
|
|
@@ -141,6 +146,17 @@ import {
|
|
|
141
146
|
importRvfCheckpoint,
|
|
142
147
|
statuslineData,
|
|
143
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,
|
|
144
160
|
computeUsage,
|
|
145
161
|
deriveCostLedger,
|
|
146
162
|
planLedgerBackfill,
|
|
@@ -167,6 +183,8 @@ import {
|
|
|
167
183
|
queryBookKnowledge,
|
|
168
184
|
loadStorePatternsSync,
|
|
169
185
|
patternRecordId,
|
|
186
|
+
patternIdentityOf,
|
|
187
|
+
mergeLessonMatchedForms,
|
|
170
188
|
loadStoreRecords,
|
|
171
189
|
recordToPattern,
|
|
172
190
|
bundleSkills,
|
|
@@ -204,12 +222,22 @@ import {
|
|
|
204
222
|
DEFAULT_RULES,
|
|
205
223
|
parsePnpmLockImporters,
|
|
206
224
|
scannableStubPath,
|
|
225
|
+
type FeatureArtifactFact,
|
|
226
|
+
type FeatureTier,
|
|
227
|
+
type FeatureVolumeFact,
|
|
228
|
+
type GuardObservation,
|
|
229
|
+
type TemplateVolumeFileFact,
|
|
230
|
+
type TemplateVolumeTargetFact,
|
|
231
|
+
type VolumeShadowInput,
|
|
207
232
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
208
233
|
assembleCandidates,
|
|
209
234
|
renderPromotionReport,
|
|
210
235
|
renderPromotionAdr,
|
|
211
236
|
normalizePromotionState,
|
|
212
237
|
nextPromotionState,
|
|
238
|
+
recordPromotionRunEvidence,
|
|
239
|
+
isLessonRuleContentAnchor,
|
|
240
|
+
isOffsetIsoTimestamp,
|
|
213
241
|
globMatch,
|
|
214
242
|
promotionAdrRelPath,
|
|
215
243
|
DEFAULT_WINDOW_DAYS,
|
|
@@ -219,6 +247,10 @@ import {
|
|
|
219
247
|
type ChangeSet,
|
|
220
248
|
type ExistingRuleView,
|
|
221
249
|
type PromotionReport,
|
|
250
|
+
type PromotionAcceptanceEvidence,
|
|
251
|
+
type PromotionRunEvidence,
|
|
252
|
+
type FunnelEvidenceSource,
|
|
253
|
+
type GuardEvent,
|
|
222
254
|
decideProvenance,
|
|
223
255
|
isInsideTree,
|
|
224
256
|
signManifest,
|
|
@@ -522,7 +554,7 @@ import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
|
522
554
|
import type { LogTail } from '@dzhechkov/harness-core';
|
|
523
555
|
import type { DeadwoodInventoryItem } from '@dzhechkov/harness-core';
|
|
524
556
|
import type { ContractDiagnostic, ContractEvidenceReader } from '@dzhechkov/harness-core';
|
|
525
|
-
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';
|
|
526
558
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
527
559
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
528
560
|
|
|
@@ -534,7 +566,7 @@ export const DZ_COMMANDS: readonly string[] = [
|
|
|
534
566
|
'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
|
|
535
567
|
'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
|
|
536
568
|
'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
|
|
537
|
-
'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
|
|
569
|
+
'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
|
|
538
570
|
'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
|
|
539
571
|
'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
|
|
540
572
|
'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
|
|
@@ -547,7 +579,7 @@ export const DZ_COMMANDS: readonly string[] = [
|
|
|
547
579
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
548
580
|
|
|
549
581
|
Usage:
|
|
550
|
-
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)
|
|
551
583
|
dz verify [--skills-dir <dir>] [--target <name>]
|
|
552
584
|
dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
|
|
553
585
|
dz update (alias of sync)
|
|
@@ -576,7 +608,7 @@ Usage:
|
|
|
576
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)
|
|
577
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)
|
|
578
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)
|
|
579
|
-
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)
|
|
580
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)
|
|
581
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)
|
|
582
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)
|
|
@@ -617,7 +649,7 @@ Usage:
|
|
|
617
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)
|
|
618
650
|
dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
|
|
619
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)
|
|
620
|
-
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)
|
|
621
653
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
622
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)
|
|
623
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)
|
|
@@ -656,6 +688,7 @@ Usage:
|
|
|
656
688
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
657
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)
|
|
658
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)
|
|
659
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)
|
|
660
693
|
dz plugin [--version <ver>]
|
|
661
694
|
dz downloads
|
|
@@ -706,6 +739,14 @@ export interface CliIo {
|
|
|
706
739
|
* command that needs it (`brain ground`), and never when stdin is a TTY (nothing piped).
|
|
707
740
|
*/
|
|
708
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 }>;
|
|
709
750
|
/**
|
|
710
751
|
* Test seam for `dz release`: overrides subprocess execution for gate steps and the
|
|
711
752
|
* gh/git side channels (production leaves it unset → real `execSync`, stdio piped).
|
|
@@ -823,6 +864,8 @@ interface InstallSkillsResult {
|
|
|
823
864
|
* the source `SKILL.md`.
|
|
824
865
|
*/
|
|
825
866
|
readonly applyFailures: SkillApplyFailure[];
|
|
867
|
+
readonly integrations: readonly IntegrationOutcome[];
|
|
868
|
+
readonly integrationDigest?: string;
|
|
826
869
|
}
|
|
827
870
|
|
|
828
871
|
/**
|
|
@@ -838,6 +881,10 @@ async function installSkills(opts: {
|
|
|
838
881
|
select?: readonly string[] | undefined;
|
|
839
882
|
force: boolean;
|
|
840
883
|
enrich: boolean;
|
|
884
|
+
noHooks?: boolean;
|
|
885
|
+
noIntegrations?: boolean;
|
|
886
|
+
noVerify?: boolean;
|
|
887
|
+
allowIntegrations?: string;
|
|
841
888
|
}): Promise<InstallSkillsResult> {
|
|
842
889
|
const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
|
|
843
890
|
|
|
@@ -850,8 +897,8 @@ async function installSkills(opts: {
|
|
|
850
897
|
// route them through one aggregation.
|
|
851
898
|
if (target === 'agents-md' || target === 'gemini') {
|
|
852
899
|
const report = target === 'gemini'
|
|
853
|
-
? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
|
|
854
|
-
: 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 } : {}) });
|
|
855
902
|
const results = report.skills.map((s) => ({
|
|
856
903
|
id: s.id,
|
|
857
904
|
written: s.written.length,
|
|
@@ -860,13 +907,21 @@ async function installSkills(opts: {
|
|
|
860
907
|
let written = 0;
|
|
861
908
|
let skipped = 0;
|
|
862
909
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
863
|
-
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 } : {}) };
|
|
864
911
|
}
|
|
865
912
|
|
|
866
913
|
const results: { id: string; written: number; skipped: number }[] = [];
|
|
867
914
|
const failures: SkillLoadFailure[] = [];
|
|
868
915
|
const applyFailures: SkillApplyFailure[] = [];
|
|
869
|
-
|
|
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()) {
|
|
870
925
|
const r = await runInit({
|
|
871
926
|
target,
|
|
872
927
|
skillsDir,
|
|
@@ -874,12 +929,24 @@ async function installSkills(opts: {
|
|
|
874
929
|
force,
|
|
875
930
|
enrich,
|
|
876
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 } : {}),
|
|
877
941
|
});
|
|
878
942
|
for (const skill of r.skills) {
|
|
879
943
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
880
944
|
}
|
|
881
945
|
failures.push(...r.failures);
|
|
882
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;
|
|
883
950
|
}
|
|
884
951
|
|
|
885
952
|
let written = 0;
|
|
@@ -887,7 +954,7 @@ async function installSkills(opts: {
|
|
|
887
954
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
888
955
|
const installed = new Set(results.map((s) => s.id));
|
|
889
956
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
890
|
-
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 } : {}) };
|
|
891
958
|
}
|
|
892
959
|
|
|
893
960
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
@@ -949,26 +1016,46 @@ async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
949
1016
|
select,
|
|
950
1017
|
force: flags.has('force'),
|
|
951
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')! } : {}),
|
|
952
1023
|
});
|
|
953
1024
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
writeMissingSkillsHint(write, r.missing, presetName);
|
|
959
|
-
|
|
960
|
-
// Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
|
|
961
|
-
// target's harness: the veto + recall legs are what `--target codex` promises.
|
|
962
|
-
// `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
|
|
963
|
-
// carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
|
|
964
|
-
// 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;
|
|
965
1029
|
let codexHooksOk = true;
|
|
966
1030
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
967
1031
|
const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
|
|
968
1032
|
codexHooksOk = delivery.ok;
|
|
969
1033
|
for (const line of delivery.stdout) write(line);
|
|
970
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
|
+
}
|
|
971
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
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
writeMissingSkillsHint(write, r.missing, presetName);
|
|
1058
|
+
|
|
972
1059
|
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
973
1060
|
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
974
1061
|
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
@@ -987,7 +1074,35 @@ async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
987
1074
|
for (const line of formatSkillApplyFailures(r.applyFailures)) writeErr(line);
|
|
988
1075
|
return 1;
|
|
989
1076
|
}
|
|
990
|
-
|
|
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;
|
|
991
1106
|
}
|
|
992
1107
|
|
|
993
1108
|
async function cmdVerify(options: Map<string, string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
@@ -2423,6 +2538,146 @@ function cmdStatuslineFaRecord(options: Map<string, string>, cwd: string, write:
|
|
|
2423
2538
|
return 0;
|
|
2424
2539
|
}
|
|
2425
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
|
+
|
|
2426
2681
|
/**
|
|
2427
2682
|
* `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
|
|
2428
2683
|
* Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
|
|
@@ -2447,9 +2702,22 @@ function cmdStatusline(
|
|
|
2447
2702
|
try {
|
|
2448
2703
|
const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
|
|
2449
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
|
+
}
|
|
2450
2718
|
|
|
2451
2719
|
if (flags.has('json')) {
|
|
2452
|
-
write(JSON.stringify(data));
|
|
2720
|
+
write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
|
|
2453
2721
|
return 0;
|
|
2454
2722
|
}
|
|
2455
2723
|
|
|
@@ -2459,13 +2727,12 @@ function cmdStatusline(
|
|
|
2459
2727
|
if (data.consolidatedAgeH !== undefined) line += ` · ⟳ ${data.consolidatedAgeH}h`;
|
|
2460
2728
|
|
|
2461
2729
|
// Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
|
|
2462
|
-
const fa = data.featureAdr;
|
|
2463
2730
|
if (fa !== undefined) {
|
|
2464
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.
|
|
2465
2732
|
if (fa.kind === 'loop') {
|
|
2466
2733
|
line = `🔁 loop ${fa.step} · ${line}`;
|
|
2467
2734
|
} else {
|
|
2468
|
-
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}`;
|
|
2469
2736
|
}
|
|
2470
2737
|
}
|
|
2471
2738
|
|
|
@@ -3092,7 +3359,22 @@ function learningStoreLine(
|
|
|
3092
3359
|
) + (reason ? ' [' + reason + ']' : '');
|
|
3093
3360
|
}
|
|
3094
3361
|
|
|
3095
|
-
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> {
|
|
3096
3378
|
// WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
|
|
3097
3379
|
// `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
|
|
3098
3380
|
// per-session choice; for a CLI every invocation is a fresh process, so the only honest session
|
|
@@ -3176,26 +3458,39 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3176
3458
|
write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
|
|
3177
3459
|
return 1;
|
|
3178
3460
|
}
|
|
3179
|
-
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));
|
|
3180
3465
|
let imported = 0;
|
|
3181
3466
|
let skipped = 0;
|
|
3182
3467
|
const importedRecs: PatternRecord[] = [];
|
|
3183
3468
|
for (const item of parsed) {
|
|
3184
3469
|
const p = item as Partial<PatternRecord>;
|
|
3185
|
-
if (!p || typeof p
|
|
3470
|
+
if (!p || typeof p !== 'object') {
|
|
3186
3471
|
skipped += 1;
|
|
3187
3472
|
continue;
|
|
3188
3473
|
}
|
|
3189
|
-
const
|
|
3190
|
-
|
|
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 : '',
|
|
3191
3480
|
type: (typeof p.type === 'string' ? p.type : 'lesson-learned') as PatternRecord['type'],
|
|
3192
3481
|
reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
|
|
3193
3482
|
domain: typeof p.domain === 'string' ? p.domain : 'general',
|
|
3194
3483
|
ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
|
|
3195
3484
|
source: 'dz-teach-import',
|
|
3196
|
-
|
|
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;
|
|
3197
3492
|
await recordPattern(storeRoot, rec);
|
|
3198
|
-
existing.add(
|
|
3493
|
+
existing.add(importedKey(rec));
|
|
3199
3494
|
importedRecs.push(rec);
|
|
3200
3495
|
imported += 1;
|
|
3201
3496
|
}
|
|
@@ -3279,23 +3574,30 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3279
3574
|
|
|
3280
3575
|
const reward = parseFloat(options.get('reward') ?? '0.8');
|
|
3281
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;
|
|
3282
3580
|
|
|
3283
3581
|
if (flags.has('guard')) {
|
|
3284
|
-
const verdict = await
|
|
3582
|
+
const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
|
|
3285
3583
|
if (verdict.action === 'reinforce') {
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
const
|
|
3295
|
-
if (
|
|
3296
|
-
|
|
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`);
|
|
3297
3600
|
}
|
|
3298
|
-
write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
|
|
3299
3601
|
}
|
|
3300
3602
|
}
|
|
3301
3603
|
|
|
@@ -3310,24 +3612,63 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3310
3612
|
// Typed through the shared schema (harness-core owns PatternRecord) so the
|
|
3311
3613
|
// write side and the read side (recommend's loadPatterns) can never drift —
|
|
3312
3614
|
// a field rename here is a compile error, not a silently re-muted loop (audit #2).
|
|
3313
|
-
const entry: PatternRecord = {
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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
|
+
};
|
|
3321
3623
|
|
|
3322
3624
|
// Tier-2 (ADR-005): persist through the unified @dzhechkov/memory store. recordPattern
|
|
3323
3625
|
// folds any legacy .dz/patterns.jsonl into the backend (idempotent) and returns the
|
|
3324
3626
|
// total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
|
|
3325
3627
|
// lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
|
|
3326
3628
|
const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
|
|
3327
|
-
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;
|
|
3328
3669
|
|
|
3329
|
-
write(
|
|
3330
|
-
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)`);
|
|
3331
3672
|
write(` Total patterns: ${count}`);
|
|
3332
3673
|
// WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
|
|
3333
3674
|
// domain, the reward and the backend — and not one word about the path, so a store written to
|
|
@@ -3337,6 +3678,20 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3337
3678
|
// sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
|
|
3338
3679
|
// nothing, so the line stays byte-identical for everyone who set nothing.
|
|
3339
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
|
+
}
|
|
3340
3695
|
// ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
|
|
3341
3696
|
// directories and this binary; refusing would be defending a user against themselves,
|
|
3342
3697
|
// which this design does not attempt. Making the choice INFORMED is the part that is
|
|
@@ -3351,8 +3706,8 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
3351
3706
|
write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
|
|
3352
3707
|
}
|
|
3353
3708
|
// The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
|
|
3354
|
-
await emitMirrorQ(storeRoot,
|
|
3355
|
-
return 0;
|
|
3709
|
+
await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
|
|
3710
|
+
return commandFailed ? 1 : 0;
|
|
3356
3711
|
}
|
|
3357
3712
|
|
|
3358
3713
|
async function cmdConsolidate(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
@@ -3704,7 +4059,14 @@ async function cmdRecallPromote(
|
|
|
3704
4059
|
return 0;
|
|
3705
4060
|
}
|
|
3706
4061
|
|
|
3707
|
-
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> {
|
|
3708
4070
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3709
4071
|
const asJson = flags.has('json');
|
|
3710
4072
|
const all = flags.has('all');
|
|
@@ -3723,7 +4085,17 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3723
4085
|
// review could not make correct: a tag set by the writer is decidable, prose is not.
|
|
3724
4086
|
const allPatterns = loadStorePatternsSync(projectRoot);
|
|
3725
4087
|
const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
|
|
3726
|
-
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
|
+
});
|
|
3727
4099
|
const holdoutNote = renderHoldoutNote(holdout);
|
|
3728
4100
|
// The opt-in is honoured without argument — and named out loud. A flag that silently
|
|
3729
4101
|
// includes medical lessons in a portable export is a flag whose consequence the user
|
|
@@ -3757,7 +4129,15 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3757
4129
|
const perDomain = Object.fromEntries(
|
|
3758
4130
|
Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))),
|
|
3759
4131
|
);
|
|
3760
|
-
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
|
+
};
|
|
3761
4141
|
const backendStats = resolveLearningBackend(projectRoot).getStats();
|
|
3762
4142
|
if (asJson) {
|
|
3763
4143
|
write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
|
|
@@ -3769,6 +4149,7 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3769
4149
|
write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
|
|
3770
4150
|
write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
|
|
3771
4151
|
write(` exact-dup groups: ${stats.exactDupGroups}`);
|
|
4152
|
+
write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
|
|
3772
4153
|
write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
|
|
3773
4154
|
write(' top uses:');
|
|
3774
4155
|
for (const row of stats.topUses) write(` ${row.uses}× [${row.reward.toFixed(2)}] (${row.domain}) ${row.pattern.slice(0, 80)}`);
|
|
@@ -3889,6 +4270,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3889
4270
|
? ('lexical' as const)
|
|
3890
4271
|
: ('hybrid' as const);
|
|
3891
4272
|
const wantedDomain = options.get('domain');
|
|
4273
|
+
const classRecallOptions = {
|
|
4274
|
+
onClassDegraded: writeErr,
|
|
4275
|
+
...(classMatcher === undefined ? {} : { classMatcher }),
|
|
4276
|
+
};
|
|
3892
4277
|
// OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
|
|
3893
4278
|
// truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
|
|
3894
4279
|
// never receive its promised lift — the feature was weakest in exactly the case it
|
|
@@ -3911,8 +4296,8 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3911
4296
|
// second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
|
|
3912
4297
|
// own output is written after it returns.
|
|
3913
4298
|
const result = asJson
|
|
3914
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3915
|
-
: 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 } : {}) });
|
|
3916
4301
|
|
|
3917
4302
|
if (mode === 'semantic' && result.vectorEngine === 'none') {
|
|
3918
4303
|
// --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
|
|
@@ -3948,13 +4333,26 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
3948
4333
|
let globalHits: typeof result.hits = [];
|
|
3949
4334
|
if (readGlobal) {
|
|
3950
4335
|
const g = asJson
|
|
3951
|
-
? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
|
|
3952
|
-
: 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 } : {}) });
|
|
3953
4338
|
globalHits = g.hits;
|
|
3954
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]));
|
|
3955
4349
|
const merged = readGlobal
|
|
3956
|
-
? mergeStoreHits(
|
|
3957
|
-
|
|
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;
|
|
3958
4356
|
const hits = merged.slice(0, limit);
|
|
3959
4357
|
// Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
|
|
3960
4358
|
// paths that return earlier — `--json` and the zero-hits branch — still reported success. That
|
|
@@ -4002,6 +4400,7 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4002
4400
|
// version emitted the number while its own comment promised null (found by independent review).
|
|
4003
4401
|
write(JSON.stringify(hits.map((h) => ({
|
|
4004
4402
|
...h.pattern,
|
|
4403
|
+
...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
|
|
4005
4404
|
relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
|
|
4006
4405
|
// The TRUE cosine, as a second companion key — the design named it and the first ship missed
|
|
4007
4406
|
// it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
|
|
@@ -4066,6 +4465,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4066
4465
|
// guessing which second one answered.
|
|
4067
4466
|
write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
|
|
4068
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
|
+
}
|
|
4069
4472
|
let sawQuarantined = false;
|
|
4070
4473
|
for (const h of hits) {
|
|
4071
4474
|
const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
|
|
@@ -4090,9 +4493,21 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
4090
4493
|
// 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
|
|
4091
4494
|
// hidden remainder, and the cosine then appears to describe the visible fragment rather than
|
|
4092
4495
|
// the whole lesson. `--full` prints it all, still on one line.
|
|
4093
|
-
const
|
|
4094
|
-
const
|
|
4095
|
-
|
|
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
|
+
}
|
|
4096
4511
|
// WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
|
|
4097
4512
|
// blindness the store-location line just removed, one level down: the reader would see more
|
|
4098
4513
|
// results and have no way to tell whether the global store is even connected.
|
|
@@ -5096,7 +5511,13 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5096
5511
|
? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
5097
5512
|
: getPreset(preset)?.skills;
|
|
5098
5513
|
const install = select !== undefined && select.length > 0
|
|
5099
|
-
? 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
|
+
})
|
|
5100
5521
|
: undefined;
|
|
5101
5522
|
if (install) {
|
|
5102
5523
|
write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
|
|
@@ -5106,6 +5527,7 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5106
5527
|
|
|
5107
5528
|
// Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
|
|
5108
5529
|
// setup has already run and the summary still prints; only the exit code carries the failure.
|
|
5530
|
+
let setupIntegrationOutcomes: readonly IntegrationOutcome[] = install?.integrations ?? [];
|
|
5109
5531
|
let codexHooksOk = true;
|
|
5110
5532
|
if (target === 'codex' && !flags.has('no-hooks')) {
|
|
5111
5533
|
write(`║ 5. Delivering codex hooks (live verify)... ║`);
|
|
@@ -5113,6 +5535,11 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5113
5535
|
codexHooksOk = delivery.ok;
|
|
5114
5536
|
for (const line of delivery.stdout) write(line);
|
|
5115
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
|
+
}
|
|
5116
5543
|
}
|
|
5117
5544
|
|
|
5118
5545
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
@@ -5132,6 +5559,9 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5132
5559
|
if (install) {
|
|
5133
5560
|
write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
|
|
5134
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
|
+
}
|
|
5135
5565
|
writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
|
|
5136
5566
|
}
|
|
5137
5567
|
// A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
|
|
@@ -5146,7 +5576,8 @@ async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
5146
5576
|
if (erroredSteps.length > 0) {
|
|
5147
5577
|
write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
|
|
5148
5578
|
}
|
|
5149
|
-
|
|
5579
|
+
const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
|
|
5580
|
+
return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
|
|
5150
5581
|
}
|
|
5151
5582
|
|
|
5152
5583
|
function cmdPretrain(options: Map<string, string>, cwd: string, write: Write): number {
|
|
@@ -5804,6 +6235,9 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
5804
6235
|
if (guardResult.verdict === 'block') write(`dz publish: ⚠ guard BLOCK overridden via --no-guard: ${noGuard} (logged)`);
|
|
5805
6236
|
else if (guardResult.verdict === 'warn') for (const v of guardResult.violations) write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
|
|
5806
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
|
+
}
|
|
5807
6241
|
for (const n of guardResult.notes ?? []) write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
|
|
5808
6242
|
}
|
|
5809
6243
|
|
|
@@ -7503,6 +7937,31 @@ export interface CodexHooksSummary {
|
|
|
7503
7937
|
readonly stderr: readonly string[];
|
|
7504
7938
|
}
|
|
7505
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
|
+
|
|
7506
7965
|
/**
|
|
7507
7966
|
* What the user is told about a sync report — the ONE place the success word can be printed.
|
|
7508
7967
|
*
|
|
@@ -7831,6 +8290,392 @@ function gatherReadmeCounts(root: string): { label: string; a: number; b: number
|
|
|
7831
8290
|
return pairs;
|
|
7832
8291
|
}
|
|
7833
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
|
+
|
|
7834
8679
|
/** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
|
|
7835
8680
|
function gatherGuardFacts(op: string, root: string, text: string | undefined, storeCap: number): Record<string, unknown> {
|
|
7836
8681
|
const facts: Record<string, unknown> = { op };
|
|
@@ -7954,7 +8799,7 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
7954
8799
|
const manifests: Manifest[] = [];
|
|
7955
8800
|
const located: { dir: string; m: Manifest }[] = [];
|
|
7956
8801
|
try {
|
|
7957
|
-
const out =
|
|
8802
|
+
const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
|
|
7958
8803
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
7959
8804
|
try {
|
|
7960
8805
|
const m = JSON.parse(readFileSync(join(root, rel), 'utf8')) as Manifest;
|
|
@@ -7978,6 +8823,11 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
7978
8823
|
packages.push({ name: m.name ?? '(unnamed)', deps });
|
|
7979
8824
|
}
|
|
7980
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
|
+
})));
|
|
7981
8831
|
|
|
7982
8832
|
// licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
|
|
7983
8833
|
// `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
|
|
@@ -8202,6 +9052,11 @@ function runGuardEvaluation(root: string, op: string, text: string | undefined,
|
|
|
8202
9052
|
return result;
|
|
8203
9053
|
}
|
|
8204
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
|
+
|
|
8205
9060
|
/**
|
|
8206
9061
|
* The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
|
|
8207
9062
|
* lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
|
|
@@ -8542,7 +9397,8 @@ function cmdGuardPromote(options: Map<string, string>, flags: Set<string>, root:
|
|
|
8542
9397
|
...next,
|
|
8543
9398
|
entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
|
|
8544
9399
|
};
|
|
8545
|
-
|
|
9400
|
+
const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
|
|
9401
|
+
try { writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence); } catch { /* best-effort */ }
|
|
8546
9402
|
}
|
|
8547
9403
|
|
|
8548
9404
|
// A refused conflict means the requested apply did NOT fully happen — exit non-zero rather than
|
|
@@ -8634,6 +9490,7 @@ function cmdGuard(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
8634
9490
|
|
|
8635
9491
|
const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
|
|
8636
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));
|
|
8637
9494
|
for (const v of result.violations) write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
|
|
8638
9495
|
for (const n of result.notes ?? []) write(` [note] ${n}`); // information, never a verdict input (FN-7)
|
|
8639
9496
|
if (result.verdict === 'block' && forced) write(` → forced through: ${force} (logged to .dz/guard-audit.jsonl)`);
|
|
@@ -13058,6 +13915,160 @@ function readOptionalText(path: string): string {
|
|
|
13058
13915
|
}
|
|
13059
13916
|
}
|
|
13060
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
|
+
|
|
13061
14072
|
function deadwoodAllowlistText(): string {
|
|
13062
14073
|
const require = createRequire(import.meta.url);
|
|
13063
14074
|
const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
|
|
@@ -13143,7 +14154,7 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13143
14154
|
const json = flags.has('json');
|
|
13144
14155
|
if (flags.has('help')) {
|
|
13145
14156
|
write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
|
|
13146
|
-
write(' pool payoff
|
|
14157
|
+
write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
|
|
13147
14158
|
return 0;
|
|
13148
14159
|
}
|
|
13149
14160
|
for (const flag of flags) {
|
|
@@ -13175,40 +14186,24 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13175
14186
|
// apply-leg usage events (read records only; aggregate rows carry no query by construction)
|
|
13176
14187
|
const usage = readRecallUsageEvents(root);
|
|
13177
14188
|
|
|
13178
|
-
|
|
13179
|
-
const
|
|
13180
|
-
|
|
13181
|
-
const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
|
|
13182
|
-
for (const line of text.split('\n')) {
|
|
13183
|
-
if (line.trim() === '') continue;
|
|
13184
|
-
try {
|
|
13185
|
-
const o = JSON.parse(line) as { ts?: unknown; verdict?: unknown; violations?: { rule?: unknown }[] };
|
|
13186
|
-
if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
|
|
13187
|
-
guard.push({
|
|
13188
|
-
ts: o.ts,
|
|
13189
|
-
verdict: o.verdict,
|
|
13190
|
-
rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
|
|
13191
|
-
});
|
|
13192
|
-
}
|
|
13193
|
-
} catch {
|
|
13194
|
-
/* skip */
|
|
13195
|
-
}
|
|
13196
|
-
}
|
|
13197
|
-
} catch {
|
|
13198
|
-
/* no audit yet */
|
|
13199
|
-
}
|
|
14189
|
+
const promotionEvidence = readPromotionRunEvidence(root);
|
|
14190
|
+
const guardEvidence = readGuardAuditEvidence(root);
|
|
14191
|
+
const guard = [...guardEvidence.rows];
|
|
13200
14192
|
|
|
13201
14193
|
// The evidence logs themselves, verbatim: the report verifies their hash chains (feature
|
|
13202
14194
|
// event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
|
|
13203
14195
|
// "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
|
|
13204
14196
|
const evidenceLogs: { log: string; text: string }[] = [];
|
|
13205
|
-
for (const rel of ['.dz/recall-usage.jsonl'
|
|
14197
|
+
for (const rel of ['.dz/recall-usage.jsonl']) {
|
|
13206
14198
|
try {
|
|
13207
14199
|
evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
|
|
13208
14200
|
} catch {
|
|
13209
14201
|
/* absent log — reported by its own gate above, not invented here */
|
|
13210
14202
|
}
|
|
13211
14203
|
}
|
|
14204
|
+
if (guardEvidence.text !== null) {
|
|
14205
|
+
evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
|
|
14206
|
+
}
|
|
13212
14207
|
|
|
13213
14208
|
const now = new Date();
|
|
13214
14209
|
const report = assembleCompoundingReport({
|
|
@@ -13218,6 +14213,13 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
13218
14213
|
nowTs: now.toISOString(),
|
|
13219
14214
|
evidenceLogs,
|
|
13220
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
|
+
},
|
|
13221
14223
|
});
|
|
13222
14224
|
// lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
|
|
13223
14225
|
// dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
|
|
@@ -15110,11 +16112,20 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
15110
16112
|
case 'bundle':
|
|
15111
16113
|
return cmdBundle(options, flags, cwd, write);
|
|
15112
16114
|
case 'teach':
|
|
15113
|
-
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
|
+
);
|
|
15114
16125
|
case 'consolidate':
|
|
15115
16126
|
return await cmdConsolidate(options, flags, cwd, write);
|
|
15116
16127
|
case 'recall':
|
|
15117
|
-
return await cmdRecall(options, flags, cwd, write);
|
|
16128
|
+
return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
|
|
15118
16129
|
case 'vector':
|
|
15119
16130
|
return await cmdVector(options, flags, cwd, write);
|
|
15120
16131
|
case 'brain':
|
|
@@ -15167,6 +16178,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
15167
16178
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
15168
16179
|
case 'hooks-sync':
|
|
15169
16180
|
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
16181
|
+
case 'integrations-verify':
|
|
16182
|
+
return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
|
|
15170
16183
|
case 'agents-sync':
|
|
15171
16184
|
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
15172
16185
|
case 'sync-canonical':
|