@dzhechkov/harness-cli 0.7.0 → 0.7.2

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.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 67 commands, 14 presets, 10 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "696aae63a95e8f8726eecc1aa0e68f20d7980e99bb4008b22f2e1058ca67c6c0"
28
+ "content": "ca0d42d114330fe7a2d0833abbf2fe90f3a60221e6cffeb3dc3b7edc7f818ec6"
29
29
  }
30
30
  ]
31
31
  },
@@ -75,7 +75,7 @@
75
75
  "hashes": [
76
76
  {
77
77
  "alg": "SHA-256",
78
- "content": "3893831e365016b5bd118f7f0ce2e8b106cfd0400689b9e190a5f80aa61b79f5"
78
+ "content": "5aeeec8ab84e3922ce0c7b7ea711fd1cdff67b68e1ded0b66e920a907e6ac267"
79
79
  }
80
80
  ]
81
81
  },
@@ -85,7 +85,7 @@
85
85
  "hashes": [
86
86
  {
87
87
  "alg": "SHA-256",
88
- "content": "ea26d4cfd1265fc9f2397a17055580425ddf1ee312e0f28a06a214804a58906a"
88
+ "content": "f1cc2dc4a5de433a2d3cbd1e0567bf9c7bb5f3d04e90f72d10c54a9ccc5b00ae"
89
89
  }
90
90
  ]
91
91
  },
@@ -95,7 +95,7 @@
95
95
  "hashes": [
96
96
  {
97
97
  "alg": "SHA-256",
98
- "content": "aa547fee89f14b634fb3b95f95ccbe1bfcc3f29a1c8f11b01a4ecc98ff3e736d"
98
+ "content": "a1154eea12c861e092af90e33b5074d1219401f2194e2205f29ae9deb742834e"
99
99
  }
100
100
  ]
101
101
  },
@@ -105,7 +105,7 @@
105
105
  "hashes": [
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "11c89da8d9b115a70397da792e9b2220e32936ecebcd2e26ab03963c14fc56d8"
108
+ "content": "a1fcb5f0f1524e7564bdec11bcf3b1c8d9d41ddb08ea5502dc72bbb3de439baa"
109
109
  }
110
110
  ]
111
111
  },
@@ -115,7 +115,7 @@
115
115
  "hashes": [
116
116
  {
117
117
  "alg": "SHA-256",
118
- "content": "b735a6c18bfbe5adf17bbcfe6e2eeae71477b877e958c36e0a5e73e59f100d6a"
118
+ "content": "4ab523bc0a5f777fd66b8ed44c07a8b240a4cf2ce32da779a3916e6ddc53fa33"
119
119
  }
120
120
  ]
121
121
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "bce1ded9ffac15080908af1a9571b67141c141374ec4ba90b987fd6fd8883aab"
128
+ "content": "e378022dd85db141f1d5b4d06d4791072b4caf6900b992dde5781d09b0559c35"
129
129
  }
130
130
  ]
131
131
  },
@@ -135,7 +135,7 @@
135
135
  "hashes": [
136
136
  {
137
137
  "alg": "SHA-256",
138
- "content": "6d052e0a7352e9824bc1d4a319d03b42320f6551210f7dfd360afbdca4e556f0"
138
+ "content": "0c33ad728e7b3b25211d0b16d2803b9ca35d0aae6d3a570b57aa9b8377fcde1f"
139
139
  }
140
140
  ]
141
141
  },
@@ -145,7 +145,7 @@
145
145
  "hashes": [
146
146
  {
147
147
  "alg": "SHA-256",
148
- "content": "e95005978fdd733f78208d6d916f62f970123530241739c9da7d3a74154b7a13"
148
+ "content": "342b0c716f6ac9f853e6fb9e10d2d3ae4c643f047becd200d6f8576a84674fb2"
149
149
  }
150
150
  ]
151
151
  },
@@ -155,7 +155,7 @@
155
155
  "hashes": [
156
156
  {
157
157
  "alg": "SHA-256",
158
- "content": "a25d0a97a30fa5abc35473ed4ca1bc32bc5a8bcb8a45c57d7feeb03cb6058e02"
158
+ "content": "936596fd99f3767ca0119a02f07dfabf22b5b1c30e7007c63c3ff21cfced3836"
159
159
  }
160
160
  ]
161
161
  },
@@ -165,7 +165,7 @@
165
165
  "hashes": [
166
166
  {
167
167
  "alg": "SHA-256",
168
- "content": "7c554d109fb0aabcf9a2923b0d9ac84723652abdec81202b2cc0a726d38c84bd"
168
+ "content": "9096b181a0164cd59061bddcf9f4bee59123c1ab776e7a12da8b9808bd5b7936"
169
169
  }
170
170
  ]
171
171
  },
@@ -175,7 +175,7 @@
175
175
  "hashes": [
176
176
  {
177
177
  "alg": "SHA-256",
178
- "content": "5b7992e09b843a01c9bf7a05b5db4475ca9abbf893ead79f7bad73f0f92c4249"
178
+ "content": "8225f1de31f005c0d883fa9cf9b3a106f5e244ebbe5b4b3d2375c8127ba821f8"
179
179
  }
180
180
  ]
