@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.
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/package.json +68 -0
- package/plugins/harness/.claude-plugin/plugin.json +8 -0
- package/plugins/harness/README.md +74 -0
- package/plugins/harness/bin/harness-check-instruction-drift.mjs +77 -0
- package/plugins/harness/bin/harness-check-spec-coverage.mjs +81 -0
- package/plugins/harness/bin/harness-detect-drift.mjs +53 -0
- package/plugins/harness/bin/harness-doctor.mjs +145 -0
- package/plugins/harness/bin/harness-init.mjs +89 -0
- package/plugins/harness/bin/harness-validate-skills.mjs +92 -0
- package/plugins/harness/bin/harness-validate-specs.mjs +70 -0
- package/plugins/harness/bin/harness.mjs +93 -0
- package/plugins/harness/hooks/guard-destructive-git.sh +58 -0
- package/plugins/harness/scripts/auto-update-manifest.mjs +20 -0
- package/plugins/harness/scripts/detect-branch-drift.mjs +81 -0
- package/plugins/harness/scripts/lib/output.sh +105 -0
- package/plugins/harness/scripts/refresh-worktrees.sh +35 -0
- package/plugins/harness/scripts/validate-settings.sh +202 -0
- package/plugins/harness/src/check-instruction-drift.mjs +127 -0
- package/plugins/harness/src/check-spec-coverage.mjs +95 -0
- package/plugins/harness/src/index.mjs +57 -0
- package/plugins/harness/src/init-harness-scaffold.mjs +121 -0
- package/plugins/harness/src/lib/argv.mjs +108 -0
- package/plugins/harness/src/lib/debug.mjs +37 -0
- package/plugins/harness/src/lib/errors.mjs +147 -0
- package/plugins/harness/src/lib/exit-codes.mjs +18 -0
- package/plugins/harness/src/lib/output.mjs +90 -0
- package/plugins/harness/src/spec-harness-lib.mjs +359 -0
- package/plugins/harness/src/validate-skills-inventory.mjs +148 -0
- package/plugins/harness/src/validate-specs.mjs +217 -0
- package/plugins/harness/templates/claude/hooks/guard-destructive-git.sh +50 -0
- package/plugins/harness/templates/claude/settings.headless.json +24 -0
- package/plugins/harness/templates/claude/settings.json +16 -0
- package/plugins/harness/templates/claude/skills-manifest.json +6 -0
- package/plugins/harness/templates/docs/repo-facts.json +17 -0
- package/plugins/harness/templates/docs/specs/README.md +36 -0
- package/plugins/harness/templates/githooks/pre-commit +9 -0
- package/plugins/harness/templates/workflows/ai-review.yml +28 -0
- package/plugins/harness/templates/workflows/detect-drift.yml +15 -0
- 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
|
+
[](https://www.npmjs.com/package/@kaiohenricunha/harness)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](./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);
|