@kaiohenricunha/harness 0.2.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/package.json +68 -0
  4. package/plugins/harness/.claude-plugin/plugin.json +8 -0
  5. package/plugins/harness/README.md +74 -0
  6. package/plugins/harness/bin/harness-check-instruction-drift.mjs +77 -0
  7. package/plugins/harness/bin/harness-check-spec-coverage.mjs +81 -0
  8. package/plugins/harness/bin/harness-detect-drift.mjs +53 -0
  9. package/plugins/harness/bin/harness-doctor.mjs +145 -0
  10. package/plugins/harness/bin/harness-init.mjs +89 -0
  11. package/plugins/harness/bin/harness-validate-skills.mjs +92 -0
  12. package/plugins/harness/bin/harness-validate-specs.mjs +70 -0
  13. package/plugins/harness/bin/harness.mjs +93 -0
  14. package/plugins/harness/hooks/guard-destructive-git.sh +58 -0
  15. package/plugins/harness/scripts/auto-update-manifest.mjs +20 -0
  16. package/plugins/harness/scripts/detect-branch-drift.mjs +81 -0
  17. package/plugins/harness/scripts/lib/output.sh +105 -0
  18. package/plugins/harness/scripts/refresh-worktrees.sh +35 -0
  19. package/plugins/harness/scripts/validate-settings.sh +202 -0
  20. package/plugins/harness/src/check-instruction-drift.mjs +127 -0
  21. package/plugins/harness/src/check-spec-coverage.mjs +95 -0
  22. package/plugins/harness/src/index.mjs +57 -0
  23. package/plugins/harness/src/init-harness-scaffold.mjs +121 -0
  24. package/plugins/harness/src/lib/argv.mjs +108 -0
  25. package/plugins/harness/src/lib/debug.mjs +37 -0
  26. package/plugins/harness/src/lib/errors.mjs +147 -0
  27. package/plugins/harness/src/lib/exit-codes.mjs +18 -0
  28. package/plugins/harness/src/lib/output.mjs +90 -0
  29. package/plugins/harness/src/spec-harness-lib.mjs +359 -0
  30. package/plugins/harness/src/validate-skills-inventory.mjs +148 -0
  31. package/plugins/harness/src/validate-specs.mjs +217 -0
  32. package/plugins/harness/templates/claude/hooks/guard-destructive-git.sh +50 -0
  33. package/plugins/harness/templates/claude/settings.headless.json +24 -0
  34. package/plugins/harness/templates/claude/settings.json +16 -0
  35. package/plugins/harness/templates/claude/skills-manifest.json +6 -0
  36. package/plugins/harness/templates/docs/repo-facts.json +17 -0
  37. package/plugins/harness/templates/docs/specs/README.md +36 -0
  38. package/plugins/harness/templates/githooks/pre-commit +9 -0
  39. package/plugins/harness/templates/workflows/ai-review.yml +28 -0
  40. package/plugins/harness/templates/workflows/detect-drift.yml +15 -0
  41. package/plugins/harness/templates/workflows/validate-skills.yml +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaio Henrique Cunha
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.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # `@kaiohenricunha/harness`
2
+
3
+ [![npm](https://img.shields.io/npm/v/@kaiohenricunha/harness.svg)](https://www.npmjs.com/package/@kaiohenricunha/harness)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+ [![changelog](https://img.shields.io/badge/changelog-keep--a--changelog-orange.svg)](./CHANGELOG.md)
6
+
7
+ Portable Claude Code plugin + zero-dependency npm package that bootstraps
8
+ spec-driven-development governance into consumer repos. Ships a structured-error
9
+ CLI, an umbrella `harness` dispatcher, seven standalone bins, a destructive-git
10
+ PreToolUse hook, and a gold-standard shell settings validator.
11
+
12
+ **Two personas live in this repo** (by design — see [docs/personas.md](./docs/personas.md)):
13
+
14
+ - The **npm package** under `plugins/harness/` (what consumers install).
15
+ - Kaio's **personal dotfiles** at the top level (symlinked into `~/.claude/` via `bootstrap.sh`).
16
+
17
+ If you're installing the package, ignore the top-level scripts —
18
+ `package.json.files` excludes them from the tarball.
19
+
20
+ ---
21
+
22
+ ## Consumer quickstart
23
+
24
+ ```bash
25
+ npm i -D @kaiohenricunha/harness
26
+ npx harness-init --project-name my-project --project-type node
27
+ npx harness-doctor # self-diagnostic
28
+ npx harness-validate-specs # every bin works standalone or via `npx harness <sub>`
29
+ ```
30
+
31
+ Five minutes end-to-end: [docs/quickstart.md](./docs/quickstart.md).
32
+
33
+ ### Node API
34
+
35
+ ```js
36
+ import {
37
+ createHarnessContext,
38
+ validateSpecs,
39
+ validateManifest,
40
+ checkSpecCoverage,
41
+ checkInstructionDrift,
42
+ scaffoldHarness,
43
+ ValidationError,
44
+ ERROR_CODES,
45
+ EXIT_CODES,
46
+ } from "@kaiohenricunha/harness";
47
+
48
+ const ctx = createHarnessContext(); // resolves repo root via git
49
+ const { ok, errors } = validateSpecs(ctx); // errors are ValidationError instances
50
+ if (!ok) {
51
+ for (const err of errors) {
52
+ if (err.code === ERROR_CODES.SPEC_STATUS_INVALID) {
53
+ // programmatic reaction to a specific failure class
54
+ }
55
+ }
56
+ process.exit(EXIT_CODES.VALIDATION);
57
+ }
58
+ ```
59
+
60
+ Full contract: [docs/api-reference.md](./docs/api-reference.md).
61
+
62
+ ### CLI contract
63
+
64
+ Every bin honors `--help`, `--version`, `--json`, `--verbose`, `--no-color` and exits with the named enum:
65
+
66
+ | Code | Name | Meaning |
67
+ | ---- | ---------- | ------------------------------------------------------ |
68
+ | 0 | OK | Success |
69
+ | 1 | VALIDATION | Rule failure (expected failure mode) |
70
+ | 2 | ENV | Misconfigured environment |
71
+ | 64 | USAGE | Bad CLI invocation (matches BSD `sysexits.h EX_USAGE`) |
72
+
73
+ Per-bin details: [docs/cli-reference.md](./docs/cli-reference.md).
74
+
75
+ ---
76
+
77
+ ## Hardening decisions
78
+
79
+ Each row links to its ADR (see [docs/adr/](./docs/adr/)):
80
+
81
+ | Decision | ADR |
82
+ | ---------------------------------------- | ------------------------------------------------------- |
83
+ | Monorepo dual-persona layout | [0001](./docs/adr/0001-monorepo-dual-persona-layout.md) |
84
+ | No TypeScript; JSDoc + zero runtime deps | [0002](./docs/adr/0002-no-typescript.md) |
85
+ | Structured `ValidationError` contract | [0012](./docs/adr/0012-structured-error-contract.md) |
86
+ | Exit-code convention `{0,1,2,64}` | [0013](./docs/adr/0013-exit-code-convention.md) |
87
+ | CLI ✓/✗/⚠ output format | [0014](./docs/adr/0014-cli-tick-cross-warn-format.md) |
88
+
89
+ Shell-level hardening (SEC-1..4, OPS-1..2) is enforced today at
90
+ `plugins/harness/scripts/validate-settings.sh`; its 12-case behavioral
91
+ suite at `plugins/harness/tests/test_validate_settings.sh` pins every
92
+ contract.
93
+
94
+ ---
95
+
96
+ ## Personal dotfiles persona
97
+
98
+ If you're Kaio (or forking for your own dotfiles), the entry-point is:
99
+
100
+ ```bash
101
+ git clone https://github.com/kaiohenricunha/dotclaude.git ~/projects/kaiohenricunha/dotclaude
102
+ cd ~/projects/kaiohenricunha/dotclaude
103
+ ./bootstrap.sh # symlinks commands/ + skills/ + CLAUDE.md into ~/.claude/
104
+ ./sync.sh pull # pull + re-bootstrap
105
+ ./sync.sh push # secret-scan + commit + push
106
+ ```
107
+
108
+ See [CLAUDE.md](./CLAUDE.md) for the global rules this installs.
109
+
110
+ ---
111
+
112
+ ## Further reading
113
+
114
+ | | |
115
+ | ---------------------------------------------------- | ------------------------------------------- |
116
+ | [docs/index.md](./docs/index.md) | Nav map with persona-tailored entry points |
117
+ | [docs/quickstart.md](./docs/quickstart.md) | Install → scaffold → first green validator |
118
+ | [docs/cli-reference.md](./docs/cli-reference.md) | Every bin, flag, exit code, `--json` schema |
119
+ | [docs/api-reference.md](./docs/api-reference.md) | Node API surface |
120
+ | [docs/architecture.md](./docs/architecture.md) | Layer diagram + PR-time sequence |
121
+ | [docs/troubleshooting.md](./docs/troubleshooting.md) | Error-code → remediation index |
122
+ | [docs/upgrade-guide.md](./docs/upgrade-guide.md) | 0.1 → 0.2 migration, forking |
123
+ | [docs/personas.md](./docs/personas.md) | Who reads which file |
124
+ | [CONTRIBUTING.md](./CONTRIBUTING.md) | Dev workflow + local gates |
125
+ | [SECURITY.md](./SECURITY.md) | Private vulnerability disclosure |
126
+ | [CHANGELOG.md](./CHANGELOG.md) | Keep-a-Changelog history |
127
+
128
+ ## License
129
+
130
+ MIT — see [LICENSE](./LICENSE).
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@kaiohenricunha/harness",
3
+ "version": "0.2.0",
4
+ "description": "Portable harness + SDD validators for Claude Code projects.",
5
+ "type": "module",
6
+ "main": "plugins/harness/src/index.mjs",
7
+ "exports": {
8
+ ".": "./plugins/harness/src/index.mjs",
9
+ "./errors": "./plugins/harness/src/lib/errors.mjs",
10
+ "./exit-codes": "./plugins/harness/src/lib/exit-codes.mjs",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "bin": {
14
+ "harness": "./plugins/harness/bin/harness.mjs",
15
+ "harness-doctor": "./plugins/harness/bin/harness-doctor.mjs",
16
+ "harness-detect-drift": "./plugins/harness/bin/harness-detect-drift.mjs",
17
+ "harness-validate-skills": "./plugins/harness/bin/harness-validate-skills.mjs",
18
+ "harness-check-spec-coverage": "./plugins/harness/bin/harness-check-spec-coverage.mjs",
19
+ "harness-validate-specs": "./plugins/harness/bin/harness-validate-specs.mjs",
20
+ "harness-check-instruction-drift": "./plugins/harness/bin/harness-check-instruction-drift.mjs",
21
+ "harness-init": "./plugins/harness/bin/harness-init.mjs"
22
+ },
23
+ "scripts": {
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "coverage": "vitest run --coverage",
27
+ "lint": "echo 'lint target stubs — prettier + markdownlint wiring lands in PR 4/6' && exit 0",
28
+ "shellcheck": "shellcheck -x bootstrap.sh sync.sh plugins/harness/scripts/*.sh plugins/harness/hooks/*.sh plugins/harness/tests/*.sh plugins/harness/templates/claude/hooks/*.sh plugins/harness/templates/githooks/pre-commit",
29
+ "dogfood": "npx harness-validate-skills && npx harness-validate-specs && npx harness-check-instruction-drift && npx harness-check-spec-coverage"
30
+ },
31
+ "files": [
32
+ "plugins/harness/src/",
33
+ "plugins/harness/bin/",
34
+ "plugins/harness/scripts/",
35
+ "plugins/harness/templates/",
36
+ "plugins/harness/hooks/",
37
+ "plugins/harness/README.md",
38
+ "plugins/harness/.claude-plugin/"
39
+ ],
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "devDependencies": {
44
+ "@vitest/coverage-v8": "^4.1.4",
45
+ "bats": "^1.11.1",
46
+ "vitest": "^4.1.4"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/kaiohenricunha/dotclaude.git"
51
+ },
52
+ "homepage": "https://github.com/kaiohenricunha/dotclaude#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/kaiohenricunha/dotclaude/issues"
55
+ },
56
+ "keywords": [
57
+ "claude-code",
58
+ "claude",
59
+ "harness",
60
+ "sdd",
61
+ "spec-driven",
62
+ "validator",
63
+ "linter",
64
+ "governance",
65
+ "cli"
66
+ ],
67
+ "license": "MIT"
68
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "harness",
3
+ "description": "Portable SDD + harness engineering kit: skills-manifest validator, spec-coverage gate, spec-kit validators, instruction-drift detector, destructive-git hook, and `/init-harness` scaffolder. Works in any repo that follows the repo-facts.json + docs/specs/ conventions.",
4
+ "author": {
5
+ "name": "Kaio Cunha",
6
+ "email": "kaiohenricunha@example.com"
7
+ }
8
+ }
@@ -0,0 +1,74 @@
1
+ # `@kaiohenricunha/harness`
2
+
3
+ Portable Claude Code plugin + zero-dependency npm package for
4
+ spec-driven-development governance. Installs seven CLI bins, a Node API
5
+ barrel, a destructive-git PreToolUse hook, and a gold-standard shell
6
+ settings validator.
7
+
8
+ This README is the npm tarball's entry point. **The full docs set lives at
9
+ <https://github.com/kaiohenricunha/dotclaude/tree/main/docs>.**
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -D @kaiohenricunha/harness
15
+ ```
16
+
17
+ Zero runtime dependencies. Engines: Node `>=20`.
18
+
19
+ ## Scaffold + validate
20
+
21
+ ```bash
22
+ npx harness-init --project-name my-project --project-type node
23
+ npx harness-doctor # self-diagnostic
24
+ npx harness-validate-specs # or: npx harness validate-specs
25
+ ```
26
+
27
+ ## Node API
28
+
29
+ ```js
30
+ import {
31
+ createHarnessContext,
32
+ validateSpecs,
33
+ validateManifest,
34
+ checkSpecCoverage,
35
+ checkInstructionDrift,
36
+ scaffoldHarness,
37
+ ValidationError,
38
+ ERROR_CODES,
39
+ EXIT_CODES,
40
+ } from "@kaiohenricunha/harness";
41
+
42
+ const ctx = createHarnessContext();
43
+ const { ok, errors } = validateSpecs(ctx); // errors are ValidationError instances
44
+ ```
45
+
46
+ See [api-reference](https://github.com/kaiohenricunha/dotclaude/blob/main/docs/api-reference.md)
47
+ for the full surface.
48
+
49
+ ## Bins
50
+
51
+ - `harness` — umbrella dispatcher (`harness validate-specs`, `harness doctor`, …)
52
+ - `harness-doctor` — self-diagnostic
53
+ - `harness-init` — scaffold governance tree
54
+ - `harness-validate-specs`, `harness-validate-skills`
55
+ - `harness-check-spec-coverage`, `harness-check-instruction-drift`
56
+ - `harness-detect-drift`
57
+
58
+ Every bin supports `--help`, `--version`, `--json`, `--verbose`, `--no-color`.
59
+
60
+ ## Exit codes
61
+
62
+ `{OK:0, VALIDATION:1, ENV:2, USAGE:64}` — `64` mirrors BSD `sysexits.h EX_USAGE`.
63
+
64
+ ## License
65
+
66
+ MIT. See <https://github.com/kaiohenricunha/dotclaude/blob/main/LICENSE>.
67
+
68
+ ## Links
69
+
70
+ - [Changelog](https://github.com/kaiohenricunha/dotclaude/blob/main/CHANGELOG.md)
71
+ - [Contributing](https://github.com/kaiohenricunha/dotclaude/blob/main/CONTRIBUTING.md)
72
+ - [Security](https://github.com/kaiohenricunha/dotclaude/blob/main/SECURITY.md)
73
+ - [Quickstart](https://github.com/kaiohenricunha/dotclaude/blob/main/docs/quickstart.md)
74
+ - [Troubleshooting](https://github.com/kaiohenricunha/dotclaude/blob/main/docs/troubleshooting.md)
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness-check-instruction-drift — cross-references `docs/repo-facts.json`
4
+ * against instruction files (CLAUDE.md, README.md, …) to catch stale
5
+ * team_count claims, missing protected-path documentation, and broken
6
+ * instruction-file references.
7
+ *
8
+ * Exits: 0 no drift, 1 drift detected, 2 env error, 64 usage error.
9
+ */
10
+
11
+ import { parse, helpText } from "../src/lib/argv.mjs";
12
+ import { createOutput } from "../src/lib/output.mjs";
13
+ import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
14
+ import { formatError } from "../src/lib/errors.mjs";
15
+ import { version } from "../src/index.mjs";
16
+ import {
17
+ createHarnessContext,
18
+ checkInstructionDrift,
19
+ } from "../src/index.mjs";
20
+
21
+ const META = {
22
+ name: "harness-check-instruction-drift",
23
+ synopsis: "harness-check-instruction-drift [OPTIONS]",
24
+ description: "Detect drift between docs/repo-facts.json and instruction files (team_count, protected_paths, instruction_files).",
25
+ flags: {
26
+ "repo-root": { type: "string" },
27
+ },
28
+ };
29
+
30
+ let argv;
31
+ try {
32
+ argv = parse(process.argv.slice(2), META.flags);
33
+ } catch (err) {
34
+ process.stderr.write(`${err.message}\n`);
35
+ process.exit(EXIT_CODES.USAGE);
36
+ }
37
+
38
+ if (argv.help) {
39
+ process.stdout.write(`${helpText(META)}\n`);
40
+ process.exit(EXIT_CODES.OK);
41
+ }
42
+ if (argv.version) {
43
+ process.stdout.write(`${version}\n`);
44
+ process.exit(EXIT_CODES.OK);
45
+ }
46
+
47
+ const out = createOutput({ json: argv.json, noColor: argv.noColor });
48
+
49
+ let ctx;
50
+ try {
51
+ ctx = createHarnessContext({ repoRoot: argv.flags["repo-root"] });
52
+ } catch (err) {
53
+ out.fail(`could not resolve repo root: ${err.message}`);
54
+ out.flush();
55
+ process.exit(EXIT_CODES.ENV);
56
+ }
57
+
58
+ let result;
59
+ try {
60
+ result = checkInstructionDrift(ctx);
61
+ } catch (err) {
62
+ out.fail(`drift check failed: ${err.message}`);
63
+ out.flush();
64
+ process.exit(EXIT_CODES.ENV);
65
+ }
66
+
67
+ if (result.ok) {
68
+ out.pass("instruction files match repo facts");
69
+ out.flush();
70
+ process.exit(EXIT_CODES.OK);
71
+ }
72
+
73
+ for (const err of result.errors) {
74
+ out.fail(formatError(err, { verbose: argv.verbose }), err.toJSON ? err.toJSON() : undefined);
75
+ }
76
+ out.flush();
77
+ process.exit(EXIT_CODES.VALIDATION);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness-check-spec-coverage — verifies that every change to a protected
4
+ * path is covered by an approved/implementing/done spec, or the PR body
5
+ * carries a `## No-spec rationale` section.
6
+ *
7
+ * Exits: 0 covered, 1 violation, 2 env error, 64 usage error.
8
+ */
9
+
10
+ import { parse, helpText } from "../src/lib/argv.mjs";
11
+ import { createOutput } from "../src/lib/output.mjs";
12
+ import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
13
+ import { formatError } from "../src/lib/errors.mjs";
14
+ import { version } from "../src/index.mjs";
15
+ import {
16
+ createHarnessContext,
17
+ checkSpecCoverage,
18
+ getChangedFiles,
19
+ getPullRequestContext,
20
+ } from "../src/index.mjs";
21
+
22
+ const META = {
23
+ name: "harness-check-spec-coverage",
24
+ synopsis: "harness-check-spec-coverage [OPTIONS]",
25
+ description: "Check that protected-path changes are covered by a spec (or a No-spec rationale) in the current PR.",
26
+ flags: {
27
+ "repo-root": { type: "string" },
28
+ },
29
+ };
30
+
31
+ let argv;
32
+ try {
33
+ argv = parse(process.argv.slice(2), META.flags);
34
+ } catch (err) {
35
+ process.stderr.write(`${err.message}\n`);
36
+ process.exit(EXIT_CODES.USAGE);
37
+ }
38
+
39
+ if (argv.help) {
40
+ process.stdout.write(`${helpText(META)}\n`);
41
+ process.exit(EXIT_CODES.OK);
42
+ }
43
+ if (argv.version) {
44
+ process.stdout.write(`${version}\n`);
45
+ process.exit(EXIT_CODES.OK);
46
+ }
47
+
48
+ const out = createOutput({ json: argv.json, noColor: argv.noColor });
49
+
50
+ let ctx;
51
+ try {
52
+ ctx = createHarnessContext({ repoRoot: argv.flags["repo-root"] });
53
+ } catch (err) {
54
+ out.fail(`could not resolve repo root: ${err.message}`);
55
+ out.flush();
56
+ process.exit(EXIT_CODES.ENV);
57
+ }
58
+
59
+ const { isPullRequest, body, actor } = getPullRequestContext();
60
+ const changedFiles = getChangedFiles();
61
+
62
+ let result;
63
+ try {
64
+ result = checkSpecCoverage(ctx, { changedFiles, isPullRequest, body, actor });
65
+ } catch (err) {
66
+ out.fail(`coverage check failed: ${err.message}`);
67
+ out.flush();
68
+ process.exit(EXIT_CODES.ENV);
69
+ }
70
+
71
+ if (result.ok) {
72
+ out.pass(`spec coverage ok (${result.protectedFiles.length} protected file(s) changed)`);
73
+ out.flush();
74
+ process.exit(EXIT_CODES.OK);
75
+ }
76
+
77
+ for (const err of result.errors) {
78
+ out.fail(formatError(err, { verbose: argv.verbose }), err.toJSON ? err.toJSON() : undefined);
79
+ }
80
+ out.flush();
81
+ process.exit(EXIT_CODES.VALIDATION);
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness-detect-drift — thin bin wrapping `plugins/harness/scripts/detect-branch-drift.mjs`
4
+ * so `npx harness-detect-drift` works (fixes the broken invocation at
5
+ * `plugins/harness/templates/workflows/detect-drift.yml:15`).
6
+ *
7
+ * Forwards every flag through. Owns --help / --version only.
8
+ *
9
+ * Exit codes: whatever detect-branch-drift.mjs returns.
10
+ */
11
+
12
+ import { spawn } from "node:child_process";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, resolve } from "node:path";
15
+ import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
16
+ import { version } from "../src/index.mjs";
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(
21
+ [
22
+ "harness-detect-drift [OPTIONS]",
23
+ "",
24
+ "Flag .claude/commands/*.md and skills/**/SKILL.md that diverge from origin/main",
25
+ "for longer than the drift threshold. Wraps plugins/harness/scripts/detect-branch-drift.mjs.",
26
+ "",
27
+ "Options:",
28
+ " --help, -h show this help",
29
+ " --version, -V print harness version",
30
+ " (all other flags are forwarded to the underlying script)",
31
+ "",
32
+ "Exit codes: 0 ok, 1 drift detected, 2 env error.",
33
+ "",
34
+ ].join("\n")
35
+ );
36
+ process.exit(EXIT_CODES.OK);
37
+ }
38
+ if (args.includes("--version") || args.includes("-V")) {
39
+ process.stdout.write(`${version}\n`);
40
+ process.exit(EXIT_CODES.OK);
41
+ }
42
+
43
+ const __dirname = dirname(fileURLToPath(import.meta.url));
44
+ const scriptPath = resolve(__dirname, "..", "scripts", "detect-branch-drift.mjs");
45
+
46
+ const child = spawn(process.execPath, [scriptPath, ...args], { stdio: "inherit" });
47
+ child.on("exit", (code, signal) => {
48
+ if (signal) {
49
+ process.kill(process.pid, signal);
50
+ return;
51
+ }
52
+ process.exit(code ?? EXIT_CODES.ENV);
53
+ });
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * harness-doctor — diagnostic self-check.
4
+ *
5
+ * Walks through every invariant a consumer repo (or the harness repo itself,
6
+ * when dogfooding) must satisfy for validators to run:
7
+ *
8
+ * env Node >= 20, git on PATH
9
+ * repo git rev-parse --show-toplevel resolves; repoRoot usable
10
+ * facts docs/repo-facts.json exists and parses
11
+ * manifest .claude/skills-manifest.json checksums match (via validateManifest)
12
+ * specs docs/specs/ scanned; validateSpecs clean
13
+ * drift checkInstructionDrift clean
14
+ * hook plugins/harness/hooks/guard-destructive-git.sh present + exec bit
15
+ *
16
+ * Exit codes: 0 all green, 1 one or more checks failed (validation), 2 env error.
17
+ */
18
+
19
+ import { existsSync, statSync } from "node:fs";
20
+ import { execFileSync } from "node:child_process";
21
+ import { resolve } from "node:path";
22
+ import { parse, helpText } from "../src/lib/argv.mjs";
23
+ import { createOutput } from "../src/lib/output.mjs";
24
+ import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
25
+ import { version } from "../src/index.mjs";
26
+ import {
27
+ createHarnessContext,
28
+ validateManifest,
29
+ validateSpecs,
30
+ checkInstructionDrift,
31
+ pathExists,
32
+ } from "../src/index.mjs";
33
+
34
+ const META = {
35
+ name: "harness-doctor",
36
+ synopsis: "harness-doctor [OPTIONS]",
37
+ description: "Run the harness self-diagnostic across env, repo, facts, manifest, specs, drift, and hooks.",
38
+ flags: {
39
+ "repo-root": { type: "string" },
40
+ },
41
+ };
42
+
43
+ let argv;
44
+ try {
45
+ argv = parse(process.argv.slice(2), META.flags);
46
+ } catch (err) {
47
+ process.stderr.write(`${err.message}\n`);
48
+ process.exit(EXIT_CODES.USAGE);
49
+ }
50
+
51
+ if (argv.help) {
52
+ process.stdout.write(`${helpText(META)}\n`);
53
+ process.exit(EXIT_CODES.OK);
54
+ }
55
+ if (argv.version) {
56
+ process.stdout.write(`${version}\n`);
57
+ process.exit(EXIT_CODES.OK);
58
+ }
59
+
60
+ const out = createOutput({
61
+ json: argv.json,
62
+ noColor: argv.noColor,
63
+ });
64
+
65
+ let envError = false;
66
+
67
+ // env: Node + git
68
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
69
+ if (nodeMajor >= 20) {
70
+ out.pass(`Node ${process.versions.node} (>=20 required)`);
71
+ } else {
72
+ out.fail(`Node ${process.versions.node} is below the >=20 requirement`);
73
+ envError = true;
74
+ }
75
+ try {
76
+ const gitVersion = execFileSync("git", ["--version"], { encoding: "utf8" }).trim();
77
+ out.pass(`git available — ${gitVersion}`);
78
+ } catch {
79
+ out.fail("git is not on PATH");
80
+ envError = true;
81
+ }
82
+
83
+ // repo: resolve context
84
+ const repoRoot = /** @type {string | undefined} */ (argv.flags["repo-root"]);
85
+ let ctx;
86
+ try {
87
+ ctx = createHarnessContext({ repoRoot });
88
+ out.pass(`repo root resolved to ${ctx.repoRoot}`);
89
+ } catch (err) {
90
+ out.fail(`could not resolve repo root: ${err.message}`);
91
+ envError = true;
92
+ }
93
+
94
+ if (envError) {
95
+ out.flush();
96
+ process.exit(EXIT_CODES.ENV);
97
+ }
98
+
99
+ // facts
100
+ if (pathExists(ctx, "docs/repo-facts.json")) {
101
+ out.pass("docs/repo-facts.json present");
102
+ } else {
103
+ out.warn("docs/repo-facts.json missing — coverage/drift checks will be no-ops");
104
+ }
105
+
106
+ // manifest
107
+ if (pathExists(ctx, ".claude/skills-manifest.json")) {
108
+ const r = validateManifest(ctx);
109
+ if (r.ok) out.pass(`manifest valid (${r.manifest.skills.length} skills)`);
110
+ else out.fail(`manifest has ${r.errors.length} error(s)`, { errors: r.errors });
111
+ } else {
112
+ out.warn(".claude/skills-manifest.json missing — skill inventory not indexed");
113
+ }
114
+
115
+ // specs
116
+ if (pathExists(ctx, "docs/specs")) {
117
+ const r = validateSpecs(ctx);
118
+ if (r.ok) out.pass("specs valid");
119
+ else out.fail(`specs have ${r.errors.length} error(s)`, { errors: r.errors });
120
+ } else {
121
+ out.warn("docs/specs/ missing — no specs to validate");
122
+ }
123
+
124
+ // drift
125
+ try {
126
+ const r = checkInstructionDrift(ctx);
127
+ if (r.ok) out.pass("instruction drift clean");
128
+ else out.fail(`instruction drift: ${r.errors.length} issue(s)`, { errors: r.errors });
129
+ } catch (err) {
130
+ out.warn(`drift check skipped: ${err.message}`);
131
+ }
132
+
133
+ // hook
134
+ const hookPath = resolve(ctx.repoRoot, "plugins/harness/hooks/guard-destructive-git.sh");
135
+ if (existsSync(hookPath)) {
136
+ const mode = statSync(hookPath).mode & 0o111;
137
+ if (mode) out.pass("guard-destructive-git.sh present + executable");
138
+ else out.fail("guard-destructive-git.sh present but NOT executable (chmod +x)");
139
+ } else {
140
+ out.warn("guard-destructive-git.sh missing — destructive git commands are unguarded");
141
+ }
142
+
143
+ out.flush();
144
+ const { fail } = out.counts();
145
+ process.exit(fail > 0 ? EXIT_CODES.VALIDATION : EXIT_CODES.OK);