181
181
  },
@@ -185,7 +185,7 @@
185
185
  "hashes": [
186
186
  {
187
187
  "alg": "SHA-256",
188
- "content": "54c8133a172c2ca81c3f5a32e701386812a621198758423fcc7f0bf3db8c1ff7"
188
+ "content": "02b6b6f1b17a53d652a75555d185a20ea5d3e3110aa915ab228bf5521b0ce5fb"
189
189
  }
190
190
  ]
191
191
  },
@@ -215,7 +215,7 @@
215
215
  "hashes": [
216
216
  {
217
217
  "alg": "SHA-256",
218
- "content": "c8e09eb658132733bf3fbd6e4b54eaa0e8084264fddcd279e663e32809a9c485"
218
+ "content": "ec93467aada7c720f48874e6f24a5fb57bb77e8f5e01a450a29bc363b81f7b9a"
219
219
  }
220
220
  ]
221
221
  },
@@ -235,7 +235,7 @@
235
235
  "hashes": [
236
236
  {
237
237
  "alg": "SHA-256",
238
- "content": "b4f400b3e08cb7bb6326f973ca09c87411dd942ae9c8cdcd9d2d27dc562dac1f"
238
+ "content": "a31c2567ee0817e7a01eb034ac4ae8e9c44e18edea85ef722af2d32b4a2dabc4"
239
239
  }
240
240
  ]
241
241
  },
@@ -245,7 +245,7 @@
245
245
  "hashes": [
246
246
  {
247
247
  "alg": "SHA-256",
248
- "content": "5fef10b3d968d299eb7463157e8b164413872f7cc8bf5e9ca1c605e63569a9f7"
248
+ "content": "b6e2b8d73bb492afec8717f39ecfe3638c62b2f1d2479a9be43c493adebf89d6"
249
249
  }
250
250
  ]
251
251
  },
@@ -255,7 +255,7 @@
255
255
  "hashes": [
256
256
  {
257
257
  "alg": "SHA-256",
258
- "content": "76c2dc1598e72d11df59319cc64fb2aa39b8c3af4eb3077f66598f4f2a3323c0"
258
+ "content": "d4258f3b183d03047aa07399ae96bdb2b3af83f0005127a24a39f8932982bf78"
259
259
  }
260
260
  ]
261
261
  }
package/src/cli.ts CHANGED
@@ -5192,17 +5192,40 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
5192
5192
  const manifestPath = join(pk.dir, MANIFEST_NAME);
5193
5193
  const manifestPresent = existsSync(manifestPath);
5194
5194
  let verifyOk = false;
