harnery 0.3.1 → 0.4.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/README.md +20 -7
- package/dist/commander.js +2 -2
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +14 -1
- package/dist/commands/deinit.d.ts +51 -0
- package/dist/commands/deinit.d.ts.map +1 -0
- package/dist/commands/{uninstall.js → deinit.js} +83 -14
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +47 -9
- package/dist/commands/init.d.ts +2 -21
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -15
- package/dist/core/agents/coord-client.js +20 -5
- package/dist/core/agents/render/session-context.d.ts +12 -2
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +75 -36
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +29 -11
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +7 -1
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/hooks/cli.js +25 -20
- package/dist/core/hooks/harness/wiring.d.ts +85 -0
- package/dist/core/hooks/harness/wiring.d.ts.map +1 -0
- package/dist/core/hooks/harness/wiring.js +137 -0
- package/dist/core/hooks/resolve/anchor.d.ts +15 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -1
- package/dist/core/hooks/resolve/anchor.js +22 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +20 -8
- package/package.json +1 -1
- package/schemas/config.schema.json +4 -0
- package/src/commander.ts +2 -2
- package/src/commands/agents.ts +14 -1
- package/src/commands/{uninstall.ts → deinit.ts} +107 -15
- package/src/commands/doctor.ts +47 -9
- package/src/commands/init.ts +12 -39
- package/src/core/agents/coord-client.ts +17 -4
- package/src/core/agents/render/session-context.ts +74 -34
- package/src/core/agents/rules/claim-conflict.ts +28 -11
- package/src/core/agents/state/heartbeat-writer.ts +8 -1
- package/src/core/config.ts +21 -0
- package/src/core/hooks/cli.ts +24 -20
- package/src/core/hooks/harness/wiring.ts +185 -0
- package/src/core/hooks/resolve/anchor.ts +21 -0
- package/src/core/hooks/resolve/owner.ts +17 -8
- package/dist/commands/uninstall.d.ts +0 -22
- package/dist/commands/uninstall.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -27,23 +27,36 @@
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
|
|
30
|
+
curl -fsSL https://harnery.com/install.sh | bash
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Or
|
|
33
|
+
One line, no clone: it installs the `harn` CLI globally (npm preferred, Bun fallback), puts it on your `PATH`, and verifies it. Or drive your package manager directly — `npm install -g harnery` / `bun add -g harnery` (or `npm install harnery` for a project dep). Then wire a project:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
36
|
+
harn init # creates .harnery/ + registers the harness hooks
|
|
37
|
+
harn doctor # optional: one-time runtime + dependency check
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
> **From a git clone?** Cloning for the `web/` dashboard or to contribute? `./scripts/setup.sh` does the clone setup in one shot: installs deps, builds `dist/` on a Bun-free host, runs `harn init`, and links the bins onto your `PATH`.
|
|
41
|
+
|
|
42
|
+
> **npm gives you the engine + CLI.** The `web/` dashboard and the `docs/` site live in the git repo, not the npm package (which is the CLI + coord engine: `bin`, `dist`, `src`, `schemas`). To run the dashboard, `git clone` the repo, `bun install`, and `harn web up` from there, pointing it at your project with `--coord-root <dir>` (or just run it from inside the project). `harn web up` prints these exact steps if you invoke it without the clone present.
|
|
43
|
+
|
|
44
|
+
## Uninstall
|
|
45
|
+
|
|
46
|
+
Two layers. **Unwire a project** (keeps `.harnery/` history by default; on a terminal it asks before deleting it):
|
|
40
47
|
|
|
41
48
|
```bash
|
|
42
|
-
harn
|
|
43
|
-
harn
|
|
49
|
+
harn deinit # unwire the harness hooks
|
|
50
|
+
harn deinit --purge-state # also delete .harnery/ (destructive)
|
|
44
51
|
```
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
**Remove the CLI** with the hosted one-liner (`npm rm -g harnery` / `bun remove -g harnery` work too):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl -fsSL https://harnery.com/uninstall.sh | bash
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From a git clone, `./scripts/teardown.sh` is the mirror of `./scripts/setup.sh`: it runs `harn deinit`, removes the `PATH` symlinks, and — on a terminal — asks whether to also delete this project's `.harnery/` history and the clone itself. Both default to no; pre-answer with `--purge-state` and `--remove-clone` for unattended runs.
|
|
47
60
|
|
|
48
61
|
## Use as a CLI library
|
|
49
62
|
|
package/dist/commander.js
CHANGED
|
@@ -26,6 +26,7 @@ import { registerCompletionCommand } from "./commands/completion.js";
|
|
|
26
26
|
import { registerConfigGetCommand } from "./commands/config-get.js";
|
|
27
27
|
import { registerContextCommand } from "./commands/context.js";
|
|
28
28
|
import { registerCookiesCommand } from "./commands/cookies.js";
|
|
29
|
+
import { registerDeinitCommand } from "./commands/deinit.js";
|
|
29
30
|
import { registerDocsCommand } from "./commands/docs.js";
|
|
30
31
|
import { registerDoctorCommand } from "./commands/doctor.js";
|
|
31
32
|
import { registerEditBatchCommand } from "./commands/edit-batch.js";
|
|
@@ -44,7 +45,6 @@ import { registerSyncCommand } from "./commands/sync.js";
|
|
|
44
45
|
import { registerSectionCommand, registerTocCommand } from "./commands/toc.js";
|
|
45
46
|
import { registerTokensCommand } from "./commands/tokens.js";
|
|
46
47
|
import { registerTunnelCommand } from "./commands/tunnel.js";
|
|
47
|
-
import { registerUninstallCommand } from "./commands/uninstall.js";
|
|
48
48
|
import { registerWebCommand } from "./commands/web.js";
|
|
49
49
|
export const defaultEmit = {
|
|
50
50
|
config() {
|
|
@@ -115,7 +115,7 @@ export function createHarneryProgram(opts = {}) {
|
|
|
115
115
|
registerAgentsCommand(program, emit);
|
|
116
116
|
registerDoctorCommand(program, emit);
|
|
117
117
|
registerInitCommand(program, emit, opts.binName);
|
|
118
|
-
|
|
118
|
+
registerDeinitCommand(program, emit, opts.binName);
|
|
119
119
|
registerBackupCommand(program, emit);
|
|
120
120
|
registerSyncCommand(program, emit);
|
|
121
121
|
if (include("web"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA6OpF"}
|
package/dist/commands/agents.js
CHANGED
|
@@ -18,6 +18,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
19
|
import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerWithSource, } from "../core/agents/index.js";
|
|
20
20
|
import { resolveBinName } from "../core/config.js";
|
|
21
|
+
import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
|
|
21
22
|
import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
|
|
22
23
|
import { displayName as displayAgentName, ensureIdentity, listIdentities, lookupById as lookupIdentityById, lookupByName as lookupIdentityByName, } from "../lib/identities/index.js";
|
|
23
24
|
import { appendEntry, resolveOwnerByName } from "../lib/scratch/index.js";
|
|
@@ -2251,14 +2252,26 @@ function runHarnessProbe(id, opts) {
|
|
|
2251
2252
|
for (let hops = 0; hops < 20; hops++) {
|
|
2252
2253
|
let comm = "?";
|
|
2253
2254
|
let ppid = 0;
|
|
2255
|
+
let got = false;
|
|
2254
2256
|
try {
|
|
2255
2257
|
comm = readFileSync(`/proc/${walkPid}/comm`, "utf8").trim() || "?";
|
|
2256
2258
|
const status = readFileSync(`/proc/${walkPid}/status`, "utf8");
|
|
2257
2259
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
2258
2260
|
ppid = m ? Number(m[1]) : 0;
|
|
2261
|
+
got = true;
|
|
2259
2262
|
}
|
|
2260
2263
|
catch {
|
|
2261
|
-
// non-Linux (no /proc)
|
|
2264
|
+
// non-Linux (no /proc): fall through to the portable ps walk below.
|
|
2265
|
+
}
|
|
2266
|
+
if (!got) {
|
|
2267
|
+
const out = spawnSync("ps", ["-o", "ppid=,comm=", "-p", String(walkPid)], {
|
|
2268
|
+
encoding: "utf8",
|
|
2269
|
+
});
|
|
2270
|
+
const parsed = out.status === 0 ? parsePsChainLine(out.stdout) : null;
|
|
2271
|
+
if (parsed) {
|
|
2272
|
+
comm = parsed.comm || "?";
|
|
2273
|
+
ppid = parsed.ppid;
|
|
2274
|
+
}
|
|
2262
2275
|
}
|
|
2263
2276
|
chainParts.push(`${walkPid}:${comm}`);
|
|
2264
2277
|
if (!anchorPid && anchorTokens.has(comm))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harn deinit`: reverse what `harn init` wired into a project.
|
|
3
|
+
*
|
|
4
|
+
* `init` makes two kinds of change outside the harnery package:
|
|
5
|
+
* 1. Merges `agent-hook` entries into the harness settings file
|
|
6
|
+
* (Claude Code `.claude/settings.json`, Cursor `.cursor/hooks.json`, or
|
|
7
|
+
* Codex `.codex/hooks.json`).
|
|
8
|
+
* 2. Creates the `.harnery/` coord root (runtime state: events, councils,
|
|
9
|
+
* identities, scratch) and stamps the host bin name into
|
|
10
|
+
* `.harnery/config.jsonc`.
|
|
11
|
+
*
|
|
12
|
+
* `deinit` undoes (1) by default: it removes only harnery's hook entries from
|
|
13
|
+
* the settings file, preserving any other hooks the consumer added, and deletes
|
|
14
|
+
* the settings file outright when it's left harnery-only. It does NOT touch the
|
|
15
|
+
* `.harnery/` coord root unless `--purge-state` is passed, because that directory
|
|
16
|
+
* holds session history a consumer may want to keep. Idempotent + `--dry-run`,
|
|
17
|
+
* mirroring `init`.
|
|
18
|
+
*
|
|
19
|
+
* For standalone `harn` on a terminal it also handles the destructive extra
|
|
20
|
+
* conversationally: when `--purge-state` wasn't passed and `.harnery/` exists, it
|
|
21
|
+
* asks before deleting it, and afterward it prints how to remove the harnery CLI
|
|
22
|
+
* itself (which a running process can't do to its own package). Both are gated to
|
|
23
|
+
* standalone harn — an embedding host routes output through its own emit and owns
|
|
24
|
+
* its install lifecycle — and the prompt never fires off a TTY, so scripted / CI
|
|
25
|
+
* runs keep the flag-driven behavior untouched. The shell wrapper `scripts/teardown.sh`
|
|
26
|
+
* is the fuller mirror (it also unlinks the PATH bins and can delete the clone).
|
|
27
|
+
*/
|
|
28
|
+
import type { Command } from "commander";
|
|
29
|
+
import type { EmitContext } from "../commander.js";
|
|
30
|
+
export declare function registerDeinitCommand(program: Command, emit: EmitContext, binName?: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to interactively ask before deleting `.harnery/`. True only for
|
|
33
|
+
* standalone harn on a TTY, when the user didn't already answer with
|
|
34
|
+
* --purge-state, it isn't a dry run, and there's actually a coord root to
|
|
35
|
+
* delete. Pure so the gating is unit-testable without a terminal.
|
|
36
|
+
*/
|
|
37
|
+
export declare function shouldPromptForState(o: {
|
|
38
|
+
standalone: boolean;
|
|
39
|
+
interactive: boolean;
|
|
40
|
+
dryRun: boolean;
|
|
41
|
+
purgeState: boolean;
|
|
42
|
+
coordExists: boolean;
|
|
43
|
+
}): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The "harnery itself is still installed" line shown to standalone-harn users
|
|
46
|
+
* after a real deinit. `harn deinit` can't remove the package it's running
|
|
47
|
+
* from, so it points at the two ways to finish the job. Pure + exported for the
|
|
48
|
+
* test.
|
|
49
|
+
*/
|
|
50
|
+
export declare function engineRemovalHint(): string;
|
|
51
|
+
//# sourceMappingURL=deinit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CA2GjG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `harn
|
|
2
|
+
* `harn deinit`: reverse what `harn init` wired into a project.
|
|
3
3
|
*
|
|
4
4
|
* `init` makes two kinds of change outside the harnery package:
|
|
5
5
|
* 1. Merges `agent-hook` entries into the harness settings file
|
|
@@ -9,29 +9,46 @@
|
|
|
9
9
|
* identities, scratch) and stamps the host bin name into
|
|
10
10
|
* `.harnery/config.jsonc`.
|
|
11
11
|
*
|
|
12
|
-
* `
|
|
12
|
+
* `deinit` undoes (1) by default: it removes only harnery's hook entries from
|
|
13
13
|
* the settings file, preserving any other hooks the consumer added, and deletes
|
|
14
14
|
* the settings file outright when it's left harnery-only. It does NOT touch the
|
|
15
15
|
* `.harnery/` coord root unless `--purge-state` is passed, because that directory
|
|
16
16
|
* holds session history a consumer may want to keep. Idempotent + `--dry-run`,
|
|
17
17
|
* mirroring `init`.
|
|
18
|
+
*
|
|
19
|
+
* For standalone `harn` on a terminal it also handles the destructive extra
|
|
20
|
+
* conversationally: when `--purge-state` wasn't passed and `.harnery/` exists, it
|
|
21
|
+
* asks before deleting it, and afterward it prints how to remove the harnery CLI
|
|
22
|
+
* itself (which a running process can't do to its own package). Both are gated to
|
|
23
|
+
* standalone harn — an embedding host routes output through its own emit and owns
|
|
24
|
+
* its install lifecycle — and the prompt never fires off a TTY, so scripted / CI
|
|
25
|
+
* runs keep the flag-driven behavior untouched. The shell wrapper `scripts/teardown.sh`
|
|
26
|
+
* is the fuller mirror (it also unlinks the PATH bins and can delete the clone).
|
|
18
27
|
*/
|
|
19
28
|
import { spawnSync } from "node:child_process";
|
|
20
29
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
21
30
|
import { relative, resolve } from "node:path";
|
|
31
|
+
import { createInterface } from "node:readline";
|
|
32
|
+
import { DEFAULT_BIN_NAME } from "../core/config.js";
|
|
22
33
|
import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
|
|
23
34
|
import { unwireHooks } from "./init.js";
|
|
24
|
-
export function
|
|
35
|
+
export function registerDeinitCommand(program, emit, binName) {
|
|
36
|
+
// The interactive prompt + engine-removal hint are standalone-harn niceties.
|
|
37
|
+
// An embedding host routes output through its own emit and owns its install
|
|
38
|
+
// lifecycle, so for a host (binName set + non-default) deinit stays strictly
|
|
39
|
+
// flag-driven and says nothing about removing "the package".
|
|
40
|
+
const standalone = !binName || binName === DEFAULT_BIN_NAME;
|
|
25
41
|
program
|
|
26
|
-
.command("
|
|
42
|
+
.command("deinit")
|
|
27
43
|
.description("Reverse `harn init`: remove harnery's hook entries from the harness " +
|
|
28
44
|
"settings file (keeps any others). Pass --purge-state to also delete the " +
|
|
29
|
-
".harnery/ coord root. Idempotent; use
|
|
45
|
+
".harnery/ coord root (on a terminal it asks first). Idempotent; use " +
|
|
46
|
+
"--dry-run to preview.")
|
|
30
47
|
.option("--harness <id>", "claude-code | cursor | codex", "claude-code")
|
|
31
48
|
.option("--dry-run", "Show what would change without writing")
|
|
32
49
|
.option("--project-root <path>", "Project root (default: git toplevel, else cwd)")
|
|
33
50
|
.option("--purge-state", "Also delete the .harnery/ coord root (runtime state, destructive)")
|
|
34
|
-
.action((opts) => {
|
|
51
|
+
.action(async (opts) => {
|
|
35
52
|
const harness = opts.harness;
|
|
36
53
|
const spec = HARNESS_SPECS[harness];
|
|
37
54
|
if (!spec) {
|
|
@@ -41,7 +58,20 @@ export function registerUninstallCommand(program, emit) {
|
|
|
41
58
|
}
|
|
42
59
|
const projectRoot = resolve(opts.projectRoot ?? gitTopLevel() ?? process.cwd());
|
|
43
60
|
const dryRun = opts.dryRun === true;
|
|
61
|
+
const coordDir = resolve(projectRoot, ".harnery");
|
|
62
|
+
const coordExists = existsSync(coordDir);
|
|
63
|
+
let purgeState = opts.purgeState === true;
|
|
44
64
|
const actions = [];
|
|
65
|
+
// ── 0. interactive: offer to delete .harnery/ (standalone harn, TTY) ───
|
|
66
|
+
if (shouldPromptForState({
|
|
67
|
+
standalone,
|
|
68
|
+
interactive: process.stdin.isTTY === true,
|
|
69
|
+
dryRun,
|
|
70
|
+
purgeState,
|
|
71
|
+
coordExists,
|
|
72
|
+
})) {
|
|
73
|
+
purgeState = await confirmDeleteState(coordDir);
|
|
74
|
+
}
|
|
45
75
|
// ── 1. unwire harness hooks ────────────────────────────────────────────
|
|
46
76
|
const settingsPath = resolve(projectRoot, spec.settingsFile);
|
|
47
77
|
if (!existsSync(settingsPath)) {
|
|
@@ -84,9 +114,8 @@ export function registerUninstallCommand(program, emit) {
|
|
|
84
114
|
}
|
|
85
115
|
}
|
|
86
116
|
// ── 2. coord root (opt-in; destructive) ────────────────────────────────
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!existsSync(coordDir)) {
|
|
117
|
+
if (purgeState) {
|
|
118
|
+
if (!coordExists) {
|
|
90
119
|
actions.push("· .harnery/ doesn't exist; nothing to purge");
|
|
91
120
|
}
|
|
92
121
|
else if (dryRun) {
|
|
@@ -97,10 +126,49 @@ export function registerUninstallCommand(program, emit) {
|
|
|
97
126
|
actions.push("+ deleted .harnery/ and all coord state");
|
|
98
127
|
}
|
|
99
128
|
}
|
|
100
|
-
else if (
|
|
129
|
+
else if (coordExists) {
|
|
101
130
|
actions.push("· left .harnery/ coord root in place (pass --purge-state to delete it)");
|
|
102
131
|
}
|
|
103
|
-
|
|
132
|
+
const hint = standalone && !dryRun ? engineRemovalHint() : null;
|
|
133
|
+
emit.text(render(projectRoot, dryRun, actions, hint));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether to interactively ask before deleting `.harnery/`. True only for
|
|
138
|
+
* standalone harn on a TTY, when the user didn't already answer with
|
|
139
|
+
* --purge-state, it isn't a dry run, and there's actually a coord root to
|
|
140
|
+
* delete. Pure so the gating is unit-testable without a terminal.
|
|
141
|
+
*/
|
|
142
|
+
export function shouldPromptForState(o) {
|
|
143
|
+
return o.standalone && o.interactive && !o.dryRun && !o.purgeState && o.coordExists;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The "harnery itself is still installed" line shown to standalone-harn users
|
|
147
|
+
* after a real deinit. `harn deinit` can't remove the package it's running
|
|
148
|
+
* from, so it points at the two ways to finish the job. Pure + exported for the
|
|
149
|
+
* test.
|
|
150
|
+
*/
|
|
151
|
+
export function engineRemovalHint() {
|
|
152
|
+
return ("harnery itself is still installed. To remove the CLI too: `npm rm -g harnery` " +
|
|
153
|
+
"(if you installed it with npm/bun); from a git clone, " +
|
|
154
|
+
"`scripts/teardown.sh --remove-clone` removes the checkout.");
|
|
155
|
+
}
|
|
156
|
+
/** Mirror scripts/teardown.sh's wording: explain what .harnery/ holds, then ask. */
|
|
157
|
+
function confirmDeleteState(coordDir) {
|
|
158
|
+
process.stdout.write("\nharnery saved this project's coordination history in .harnery/\n" +
|
|
159
|
+
"(its event log, councils, agent identities, and scratchpads):\n" +
|
|
160
|
+
` ${coordDir}\n` +
|
|
161
|
+
"Unwiring leaves that in place. Deleting it can't be undone.\n");
|
|
162
|
+
return confirm("Delete this project's harnery history too? [y/N]");
|
|
163
|
+
}
|
|
164
|
+
/** Read one y/N answer from the TTY. Yes only on an explicit y / yes. */
|
|
165
|
+
function confirm(question) {
|
|
166
|
+
return new Promise((resolveAnswer) => {
|
|
167
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
168
|
+
rl.question(`${question} `, (answer) => {
|
|
169
|
+
rl.close();
|
|
170
|
+
resolveAnswer(/^y(es)?$/i.test(answer.trim()));
|
|
171
|
+
});
|
|
104
172
|
});
|
|
105
173
|
}
|
|
106
174
|
/** True when an unwired settings object holds nothing but (optionally) `version`. */
|
|
@@ -108,12 +176,13 @@ function harnessOnly(settings) {
|
|
|
108
176
|
const keys = Object.keys(settings);
|
|
109
177
|
return keys.length === 0 || (keys.length === 1 && keys[0] === "version");
|
|
110
178
|
}
|
|
111
|
-
function render(projectRoot, dryRun, actions) {
|
|
112
|
-
const head = dryRun ? "harn
|
|
179
|
+
function render(projectRoot, dryRun, actions, hint) {
|
|
180
|
+
const head = dryRun ? "harn deinit (dry run): no changes written" : "harn deinit";
|
|
113
181
|
const tail = dryRun
|
|
114
182
|
? "\nRe-run without --dry-run to apply."
|
|
115
183
|
: "\nDone. harnery hooks are unwired; restart your harness session to drop them.";
|
|
116
|
-
|
|
184
|
+
const hintBlock = hint ? `\n\n${hint}` : "";
|
|
185
|
+
return `${head}\n root: ${projectRoot}\n${actions.map((a) => ` ${a}`).join("\n")}${tail}${hintBlock}`;
|
|
117
186
|
}
|
|
118
187
|
function gitTopLevel() {
|
|
119
188
|
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwC/E;AAED,wBAAgB,SAAS,IAAI,KAAK,EAAE,CAYnC"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -14,6 +14,8 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
|
+
import { resolveBinName } from "../core/config.js";
|
|
18
|
+
import { loadHarnessWiring } from "../core/hooks/harness/wiring.js";
|
|
17
19
|
export function registerDoctorCommand(program, emit) {
|
|
18
20
|
program
|
|
19
21
|
.command("doctor")
|
|
@@ -55,6 +57,7 @@ export function runChecks() {
|
|
|
55
57
|
checkGit(),
|
|
56
58
|
checkBun(),
|
|
57
59
|
checkHarneryDir(),
|
|
60
|
+
checkHarnessHooks(),
|
|
58
61
|
checkRestic(),
|
|
59
62
|
checkRclone(),
|
|
60
63
|
checkPlaywright(),
|
|
@@ -172,22 +175,24 @@ function checkPython() {
|
|
|
172
175
|
}
|
|
173
176
|
return { name: "python3", severity: "ok", detail: r.out };
|
|
174
177
|
}
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
/** Walk up from cwd to the nearest dir containing `.harnery/`; null if none. */
|
|
179
|
+
function findCoordProjectRoot() {
|
|
177
180
|
let dir = process.cwd();
|
|
178
181
|
for (let i = 0; i < 8; i++) {
|
|
179
|
-
if (existsSync(path.join(dir, ".harnery")))
|
|
180
|
-
return
|
|
181
|
-
name: ".harnery/",
|
|
182
|
-
severity: "ok",
|
|
183
|
-
detail: path.join(dir, ".harnery"),
|
|
184
|
-
};
|
|
185
|
-
}
|
|
182
|
+
if (existsSync(path.join(dir, ".harnery")))
|
|
183
|
+
return dir;
|
|
186
184
|
const parent = path.dirname(dir);
|
|
187
185
|
if (parent === dir)
|
|
188
186
|
break;
|
|
189
187
|
dir = parent;
|
|
190
188
|
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function checkHarneryDir() {
|
|
192
|
+
const root = findCoordProjectRoot();
|
|
193
|
+
if (root) {
|
|
194
|
+
return { name: ".harnery/", severity: "ok", detail: path.join(root, ".harnery") };
|
|
195
|
+
}
|
|
191
196
|
return {
|
|
192
197
|
name: ".harnery/",
|
|
193
198
|
severity: "warn",
|
|
@@ -195,6 +200,39 @@ function checkHarneryDir() {
|
|
|
195
200
|
hint: "create one with `mkdir -p .harnery/active` from your monorepo root",
|
|
196
201
|
};
|
|
197
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Compare the project's wired harness hooks against HARNESS_SPECS. Catches the
|
|
205
|
+
* post-upgrade case where a harnery release added (or renamed) a hook event but
|
|
206
|
+
* the consumer's settings file hasn't been re-wired. Only fires for a harness
|
|
207
|
+
* the project has opted into (≥1 harnery hook already wired) — see
|
|
208
|
+
* loadHarnessWiring — so a bare settings file never false-warns. The remedy is
|
|
209
|
+
* always the same: re-run `<bin> init` (idempotent, additive).
|
|
210
|
+
*/
|
|
211
|
+
function checkHarnessHooks() {
|
|
212
|
+
const root = findCoordProjectRoot();
|
|
213
|
+
if (!root) {
|
|
214
|
+
return { name: "harness hooks", severity: "ok", detail: "n/a (no .harnery/ above cwd)" };
|
|
215
|
+
}
|
|
216
|
+
const drift = loadHarnessWiring(root);
|
|
217
|
+
if (drift.length === 0) {
|
|
218
|
+
return { name: "harness hooks", severity: "ok", detail: "wired + current" };
|
|
219
|
+
}
|
|
220
|
+
const bin = resolveBinName(root);
|
|
221
|
+
const parts = drift.map((d) => {
|
|
222
|
+
const bits = [];
|
|
223
|
+
if (d.missing.length > 0) {
|
|
224
|
+
bits.push(`${d.missing.length} missing (${d.missing.map((m) => m.subcommand).join(", ")})`);
|
|
225
|
+
}
|
|
226
|
+
if (d.orphans.length > 0)
|
|
227
|
+
bits.push(`${d.orphans.length} orphaned (${d.orphans.join(", ")})`);
|
|
228
|
+
return `${d.settingsFile}: ${bits.join("; ")}`;
|
|
229
|
+
});
|
|
230
|
+
const hasOrphans = drift.some((d) => d.orphans.length > 0);
|
|
231
|
+
const hint = hasOrphans
|
|
232
|
+
? `run \`${bin} init\` to wire missing hooks; remove orphaned entries (renamed/dropped events) with \`${bin} deinit\` then \`${bin} init\``
|
|
233
|
+
: `run \`${bin} init\` to wire the new hook(s) (idempotent)`;
|
|
234
|
+
return { name: "harness hooks", severity: "warn", detail: parts.join(" | "), hint };
|
|
235
|
+
}
|
|
198
236
|
function macOrLinux(mac, linux) {
|
|
199
237
|
return os.platform() === "darwin" ? mac : linux;
|
|
200
238
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -18,26 +18,8 @@
|
|
|
18
18
|
import type { Command } from "commander";
|
|
19
19
|
import type { EmitContext } from "../commander.js";
|
|
20
20
|
import { type HarnessId, type HarnessSpec } from "../core/hooks/harness/events.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
matcher?: string;
|
|
24
|
-
hooks: {
|
|
25
|
-
type: string;
|
|
26
|
-
command: string;
|
|
27
|
-
}[];
|
|
28
|
-
}
|
|
29
|
-
/** Cursor entry: a flat `{ command }`. */
|
|
30
|
-
interface CursorHookGroup {
|
|
31
|
-
command: string;
|
|
32
|
-
type?: string;
|
|
33
|
-
matcher?: string;
|
|
34
|
-
}
|
|
35
|
-
type HookGroup = ClaudeHookGroup | CursorHookGroup;
|
|
36
|
-
export interface SettingsFile {
|
|
37
|
-
version?: number;
|
|
38
|
-
hooks?: Record<string, HookGroup[]>;
|
|
39
|
-
[k: string]: unknown;
|
|
40
|
-
}
|
|
21
|
+
import { type HookGroup, type SettingsFile } from "../core/hooks/harness/wiring.js";
|
|
22
|
+
export type { HookGroup, SettingsFile };
|
|
41
23
|
export declare function registerInitCommand(program: Command, emit: EmitContext, binName?: string): void;
|
|
42
24
|
/**
|
|
43
25
|
* Merge agent-hook entries into a harness settings object in place, idempotently.
|
|
@@ -78,5 +60,4 @@ export declare function unwireHooks(settings: SettingsFile): {
|
|
|
78
60
|
* - key absent → splice it in as the first key (comment-safe).
|
|
79
61
|
*/
|
|
80
62
|
export declare function stampBinName(configPath: string, binName: string, dryRun: boolean): string | null;
|
|
81
|
-
export {};
|
|
82
63
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAClG,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,iCAAiC,CAAC;AAIzC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAWxC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAsF/F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,SAAS,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAsBpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAmB1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmDhG"}
|
package/dist/commands/init.js
CHANGED
|
@@ -20,7 +20,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
20
20
|
import { dirname, relative, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { DEFAULT_BIN_NAME, stripJsonComments } from "../core/config.js";
|
|
23
|
-
import { HARNESS_SPECS
|
|
23
|
+
import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
|
|
24
|
+
import { commandWiresSubcommand, groupCommands, makeEntry, } from "../core/hooks/harness/wiring.js";
|
|
24
25
|
// This file is src/commands/init.ts → harnery package root is two levels up.
|
|
25
26
|
const HARNERY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
26
27
|
export function registerInitCommand(program, emit, binName) {
|
|
@@ -122,7 +123,7 @@ export function wireHooks(settings, spec, agentHookPath, harness) {
|
|
|
122
123
|
for (const { settingsKey, subcommand } of spec.events) {
|
|
123
124
|
const command = `bash ${agentHookPath} ${subcommand} --harness ${harness}`;
|
|
124
125
|
const groups = settings.hooks[settingsKey] ?? [];
|
|
125
|
-
const present = groups.some((g) => groupCommands(g).some((c) => c
|
|
126
|
+
const present = groups.some((g) => groupCommands(g).some((c) => commandWiresSubcommand(c, subcommand)));
|
|
126
127
|
if (present) {
|
|
127
128
|
already++;
|
|
128
129
|
continue;
|
|
@@ -169,19 +170,6 @@ export function unwireHooks(settings) {
|
|
|
169
170
|
delete settings.hooks;
|
|
170
171
|
return { removed, remaining };
|
|
171
172
|
}
|
|
172
|
-
/** Build a hook entry in the harness's shape. */
|
|
173
|
-
function makeEntry(shape, command) {
|
|
174
|
-
return shape === "cursor" ? { command } : { hooks: [{ type: "command", command }] };
|
|
175
|
-
}
|
|
176
|
-
/** Pull every command string out of a hook entry, regardless of shape. */
|
|
177
|
-
function groupCommands(group) {
|
|
178
|
-
if ("command" in group && typeof group.command === "string")
|
|
179
|
-
return [group.command];
|
|
180
|
-
if ("hooks" in group && Array.isArray(group.hooks)) {
|
|
181
|
-
return group.hooks.map((h) => h.command).filter((c) => typeof c === "string");
|
|
182
|
-
}
|
|
183
|
-
return [];
|
|
184
|
-
}
|
|
185
173
|
/**
|
|
186
174
|
* Idempotently record `binName` in `.harnery/config.jsonc`, preserving any
|
|
187
175
|
* existing JSONC comments and the `files` section. Returns an action line, or
|
|
@@ -330,15 +330,30 @@ export function selfDisplayName() {
|
|
|
330
330
|
return displayName(owner);
|
|
331
331
|
}
|
|
332
332
|
function readPpid(pid) {
|
|
333
|
+
// Linux/WSL fast path: /proc/<pid>/status carries `PPid:`.
|
|
333
334
|
try {
|
|
334
335
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
335
336
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
if (m) {
|
|
338
|
+
const parsed = Number.parseInt(m[1], 10);
|
|
339
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
340
342
|
}
|
|
341
343
|
catch {
|
|
342
|
-
|
|
344
|
+
// no /proc (macOS/BSD) — fall through to ps
|
|
345
|
+
}
|
|
346
|
+
// Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
|
|
347
|
+
try {
|
|
348
|
+
const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
|
|
349
|
+
if (out.status === 0) {
|
|
350
|
+
const parsed = Number.parseInt(out.stdout.trim(), 10);
|
|
351
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// ps unavailable — give up
|
|
343
357
|
}
|
|
358
|
+
return null;
|
|
344
359
|
}
|
|
@@ -22,8 +22,18 @@ export interface RenderOpts {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function renderSessionContext(opts: RenderOpts): string;
|
|
24
24
|
/**
|
|
25
|
-
* Returns a list of wiring issues (empty when
|
|
26
|
-
*
|
|
25
|
+
* Returns a list of commit-guard wiring issues (empty when wired). Portable
|
|
26
|
+
* across host projects: it asserts the FUNCTIONAL property ("does this repo's
|
|
27
|
+
* pre-commit invoke harnery's guard?") rather than any path convention. For
|
|
28
|
+
* each repo it resolves the EFFECTIVE git-hooks dir via
|
|
29
|
+
* `git rev-parse --git-path hooks` (which already honors `core.hooksPath`,
|
|
30
|
+
* linked worktrees, and submodule gitdirs) and checks whether the `pre-commit`
|
|
31
|
+
* there calls `agent-coord` / `agent-hook`. Checks the parent repo + one
|
|
32
|
+
* representative submodule (others almost always share the same setup).
|
|
33
|
+
*
|
|
34
|
+
* harnery does not install git hooks itself — each host wires its own
|
|
35
|
+
* pre-commit to invoke the guard — so the remediation command is host-specific
|
|
36
|
+
* and supplied via `hooksSetupHint` in `.harnery/config.jsonc` (see the caller).
|
|
27
37
|
*/
|
|
28
38
|
export declare function checkWiring(coordRoot: string): string[];
|
|
29
39
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAoB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA+D7D;AAwHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
|