@dzhechkov/harness-cli 0.5.1 → 0.5.3

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.
@@ -39,7 +39,7 @@
39
39
  * standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
40
40
  * because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
41
41
  */
42
- export declare const MIN_CORE = "0.5.1";
42
+ export declare const MIN_CORE = "0.5.3";
43
43
  /** The npm name of the guarded package — one literal, used by every leg below. */
44
44
  export declare const CORE_PACKAGE_NAME = "@dzhechkov/harness-core";
45
45
  /**
@@ -42,7 +42,7 @@ import { fileURLToPath } from 'node:url';
42
42
  * standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
43
43
  * because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
44
44
  */
45
- export const MIN_CORE = '0.5.1';
45
+ export const MIN_CORE = '0.5.3';
46
46
  /** The npm name of the guarded package — one literal, used by every leg below. */
47
47
  export const CORE_PACKAGE_NAME = '@dzhechkov/harness-core';
48
48
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",
@@ -40,8 +40,15 @@
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
- "@dzhechkov/harness-core": "^0.5.1",
51
+ "@dzhechkov/harness-core": "^0.5.3",
45
52
  "@dzhechkov/harness-presets": "^0.5.0",
46
53
  "@dzhechkov/scout": "^0.8.0",
47
54
  "@dzhechkov/skills-devops": "^0.3.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
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "6d4e868f0fef3d86f83c6316f4bb760fd9228ef1008d3bbb31916f6c8890b944"
28
+ "content": "0d475969dbc98308bf0e23a15700c537ed4947b3cf910b334e647e3972fc78fe"
29
29
  }
30
30
  ]
31
31
  },
@@ -105,7 +105,7 @@
105
105
  "hashes": [
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "514bbdd5b14a1257693418c64bf2fb6c13d78beccd307f0f3a9c66461a42a62f"
108
+ "content": "7c64ed0d976d79fd8f3970ced13e664db1e97c43f96a7b153d7b993948cc5f9f"
109
109
  }
110
110
  ]
111
111
  },
@@ -115,7 +115,7 @@
115
115
  "hashes": [
116
116
  {
117
117
  "alg": "SHA-256",
118
- "content": "09277bf170699a591ca9d2c8f6ac3b2727ec41cff3a75e56058724bd1e4686e1"
118
+ "content": "8052367d84a5a1c4a9f7fb3397a6297edf707031bbfa59b945d240d17e221f04"
119
119
  }
120
120
  ]
121
121
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "e2a46281a86931f342cf726ae0fd0aeb25eef7ce94edbd07abce68635f547e78"
128
+ "content": "f759949bcfaa04c3f4eaebbccde688192d44f129b61115313533f8828f7376f8"
129
129
  }
130
130
  ]
131
131
  },
@@ -135,7 +135,7 @@
135
135
  "hashes": [
136
136
  {
137
137
  "alg": "SHA-256",
138
- "content": "868dd0e0ec12ea9df5f4df148518f5c59226ecc1de04fa67d5455f4932a7819b"
138
+ "content": "f89dbd282c83fa64f23c8fff3c83bc85c08c8deef451fd4d525e33784c2a0e13"
139
139
  }
140
140
  ]
141
141
  },
@@ -155,7 +155,7 @@
155
155
  "hashes": [
156
156
  {
157
157
  "alg": "SHA-256",
158
- "content": "973e9bd7c0951e9f74bd3e8b38a0cc81076d6ea8393fa3e52a428b92ebc967c8"
158
+ "content": "66fd34797f007aa0607698a159fb367a772f32f5be1c2a90748c20cbf4b973cf"
159
159
  }
160
160
  ]
161
161
  },
@@ -225,7 +225,7 @@
225
225
  "hashes": [
226
226
  {
227
227
  "alg": "SHA-256",
228
- "content": "503d43d8ab6e7fb04c28f96270eb00da9782e4ec0570772f2f701aef33037c25"
228
+ "content": "797bfd035a68dc039ca33393f02f4ed1fe97e1021559e01906a0fbbc9b28c1dc"
229
229
  }
230
230
  ]
231
231
  },
@@ -245,7 +245,7 @@
245
245
  "hashes": [
246
246
  {
247
247
  "alg": "SHA-256",
248
- "content": "747d3a279349208ce8f5c6121bbf8ea039ebafaa8251c520ba3092a7aa80013f"
248
+ "content": "9338e5f5dd5cbb1f3692cb7db53531f103bc28450c109765bf161664a7598a5b"
249
249
  }
250
250
  ]