5195
+ let artifactUnavailable = false;
5195
5196
  if (trustRootPresent && manifestPresent) {
5197
+ // Verify the ARTIFACT, through the SAME extraction `dz sign` uses. This used to walk the
5198
+ // source TREE narrowed to a shipped-path list — a different object by construction, because
5199
+ // npm synthesises a LICENSE into the pack of a package whose tree has none and pnpm rewrites
5200
+ // package.json at pack time. So the gate named two failures that were both true about the
5201
+ // tree and both wrong about what ships, for EVERY package, and a fail-closed gate no input
5202
+ // can satisfy is a gate that gets routed around — it was: the 0.7.0 release went out through
5203
+ // `pnpm publish` and skipped re-signing (ADR-001, features/publish-gate-verifies-the-tarball).
5204
+ let cleanupGate: (() => void) | null = null;
5196
5205
  try {
5197
5206
  const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
5198
- let shippedForGate: string[] | undefined;
5199
- try { shippedForGate = npmPackedPaths(pk.dir); } catch { shippedForGate = undefined; }
5200
- verifyOk = verifyManifest(pk.dir, signed, readFileSync(trustRoot, 'utf8'), shippedForGate).ok;
5207
+ let extracted: { dir: string; cleanup: () => void } | undefined;
5208
+ try {
5209
+ extracted = extractPublishTarball(pk.dir);
5210
+ cleanupGate = extracted.cleanup;
5211
+ } catch (err) {
5212
+ // Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
5213
+ // TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
5214
+ // signature"; with no artifact, nothing was compared, and reporting a pass would be a
5215
+ // claim about an object that was never built. Say why, and block.
5216
+ write(`dz publish: could not pack ${pk.name} (${(err as Error).message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
5217
+ artifactUnavailable = true;
5218
+ }
5219
+ verifyOk =
5220
+ extracted !== undefined &&
5221
+ verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
5201
5222
  } catch {
5202
5223
  verifyOk = false;
5224
+ } finally {
5225
+ cleanupGate?.();
5203
5226
  }
5204
5227
  }
5205
- const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning });
5228
+ const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning, artifactUnavailable });
5206
5229
  if (decision.action === 'block') {
5207
5230
  write(`dz publish: BLOCKED ${pk.name} — ${decision.reason}`);
5208
5231
  blocked++;
@@ -5232,16 +5255,31 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
5232
5255
  // Verify the OUTCOME against the trust root a CONSUMER would use — an existing key may be the
5233
5256
  // WRONG key, and enumerating that state is a losing game (round-1 review). The pack NAME travels
5234
5257
  // with the verdict so a pass about a different artifact cannot be mistaken for this one.
5258
+ // The identity must be the one the PUBLISHER uses — the npm package name — or the check
5259
+ // compares two vocabularies and can never agree. It did: `basename(packDir)` is `memory`
5260
+ // where the publisher says `@dzhechkov/memory`, so every scoped package (all of them here)
5261
+ // was refused as "a pass about another artifact", and the release routed around the gate
5262
+ // instead. MEASURED 2026-08-22 by running `dz publish --filter memory --yes`.
5235
5263
  const trustRoot = resolve(cwd, TRUST_ROOT_REL);
5236
- if (!existsSync(trustRoot)) return { ok: false, trustRootPresent: false, pack: basename(packDir) };
5264
+ const identity = packNpmName(packDir);
5265
+ const id = identity === undefined ? {} : { pack: identity };
5266
+ if (!existsSync(trustRoot)) return { ok: false, trustRootPresent: false, ...id };
5267
+ // Verify the SAME OBJECT `reSign` hashed: the extracted tarball. Walking the source tree here
5268
+ // compares a manifest built from the artifact against files npm rewrites at pack time, so it
5269
+ // reported "does not verify — most likely the WRONG signing key" about a correctly signed pack
5270
+ // (MEASURED 2026-08-22, `dz publish --filter memory --yes`). That message sends the operator
5271
+ // hunting for a key problem that does not exist; the object was simply the wrong one.
5272
+ let cleanup: (() => void) | null = null;
5237
5273
  try {
5238
5274
  const signed = JSON.parse(readFileSync(join(packDir, MANIFEST_NAME), 'utf8')) as never;
5239
- let shipped: string[] | undefined;
5240
- try { shipped = npmPackedPaths(packDir); } catch { shipped = undefined; }
5241
- const res = verifyManifest(packDir, signed, readFileSync(trustRoot, 'utf8'), shipped);
5242
- return { ok: res.ok, trustRootPresent: true, pack: basename(packDir) };
5275
+ const extracted = extractPublishTarball(packDir);
5276
+ cleanup = extracted.cleanup;
5277
+ const res = verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8'));
5278
+ return { ok: res.ok, trustRootPresent: true, ...id };
5243
5279
  } catch {
5244
- return { ok: false, trustRootPresent: true, pack: basename(packDir) };
5280
+ return { ok: false, trustRootPresent: true, ...id };
5281
+ } finally {
5282
+ cleanup?.();
5245
5283
  }
5246
5284
  },
5247
5285
  reSign: (packDir: string, keyPath: string): void => {
@@ -9374,6 +9412,19 @@ function scanOneReqeRoot(
9374
9412
  * authority on what `files[]` ships; reimplementing its globbing would put a second, divergent answer
9375
9413
  * next to the real one — which is the class of defect this whole change exists to remove.
9376
9414
  */
9415
+ /**
9416
+ * The npm name of a pack on disk — the identity a publisher, a registry and a consumer all use.
9417
+ * `undefined` when it cannot be read, so an unestablished identity is never asserted as a match.
9418
+ */
9419
+ function packNpmName(packDir: string): string | undefined {
9420
+ try {
9421
+ const name = (JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8')) as { name?: unknown }).name;
9422
+ return typeof name === 'string' && name !== '' ? name : undefined;
9423
+ } catch {
9424
+ return undefined;
9425
+ }
9426
+ }
9427
+
9377
9428
  function npmPackedPaths(packDir: string): string[] {
9378
9429
  // `pnpm`, not `npm`: the PUBLISHER is `pnpm publish` (see `publishArgv`), and the two packers do not
9379
9430
  // agree. MEASURED 2026-08-21 on `skills-news`: `npm pack` emits a 1051-byte package.json identical
@@ -9399,6 +9450,17 @@ function npmPackedPaths(packDir: string): string[] {
9399
9450
  */
9400
9451
  function extractPublishTarball(packDir: string): { dir: string; cleanup: () => void } {
9401
9452
  const tmp = mkdtempSync(join(tmpdir(), 'dz-sign-pack-'));
9453
+ try {
9454
+ return extractIntoTempDir(packDir, tmp);
9455
+ } catch (err) {
9456
+ // The caller never receives a cleanup for a throw, so the directory this function created must
9457
+ // be removed HERE or it leaks once per failed pack (cross-family review, 2026-08-22).
9458
+ try { rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ }
9459
+ throw err;
9460
+ }
9461
+ }
9462
+
9463
+ function extractIntoTempDir(packDir: string, tmp: string): { dir: string; cleanup: () => void } {
9402
9464
  const out = execFileSync('pnpm', ['pack', '--pack-destination', tmp, '--json'], {
9403
9465
  cwd: packDir,
9404
9466
  encoding: 'utf-8',