@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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* harness-init — scaffold the harness template tree into a target repository.
|
|
4
|
+
*
|
|
5
|
+
* Flags:
|
|
6
|
+
* --project-name <name> defaults to basename(cwd)
|
|
7
|
+
* --project-type <type> defaults to "unknown"
|
|
8
|
+
* --force overwrite an already-initialized repo
|
|
9
|
+
* --target-dir <path> scaffolding destination (defaults to cwd)
|
|
10
|
+
*
|
|
11
|
+
* Exits: 0 scaffold complete, 1 ValidationError (SCAFFOLD_CONFLICT), 2 env
|
|
12
|
+
* error, 64 usage error.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { parse, helpText } from "../src/lib/argv.mjs";
|
|
18
|
+
import { createOutput } from "../src/lib/output.mjs";
|
|
19
|
+
import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
|
|
20
|
+
import { formatError, ValidationError } from "../src/lib/errors.mjs";
|
|
21
|
+
import { version } from "../src/index.mjs";
|
|
22
|
+
import { scaffoldHarness } from "../src/index.mjs";
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
const META = {
|
|
27
|
+
name: "harness-init",
|
|
28
|
+
synopsis: "harness-init [OPTIONS]",
|
|
29
|
+
description: "Scaffold the harness template tree (.claude/, docs/, .github/workflows/, githooks/) into the current repo.",
|
|
30
|
+
flags: {
|
|
31
|
+
"project-name": { type: "string" },
|
|
32
|
+
"project-type": { type: "string" },
|
|
33
|
+
"target-dir": { type: "string" },
|
|
34
|
+
force: { type: "boolean" },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let argv;
|
|
39
|
+
try {
|
|
40
|
+
argv = parse(process.argv.slice(2), META.flags);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
process.stderr.write(`${err.message}\n`);
|
|
43
|
+
process.exit(EXIT_CODES.USAGE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (argv.help) {
|
|
47
|
+
process.stdout.write(`${helpText(META)}\n`);
|
|
48
|
+
process.exit(EXIT_CODES.OK);
|
|
49
|
+
}
|
|
50
|
+
if (argv.version) {
|
|
51
|
+
process.stdout.write(`${version}\n`);
|
|
52
|
+
process.exit(EXIT_CODES.OK);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const out = createOutput({ json: argv.json, noColor: argv.noColor });
|
|
56
|
+
|
|
57
|
+
const targetDir = /** @type {string} */ (argv.flags["target-dir"] ?? process.cwd());
|
|
58
|
+
const projectName = /** @type {string} */ (argv.flags["project-name"] ?? path.basename(targetDir));
|
|
59
|
+
const projectType = /** @type {string} */ (argv.flags["project-type"] ?? "unknown");
|
|
60
|
+
const force = Boolean(argv.flags.force);
|
|
61
|
+
|
|
62
|
+
const templatesDir = path.resolve(__dirname, "..", "templates");
|
|
63
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { filesWritten } = scaffoldHarness(
|
|
67
|
+
{
|
|
68
|
+
templatesDir,
|
|
69
|
+
targetDir,
|
|
70
|
+
placeholders: { project_name: projectName, project_type: projectType, today },
|
|
71
|
+
},
|
|
72
|
+
{ force }
|
|
73
|
+
);
|
|
74
|
+
out.pass(`harness initialized in ${targetDir} (${filesWritten.length} files)`);
|
|
75
|
+
if (argv.verbose) {
|
|
76
|
+
for (const f of filesWritten) out.info(` ${f}`);
|
|
77
|
+
}
|
|
78
|
+
out.flush();
|
|
79
|
+
process.exit(EXIT_CODES.OK);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof ValidationError) {
|
|
82
|
+
out.fail(formatError(err, { verbose: argv.verbose }), err.toJSON());
|
|
83
|
+
out.flush();
|
|
84
|
+
process.exit(EXIT_CODES.VALIDATION);
|
|
85
|
+
}
|
|
86
|
+
out.fail(`scaffold failed: ${err.message}`);
|
|
87
|
+
out.flush();
|
|
88
|
+
process.exit(EXIT_CODES.ENV);
|
|
89
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* harness-validate-skills — validates `.claude/skills-manifest.json` against
|
|
4
|
+
* the sha256 checksums recorded for every indexed skill/command, and flags
|
|
5
|
+
* orphan files on disk + dependency cycles.
|
|
6
|
+
*
|
|
7
|
+
* Exits: 0 manifest valid, 1 one or more violations, 2 env error, 64 usage
|
|
8
|
+
* 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
|
+
validateManifest,
|
|
19
|
+
refreshChecksums,
|
|
20
|
+
} from "../src/index.mjs";
|
|
21
|
+
|
|
22
|
+
const META = {
|
|
23
|
+
name: "harness-validate-skills",
|
|
24
|
+
synopsis: "harness-validate-skills [OPTIONS]",
|
|
25
|
+
description: "Validate .claude/skills-manifest.json checksums, orphans, and DAG. Use --update to rewrite checksums in place.",
|
|
26
|
+
flags: {
|
|
27
|
+
"repo-root": { type: "string" },
|
|
28
|
+
update: { type: "boolean" },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let argv;
|
|
33
|
+
try {
|
|
34
|
+
argv = parse(process.argv.slice(2), META.flags);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
process.stderr.write(`${err.message}\n`);
|
|
37
|
+
process.exit(EXIT_CODES.USAGE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (argv.help) {
|
|
41
|
+
process.stdout.write(`${helpText(META)}\n`);
|
|
42
|
+
process.exit(EXIT_CODES.OK);
|
|
43
|
+
}
|
|
44
|
+
if (argv.version) {
|
|
45
|
+
process.stdout.write(`${version}\n`);
|
|
46
|
+
process.exit(EXIT_CODES.OK);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const out = createOutput({ json: argv.json, noColor: argv.noColor });
|
|
50
|
+
|
|
51
|
+
let ctx;
|
|
52
|
+
try {
|
|
53
|
+
ctx = createHarnessContext({ repoRoot: argv.flags["repo-root"] });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
out.fail(`could not resolve repo root: ${err.message}`);
|
|
56
|
+
out.flush();
|
|
57
|
+
process.exit(EXIT_CODES.ENV);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (argv.flags.update) {
|
|
61
|
+
try {
|
|
62
|
+
refreshChecksums(ctx);
|
|
63
|
+
out.pass(`manifest refreshed at ${ctx.manifestPath}`);
|
|
64
|
+
out.flush();
|
|
65
|
+
process.exit(EXIT_CODES.OK);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
out.fail(`refresh failed: ${err.message}`);
|
|
68
|
+
out.flush();
|
|
69
|
+
process.exit(EXIT_CODES.ENV);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = validateManifest(ctx);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
out.fail(err.message);
|
|
78
|
+
out.flush();
|
|
79
|
+
process.exit(EXIT_CODES.ENV);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.ok) {
|
|
83
|
+
out.pass(`manifest valid (${result.manifest.skills.length} skills)`);
|
|
84
|
+
out.flush();
|
|
85
|
+
process.exit(EXIT_CODES.OK);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const err of result.errors) {
|
|
89
|
+
out.fail(formatError(err, { verbose: argv.verbose }), err.toJSON ? err.toJSON() : undefined);
|
|
90
|
+
}
|
|
91
|
+
out.flush();
|
|
92
|
+
process.exit(EXIT_CODES.VALIDATION);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* harness-validate-specs — validates every `docs/specs/<id>/spec.json` in the
|
|
4
|
+
* repo against the structured-error contract from `validate-specs.mjs`.
|
|
5
|
+
*
|
|
6
|
+
* Exits: 0 all specs valid, 1 one or more validation failures, 2 env error,
|
|
7
|
+
* 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
|
+
listSpecDirs,
|
|
18
|
+
validateSpecs,
|
|
19
|
+
} from "../src/index.mjs";
|
|
20
|
+
|
|
21
|
+
const META = {
|
|
22
|
+
name: "harness-validate-specs",
|
|
23
|
+
synopsis: "harness-validate-specs [OPTIONS]",
|
|
24
|
+
description: "Validate every spec.json under docs/specs/ against the StructuredError contract.",
|
|
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
|
+
const result = validateSpecs(ctx);
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
const count = listSpecDirs(ctx).length;
|
|
61
|
+
out.pass(`${count} spec(s) valid`);
|
|
62
|
+
out.flush();
|
|
63
|
+
process.exit(EXIT_CODES.OK);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const err of result.errors) {
|
|
67
|
+
out.fail(formatError(err, { verbose: argv.verbose }), err.toJSON ? err.toJSON() : undefined);
|
|
68
|
+
}
|
|
69
|
+
out.flush();
|
|
70
|
+
process.exit(EXIT_CODES.VALIDATION);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Umbrella dispatcher. Usage:
|
|
4
|
+
*
|
|
5
|
+
* harness --version
|
|
6
|
+
* harness --help
|
|
7
|
+
* harness <subcommand> [args...] # delegates to harness-<subcommand>.mjs
|
|
8
|
+
*
|
|
9
|
+
* Known subcommands mirror the bin/* entries shipped by the package:
|
|
10
|
+
* validate-skills, validate-specs, check-spec-coverage,
|
|
11
|
+
* check-instruction-drift, detect-drift, doctor, init.
|
|
12
|
+
*
|
|
13
|
+
* Exits: 0 ok, 1 delegated failure, 2 env error, 64 usage error.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { version } from "../src/index.mjs";
|
|
21
|
+
import { EXIT_CODES } from "../src/lib/exit-codes.mjs";
|
|
22
|
+
|
|
23
|
+
const SUBCOMMANDS = [
|
|
24
|
+
"validate-skills",
|
|
25
|
+
"validate-specs",
|
|
26
|
+
"check-spec-coverage",
|
|
27
|
+
"check-instruction-drift",
|
|
28
|
+
"detect-drift",
|
|
29
|
+
"doctor",
|
|
30
|
+
"init",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function printUsage() {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
[
|
|
36
|
+
"harness — structured-error-emitting CLI for @kaiohenricunha/harness",
|
|
37
|
+
"",
|
|
38
|
+
"Usage:",
|
|
39
|
+
" harness <subcommand> [options]",
|
|
40
|
+
" harness --version",
|
|
41
|
+
" harness --help",
|
|
42
|
+
"",
|
|
43
|
+
"Subcommands:",
|
|
44
|
+
...SUBCOMMANDS.map((s) => ` ${s.padEnd(26)}runs harness-${s}`),
|
|
45
|
+
"",
|
|
46
|
+
"Every subcommand also exists as a standalone bin (e.g. `npx harness-doctor`).",
|
|
47
|
+
"Each subcommand supports --help / --version / --json / --verbose / --no-color.",
|
|
48
|
+
"",
|
|
49
|
+
"Exit codes: 0 ok, 1 validation failure, 2 env error, 64 usage error.",
|
|
50
|
+
"",
|
|
51
|
+
].join("\n")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
|
|
57
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
58
|
+
printUsage();
|
|
59
|
+
process.exit(EXIT_CODES.OK);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (args[0] === "--version" || args[0] === "-V") {
|
|
63
|
+
process.stdout.write(`${version}\n`);
|
|
64
|
+
process.exit(EXIT_CODES.OK);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sub = args[0];
|
|
68
|
+
if (!SUBCOMMANDS.includes(sub)) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`harness: unknown subcommand '${sub}'. Run 'harness --help' for the list.\n`
|
|
71
|
+
);
|
|
72
|
+
process.exit(EXIT_CODES.USAGE);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const binPath = resolve(__dirname, `harness-${sub}.mjs`);
|
|
77
|
+
if (!existsSync(binPath)) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`harness: bin 'harness-${sub}' not found at ${binPath}. Did the package install correctly?\n`
|
|
80
|
+
);
|
|
81
|
+
process.exit(EXIT_CODES.ENV);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const child = spawn(process.execPath, [binPath, ...args.slice(1)], {
|
|
85
|
+
stdio: "inherit",
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", (code, signal) => {
|
|
88
|
+
if (signal) {
|
|
89
|
+
process.kill(process.pid, signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
process.exit(code ?? EXIT_CODES.ENV);
|
|
93
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook: block destructive git operations.
|
|
3
|
+
# Reads JSON from stdin (Claude Code hook protocol).
|
|
4
|
+
# Exit 2 = block the tool call (Claude Code hook protocol — NOT the harness
|
|
5
|
+
# validator exit convention). Exit 0 = allow.
|
|
6
|
+
#
|
|
7
|
+
# Bypass: set BYPASS_DESTRUCTIVE_GIT=1 in the command's environment when you
|
|
8
|
+
# genuinely need to run a destructive git invocation. Use sparingly — the
|
|
9
|
+
# block exists because these operations are silently destructive.
|
|
10
|
+
|
|
11
|
+
# Fail open if jq is not installed (don't break all Bash tool calls).
|
|
12
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ "${BYPASS_DESTRUCTIVE_GIT:-0}" = "1" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
|
|
22
|
+
[ "$TOOL" = "Bash" ] || exit 0
|
|
23
|
+
|
|
24
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')
|
|
25
|
+
# Normalize whitespace: tabs -> space, collapse runs of whitespace so regex
|
|
26
|
+
# anchors only have to reason about single spaces.
|
|
27
|
+
NORM=$(printf '%s' "$CMD" | tr '\t' ' ' | tr -s ' ')
|
|
28
|
+
|
|
29
|
+
# Boundary before the `git` token is one of: start-of-line, whitespace, or a
|
|
30
|
+
# command-chaining separator (`;`, `&&`, `||`, `|`). This prevents false
|
|
31
|
+
# positives like `echo git` inside a quoted string while still catching
|
|
32
|
+
# `foo && git reset --hard`.
|
|
33
|
+
BOUNDARY='(^|[[:space:];&|])'
|
|
34
|
+
G='git[[:space:]]+'
|
|
35
|
+
|
|
36
|
+
# Destructive verbs. Each alternative is anchored on the right by at least one
|
|
37
|
+
# flag/keyword that makes the call unambiguously destructive.
|
|
38
|
+
PATTERNS=(
|
|
39
|
+
"${BOUNDARY}${G}reset[[:space:]]+--hard(\b|[[:space:]]|$)"
|
|
40
|
+
"${BOUNDARY}${G}push[[:space:]][^&;|]*(-f|--force|--force-with-lease)(\b|=|[[:space:]]|$)"
|
|
41
|
+
"${BOUNDARY}${G}clean[[:space:]][^&;|]*(-[a-zA-Z]*f[a-zA-Z]*|--force)(\b|=|[[:space:]]|$)"
|
|
42
|
+
"${BOUNDARY}${G}checkout[[:space:]]+\.(\b|$)"
|
|
43
|
+
"${BOUNDARY}${G}restore[[:space:]]+\.(\b|$)"
|
|
44
|
+
"${BOUNDARY}${G}branch[[:space:]]+-D\b"
|
|
45
|
+
"${BOUNDARY}${G}worktree[[:space:]]+remove[[:space:]]+--force\b"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for rx in "${PATTERNS[@]}"; do
|
|
49
|
+
if printf '%s' "$NORM" | grep -qE "$rx"; then
|
|
50
|
+
{
|
|
51
|
+
echo "BLOCKED: Destructive git operation detected. Get explicit user confirmation first."
|
|
52
|
+
echo " Bypass (only with user confirmation): BYPASS_DESTRUCTIVE_GIT=1 <your command>"
|
|
53
|
+
} >&2
|
|
54
|
+
exit 2
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// auto-update-manifest.mjs — thin pre-commit wrapper.
|
|
3
|
+
// Invokes harness-validate-skills --update from the package bin,
|
|
4
|
+
// using the current working directory as repo root.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const bin = join(__dirname, '..', 'bin', 'harness-validate-skills.mjs');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
execFileSync(process.execPath, [bin, '--update'], {
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
});
|
|
18
|
+
} catch (e) {
|
|
19
|
+
process.exit(e.status ?? 1);
|
|
20
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// detect-branch-drift.mjs — walk .claude/commands/*.md on HEAD,
|
|
3
|
+
// diff each against origin/main, report a table. Exit 1 if any file
|
|
4
|
+
// has diverged AND its last-main-commit is >14 days old.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
const DAY_S = 86400;
|
|
9
|
+
const STALE_DAYS = 14;
|
|
10
|
+
|
|
11
|
+
function exec(cmd, args, cwd) {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync(cmd, args, { cwd, encoding: 'utf8' }).trim();
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Parse --repo-root flag
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
let repoRoot;
|
|
22
|
+
const flagIdx = args.indexOf('--repo-root');
|
|
23
|
+
if (flagIdx !== -1) {
|
|
24
|
+
repoRoot = args[flagIdx + 1];
|
|
25
|
+
} else {
|
|
26
|
+
repoRoot = exec('git', ['rev-parse', '--show-toplevel'], process.cwd());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!repoRoot) {
|
|
30
|
+
console.error('Could not determine repo root. Use --repo-root <path>.');
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// List .claude/commands/*.md files tracked on HEAD
|
|
35
|
+
const lsTree = exec('git', ['ls-tree', '-r', '--name-only', 'HEAD', '.claude/commands/'], repoRoot);
|
|
36
|
+
const files = lsTree.split('\n').filter(f => f.endsWith('.md'));
|
|
37
|
+
|
|
38
|
+
if (files.length === 0) {
|
|
39
|
+
console.log('no drift detected (no .claude/commands/*.md on HEAD)');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const now = Math.floor(Date.now() / 1000);
|
|
44
|
+
const rows = [];
|
|
45
|
+
let anyStale = false;
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const diff = exec('git', ['diff', 'origin/main', '--', file], repoRoot);
|
|
49
|
+
const diverged = diff.length > 0;
|
|
50
|
+
|
|
51
|
+
let daysBehind = 0;
|
|
52
|
+
if (diverged) {
|
|
53
|
+
const tsStr = exec('git', ['log', '-1', '--format=%ct', 'origin/main', '--', file], repoRoot);
|
|
54
|
+
const ts = parseInt(tsStr, 10);
|
|
55
|
+
if (!isNaN(ts) && ts > 0) {
|
|
56
|
+
daysBehind = Math.floor((now - ts) / DAY_S);
|
|
57
|
+
}
|
|
58
|
+
if (daysBehind > STALE_DAYS) anyStale = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
rows.push({ file, diverged, daysBehind });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const anyDiverged = rows.some(r => r.diverged);
|
|
65
|
+
if (!anyDiverged) {
|
|
66
|
+
console.log('no drift detected');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Print table
|
|
71
|
+
const header = `${'FILE'.padEnd(50)} ${'DIVERGED'.padEnd(10)} DAYS-BEHIND-MAIN`;
|
|
72
|
+
console.log(header);
|
|
73
|
+
console.log('-'.repeat(header.length));
|
|
74
|
+
for (const r of rows) {
|
|
75
|
+
const name = r.file.padEnd(50);
|
|
76
|
+
const div = (r.diverged ? 'yes' : 'no').padEnd(10);
|
|
77
|
+
const days = r.diverged ? String(r.daysBehind) : '-';
|
|
78
|
+
console.log(`${name} ${div} ${days}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process.exit(anyStale ? 1 : 0);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# output.sh — shared ✓/✗/⚠ helpers for every harness shell script.
|
|
3
|
+
#
|
|
4
|
+
# Gold-standard originates from plugins/harness/scripts/validate-settings.sh:43-45.
|
|
5
|
+
# Every consumer should:
|
|
6
|
+
#
|
|
7
|
+
# # shellcheck source=plugins/harness/scripts/lib/output.sh
|
|
8
|
+
# source "$(dirname "${BASH_SOURCE[0]}")/lib/output.sh"
|
|
9
|
+
# out_init # sets G/R/Y/N + FAIL/WARN globals, honors --json + NO_COLOR
|
|
10
|
+
# pass "JSON well-formed"
|
|
11
|
+
# fail "SEC-2 skipDangerousModePermissionPrompt is set"
|
|
12
|
+
# warn "projects/ close to budget"
|
|
13
|
+
# out_summary # prints "Summary: N failure(s), N warning(s)"
|
|
14
|
+
#
|
|
15
|
+
# When HARNESS_JSON=1 is set, pass/fail/warn buffer JSON objects with the shape
|
|
16
|
+
# { "check": "...", "category": "...", "status": "pass|fail|warn", "message": "..." }
|
|
17
|
+
# into HARNESS_JSON_BUFFER; callers flush with `out_flush`. The `category`
|
|
18
|
+
# defaults to the CATEGORY env; individual calls can override with the
|
|
19
|
+
# two-argument form: `fail SEC-2 "skipDangerous... is set"`.
|
|
20
|
+
|
|
21
|
+
# Shell-scoped globals are set by out_init. Declaring defaults here keeps
|
|
22
|
+
# callers working even if they forget out_init and lets linters see the
|
|
23
|
+
# assignments.
|
|
24
|
+
FAIL=${FAIL:-0}
|
|
25
|
+
WARN=${WARN:-0}
|
|
26
|
+
G=""; R=""; Y=""; N=""
|
|
27
|
+
HARNESS_JSON=${HARNESS_JSON:-0}
|
|
28
|
+
HARNESS_JSON_BUFFER=""
|
|
29
|
+
# shellcheck disable=SC2034 # CATEGORY is consumed by sourced scripts
|
|
30
|
+
CATEGORY=${CATEGORY:-general}
|
|
31
|
+
|
|
32
|
+
out_init() {
|
|
33
|
+
FAIL=0
|
|
34
|
+
WARN=0
|
|
35
|
+
HARNESS_JSON_BUFFER=""
|
|
36
|
+
if [ "${HARNESS_JSON:-0}" = "1" ] || [ "${NO_COLOR:-}" != "" ] || [ ! -t 1 ]; then
|
|
37
|
+
G=""; R=""; Y=""; N=""
|
|
38
|
+
else
|
|
39
|
+
G=$'\033[32m'; R=$'\033[31m'; Y=$'\033[33m'; N=$'\033[0m'
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Internal: buffer one JSON object. Escapes the double-quotes and backslashes in $message.
|
|
44
|
+
_out_json_push() {
|
|
45
|
+
local status="$1" check="$2" message="$3"
|
|
46
|
+
local cat="${4:-$CATEGORY}"
|
|
47
|
+
# Escape backslash then double-quote for safe JSON inclusion.
|
|
48
|
+
message=${message//\\/\\\\}
|
|
49
|
+
message=${message//\"/\\\"}
|
|
50
|
+
check=${check//\\/\\\\}
|
|
51
|
+
check=${check//\"/\\\"}
|
|
52
|
+
local entry
|
|
53
|
+
entry=$(printf '{"check":"%s","category":"%s","status":"%s","message":"%s"}' \
|
|
54
|
+
"$check" "$cat" "$status" "$message")
|
|
55
|
+
if [ -z "$HARNESS_JSON_BUFFER" ]; then
|
|
56
|
+
HARNESS_JSON_BUFFER="$entry"
|
|
57
|
+
else
|
|
58
|
+
HARNESS_JSON_BUFFER="$HARNESS_JSON_BUFFER,$entry"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pass() {
|
|
63
|
+
local msg="$1"
|
|
64
|
+
if [ "${HARNESS_JSON:-0}" = "1" ]; then
|
|
65
|
+
_out_json_push pass "${2:-$msg}" "$msg"
|
|
66
|
+
else
|
|
67
|
+
printf ' %s✓%s %s\n' "$G" "$N" "$msg"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fail() {
|
|
72
|
+
local msg="$1"
|
|
73
|
+
FAIL=$((FAIL+1))
|
|
74
|
+
if [ "${HARNESS_JSON:-0}" = "1" ]; then
|
|
75
|
+
_out_json_push fail "${2:-$msg}" "$msg"
|
|
76
|
+
else
|
|
77
|
+
printf ' %s✗%s %s\n' "$R" "$N" "$msg"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
warn() {
|
|
82
|
+
local msg="$1"
|
|
83
|
+
WARN=$((WARN+1))
|
|
84
|
+
if [ "${HARNESS_JSON:-0}" = "1" ]; then
|
|
85
|
+
_out_json_push warn "${2:-$msg}" "$msg"
|
|
86
|
+
else
|
|
87
|
+
printf ' %s⚠%s %s\n' "$Y" "$N" "$msg"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
out_flush() {
|
|
92
|
+
if [ "${HARNESS_JSON:-0}" = "1" ]; then
|
|
93
|
+
printf '{"events":[%s],"counts":{"fail":%d,"warn":%d}}\n' \
|
|
94
|
+
"$HARNESS_JSON_BUFFER" "$FAIL" "$WARN"
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
out_summary() {
|
|
99
|
+
if [ "${HARNESS_JSON:-0}" = "1" ]; then
|
|
100
|
+
out_flush
|
|
101
|
+
else
|
|
102
|
+
echo
|
|
103
|
+
echo "Summary: $FAIL failure(s), $WARN warning(s)"
|
|
104
|
+
fi
|
|
105
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# refresh-worktrees.sh — for each active worktree under .claude/worktrees/,
|
|
3
|
+
# run `git fetch origin main` and `git merge --ff-only origin/main` if the
|
|
4
|
+
# worktree is clean. Report (and skip) any dirty worktree.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="${1:-$(git rev-parse --show-toplevel)}"
|
|
9
|
+
cd "$ROOT"
|
|
10
|
+
|
|
11
|
+
WT_BASE="$ROOT/.claude/worktrees"
|
|
12
|
+
[ -d "$WT_BASE" ] || { echo "no worktrees at $WT_BASE"; exit 0; }
|
|
13
|
+
|
|
14
|
+
git fetch origin main
|
|
15
|
+
|
|
16
|
+
for wt in "$WT_BASE"/*/; do
|
|
17
|
+
[ -d "$wt" ] || continue
|
|
18
|
+
name=$(basename "$wt")
|
|
19
|
+
(
|
|
20
|
+
cd "$wt"
|
|
21
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
22
|
+
echo "SKIP (dirty): $name"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
if git merge-base --is-ancestor origin/main HEAD; then
|
|
26
|
+
echo "OK: $name (up to date)"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
if git merge --ff-only origin/main; then
|
|
30
|
+
echo "FF: $name"
|
|
31
|
+
else
|
|
32
|
+
echo "CONFLICT: $name (manual resolution needed)"
|
|
33
|
+
fi
|
|
34
|
+
)
|
|
35
|
+
done
|