@dzhechkov/harness-cli 0.4.4 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,13 @@
40
40
  ".dz-manifest.json",
41
41
  "sbom.json"
42
42
  ],
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit",
48
+ "lint": "tsc -p tsconfig.json --noEmit"
49
+ },
43
50
  "dependencies": {
44
51
  "@dzhechkov/harness-core": "^0.4.0",
45
52
  "@dzhechkov/harness-presets": "^0.5.0",
@@ -75,12 +82,5 @@
75
82
  "url": "https://github.com/djd1m/dz-harness-hub.git",
76
83
  "directory": "packages/@dzhechkov/harness-cli"
77
84
  },
78
- "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme",
79
- "scripts": {
80
- "build": "tsc -p tsconfig.json",
81
- "test": "vitest run",
82
- "test:watch": "vitest",
83
- "typecheck": "tsc -p tsconfig.json --noEmit",
84
- "lint": "tsc -p tsconfig.json --noEmit"
85
- }
86
- }
85
+ "homepage": "https://github.com/djd1m/dz-harness-hub/tree/main/packages/@dzhechkov/harness-cli#readme"
86
+ }
package/sbom.json CHANGED
@@ -175,7 +175,7 @@
175
175
  "hashes": [
176
176
  {
177
177
  "alg": "SHA-256",
178
- "content": "5bd26341b4a5885c116432bf621b7fdb4172b2604db47ad3c1c001deb4fe6ed4"
178
+ "content": "00a53276d145112e3c2ae5ca2950ac74877ea4b069b2b81040d49c5903eb6482"
179
179
  }
180
180
  ]
181
181
  },
