@dzhechkov/harness-cli 0.3.256 → 0.3.257

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.3.256",
3
+ "version": "0.3.257",
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",
@@ -41,7 +41,7 @@
41
41
  "sbom.json"
42
42
  ],
43
43
  "dependencies": {
44
- "@dzhechkov/harness-core": "^0.3.145",
44
+ "@dzhechkov/harness-core": "^0.3.146",
45
45
  "@dzhechkov/harness-presets": "^0.5.0",
46
46
  "@dzhechkov/scout": "^0.8.0",
47
47
  "@dzhechkov/skills-devops": "^0.3.0",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "5f73e2c6d9c0ea38d5324f058cbd921bdb28dab60aa97ded97979ed994426731"
28
+ "content": "7333dd44bcbeb6536e8ee0f0490a57fefca211e706535da7f44cea7012388213"
29
29
  }
30
30
  ]
31
31
  },
@@ -105,7 +105,7 @@
105
105
  "hashes": [
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "18d5f55ed3c56718e470fef8bd677c55de08f4505e188b1dd29a0f0bbec09d4b"
108
+ "content": "818293a250cdf1bd10b5d69267b714d9a8ab90017c1fa18de29fc24e28ad8a55"
109
109
  }
110
110
  ]
111
111
  },
@@ -115,7 +115,7 @@
115
115
  "hashes": [
116
116
  {
117
117
  "alg": "SHA-256",
118
- "content": "c47d11fd3ed7e6049b1080cd9292ed3498e113d8bc74a9af65a027d70fa96435"
118
+ "content": "9c37499ed55b571232b5718c863b83b31f7a79faab46a9dcbe3283b993b1862a"
119
119
  }
120
120
  ]
121
121
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "684ea0c6fa4edbbbea1ffcd943167cd374166eb196100433d4e8d02d70560794"
128
+ "content": "6cf537925ccb4b1ffff84c8fd255b487438b78b56a46f9d91d33b25500ae520d"
129
129
  }
130
130
  ]
131
131
  },
@@ -185,7 +185,7 @@
185
185
  "hashes": [
186
186
  {
187
187
  "alg": "SHA-256",
188
- "content": "154755702a4652597bf8ca1410f9a3303b76c6a8dae8dacd867921a88c7b3626"
188
+ "content": "7abb07cdda09bea2b8e948cecdbaed4579371e8d0e8cee4436e7fcb20f430cdd"
189
189
  }
190
190
  ]
191
191
  },
@@ -205,7 +205,7 @@
205
205
  "hashes": [
206
206
  {
207
207
  "alg": "SHA-256",
208
- "content": "0dce827134ab5f7aaf55bd76325b5e1e17881770310e64e0d66bafbedc91687e"
208
+ "content": "d64af623372238bb5f2ead34e733130d7b013c49058a26f4795f09586b2b5dda"
209
209
  }
210
210
  ]
211
211
  },
@@ -225,7 +225,7 @@
225
225
  "hashes": [
226
226
  {
227
227
  "alg": "SHA-256",
228
- "content": "7d6ba26ec1188db3b3ac89f50b015ba1f0c6117b5c30d99b22da41da10c96c12"
228
+ "content": "555aed692218a73f23a343df2f2b38ce028e694dd9d4aff37e727096f88eb50e"
229
229
  }
230
230
  ]
231
231
  },
package/src/cli.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
 