251
251
  },
@@ -255,7 +255,7 @@
255
255
  "hashes": [
256
256
  {
257
257
  "alg": "SHA-256",
258
- "content": "53747612facd41ade5c291949b854d354e694b733e1f294cb3e521193760ae26"
258
+ "content": "ba96d37f80c98618a1d8ca61cf071c9d26c74dab8d277106e9079068428a5b71"
259
259
  }
260
260
  ]
261
261
  },
@@ -679,6 +679,16 @@
679
679
  }
680
680
  ]
681
681
  },
682
+ {
683
+ "type": "file",
684
+ "name": "test/roam-nested-archive.test.ts",
685
+ "hashes": [
686
+ {
687
+ "alg": "SHA-256",
688
+ "content": "0c078b6e03cc749a64e0ac327885de1eea3c13e940c8e3f231174ab84076d44c"
689
+ }
690
+ ]
691
+ },
682
692
  {
683
693
  "type": "file",
684
694
  "name": "test/skills-verify-static-advisories.test.ts",
@@ -779,6 +789,16 @@
779
789
  }
780
790
  ]
781
791
  },
792
+ {
793
+ "type": "file",
794
+ "name": "test/workflow-trace-attestation-cli.test.ts",
795
+ "hashes": [
796
+ {
797
+ "alg": "SHA-256",
798
+ "content": "dad2efd147d9186b29989ef36f6e62614c4ded983d7894a7c1f9b1628e621871"
799
+ }
800
+ ]
801
+ },
782
802
  {
783
803
  "type": "file",
784
804
  "name": "test/workflow-trace-cli-surface.test.ts",
package/src/cli.ts CHANGED
@@ -103,6 +103,10 @@ import {
103
103
  parseTrace,
104
104
  assembleTimeline,
105
105
  runInvariants,
106
+ deriveAttestation,
107
+ stampAttestation,
108
+ corroborate,
109
+ NOT_WITNESSED,
106
110
  renderTimelineHtml,
107
111
  importEcc,
108
112
  recordPattern,
@@ -425,7 +429,7 @@ Usage:
425
429
  dz workflow render <plan.json> --o <script.js> [--check] [--force] (plan → region-delimited loop script; USER regions preserved)
426
430
  dz workflow blobs [--check] (list/self-check the subsystem blob registry)
427
431
  dz workflow-lint <script.js> [--plan <plan.json>] [--require-plan|--legacy] [--json] (layer-1 gate; exit 0/1/3 — inconclusive is never a pass)
428
- dz workflow-trace <runDir|--slug <s>|--run <id>> [--invariants <plan.json>] [--html <out.html>] [--json] (timeline + SEQ invariant runner)
432
+ dz workflow-trace <runDir|--slug <s>|--run <id>> [--invariants <plan.json>] [--corroborate <hostRunDir>] [--html <out.html>] [--json] (timeline + SEQ invariant runner; ALWAYS reports who attested the trace — instrument|agent|unknown — and --corroborate checks the Claude host's own records for the half they can witness)
429
433
  dz workflow-trace export <run> --o <file> [--include-pairs --yes] [--strict] (one run's telemetry as ONE movable file: events, not aggregates; degradation is typed and LOUD, --strict fails closed)
430
434
  dz workflow-trace import <bundle> --into <root> [--force] [--with-pairs] (reconstruct that run under an explicit root; FAIL-CLOSED — never writes over a run that already has content)
431
435
  dz install <npm-pkg> [--target <name>] [--project <dir>] [--force]
@@ -1577,6 +1581,57 @@ function cmdWorkflowTrace(options: Map<string, string>, flags: Set<string>, cwd:
1577
1581
  const ckptFile = join(runDir, '.fa-state', 'checkpoints.jsonl');
1578
1582
  const ledgerFile = resolve(cwd, '.dz', 'feature-adr', 'run-cost-ledger.jsonl');
1579
1583
  const journalFile = join(runDir, 'journal.jsonl');
1584
+ // ── provenance (feature honest-trace-provenance) ──────────────────────────────────────────────
1585
+ // The attestation is DERIVED here, from run-state.json — an artifact the sandboxed rendered
1586
+ // script cannot write. It is never read out of the trace itself (ADR-001).
1587
+ const parsedRun = parseTrace(traceText);
1588
+ const runStateFile = join(runDir, 'run-state.json');
1589
+ const runStateForAttestation = existsSync(runStateFile)
1590
+ ? ((): Record<string, unknown> | null => {
1591
+ try {
1592
+ return JSON.parse(readFileSync(runStateFile, 'utf8')) as Record<string, unknown>;
1593
+ } catch {
1594
+ return null; // an unreadable state binds nothing — the reader will say `unknown`
1595
+ }
1596
+ })()
1597
+ : null;
1598
+ const attestation = deriveAttestation(parsedRun, runStateForAttestation, {
1599
+ sha256: createHash('sha256').update(traceText, 'utf8').digest('hex'),
1600
+ lines: traceText.split('\n').filter((l) => l.trim() !== '').length,
1601
+ });
1602
+ const ATTESTATION_SENTENCE: Record<typeof attestation, string> = {
1603
+ instrument: 'ATTESTATION instrument — the bytes read match the identifiers, hash and line count asserted by the co-located run-state.json. That is NOT proof that dz historically wrote them, nor that this is the directory it wrote them in.',
1604
+ agent: 'ATTESTATION agent — this trace was appended by an AGENT asked to run the flush command, not by an instrument. Treat every verdict below as testimony.',
1605
+ unknown: 'ATTESTATION unknown — no binding run-state.json and no emitter declaration. Not instrument; treat as testimony.',
1606
+ };
1607
+ // ── corroboration (ADR-002, opt-in) ───────────────────────────────────────────────────────────
1608
+ // The host's OWN records, for the half they can witness. Directory containment is the ONLY
1609
+ // binding available — trace and journal.jsonl share no identifier — and every result says so.
1610
+ const corroborateDir = options.get('corroborate');
1611
+ let corroboration: ReturnType<typeof corroborate> | null = null;
1612
+ if (corroborateDir !== undefined) {
1613
+ const hostDir = resolve(cwd, corroborateDir);
1614
+ const jf = join(hostDir, 'journal.jsonl');
1615
+ const transcripts: Record<string, string> = {};
1616
+ try {
1617
+ for (const f of readdirSync(hostDir)) {
1618
+ const m = /^agent-(.+)\.jsonl$/.exec(f);
1619
+ if (m) transcripts[m[1]!] = readFileSync(join(hostDir, f), 'utf8');
1620
+ }
1621
+ } catch {
1622
+ /* an unreadable host dir is INCONCLUSIVE, decided by corroborate() below — never a throw */
1623
+ }
1624
+ // The trace side, projected: the agent ids the trace claims took part, in seq order. The trace
1625
+ // has no agentId of its own, so the invocationId is what we can offer — and that is exactly why
1626
+ // the binding is by-directory and the verdict is scoped.
1627
+ const claimed: string[] = [];
1628
+ for (const e of parsedRun.events) if (e.event === 'dispatched') claimed.push(e.invocationId);
1629
+ corroboration = corroborate(
1630
+ { agentIds: claimed },
1631
+ { journal: existsSync(jf) ? readFileSync(jf, 'utf8') : null, agentTranscripts: transcripts },
1632
+ hostDir,
1633
+ );
1634
+ }
1580
1635
  const timeline = assembleTimeline({
1581
1636
  trace: traceText,
1582
1637
  checkpoints: existsSync(ckptFile) ? readFileSync(ckptFile, 'utf8') : null,
@@ -1598,7 +1653,7 @@ function cmdWorkflowTrace(options: Map<string, string>, flags: Set<string>, cwd:
1598
1653
  return 1;
1599
1654
  }
1600
1655
  projection = toTraceProjection(parsed);
1601
- verdicts = runInvariants(projection, parseTrace(traceText));
1656
+ verdicts = stampAttestation(runInvariants(projection, parsedRun), attestation);
1602
1657
  }
1603
1658
  const htmlOut = options.get('html');
1604
1659
  if (htmlOut !== undefined) {
@@ -1607,9 +1662,14 @@ function cmdWorkflowTrace(options: Map<string, string>, flags: Set<string>, cwd:
1607
1662
  write(`dz workflow-trace: wrote ${absHtml} (self-contained: mermaid topology + HTML waterfall)`);
1608
1663
  }
1609
1664
  if (flags.has('json')) {
1610
- write(JSON.stringify({ timeline, verdicts }, null, 2));
1665
+ write(JSON.stringify({ timeline, attestation, corroboration, verdicts }, null, 2));
1611
1666
  return verdicts.some((v) => v.status === 'fail') ? 1 : 0;
1612
1667
  }
1668
+ write(ATTESTATION_SENTENCE[attestation]);
1669
+ if (corroboration !== null) {
1670
+ write(`CORROBORATION ${corroboration.verdict} (binding: ${corroboration.binding}, ${corroboration.hostDir}) — ${corroboration.detail}`);
1671
+ write(` witnessed: ${corroboration.witnessed.join(', ')} NOT witnessed: ${NOT_WITNESSED.join(', ')}`);
1672
+ }
1613
1673
  write(`run ${timeline.runId ?? '(unknown)'}${timeline.incomplete ? ' — INCOMPLETE (no run.closed; unflushed tail may be lost)' : ''}; sources: ${timeline.sources.join(', ')}`);
1614
1674
  for (const r of timeline.rows.filter((x) => x.kind === 'trace')) {
1615
1675
  write(` ${String(r.seq).padStart(5)} ${r.label} ${r.detail}${r.wallTime ? ` [wall ${r.wallTime} — diagnostic only]` : ''}`);
@@ -1661,6 +1721,17 @@ function cmdRoam(options: Map<string, string>, flags: Set<string>, cwd: string,
1661
1721
  return 1;
1662
1722
  }
1663
1723
 
1724
+ // The roam archive is its OWN git repository (owner decision 2026-08-20), so `git rev-parse` run
1725
+ // from inside it resolves to the archive, not the project. Left unguarded that computes
1726
+ // `<repo>/roam/roam/claude-state` and, with --apply, creates a symlink under a fabricated slug.
1727
+ // Detect the nested archive by its own shape rather than by its name alone: a directory called
1728
+ // `roam` that CONTAINS `claude-state` is the archive, wherever it lives.
1729
+ if (basename(repoRoot) === 'roam' && existsSync(join(repoRoot, 'claude-state'))) {
1730
+ write('dz roam: this is the roam ARCHIVE, not the project — its git root is its own.');
1731
+ write(` Run from the project root instead: cd ${dirname(repoRoot)} && dz roam`);
1732
+ return 1;
1733
+ }
1734
+
1664
1735
  const roamDir = join(repoRoot, 'roam', 'claude-state');
1665
1736
  const claudeProjectsDir = process.env['CLAUDE_PROJECTS_DIR'] ?? join(homedir(), '.claude', 'projects');
1666
1737
 
@@ -8424,6 +8495,13 @@ function wfMakeStore(runDir: string, repoRoot: string, targetCwd: string): RunSt
8424
8495
  runDirExists: () => existsSync(runDir),
8425
8496
  hasTrace: () => existsSync(tracePath),
8426
8497
  readTraceText: () => (existsSync(tracePath) ? readFileSync(tracePath, 'utf8') : null),
8498
+ // The CONTENT half of the attestation binding. Hashes the file AS IT STANDS, so it must be
8499
+ // called after the final flush — the scheduler's stampTraceBinding is the single caller.
8500
+ measureTrace: () => {
8501
+ if (!existsSync(tracePath)) return null;
8502
+ const text = readFileSync(tracePath, 'utf8');
8503
+ return { sha256: createHash('sha256').update(text, 'utf8').digest('hex'), lines: text.split('\n').filter((l) => l.trim() !== '').length };
8504
+ },
8427
8505
  readRunState: () => wfReadJson(statePath) as WfRunState | null,
8428
8506
  writeRunState: (s) => {
8429
8507
  mkdirSync(runDir, { recursive: true });
@@ -44,7 +44,7 @@ import { fileURLToPath } from 'node:url';
44
44
  * standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
45
45
  * because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
46
46
  */
47
- export const MIN_CORE = '0.5.1';
47
+ export const MIN_CORE = '0.5.3';
48
48
 
49
49
  /** The npm name of the guarded package — one literal, used by every leg below. */
50
50
  export const CORE_PACKAGE_NAME = '@dzhechkov/harness-core';
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.