harnery 0.31.6 → 0.32.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +7 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +169 -27
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +27 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +10 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +27 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
package/src/commands/docs.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
runFrontmatterMigration,
|
|
7
7
|
} from "../lib/docs-frontmatter-migrate.ts";
|
|
8
8
|
import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.ts";
|
|
9
|
+
import { initDocsContext as initDocsLinks, runLinks } from "../lib/docs-links.ts";
|
|
9
10
|
import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.ts";
|
|
10
11
|
import { readDocsMetadata, readDocsMetadataKey } from "../lib/docs-meta.ts";
|
|
11
12
|
import {
|
|
@@ -27,6 +28,7 @@ function ensureContext(context: HarneryProgramContext | undefined): void {
|
|
|
27
28
|
extraExcludedPrefixes: context.extraDocsExcludedPrefixes,
|
|
28
29
|
docsRootAllowlist: context.docsRootAllowlist,
|
|
29
30
|
});
|
|
31
|
+
initDocsLinks({ ...opts, extraExcludedPrefixes: context.extraDocsExcludedPrefixes });
|
|
30
32
|
initDocsSweep(opts);
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -115,6 +117,34 @@ export function registerDocsCommand(
|
|
|
115
117
|
}
|
|
116
118
|
});
|
|
117
119
|
|
|
120
|
+
docs
|
|
121
|
+
.command("links")
|
|
122
|
+
.description("Check internal Markdown link targets and heading fragments")
|
|
123
|
+
.option("--repo <name>", "Limit to one submodule or '.' for parent")
|
|
124
|
+
.option("--no-fragments", "Check target existence only; skip heading-fragment validation")
|
|
125
|
+
.option("--strict", "Report findings in archive/audit/changelog docs as errors, not warnings")
|
|
126
|
+
.option("--check-escapes", "Also flag links that resolve outside their own repo root")
|
|
127
|
+
.option("--fail", "Exit non-zero when errors are found (advisory by default)")
|
|
128
|
+
.option("--format <type>", "Output format: human, json", "human")
|
|
129
|
+
.action(
|
|
130
|
+
async (opts: {
|
|
131
|
+
repo?: string;
|
|
132
|
+
fragments?: boolean;
|
|
133
|
+
strict?: boolean;
|
|
134
|
+
checkEscapes?: boolean;
|
|
135
|
+
fail?: boolean;
|
|
136
|
+
format: string;
|
|
137
|
+
}) => {
|
|
138
|
+
try {
|
|
139
|
+
ensureContext(context);
|
|
140
|
+
await handleLinks(opts);
|
|
141
|
+
} catch (err: unknown) {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
emit.error({ code: "docs_error", message: msg });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
118
148
|
docs
|
|
119
149
|
.command("sweep")
|
|
120
150
|
.description(
|
|
@@ -232,6 +262,32 @@ async function handleLint(opts: { fast?: boolean; repo?: string; format: string
|
|
|
232
262
|
if (errors.length > 0) emit.setExitCode(1);
|
|
233
263
|
}
|
|
234
264
|
|
|
265
|
+
// --- `harn docs links` ---
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Advisory by default: a clean exit code even with findings, because turning
|
|
269
|
+
* link health into a gate is an enforcement policy decision for the host, not
|
|
270
|
+
* something this command should assume. `--fail` is the opt-in for CI or a hook.
|
|
271
|
+
*/
|
|
272
|
+
async function handleLinks(opts: {
|
|
273
|
+
repo?: string;
|
|
274
|
+
fragments?: boolean;
|
|
275
|
+
strict?: boolean;
|
|
276
|
+
checkEscapes?: boolean;
|
|
277
|
+
fail?: boolean;
|
|
278
|
+
format: string;
|
|
279
|
+
}): Promise<void> {
|
|
280
|
+
if (opts.format === "json") emit.config({ format: "json" });
|
|
281
|
+
const report = await runLinks({
|
|
282
|
+
repo: opts.repo,
|
|
283
|
+
noFragments: opts.fragments === false,
|
|
284
|
+
strict: opts.strict,
|
|
285
|
+
checkEscapes: opts.checkEscapes,
|
|
286
|
+
});
|
|
287
|
+
emit.data({ ...report, advisory: !opts.fail });
|
|
288
|
+
if (opts.fail && report.error_count > 0) emit.setExitCode(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
235
291
|
// --- `harn docs sweep` ---
|
|
236
292
|
|
|
237
293
|
async function handleSweep(opts: { repo?: string; format: string }): Promise<void> {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Command } from "commander";
|
|
|
19
19
|
import type { EmitContext } from "../commander.ts";
|
|
20
20
|
import { BUILTIN_ADAPTER_IDS } from "../core/adapters/index.ts";
|
|
21
21
|
import { resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { ADAPTER_SPECS, type AdapterId } from "../core/hooks/adapter/events.ts";
|
|
23
|
+
import { loadAdapterWiring, summarizeAdapterWiring } from "../core/hooks/adapter/wiring.ts";
|
|
24
|
+
import { inspectCodexWslBridge } from "../core/hooks/codex-wsl-bridge.ts";
|
|
23
25
|
import {
|
|
24
26
|
ADAPTER_BINARIES,
|
|
25
27
|
ADAPTER_INSTALL_HINTS,
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
} from "../core/workflow/adapters.ts";
|
|
28
30
|
import { probeBilling } from "../core/workflow/billing.ts";
|
|
29
31
|
import type { AdapterName } from "../core/workflow/types.ts";
|
|
32
|
+
import { checkGitHooks, gitHooksInstalled } from "../lib/instructions/git-hooks.ts";
|
|
30
33
|
import { findRg, installRg, managedRgPath, rgInstallSupported } from "../lib/tools/ripgrep.ts";
|
|
31
34
|
|
|
32
35
|
type Severity = "ok" | "warn" | "fail";
|
|
@@ -97,14 +100,22 @@ export function registerDoctorCommand(program: Command, emit: EmitContext): void
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
export function runChecks(): Check[] {
|
|
103
|
+
const codexWslBridge = checkCodexWslBridge();
|
|
104
|
+
// Probe the adapter CLIs before the hook check runs, not in output order:
|
|
105
|
+
// an unwired adapter only matters if its CLI is actually installed, and
|
|
106
|
+
// probing once here keeps that join to one spawn per CLI.
|
|
107
|
+
const workflow = BUILTIN_ADAPTER_IDS.map((id) => ({ id, ...checkWorkflowAdapter(id) }));
|
|
108
|
+
const installed = workflow.filter((w) => w.installed).map((w) => w.id as AdapterId);
|
|
100
109
|
return [
|
|
101
110
|
checkNode(),
|
|
102
111
|
checkGit(),
|
|
103
112
|
checkBun(),
|
|
104
113
|
checkRipgrep(),
|
|
105
114
|
checkHarneryDir(),
|
|
106
|
-
checkAdapterHooks(),
|
|
107
|
-
|
|
115
|
+
checkAdapterHooks(installed),
|
|
116
|
+
checkGitHookRegions(),
|
|
117
|
+
...(codexWslBridge ? [codexWslBridge] : []),
|
|
118
|
+
...workflow.map((w) => w.check),
|
|
108
119
|
checkRestic(),
|
|
109
120
|
checkRclone(),
|
|
110
121
|
checkPlaywright(),
|
|
@@ -112,6 +123,49 @@ export function runChecks(): Check[] {
|
|
|
112
123
|
];
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
function checkGitHookRegions(): Check {
|
|
127
|
+
const root = findCoordProjectRoot();
|
|
128
|
+
if (!root) {
|
|
129
|
+
return { name: "git hooks", severity: "ok", detail: "n/a (no .harnery/ above cwd)" };
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
if (!gitHooksInstalled(root)) {
|
|
133
|
+
const bin = resolveBinName(root);
|
|
134
|
+
return {
|
|
135
|
+
name: "git hooks",
|
|
136
|
+
severity: "warn",
|
|
137
|
+
detail:
|
|
138
|
+
`not installed - commits are not guarded against peer-claim conflicts and ` +
|
|
139
|
+
`claims never auto-release on commit/checkout; run \`${bin} init\` to install`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const { status, issues } = checkGitHooks(root);
|
|
143
|
+
if (status === "fresh") {
|
|
144
|
+
return { name: "git hooks", severity: "ok", detail: "managed regions current" };
|
|
145
|
+
}
|
|
146
|
+
return { name: "git hooks", severity: "warn", detail: issues.join("; ") };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return {
|
|
149
|
+
name: "git hooks",
|
|
150
|
+
severity: "warn",
|
|
151
|
+
detail: `check failed: ${(err as Error).message}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkCodexWslBridge(): Check | null {
|
|
157
|
+
const status = inspectCodexWslBridge();
|
|
158
|
+
if (!status) return null;
|
|
159
|
+
return {
|
|
160
|
+
name: "codex:WSL bridge",
|
|
161
|
+
severity: status.ok ? "ok" : "warn",
|
|
162
|
+
detail: status.detail,
|
|
163
|
+
hint: status.ok
|
|
164
|
+
? undefined
|
|
165
|
+
: "forward CODEX_THREAD_ID through WSLENV in the Codex shell environment policy, then start a fresh task",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
115
169
|
function checkRipgrep(): Check {
|
|
116
170
|
const found = findRg();
|
|
117
171
|
if (found) {
|
|
@@ -157,26 +211,35 @@ function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolea
|
|
|
157
211
|
* One workflow spawn target: is the adapter CLI installed, and how will its
|
|
158
212
|
* headless children bill (subscription login vs API key — see billing.ts)?
|
|
159
213
|
* Missing is a warn, not a fail: workflows degrade to the adapters you have.
|
|
214
|
+
*
|
|
215
|
+
* Reports `installed` alongside the check so `checkAdapterHooks` can reuse the
|
|
216
|
+
* probe instead of spawning every adapter CLI a second time.
|
|
160
217
|
*/
|
|
161
|
-
function checkWorkflowAdapter(adapter: AdapterName): Check {
|
|
218
|
+
function checkWorkflowAdapter(adapter: AdapterName): { check: Check; installed: boolean } {
|
|
162
219
|
const bin = ADAPTER_BINARIES[adapter];
|
|
163
220
|
const name = `workflow:${adapter}`;
|
|
164
221
|
const r = whichVersion(bin);
|
|
165
222
|
if (!r.ok) {
|
|
166
223
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
224
|
+
check: {
|
|
225
|
+
name,
|
|
226
|
+
severity: "warn",
|
|
227
|
+
detail: `${bin} missing (workflow --adapter ${adapter} unavailable)`,
|
|
228
|
+
hint: `${ADAPTER_INSTALL_HINTS[adapter]} then: ${ADAPTER_LOGIN_HINTS[adapter]}`,
|
|
229
|
+
},
|
|
230
|
+
installed: false,
|
|
171
231
|
};
|
|
172
232
|
}
|
|
173
233
|
const probe = probeBilling(adapter);
|
|
174
234
|
if (probe.login === "absent" && !probe.apiKeyPresent) {
|
|
175
235
|
return {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
236
|
+
check: {
|
|
237
|
+
name,
|
|
238
|
+
severity: "warn",
|
|
239
|
+
detail: `${r.out} — installed, but no stored login or API key detected`,
|
|
240
|
+
hint: ADAPTER_LOGIN_HINTS[adapter],
|
|
241
|
+
},
|
|
242
|
+
installed: true,
|
|
180
243
|
};
|
|
181
244
|
}
|
|
182
245
|
const billing =
|
|
@@ -185,7 +248,7 @@ function checkWorkflowAdapter(adapter: AdapterName): Check {
|
|
|
185
248
|
? "billing: subscription"
|
|
186
249
|
: "billing: subscription (login unverifiable, CLI is the authority)"
|
|
187
250
|
: `billing: ${probe.mode} (${probe.apiKeySource})`;
|
|
188
|
-
return { name, severity: "ok", detail: `${r.out} — ${billing}` };
|
|
251
|
+
return { check: { name, severity: "ok", detail: `${r.out} — ${billing}` }, installed: true };
|
|
189
252
|
}
|
|
190
253
|
|
|
191
254
|
function checkNode(): Check {
|
|
@@ -331,13 +394,22 @@ function checkHarneryDir(): Check {
|
|
|
331
394
|
* loadAdapterWiring — so a bare settings file never false-warns. The remedy is
|
|
332
395
|
* always the same: re-run `<bin> init` (idempotent, additive).
|
|
333
396
|
*/
|
|
334
|
-
function checkAdapterHooks(): Check {
|
|
397
|
+
function checkAdapterHooks(installedAdapters: AdapterId[] = []): Check {
|
|
335
398
|
const root = findCoordProjectRoot();
|
|
336
399
|
if (!root) {
|
|
337
400
|
return { name: "adapter hooks", severity: "ok", detail: "n/a (no .harnery/ above cwd)" };
|
|
338
401
|
}
|
|
339
402
|
const drift = loadAdapterWiring(root);
|
|
340
|
-
|
|
403
|
+
// A adapter with zero harnery hooks is only worth flagging when the project
|
|
404
|
+
// plainly uses harnery hooks (something else is wired) AND that adapter's CLI
|
|
405
|
+
// is installed. Both conditions matter: the first keeps a project that has
|
|
406
|
+
// never run init quiet, the second keeps a project that simply doesn't have
|
|
407
|
+
// Codex quiet. What's left is the case that reads as healthy but isn't — an
|
|
408
|
+
// agent can start a session through that adapter and register nothing.
|
|
409
|
+
const summary = summarizeAdapterWiring(root);
|
|
410
|
+
const unwired =
|
|
411
|
+
summary.wired.length > 0 ? summary.unwired.filter((id) => installedAdapters.includes(id)) : [];
|
|
412
|
+
if (drift.length === 0 && unwired.length === 0) {
|
|
341
413
|
return { name: "adapter hooks", severity: "ok", detail: "wired + current" };
|
|
342
414
|
}
|
|
343
415
|
const bin = resolveBinName(root);
|
|
@@ -356,12 +428,22 @@ function checkAdapterHooks(): Check {
|
|
|
356
428
|
}
|
|
357
429
|
return `${d.settingsFile}: ${bits.join("; ")}`;
|
|
358
430
|
});
|
|
431
|
+
for (const id of unwired) {
|
|
432
|
+
parts.push(`${ADAPTER_SPECS[id].settingsFile}: ${id} CLI installed, no harnery hooks wired`);
|
|
433
|
+
}
|
|
359
434
|
const needsManualRepair = drift.some(
|
|
360
435
|
(d) => d.parseError || d.invalidTopLevelKeys.length > 0 || d.invalidEventKeys.length > 0,
|
|
361
436
|
);
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
437
|
+
const wireHints = unwired.map((id) => `\`${bin} init --adapter ${id}\``);
|
|
438
|
+
let hint: string;
|
|
439
|
+
if (needsManualRepair) {
|
|
440
|
+
hint = `repair the invalid adapter settings, then run \`${bin} init\` to migrate harnery hooks`;
|
|
441
|
+
} else if (drift.length > 0) {
|
|
442
|
+
hint = `run \`${bin} init\` to migrate the hook set (idempotent)`;
|
|
443
|
+
if (wireHints.length > 0) hint += `; ${wireHints.join(" and ")} to wire the rest`;
|
|
444
|
+
} else {
|
|
445
|
+
hint = `run ${wireHints.join(" and ")} (idempotent, additive)`;
|
|
446
|
+
}
|
|
365
447
|
return { name: "adapter hooks", severity: "warn", detail: parts.join(" | "), hint };
|
|
366
448
|
}
|
|
367
449
|
|
package/src/commands/init.ts
CHANGED
|
@@ -36,12 +36,9 @@ import {
|
|
|
36
36
|
applyInstructions,
|
|
37
37
|
checkInstructions,
|
|
38
38
|
} from "../lib/instructions/apply.ts";
|
|
39
|
+
import { applyGitHooks, checkGitHooks } from "../lib/instructions/git-hooks.ts";
|
|
39
40
|
import { HostAddendumError } from "../lib/instructions/host-addendum.ts";
|
|
40
41
|
|
|
41
|
-
// Re-exported for back-compat: callers (deinit, tests) import these names
|
|
42
|
-
// from init.ts. The definitions now live in core/hooks/adapter/wiring.ts.
|
|
43
|
-
export type { HookGroup, SettingsFile };
|
|
44
|
-
|
|
45
42
|
// This file is src/commands/init.ts → harnery package root is two levels up.
|
|
46
43
|
const HARNERY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
47
44
|
|
|
@@ -85,15 +82,19 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
85
82
|
// ── --check: read-only drift report on the block + skills ──────────────
|
|
86
83
|
if (opts.check === true) {
|
|
87
84
|
const { status, issues } = checkInstructions(projectRoot, { binName: bin, adapter });
|
|
85
|
+
const gitHooks = checkGitHooks(projectRoot);
|
|
86
|
+
if (gitHooks.status !== "fresh") issues.push(...gitHooks.issues);
|
|
87
|
+
const merged =
|
|
88
|
+
status === "error" ? "error" : gitHooks.status !== "fresh" ? "drift" : status;
|
|
88
89
|
const head =
|
|
89
|
-
|
|
90
|
-
? "harn init --check: instructions + skills are current"
|
|
91
|
-
:
|
|
90
|
+
merged === "fresh"
|
|
91
|
+
? "harn init --check: instructions + skills + git hooks are current"
|
|
92
|
+
: merged === "drift"
|
|
92
93
|
? "harn init --check: drift found (re-run `init` to refresh)"
|
|
93
94
|
: "harn init --check: error";
|
|
94
95
|
const lines = issues.length ? `\n${issues.map((i) => ` ✗ ${i}`).join("\n")}` : "";
|
|
95
96
|
emit.text(`${head}${lines}`);
|
|
96
|
-
emit.setExitCode(
|
|
97
|
+
emit.setExitCode(merged === "fresh" ? 0 : merged === "drift" ? 2 : 1);
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -190,7 +191,14 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
190
191
|
}
|
|
191
192
|
actions.push(...applied.actions);
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
// ── 4. git-hook managed regions ────────────────────────────────────────
|
|
195
|
+
// Same lifecycle contract as the instructions block: the coordination
|
|
196
|
+
// content of the consumer's git hooks is machine-owned and versioned;
|
|
197
|
+
// everything else in those files belongs to the host.
|
|
198
|
+
const gitHooks = applyGitHooks(projectRoot, { dryRun });
|
|
199
|
+
actions.push(...gitHooks.actions);
|
|
200
|
+
|
|
201
|
+
emit.text(render(projectRoot, dryRun, actions, [...applied.warnings, ...gitHooks.warnings]));
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -76,7 +76,6 @@ interface LogsOpts {
|
|
|
76
76
|
name?: string;
|
|
77
77
|
follow?: boolean;
|
|
78
78
|
gate?: boolean;
|
|
79
|
-
cloudflared?: boolean;
|
|
80
79
|
provider?: boolean;
|
|
81
80
|
}
|
|
82
81
|
|
|
@@ -916,7 +915,7 @@ function status(opts: StatusOpts): void {
|
|
|
916
915
|
return;
|
|
917
916
|
}
|
|
918
917
|
if (states.length === 1) {
|
|
919
|
-
//
|
|
918
|
+
// A single tunnel is clearer as a detail block than as a one-row table.
|
|
920
919
|
statusDetail(states[0]);
|
|
921
920
|
return;
|
|
922
921
|
}
|
|
@@ -953,8 +952,7 @@ function logs(opts: LogsOpts): void {
|
|
|
953
952
|
const name = resolveName(opts.name);
|
|
954
953
|
const state = readState(name);
|
|
955
954
|
const provider = state?.provider ?? "cloudflare";
|
|
956
|
-
const which =
|
|
957
|
-
opts.cloudflared || opts.provider ? providerLogFile(name, provider) : gateLogFile(name);
|
|
955
|
+
const which = opts.provider ? providerLogFile(name, provider) : gateLogFile(name);
|
|
958
956
|
const path = cachePath("tunnel", which);
|
|
959
957
|
if (!existsSync(path)) {
|
|
960
958
|
emit.error({ code: "tunnel_no_log", message: `No log file at ${path}` });
|
|
@@ -1089,7 +1087,6 @@ export function registerTunnelCommand(
|
|
|
1089
1087
|
.option("-f, --follow", "follow the log")
|
|
1090
1088
|
.option("--gate", "tail the gate log (default)")
|
|
1091
1089
|
.option("--provider", "tail the provider log instead")
|
|
1092
|
-
.option("--cloudflared", "tail the Cloudflare provider log instead (legacy alias)")
|
|
1093
1090
|
.action(logs);
|
|
1094
1091
|
|
|
1095
1092
|
const allow = cmd
|
|
@@ -78,8 +78,7 @@ export function emitCanonical(input: CanonicalEmitInput): void {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Normalize the heartbeat's `platform` field
|
|
82
|
-
* `claude_code`) to the canonical kebab-case Adapter type.
|
|
81
|
+
* Normalize the heartbeat's `platform` field to the canonical Adapter type.
|
|
83
82
|
*/
|
|
84
83
|
export function normalizeAdapter(platform: string | undefined): "claude-code" | "cursor" | "codex" {
|
|
85
84
|
if (platform === "cursor") return "cursor";
|
package/src/core/agents/cli.ts
CHANGED
|
@@ -96,23 +96,11 @@ async function handleVerdict(root: string): Promise<number> {
|
|
|
96
96
|
} else if (parsed.rule === "claim") {
|
|
97
97
|
const { evaluateClaim } = await import("./rules/claim-conflict.ts");
|
|
98
98
|
verdict = evaluateClaim(root, parsed as unknown as Parameters<typeof evaluateClaim>[1]);
|
|
99
|
-
} else if (parsed.rule === "commit") {
|
|
100
|
-
const { evaluateCommit } = await import("./rules/commit-conflict.ts");
|
|
101
|
-
const result = evaluateCommit(root, parsed as unknown as Parameters<typeof evaluateCommit>[1]);
|
|
102
|
-
// Map CommitVerdictResult → the standard verdict envelope, stash details
|
|
103
|
-
// on extra fields so the bash caller can pull conflicts + log_lines.
|
|
104
|
-
verdict = {
|
|
105
|
-
allow: result.allow,
|
|
106
|
-
exit_code: result.exit_code,
|
|
107
|
-
rule: result.rule,
|
|
108
|
-
reason: result.message,
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
110
|
-
} as typeof verdict;
|
|
111
|
-
(verdict as Record<string, unknown>).conflicts = result.conflicts;
|
|
112
|
-
(verdict as Record<string, unknown>).log_lines = result.log_lines;
|
|
113
|
-
(verdict as Record<string, unknown>).suppressed_self_attribution =
|
|
114
|
-
result.suppressed_self_attribution ?? false;
|
|
115
99
|
} else {
|
|
100
|
+
// NB: rule === "commit" was served here until 2026-08-09; the commit
|
|
101
|
+
// guard now enters through `git-hook pre-commit`, which runs the same
|
|
102
|
+
// evaluateCommit in-process. A straggler host lands in this fail-open
|
|
103
|
+
// branch, which is the pre-migration behavior for a malformed request.
|
|
116
104
|
verdict = {
|
|
117
105
|
allow: true,
|
|
118
106
|
exit_code: 0,
|
|
@@ -261,14 +249,6 @@ async function handleStateAction(root: string, action: string, rest: string[]):
|
|
|
261
249
|
);
|
|
262
250
|
return 0;
|
|
263
251
|
}
|
|
264
|
-
case "stamp-status-call": {
|
|
265
|
-
const hb = writer.stampStatusCheck(root, owner);
|
|
266
|
-
if (!hb) return 1;
|
|
267
|
-
process.stdout.write(
|
|
268
|
-
`${JSON.stringify({ instance_id: owner, last_status_at: hb.last_status_at })}\n`,
|
|
269
|
-
);
|
|
270
|
-
return 0;
|
|
271
|
-
}
|
|
272
252
|
case "set-turn-summary": {
|
|
273
253
|
const summary = args.join(" ");
|
|
274
254
|
const hb = writer.setTurnSummary(root, owner, summary);
|
|
@@ -344,10 +324,20 @@ async function handleStateAction(root: string, action: string, rest: string[]):
|
|
|
344
324
|
// fragility. Positionals (sessionId, model) stay as-is once flags are
|
|
345
325
|
// filtered out.
|
|
346
326
|
const adapter = args.find((a) => a.startsWith("--adapter="))?.slice("--adapter=".length);
|
|
327
|
+
const forkedFrom = args
|
|
328
|
+
.find((a) => a.startsWith("--forked-from="))
|
|
329
|
+
?.slice("--forked-from=".length);
|
|
347
330
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
348
331
|
const sessionId = positional[0];
|
|
349
332
|
const model = positional[1];
|
|
350
|
-
const hb = writer.healHeartbeat(
|
|
333
|
+
const hb = writer.healHeartbeat(
|
|
334
|
+
root,
|
|
335
|
+
owner,
|
|
336
|
+
sessionId,
|
|
337
|
+
model,
|
|
338
|
+
adapter,
|
|
339
|
+
forkedFrom ? { forkedFrom } : undefined,
|
|
340
|
+
);
|
|
351
341
|
process.stdout.write(`${JSON.stringify({ instance_id: owner, recreated: !!hb })}\n`);
|
|
352
342
|
return hb ? 0 : 1;
|
|
353
343
|
}
|
|
@@ -497,69 +487,39 @@ async function handleCouncilAction(root: string, action: string, rest: string[])
|
|
|
497
487
|
|
|
498
488
|
async function handleAssignName(root: string, rest: string[]): Promise<number> {
|
|
499
489
|
const { assignName } = await import("./state/names.ts");
|
|
500
|
-
|
|
490
|
+
// Optional recorded-fork-lineage flag: --forked-from <parent_instance_id>.
|
|
491
|
+
// Supplied by an adapter layer that knows (or detected) that this session
|
|
492
|
+
// was branched from another conversation. Inert on resume: assignName only
|
|
493
|
+
// stamps lineage on the row that first assigns the instance.
|
|
494
|
+
let forkedFrom: string | undefined;
|
|
495
|
+
const positional: string[] = [];
|
|
496
|
+
for (let i = 0; i < rest.length; i++) {
|
|
497
|
+
if (rest[i] === "--forked-from") {
|
|
498
|
+
forkedFrom = rest[++i];
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
positional.push(rest[i]!);
|
|
502
|
+
}
|
|
503
|
+
const [owner, kindArg] = positional;
|
|
501
504
|
if (!owner || !kindArg) {
|
|
502
|
-
process.stderr.write(
|
|
505
|
+
process.stderr.write(
|
|
506
|
+
"agent-coord assign-name <instance_id> <session|subagent|transient> [--forked-from <instance_id>]\n",
|
|
507
|
+
);
|
|
503
508
|
return 2;
|
|
504
509
|
}
|
|
505
510
|
if (kindArg !== "session" && kindArg !== "subagent" && kindArg !== "transient") {
|
|
506
511
|
process.stderr.write(`agent-coord assign-name: invalid kind ${kindArg}\n`);
|
|
507
512
|
return 2;
|
|
508
513
|
}
|
|
509
|
-
const name = assignName(root, owner, kindArg);
|
|
510
|
-
process.stdout.write(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
req = JSON.parse(raw);
|
|
519
|
-
} catch {
|
|
520
|
-
return 0;
|
|
521
|
-
}
|
|
522
|
-
const { groupUnclaim } = await import("./state/heartbeat-writer.ts");
|
|
523
|
-
|
|
524
|
-
// Session-group-wide unclaim. `owner` is the parent's session_id which is
|
|
525
|
-
// also the group key (parent + subagents share session_id). Each actual
|
|
526
|
-
// removal also emits a durable claim.release event — the projector rebuilds
|
|
527
|
-
// files_touched from the permanent Edit/Write events, so a file-only prune
|
|
528
|
-
// would resurrect on the next replay.
|
|
529
|
-
if (req.owner && Array.isArray(req.prune)) {
|
|
530
|
-
for (const path of req.prune) {
|
|
531
|
-
try {
|
|
532
|
-
for (const hit of groupUnclaim(root, req.owner, path)) {
|
|
533
|
-
await emitClaimRelease(root, hit.instance_id, hit, path, "commit");
|
|
534
|
-
}
|
|
535
|
-
} catch {
|
|
536
|
-
/* best-effort */
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return 0;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
async function handlePostCheckout(root: string, _rest: string[]): Promise<number> {
|
|
544
|
-
const raw = await readStdin();
|
|
545
|
-
let req: { owner?: string; removed?: string[] } = {};
|
|
546
|
-
try {
|
|
547
|
-
req = JSON.parse(raw);
|
|
548
|
-
} catch {
|
|
549
|
-
return 0;
|
|
550
|
-
}
|
|
551
|
-
const { groupUnclaim } = await import("./state/heartbeat-writer.ts");
|
|
552
|
-
if (req.owner && Array.isArray(req.removed)) {
|
|
553
|
-
for (const path of req.removed) {
|
|
554
|
-
try {
|
|
555
|
-
for (const hit of groupUnclaim(root, req.owner, path)) {
|
|
556
|
-
await emitClaimRelease(root, hit.instance_id, hit, path, "checkout");
|
|
557
|
-
}
|
|
558
|
-
} catch {
|
|
559
|
-
/* best-effort */
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
514
|
+
const name = assignName(root, owner, kindArg, forkedFrom ? { forkedFrom } : undefined);
|
|
515
|
+
process.stdout.write(
|
|
516
|
+
`${JSON.stringify({
|
|
517
|
+
instance_id: owner,
|
|
518
|
+
name,
|
|
519
|
+
kind: kindArg,
|
|
520
|
+
...(forkedFrom ? { forked_from: forkedFrom } : {}),
|
|
521
|
+
})}\n`,
|
|
522
|
+
);
|
|
563
523
|
return 0;
|
|
564
524
|
}
|
|
565
525
|
|
|
@@ -841,6 +801,100 @@ async function handleEmitEvent(root: string, rest: string[]): Promise<number> {
|
|
|
841
801
|
return 0;
|
|
842
802
|
}
|
|
843
803
|
|
|
804
|
+
/**
|
|
805
|
+
* `agent-coord git-hook <event> [args]` — the single entry a host git hook
|
|
806
|
+
* invokes. Owns ALL of the coordination git plumbing that hosts used to carry
|
|
807
|
+
* as bash (staged/committed/checkout-removed collection, submodule
|
|
808
|
+
* canonicalization, gitlink probe, verdict, claim pruning), so a harnery
|
|
809
|
+
* upgrade upgrades hook behavior with no change to the host's hook file.
|
|
810
|
+
*
|
|
811
|
+
* Events: pre-commit (E-guard verdict; exit 1 blocks the commit),
|
|
812
|
+
* post-commit (prune committed paths' claims), post-checkout <old> <new>
|
|
813
|
+
* (release claims on paths the ref move rewrote).
|
|
814
|
+
*
|
|
815
|
+
* Fail-open by design: an internal error never blocks a commit (parity with
|
|
816
|
+
* the bash era, where a crashed verdict read as empty and the hook moved on).
|
|
817
|
+
* A clean conflict verdict still blocks.
|
|
818
|
+
*/
|
|
819
|
+
async function handleGitHook(fallbackRoot: string, rest: string[]): Promise<number> {
|
|
820
|
+
const event = rest[0];
|
|
821
|
+
if (process.env.HARNERY_AGENT_COORD_OFF === "1") return 0;
|
|
822
|
+
const cwd = process.cwd();
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const gh = await import("./git-hook.ts");
|
|
826
|
+
// Pin the coord root to the superproject: a submodule cwd may carry its
|
|
827
|
+
// own config-only .harnery that the generic root walk would land on.
|
|
828
|
+
const root = gh.discoverCoordRoot(cwd) ?? fallbackRoot;
|
|
829
|
+
|
|
830
|
+
if (event === "pre-commit") {
|
|
831
|
+
const { staged, gitlinks } = gh.collectStaged(cwd);
|
|
832
|
+
if (staged.length === 0) return 0;
|
|
833
|
+
const { evaluateCommit } = await import("./rules/commit-conflict.ts");
|
|
834
|
+
const result = evaluateCommit(root, {
|
|
835
|
+
staged_paths: staged,
|
|
836
|
+
staged_gitlinks: gitlinks,
|
|
837
|
+
bypass: process.env.HARNERY_AGENT_COORD_BYPASS === "1",
|
|
838
|
+
});
|
|
839
|
+
// Forensics channel shared with the stdin `verdict` path.
|
|
840
|
+
try {
|
|
841
|
+
const logPath = join(root, ".harnery", "debug", "agent-coord-verdict.ndjson");
|
|
842
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
843
|
+
await appendFile(
|
|
844
|
+
logPath,
|
|
845
|
+
`${JSON.stringify({ ts: new Date().toISOString(), via: "git-hook", staged, verdict: result })}\n`,
|
|
846
|
+
);
|
|
847
|
+
} catch {
|
|
848
|
+
/* diagnostics never break a verdict */
|
|
849
|
+
}
|
|
850
|
+
if (result.rule !== "commit.pass") {
|
|
851
|
+
if (result.message) process.stdout.write(`\n${result.message}\n\n`);
|
|
852
|
+
for (const c of result.conflicts) {
|
|
853
|
+
process.stdout.write(` ${c.staged_path} (held by ${c.short_name})\n`);
|
|
854
|
+
}
|
|
855
|
+
process.stdout.write("\n");
|
|
856
|
+
}
|
|
857
|
+
if (!result.allow) {
|
|
858
|
+
process.stdout.write(
|
|
859
|
+
" Wait for them to finish, split the commit, or set\n" +
|
|
860
|
+
" HARNERY_AGENT_COORD_BYPASS=1 to force this commit through.\n\n",
|
|
861
|
+
);
|
|
862
|
+
return result.exit_code || 1;
|
|
863
|
+
}
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (event === "post-commit" || event === "post-checkout") {
|
|
868
|
+
const paths =
|
|
869
|
+
event === "post-commit"
|
|
870
|
+
? gh.collectCommitted(cwd)
|
|
871
|
+
: gh.collectCheckoutRemoved(cwd, rest[1] ?? "", rest[2] ?? "");
|
|
872
|
+
if (paths.length === 0) return 0;
|
|
873
|
+
const { resolveOwner } = await import("../hooks/resolve/owner.ts");
|
|
874
|
+
const owner = resolveOwner({ payload: null, coordRoot: root })?.instance_id;
|
|
875
|
+
if (!owner) return 0;
|
|
876
|
+
const { groupUnclaim } = await import("./state/heartbeat-writer.ts");
|
|
877
|
+
const reason = event === "post-commit" ? ("commit" as const) : ("checkout" as const);
|
|
878
|
+
for (const path of paths) {
|
|
879
|
+
try {
|
|
880
|
+
for (const hit of groupUnclaim(root, owner, path)) {
|
|
881
|
+
await emitClaimRelease(root, hit.instance_id, hit, path, reason);
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
/* best-effort */
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return 0;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
process.stderr.write(`agent-coord git-hook: unknown event ${event ?? "(none)"}\n`);
|
|
891
|
+
return 0;
|
|
892
|
+
} catch {
|
|
893
|
+
// Fail open: coordination must never brick git.
|
|
894
|
+
return 0;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
844
898
|
async function main(): Promise<number> {
|
|
845
899
|
const [subcommand, ...rest] = process.argv.slice(2);
|
|
846
900
|
const root = findCoordRoot(process.cwd());
|
|
@@ -860,7 +914,6 @@ async function main(): Promise<number> {
|
|
|
860
914
|
|
|
861
915
|
if (
|
|
862
916
|
subcommand === "set-task" ||
|
|
863
|
-
subcommand === "stamp-status-call" ||
|
|
864
917
|
subcommand === "set-turn-summary" ||
|
|
865
918
|
subcommand === "release-claim" ||
|
|
866
919
|
subcommand === "kill-heartbeat" ||
|
|
@@ -918,12 +971,8 @@ async function main(): Promise<number> {
|
|
|
918
971
|
return handleShellMutationClaimLog(root, rest);
|
|
919
972
|
}
|
|
920
973
|
|
|
921
|
-
if (subcommand === "
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (subcommand === "post-checkout") {
|
|
926
|
-
return handlePostCheckout(root, rest);
|
|
974
|
+
if (subcommand === "git-hook") {
|
|
975
|
+
return handleGitHook(root, rest);
|
|
927
976
|
}
|
|
928
977
|
|
|
929
978
|
await logNoop(root, subcommand ?? "(none)", rest);
|