@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/dist/cli.js
CHANGED
|
@@ -10,9 +10,9 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
11
|
import { homedir, tmpdir } from 'node:os';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
-
import { createSkill, getSkillInfo,
|
|
13
|
+
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, renderTimelineHtml, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
14
14
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
15
|
-
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport,
|
|
15
|
+
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport,
|
|
16
16
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
17
17
|
replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
|
|
18
18
|
// Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
|
|
@@ -110,6 +110,8 @@ Usage:
|
|
|
110
110
|
(build-time: reconcile project grants vs installed skills' declared capabilities; dz never enforces — the host does)
|
|
111
111
|
dz sync-upstream [--package <dir>] [--list] [--all]
|
|
112
112
|
dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
|
|
113
|
+
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)
|
|
114
|
+
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)
|
|
113
115
|
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)
|
|
114
116
|
dz plugin [--version <ver>]
|
|
115
117
|
dz downloads
|
|
@@ -119,6 +121,8 @@ Usage:
|
|
|
119
121
|
dz import-ecc [--local-path <dir>] [--select id,id,...] [--limit N] [--output <dir>] [--force]
|
|
120
122
|
dz help
|
|
121
123
|
|
|
124
|
+
Global: --version | -v [--json] (prints this CLI's own semver on one line, exit 0; "unknown" + exit 1 when unresolvable)
|
|
125
|
+
|
|
122
126
|
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)
|
|
123
127
|
|
|
124
128
|
Targets: ${TARGET_NAMES.join(', ')}
|
|
@@ -206,9 +210,11 @@ async function installSkills(opts) {
|
|
|
206
210
|
written += s.written;
|
|
207
211
|
skipped += s.skipped;
|
|
208
212
|
}
|
|
209
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing] };
|
|
213
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
|
|
210
214
|
}
|
|
211
215
|
const results = [];
|
|
216
|
+
const failures = [];
|
|
217
|
+
const applyFailures = [];
|
|
212
218
|
for (const skillsDir of skillsDirs) {
|
|
213
219
|
const r = await runInit({
|
|
214
220
|
target,
|
|
@@ -221,6 +227,8 @@ async function installSkills(opts) {
|
|
|
221
227
|
for (const skill of r.skills) {
|
|
222
228
|
results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
|
|
223
229
|
}
|
|
230
|
+
failures.push(...r.failures);
|
|
231
|
+
applyFailures.push(...r.applyFailures);
|
|
224
232
|
}
|
|
225
233
|
let written = 0;
|
|
226
234
|
let skipped = 0;
|
|
@@ -230,7 +238,7 @@ async function installSkills(opts) {
|
|
|
230
238
|
}
|
|
231
239
|
const installed = new Set(results.map((s) => s.id));
|
|
232
240
|
const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
|
|
233
|
-
return { results, dirsSearched: skillsDirs.length, written, skipped, missing };
|
|
241
|
+
return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
|
|
234
242
|
}
|
|
235
243
|
/** Warn about preset/select ids that weren't found in any installed pack. */
|
|
236
244
|
function writeMissingSkillsHint(write, missing, presetName) {
|
|
@@ -246,12 +254,24 @@ function writeMissingSkillsHint(write, missing, presetName) {
|
|
|
246
254
|
write(` Install their packs first (e.g. dz install @dzhechkov/skills-<pack>) or check the ids.`);
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
|
-
async function cmdInit(options, flags, cwd, write) {
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
|
|
257
|
+
async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
258
|
+
const targetOpt = options.get('target');
|
|
259
|
+
if (targetOpt === undefined) {
|
|
260
|
+
// A missing `--target` is the same accusation as an unresolvable one, so it takes
|
|
261
|
+
// the same channel: diagnostics on stderr, stdout stays a data channel (ADR-002
|
|
262
|
+
// §Decision 2 / driver D6; fix round 1, QE F2).
|
|
263
|
+
writeErr(`dz init: --target must be one of: ${TARGET_NAMES_SORTED.join(', ')}`);
|
|
253
264
|
return 1;
|
|
254
265
|
}
|
|
266
|
+
const resolution = resolveTargetName(targetOpt);
|
|
267
|
+
if (resolution.kind === 'unknown') {
|
|
268
|
+
for (const line of formatTargetProblem('dz init', resolution))
|
|
269
|
+
writeErr(line);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
const target = resolution.target;
|
|
273
|
+
if (resolution.via === 'alias')
|
|
274
|
+
writeErr(formatTargetAliasNote('dz init', targetOpt, target));
|
|
255
275
|
const explicitSkillsDir = options.get('skills-dir');
|
|
256
276
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
257
277
|
const presetName = options.get('preset');
|
|
@@ -287,18 +307,48 @@ async function cmdInit(options, flags, cwd, write) {
|
|
|
287
307
|
write(` (searched ${r.dirsSearched} skill directories)`);
|
|
288
308
|
}
|
|
289
309
|
writeMissingSkillsHint(write, r.missing, presetName);
|
|
310
|
+
// Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
|
|
311
|
+
// named on stderr and the command exits 1 (it exited 1 before too — by throwing).
|
|
312
|
+
if (r.failures.length > 0 || r.applyFailures.length > 0) {
|
|
313
|
+
// Counts first, then the named block — the same shape `dz list` uses. The block's
|
|
314
|
+
// own header already says "N skipped", so this line carries what it cannot: how many
|
|
315
|
+
// DID install, so a reader can tell a mostly-fine install from a mostly-broken one.
|
|
316
|
+
//
|
|
317
|
+
// The two failure kinds are counted and rendered SEPARATELY (fix round 1, QE F4):
|
|
318
|
+
// an unwritable target directory is not a broken pack, and printing it as one names
|
|
319
|
+
// the wrong file.
|
|
320
|
+
const parts = [`${r.results.length} installed`];
|
|
321
|
+
if (r.failures.length > 0)
|
|
322
|
+
parts.push(`${r.failures.length} skipped`);
|
|
323
|
+
if (r.applyFailures.length > 0)
|
|
324
|
+
parts.push(`${r.applyFailures.length} failed to write`);
|
|
325
|
+
writeErr(`dz init: ${parts.join(', ')}`);
|
|
326
|
+
for (const line of formatSkillLoadFailures(r.failures))
|
|
327
|
+
writeErr(line);
|
|
328
|
+
for (const line of formatSkillApplyFailures(r.applyFailures))
|
|
329
|
+
writeErr(line);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
290
332
|
return 0;
|
|
291
333
|
}
|
|
292
|
-
async function cmdVerify(options, cwd, write) {
|
|
334
|
+
async function cmdVerify(options, cwd, write, writeErr) {
|
|
293
335
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
294
336
|
const targetOpt = options.get('target');
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
337
|
+
let target;
|
|
338
|
+
if (targetOpt !== undefined) {
|
|
339
|
+
const resolution = resolveTargetName(targetOpt);
|
|
340
|
+
if (resolution.kind === 'unknown') {
|
|
341
|
+
for (const line of formatTargetProblem('dz verify', resolution))
|
|
342
|
+
writeErr(line);
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
target = resolution.target;
|
|
346
|
+
if (resolution.via === 'alias')
|
|
347
|
+
writeErr(formatTargetAliasNote('dz verify', targetOpt, target));
|
|
298
348
|
}
|
|
299
349
|
const report = await runVerify({
|
|
300
350
|
skillsDir,
|
|
301
|
-
...(
|
|
351
|
+
...(target !== undefined ? { target } : {}),
|
|
302
352
|
});
|
|
303
353
|
write(`dz verify (${report.target}): ${report.valid}/${report.total} skill(s) valid`);
|
|
304
354
|
for (const skill of report.skills) {
|
|
@@ -307,7 +357,7 @@ async function cmdVerify(options, cwd, write) {
|
|
|
307
357
|
}
|
|
308
358
|
return report.valid === report.total ? 0 : 1;
|
|
309
359
|
}
|
|
310
|
-
async function cmdSync(options, flags, cwd, write) {
|
|
360
|
+
async function cmdSync(options, flags, cwd, write, writeErr) {
|
|
311
361
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
312
362
|
const canonicalArg = options.get('canonical');
|
|
313
363
|
// Auto-discover all skills-* packs, or use explicit --canonical
|
|
@@ -331,6 +381,15 @@ async function cmdSync(options, flags, cwd, write) {
|
|
|
331
381
|
});
|
|
332
382
|
const { total, inSync, missing, drift } = report.summary;
|
|
333
383
|
write(`dz sync${report.dryRun ? ' --dry-run' : ''}: ${inSync}/${total} in sync, ${missing} missing, ${drift} drift`);
|
|
384
|
+
// Skip-and-collect (D1): the broken canonical skills are NAMED on stderr, and their
|
|
385
|
+
// presence keeps the exit code non-zero — a partial sync is not a clean sync.
|
|
386
|
+
if (report.failures.length > 0) {
|
|
387
|
+
// See `cmdInit` above: counts here, names in the block below.
|
|
388
|
+
writeErr(`dz sync: ${report.skills.length} compared, ${report.failures.length} skipped`);
|
|
389
|
+
for (const line of formatSkillLoadFailures(report.failures))
|
|
390
|
+
writeErr(line);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
334
393
|
return missing === 0 && drift === 0 ? 0 : 1;
|
|
335
394
|
}
|
|
336
395
|
function cmdCreateSkill(options, flags, cwd, write) {
|
|
@@ -372,19 +431,44 @@ function cmdCreateSkill(options, flags, cwd, write) {
|
|
|
372
431
|
}
|
|
373
432
|
return 0;
|
|
374
433
|
}
|
|
375
|
-
|
|
434
|
+
/**
|
|
435
|
+
* `dz list` — skip-and-collect (feature dz-cli-defects, D1).
|
|
436
|
+
*
|
|
437
|
+
* One unparseable `SKILL.md` used to discard the ENTIRE listing with a message naming
|
|
438
|
+
* neither the file nor the count. Now the parseable skills list on stdout and the
|
|
439
|
+
* broken ones are named on stderr. The whole emit contract, in one place:
|
|
440
|
+
*
|
|
441
|
+
* | valid | skipped | stdout | stderr | exit |
|
|
442
|
+
* |-------|---------|--------|--------|------|
|
|
443
|
+
* | >0 | 0 | listing | *empty* | 0 |
|
|
444
|
+
* | >0 | >0 | listing of the valid ones | named summary | 1 |
|
|
445
|
+
* | 0 | >0 | *nothing* | named summary | 1 |
|
|
446
|
+
* | 0 | 0 | *nothing* | `no skills found in <dir>` | 1 |
|
|
447
|
+
*
|
|
448
|
+
* The last row is the ONE intentional departure from byte-identical output: that line
|
|
449
|
+
* used to go to stdout. Moving it keeps *stdout is data, stderr is diagnosis* whole —
|
|
450
|
+
* the invariant that makes `dz list > out.txt` trustworthy.
|
|
451
|
+
*/
|
|
452
|
+
function cmdList(options, cwd, write, writeErr) {
|
|
376
453
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
377
|
-
const skills =
|
|
378
|
-
if (skills.length === 0) {
|
|
379
|
-
|
|
454
|
+
const { skills, failures } = listSkillsDetailed(skillsDir);
|
|
455
|
+
if (skills.length === 0 && failures.length === 0) {
|
|
456
|
+
writeErr(`dz list: no skills found in ${skillsDir}`);
|
|
380
457
|
return 1;
|
|
381
458
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
459
|
+
if (skills.length > 0) {
|
|
460
|
+
write(`${skills.length} skill(s) in ${skillsDir}:\n`);
|
|
461
|
+
for (const skill of skills) {
|
|
462
|
+
const desc = skill.description.length > 80 ? skill.description.slice(0, 77) + '...' : skill.description;
|
|
463
|
+
write(` ${skill.id.padEnd(35)} ${desc}`);
|
|
464
|
+
}
|
|
386
465
|
}
|
|
387
|
-
|
|
466
|
+
if (failures.length === 0)
|
|
467
|
+
return 0;
|
|
468
|
+
writeErr(`dz list: ${skills.length} listed, ${failures.length} skipped in ${skillsDir}`);
|
|
469
|
+
for (const line of formatSkillLoadFailures(failures))
|
|
470
|
+
writeErr(line);
|
|
471
|
+
return 1;
|
|
388
472
|
}
|
|
389
473
|
function cmdInfo(options, args, cwd, write) {
|
|
390
474
|
const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
|
|
@@ -529,8 +613,8 @@ function workflowInitPlan(name, pattern) {
|
|
|
529
613
|
...base,
|
|
530
614
|
steps: [
|
|
531
615
|
{ stepId: 'fan', kind: 'fanout', phase: 'Work', concurrency: 'pipeline', budget: { maxAgents: 8 } },
|
|
532
|
-
{ 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)
|
|
533
|
-
{ 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)
|
|
616
|
+
{ 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)
|
|
617
|
+
{ 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)
|
|
534
618
|
{ stepId: 'jn', kind: 'join', phase: 'Work', deps: ['fan'] },
|
|
535
619
|
],
|
|
536
620
|
fanouts: [{ stage: 'fan', registry: ['item1', 'item2', 'item3'], maxFanout: 3, chain: ['a', 'b'] }],
|
|
@@ -542,10 +626,10 @@ function workflowInitPlan(name, pattern) {
|
|
|
542
626
|
...base,
|
|
543
627
|
steps: [
|
|
544
628
|
{ stepId: 'fan', kind: 'fanout', phase: 'Lanes', concurrency: 'barrier', budget: { maxAgents: 6 } },
|
|
545
|
-
{ 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)
|
|
629
|
+
{ 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)
|
|
546
630
|
{ stepId: 'jn', kind: 'join', phase: 'Lanes', deps: ['fan'] },
|
|
547
631
|
// the consumer hangs off the BARRIER (jn), never the fork — barrier-postdominates teaches this
|
|
548
|
-
{ 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)
|
|
632
|
+
{ 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)
|
|
549
633
|
],
|
|
550
634
|
fanouts: [{ stage: 'fan', registry: ['lane1', 'lane2', 'lane3'], maxFanout: 3, chain: ['lane'] }],
|
|
551
635
|
joins: [{ stage: 'jn', forStage: 'fan', joinPolicy: 'all-activated', onInvalid: 'named-failure' }],
|
|
@@ -555,8 +639,8 @@ function workflowInitPlan(name, pattern) {
|
|
|
555
639
|
return {
|
|
556
640
|
...base,
|
|
557
641
|
steps: [
|
|
558
|
-
{ 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)
|
|
559
|
-
{ 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)
|
|
642
|
+
{ 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)
|
|
643
|
+
{ 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)
|
|
560
644
|
],
|
|
561
645
|
gates: [{ stepId: 'gate', kind: 'parse-verdict', failRoute: 'work', maxRedos: 1 }],
|
|
562
646
|
};
|
|
@@ -564,7 +648,7 @@ function workflowInitPlan(name, pattern) {
|
|
|
564
648
|
// minimal default: one agent step
|
|
565
649
|
return {
|
|
566
650
|
...base,
|
|
567
|
-
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)
|
|
651
|
+
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)
|
|
568
652
|
};
|
|
569
653
|
}
|
|
570
654
|
/**
|
|
@@ -762,8 +846,12 @@ function cmdWorkflowLint(options, flags, cwd, write) {
|
|
|
762
846
|
/** `dz workflow-trace` — timeline + invariant runner over a run's trace.jsonl. Scope is CAPPED
|
|
763
847
|
* (AM-8): <runDir|--slug|--run>, --invariants, --html, --json. NO watch/filter/compare/search/
|
|
764
848
|
* retention/access-control — adding one needs an ADR amendment (the surface test pins this). */
|
|
765
|
-
/**
|
|
766
|
-
|
|
849
|
+
/**
|
|
850
|
+
* This CLI's OWN semver, read from its package.json. `unknown` is honest; a throw is not, and an
|
|
851
|
+
* invented number is worse than either — a downstream version guard that is handed a fabricated
|
|
852
|
+
* version happily calls a binary it should have refused.
|
|
853
|
+
*/
|
|
854
|
+
function dzOwnVersion() {
|
|
767
855
|
try {
|
|
768
856
|
const req = createRequire(import.meta.url);
|
|
769
857
|
const pkg = req('../package.json');
|
|
@@ -773,6 +861,10 @@ function traceBundleToolVersion() {
|
|
|
773
861
|
return 'unknown';
|
|
774
862
|
}
|
|
775
863
|
}
|
|
864
|
+
/** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
|
|
865
|
+
function traceBundleToolVersion() {
|
|
866
|
+
return dzOwnVersion();
|
|
867
|
+
}
|
|
776
868
|
/** Resolve a run the SAME three ways the timeline reader does — positional, --slug, --run — so a
|
|
777
869
|
* bundle can never address a run by a scheme the rest of the command does not understand. */
|
|
778
870
|
function resolveTraceRun(options, cwd, positional) {
|
|
@@ -1207,17 +1299,22 @@ function cmdBundle(options, flags, cwd, write) {
|
|
|
1207
1299
|
}
|
|
1208
1300
|
return 0;
|
|
1209
1301
|
}
|
|
1210
|
-
async function cmdInstall(options, flags, cwd, write, installRunner) {
|
|
1302
|
+
async function cmdInstall(options, flags, cwd, write, writeErr, installRunner) {
|
|
1211
1303
|
const pkg = options.get('_positional_0');
|
|
1212
1304
|
if (!pkg) {
|
|
1213
1305
|
write('dz install: package name required (e.g., dz install @dzhechkov/skills-devops)');
|
|
1214
1306
|
return 1;
|
|
1215
1307
|
}
|
|
1216
1308
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
1217
|
-
|
|
1218
|
-
|
|
1309
|
+
const targetResolution = resolveTargetName(targetOpt);
|
|
1310
|
+
if (targetResolution.kind === 'unknown') {
|
|
1311
|
+
for (const line of formatTargetProblem('dz install', targetResolution))
|
|
1312
|
+
writeErr(line);
|
|
1219
1313
|
return 1;
|
|
1220
1314
|
}
|
|
1315
|
+
const target = targetResolution.target;
|
|
1316
|
+
if (targetResolution.via === 'alias')
|
|
1317
|
+
writeErr(formatTargetAliasNote('dz install', targetOpt, target));
|
|
1221
1318
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
1222
1319
|
// Step 1: npm install the package (installRunner is the CliIo test seam — unset in production)
|
|
1223
1320
|
write(`Installing ${pkg}...`);
|
|
@@ -1251,7 +1348,7 @@ async function cmdInstall(options, flags, cwd, write, installRunner) {
|
|
|
1251
1348
|
const root = roots[0];
|
|
1252
1349
|
// Step 3: Use dz init with the resolved skills root as source
|
|
1253
1350
|
const report = await runInit({
|
|
1254
|
-
target
|
|
1351
|
+
target,
|
|
1255
1352
|
skillsDir: root.dir,
|
|
1256
1353
|
projectRoot,
|
|
1257
1354
|
force: flags.has('force'),
|
|
@@ -1268,20 +1365,38 @@ async function cmdInstall(options, flags, cwd, write, installRunner) {
|
|
|
1268
1365
|
if (root.layout === 'npx-template' && root.hasCompanionAssets) {
|
|
1269
1366
|
write(` note: ${pkg} also ships commands/hooks/agents — \`npx -y ${pkg} init\` installs the full kit.`);
|
|
1270
1367
|
}
|
|
1368
|
+
// Skip-and-collect at install time (D1 / the report's D2 amendment): the offending
|
|
1369
|
+
// SKILL.md came out of the DOWNLOADED TARBALL, so the path is rendered relative to
|
|
1370
|
+
// the package root (a `node_modules/**` absolute path is not actionable) and the
|
|
1371
|
+
// message says whose defect it is. Exit 1 — a pack that shipped an unloadable skill
|
|
1372
|
+
// did not fully install.
|
|
1373
|
+
if (report.failures.length > 0) {
|
|
1374
|
+
writeErr(`dz install: ${pkg} ships ${report.failures.length} unparseable skill(s) —`);
|
|
1375
|
+
for (const line of formatSkillLoadFailures(report.failures, { relativeTo: pkgDir }))
|
|
1376
|
+
writeErr(line);
|
|
1377
|
+
writeErr('This is a defect in the package, not in your project.');
|
|
1378
|
+
writeErr(`Workaround: npx -y ${pkg} init`);
|
|
1379
|
+
return 1;
|
|
1380
|
+
}
|
|
1271
1381
|
return 0;
|
|
1272
1382
|
}
|
|
1273
|
-
function cmdCompose(options, cwd, write) {
|
|
1383
|
+
function cmdCompose(options, cwd, write, writeErr) {
|
|
1274
1384
|
const combo = options.get('_positional_0');
|
|
1275
1385
|
if (!combo) {
|
|
1276
1386
|
write('dz compose: preset combination required (e.g., dz compose devops+mcp+web3)');
|
|
1277
1387
|
return 1;
|
|
1278
1388
|
}
|
|
1279
1389
|
// --target is documented; honor it in the suggested install command (was hardcoded claude-code).
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1390
|
+
const targetOpt = options.get('target') ?? 'claude-code';
|
|
1391
|
+
const composeResolution = resolveTargetName(targetOpt);
|
|
1392
|
+
if (composeResolution.kind === 'unknown') {
|
|
1393
|
+
for (const line of formatTargetProblem('dz compose', composeResolution))
|
|
1394
|
+
writeErr(line);
|
|
1283
1395
|
return 1;
|
|
1284
1396
|
}
|
|
1397
|
+
const target = composeResolution.target;
|
|
1398
|
+
if (composeResolution.via === 'alias')
|
|
1399
|
+
writeErr(formatTargetAliasNote('dz compose', targetOpt, target));
|
|
1285
1400
|
const presetNames = combo.split('+').map((s) => s.trim());
|
|
1286
1401
|
const allSkills = new Set();
|
|
1287
1402
|
const resolved = [];
|
|
@@ -3479,12 +3594,21 @@ async function cmdBrain(options, flags, cwd, write, readStdin) {
|
|
|
3479
3594
|
write(BRAIN_USAGE);
|
|
3480
3595
|
return sub === undefined ? 0 : 1;
|
|
3481
3596
|
}
|
|
3482
|
-
async function cmdSetup(options, flags, cwd, write) {
|
|
3483
|
-
const
|
|
3484
|
-
if (!
|
|
3485
|
-
|
|
3597
|
+
async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
3598
|
+
const targetOpt = options.get('target');
|
|
3599
|
+
if (!targetOpt) {
|
|
3600
|
+
writeErr(`dz setup: --target required (${TARGET_NAMES_SORTED.join(', ')})`);
|
|
3486
3601
|
return 1;
|
|
3487
3602
|
}
|
|
3603
|
+
const setupResolution = resolveTargetName(targetOpt);
|
|
3604
|
+
if (setupResolution.kind === 'unknown') {
|
|
3605
|
+
for (const line of formatTargetProblem('dz setup', setupResolution))
|
|
3606
|
+
writeErr(line);
|
|
3607
|
+
return 1;
|
|
3608
|
+
}
|
|
3609
|
+
const target = setupResolution.target;
|
|
3610
|
+
if (setupResolution.via === 'alias')
|
|
3611
|
+
writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
|
|
3488
3612
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3489
3613
|
const presetName = options.get('preset');
|
|
3490
3614
|
write(`\n╔══════════════════════════════════════════════════════╗`);
|
|
@@ -3649,12 +3773,19 @@ function cmdRecommend(options, cwd, write) {
|
|
|
3649
3773
|
write(` Get it: dz init --target claude-code --select skill-advisor (or --preset meta)`);
|
|
3650
3774
|
return 0;
|
|
3651
3775
|
}
|
|
3652
|
-
function cmdUpgrade(options, flags, cwd, write) {
|
|
3776
|
+
function cmdUpgrade(options, flags, cwd, write, writeErr) {
|
|
3653
3777
|
const targetOpt = options.get('target') ?? 'claude-code';
|
|
3654
|
-
|
|
3655
|
-
|
|
3778
|
+
const upgradeResolution = resolveTargetName(targetOpt);
|
|
3779
|
+
if (upgradeResolution.kind === 'unknown') {
|
|
3780
|
+
for (const line of formatTargetProblem('dz upgrade', upgradeResolution))
|
|
3781
|
+
writeErr(line);
|
|
3656
3782
|
return 1;
|
|
3657
3783
|
}
|
|
3784
|
+
// The dir map is keyed by the RESOLVED name — keying it by the raw `--target` would
|
|
3785
|
+
// let an alias validate and then miss the map.
|
|
3786
|
+
const upgradeTarget = upgradeResolution.target;
|
|
3787
|
+
if (upgradeResolution.via === 'alias')
|
|
3788
|
+
writeErr(formatTargetAliasNote('dz upgrade', targetOpt, upgradeTarget));
|
|
3658
3789
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
3659
3790
|
const targetDirMap = {
|
|
3660
3791
|
'claude-code': '.claude/skills', codex: '.agents/skills', opencode: '.opencode/skills',
|
|
@@ -3662,9 +3793,9 @@ function cmdUpgrade(options, flags, cwd, write) {
|
|
|
3662
3793
|
'agents-md': 'AGENTS.md', cursor: '.cursor/rules', gemini: 'GEMINI.md',
|
|
3663
3794
|
windsurf: '.windsurf/rules',
|
|
3664
3795
|
};
|
|
3665
|
-
const mappedDir = targetDirMap[
|
|
3796
|
+
const mappedDir = targetDirMap[upgradeTarget];
|
|
3666
3797
|
if (mappedDir === undefined) {
|
|
3667
|
-
write(`dz upgrade: no skills directory mapping for target ${
|
|
3798
|
+
write(`dz upgrade: no skills directory mapping for target ${upgradeTarget}`);
|
|
3668
3799
|
return 1;
|
|
3669
3800
|
}
|
|
3670
3801
|
const targetDir = join(projectRoot, mappedDir);
|
|
@@ -3679,14 +3810,14 @@ function cmdUpgrade(options, flags, cwd, write) {
|
|
|
3679
3810
|
}
|
|
3680
3811
|
}
|
|
3681
3812
|
const report = checkUpgrades(targetDir, canonicalDirs);
|
|
3682
|
-
write(`\ndz upgrade — ${
|
|
3813
|
+
write(`\ndz upgrade — ${upgradeTarget} (${targetDir})`);
|
|
3683
3814
|
write(` Installed: ${report.installed} Needs update: ${report.needsUpdate} Up-to-date: ${report.upToDate} Custom: ${report.notInCanonical}\n`);
|
|
3684
3815
|
for (const check of report.skills) {
|
|
3685
3816
|
const icon = check.needsUpdate ? '△' : check.canonicalSize === undefined ? '?' : '✓';
|
|
3686
3817
|
write(` ${icon} ${check.id.padEnd(25)} ${check.reason}`);
|
|
3687
3818
|
}
|
|
3688
3819
|
if (report.needsUpdate > 0) {
|
|
3689
|
-
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${
|
|
3820
|
+
write(`\n${report.needsUpdate} skill(s) need update. Run: dz init --target ${upgradeTarget} --force`);
|
|
3690
3821
|
}
|
|
3691
3822
|
// ADR-001 (verify-apply-leg): verify what we just left on disk. A TAMPERED pack aborts.
|
|
3692
3823
|
const sigFatal = reportPackVerification(projectRoot, options.get('pubkey'), flags.has('require-signing'), write);
|
|
@@ -4239,7 +4370,7 @@ function cmdPublish(options, flags, cwd, write) {
|
|
|
4239
4370
|
/* dz parity — the honest feature×target map (target-parity-matrix, */
|
|
4240
4371
|
/* ADR-001): computed from the declarative model, never hand-written */
|
|
4241
4372
|
/* ------------------------------------------------------------------ */
|
|
4242
|
-
function cmdParity(options, flags, write) {
|
|
4373
|
+
function cmdParity(options, flags, write, writeErr) {
|
|
4243
4374
|
const json = flags.has('json');
|
|
4244
4375
|
if (flags.has('help')) {
|
|
4245
4376
|
write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
|
|
@@ -4268,13 +4399,31 @@ function cmdParity(options, flags, write) {
|
|
|
4268
4399
|
}
|
|
4269
4400
|
}
|
|
4270
4401
|
const matrix = buildParityMatrix();
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4402
|
+
// Site 8 of the D3 rewiring, closed in fix round 1 (QE F1). It shipped spelling its
|
|
4403
|
+
// own bare guard `TARGET_NAMES.includes(...)` and was therefore invisible to the AM-2
|
|
4404
|
+
// grep-guard, which searched for the token `isTargetName(` — a PRESENCE check on one
|
|
4405
|
+
// spelling where the property was "no call site bypasses the resolver". The guard in
|
|
4406
|
+
// `test/target-alias-cli.test.ts` now checks the class, and the sweep list is derived
|
|
4407
|
+
// from the help text so a ninth command cannot be missed the same way.
|
|
4408
|
+
const targetOpt = options.get('target');
|
|
4409
|
+
let target;
|
|
4410
|
+
if (targetOpt !== undefined) {
|
|
4411
|
+
const parityResolution = resolveTargetName(targetOpt);
|
|
4412
|
+
if (parityResolution.kind === 'unknown') {
|
|
4413
|
+
// Both forms go to stderr: an error is not data, and `dz parity --json | jq`
|
|
4414
|
+
// must not be fed a diagnostic (ADR-002 §Decision 2 / driver D6).
|
|
4415
|
+
if (json) {
|
|
4416
|
+
writeErr(JSON.stringify({ error: `unknown target ${JSON.stringify(targetOpt)}`, suggestion: parityResolution.suggestion, targets: TARGET_NAMES_SORTED, exitCode: 1 }, null, 2));
|
|
4417
|
+
}
|
|
4418
|
+
else {
|
|
4419
|
+
for (const line of formatTargetProblem('dz parity', parityResolution))
|
|
4420
|
+
writeErr(line);
|
|
4421
|
+
}
|
|
4422
|
+
return 1;
|
|
4423
|
+
}
|
|
4424
|
+
target = parityResolution.target;
|
|
4425
|
+
if (parityResolution.via === 'alias')
|
|
4426
|
+
writeErr(formatTargetAliasNote('dz parity', targetOpt, target));
|
|
4278
4427
|
}
|
|
4279
4428
|
if (json) {
|
|
4280
4429
|
const rows = matrix.map((r) => ({
|
|
@@ -5263,6 +5412,172 @@ function readDriftAllowlist(root) {
|
|
|
5263
5412
|
return [];
|
|
5264
5413
|
}
|
|
5265
5414
|
}
|
|
5415
|
+
/** Refresh or verify the root AGENTS.md bearing-policy projection. */
|
|
5416
|
+
/**
|
|
5417
|
+
* `dz hooks-sync --target codex` (`crossrt-2-codex-hooks`, AM-14).
|
|
5418
|
+
*
|
|
5419
|
+
* ONE verb in the existing target vocabulary (`parity`, `delivery-check`, `--target`), extensible to
|
|
5420
|
+
* a future runtime without a third surface. **No alias** — `dz codex-hooks` resolves to nothing.
|
|
5421
|
+
*
|
|
5422
|
+
* Exit map (ADR-002 §5, pinned by test):
|
|
5423
|
+
* 0 = `armed` AND `trust: 'trusted'` — the ONLY outcome that may print a success word (AM-17)
|
|
5424
|
+
* 1 = not armed, armed-but-trust-pending, drift, or a refusal
|
|
5425
|
+
* 3 = inconclusive (including "no codex binary on PATH")
|
|
5426
|
+
*/
|
|
5427
|
+
function cmdHooksSync(options, flags, cwd, write, writeErr) {
|
|
5428
|
+
const json = flags.has('json');
|
|
5429
|
+
const usage = 'dz hooks-sync --target codex [--check] [--verify] [--remove] [--json] [--project <dir>] [--no-verify]';
|
|
5430
|
+
if (flags.has('help')) {
|
|
5431
|
+
write(`${usage} — install/verify the dz veto + recall hooks in $CODEX_HOME/hooks.json`);
|
|
5432
|
+
return 0;
|
|
5433
|
+
}
|
|
5434
|
+
for (const flag of flags) {
|
|
5435
|
+
if (!['check', 'verify', 'no-verify', 'remove', 'json', 'help'].includes(flag)) {
|
|
5436
|
+
const message = `dz hooks-sync: unknown option --${flag}\n${usage}`;
|
|
5437
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : message);
|
|
5438
|
+
return 1;
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
for (const key of options.keys()) {
|
|
5442
|
+
if (key !== 'target' && key !== 'project' && key !== 'codex-home') {
|
|
5443
|
+
const message = key.startsWith('_positional_') ? `unexpected argument ${JSON.stringify(options.get(key))}` : `unknown option --${key}`;
|
|
5444
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5445
|
+
return 1;
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
// Every `--target` read in this CLI goes through resolveTargetName (alias support + one spelling
|
|
5449
|
+
// of the unknown-target message), pinned by `everyTargetGuardUsesResolveTargetName`.
|
|
5450
|
+
const targetOpt = options.get('target');
|
|
5451
|
+
if (targetOpt === undefined) {
|
|
5452
|
+
const message = '--target is required';
|
|
5453
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5454
|
+
return 1;
|
|
5455
|
+
}
|
|
5456
|
+
const resolution = resolveTargetName(targetOpt);
|
|
5457
|
+
if (resolution.kind === 'unknown') {
|
|
5458
|
+
if (json) {
|
|
5459
|
+
write(JSON.stringify({ error: `unknown target ${targetOpt}`, exitCode: 1 }));
|
|
5460
|
+
}
|
|
5461
|
+
else {
|
|
5462
|
+
for (const line of formatTargetProblem('dz hooks-sync', resolution))
|
|
5463
|
+
writeErr(line);
|
|
5464
|
+
}
|
|
5465
|
+
return 1;
|
|
5466
|
+
}
|
|
5467
|
+
const target = resolution.target;
|
|
5468
|
+
if (resolution.via === 'alias')
|
|
5469
|
+
writeErr(formatTargetAliasNote('dz hooks-sync', targetOpt, target));
|
|
5470
|
+
if (target !== 'codex') {
|
|
5471
|
+
// Deliberately narrow: only Codex has a hook carrier today. Naming the reason keeps a future
|
|
5472
|
+
// reader from assuming the other nine are simply unimplemented here.
|
|
5473
|
+
const message = `unsupported --target ${target} (only "codex" has a hook carrier today)`;
|
|
5474
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz hooks-sync: ${message}\n${usage}`);
|
|
5475
|
+
return 1;
|
|
5476
|
+
}
|
|
5477
|
+
const codexHome = options.get('codex-home');
|
|
5478
|
+
const report = runSyncCodexHooks({
|
|
5479
|
+
...(codexHome !== undefined ? { codexHome } : {}),
|
|
5480
|
+
check: flags.has('check'),
|
|
5481
|
+
remove: flags.has('remove'),
|
|
5482
|
+
});
|
|
5483
|
+
if (json) {
|
|
5484
|
+
write(JSON.stringify({ ...report, exitCode: report.exitCode }));
|
|
5485
|
+
return report.exitCode;
|
|
5486
|
+
}
|
|
5487
|
+
for (const err of report.errors)
|
|
5488
|
+
writeErr(`dz hooks-sync: ${err}`);
|
|
5489
|
+
for (const warn of report.warnings)
|
|
5490
|
+
writeErr(`dz hooks-sync: warning: ${warn}`);
|
|
5491
|
+
// SILENT in a home that never opted in — the leg-1 F12 lesson: a --check that chatters in every
|
|
5492
|
+
// unrelated project trains its reader to ignore it.
|
|
5493
|
+
if (flags.has('check') && !report.installed && report.errors.length === 0)
|
|
5494
|
+
return report.exitCode;
|
|
5495
|
+
if (flags.has('remove')) {
|
|
5496
|
+
write(`dz hooks-sync: removed ${report.removed} managed entr(ies) from ${report.registryPath}`);
|
|
5497
|
+
return report.exitCode;
|
|
5498
|
+
}
|
|
5499
|
+
// AM-17 / G-G: the success word is reachable ONLY from armed AND trusted. The trust clause is
|
|
5500
|
+
// asserted HERE as well as in the exit map — a read-only `--check` that could not establish trust
|
|
5501
|
+
// once returned exit 0 with `trust: 'unknown'`, and this line printed "ready" for it.
|
|
5502
|
+
if (report.exitCode === 0 && report.trust === 'trusted' && report.installed) {
|
|
5503
|
+
write(`dz hooks-sync: codex hooks installed and ARMED (trust: ${report.trust}) — ready`);
|
|
5504
|
+
}
|
|
5505
|
+
else if (report.installed) {
|
|
5506
|
+
writeErr(`dz hooks-sync: installed, NOT verified — ARMED = NO (trust: ${report.trust}, executable: ${report.executable})`);
|
|
5507
|
+
writeErr('→ open an interactive Codex session in this directory and approve the two dz hooks, then re-run with --verify');
|
|
5508
|
+
}
|
|
5509
|
+
else {
|
|
5510
|
+
writeErr('dz hooks-sync: ARMED = NO — the managed entries are not present in the registry');
|
|
5511
|
+
}
|
|
5512
|
+
return report.exitCode;
|
|
5513
|
+
}
|
|
5514
|
+
function cmdAgentsSync(options, flags, cwd, write, writeErr) {
|
|
5515
|
+
const json = flags.has('json');
|
|
5516
|
+
const usage = 'dz agents-sync [--project <dir>] [--check] [--json]';
|
|
5517
|
+
if (flags.has('help')) {
|
|
5518
|
+
write(`${usage} — sync/verify the dz:policies fence in root AGENTS.md`);
|
|
5519
|
+
return 0;
|
|
5520
|
+
}
|
|
5521
|
+
for (const flag of flags) {
|
|
5522
|
+
if (!['check', 'json', 'help'].includes(flag)) {
|
|
5523
|
+
const message = `dz agents-sync: unknown option --${flag}\n${usage}`;
|
|
5524
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : message);
|
|
5525
|
+
return 1;
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
for (const key of options.keys()) {
|
|
5529
|
+
if (key !== 'project') {
|
|
5530
|
+
const message = key.startsWith('_positional_') ? `unexpected argument ${JSON.stringify(options.get(key))}` : `unknown option --${key}`;
|
|
5531
|
+
(json ? write : writeErr)(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz agents-sync: ${message}\n${usage}`);
|
|
5532
|
+
return 1;
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
5536
|
+
try {
|
|
5537
|
+
const report = runSyncAgentsPolicy({ projectRoot: root, check: flags.has('check') });
|
|
5538
|
+
const drifted = report.drift.filter((finding) => finding.status !== 'ok');
|
|
5539
|
+
const inconclusive = report.missing.length > 0;
|
|
5540
|
+
const failed = flags.has('check')
|
|
5541
|
+
? report.changed || report.budget.overflow || drifted.length > 0
|
|
5542
|
+
: !report.inSync;
|
|
5543
|
+
const exitCode = inconclusive ? 3 : failed ? 1 : 0;
|
|
5544
|
+
if (json) {
|
|
5545
|
+
write(JSON.stringify({ ...report, sections: report.blocks, exitCode }));
|
|
5546
|
+
return exitCode;
|
|
5547
|
+
}
|
|
5548
|
+
if (inconclusive) {
|
|
5549
|
+
writeErr(`dz agents-sync: INCONCLUSIVE — unreadable or unanchored policy source(s): ${report.missing.join(', ')}`);
|
|
5550
|
+
writeErr('→ heal with: restore the named source anchors, then run dz agents-sync');
|
|
5551
|
+
return 3;
|
|
5552
|
+
}
|
|
5553
|
+
if (failed) {
|
|
5554
|
+
const effect = flags.has('check') ? 'AGENTS.md would change' : 'AGENTS.md was not rewritten';
|
|
5555
|
+
writeErr(`dz agents-sync: DRIFT — ${drifted.length} stale/missing section(s); ${effect}`);
|
|
5556
|
+
for (const finding of drifted)
|
|
5557
|
+
writeErr(` ${finding.id}: ${finding.file} (${finding.status})`);
|
|
5558
|
+
if (drifted.some((finding) => finding.id === 'dz:policies')) {
|
|
5559
|
+
writeErr('→ heal with: repair duplicate/unmatched dz:policies markers, then run dz agents-sync');
|
|
5560
|
+
}
|
|
5561
|
+
else {
|
|
5562
|
+
writeErr('→ heal with: dz agents-sync');
|
|
5563
|
+
}
|
|
5564
|
+
return 1;
|
|
5565
|
+
}
|
|
5566
|
+
const verb = report.written ? 'wrote' : 'in sync';
|
|
5567
|
+
write(`dz agents-sync: ${verb} — ${report.blocks.length} policy section(s), ${report.budget.bytes} bytes (${report.budget.pct}% of ${report.budget.cap})`);
|
|
5568
|
+
for (const warning of report.warnings)
|
|
5569
|
+
writeErr(`dz agents-sync: warning: ${warning}`);
|
|
5570
|
+
return 0;
|
|
5571
|
+
}
|
|
5572
|
+
catch (error) {
|
|
5573
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5574
|
+
if (json)
|
|
5575
|
+
write(JSON.stringify({ error: message, exitCode: 1 }));
|
|
5576
|
+
else
|
|
5577
|
+
writeErr(`dz agents-sync: ${message}`);
|
|
5578
|
+
return 1;
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5266
5581
|
function cmdDriftCheck(options, flags, cwd, write) {
|
|
5267
5582
|
const root = resolve(cwd, options.get('project') ?? '.');
|
|
5268
5583
|
// Default scope = PUBLISHED packages only: the `.claude/skills` dogfood copies legitimately lag the
|
|
@@ -5370,6 +5685,38 @@ function gatherReadmeCounts(root) {
|
|
|
5370
5685
|
function gatherGuardFacts(op, root, text, storeCap) {
|
|
5371
5686
|
const facts = { op };
|
|
5372
5687
|
if (op === 'publish') {
|
|
5688
|
+
// agents-md-policy-sync: fixed registry, no tree walk. The pure detector
|
|
5689
|
+
// recomputes every expected hash from current source text; this gatherer
|
|
5690
|
+
// only supplies bytes. Any unexpected gather failure omits the fact, and
|
|
5691
|
+
// evaluateGuard records that advisory coverage gap in `notes`.
|
|
5692
|
+
try {
|
|
5693
|
+
const policyFiles = new Map();
|
|
5694
|
+
for (const file of new Set(POLICY_SOURCES.map((source) => source.file))) {
|
|
5695
|
+
try {
|
|
5696
|
+
policyFiles.set(file, readFileSync(join(root, file), 'utf8'));
|
|
5697
|
+
}
|
|
5698
|
+
catch {
|
|
5699
|
+
policyFiles.set(file, null);
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
let agentsMd = null;
|
|
5703
|
+
try {
|
|
5704
|
+
agentsMd = readFileSync(join(root, 'AGENTS.md'), 'utf8');
|
|
5705
|
+
}
|
|
5706
|
+
catch { /* missing stamp evidence */ }
|
|
5707
|
+
const policyDrift = detectPolicyDrift(policyFiles, agentsMd, POLICY_SOURCES);
|
|
5708
|
+
facts['policyDrift'] = {
|
|
5709
|
+
applicable: policyDrift.applicable,
|
|
5710
|
+
// Did this repo OPT IN? A `dz:policies` fence in AGENTS.md is the only durable signal that
|
|
5711
|
+
// someone ran `dz agents-sync` here. Without it the advisory rule is out of scope and stays
|
|
5712
|
+
// silent; with it, unreadable sources become a loud note instead of a silent skip.
|
|
5713
|
+
fenced: hasPolicyFence(agentsMd),
|
|
5714
|
+
drifted: policyDrift.findings
|
|
5715
|
+
.filter((finding) => finding.status !== 'ok')
|
|
5716
|
+
.map((finding) => `${finding.id}:${finding.file}:${finding.status}`),
|
|
5717
|
+
};
|
|
5718
|
+
}
|
|
5719
|
+
catch { /* unexpected gather failure — omission becomes a visible guard note */ }
|
|
5373
5720
|
const manifests = [];
|
|
5374
5721
|
const located = [];
|
|
5375
5722
|
try {
|
|
@@ -6484,7 +6831,7 @@ async function cmdRetro(options, flags, cwd, write) {
|
|
|
6484
6831
|
* --from-spec <spec.json> preview the scaffold (create / augment per file); the SKILL fills the spec
|
|
6485
6832
|
* --apply with --from-spec: WRITE (create missing, AUGMENT existing — never clobber)
|
|
6486
6833
|
*/
|
|
6487
|
-
function cmdFeatureAdrSetup(options, flags, cwd, write) {
|
|
6834
|
+
function cmdFeatureAdrSetup(options, flags, cwd, write, writeErr) {
|
|
6488
6835
|
let repoRoot = cwd;
|
|
6489
6836
|
try {
|
|
6490
6837
|
repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd;
|
|
@@ -6504,11 +6851,25 @@ function cmdFeatureAdrSetup(options, flags, cwd, write) {
|
|
|
6504
6851
|
// Its "runnable here" list is computed for --target (default agents-md, the AGENTS.md-class target class).
|
|
6505
6852
|
const wantGates = flags.has('gates');
|
|
6506
6853
|
const targetOpt = options.get('target');
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6854
|
+
// Sites 7 AND 8 of the D3 rewiring. Site 8 (the coercion below) is NOT a guard — it
|
|
6855
|
+
// is a silent fallback, and a mechanical "replace isTargetName with resolveTargetName"
|
|
6856
|
+
// pass would miss it. With aliasing in place and the coercion left alone,
|
|
6857
|
+
// `dz feature-adr-setup --gates --target claude` would pass validation and then emit
|
|
6858
|
+
// for **agents-md**. So the coercion CONSUMES the resolution computed once, above;
|
|
6859
|
+
// `agents-md` is the default only when `--target` is ABSENT.
|
|
6860
|
+
let gatesTarget = 'agents-md';
|
|
6861
|
+
if (targetOpt !== undefined) {
|
|
6862
|
+
const gatesResolution = resolveTargetName(targetOpt);
|
|
6863
|
+
if (gatesResolution.kind === 'unknown') {
|
|
6864
|
+
for (const line of formatTargetProblem('dz feature-adr-setup', gatesResolution))
|
|
6865
|
+
writeErr(line);
|
|
6866
|
+
return 1;
|
|
6867
|
+
}
|
|
6868
|
+
gatesTarget = gatesResolution.target;
|
|
6869
|
+
if (gatesResolution.via === 'alias') {
|
|
6870
|
+
writeErr(formatTargetAliasNote('dz feature-adr-setup', targetOpt, gatesTarget));
|
|
6871
|
+
}
|
|
6510
6872
|
}
|
|
6511
|
-
const gatesTarget = isTargetName(targetOpt ?? '') ? targetOpt : 'agents-md';
|
|
6512
6873
|
const specPath = options.get('from-spec');
|
|
6513
6874
|
if (specPath === undefined && !wantGuards && !wantGates) {
|
|
6514
6875
|
// default + --plan: the read-only "which documents, and where?" answer.
|
|
@@ -6642,7 +7003,15 @@ function cmdChallenge(options, flags, cwd, write) {
|
|
|
6642
7003
|
* --base <ref> the base ref to fail against (default HEAD)
|
|
6643
7004
|
* --name '<filter>' optional -t test-name filter applied to every target
|
|
6644
7005
|
* --runner '<cmd>' test runner (default `npx vitest run`)
|
|
6645
|
-
* --
|
|
7006
|
+
* --timeout <ms> per-run timeout (default 300000; a timed-out run is CANNOT_ISOLATE)
|
|
7007
|
+
* --json machine-readable {plan, results, tipTree, perTest, aggregate,
|
|
7008
|
+
* findings, measurementValid, primaryAction}
|
|
7009
|
+
*
|
|
7010
|
+
* This executor is THIN by design (house style: pure classifier + thin executor). It performs exactly the
|
|
7011
|
+
* I/O the pure gate cannot — stat, worktree, run, capture — and hands OBSERVATIONS back. It no longer
|
|
7012
|
+
* interprets anything: the pre-epoch load-error regex that lived here (`/cannot find module|failed to
|
|
7013
|
+
* load|.../i`) is DELETED, because a regex over a runner's stderr, written in the executor, is exactly the
|
|
7014
|
+
* probabilistic channel that minted `DISCRIMINATES` for `--runner false`.
|
|
6646
7015
|
*
|
|
6647
7016
|
* NEVER auto-aborts: a non-discriminating (false-green) test is reported as a HIGH finding for the owner to
|
|
6648
7017
|
* decide (dz's rule — a false gate kills trust). Exit code is 0 on a clean run regardless of verdict; 2 only on
|
|
@@ -6663,6 +7032,9 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
6663
7032
|
const propertyTests = testArg.split(',').map((s) => s.trim()).filter(Boolean).map((file) => nameFilter !== undefined && nameFilter.trim() !== '' ? { file, name: nameFilter.trim() } : { file });
|
|
6664
7033
|
const baseRef = options.get('base') ?? 'HEAD';
|
|
6665
7034
|
const runnerOpt = options.get('runner');
|
|
7035
|
+
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
7036
|
+
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
7037
|
+
const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
|
|
6666
7038
|
const plan = planDiscriminationCheck(runnerOpt !== undefined ? { baseRef, propertyTests, runner: runnerOpt } : { baseRef, propertyTests });
|
|
6667
7039
|
if (!plan.runnable) {
|
|
6668
7040
|
// No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
|
|
@@ -6677,113 +7049,218 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
6677
7049
|
write(` → ${result.finding?.detail ?? 'no property test to check'}`);
|
|
6678
7050
|
return 0;
|
|
6679
7051
|
}
|
|
6680
|
-
//
|
|
6681
|
-
//
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
const
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
7052
|
+
// ── (1) stat + isFile, BEFORE the worktree (AM-6 / FR-A1) ──────────────────────────────────
|
|
7053
|
+
// R12: `stat` FOLLOWS symlinks on purpose. A dangling symlink lstat-exists but has no readable
|
|
7054
|
+
// content — that IS absence of the named check. A directory stat-exists but is not a regular
|
|
7055
|
+
// file. Pre-epoch both reached the copy step, threw, and were caught into `outcome:'error'`,
|
|
7056
|
+
// which minted the near-pass DISCRIMINATES_VIA_ERROR (MEASURED — acid A1 / the dangling-symlink
|
|
7057
|
+
// and directory rows of features/wave1-instrument-repair/07_code_changes/acid-red-runs.md).
|
|
7058
|
+
const absent = [];
|
|
7059
|
+
const present = [];
|
|
7060
|
+
for (const t of plan.targets) {
|
|
7061
|
+
let isRegular = false;
|
|
7062
|
+
let isDirectory = false;
|
|
7063
|
+
try {
|
|
7064
|
+
const st = statSync(resolve(repoRoot, t.file));
|
|
7065
|
+
isRegular = st.isFile();
|
|
7066
|
+
isDirectory = st.isDirectory();
|
|
7067
|
+
}
|
|
7068
|
+
catch { /* ENOENT / dangling symlink / permission — absence, either way */ }
|
|
7069
|
+
if (isRegular) {
|
|
7070
|
+
present.push(t.name !== undefined ? { file: t.file, name: t.name } : { file: t.file });
|
|
7071
|
+
continue;
|
|
7072
|
+
}
|
|
7073
|
+
const row = t.name !== undefined ? { file: t.file, name: t.name, outcome: 'absent' } : { file: t.file, outcome: 'absent' };
|
|
7074
|
+
// Out-of-band detail channel: TEST_FILE_ABSENT is evidence-EXEMPT (its evidence is the stat
|
|
7075
|
+
// itself), and absent rows never consult the evidence gate — so this object cannot degrade the
|
|
7076
|
+
// row. It exists only so the finding can tell the operator WHY the path is not a test file.
|
|
7077
|
+
absent.push(isDirectory
|
|
7078
|
+
? { ...row, evidence: { exitCode: null, runner: 'unrecognised', failureKind: 'unrecognised', testsExecuted: null, targetSeen: false, evidenceLine: 'not-a-regular-file' } }
|
|
7079
|
+
: row);
|
|
7080
|
+
}
|
|
7081
|
+
const results = [...absent];
|
|
7082
|
+
let tipTree = null;
|
|
7083
|
+
// Confirmation 12: with nothing present there is nothing to run — no worktree is built at all.
|
|
7084
|
+
if (present.length > 0) {
|
|
7085
|
+
const runner = runnerOpt !== undefined && plan.commands.some((c) => c.includes(runnerOpt)) ? runnerOpt : 'npx vitest run';
|
|
7086
|
+
// Execute the plan in a temp worktree WE own; substitute {{WORKTREE}} and always clean up.
|
|
7087
|
+
// `git worktree add` must CREATE the path, so compute a fresh non-existent one (do NOT mkdtemp it).
|
|
7088
|
+
const worktree = join(mkdtempSync(join(tmpdir(), 'dz-disc-')), 'wt');
|
|
7089
|
+
try {
|
|
7090
|
+
// 1) add the detached worktree at base (git creates `worktree`; its parent already exists).
|
|
7091
|
+
const addCmd = plan.commands[0].replace(/\{\{WORKTREE\}\}/g, worktree);
|
|
7092
|
+
execSync(addCmd, { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
7093
|
+
// 1b) a fresh worktree has NO node_modules — without this, every test fails to load (runner + deps
|
|
7094
|
+
// unresolvable) and the gate collapses to always-VIA_ERROR, blind to false greens. Absolute-path
|
|
7095
|
+
// symlinks point back at the main checkout's already-installed trees, robust across pnpm's layout.
|
|
7096
|
+
const linkNodeModules = (relDir) => {
|
|
7097
|
+
const srcNm = join(repoRoot, relDir, 'node_modules');
|
|
7098
|
+
if (!existsSync(srcNm))
|
|
7099
|
+
return;
|
|
7100
|
+
const dstNm = join(worktree, relDir, 'node_modules');
|
|
7101
|
+
if (existsSync(dstNm))
|
|
7102
|
+
return;
|
|
7103
|
+
try {
|
|
7104
|
+
mkdirSync(dirname(dstNm), { recursive: true });
|
|
7105
|
+
symlinkSync(srcNm, dstNm, 'dir');
|
|
7106
|
+
}
|
|
7107
|
+
catch { /* best effort */ }
|
|
7108
|
+
};
|
|
7109
|
+
linkNodeModules('.'); // root (hoisted deps + .bin)
|
|
7110
|
+
const pkgDirs = new Set();
|
|
7111
|
+
for (const t of present) {
|
|
7112
|
+
let d = dirname(t.file);
|
|
7113
|
+
while (d && d !== '.' && d !== sep) {
|
|
7114
|
+
if (existsSync(join(repoRoot, d, 'package.json'))) {
|
|
7115
|
+
pkgDirs.add(d);
|
|
7116
|
+
break;
|
|
7117
|
+
}
|
|
7118
|
+
d = dirname(d);
|
|
6712
7119
|
}
|
|
6713
|
-
d = dirname(d);
|
|
6714
7120
|
}
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
7121
|
+
for (const d of pkgDirs)
|
|
7122
|
+
linkNodeModules(d);
|
|
7123
|
+
// 2) copy each property test into the base worktree, then 3) run it and record the OBSERVATION.
|
|
7124
|
+
for (const t of present) {
|
|
7125
|
+
try {
|
|
7126
|
+
const src = resolve(repoRoot, t.file);
|
|
7127
|
+
// containment guard (defense in depth beyond planDiscriminationCheck's path sanitation).
|
|
7128
|
+
if (!resolve(src).startsWith(resolve(repoRoot) + sep)) {
|
|
7129
|
+
results.push(nameFor(t, 'error'));
|
|
7130
|
+
continue;
|
|
7131
|
+
}
|
|
7132
|
+
const dst = join(worktree, t.file);
|
|
7133
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
7134
|
+
writeFileSync(dst, readFileSync(src));
|
|
7135
|
+
}
|
|
7136
|
+
catch {
|
|
7137
|
+
// the file STAT-PASSED and the copy still failed: degrade LOUDLY as an error with NO
|
|
7138
|
+
// evidence (the gate reads it as CANNOT_ISOLATE), never as absence and never as a pass.
|
|
6724
7139
|
results.push(nameFor(t, 'error'));
|
|
6725
7140
|
continue;
|
|
6726
7141
|
}
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
7142
|
+
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
7143
|
+
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
7144
|
+
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
7145
|
+
const cmd = `${runner}${nameArg} -- '${t.file}'`;
|
|
7146
|
+
const base = runCapturedTest(cmd, worktree, timeoutMs);
|
|
7147
|
+
const evidence = classifyExecutionEvidence(base.output, base.exitCode, t.file);
|
|
7148
|
+
const outcome = discriminationOutcomeOf(base.exitCode, evidence);
|
|
7149
|
+
const row = t.name !== undefined
|
|
7150
|
+
? { file: t.file, name: t.name, outcome, evidence }
|
|
7151
|
+
: { file: t.file, outcome, evidence };
|
|
7152
|
+
// 4) TIP CONTROL (FR-A2 + Confirmation 17). Run it for ALL non-assertion redness — file-load
|
|
7153
|
+
// redness (the matrix's EVIDENCED-error rows) AND unrecognised redness (so the invocation
|
|
7154
|
+
// ledger can prove the tip was REACHED). The CLASSIFIER still ignores the tip for unevidenced
|
|
7155
|
+
// base rows per the matrix; running it is cheap and only ever on an already-broken path.
|
|
7156
|
+
// Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
|
|
7157
|
+
if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
|
|
7158
|
+
const tip = runCapturedTest(cmd, repoRoot, timeoutMs);
|
|
7159
|
+
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, t.file);
|
|
7160
|
+
row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
|
|
7161
|
+
row['tipEvidence'] = tipEvidence;
|
|
7162
|
+
// R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
|
|
7163
|
+
// tree, where a concurrent writer can flip the observation mid-gate. No lock is taken
|
|
7164
|
+
// (deferred to backlog 9520e506); instead every tip-derived reading carries the tree
|
|
7165
|
+
// CONDITIONS it was taken under, so a surprising verdict can be re-read against them.
|
|
7166
|
+
if (tipTree === null)
|
|
7167
|
+
tipTree = readTipTreeConditions(repoRoot);
|
|
7168
|
+
}
|
|
7169
|
+
results.push(row);
|
|
6730
7170
|
}
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
7171
|
+
}
|
|
7172
|
+
catch (e) {
|
|
7173
|
+
if (flags.has('json')) {
|
|
7174
|
+
write(JSON.stringify({ plan, error: 'worktree-setup-failed', detail: String(e.message).slice(0, 300) }, null, 2));
|
|
6734
7175
|
}
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
7176
|
+
else
|
|
7177
|
+
write(`discrimination-check: could not create worktree at ${baseRef}: ${String(e.message).slice(0, 200)}`);
|
|
7178
|
+
return 2;
|
|
7179
|
+
}
|
|
7180
|
+
finally {
|
|
6739
7181
|
try {
|
|
6740
|
-
execSync(
|
|
6741
|
-
results.push(nameFor(t, 'pass')); // exit 0 → test PASSED at base → false green
|
|
7182
|
+
execSync(`git worktree remove --force ${worktree}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
6742
7183
|
}
|
|
6743
|
-
catch
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
const isLoadError = /cannot find module|failed to load|no test (files )?found|error: cannot|transform failed|esbuild/i.test(out);
|
|
6748
|
-
results.push(nameFor(t, isLoadError ? 'error' : 'fail'));
|
|
7184
|
+
catch { /* fall through to rm */ }
|
|
7185
|
+
// remove the whole mkdtemp parent (worktree is `<mkdtemp>/wt`), so nothing leaks under tmp even on error.
|
|
7186
|
+
try {
|
|
7187
|
+
rmSync(dirname(worktree), { recursive: true, force: true });
|
|
6749
7188
|
}
|
|
7189
|
+
catch { /* best effort */ }
|
|
7190
|
+
try {
|
|
7191
|
+
execSync('git worktree prune', { cwd: repoRoot, stdio: 'pipe' });
|
|
7192
|
+
}
|
|
7193
|
+
catch { /* best effort */ }
|
|
6750
7194
|
}
|
|
6751
7195
|
}
|
|
6752
|
-
catch (e) {
|
|
6753
|
-
if (flags.has('json')) {
|
|
6754
|
-
write(JSON.stringify({ plan, error: 'worktree-setup-failed', detail: String(e.message).slice(0, 300) }, null, 2));
|
|
6755
|
-
}
|
|
6756
|
-
else
|
|
6757
|
-
write(`discrimination-check: could not create worktree at ${baseRef}: ${String(e.message).slice(0, 200)}`);
|
|
6758
|
-
return 2;
|
|
6759
|
-
}
|
|
6760
|
-
finally {
|
|
6761
|
-
try {
|
|
6762
|
-
execSync(`git worktree remove --force ${worktree}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
6763
|
-
}
|
|
6764
|
-
catch { /* fall through to rm */ }
|
|
6765
|
-
// remove the whole mkdtemp parent (worktree is `<mkdtemp>/wt`), so nothing leaks under tmp even on error.
|
|
6766
|
-
try {
|
|
6767
|
-
rmSync(dirname(worktree), { recursive: true, force: true });
|
|
6768
|
-
}
|
|
6769
|
-
catch { /* best effort */ }
|
|
6770
|
-
try {
|
|
6771
|
-
execSync('git worktree prune', { cwd: repoRoot, stdio: 'pipe' });
|
|
6772
|
-
}
|
|
6773
|
-
catch { /* best effort */ }
|
|
6774
|
-
}
|
|
6775
7196
|
const result = classifyDiscrimination({ propertyTests, results });
|
|
6776
7197
|
if (flags.has('json')) {
|
|
6777
|
-
write(JSON.stringify({ plan, results, ...result }, null, 2));
|
|
7198
|
+
write(JSON.stringify({ plan, results, tipTree, ...result }, null, 2));
|
|
6778
7199
|
return 0;
|
|
6779
7200
|
}
|
|
6780
7201
|
write(`discrimination-check @ ${baseRef} — verdict: ${result.aggregate}`);
|
|
6781
|
-
for (const p of result.perTest)
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
7202
|
+
for (const p of result.perTest) {
|
|
7203
|
+
// FR-A6: ✓ is reserved for the two ESTABLISHED trust verdicts. Every other value — including
|
|
7204
|
+
// every degraded reading — renders ✗, because a ✗ the operator investigates beats a ✓ that
|
|
7205
|
+
// silently meant "we could not tell".
|
|
7206
|
+
const mark = p.verdict === 'DISCRIMINATES' || p.verdict === 'DISCRIMINATES_VIA_ERROR' ? '✓' : '✗';
|
|
7207
|
+
write(` ${mark} ${p.file}${p.name ? ` (${p.name})` : ''}: ${p.verdict}${p.reason ? ` (reason: ${p.reason})` : ''}`);
|
|
7208
|
+
}
|
|
7209
|
+
write(` measurementValid: ${String(result.measurementValid)} · primaryAction: ${result.primaryAction}`);
|
|
7210
|
+
// ALL findings print, not just the worst: the scalar aggregate names one state, and a corpus with
|
|
7211
|
+
// a false green AND an absent file has two problems, each with its own operator action.
|
|
7212
|
+
for (const f of result.findings)
|
|
7213
|
+
write(`\n [${f.severity}] ${f.title}\n ${f.detail}`);
|
|
6785
7214
|
return 0;
|
|
6786
7215
|
}
|
|
7216
|
+
/**
|
|
7217
|
+
* Run one test command and CAPTURE the observation — output plus the exit code, including the
|
|
7218
|
+
* "no exit code at all" case. `execSync`'s timeout kills the child via signal and leaves
|
|
7219
|
+
* `status` null; a spawn failure does the same. That null is not an error to swallow, it is the
|
|
7220
|
+
* evidence (`CANNOT_ISOLATE` reason `'timeout'`), so it is returned as data.
|
|
7221
|
+
*/
|
|
7222
|
+
function runCapturedTest(cmd, cwd, timeoutMs) {
|
|
7223
|
+
try {
|
|
7224
|
+
const stdout = execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf-8', timeout: timeoutMs });
|
|
7225
|
+
return { output: String(stdout ?? ''), exitCode: 0 };
|
|
7226
|
+
}
|
|
7227
|
+
catch (e) {
|
|
7228
|
+
const err = e;
|
|
7229
|
+
return {
|
|
7230
|
+
output: String(err.stdout ?? '') + String(err.stderr ?? ''),
|
|
7231
|
+
exitCode: typeof err.status === 'number' ? err.status : null,
|
|
7232
|
+
};
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
/**
|
|
7236
|
+
* The outcome VALUE for one captured run. The executor's whole remaining judgment, and it is
|
|
7237
|
+
* mechanical: exit 0 is a pass, no exit code is an error, and a non-zero exit is an error only when
|
|
7238
|
+
* the classifier RECOGNISED a file-load failure. An unrecognised red is deliberately recorded as a
|
|
7239
|
+
* `fail` VALUE whose evidence then degrades it — exactly acid A6's pinned shape, and the reason the
|
|
7240
|
+
* executor no longer owns a regex.
|
|
7241
|
+
*/
|
|
7242
|
+
function discriminationOutcomeOf(exitCode, evidence) {
|
|
7243
|
+
if (exitCode === null)
|
|
7244
|
+
return 'error';
|
|
7245
|
+
if (exitCode === 0)
|
|
7246
|
+
return 'pass';
|
|
7247
|
+
return evidence.failureKind === 'file-load' ? 'error' : 'fail';
|
|
7248
|
+
}
|
|
7249
|
+
/** The live tree's identity at tip-run time (R15). Best-effort: unknown conditions read as such. */
|
|
7250
|
+
function readTipTreeConditions(repoRoot) {
|
|
7251
|
+
let headSha = 'unknown';
|
|
7252
|
+
let dirtyFiles = -1;
|
|
7253
|
+
try {
|
|
7254
|
+
headSha = execSync('git rev-parse HEAD', { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
7255
|
+
}
|
|
7256
|
+
catch { /* best effort */ }
|
|
7257
|
+
try {
|
|
7258
|
+
const porcelain = execSync('git status --porcelain', { cwd: repoRoot, stdio: 'pipe', encoding: 'utf-8' });
|
|
7259
|
+
dirtyFiles = String(porcelain).split('\n').filter((l) => l.trim() !== '').length;
|
|
7260
|
+
}
|
|
7261
|
+
catch { /* best effort */ }
|
|
7262
|
+
return { headSha, dirtyFiles };
|
|
7263
|
+
}
|
|
6787
7264
|
/** small helper: build a result row, omitting `name` when absent (exactOptionalPropertyTypes). */
|
|
6788
7265
|
function nameFor(t, outcome) {
|
|
6789
7266
|
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
@@ -7926,7 +8403,7 @@ const PROBE_SCRUB_ENV = [
|
|
|
7926
8403
|
* lands, then kill it — the model never answers, so the probe costs ~no tokens. Never throws:
|
|
7927
8404
|
* every failure becomes an `error` string, which the pure classifier turns into `inconclusive`.
|
|
7928
8405
|
*/
|
|
7929
|
-
function probeInitStream(projectDir, timeoutMs) {
|
|
8406
|
+
function probeInitStream(projectDir, timeoutMs, pluginDir = null) {
|
|
7930
8407
|
return new Promise((resolveProbe) => {
|
|
7931
8408
|
const env = { ...process.env };
|
|
7932
8409
|
for (const key of PROBE_SCRUB_ENV)
|
|
@@ -7952,7 +8429,13 @@ function probeInitStream(projectDir, timeoutMs) {
|
|
|
7952
8429
|
const timer = setTimeout(() => finish(`no init event within ${Math.round(timeoutMs / 1000)}s (is \`claude\` logged in?)`), timeoutMs);
|
|
7953
8430
|
try {
|
|
7954
8431
|
// NOT `--bare`: that mode skips plugin credentials and fails with "Not logged in".
|
|
7955
|
-
|
|
8432
|
+
const args = ['-p', 'ok', '--output-format', 'stream-json', '--verbose'];
|
|
8433
|
+
// Session-scoped plugin load — the marketplace-free vehicle (ADR-003 D-3). Without this the
|
|
8434
|
+
// probe reads a session in which the plugin was never loaded, and reports its commands
|
|
8435
|
+
// missing for a reason that has nothing to do with the package under test.
|
|
8436
|
+
if (pluginDir !== null)
|
|
8437
|
+
args.push('--plugin-dir', pluginDir);
|
|
8438
|
+
child = spawn('claude', args, {
|
|
7956
8439
|
cwd: projectDir,
|
|
7957
8440
|
env,
|
|
7958
8441
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -8026,8 +8509,11 @@ function probeContent(projectDir, prompt, timeoutMs) {
|
|
|
8026
8509
|
async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
8027
8510
|
const json = flags.has('json');
|
|
8028
8511
|
if (flags.has('help')) {
|
|
8029
|
-
write('dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json]');
|
|
8512
|
+
write('dz skills-verify [--dir <project>] [--expect a,b] [--expect-commands a,b] [--plugin-dir <dir>] [--static] [--strict] [--timeout <s>] [--json]');
|
|
8030
8513
|
write(' Verifies that a project\'s .claude/skills/ actually register in Claude Code.');
|
|
8514
|
+
write(' --plugin-dir <dir> load a plugin into the probe session (session-scoped, no marketplace);');
|
|
8515
|
+
write(' with no --expect-commands, the expectation defaults to the manifest\'s own commands[]');
|
|
8516
|
+
write(' --expect-commands slash commands that MUST appear in the session listing, e.g. loop-designer:init');
|
|
8031
8517
|
write(' --static layout scan only (no Claude session, CI-safe): flags dirs that can never register');
|
|
8032
8518
|
write(' --live-content ADVISORY extra turn: ask a live model to name the skills and quote one, proving');
|
|
8033
8519
|
write(' the CONTENT is usable — registration is not usability. Never changes the exit code.');
|
|
@@ -8036,8 +8522,8 @@ async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
|
8036
8522
|
return 0;
|
|
8037
8523
|
}
|
|
8038
8524
|
const allowedFlags = new Set(['json', 'help', 'static', 'strict', 'live-content']);
|
|
8039
|
-
const allowedOptions = new Set(['dir', 'expect', 'timeout']);
|
|
8040
|
-
const usage = ' allowed: --dir <project>, --expect a,b, --timeout <s>, --static, --strict, --live-content, --json';
|
|
8525
|
+
const allowedOptions = new Set(['dir', 'expect', 'expect-commands', 'plugin-dir', 'timeout']);
|
|
8526
|
+
const usage = ' allowed: --dir <project>, --expect a,b, --expect-commands a,b, --plugin-dir <dir>, --timeout <s>, --static, --strict, --live-content, --json';
|
|
8041
8527
|
if (options.has('_positional_0')) {
|
|
8042
8528
|
const message = `unexpected argument "${options.get('_positional_0')}"`;
|
|
8043
8529
|
write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}\n${usage}`);
|
|
@@ -8065,6 +8551,27 @@ async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
|
8065
8551
|
.map((s) => s.trim())
|
|
8066
8552
|
.filter(Boolean)
|
|
8067
8553
|
: scan.registrable;
|
|
8554
|
+
// A plugin loaded with `--plugin-dir` is session-scoped, so its surface is NOT on the project's
|
|
8555
|
+
// disk and `scan.registrable` cannot describe it. The manifest can — and defaulting to the
|
|
8556
|
+
// manifest's own `commands[]` keeps the gate honest without a hand-typed list that silently
|
|
8557
|
+
// drifts from the manifest it is supposed to be checking. An UNREADABLE manifest is refused
|
|
8558
|
+
// rather than defaulted to an empty expectation: an empty expectation passes without checking.
|
|
8559
|
+
const pluginDir = options.has('plugin-dir') ? resolve(cwd, options.get('plugin-dir') ?? '') : null;
|
|
8560
|
+
let expectedCommands = options.has('expect-commands')
|
|
8561
|
+
? (options.get('expect-commands') ?? '')
|
|
8562
|
+
.split(',')
|
|
8563
|
+
.map((s) => s.trim())
|
|
8564
|
+
.filter(Boolean)
|
|
8565
|
+
: [];
|
|
8566
|
+
if (pluginDir !== null && !options.has('expect-commands')) {
|
|
8567
|
+
const surface = declaredPluginSurface(pluginDir);
|
|
8568
|
+
if (surface === null) {
|
|
8569
|
+
const message = `cannot read ${join(pluginDir, '.claude-plugin', 'plugin.json')} (or it declares no name) — pass --expect-commands explicitly`;
|
|
8570
|
+
write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}`);
|
|
8571
|
+
return 1;
|
|
8572
|
+
}
|
|
8573
|
+
expectedCommands = surface.commands;
|
|
8574
|
+
}
|
|
8068
8575
|
// ── L1 only: deterministic, no session, safe for CI ──
|
|
8069
8576
|
if (flags.has('static')) {
|
|
8070
8577
|
const exitCode = scan.findings.length > 0 ? 1 : 0;
|
|
@@ -8078,6 +8585,12 @@ async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
|
8078
8585
|
for (const f of scan.findings)
|
|
8079
8586
|
write(` [${f.kind}] ${f.detail}`);
|
|
8080
8587
|
write(scan.findings.length ? ` ${scan.findings.length} layout problem(s) — these can never register` : ' no layout problems found');
|
|
8588
|
+
// Advisories were collected but never PRINTED in static mode: a `.claude-plugin/plugin.json`
|
|
8589
|
+
// under `.claude/skills` produced "no layout problems found" and nothing else, so the one
|
|
8590
|
+
// shape most likely to be a silent non-registration was invisible in exactly the mode CI and
|
|
8591
|
+
// humans run most. Reported, still never fatal (that distinction is the whole point).
|
|
8592
|
+
for (const a of scan.advisories)
|
|
8593
|
+
write(` [${a.kind}] ADVISORY: ${a.detail}`);
|
|
8081
8594
|
write(' (static is a PROXY — run without --static to read the real registration listing)');
|
|
8082
8595
|
}
|
|
8083
8596
|
return exitCode;
|
|
@@ -8087,7 +8600,7 @@ async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
|
8087
8600
|
const timeoutMs = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec * 1000 : 180_000;
|
|
8088
8601
|
if (!json)
|
|
8089
8602
|
write(`dz skills-verify: starting a session in ${projectDir} to read the real registration listing…`);
|
|
8090
|
-
const { stream, error } = await probeInitStream(projectDir, timeoutMs);
|
|
8603
|
+
const { stream, error } = await probeInitStream(projectDir, timeoutMs, pluginDir);
|
|
8091
8604
|
// `init.skills` carries names, not provenance: a USER-level skill of the same name would satisfy
|
|
8092
8605
|
// the expectation while the project's own copy stays broken. Collect the collisions so the
|
|
8093
8606
|
// classifier can refuse to attribute registration to this project (Codex QE #2).
|
|
@@ -8109,6 +8622,7 @@ async function cmdSkillsVerify(options, flags, cwd, write) {
|
|
|
8109
8622
|
// The provenance check RAN (that is what `checked: true` asserts) — see `ambiguous` above.
|
|
8110
8623
|
provenance: { checked: true, ambiguous },
|
|
8111
8624
|
...(options.has('expect') ? { expected } : {}),
|
|
8625
|
+
...(expectedCommands.length > 0 ? { expectedCommands } : {}),
|
|
8112
8626
|
}, { resolvePath: canonical });
|
|
8113
8627
|
const exitCode = registrationExitCode(result.verdict, flags.has('strict'));
|
|
8114
8628
|
// ADVISORY layer: registration is not usability. Costs a real model turn, so it is opt-in, and it
|
|
@@ -9148,6 +9662,8 @@ async function cmdImportEcc(options, flags, cwd, write) {
|
|
|
9148
9662
|
export async function runCli(argv, io = {}) {
|
|
9149
9663
|
const cwd = io.cwd ?? process.cwd();
|
|
9150
9664
|
const write = io.write ?? ((line) => { console.log(line); });
|
|
9665
|
+
// Diagnostics go to stderr so `dz <cmd> > out.txt` yields clean data (feature dz-cli-defects).
|
|
9666
|
+
const writeErr = io.writeErr ?? ((line) => { console.error(line); });
|
|
9151
9667
|
// Lazy STDIN reader — only `dz brain ground` reads it, and only when no positional prompt is
|
|
9152
9668
|
// given. Never blocks: injected `io.stdin` wins; else read fd 0 synchronously, but bail to '' on
|
|
9153
9669
|
// a TTY (nothing piped) or any read error. Grounding must never hang waiting on an empty pipe.
|
|
@@ -9164,6 +9680,26 @@ export async function runCli(argv, io = {}) {
|
|
|
9164
9680
|
}
|
|
9165
9681
|
};
|
|
9166
9682
|
const { command, options, optionLists, flags } = parseArgs(argv);
|
|
9683
|
+
// ── `dz --version` / `dz -v` / `dz version` — PRE-DISPATCH, before the help branch ──
|
|
9684
|
+
//
|
|
9685
|
+
// Until now `dz --version` printed the whole USAGE manual and exited 0 (MEASURED 2026-08-17,
|
|
9686
|
+
// reproducer `node dist/bin.js --version`). Exit 0 plus prose is the worst possible answer for a
|
|
9687
|
+
// caller that must decide whether a `dz` it found on PATH is safe to invoke: the status code says
|
|
9688
|
+
// "fine" and there is no number to parse. Any wrapper guarding a version range needs exactly one
|
|
9689
|
+
// parseable line. Recognised only as the FIRST token (or the `version` subcommand) so that a
|
|
9690
|
+
// later positional `-v` belonging to a subcommand keeps its own meaning.
|
|
9691
|
+
if (argv[0] === '--version' || argv[0] === '-v' || command === 'version') {
|
|
9692
|
+
const version = dzOwnVersion();
|
|
9693
|
+
if (flags.has('json')) {
|
|
9694
|
+
write(JSON.stringify({ name: 'dz', version, node: process.version, schemas: { loopPlan: 'loop-plan/1' } }));
|
|
9695
|
+
}
|
|
9696
|
+
else {
|
|
9697
|
+
write(version);
|
|
9698
|
+
}
|
|
9699
|
+
// An unresolvable version is a FAILURE, not a value: exiting 0 with the literal `unknown` would
|
|
9700
|
+
// let a guard treat "I could not tell you" as "I answered you".
|
|
9701
|
+
return version === 'unknown' ? 1 : 0;
|
|
9702
|
+
}
|
|
9167
9703
|
if (command === '' || command === 'help' || flags.has('help')) {
|
|
9168
9704
|
write(USAGE);
|
|
9169
9705
|
return 0;
|
|
@@ -9171,14 +9707,14 @@ export async function runCli(argv, io = {}) {
|
|
|
9171
9707
|
try {
|
|
9172
9708
|
switch (command) {
|
|
9173
9709
|
case 'init':
|
|
9174
|
-
return await cmdInit(options, flags, cwd, write);
|
|
9710
|
+
return await cmdInit(options, flags, cwd, write, writeErr);
|
|
9175
9711
|
case 'verify':
|
|
9176
|
-
return await cmdVerify(options, cwd, write);
|
|
9712
|
+
return await cmdVerify(options, cwd, write, writeErr);
|
|
9177
9713
|
case 'sync':
|
|
9178
9714
|
case 'update':
|
|
9179
|
-
return await cmdSync(options, flags, cwd, write);
|
|
9715
|
+
return await cmdSync(options, flags, cwd, write, writeErr);
|
|
9180
9716
|
case 'list':
|
|
9181
|
-
return cmdList(options, cwd, write);
|
|
9717
|
+
return cmdList(options, cwd, write, writeErr);
|
|
9182
9718
|
case 'create-skill':
|
|
9183
9719
|
return cmdCreateSkill(options, flags, cwd, write);
|
|
9184
9720
|
case 'info':
|
|
@@ -9196,7 +9732,7 @@ export async function runCli(argv, io = {}) {
|
|
|
9196
9732
|
case 'doctor':
|
|
9197
9733
|
return await cmdDoctor(options, flags, cwd, write);
|
|
9198
9734
|
case 'install':
|
|
9199
|
-
return await cmdInstall(options, flags, cwd, write, io.installRunner);
|
|
9735
|
+
return await cmdInstall(options, flags, cwd, write, writeErr, io.installRunner);
|
|
9200
9736
|
case 'bundle':
|
|
9201
9737
|
return cmdBundle(options, flags, cwd, write);
|
|
9202
9738
|
case 'teach':
|
|
@@ -9224,17 +9760,17 @@ export async function runCli(argv, io = {}) {
|
|
|
9224
9760
|
case 'verify-pack':
|
|
9225
9761
|
return cmdVerifyPack(options, flags, cwd, write);
|
|
9226
9762
|
case 'setup':
|
|
9227
|
-
return await cmdSetup(options, flags, cwd, write);
|
|
9763
|
+
return await cmdSetup(options, flags, cwd, write, writeErr);
|
|
9228
9764
|
case 'pretrain':
|
|
9229
9765
|
return cmdPretrain(options, cwd, write);
|
|
9230
9766
|
case 'compose':
|
|
9231
|
-
return cmdCompose(options, cwd, write);
|
|
9767
|
+
return cmdCompose(options, cwd, write, writeErr);
|
|
9232
9768
|
case 'diff':
|
|
9233
9769
|
return cmdDiff(options, cwd, write);
|
|
9234
9770
|
case 'recommend':
|
|
9235
9771
|
return cmdRecommend(options, cwd, write);
|
|
9236
9772
|
case 'upgrade':
|
|
9237
|
-
return cmdUpgrade(options, flags, cwd, write);
|
|
9773
|
+
return cmdUpgrade(options, flags, cwd, write, writeErr);
|
|
9238
9774
|
case 'auto-canonicalize':
|
|
9239
9775
|
return await cmdAutoCanonicalize(options, cwd, write);
|
|
9240
9776
|
case 'publish':
|
|
@@ -9242,7 +9778,7 @@ export async function runCli(argv, io = {}) {
|
|
|
9242
9778
|
case 'release':
|
|
9243
9779
|
return cmdRelease(options, flags, cwd, write, io.releaseRunner);
|
|
9244
9780
|
case 'parity':
|
|
9245
|
-
return cmdParity(options, flags, write);
|
|
9781
|
+
return cmdParity(options, flags, write, writeErr);
|
|
9246
9782
|
case 'registry':
|
|
9247
9783
|
return cmdRegistry(options, cwd, write);
|
|
9248
9784
|
case 'benchmark':
|
|
@@ -9253,6 +9789,10 @@ export async function runCli(argv, io = {}) {
|
|
|
9253
9789
|
return await cmdSyncUpstream(options, flags, cwd, write);
|
|
9254
9790
|
case 'drift-check':
|
|
9255
9791
|
return cmdDriftCheck(options, flags, cwd, write);
|
|
9792
|
+
case 'hooks-sync':
|
|
9793
|
+
return cmdHooksSync(options, flags, cwd, write, writeErr);
|
|
9794
|
+
case 'agents-sync':
|
|
9795
|
+
return cmdAgentsSync(options, flags, cwd, write, writeErr);
|
|
9256
9796
|
case 'sync-canonical':
|
|
9257
9797
|
return cmdSyncCanonical(options, flags, cwd, write);
|
|
9258
9798
|
case 'plugin':
|
|
@@ -9270,7 +9810,7 @@ export async function runCli(argv, io = {}) {
|
|
|
9270
9810
|
case 'retro':
|
|
9271
9811
|
return await cmdRetro(options, flags, cwd, write);
|
|
9272
9812
|
case 'feature-adr-setup':
|
|
9273
|
-
return cmdFeatureAdrSetup(options, flags, cwd, write);
|
|
9813
|
+
return cmdFeatureAdrSetup(options, flags, cwd, write, writeErr);
|
|
9274
9814
|
case 'challenge':
|
|
9275
9815
|
return cmdChallenge(options, flags, cwd, write);
|
|
9276
9816
|
case 'discrimination-check':
|
|
@@ -9308,7 +9848,9 @@ export async function runCli(argv, io = {}) {
|
|
|
9308
9848
|
}
|
|
9309
9849
|
}
|
|
9310
9850
|
catch (error) {
|
|
9311
|
-
|
|
9851
|
+
// stderr, not stdout: an uncaught failure is a diagnostic, and routing it through
|
|
9852
|
+
// `write` is what made `dz list > skills.txt` write the error into the data file.
|
|
9853
|
+
writeErr(`dz: ${error instanceof Error ? error.message : String(error)}`);
|
|
9312
9854
|
return 1;
|
|
9313
9855
|
}
|
|
9314
9856
|
}
|