7
- import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
7
+ import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { execFileSync, execSync, spawn } from 'node:child_process';
@@ -107,6 +107,11 @@ import {
107
107
  RECALL_USAGE_LOG_MAX_BYTES,
108
108
  parseRecallUsageLog,
109
109
  buildRecallUsageReport,
110
+ EVENT_CHAIN_TAIL_BYTES,
111
+ EMPTY_LOG_TAIL,
112
+ readTailInfo,
113
+ appendChainedLines,
114
+ verifyEventChainText,
110
115
  buildManifest,
111
116
  buildSbom,
112
117
  resolveTrustRoot,
@@ -269,6 +274,7 @@ import {
269
274
  import type { IdeaRecord, IdeaStatus } from '@dzhechkov/harness-core';
270
275
  import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
271
276
  import type { SetupSpec } from '@dzhechkov/harness-core';
277
+ import type { LogTail } from '@dzhechkov/harness-core';
272
278
  import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow, GateExecution, GateStep } from '@dzhechkov/harness-core';
273
279
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
274
280
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
@@ -5045,11 +5051,44 @@ function runGuardEvaluation(root: string, op: string, text: string | undefined,
5045
5051
  try {
5046
5052
  const rec = auditRecord(result, new Date().toISOString(), overrideReason !== undefined ? { reason: overrideReason } : undefined);
5047
5053
  mkdirSync(join(root, '.dz'), { recursive: true });
5048
- writeFileSync(join(root, '.dz', 'guard-audit.jsonl'), JSON.stringify(rec) + '\n', { flag: 'a' });
5054
+ const auditPath = join(root, '.dz', 'guard-audit.jsonl');
5055
+ // event-chain (ADR-001): seq + prevHash derived from the LAST LINE ONLY — this file is the
5056
+ // evidence base `dz guard promote` decides on, and a rewrite that loses or duplicates a record
5057
+ // must not be able to look intact. A tail that cannot be read starts a MARKED segment rather
5058
+ // than blocking the audit: the verdict is never held hostage to a broken log.
5059
+ writeFileSync(auditPath, appendChainedLines([rec], readLogTail(auditPath)), { flag: 'a' });
5049
5060
  } catch { /* audit is best-effort, never blocks the verdict */ }
5050
5061
  return result;
5051
5062
  }
5052
5063
 
5064
+ /**
5065
+ * The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
5066
+ * lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
5067
+ * yields {@link EMPTY_LOG_TAIL}; the caller then starts a marked segment rather than blocking.
5068
+ */
5069
+ function readLogTail(path: string): LogTail {
5070
+ let fd: number | undefined;
5071
+ try {
5072
+ if (!existsSync(path)) return EMPTY_LOG_TAIL;
5073
+ fd = openSync(path, 'r');
5074
+ const size = fstatSync(fd).size;
5075
+ if (!Number.isFinite(size) || size <= 0) return EMPTY_LOG_TAIL;
5076
+ const want = Math.min(size, EVENT_CHAIN_TAIL_BYTES);
5077
+ const buf = Buffer.alloc(want);
5078
+ readSync(fd, buf, 0, want, size - want);
5079
+ return readTailInfo(buf.toString('utf-8'), { partial: want < size });
5080
+ } catch {
5081
+ // A read FAILURE is not an empty file (Codex re-QE LOW): EMPTY_LOG_TAIL means "there is
5082
+ // nothing", which lets the writer open an UNMARKED genesis on a file we merely failed to
5083
+ // read. An unreadable tail must force a marked reset, per the AM-6 contract.
5084
+ return { ...EMPTY_LOG_TAIL, unreadable: true };
5085
+ } finally {
5086
+ if (fd !== undefined) {
5087
+ try { closeSync(fd); } catch { /* nothing to do */ }
5088
+ }
5089
+ }
5090
+ }
5091
+
5053
5092
  // ── `dz guard promote` (feature guard-promotion, scout idea #1) ─────────────────────────────────
5054
5093
 
5055
5094
  const PROMOTIONS_DIR = join('features', 'guard-promotion', 'promotions');
@@ -6290,7 +6329,19 @@ function cmdCompounding(options: Map<string, string>, flags: Set<string>, cwd: s
6290
6329
  /* no audit yet */
6291
6330
  }
6292
6331
 
6293
- const report = assembleCompoundingReport({ lessons, usage, guard, nowTs: new Date().toISOString() });
6332
+ // The evidence logs themselves, verbatim: the report verifies their hash chains (feature
6333
+ // event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
6334
+ // "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
6335
+ const evidenceLogs: { log: string; text: string }[] = [];
6336
+ for (const rel of ['.dz/recall-usage.jsonl', '.dz/guard-audit.jsonl']) {
6337
+ try {
6338
+ evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
6339
+ } catch {
6340
+ /* absent log — reported by its own gate above, not invented here */
6341
+ }
6342
+ }
6343
+
6344
+ const report = assembleCompoundingReport({ lessons, usage, guard, nowTs: new Date().toISOString(), evidenceLogs });
6294
6345
  if (json) write(JSON.stringify({ ...report, exitCode: 0 }, null, 2));
6295
6346
  else write(renderCompoundingReport(report));
6296
6347
  return 0;