@dzhechkov/harness-cli 0.4.5 → 0.5.0
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 +92 -16
- package/LICENSE +21 -0
- package/README.md +345 -18
- package/dist/bin.d.ts +10 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +45 -12
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +712 -170
- package/dist/cli.js.map +1 -1
- package/dist/core-compat.d.ts +106 -0
- package/dist/core-compat.d.ts.map +1 -0
- package/dist/core-compat.js +252 -0
- package/dist/core-compat.js.map +1 -0
- package/package.json +12 -12
- package/sbom.json +205 -15
- package/src/bin.ts +43 -12
- package/src/cli.ts +707 -146
- package/src/core-compat.ts +240 -0
package/src/cli.ts
CHANGED
|
@@ -15,8 +15,13 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import {
|
|
16
16
|
createSkill,
|
|
17
17
|
getSkillInfo,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
listSkillsDetailed,
|
|
19
|
+
formatSkillLoadFailures,
|
|
20
|
+
formatSkillApplyFailures,
|
|
21
|
+
resolveTargetName,
|
|
22
|
+
formatTargetProblem,
|
|
23
|
+
formatTargetAliasNote,
|
|
24
|
+
TARGET_NAMES_SORTED,
|
|
20
25
|
runDoctor,
|
|
21
26
|
runInit,
|
|
22
27
|
resolvePackageSkillRoots,
|
|
@@ -49,6 +54,11 @@ import {
|
|
|
49
54
|
runVerify,
|
|
50
55
|
runInitAgentsMd,
|
|
51
56
|
runInitGeminiMd,
|
|
57
|
+
runSyncAgentsPolicy,
|
|
58
|
+
runSyncCodexHooks,
|
|
59
|
+
POLICY_SOURCES,
|
|
60
|
+
detectPolicyDrift,
|
|
61
|
+
hasPolicyFence,
|
|
52
62
|
TARGET_NAMES,
|
|
53
63
|
buildParityMatrix,
|
|
54
64
|
TARGET_CAPABILITIES,
|
|
@@ -215,6 +225,7 @@ import {
|
|
|
215
225
|
buildChallengeBrief,
|
|
216
226
|
planDiscriminationCheck,
|
|
217
227
|
classifyDiscrimination,
|
|
228
|
+
classifyExecutionEvidence,
|
|
218
229
|
pickAdversaryModel,
|
|
219
230
|
CHALLENGE_QUESTIONS,
|
|
220
231
|
loadOutcomes,
|
|
@@ -242,6 +253,7 @@ import {
|
|
|
242
253
|
isUsablePlaneResult,
|
|
243
254
|
renderDeliveryReview,
|
|
244
255
|
scanSkillsLayout,
|
|
256
|
+
declaredPluginSurface,
|
|
245
257
|
parseInitFacts,
|
|
246
258
|
verifyRegistration,
|
|
247
259
|
buildContentProbePrompt,
|
|
@@ -342,7 +354,9 @@ import {
|
|
|
342
354
|
planImport,
|
|
343
355
|
} from '@dzhechkov/harness-core';
|
|
344
356
|
import type { MutationEntryResult, MutationObservation, MutationRegistryEntry } from '@dzhechkov/harness-core';
|
|
357
|
+
import type { SkillApplyFailure, SkillLoadFailure } from '@dzhechkov/harness-core';
|
|
345
358
|
import type { ReqeDebt } from '@dzhechkov/harness-core';
|
|
359
|
+
import type { ClassifyResultRow, ExecutionEvidence } from '@dzhechkov/harness-core';
|
|
346
360
|
import type { IdeaRecord, IdeaStatus } from '@dzhechkov/harness-core';
|
|
347
361
|
import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
|
|
348
362
|
import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
@@ -440,6 +454,8 @@ Usage:
|
|
|
440
454
|
(build-time: reconcile project grants vs installed skills' declared capabilities; dz never enforces — the host does)
|
|
441
455
|
dz sync-upstream [--package <dir>] [--list] [--all]
|
|
442
456
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
457
|
+
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)
|
|
458
|
+
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)
|
|
443
459
|
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)
|
|
444
460
|
dz plugin [--version <ver>]
|
|
445
461
|
dz downloads
|
|
@@ -449,6 +465,8 @@ Usage:
|
|
|
449
465
|
dz import-ecc [--local-path <dir>] [--select id,id,...] [--limit N] [--output <dir>] [--force]
|
|
450
466
|
dz help
|
|
451
467
|
|
|
468
|
+
Global: --version | -v [--json] (prints this CLI's own semver on one line, exit 0; "unknown" + exit 1 when unresolvable)
|
|
469
|
+
|
|
452
470
|
Workflows: author loop-plan/1 plans with dz workflow init/validate/render; gate them with dz workflow-lint; read runs with dz workflow-trace (the ADR-005 templates are retired)
|
|
453
471
|
|
|
454
472
|
Targets: ${TARGET_NAMES.join(', ')}
|
|
@@ -458,6 +476,18 @@ Presets: ${PRESET_NAMES.join(', ')}`;
|
|
|
458
476
|
export interface CliIo {
|
|
459
477
|
readonly cwd?: string;
|
|
460
478
|
readonly write?: (line: string) => void;
|
|
479
|
+
/**
|
|
480
|
+
* Diagnostics sink — **stderr**, defaulting to `console.error`.
|
|
481
|
+
*
|
|
482
|
+
* Before feature dz-cli-defects `CliIo` had no stderr seam at all, so every
|
|
483
|
+
* diagnostic (including the top-level error handler) landed on stdout and
|
|
484
|
+
* `dz list > skills.txt` wrote the error INTO the data file. `write` stays "data
|
|
485
|
+
* only"; `writeErr` is "diagnosis only".
|
|
486
|
+
*
|
|
487
|
+
* There is deliberately **no** fall-back to `write`: a test that wants to assert on
|
|
488
|
+
* stderr must inject `writeErr`, or the assertion would be theatre.
|
|
489
|
+
*/
|
|
490
|
+
readonly writeErr?: (line: string) => void;
|
|
461
491
|
/**
|
|
462
492
|
* Pre-read STDIN content (injectable so `dz brain ground`'s hook path is testable without
|
|
463
493
|
* an actual pipe). When omitted, the CLI reads fd 0 synchronously — but only for the one
|
|
@@ -528,6 +558,8 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
528
558
|
}
|
|
529
559
|
|
|
530
560
|
type Write = (line: string) => void;
|
|
561
|
+
/** Mirrors {@link Write}, but for the stderr seam (see {@link CliIo.writeErr}). */
|
|
562
|
+
type WriteErr = (line: string) => void;
|
|
531
563
|
|
|
532
564
|
/**
|
|
533
565
|
* Discover skill source directories: explicit `--skills-dir` if given, else
|
|
@@ -557,6 +589,14 @@ interface InstallSkillsResult {
|
|
|
557
589
|
readonly skipped: number;
|
|
558
590
|
/** Selected ids found in NO searched directory (empty when no `select` was given). */
|
|
559
591
|
readonly missing: string[];
|
|
592
|
+
/** Skill dirs that could not be LOADED, across every searched directory (D1). */
|
|
593
|
+
readonly failures: SkillLoadFailure[];
|
|
594
|
+
/**
|
|
595
|
+
* Skills that loaded but could not be COMPILED or WRITTEN (fix round 1, QE F4).
|
|
596
|
+
* A separate list because it accuses a different artifact — the target tree, not
|
|
597
|
+
* the source `SKILL.md`.
|
|
598
|
+
*/
|
|
599
|
+
readonly applyFailures: SkillApplyFailure[];
|
|
560
600
|
}
|
|
561
601
|
|
|
562
602
|
/**
|
|
@@ -594,10 +634,12 @@ async function installSkills(opts: {
|
|
|
594
634
|
let written = 0;
|
|
595
635
|
let skipped = 0;
|
|
596
636
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
597
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing] };
|
|
637
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
|
|
598
638
|
}
|
|
599
639
|
|
|
600
640
|
const results: { id: string; written: number; skipped: number }[] = [];
|
|
641
|
+
const failures: SkillLoadFailure[] = [];
|
|
642
|
+
const applyFailures: SkillApplyFailure[] = [];
|
|
601
643
|
for (const skillsDir of skillsDirs) {
|
|
602
644
|
const r = await runInit({
|
|
603
645
|
target,
|
|
@@ -610,6 +652,8 @@ async function installSkills(opts: {
|
|
|
610
652
|
for (const skill of r.skills) {
|
|
611
653
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
612
654
|
}
|
|
655
|
+
failures.push(...r.failures);
|
|
656
|
+
applyFailures.push(...r.applyFailures);
|
|
613
657
|
}
|
|
614
658
|
|
|
615
659
|
let written = 0;
|
|
@@ -617,7 +661,7 @@ async function installSkills(opts: {
|
|
|
617
661
|
for (const s of results) { written += s.written; skipped += s.skipped; }
|
|
618
662
|
const installed = new Set(results.map((s) => s.id));
|
|
619
663
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
620
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing };
|
|
664
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
|
|
621
665
|
}
|
|
622
666
|
|
|
623
667
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
@@ -633,12 +677,22 @@ function writeMissingSkillsHint(write: Write, missing: string[], presetName: str
|
|
|
633
677
|
}
|
|
634
678
|
}
|
|
635
679
|
|
|
636
|
-
async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
637
|
-
const
|
|
638
|
-
if (
|
|
639
|
-
|
|
680
|
+
async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
681
|
+
const targetOpt = options.get('target');
|
|
682
|
+
if (targetOpt === undefined) {
|
|
683
|
+
// A missing `--target` is the same accusation as an unresolvable one, so it takes
|
|
684
|
+
// the same channel: diagnostics on stderr, stdout stays a data channel (ADR-002
|
|
685
|
+
// §Decision 2 / driver D6; fix round 1, QE F2).
|
|
686
|
+
writeErr(`dz init: --target must be one of: ${TARGET_NAMES_SORTED.join(', ')}`);
|
|
640
687
|
return 1;
|
|
641
688
|
}
|
|
689
|
+
const resolution = resolveTargetName(targetOpt);
|
|
690
|
+
if (resolution.kind === 'unknown') {
|
|
691
|
+
for (const line of formatTargetProblem('dz init', resolution)) writeErr(line);
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
const target = resolution.target;
|
|
695
|
+
if (resolution.via === 'alias') writeErr(formatTargetAliasNote('dz init', targetOpt, target));
|
|
642
696
|
const explicitSkillsDir = options.get('skills-dir');
|
|
643
697
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
644
698
|
|
|
@@ -676,19 +730,43 @@ async function cmdInit(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
676
730
|
write(` (searched ${r.dirsSearched} skill directories)`);
|
|
677
731
|
}
|
|
678
732
|
writeMissingSkillsHint(write, r.missing, presetName);
|
|
733
|
+
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
734
|
+
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
735
|
+
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
736
|
+
// Counts first, then the named block — the same shape `dz list` uses. The block's
|
|
737
|
+
// own header already says "N skipped", so this line carries what it cannot: how many
|
|
738
|
+
// DID install, so a reader can tell a mostly-fine install from a mostly-broken one.
|
|
739
|
+
//
|
|
740
|
+
// The two failure kinds are counted and rendered SEPARATELY (fix round 1, QE F4):
|
|
741
|
+
// an unwritable target directory is not a broken pack, and printing it as one names
|
|
742
|
+
// the wrong file.
|
|
743
|
+
const parts = [`${r.results.length} installed`];
|
|
744
|
+
if (r.failures.length > 0) parts.push(`${r.failures.length} skipped`);
|
|
745
|
+
if (r.applyFailures.length > 0) parts.push(`${r.applyFailures.length} failed to write`);
|
|
746
|
+
writeErr(`dz init: ${parts.join(', ')}`);
|
|
747
|
+
for (const line of formatSkillLoadFailures(r.failures)) writeErr(line);
|
|
748
|
+
for (const line of formatSkillApplyFailures(r.applyFailures)) writeErr(line);
|
|
749
|
+
return 1;
|
|
750
|
+
}
|
|
679
751
|
return 0;
|
|
680
752
|
}
|
|
681
753
|
|
|
682
|
-
async function cmdVerify(options: Map<string, string>, cwd: string, write: Write): Promise<number> {
|
|
754
|
+
async function cmdVerify(options: Map<string, string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
683
755
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
684
756
|
const targetOpt = options.get('target');
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
757
|
+
let target: TargetName | undefined;
|
|
758
|
+
if (targetOpt !== undefined) {
|
|
759
|
+
const resolution = resolveTargetName(targetOpt);
|
|
760
|
+
if (resolution.kind === 'unknown') {
|
|
761
|
+
for (const line of formatTargetProblem('dz verify', resolution)) writeErr(line);
|
|
762
|
+
return 1;
|
|
763
|
+
}
|
|
764
|
+
target = resolution.target;
|
|
765
|
+
if (resolution.via === 'alias') writeErr(formatTargetAliasNote('dz verify', targetOpt, target));
|
|
688
766
|
}
|
|
689
767
|
const report = await runVerify({
|
|
690
768
|
skillsDir,
|
|
691
|
-
...(
|
|
769
|
+
...(target !== undefined ? { target } : {}),
|
|
692
770
|
});
|
|
693
771
|
write(`dz verify (${report.target}): ${report.valid}/${report.total} skill(s) valid`);
|
|
694
772
|
for (const skill of report.skills) {
|
|
@@ -697,7 +775,7 @@ async function cmdVerify(options: Map<string, string>, cwd: string, write: Write
|
|
|
697
775
|
return report.valid === report.total ? 0 : 1;
|
|
698
776
|
}
|
|
699
777
|
|
|
700
|
-
async function cmdSync(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
778
|
+
async function cmdSync(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
701
779
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
702
780
|
const canonicalArg = options.get('canonical');
|
|
703
781
|
|
|
@@ -724,6 +802,14 @@ async function cmdSync(options: Map<string, string>, flags: Set<string>, cwd: st
|
|
|
724
802
|
write(
|
|
725
803
|
`dz sync${report.dryRun ? ' --dry-run' : ''}: ${inSync}/${total} in sync, ${missing} missing, ${drift} drift`,
|
|
726
804
|
);
|
|
805
|
+
// Skip-and-collect (D1): the broken canonical skills are NAMED on stderr, and their
|
|
806
|
+
// presence keeps the exit code non-zero — a partial sync is not a clean sync.
|
|
807
|
+
if (report.failures.length > 0) {
|
|
808
|
+
// See `cmdInit` above: counts here, names in the block below.
|
|
809
|
+
writeErr(`dz sync: ${report.skills.length} compared, ${report.failures.length} skipped`);
|
|
810
|
+
for (const line of formatSkillLoadFailures(report.failures)) writeErr(line);
|
|
811
|
+
return 1;
|
|
812
|
+
}
|
|
727
813
|
return missing === 0 && drift === 0 ? 0 : 1;
|
|
728
814
|
}
|
|
729
815
|
|
|
@@ -770,19 +856,42 @@ function cmdCreateSkill(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
770
856
|
return 0;
|
|
771
857
|
}
|
|
772
858
|
|
|
773
|
-
|
|
859
|
+
/**
|
|
860
|
+
* `dz list` — skip-and-collect (feature dz-cli-defects, D1).
|
|
861
|
+
*
|
|
862
|
+
* One unparseable `SKILL.md` used to discard the ENTIRE listing with a message naming
|
|
863
|
+
* neither the file nor the count. Now the parseable skills list on stdout and the
|
|
864
|
+
* broken ones are named on stderr. The whole emit contract, in one place:
|
|
865
|
+
*
|
|
866
|
+
* | valid | skipped | stdout | stderr | exit |
|
|
867
|
+
* |-------|---------|--------|--------|------|
|
|
868
|
+
* | >0 | 0 | listing | *empty* | 0 |
|
|
869
|
+
* | >0 | >0 | listing of the valid ones | named summary | 1 |
|
|
870
|
+
* | 0 | >0 | *nothing* | named summary | 1 |
|
|
871
|
+
* | 0 | 0 | *nothing* | `no skills found in <dir>` | 1 |
|
|
872
|
+
*
|
|
873
|
+
* The last row is the ONE intentional departure from byte-identical output: that line
|
|
874
|
+
* used to go to stdout. Moving it keeps *stdout is data, stderr is diagnosis* whole —
|
|
875
|
+
* the invariant that makes `dz list > out.txt` trustworthy.
|
|
876
|
+
*/
|
|
877
|
+
function cmdList(options: Map<string, string>, cwd: string, write: Write, writeErr: WriteErr): number {
|
|
774
878
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
775
|
-
const skills =
|
|
776
|
-
if (skills.length === 0) {
|
|
777
|
-
|
|
879
|
+
const { skills, failures } = listSkillsDetailed(skillsDir);
|
|
880
|
+
if (skills.length === 0 && failures.length === 0) {
|
|
881
|
+
writeErr(`dz list: no skills found in ${skillsDir}`);
|
|
778
882
|
return 1;
|
|
779
883
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
884
|
+
if (skills.length > 0) {
|
|
885
|
+
write(`${skills.length} skill(s) in ${skillsDir}:\n`);
|
|
886
|
+
for (const skill of skills) {
|
|
887
|
+
const desc = skill.description.length > 80 ? skill.description.slice(0, 77) + '...' : skill.description;
|
|
888
|
+
write(` ${skill.id.padEnd(35)} ${desc}`);
|
|
889
|
+
}
|
|
784
890
|
}
|
|
785
|
-
return 0;
|
|
891
|
+
if (failures.length === 0) return 0;
|
|
892
|
+
writeErr(`dz list: ${skills.length} listed, ${failures.length} skipped in ${skillsDir}`);
|
|
893
|
+
for (const line of formatSkillLoadFailures(failures)) writeErr(line);
|
|
894
|
+
return 1;
|
|
786
895
|
}
|
|
787
896
|
|
|
788
897
|
function cmdInfo(options: Map<string, string>, args: ParsedArgs, cwd: string, write: Write): number {
|
|
@@ -935,8 +1044,8 @@ function workflowInitPlan(name: string, pattern: string): object {
|
|
|
935
1044
|
...base,
|
|
936
1045
|
steps: [
|
|
937
1046
|
{ stepId: 'fan', kind: 'fanout', phase: 'Work', concurrency: 'pipeline', budget: { maxAgents: 8 } },
|
|
938
|
-
{ stepId: 'a', kind: 'agent', phase: 'Work', prompt: 'TODO: stage A per item', budget: { maxAgents: 4 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
939
|
-
{ stepId: 'b', kind: 'agent', phase: 'Work', prompt: 'TODO: stage B per item', budget: { maxAgents: 4 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1047
|
+
{ stepId: 'a', kind: 'agent', phase: 'Work', prompt: 'TODO: stage A per item', budget: { maxAgents: 4 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1048
|
+
{ stepId: 'b', kind: 'agent', phase: 'Work', prompt: 'TODO: stage B per item', budget: { maxAgents: 4 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
940
1049
|
{ stepId: 'jn', kind: 'join', phase: 'Work', deps: ['fan'] },
|
|
941
1050
|
],
|
|
942
1051
|
fanouts: [{ stage: 'fan', registry: ['item1', 'item2', 'item3'], maxFanout: 3, chain: ['a', 'b'] }],
|
|
@@ -948,10 +1057,10 @@ function workflowInitPlan(name: string, pattern: string): object {
|
|
|
948
1057
|
...base,
|
|
949
1058
|
steps: [
|
|
950
1059
|
{ stepId: 'fan', kind: 'fanout', phase: 'Lanes', concurrency: 'barrier', budget: { maxAgents: 6 } },
|
|
951
|
-
{ stepId: 'lane', kind: 'agent', phase: 'Lanes', prompt: 'TODO: one lane', budget: { maxAgents: 6 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1060
|
+
{ stepId: 'lane', kind: 'agent', phase: 'Lanes', prompt: 'TODO: one lane', budget: { maxAgents: 6 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
952
1061
|
{ stepId: 'jn', kind: 'join', phase: 'Lanes', deps: ['fan'] },
|
|
953
1062
|
// the consumer hangs off the BARRIER (jn), never the fork — barrier-postdominates teaches this
|
|
954
|
-
{ stepId: 'synthesize', kind: 'agent', phase: 'Synthesize', deps: ['jn'], prompt: 'TODO: synthesize across lanes', budget: { maxAgents: 1 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1063
|
+
{ stepId: 'synthesize', kind: 'agent', phase: 'Synthesize', deps: ['jn'], prompt: 'TODO: synthesize across lanes', budget: { maxAgents: 1 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
955
1064
|
],
|
|
956
1065
|
fanouts: [{ stage: 'fan', registry: ['lane1', 'lane2', 'lane3'], maxFanout: 3, chain: ['lane'] }],
|
|
957
1066
|
joins: [{ stage: 'jn', forStage: 'fan', joinPolicy: 'all-activated', onInvalid: 'named-failure' }],
|
|
@@ -961,8 +1070,8 @@ function workflowInitPlan(name: string, pattern: string): object {
|
|
|
961
1070
|
return {
|
|
962
1071
|
...base,
|
|
963
1072
|
steps: [
|
|
964
|
-
{ stepId: 'work', kind: 'agent', phase: 'Work', prompt: 'TODO: produce the artifact', budget: { maxAgents: 2 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
965
|
-
{ stepId: 'gate', kind: 'gate', phase: 'Gate', deps: ['work'], prompt: 'TODO: gate check (parse the verdict, never synthesize one)', budget: { maxAgents: 1 } }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1073
|
+
{ stepId: 'work', kind: 'agent', phase: 'Work', prompt: 'TODO: produce the artifact', budget: { maxAgents: 2 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1074
|
+
{ stepId: 'gate', kind: 'gate', phase: 'Gate', deps: ['work'], prompt: 'TODO: gate check (parse the verdict, never synthesize one)', budget: { maxAgents: 1 }, tools: [] }, // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
966
1075
|
],
|
|
967
1076
|
gates: [{ stepId: 'gate', kind: 'parse-verdict', failRoute: 'work', maxRedos: 1 }],
|
|
968
1077
|
};
|
|
@@ -970,7 +1079,7 @@ function workflowInitPlan(name: string, pattern: string): object {
|
|
|
970
1079
|
// minimal default: one agent step
|
|
971
1080
|
return {
|
|
972
1081
|
...base,
|
|
973
|
-
steps: [{ stepId: 'main', kind: 'agent', phase: 'Work', prompt: 'TODO: the one step', budget: { maxAgents: 1 } }], // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
1082
|
+
steps: [{ stepId: 'main', kind: 'agent', phase: 'Work', prompt: 'TODO: the one step', budget: { maxAgents: 1 }, tools: [] }], // no-stubs: workflow-init scaffold sentinel the author replaces (deliberate authoring cue, not unfinished code)
|
|
974
1083
|
};
|
|
975
1084
|
}
|
|
976
1085
|
|
|
@@ -1161,8 +1270,12 @@ function cmdWorkflowLint(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
1161
1270
|
/** `dz workflow-trace` — timeline + invariant runner over a run's trace.jsonl. Scope is CAPPED
|
|
1162
1271
|
* (AM-8): <runDir|--slug|--run>, --invariants, --html, --json. NO watch/filter/compare/search/
|
|
1163
1272
|
* retention/access-control — adding one needs an ADR amendment (the surface test pins this). */
|
|
1164
|
-
/**
|
|
1165
|
-
|
|
1273
|
+
/**
|
|
1274
|
+
* This CLI's OWN semver, read from its package.json. `unknown` is honest; a throw is not, and an
|
|
1275
|
+
* invented number is worse than either — a downstream version guard that is handed a fabricated
|
|
1276
|
+
* version happily calls a binary it should have refused.
|
|
1277
|
+
*/
|
|
1278
|
+
function dzOwnVersion(): string {
|
|
1166
1279
|
try {
|
|
1167
1280
|
const req = createRequire(import.meta.url);
|
|
1168
1281
|
const pkg = req('../package.json') as { version?: unknown };
|
|
@@ -1170,6 +1283,11 @@ function traceBundleToolVersion(): string {
|
|
|
1170
1283
|
} catch { return 'unknown'; }
|
|
1171
1284
|
}
|
|
1172
1285
|
|
|
1286
|
+
/** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
|
|
1287
|
+
function traceBundleToolVersion(): string {
|
|
1288
|
+
return dzOwnVersion();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1173
1291
|
/** Resolve a run the SAME three ways the timeline reader does — positional, --slug, --run — so a
|
|
1174
1292
|
* bundle can never address a run by a scheme the rest of the command does not understand. */
|
|
1175
1293
|
function resolveTraceRun(options: Map<string, string>, cwd: string, positional: string | undefined):
|
|
@@ -1587,6 +1705,7 @@ async function cmdInstall(
|
|
|
1587
1705
|
flags: Set<string>,
|
|
1588
1706
|
cwd: string,
|
|
1589
1707
|
write: Write,
|
|
1708
|
+
writeErr: WriteErr,
|
|
1590
1709
|
installRunner?: (command: string, cwd: string) => void,
|
|
1591
1710
|
): Promise<number> {
|
|
1592
1711
|
const pkg = options.get('_positional_0');
|
|
@@ -1596,10 +1715,13 @@ async function cmdInstall(
|
|
|
1596
1715
|
}
|
|
1597
1716
|
|
|
1598
1717
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
1599
|
-
|
|
1600
|
-
|
|
1718
|
+
const targetResolution = resolveTargetName(targetOpt);
|
|
1719
|
+
if (targetResolution.kind === 'unknown') {
|
|
1720
|
+
for (const line of formatTargetProblem('dz install', targetResolution)) writeErr(line);
|
|
1601
1721
|
return 1;
|
|
1602
1722
|
}
|
|
1723
|
+
const target = targetResolution.target;
|
|
1724
|
+
if (targetResolution.via === 'alias') writeErr(formatTargetAliasNote('dz install', targetOpt, target));
|
|
1603
1725
|
|
|
1604
1726
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1605
1727
|
|
|
@@ -1635,7 +1757,7 @@ async function cmdInstall(
|
|
|
1635
1757
|
|
|
1636
1758
|
// Step 3: Use dz init with the resolved skills root as source
|
|
1637
1759
|
const report = await runInit({
|
|
1638
|
-
target
|
|
1760
|
+
target,
|
|
1639
1761
|
skillsDir: root.dir,
|
|
1640
1762
|
projectRoot,
|
|
1641
1763
|
force: flags.has('force'),
|
|
@@ -1654,21 +1776,36 @@ async function cmdInstall(
|
|
|
1654
1776
|
if (root.layout === 'npx-template' && root.hasCompanionAssets) {
|
|
1655
1777
|
write(` note: ${pkg} also ships commands/hooks/agents — \`npx -y ${pkg} init\` installs the full kit.`);
|
|
1656
1778
|
}
|
|
1779
|
+
// Skip-and-collect at install time (D1 / the report's D2 amendment): the offending
|
|
1780
|
+
// SKILL.md came out of the DOWNLOADED TARBALL, so the path is rendered relative to
|
|
1781
|
+
// the package root (a `node_modules/**` absolute path is not actionable) and the
|
|
1782
|
+
// message says whose defect it is. Exit 1 — a pack that shipped an unloadable skill
|
|
1783
|
+
// did not fully install.
|
|
1784
|
+
if (report.failures.length > 0) {
|
|
1785
|
+
writeErr(`dz install: ${pkg} ships ${report.failures.length} unparseable skill(s) —`);
|
|
1786
|
+
for (const line of formatSkillLoadFailures(report.failures, { relativeTo: pkgDir })) writeErr(line);
|
|
1787
|
+
writeErr('This is a defect in the package, not in your project.');
|
|
1788
|
+
writeErr(`Workaround: npx -y ${pkg} init`);
|
|
1789
|
+
return 1;
|
|
1790
|
+
}
|
|
1657
1791
|
return 0;
|
|
1658
1792
|
}
|
|
1659
1793
|
|
|
1660
|
-
function cmdCompose(options: Map<string, string>, cwd: string, write: Write): number {
|
|
1794
|
+
function cmdCompose(options: Map<string, string>, cwd: string, write: Write, writeErr: WriteErr): number {
|
|
1661
1795
|
const combo = options.get('_positional_0');
|
|
1662
1796
|
if (!combo) {
|
|
1663
1797
|
write('dz compose: preset combination required (e.g., dz compose devops+mcp+web3)');
|
|
1664
1798
|
return 1;
|
|
1665
1799
|
}
|
|
1666
1800
|
// --target is documented; honor it in the suggested install command (was hardcoded claude-code).
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1801
|
+
const targetOpt = options.get('target') ?? 'claude-code';
|
|
1802
|
+
const composeResolution = resolveTargetName(targetOpt);
|
|
1803
|
+
if (composeResolution.kind === 'unknown') {
|
|
1804
|
+
for (const line of formatTargetProblem('dz compose', composeResolution)) writeErr(line);
|
|
1670
1805
|
return 1;
|
|
1671
1806
|
}
|
|
1807
|
+
const target = composeResolution.target;
|
|
1808
|
+
if (composeResolution.via === 'alias') writeErr(formatTargetAliasNote('dz compose', targetOpt, target));
|
|
1672
1809
|
const presetNames = combo.split('+').map((s) => s.trim());
|
|
1673
1810
|
const allSkills = new Set<string>();
|
|
1674
1811
|
const resolved: string[] = [];
|
|
@@ -3885,12 +4022,19 @@ async function cmdBrain(
|
|
|
3885
4022
|
return sub === undefined ? 0 : 1;
|
|
3886
4023
|
}
|
|
3887
4024
|
|
|
3888
|
-
async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
|
|
3889
|
-
const
|
|
3890
|
-
if (!
|
|
3891
|
-
|
|
4025
|
+
async function cmdSetup(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): Promise<number> {
|
|
4026
|
+
const targetOpt = options.get('target');
|
|
4027
|
+
if (!targetOpt) {
|
|
4028
|
+
writeErr(`dz setup: --target required (${TARGET_NAMES_SORTED.join(', ')})`);
|
|
4029
|
+
return 1;
|
|
4030
|
+
}
|
|
4031
|
+
const setupResolution = resolveTargetName(targetOpt);
|
|
4032
|
+
if (setupResolution.kind === 'unknown') {
|
|
4033
|
+
for (const line of formatTargetProblem('dz setup', setupResolution)) writeErr(line);
|
|
3892
4034
|
return 1;
|
|
3893
4035
|
}
|
|
4036
|
+
const target = setupResolution.target;
|
|
4037
|
+
if (setupResolution.via === 'alias') writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
|
|
3894
4038
|
|
|
3895
4039
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3896
4040
|
const presetName = options.get('preset');
|
|
@@ -4077,12 +4221,17 @@ function cmdRecommend(options: Map<string, string>, cwd: string, write: Write):
|
|
|
4077
4221
|
return 0;
|
|
4078
4222
|
}
|
|
4079
4223
|
|
|
4080
|
-
function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
4224
|
+
function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): number {
|
|
4081
4225
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
4082
|
-
|
|
4083
|
-
|
|
4226
|
+
const upgradeResolution = resolveTargetName(targetOpt);
|
|
4227
|
+
if (upgradeResolution.kind === 'unknown') {
|
|
4228
|
+
for (const line of formatTargetProblem('dz upgrade', upgradeResolution)) writeErr(line);
|
|
4084
4229
|
return 1;
|
|
4085
4230
|
}
|
|
4231
|
+
// The dir map is keyed by the RESOLVED name — keying it by the raw `--target` would
|
|
4232
|
+
// let an alias validate and then miss the map.
|
|
4233
|
+
const upgradeTarget = upgradeResolution.target;
|
|
4234
|
+
if (upgradeResolution.via === 'alias') writeErr(formatTargetAliasNote('dz upgrade', targetOpt, upgradeTarget));
|
|
4086
4235
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4087
4236
|
const targetDirMap: Record<string, string> = {
|
|
4088
4237
|
'claude-code': '.claude/skills', codex: '.agents/skills', opencode: '.opencode/skills',
|
|
@@ -4090,9 +4239,9 @@ function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4090
4239
|
'agents-md': 'AGENTS.md', cursor: '.cursor/rules', gemini: 'GEMINI.md',
|
|
4091
4240
|
windsurf: '.windsurf/rules',
|
|
4092
4241
|
};
|
|
4093
|
-
const mappedDir = targetDirMap[
|
|
4242
|
+
const mappedDir = targetDirMap[upgradeTarget];
|
|
4094
4243
|
if (mappedDir === undefined) {
|
|
4095
|
-
write(`dz upgrade: no skills directory mapping for target ${
|
|
4244
|
+
write(`dz upgrade: no skills directory mapping for target ${upgradeTarget}`);
|
|
4096
4245
|
return 1;
|
|
4097
4246
|
}
|
|
4098
4247
|
const targetDir = join(projectRoot, mappedDir);
|
|
@@ -4110,7 +4259,7 @@ function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4110
4259
|
|
|
4111
4260
|
const report = checkUpgrades(targetDir, canonicalDirs);
|
|
4112
4261
|
|
|
4113
|
-
write(`\ndz upgrade — ${
|
|
4262
|
+
write(`\ndz upgrade — ${upgradeTarget} (${targetDir})`);
|
|
4114
4263
|
write(` Installed: ${report.installed} Needs update: ${report.needsUpdate} Up-to-date: ${report.upToDate} Custom: ${report.notInCanonical}\n`);
|
|
4115
4264
|
|
|
4116
4265
|
for (const check of report.skills) {
|
|
@@ -4119,7 +4268,7 @@ function cmdUpgrade(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4119
4268
|
}
|
|
4120
4269
|
|
|
4121
4270
|
if (report.needsUpdate > 0) {
|
|
4122
|
-
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${
|
|
4271
|
+
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${upgradeTarget} --force`);
|
|
4123
4272
|
}
|
|
4124
4273
|
// ADR-001 (verify-apply-leg): verify what we just left on disk. A TAMPERED pack aborts.
|
|
4125
4274
|
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
@@ -4663,7 +4812,7 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4663
4812
|
/* ADR-001): computed from the declarative model, never hand-written */
|
|
4664
4813
|
/* ------------------------------------------------------------------ */
|
|
4665
4814
|
|
|
4666
|
-
function cmdParity(options: Map<string, string>, flags: Set<string>, write: Write): number {
|
|
4815
|
+
function cmdParity(options: Map<string, string>, flags: Set<string>, write: Write, writeErr: WriteErr): number {
|
|
4667
4816
|
const json = flags.has('json');
|
|
4668
4817
|
if (flags.has('help')) {
|
|
4669
4818
|
write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
|
|
@@ -4692,27 +4841,44 @@ function cmdParity(options: Map<string, string>, flags: Set<string>, write: Writ
|
|
|
4692
4841
|
}
|
|
4693
4842
|
|
|
4694
4843
|
const matrix = buildParityMatrix();
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4844
|
+
// Site 8 of the D3 rewiring, closed in fix round 1 (QE F1). It shipped spelling its
|
|
4845
|
+
// own bare guard `TARGET_NAMES.includes(...)` and was therefore invisible to the AM-2
|
|
4846
|
+
// grep-guard, which searched for the token `isTargetName(` — a PRESENCE check on one
|
|
4847
|
+
// spelling where the property was "no call site bypasses the resolver". The guard in
|
|
4848
|
+
// `test/target-alias-cli.test.ts` now checks the class, and the sweep list is derived
|
|
4849
|
+
// from the help text so a ninth command cannot be missed the same way.
|
|
4850
|
+
const targetOpt = options.get('target');
|
|
4851
|
+
let target: TargetName | undefined;
|
|
4852
|
+
if (targetOpt !== undefined) {
|
|
4853
|
+
const parityResolution = resolveTargetName(targetOpt);
|
|
4854
|
+
if (parityResolution.kind === 'unknown') {
|
|
4855
|
+
// Both forms go to stderr: an error is not data, and `dz parity --json | jq`
|
|
4856
|
+
// must not be fed a diagnostic (ADR-002 §Decision 2 / driver D6).
|
|
4857
|
+
if (json) {
|
|
4858
|
+
writeErr(JSON.stringify({ error: `unknown target ${JSON.stringify(targetOpt)}`, suggestion: parityResolution.suggestion, targets: TARGET_NAMES_SORTED, exitCode: 1 }, null, 2));
|
|
4859
|
+
} else {
|
|
4860
|
+
for (const line of formatTargetProblem('dz parity', parityResolution)) writeErr(line);
|
|
4861
|
+
}
|
|
4862
|
+
return 1;
|
|
4863
|
+
}
|
|
4864
|
+
target = parityResolution.target;
|
|
4865
|
+
if (parityResolution.via === 'alias') writeErr(formatTargetAliasNote('dz parity', targetOpt, target));
|
|
4700
4866
|
}
|
|
4701
4867
|
|
|
4702
4868
|
if (json) {
|
|
4703
4869
|
const rows = matrix.map((r) => ({
|
|
4704
4870
|
id: r.feature.id,
|
|
4705
4871
|
title: r.feature.title,
|
|
4706
|
-
cells: target !== undefined ? { [target]: r.cells[target
|
|
4872
|
+
cells: target !== undefined ? { [target]: r.cells[target] } : r.cells,
|
|
4707
4873
|
}));
|
|
4708
4874
|
// A filtered response stays internally consistent: capabilities are filtered too (Codex QE gap 9).
|
|
4709
|
-
const caps = target !== undefined ? { [target]: TARGET_CAPABILITIES[target
|
|
4875
|
+
const caps = target !== undefined ? { [target]: TARGET_CAPABILITIES[target] } : TARGET_CAPABILITIES;
|
|
4710
4876
|
write(JSON.stringify({ targets: target !== undefined ? [target] : TARGET_NAMES, capabilities: caps, features: rows }, null, 2));
|
|
4711
4877
|
return 0;
|
|
4712
4878
|
}
|
|
4713
4879
|
|
|
4714
4880
|
if (target !== undefined) {
|
|
4715
|
-
const t = target
|
|
4881
|
+
const t = target;
|
|
4716
4882
|
write(`\ndz parity — ${t} (capabilities: ${TARGET_CAPABILITIES[t].join(', ')})\n`);
|
|
4717
4883
|
for (const r of matrix) {
|
|
4718
4884
|
const c = r.cells[t];
|
|
@@ -5678,6 +5844,179 @@ function readDriftAllowlist(root: string): string[] {
|
|
|
5678
5844
|
}
|
|
5679
5845
|
}
|
|
5680
5846
|
|
|
5847
|
+
/** Refresh or verify the root AGENTS.md bearing-policy projection. */
|
|
5848
|
+
/**
|
|
5849
|
+
* `dz hooks-sync --target codex` (`crossrt-2-codex-hooks`, AM-14).
|
|
5850
|
+
*
|
|
5851
|
+
* ONE verb in the existing target vocabulary (`parity`, `delivery-check`, `--target`), extensible to
|
|
5852
|
+
* a future runtime without a third surface. **No alias** — `dz codex-hooks` resolves to nothing.
|
|
5853
|
+
*
|
|
5854
|
+
* Exit map (ADR-002 §5, pinned by test):
|
|
5855
|
+
* 0 = `armed` AND `trust: 'trusted'` — the ONLY outcome that may print a success word (AM-17)
|
|
5856
|
+
* 1 = not armed, armed-but-trust-pending, drift, or a refusal
|
|
5857
|
+
* 3 = inconclusive (including "no codex binary on PATH")
|
|
5858
|
+
*/
|
|
5859
|
+
function cmdHooksSync(
|
|
5860
|
+
options: Map<string, string>,
|
|
5861
|
+
flags: Set<string>,
|
|
5862
|
+
cwd: string,
|
|
5863
|
+
write: Write,
|
|
5864
|
+
writeErr: WriteErr,
|
|
5865
|
+
): number {
|
|
5866
|
+
const json = flags.has('json');
|
|
5867
|
+
const usage = 'dz hooks-sync --target codex [--check] [--verify] [--remove] [--json] [--project <dir>] [--no-verify]';
|
|
5868
|
+
if (flags.has('help')) {
|
|
5869
|
+
write(`${usage} — install/verify the dz veto + recall hooks in $CODEX_HOME/hooks.json`);
|
|
5870
|
+
return 0;
|
|
5871
|
+
}
|
|
5872
|
+
for (const flag of flags) {
|
|
5873
|
+
if (!['check', 'verify', 'no-verify', 'remove', 'json', 'help'].includes(flag)) {
|
|
5874
|
+
const message = `dz hooks-sync: unknown option --${flag}\n${usage}`;
|
|
5875
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : message);
|
|
5876
|
+
return 1;
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
for (const key of options.keys()) {
|
|
5880
|
+
if (key !== 'target' && key !== 'project' && key !== 'codex-home') {
|
|
5881
|
+
const message = key.startsWith('_positional_') ? `unexpected argument ${JSON.stringify(options.get(key))}` : `unknown option --${key}`;
|
|
5882
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5883
|
+
return 1;
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
// Every `--target` read in this CLI goes through resolveTargetName (alias support + one spelling
|
|
5887
|
+
// of the unknown-target message), pinned by `everyTargetGuardUsesResolveTargetName`.
|
|
5888
|
+
const targetOpt = options.get('target');
|
|
5889
|
+
if (targetOpt === undefined) {
|
|
5890
|
+
const message = '--target is required';
|
|
5891
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5892
|
+
return 1;
|
|
5893
|
+
}
|
|
5894
|
+
const resolution = resolveTargetName(targetOpt);
|
|
5895
|
+
if (resolution.kind === 'unknown') {
|
|
5896
|
+
if (json) {
|
|
5897
|
+
write(JSON.stringify({ error: `unknown target ${targetOpt}`, exitCode: 1 }));
|
|
5898
|
+
} else {
|
|
5899
|
+
for (const line of formatTargetProblem('dz hooks-sync', resolution)) writeErr(line);
|
|
5900
|
+
}
|
|
5901
|
+
return 1;
|
|
5902
|
+
}
|
|
5903
|
+
const target = resolution.target;
|
|
5904
|
+
if (resolution.via === 'alias') writeErr(formatTargetAliasNote('dz hooks-sync', targetOpt, target));
|
|
5905
|
+
if (target !== 'codex') {
|
|
5906
|
+
// Deliberately narrow: only Codex has a hook carrier today. Naming the reason keeps a future
|
|
5907
|
+
// reader from assuming the other nine are simply unimplemented here.
|
|
5908
|
+
const message = `unsupported --target ${target} (only "codex" has a hook carrier today)`;
|
|
5909
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5910
|
+
return 1;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
const codexHome = options.get('codex-home');
|
|
5914
|
+
const report = runSyncCodexHooks({
|
|
5915
|
+
...(codexHome !== undefined ? { codexHome } : {}),
|
|
5916
|
+
check: flags.has('check'),
|
|
5917
|
+
remove: flags.has('remove'),
|
|
5918
|
+
});
|
|
5919
|
+
|
|
5920
|
+
if (json) {
|
|
5921
|
+
write(JSON.stringify({ ...report, exitCode: report.exitCode }));
|
|
5922
|
+
return report.exitCode;
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
for (const err of report.errors) writeErr(`dz hooks-sync: ${err}`);
|
|
5926
|
+
for (const warn of report.warnings) writeErr(`dz hooks-sync: warning: ${warn}`);
|
|
5927
|
+
|
|
5928
|
+
// SILENT in a home that never opted in — the leg-1 F12 lesson: a --check that chatters in every
|
|
5929
|
+
// unrelated project trains its reader to ignore it.
|
|
5930
|
+
if (flags.has('check') && !report.installed && report.errors.length === 0) return report.exitCode;
|
|
5931
|
+
|
|
5932
|
+
if (flags.has('remove')) {
|
|
5933
|
+
write(`dz hooks-sync: removed ${report.removed} managed entr(ies) from ${report.registryPath}`);
|
|
5934
|
+
return report.exitCode;
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
// AM-17 / G-G: the success word is reachable ONLY from armed AND trusted. The trust clause is
|
|
5938
|
+
// asserted HERE as well as in the exit map — a read-only `--check` that could not establish trust
|
|
5939
|
+
// once returned exit 0 with `trust: 'unknown'`, and this line printed "ready" for it.
|
|
5940
|
+
if (report.exitCode === 0 && report.trust === 'trusted' && report.installed) {
|
|
5941
|
+
write(`dz hooks-sync: codex hooks installed and ARMED (trust: ${report.trust}) — ready`);
|
|
5942
|
+
} else if (report.installed) {
|
|
5943
|
+
writeErr(`dz hooks-sync: installed, NOT verified — ARMED = NO (trust: ${report.trust}, executable: ${report.executable})`);
|
|
5944
|
+
writeErr('→ open an interactive Codex session in this directory and approve the two dz hooks, then re-run with --verify');
|
|
5945
|
+
} else {
|
|
5946
|
+
writeErr('dz hooks-sync: ARMED = NO — the managed entries are not present in the registry');
|
|
5947
|
+
}
|
|
5948
|
+
return report.exitCode;
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5951
|
+
function cmdAgentsSync(
|
|
5952
|
+
options: Map<string, string>,
|
|
5953
|
+
flags: Set<string>,
|
|
5954
|
+
cwd: string,
|
|
5955
|
+
write: Write,
|
|
5956
|
+
writeErr: WriteErr,
|
|
5957
|
+
): number {
|
|
5958
|
+
const json = flags.has('json');
|
|
5959
|
+
const usage = 'dz agents-sync [--project <dir>] [--check] [--json]';
|
|
5960
|
+
if (flags.has('help')) {
|
|
5961
|
+
write(`${usage} — sync/verify the dz:policies fence in root AGENTS.md`);
|
|
5962
|
+
return 0;
|
|
5963
|
+
}
|
|
5964
|
+
for (const flag of flags) {
|
|
5965
|
+
if (!['check', 'json', 'help'].includes(flag)) {
|
|
5966
|
+
const message = `dz agents-sync: unknown option --${flag}\n${usage}`;
|
|
5967
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : message);
|
|
5968
|
+
return 1;
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
for (const key of options.keys()) {
|
|
5972
|
+
if (key !== 'project') {
|
|
5973
|
+
const message = key.startsWith('_positional_') ? `unexpected argument ${JSON.stringify(options.get(key))}` : `unknown option --${key}`;
|
|
5974
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz agents-sync: ${message}\n${usage}`);
|
|
5975
|
+
return 1;
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5979
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
5980
|
+
try {
|
|
5981
|
+
const report = runSyncAgentsPolicy({ projectRoot: root, check: flags.has('check') });
|
|
5982
|
+
const drifted = report.drift.filter((finding) => finding.status !== 'ok');
|
|
5983
|
+
const inconclusive = report.missing.length > 0;
|
|
5984
|
+
const failed = flags.has('check')
|
|
5985
|
+
? report.changed || report.budget.overflow || drifted.length > 0
|
|
5986
|
+
: !report.inSync;
|
|
5987
|
+
const exitCode = inconclusive ? 3 : failed ? 1 : 0;
|
|
5988
|
+
if (json) {
|
|
5989
|
+
write(JSON.stringify({ ...report, sections: report.blocks, exitCode }));
|
|
5990
|
+
return exitCode;
|
|
5991
|
+
}
|
|
5992
|
+
if (inconclusive) {
|
|
5993
|
+
writeErr(`dz agents-sync: INCONCLUSIVE — unreadable or unanchored policy source(s): ${report.missing.join(', ')}`);
|
|
5994
|
+
writeErr('→ heal with: restore the named source anchors, then run dz agents-sync');
|
|
5995
|
+
return 3;
|
|
5996
|
+
}
|
|
5997
|
+
if (failed) {
|
|
5998
|
+
const effect = flags.has('check') ? 'AGENTS.md would change' : 'AGENTS.md was not rewritten';
|
|
5999
|
+
writeErr(`dz agents-sync: DRIFT — ${drifted.length} stale/missing section(s); ${effect}`);
|
|
6000
|
+
for (const finding of drifted) writeErr(` ${finding.id}: ${finding.file} (${finding.status})`);
|
|
6001
|
+
if (drifted.some((finding) => finding.id === 'dz:policies')) {
|
|
6002
|
+
writeErr('→ heal with: repair duplicate/unmatched dz:policies markers, then run dz agents-sync');
|
|
6003
|
+
} else {
|
|
6004
|
+
writeErr('→ heal with: dz agents-sync');
|
|
6005
|
+
}
|
|
6006
|
+
return 1;
|
|
6007
|
+
}
|
|
6008
|
+
const verb = report.written ? 'wrote' : 'in sync';
|
|
6009
|
+
write(`dz agents-sync: ${verb} — ${report.blocks.length} policy section(s), ${report.budget.bytes} bytes (${report.budget.pct}% of ${report.budget.cap})`);
|
|
6010
|
+
for (const warning of report.warnings) writeErr(`dz agents-sync: warning: ${warning}`);
|
|
6011
|
+
return 0;
|
|
6012
|
+
} catch (error) {
|
|
6013
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6014
|
+
if (json) write(JSON.stringify({ error: message, exitCode: 1 }));
|
|
6015
|
+
else writeErr(`dz agents-sync: ${message}`);
|
|
6016
|
+
return 1;
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
|
|
5681
6020
|
function cmdDriftCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
5682
6021
|
const root = resolve(cwd, options.get('project') ?? '.');
|
|
5683
6022
|
// Default scope = PUBLISHED packages only: the `.claude/skills` dogfood copies legitimately lag the
|
|
@@ -5791,6 +6130,31 @@ function gatherReadmeCounts(root: string): { label: string; a: number; b: number
|
|
|
5791
6130
|
function gatherGuardFacts(op: string, root: string, text: string | undefined, storeCap: number): Record<string, unknown> {
|
|
5792
6131
|
const facts: Record<string, unknown> = { op };
|
|
5793
6132
|
if (op === 'publish') {
|
|
6133
|
+
// agents-md-policy-sync: fixed registry, no tree walk. The pure detector
|
|
6134
|
+
// recomputes every expected hash from current source text; this gatherer
|
|
6135
|
+
// only supplies bytes. Any unexpected gather failure omits the fact, and
|
|
6136
|
+
// evaluateGuard records that advisory coverage gap in `notes`.
|
|
6137
|
+
try {
|
|
6138
|
+
const policyFiles = new Map<string, string | null>();
|
|
6139
|
+
for (const file of new Set(POLICY_SOURCES.map((source) => source.file))) {
|
|
6140
|
+
try { policyFiles.set(file, readFileSync(join(root, file), 'utf8')); }
|
|
6141
|
+
catch { policyFiles.set(file, null); }
|
|
6142
|
+
}
|
|
6143
|
+
let agentsMd: string | null = null;
|
|
6144
|
+
try { agentsMd = readFileSync(join(root, 'AGENTS.md'), 'utf8'); } catch { /* missing stamp evidence */ }
|
|
6145
|
+
const policyDrift = detectPolicyDrift(policyFiles, agentsMd, POLICY_SOURCES);
|
|
6146
|
+
facts['policyDrift'] = {
|
|
6147
|
+
applicable: policyDrift.applicable,
|
|
6148
|
+
// Did this repo OPT IN? A `dz:policies` fence in AGENTS.md is the only durable signal that
|
|
6149
|
+
// someone ran `dz agents-sync` here. Without it the advisory rule is out of scope and stays
|
|
6150
|
+
// silent; with it, unreadable sources become a loud note instead of a silent skip.
|
|
6151
|
+
fenced: hasPolicyFence(agentsMd),
|
|
6152
|
+
drifted: policyDrift.findings
|
|
6153
|
+
.filter((finding) => finding.status !== 'ok')
|
|
6154
|
+
.map((finding) => `${finding.id}:${finding.file}:${finding.status}`),
|
|
6155
|
+
};
|
|
6156
|
+
} catch { /* unexpected gather failure — omission becomes a visible guard note */ }
|
|
6157
|
+
|
|
5794
6158
|
// Read every workspace manifest ONCE: build a name→version map, then resolve each `workspace:*` dep to the
|
|
5795
6159
|
// version pnpm WOULD publish it as. In a pnpm workspace (pnpm-workspace.yaml present) `workspace:*` in source
|
|
5796
6160
|
// is correct and gets rewritten at publish — so reporting it raw would be a FALSE gate. We mirror the rewrite:
|
|
@@ -6825,7 +7189,7 @@ async function cmdRetro(options: Map<string, string>, flags: Set<string>, cwd: s
|
|
|
6825
7189
|
* --from-spec <spec.json> preview the scaffold (create / augment per file); the SKILL fills the spec
|
|
6826
7190
|
* --apply with --from-spec: WRITE (create missing, AUGMENT existing — never clobber)
|
|
6827
7191
|
*/
|
|
6828
|
-
function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
7192
|
+
function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, writeErr: WriteErr): number {
|
|
6829
7193
|
let repoRoot = cwd;
|
|
6830
7194
|
try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; } catch { /* not git */ }
|
|
6831
7195
|
|
|
@@ -6844,11 +7208,24 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
|
|
|
6844
7208
|
// Its "runnable here" list is computed for --target (default agents-md, the AGENTS.md-class target class).
|
|
6845
7209
|
const wantGates = flags.has('gates');
|
|
6846
7210
|
const targetOpt = options.get('target');
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7211
|
+
// Sites 7 AND 8 of the D3 rewiring. Site 8 (the coercion below) is NOT a guard — it
|
|
7212
|
+
// is a silent fallback, and a mechanical "replace isTargetName with resolveTargetName"
|
|
7213
|
+
// pass would miss it. With aliasing in place and the coercion left alone,
|
|
7214
|
+
// `dz feature-adr-setup --gates --target claude` would pass validation and then emit
|
|
7215
|
+
// for **agents-md**. So the coercion CONSUMES the resolution computed once, above;
|
|
7216
|
+
// `agents-md` is the default only when `--target` is ABSENT.
|
|
7217
|
+
let gatesTarget: TargetName = 'agents-md';
|
|
7218
|
+
if (targetOpt !== undefined) {
|
|
7219
|
+
const gatesResolution = resolveTargetName(targetOpt);
|
|
7220
|
+
if (gatesResolution.kind === 'unknown') {
|
|
7221
|
+
for (const line of formatTargetProblem('dz feature-adr-setup', gatesResolution)) writeErr(line);
|
|
7222
|
+
return 1;
|
|
7223
|
+
}
|
|
7224
|
+
gatesTarget = gatesResolution.target;
|
|
7225
|
+
if (gatesResolution.via === 'alias') {
|
|
7226
|
+
writeErr(formatTargetAliasNote('dz feature-adr-setup', targetOpt, gatesTarget));
|
|
7227
|
+
}
|
|
6850
7228
|
}
|
|
6851
|
-
const gatesTarget: TargetName = isTargetName(targetOpt ?? '') ? (targetOpt as TargetName) : 'agents-md';
|
|
6852
7229
|
|
|
6853
7230
|
const specPath = options.get('from-spec');
|
|
6854
7231
|
if (specPath === undefined && !wantGuards && !wantGates) {
|
|
@@ -6964,7 +7341,15 @@ function cmdChallenge(options: Map<string, string>, flags: Set<string>, cwd: str
|
|
|
6964
7341
|
* --base <ref> the base ref to fail against (default HEAD)
|
|
6965
7342
|
* --name '<filter>' optional -t test-name filter applied to every target
|
|
6966
7343
|
* --runner '<cmd>' test runner (default `npx vitest run`)
|
|
6967
|
-
* --
|
|
7344
|
+
* --timeout <ms> per-run timeout (default 300000; a timed-out run is CANNOT_ISOLATE)
|
|
7345
|
+
* --json machine-readable {plan, results, tipTree, perTest, aggregate,
|
|
7346
|
+
* findings, measurementValid, primaryAction}
|
|
7347
|
+
*
|
|
7348
|
+
* This executor is THIN by design (house style: pure classifier + thin executor). It performs exactly the
|
|
7349
|
+
* I/O the pure gate cannot — stat, worktree, run, capture — and hands OBSERVATIONS back. It no longer
|
|
7350
|
+
* interprets anything: the pre-epoch load-error regex that lived here (`/cannot find module|failed to
|
|
7351
|
+
* load|.../i`) is DELETED, because a regex over a runner's stderr, written in the executor, is exactly the
|
|
7352
|
+
* probabilistic channel that minted `DISCRIMINATES` for `--runner false`.
|
|
6968
7353
|
*
|
|
6969
7354
|
* NEVER auto-aborts: a non-discriminating (false-green) test is reported as a HIGH finding for the owner to
|
|
6970
7355
|
* decide (dz's rule — a false gate kills trust). Exit code is 0 on a clean run regardless of verdict; 2 only on
|
|
@@ -6984,6 +7369,9 @@ function cmdDiscriminationCheck(options: Map<string, string>, flags: Set<string>
|
|
|
6984
7369
|
nameFilter !== undefined && nameFilter.trim() !== '' ? { file, name: nameFilter.trim() } : { file });
|
|
6985
7370
|
const baseRef = options.get('base') ?? 'HEAD';
|
|
6986
7371
|
const runnerOpt = options.get('runner');
|
|
7372
|
+
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
7373
|
+
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
7374
|
+
const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
|
|
6987
7375
|
|
|
6988
7376
|
const plan = planDiscriminationCheck(runnerOpt !== undefined ? { baseRef, propertyTests, runner: runnerOpt } : { baseRef, propertyTests });
|
|
6989
7377
|
|
|
@@ -6997,82 +7385,186 @@ function cmdDiscriminationCheck(options: Map<string, string>, flags: Set<string>
|
|
|
6997
7385
|
return 0;
|
|
6998
7386
|
}
|
|
6999
7387
|
|
|
7000
|
-
//
|
|
7001
|
-
//
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7388
|
+
// ── (1) stat + isFile, BEFORE the worktree (AM-6 / FR-A1) ──────────────────────────────────
|
|
7389
|
+
// R12: `stat` FOLLOWS symlinks on purpose. A dangling symlink lstat-exists but has no readable
|
|
7390
|
+
// content — that IS absence of the named check. A directory stat-exists but is not a regular
|
|
7391
|
+
// file. Pre-epoch both reached the copy step, threw, and were caught into `outcome:'error'`,
|
|
7392
|
+
// which minted the near-pass DISCRIMINATES_VIA_ERROR (MEASURED — acid A1 / the dangling-symlink
|
|
7393
|
+
// and directory rows of features/wave1-instrument-repair/07_code_changes/acid-red-runs.md).
|
|
7394
|
+
const absent: { file: string; name?: string; outcome: 'absent'; evidence?: ExecutionEvidence }[] = [];
|
|
7395
|
+
const present: { file: string; name?: string }[] = [];
|
|
7396
|
+
for (const t of plan.targets) {
|
|
7397
|
+
let isRegular = false;
|
|
7398
|
+
let isDirectory = false;
|
|
7399
|
+
try {
|
|
7400
|
+
const st = statSync(resolve(repoRoot, t.file));
|
|
7401
|
+
isRegular = st.isFile();
|
|
7402
|
+
isDirectory = st.isDirectory();
|
|
7403
|
+
} catch { /* ENOENT / dangling symlink / permission — absence, either way */ }
|
|
7404
|
+
if (isRegular) { present.push(t.name !== undefined ? { file: t.file, name: t.name } : { file: t.file }); continue; }
|
|
7405
|
+
const row = t.name !== undefined ? { file: t.file, name: t.name, outcome: 'absent' as const } : { file: t.file, outcome: 'absent' as const };
|
|
7406
|
+
// Out-of-band detail channel: TEST_FILE_ABSENT is evidence-EXEMPT (its evidence is the stat
|
|
7407
|
+
// itself), and absent rows never consult the evidence gate — so this object cannot degrade the
|
|
7408
|
+
// row. It exists only so the finding can tell the operator WHY the path is not a test file.
|
|
7409
|
+
absent.push(isDirectory
|
|
7410
|
+
? { ...row, evidence: { exitCode: null, runner: 'unrecognised', failureKind: 'unrecognised', testsExecuted: null, targetSeen: false, evidenceLine: 'not-a-regular-file' } }
|
|
7411
|
+
: row);
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
const results: ClassifyResultRow[] = [...absent];
|
|
7415
|
+
let tipTree: { headSha: string; dirtyFiles: number } | null = null;
|
|
7416
|
+
|
|
7417
|
+
// Confirmation 12: with nothing present there is nothing to run — no worktree is built at all.
|
|
7418
|
+
if (present.length > 0) {
|
|
7419
|
+
const runner = runnerOpt !== undefined && plan.commands.some((c) => c.includes(runnerOpt)) ? runnerOpt : 'npx vitest run';
|
|
7420
|
+
// Execute the plan in a temp worktree WE own; substitute {{WORKTREE}} and always clean up.
|
|
7421
|
+
// `git worktree add` must CREATE the path, so compute a fresh non-existent one (do NOT mkdtemp it).
|
|
7422
|
+
const worktree = join(mkdtempSync(join(tmpdir(), 'dz-disc-')), 'wt');
|
|
7423
|
+
try {
|
|
7424
|
+
// 1) add the detached worktree at base (git creates `worktree`; its parent already exists).
|
|
7425
|
+
const addCmd = plan.commands[0]!.replace(/\{\{WORKTREE\}\}/g, worktree);
|
|
7426
|
+
execSync(addCmd, { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
7427
|
+
|
|
7428
|
+
// 1b) a fresh worktree has NO node_modules — without this, every test fails to load (runner + deps
|
|
7429
|
+
// unresolvable) and the gate collapses to always-VIA_ERROR, blind to false greens. Absolute-path
|
|
7430
|
+
// symlinks point back at the main checkout's already-installed trees, robust across pnpm's layout.
|
|
7431
|
+
const linkNodeModules = (relDir: string): void => {
|
|
7432
|
+
const srcNm = join(repoRoot, relDir, 'node_modules');
|
|
7433
|
+
if (!existsSync(srcNm)) return;
|
|
7434
|
+
const dstNm = join(worktree, relDir, 'node_modules');
|
|
7435
|
+
if (existsSync(dstNm)) return;
|
|
7436
|
+
try { mkdirSync(dirname(dstNm), { recursive: true }); symlinkSync(srcNm, dstNm, 'dir'); } catch { /* best effort */ }
|
|
7437
|
+
};
|
|
7438
|
+
linkNodeModules('.'); // root (hoisted deps + .bin)
|
|
7439
|
+
const pkgDirs = new Set<string>();
|
|
7440
|
+
for (const t of present) {
|
|
7441
|
+
let d = dirname(t.file);
|
|
7442
|
+
while (d && d !== '.' && d !== sep) {
|
|
7443
|
+
if (existsSync(join(repoRoot, d, 'package.json'))) { pkgDirs.add(d); break; }
|
|
7444
|
+
d = dirname(d);
|
|
7445
|
+
}
|
|
7026
7446
|
}
|
|
7027
|
-
|
|
7028
|
-
for (const d of pkgDirs) linkNodeModules(d);
|
|
7447
|
+
for (const d of pkgDirs) linkNodeModules(d);
|
|
7029
7448
|
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
const
|
|
7052
|
-
const
|
|
7053
|
-
|
|
7449
|
+
// 2) copy each property test into the base worktree, then 3) run it and record the OBSERVATION.
|
|
7450
|
+
for (const t of present) {
|
|
7451
|
+
try {
|
|
7452
|
+
const src = resolve(repoRoot, t.file);
|
|
7453
|
+
// containment guard (defense in depth beyond planDiscriminationCheck's path sanitation).
|
|
7454
|
+
if (!resolve(src).startsWith(resolve(repoRoot) + sep)) { results.push(nameFor(t, 'error')); continue; }
|
|
7455
|
+
const dst = join(worktree, t.file);
|
|
7456
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
7457
|
+
writeFileSync(dst, readFileSync(src));
|
|
7458
|
+
} catch {
|
|
7459
|
+
// the file STAT-PASSED and the copy still failed: degrade LOUDLY as an error with NO
|
|
7460
|
+
// evidence (the gate reads it as CANNOT_ISOLATE), never as absence and never as a pass.
|
|
7461
|
+
results.push(nameFor(t, 'error'));
|
|
7462
|
+
continue;
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
7466
|
+
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
7467
|
+
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
7468
|
+
const cmd = `${runner}${nameArg} -- '${t.file}'`;
|
|
7469
|
+
const base = runCapturedTest(cmd, worktree, timeoutMs);
|
|
7470
|
+
const evidence = classifyExecutionEvidence(base.output, base.exitCode, t.file);
|
|
7471
|
+
const outcome = discriminationOutcomeOf(base.exitCode, evidence);
|
|
7472
|
+
const row: Record<string, unknown> = t.name !== undefined
|
|
7473
|
+
? { file: t.file, name: t.name, outcome, evidence }
|
|
7474
|
+
: { file: t.file, outcome, evidence };
|
|
7475
|
+
|
|
7476
|
+
// 4) TIP CONTROL (FR-A2 + Confirmation 17). Run it for ALL non-assertion redness — file-load
|
|
7477
|
+
// redness (the matrix's EVIDENCED-error rows) AND unrecognised redness (so the invocation
|
|
7478
|
+
// ledger can prove the tip was REACHED). The CLASSIFIER still ignores the tip for unevidenced
|
|
7479
|
+
// base rows per the matrix; running it is cheap and only ever on an already-broken path.
|
|
7480
|
+
// Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
|
|
7481
|
+
if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
|
|
7482
|
+
const tip = runCapturedTest(cmd, repoRoot, timeoutMs);
|
|
7483
|
+
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, t.file);
|
|
7484
|
+
row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
|
|
7485
|
+
row['tipEvidence'] = tipEvidence;
|
|
7486
|
+
// R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
|
|
7487
|
+
// tree, where a concurrent writer can flip the observation mid-gate. No lock is taken
|
|
7488
|
+
// (deferred to backlog 9520e506); instead every tip-derived reading carries the tree
|
|
7489
|
+
// CONDITIONS it was taken under, so a surprising verdict can be re-read against them.
|
|
7490
|
+
if (tipTree === null) tipTree = readTipTreeConditions(repoRoot);
|
|
7491
|
+
}
|
|
7492
|
+
results.push(row as unknown as ClassifyResultRow);
|
|
7054
7493
|
}
|
|
7494
|
+
} catch (e) {
|
|
7495
|
+
if (flags.has('json')) { write(JSON.stringify({ plan, error: 'worktree-setup-failed', detail: String((e as Error).message).slice(0, 300) }, null, 2)); }
|
|
7496
|
+
else write(`discrimination-check: could not create worktree at ${baseRef}: ${String((e as Error).message).slice(0, 200)}`);
|
|
7497
|
+
return 2;
|
|
7498
|
+
} finally {
|
|
7499
|
+
try { execSync(`git worktree remove --force ${worktree}`, { cwd: repoRoot, stdio: 'pipe' }); } catch { /* fall through to rm */ }
|
|
7500
|
+
// remove the whole mkdtemp parent (worktree is `<mkdtemp>/wt`), so nothing leaks under tmp even on error.
|
|
7501
|
+
try { rmSync(dirname(worktree), { recursive: true, force: true }); } catch { /* best effort */ }
|
|
7502
|
+
try { execSync('git worktree prune', { cwd: repoRoot, stdio: 'pipe' }); } catch { /* best effort */ }
|
|
7055
7503
|
}
|
|
7056
|
-
} catch (e) {
|
|
7057
|
-
if (flags.has('json')) { write(JSON.stringify({ plan, error: 'worktree-setup-failed', detail: String((e as Error).message).slice(0, 300) }, null, 2)); }
|
|
7058
|
-
else write(`discrimination-check: could not create worktree at ${baseRef}: ${String((e as Error).message).slice(0, 200)}`);
|
|
7059
|
-
return 2;
|
|
7060
|
-
} finally {
|
|
7061
|
-
try { execSync(`git worktree remove --force ${worktree}`, { cwd: repoRoot, stdio: 'pipe' }); } catch { /* fall through to rm */ }
|
|
7062
|
-
// remove the whole mkdtemp parent (worktree is `<mkdtemp>/wt`), so nothing leaks under tmp even on error.
|
|
7063
|
-
try { rmSync(dirname(worktree), { recursive: true, force: true }); } catch { /* best effort */ }
|
|
7064
|
-
try { execSync('git worktree prune', { cwd: repoRoot, stdio: 'pipe' }); } catch { /* best effort */ }
|
|
7065
7504
|
}
|
|
7066
7505
|
|
|
7067
7506
|
const result = classifyDiscrimination({ propertyTests, results });
|
|
7068
|
-
if (flags.has('json')) { write(JSON.stringify({ plan, results, ...result }, null, 2)); return 0; }
|
|
7507
|
+
if (flags.has('json')) { write(JSON.stringify({ plan, results, tipTree, ...result }, null, 2)); return 0; }
|
|
7069
7508
|
|
|
7070
7509
|
write(`discrimination-check @ ${baseRef} — verdict: ${result.aggregate}`);
|
|
7071
|
-
for (const p of result.perTest)
|
|
7072
|
-
|
|
7510
|
+
for (const p of result.perTest) {
|
|
7511
|
+
// FR-A6: ✓ is reserved for the two ESTABLISHED trust verdicts. Every other value — including
|
|
7512
|
+
// every degraded reading — renders ✗, because a ✗ the operator investigates beats a ✓ that
|
|
7513
|
+
// silently meant "we could not tell".
|
|
7514
|
+
const mark = p.verdict === 'DISCRIMINATES' || p.verdict === 'DISCRIMINATES_VIA_ERROR' ? '✓' : '✗';
|
|
7515
|
+
write(` ${mark} ${p.file}${p.name ? ` (${p.name})` : ''}: ${p.verdict}${p.reason ? ` (reason: ${p.reason})` : ''}`);
|
|
7516
|
+
}
|
|
7517
|
+
write(` measurementValid: ${String(result.measurementValid)} · primaryAction: ${result.primaryAction}`);
|
|
7518
|
+
// ALL findings print, not just the worst: the scalar aggregate names one state, and a corpus with
|
|
7519
|
+
// a false green AND an absent file has two problems, each with its own operator action.
|
|
7520
|
+
for (const f of result.findings) write(`\n [${f.severity}] ${f.title}\n ${f.detail}`);
|
|
7073
7521
|
return 0;
|
|
7074
7522
|
}
|
|
7075
7523
|
|
|
7524
|
+
/**
|
|
7525
|
+
* Run one test command and CAPTURE the observation — output plus the exit code, including the
|
|
7526
|
+
* "no exit code at all" case. `execSync`'s timeout kills the child via signal and leaves
|
|
7527
|
+
* `status` null; a spawn failure does the same. That null is not an error to swallow, it is the
|
|
7528
|
+
* evidence (`CANNOT_ISOLATE` reason `'timeout'`), so it is returned as data.
|
|
7529
|
+
*/
|
|
7530
|
+
function runCapturedTest(cmd: string, cwd: string, timeoutMs: number): { output: string; exitCode: number | null } {
|
|
7531
|
+
try {
|
|
7532
|
+
const stdout = execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf-8', timeout: timeoutMs });
|
|
7533
|
+
return { output: String(stdout ?? ''), exitCode: 0 };
|
|
7534
|
+
} catch (e) {
|
|
7535
|
+
const err = e as { stdout?: string; stderr?: string; status?: unknown };
|
|
7536
|
+
return {
|
|
7537
|
+
output: String(err.stdout ?? '') + String(err.stderr ?? ''),
|
|
7538
|
+
exitCode: typeof err.status === 'number' ? err.status : null,
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
|
|
7543
|
+
/**
|
|
7544
|
+
* The outcome VALUE for one captured run. The executor's whole remaining judgment, and it is
|
|
7545
|
+
* mechanical: exit 0 is a pass, no exit code is an error, and a non-zero exit is an error only when
|
|
7546
|
+
* the classifier RECOGNISED a file-load failure. An unrecognised red is deliberately recorded as a
|
|
7547
|
+
* `fail` VALUE whose evidence then degrades it — exactly acid A6's pinned shape, and the reason the
|
|
7548
|
+
* executor no longer owns a regex.
|
|
7549
|
+
*/
|
|
7550
|
+
function discriminationOutcomeOf(exitCode: number | null, evidence: ExecutionEvidence): 'pass' | 'fail' | 'error' {
|
|
7551
|
+
if (exitCode === null) return 'error';
|
|
7552
|
+
if (exitCode === 0) return 'pass';
|
|
7553
|
+
return evidence.failureKind === 'file-load' ? 'error' : 'fail';
|
|
7554
|
+
}
|
|
7555
|
+
|
|
7556
|
+
/** The live tree's identity at tip-run time (R15). Best-effort: unknown conditions read as such. */
|
|
7557
|
+
function readTipTreeConditions(repoRoot: string): { headSha: string; dirtyFiles: number } {
|
|
7558
|
+
let headSha = 'unknown';
|
|
7559
|
+
let dirtyFiles = -1;
|
|
7560
|
+
try { headSha = execSync('git rev-parse HEAD', { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' }).trim(); } catch { /* best effort */ }
|
|
7561
|
+
try {
|
|
7562
|
+
const porcelain = execSync('git status --porcelain', { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
7563
|
+
dirtyFiles = String(porcelain).split('\n').filter((l) => l.trim() !== '').length;
|
|
7564
|
+
} catch { /* best effort */ }
|
|
7565
|
+
return { headSha, dirtyFiles };
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7076
7568
|
/** small helper: build a result row, omitting `name` when absent (exactOptionalPropertyTypes). */
|
|
7077
7569
|
function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' | 'error'): { file: string; name?: string; outcome: 'pass' | 'fail' | 'error' } {
|
|
7078
7570
|
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
@@ -8151,7 +8643,11 @@ const PROBE_SCRUB_ENV = [
|
|
|
8151
8643
|
* lands, then kill it — the model never answers, so the probe costs ~no tokens. Never throws:
|
|
8152
8644
|
* every failure becomes an `error` string, which the pure classifier turns into `inconclusive`.
|
|
8153
8645
|
*/
|
|
8154
|
-
function probeInitStream(
|
|
8646
|
+
function probeInitStream(
|
|
8647
|
+
projectDir: string,
|
|
8648
|
+
timeoutMs: number,
|
|
8649
|
+
pluginDir: string | null = null,
|
|
8650
|
+
): Promise<{ stream: string; error: string | null }> {
|
|
8155
8651
|
return new Promise((resolveProbe) => {
|
|
8156
8652
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
8157
8653
|
for (const key of PROBE_SCRUB_ENV) delete env[key];
|
|
@@ -8181,7 +8677,12 @@ function probeInitStream(projectDir: string, timeoutMs: number): Promise<{ strea
|
|
|
8181
8677
|
|
|
8182
8678
|
try {
|
|
8183
8679
|
// NOT `--bare`: that mode skips plugin credentials and fails with "Not logged in".
|
|
8184
|
-
|
|
8680
|
+
const args = ['-p', 'ok', '--output-format', 'stream-json', '--verbose'];
|
|
8681
|
+
// Session-scoped plugin load — the marketplace-free vehicle (ADR-003 D-3). Without this the
|
|
8682
|
+
// probe reads a session in which the plugin was never loaded, and reports its commands
|
|
8683
|
+
// missing for a reason that has nothing to do with the package under test.
|
|
8684
|
+
if (pluginDir !== null) args.push('--plugin-dir', pluginDir);
|
|
8685
|
+
child = spawn('claude', args, {
|
|
8185
8686
|
cwd: projectDir,
|
|
8186
8687
|
env,
|
|
8187
8688
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -8255,8 +8756,11 @@ async function cmdSkillsVerify(
|
|
|
8255
8756
|
): Promise<number> {
|
|
8256
8757
|
const json = flags.has('json');
|
|
8257
8758
|
if (flags.has('help')) {
|
|
8258
|
-
write('dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json]');
|
|
8759
|
+
write('dz skills-verify [--dir <project>] [--expect a,b] [--expect-commands a,b] [--plugin-dir <dir>] [--static] [--strict] [--timeout <s>] [--json]');
|
|
8259
8760
|
write(' Verifies that a project\'s .claude/skills/ actually register in Claude Code.');
|
|
8761
|
+
write(' --plugin-dir <dir> load a plugin into the probe session (session-scoped, no marketplace);');
|
|
8762
|
+
write(' with no --expect-commands, the expectation defaults to the manifest\'s own commands[]');
|
|
8763
|
+
write(' --expect-commands slash commands that MUST appear in the session listing, e.g. loop-designer:init');
|
|
8260
8764
|
write(' --static layout scan only (no Claude session, CI-safe): flags dirs that can never register');
|
|
8261
8765
|
write(' --live-content ADVISORY extra turn: ask a live model to name the skills and quote one, proving');
|
|
8262
8766
|
write(' the CONTENT is usable — registration is not usability. Never changes the exit code.');
|
|
@@ -8266,8 +8770,9 @@ async function cmdSkillsVerify(
|
|
|
8266
8770
|
}
|
|
8267
8771
|
|
|
8268
8772
|
const allowedFlags = new Set(['json', 'help', 'static', 'strict', 'live-content']);
|
|
8269
|
-
const allowedOptions = new Set(['dir', 'expect', 'timeout']);
|
|
8270
|
-
const usage =
|
|
8773
|
+
const allowedOptions = new Set(['dir', 'expect', 'expect-commands', 'plugin-dir', 'timeout']);
|
|
8774
|
+
const usage =
|
|
8775
|
+
' allowed: --dir <project>, --expect a,b, --expect-commands a,b, --plugin-dir <dir>, --timeout <s>, --static, --strict, --live-content, --json';
|
|
8271
8776
|
if (options.has('_positional_0')) {
|
|
8272
8777
|
const message = `unexpected argument "${options.get('_positional_0')}"`;
|
|
8273
8778
|
write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}\n${usage}`);
|
|
@@ -8297,6 +8802,28 @@ async function cmdSkillsVerify(
|
|
|
8297
8802
|
.filter(Boolean)
|
|
8298
8803
|
: scan.registrable;
|
|
8299
8804
|
|
|
8805
|
+
// A plugin loaded with `--plugin-dir` is session-scoped, so its surface is NOT on the project's
|
|
8806
|
+
// disk and `scan.registrable` cannot describe it. The manifest can — and defaulting to the
|
|
8807
|
+
// manifest's own `commands[]` keeps the gate honest without a hand-typed list that silently
|
|
8808
|
+
// drifts from the manifest it is supposed to be checking. An UNREADABLE manifest is refused
|
|
8809
|
+
// rather than defaulted to an empty expectation: an empty expectation passes without checking.
|
|
8810
|
+
const pluginDir = options.has('plugin-dir') ? resolve(cwd, options.get('plugin-dir') ?? '') : null;
|
|
8811
|
+
let expectedCommands = options.has('expect-commands')
|
|
8812
|
+
? (options.get('expect-commands') ?? '')
|
|
8813
|
+
.split(',')
|
|
8814
|
+
.map((s) => s.trim())
|
|
8815
|
+
.filter(Boolean)
|
|
8816
|
+
: [];
|
|
8817
|
+
if (pluginDir !== null && !options.has('expect-commands')) {
|
|
8818
|
+
const surface = declaredPluginSurface(pluginDir);
|
|
8819
|
+
if (surface === null) {
|
|
8820
|
+
const message = `cannot read ${join(pluginDir, '.claude-plugin', 'plugin.json')} (or it declares no name) — pass --expect-commands explicitly`;
|
|
8821
|
+
write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}`);
|
|
8822
|
+
return 1;
|
|
8823
|
+
}
|
|
8824
|
+
expectedCommands = surface.commands;
|
|
8825
|
+
}
|
|
8826
|
+
|
|
8300
8827
|
// ── L1 only: deterministic, no session, safe for CI ──
|
|
8301
8828
|
if (flags.has('static')) {
|
|
8302
8829
|
const exitCode = scan.findings.length > 0 ? 1 : 0;
|
|
@@ -8307,6 +8834,11 @@ async function cmdSkillsVerify(
|
|
|
8307
8834
|
if (!scan.exists) write(' no .claude/skills/ directory here');
|
|
8308
8835
|
for (const f of scan.findings) write(` [${f.kind}] ${f.detail}`);
|
|
8309
8836
|
write(scan.findings.length ? ` ${scan.findings.length} layout problem(s) — these can never register` : ' no layout problems found');
|
|
8837
|
+
// Advisories were collected but never PRINTED in static mode: a `.claude-plugin/plugin.json`
|
|
8838
|
+
// under `.claude/skills` produced "no layout problems found" and nothing else, so the one
|
|
8839
|
+
// shape most likely to be a silent non-registration was invisible in exactly the mode CI and
|
|
8840
|
+
// humans run most. Reported, still never fatal (that distinction is the whole point).
|
|
8841
|
+
for (const a of scan.advisories) write(` [${a.kind}] ADVISORY: ${a.detail}`);
|
|
8310
8842
|
write(' (static is a PROXY — run without --static to read the real registration listing)');
|
|
8311
8843
|
}
|
|
8312
8844
|
return exitCode;
|
|
@@ -8317,7 +8849,7 @@ async function cmdSkillsVerify(
|
|
|
8317
8849
|
const timeoutMs = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec * 1000 : 180_000;
|
|
8318
8850
|
if (!json) write(`dz skills-verify: starting a session in ${projectDir} to read the real registration listing…`);
|
|
8319
8851
|
|
|
8320
|
-
const { stream, error } = await probeInitStream(projectDir, timeoutMs);
|
|
8852
|
+
const { stream, error } = await probeInitStream(projectDir, timeoutMs, pluginDir);
|
|
8321
8853
|
|
|
8322
8854
|
// `init.skills` carries names, not provenance: a USER-level skill of the same name would satisfy
|
|
8323
8855
|
// the expectation while the project's own copy stays broken. Collect the collisions so the
|
|
@@ -8342,6 +8874,7 @@ async function cmdSkillsVerify(
|
|
|
8342
8874
|
// The provenance check RAN (that is what `checked: true` asserts) — see `ambiguous` above.
|
|
8343
8875
|
provenance: { checked: true, ambiguous },
|
|
8344
8876
|
...(options.has('expect') ? { expected } : {}),
|
|
8877
|
+
...(expectedCommands.length > 0 ? { expectedCommands } : {}),
|
|
8345
8878
|
},
|
|
8346
8879
|
{ resolvePath: canonical },
|
|
8347
8880
|
);
|
|
@@ -9281,6 +9814,8 @@ async function cmdImportEcc(options: Map<string, string>, flags: Set<string>, cw
|
|
|
9281
9814
|
export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
9282
9815
|
const cwd = io.cwd ?? process.cwd();
|
|
9283
9816
|
const write: Write = io.write ?? ((line) => { console.log(line); });
|
|
9817
|
+
// Diagnostics go to stderr so `dz <cmd> > out.txt` yields clean data (feature dz-cli-defects).
|
|
9818
|
+
const writeErr: WriteErr = io.writeErr ?? ((line) => { console.error(line); });
|
|
9284
9819
|
// Lazy STDIN reader — only `dz brain ground` reads it, and only when no positional prompt is
|
|
9285
9820
|
// given. Never blocks: injected `io.stdin` wins; else read fd 0 synchronously, but bail to '' on
|
|
9286
9821
|
// a TTY (nothing piped) or any read error. Grounding must never hang waiting on an empty pipe.
|
|
@@ -9295,6 +9830,26 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9295
9830
|
};
|
|
9296
9831
|
const { command, options, optionLists, flags } = parseArgs(argv);
|
|
9297
9832
|
|
|
9833
|
+
// ── `dz --version` / `dz -v` / `dz version` — PRE-DISPATCH, before the help branch ──
|
|
9834
|
+
//
|
|
9835
|
+
// Until now `dz --version` printed the whole USAGE manual and exited 0 (MEASURED 2026-08-17,
|
|
9836
|
+
// reproducer `node dist/bin.js --version`). Exit 0 plus prose is the worst possible answer for a
|
|
9837
|
+
// caller that must decide whether a `dz` it found on PATH is safe to invoke: the status code says
|
|
9838
|
+
// "fine" and there is no number to parse. Any wrapper guarding a version range needs exactly one
|
|
9839
|
+
// parseable line. Recognised only as the FIRST token (or the `version` subcommand) so that a
|
|
9840
|
+
// later positional `-v` belonging to a subcommand keeps its own meaning.
|
|
9841
|
+
if (argv[0] === '--version' || argv[0] === '-v' || command === 'version') {
|
|
9842
|
+
const version = dzOwnVersion();
|
|
9843
|
+
if (flags.has('json')) {
|
|
9844
|
+
write(JSON.stringify({ name: 'dz', version, node: process.version, schemas: { loopPlan: 'loop-plan/1' } }));
|
|
9845
|
+
} else {
|
|
9846
|
+
write(version);
|
|
9847
|
+
}
|
|
9848
|
+
// An unresolvable version is a FAILURE, not a value: exiting 0 with the literal `unknown` would
|
|
9849
|
+
// let a guard treat "I could not tell you" as "I answered you".
|
|
9850
|
+
return version === 'unknown' ? 1 : 0;
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9298
9853
|
if (command === '' || command === 'help' || flags.has('help')) {
|
|
9299
9854
|
write(USAGE);
|
|
9300
9855
|
return 0;
|
|
@@ -9303,14 +9858,14 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9303
9858
|
try {
|
|
9304
9859
|
switch (command) {
|
|
9305
9860
|
case 'init':
|
|
9306
|
-
return await cmdInit(options, flags, cwd, write);
|
|
9861
|
+
return await cmdInit(options, flags, cwd, write, writeErr);
|
|
9307
9862
|
case 'verify':
|
|
9308
|
-
return await cmdVerify(options, cwd, write);
|
|
9863
|
+
return await cmdVerify(options, cwd, write, writeErr);
|
|
9309
9864
|
case 'sync':
|
|
9310
9865
|
case 'update':
|
|
9311
|
-
return await cmdSync(options, flags, cwd, write);
|
|
9866
|
+
return await cmdSync(options, flags, cwd, write, writeErr);
|
|
9312
9867
|
case 'list':
|
|
9313
|
-
return cmdList(options, cwd, write);
|
|
9868
|
+
return cmdList(options, cwd, write, writeErr);
|
|
9314
9869
|
case 'create-skill':
|
|
9315
9870
|
return cmdCreateSkill(options, flags, cwd, write);
|
|
9316
9871
|
case 'info':
|
|
@@ -9328,7 +9883,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9328
9883
|
case 'doctor':
|
|
9329
9884
|
return await cmdDoctor(options, flags, cwd, write);
|
|
9330
9885
|
case 'install':
|
|
9331
|
-
return await cmdInstall(options, flags, cwd, write, io.installRunner);
|
|
9886
|
+
return await cmdInstall(options, flags, cwd, write, writeErr, io.installRunner);
|
|
9332
9887
|
case 'bundle':
|
|
9333
9888
|
return cmdBundle(options, flags, cwd, write);
|
|
9334
9889
|
case 'teach':
|
|
@@ -9356,17 +9911,17 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9356
9911
|
case 'verify-pack':
|
|
9357
9912
|
return cmdVerifyPack(options, flags, cwd, write);
|
|
9358
9913
|
case 'setup':
|
|
9359
|
-
return await cmdSetup(options, flags, cwd, write);
|
|
9914
|
+
return await cmdSetup(options, flags, cwd, write, writeErr);
|
|
9360
9915
|
case 'pretrain':
|
|
9361
9916
|
return cmdPretrain(options, cwd, write);
|
|
9362
9917
|
case 'compose':
|
|
9363
|
-
return cmdCompose(options, cwd, write);
|
|
9918
|
+
return cmdCompose(options, cwd, write, writeErr);
|
|
9364
9919
|
case 'diff':
|
|
9365
9920
|
return cmdDiff(options, cwd, write);
|
|
9366
9921
|
case 'recommend':
|
|
9367
9922
|
return cmdRecommend(options, cwd, write);
|
|
9368
9923
|
case 'upgrade':
|
|
9369
|
-
return cmdUpgrade(options, flags, cwd, write);
|
|
9924
|
+
return cmdUpgrade(options, flags, cwd, write, writeErr);
|
|
9370
9925
|
case 'auto-canonicalize':
|
|
9371
9926
|
return await cmdAutoCanonicalize(options, cwd, write);
|
|
9372
9927
|
case 'publish':
|
|
@@ -9374,7 +9929,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9374
9929
|
case 'release':
|
|
9375
9930
|
return cmdRelease(options, flags, cwd, write, io.releaseRunner);
|
|
9376
9931
|
case 'parity':
|
|
9377
|
-
return cmdParity(options, flags, write);
|
|
9932
|
+
return cmdParity(options, flags, write, writeErr);
|
|
9378
9933
|
case 'registry':
|
|
9379
9934
|
return cmdRegistry(options, cwd, write);
|
|
9380
9935
|
case 'benchmark':
|
|
@@ -9385,6 +9940,10 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9385
9940
|
return await cmdSyncUpstream(options, flags, cwd, write);
|
|
9386
9941
|
case 'drift-check':
|
|
9387
9942
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
9943
|
+
case 'hooks-sync':
|
|
9944
|
+
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
9945
|
+
case 'agents-sync':
|
|
9946
|
+
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
9388
9947
|
case 'sync-canonical':
|
|
9389
9948
|
return cmdSyncCanonical(options, flags, cwd, write);
|
|
9390
9949
|
case 'plugin':
|
|
@@ -9402,7 +9961,7 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9402
9961
|
case 'retro':
|
|
9403
9962
|
return await cmdRetro(options, flags, cwd, write);
|
|
9404
9963
|
case 'feature-adr-setup':
|
|
9405
|
-
return cmdFeatureAdrSetup(options, flags, cwd, write);
|
|
9964
|
+
return cmdFeatureAdrSetup(options, flags, cwd, write, writeErr);
|
|
9406
9965
|
case 'challenge':
|
|
9407
9966
|
return cmdChallenge(options, flags, cwd, write);
|
|
9408
9967
|
case 'discrimination-check':
|
|
@@ -9439,7 +9998,9 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
9439
9998
|
return 1;
|
|
9440
9999
|
}
|
|
9441
10000
|
} catch (error) {
|
|
9442
|
-
|
|
10001
|
+
// stderr, not stdout: an uncaught failure is a diagnostic, and routing it through
|
|
10002
|
+
// `write` is what made `dz list > skills.txt` write the error into the data file.
|
|
10003
|
+
writeErr(`dz: ${error instanceof Error ? error.message : String(error)}`);
|
|
9443
10004
|
return 1;
|
|
9444
10005
|
}
|
|
9445
10006
|
}
|