package/src/cli.ts CHANGED
@@ -242,6 +242,7 @@ import {
242
242
  isUsablePlaneResult,
243
243
  renderDeliveryReview,
244
244
  scanSkillsLayout,
245
+ declaredPluginSurface,
245
246
  parseInitFacts,
246
247
  verifyRegistration,
247
248
  buildContentProbePrompt,
@@ -449,6 +450,8 @@ Usage:
449
450
  dz import-ecc [--local-path <dir>] [--select id,id,...] [--limit N] [--output <dir>] [--force]
450
451
  dz help
451
452
 
453
+ Global: --version | -v [--json] (prints this CLI's own semver on one line, exit 0; "unknown" + exit 1 when unresolvable)
454
+
452
455
  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
456
 
454
457
  Targets: ${TARGET_NAMES.join(', ')}
@@ -935,8 +938,8 @@ function workflowInitPlan(name: string, pattern: string): object {
935
938
  ...base,
936
939
  steps: [
937
940
  { 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)
941
+ { 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)
942
+ { 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
943
  { stepId: 'jn', kind: 'join', phase: 'Work', deps: ['fan'] },
941
944
  ],
942
945
  fanouts: [{ stage: 'fan', registry: ['item1', 'item2', 'item3'], maxFanout: 3, chain: ['a', 'b'] }],
@@ -948,10 +951,10 @@ function workflowInitPlan(name: string, pattern: string): object {
948
951
  ...base,
949
952
  steps: [
950
953
  { 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)
954
+ { 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
955
  { stepId: 'jn', kind: 'join', phase: 'Lanes', deps: ['fan'] },
953
956
  // 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)
957
+ { 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
958
  ],
956
959
  fanouts: [{ stage: 'fan', registry: ['lane1', 'lane2', 'lane3'], maxFanout: 3, chain: ['lane'] }],
957
960
  joins: [{ stage: 'jn', forStage: 'fan', joinPolicy: 'all-activated', onInvalid: 'named-failure' }],
@@ -961,8 +964,8 @@ function workflowInitPlan(name: string, pattern: string): object {
961
964
  return {
962
965
  ...base,
963
966
  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)
967
+ { 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)
968
+ { 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
969
  ],
967
970
  gates: [{ stepId: 'gate', kind: 'parse-verdict', failRoute: 'work', maxRedos: 1 }],
968
971
  };
@@ -970,7 +973,7 @@ function workflowInitPlan(name: string, pattern: string): object {
970
973
  // minimal default: one agent step
971
974
  return {
972
975
  ...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)
976
+ 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
977
  };
975
978
  }
976
979
 
@@ -1161,8 +1164,12 @@ function cmdWorkflowLint(options: Map<string, string>, flags: Set<string>, cwd:
1161
1164
  /** `dz workflow-trace` — timeline + invariant runner over a run's trace.jsonl. Scope is CAPPED
1162
1165
  * (AM-8): <runDir|--slug|--run>, --invariants, --html, --json. NO watch/filter/compare/search/
1163
1166
  * retention/access-control — adding one needs an ADR amendment (the surface test pins this). */
1164
- /** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
1165
- function traceBundleToolVersion(): string {
1167
+ /**
1168
+ * This CLI's OWN semver, read from its package.json. `unknown` is honest; a throw is not, and an
1169
+ * invented number is worse than either — a downstream version guard that is handed a fabricated
1170
+ * version happily calls a binary it should have refused.
1171
+ */
1172
+ function dzOwnVersion(): string {
1166
1173
  try {
1167
1174
  const req = createRequire(import.meta.url);
1168
1175
  const pkg = req('../package.json') as { version?: unknown };
@@ -1170,6 +1177,11 @@ function traceBundleToolVersion(): string {
1170
1177
  } catch { return 'unknown'; }
1171
1178
  }
1172
1179
 
1180
+ /** The tool version stamped into a bundle's provenance. Unknown is honest; a throw is not. */
1181
+ function traceBundleToolVersion(): string {
1182
+ return dzOwnVersion();
1183
+ }
1184
+
1173
1185
  /** Resolve a run the SAME three ways the timeline reader does — positional, --slug, --run — so a
1174
1186
  * bundle can never address a run by a scheme the rest of the command does not understand. */
1175
1187
  function resolveTraceRun(options: Map<string, string>, cwd: string, positional: string | undefined):
@@ -8151,7 +8163,11 @@ const PROBE_SCRUB_ENV = [
8151
8163
  * lands, then kill it — the model never answers, so the probe costs ~no tokens. Never throws:
8152
8164
  * every failure becomes an `error` string, which the pure classifier turns into `inconclusive`.
8153
8165
  */
8154
- function probeInitStream(projectDir: string, timeoutMs: number): Promise<{ stream: string; error: string | null }> {
8166
+ function probeInitStream(
8167
+ projectDir: string,
8168
+ timeoutMs: number,
8169
+ pluginDir: string | null = null,
8170
+ ): Promise<{ stream: string; error: string | null }> {
8155
8171
  return new Promise((resolveProbe) => {
8156
8172
  const env: NodeJS.ProcessEnv = { ...process.env };
8157
8173
  for (const key of PROBE_SCRUB_ENV) delete env[key];
@@ -8181,7 +8197,12 @@ function probeInitStream(projectDir: string, timeoutMs: number): Promise<{ strea
8181
8197
 
8182
8198
  try {
8183
8199
  // NOT `--bare`: that mode skips plugin credentials and fails with "Not logged in".
8184
- child = spawn('claude', ['-p', 'ok', '--output-format', 'stream-json', '--verbose'], {
8200
+ const args = ['-p', 'ok', '--output-format', 'stream-json', '--verbose'];
8201
+ // Session-scoped plugin load — the marketplace-free vehicle (ADR-003 D-3). Without this the
8202
+ // probe reads a session in which the plugin was never loaded, and reports its commands
8203
+ // missing for a reason that has nothing to do with the package under test.
8204
+ if (pluginDir !== null) args.push('--plugin-dir', pluginDir);
8205
+ child = spawn('claude', args, {
8185
8206
  cwd: projectDir,
8186
8207
  env,
8187
8208
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -8255,8 +8276,11 @@ async function cmdSkillsVerify(
8255
8276
  ): Promise<number> {
8256
8277
  const json = flags.has('json');
8257
8278
  if (flags.has('help')) {
8258
- write('dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json]');
8279
+ write('dz skills-verify [--dir <project>] [--expect a,b] [--expect-commands a,b] [--plugin-dir <dir>] [--static] [--strict] [--timeout <s>] [--json]');
8259
8280
  write(' Verifies that a project\'s .claude/skills/ actually register in Claude Code.');
8281
+ write(' --plugin-dir <dir> load a plugin into the probe session (session-scoped, no marketplace);');
8282
+ write(' with no --expect-commands, the expectation defaults to the manifest\'s own commands[]');
8283
+ write(' --expect-commands slash commands that MUST appear in the session listing, e.g. loop-designer:init');
8260
8284
  write(' --static layout scan only (no Claude session, CI-safe): flags dirs that can never register');
8261
8285
  write(' --live-content ADVISORY extra turn: ask a live model to name the skills and quote one, proving');
8262
8286
  write(' the CONTENT is usable — registration is not usability. Never changes the exit code.');
@@ -8266,8 +8290,9 @@ async function cmdSkillsVerify(
8266
8290
  }
8267
8291
 
8268
8292
  const allowedFlags = new Set(['json', 'help', 'static', 'strict', 'live-content']);
8269
- const allowedOptions = new Set(['dir', 'expect', 'timeout']);
8270
- const usage = ' allowed: --dir <project>, --expect a,b, --timeout <s>, --static, --strict, --live-content, --json';
8293
+ const allowedOptions = new Set(['dir', 'expect', 'expect-commands', 'plugin-dir', 'timeout']);
8294
+ const usage =
8295
+ ' allowed: --dir <project>, --expect a,b, --expect-commands a,b, --plugin-dir <dir>, --timeout <s>, --static, --strict, --live-content, --json';
8271
8296
  if (options.has('_positional_0')) {
8272
8297
  const message = `unexpected argument "${options.get('_positional_0')}"`;
8273
8298
  write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}\n${usage}`);
@@ -8297,6 +8322,28 @@ async function cmdSkillsVerify(
8297
8322
  .filter(Boolean)
8298
8323
  : scan.registrable;
8299
8324
 
8325
+ // A plugin loaded with `--plugin-dir` is session-scoped, so its surface is NOT on the project's
8326
+ // disk and `scan.registrable` cannot describe it. The manifest can — and defaulting to the
8327
+ // manifest's own `commands[]` keeps the gate honest without a hand-typed list that silently
8328
+ // drifts from the manifest it is supposed to be checking. An UNREADABLE manifest is refused
8329
+ // rather than defaulted to an empty expectation: an empty expectation passes without checking.
8330
+ const pluginDir = options.has('plugin-dir') ? resolve(cwd, options.get('plugin-dir') ?? '') : null;
8331
+ let expectedCommands = options.has('expect-commands')
8332
+ ? (options.get('expect-commands') ?? '')
8333
+ .split(',')
8334
+ .map((s) => s.trim())
8335
+ .filter(Boolean)
8336
+ : [];
8337
+ if (pluginDir !== null && !options.has('expect-commands')) {
8338
+ const surface = declaredPluginSurface(pluginDir);
8339
+ if (surface === null) {
8340
+ const message = `cannot read ${join(pluginDir, '.claude-plugin', 'plugin.json')} (or it declares no name) — pass --expect-commands explicitly`;
8341
+ write(json ? JSON.stringify({ error: message, exitCode: 1 }) : `dz skills-verify: ${message}`);
8342
+ return 1;
8343
+ }
8344
+ expectedCommands = surface.commands;
8345
+ }
8346
+
8300
8347
  // ── L1 only: deterministic, no session, safe for CI ──
8301
8348
  if (flags.has('static')) {
8302
8349
  const exitCode = scan.findings.length > 0 ? 1 : 0;
@@ -8307,6 +8354,11 @@ async function cmdSkillsVerify(
8307
8354
  if (!scan.exists) write(' no .claude/skills/ directory here');
8308
8355
  for (const f of scan.findings) write(` [${f.kind}] ${f.detail}`);
8309
8356
  write(scan.findings.length ? ` ${scan.findings.length} layout problem(s) — these can never register` : ' no layout problems found');
8357
+ // Advisories were collected but never PRINTED in static mode: a `.claude-plugin/plugin.json`
8358
+ // under `.claude/skills` produced "no layout problems found" and nothing else, so the one
8359
+ // shape most likely to be a silent non-registration was invisible in exactly the mode CI and
8360
+ // humans run most. Reported, still never fatal (that distinction is the whole point).
8361
+ for (const a of scan.advisories) write(` [${a.kind}] ADVISORY: ${a.detail}`);
8310
8362
  write(' (static is a PROXY — run without --static to read the real registration listing)');
8311
8363
  }
8312
8364
  return exitCode;
@@ -8317,7 +8369,7 @@ async function cmdSkillsVerify(
8317
8369
  const timeoutMs = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec * 1000 : 180_000;
8318
8370
  if (!json) write(`dz skills-verify: starting a session in ${projectDir} to read the real registration listing…`);
8319
8371
 
8320
- const { stream, error } = await probeInitStream(projectDir, timeoutMs);
8372
+ const { stream, error } = await probeInitStream(projectDir, timeoutMs, pluginDir);
8321
8373
 
8322
8374
  // `init.skills` carries names, not provenance: a USER-level skill of the same name would satisfy
8323
8375
  // the expectation while the project's own copy stays broken. Collect the collisions so the
@@ -8342,6 +8394,7 @@ async function cmdSkillsVerify(
8342
8394
  // The provenance check RAN (that is what `checked: true` asserts) — see `ambiguous` above.
8343
8395
  provenance: { checked: true, ambiguous },
8344
8396
  ...(options.has('expect') ? { expected } : {}),
8397
+ ...(expectedCommands.length > 0 ? { expectedCommands } : {}),
8345
8398
  },
8346
8399
  { resolvePath: canonical },
8347
8400
  );
@@ -9295,6 +9348,26 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
9295
9348
  };
9296
9349
  const { command, options, optionLists, flags } = parseArgs(argv);
9297
9350
 
9351
+ // ── `dz --version` / `dz -v` / `dz version` — PRE-DISPATCH, before the help branch ──
9352
+ //
9353
+ // Until now `dz --version` printed the whole USAGE manual and exited 0 (MEASURED 2026-08-17,
9354
+ // reproducer `node dist/bin.js --version`). Exit 0 plus prose is the worst possible answer for a
9355
+ // caller that must decide whether a `dz` it found on PATH is safe to invoke: the status code says
9356
+ // "fine" and there is no number to parse. Any wrapper guarding a version range needs exactly one
9357
+ // parseable line. Recognised only as the FIRST token (or the `version` subcommand) so that a
9358
+ // later positional `-v` belonging to a subcommand keeps its own meaning.
9359
+ if (argv[0] === '--version' || argv[0] === '-v' || command === 'version') {
9360
+ const version = dzOwnVersion();
9361
+ if (flags.has('json')) {
9362
+ write(JSON.stringify({ name: 'dz', version, node: process.version, schemas: { loopPlan: 'loop-plan/1' } }));
9363
+ } else {
9364
+ write(version);
9365
+ }
9366
+ // An unresolvable version is a FAILURE, not a value: exiting 0 with the literal `unknown` would
9367
+ // let a guard treat "I could not tell you" as "I answered you".
9368
+ return version === 'unknown' ? 1 : 0;
9369
+ }
9370
+
9298
9371
  if (command === '' || command === 'help' || flags.has('help')) {
9299
9372
  write(USAGE);
9300
9373
  return 0;
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dzhechko
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.