harnery 0.46.0 → 0.47.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 +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +52 -4
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +54 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
package/src/lib/http/client.ts
CHANGED
|
@@ -54,15 +54,6 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
|
|
|
54
54
|
extraCookies?: ExtraCookies;
|
|
55
55
|
/** Response body representation. Default `text`; use `bytes` for lossless file output. */
|
|
56
56
|
responseType?: TResponseType;
|
|
57
|
-
/**
|
|
58
|
-
* With `redirect: "follow"`, follow redirects in this client instead of the
|
|
59
|
-
* runtime's fetch. Every hop then gets its own cookie header, `extraHeaders`
|
|
60
|
-
* call, and Set-Cookie persistence, so a caller that inspects those can stop
|
|
61
|
-
* a chain between hops. Credential headers are dropped on a cross-origin hop.
|
|
62
|
-
* Default: on whenever a `jar` is passed (host-only cookies must follow the
|
|
63
|
-
* jar's exact-host rule on every hop), off without one.
|
|
64
|
-
*/
|
|
65
|
-
clientRedirects?: boolean;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
type FetchBody<TResponseType extends FetchResponseType> = TResponseType extends "bytes"
|
|
@@ -86,23 +77,40 @@ export interface FetchResult<TBody = string> {
|
|
|
86
77
|
* Pass `responseType: "bytes"` when the caller must preserve the body exactly.
|
|
87
78
|
* Streams aren't supported; this is a CLI helper, not a streaming HTTP
|
|
88
79
|
* client. Use Bun's `fetch` directly for streaming workloads.
|
|
80
|
+
*
|
|
81
|
+
* With `redirect: "follow"` (the default) this client follows redirects
|
|
82
|
+
* itself instead of the runtime's fetch, with or without a jar, so one
|
|
83
|
+
* policy governs every hop:
|
|
84
|
+
* - Each hop gets its own jar Cookie header (exact-host for host-only
|
|
85
|
+
* cookies), its own `extraHeaders(url)` call for the hop's URL, and its
|
|
86
|
+
* own Set-Cookie persistence. A callback that throws stops the chain
|
|
87
|
+
* before the next request.
|
|
88
|
+
* - A same-origin hop keeps every caller header.
|
|
89
|
+
* - A cross-origin hop keeps only the caller headers in
|
|
90
|
+
* `CROSS_ORIGIN_REDIRECT_HEADERS`; credentials, bypass tokens, and custom
|
|
91
|
+
* `X-*` headers are dropped for the rest of the chain.
|
|
92
|
+
* - A cross-origin 307 or 308 on a request that is not GET or HEAD throws
|
|
93
|
+
* instead of resending the body to the new origin.
|
|
89
94
|
*/
|
|
90
95
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
91
96
|
const MAX_CLIENT_REDIRECTS = 20;
|
|
92
97
|
|
|
98
|
+
/** Caller headers that survive a redirect to a different origin (lowercase). */
|
|
99
|
+
export const CROSS_ORIGIN_REDIRECT_HEADERS: ReadonlySet<string> = new Set([
|
|
100
|
+
"user-agent",
|
|
101
|
+
"accept",
|
|
102
|
+
"accept-language",
|
|
103
|
+
"range",
|
|
104
|
+
]);
|
|
105
|
+
|
|
93
106
|
export async function fetchWithJar<TResponseType extends FetchResponseType = "text">(
|
|
94
107
|
url: string,
|
|
95
108
|
opts: FetchOptions<TResponseType> = {},
|
|
96
109
|
): Promise<FetchResult<FetchBody<TResponseType>>> {
|
|
97
|
-
|
|
98
|
-
// header from the jar (exact-host for host-only cookies) and its own
|
|
99
|
-
// Set-Cookie persistence; the runtime would otherwise carry or drop the
|
|
100
|
-
// first hop's header on its own rules. Pass `clientRedirects: false` to opt out.
|
|
101
|
-
const clientRedirects = opts.clientRedirects ?? Boolean(opts.jar);
|
|
102
|
-
if (!clientRedirects || (opts.redirect ?? "follow") !== "follow") return fetchOnce(url, opts);
|
|
110
|
+
if ((opts.redirect ?? "follow") !== "follow") return fetchOnce(url, opts);
|
|
103
111
|
|
|
104
112
|
let current = url;
|
|
105
|
-
let method = opts.method ?? "GET";
|
|
113
|
+
let method = (opts.method ?? "GET").toUpperCase();
|
|
106
114
|
let body = opts.body;
|
|
107
115
|
let headers = opts.headers;
|
|
108
116
|
let cookiesSaved = 0;
|
|
@@ -115,17 +123,23 @@ export async function fetchWithJar<TResponseType extends FetchResponseType = "te
|
|
|
115
123
|
throw new Error(`Too many redirects (more than ${MAX_CLIENT_REDIRECTS}) starting at ${url}`);
|
|
116
124
|
}
|
|
117
125
|
const next = new URL(location, current);
|
|
126
|
+
const crossOrigin = next.origin !== new URL(current).origin;
|
|
118
127
|
if (
|
|
119
128
|
result.status === 303 ||
|
|
120
129
|
((result.status === 301 || result.status === 302) && method !== "GET" && method !== "HEAD")
|
|
121
130
|
) {
|
|
122
131
|
method = "GET";
|
|
123
132
|
body = undefined;
|
|
133
|
+
} else if (crossOrigin && method !== "GET" && method !== "HEAD") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Refusing to follow a ${result.status} redirect of a ${method} request from ${new URL(current).origin} to ${next.origin}: ` +
|
|
136
|
+
"it would resend the request body to a different origin. Send the request to the new URL directly if that is intended.",
|
|
137
|
+
);
|
|
124
138
|
}
|
|
125
|
-
if (
|
|
139
|
+
if (crossOrigin && headers) {
|
|
126
140
|
headers = Object.fromEntries(
|
|
127
|
-
Object.entries(headers).filter(
|
|
128
|
-
|
|
141
|
+
Object.entries(headers).filter(([name]) =>
|
|
142
|
+
CROSS_ORIGIN_REDIRECT_HEADERS.has(name.toLowerCase()),
|
|
129
143
|
),
|
|
130
144
|
);
|
|
131
145
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* `.claude/skills/`; Cursor and Codex use `.agents/skills/`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
24
25
|
import {
|
|
25
26
|
existsSync,
|
|
26
27
|
mkdirSync,
|
|
@@ -31,7 +32,11 @@ import {
|
|
|
31
32
|
writeFileSync,
|
|
32
33
|
} from "node:fs";
|
|
33
34
|
import { dirname, join } from "node:path";
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
endOfTurnStatusCommand,
|
|
37
|
+
readComputerConfig,
|
|
38
|
+
stripJsonComments,
|
|
39
|
+
} from "../../core/config.ts";
|
|
35
40
|
import { readHostAddendum } from "./host-addendum.ts";
|
|
36
41
|
import {
|
|
37
42
|
checkOwnedSkill,
|
|
@@ -49,6 +54,7 @@ import {
|
|
|
49
54
|
INSTRUCTIONS_REGION,
|
|
50
55
|
renderInstructionsBlock,
|
|
51
56
|
SKILLS,
|
|
57
|
+
type SkillTemplate,
|
|
52
58
|
} from "./templates.ts";
|
|
53
59
|
|
|
54
60
|
const AGENTS_FILE = "AGENTS.md";
|
|
@@ -134,12 +140,35 @@ export function readSkillsExclude(projectRoot: string): Set<string> {
|
|
|
134
140
|
function blockSkills(projectRoot: string, adapter: string): BlockSkills {
|
|
135
141
|
const supported = adapterSkillsDir(adapter) !== null;
|
|
136
142
|
const exclude = readSkillsExclude(projectRoot);
|
|
137
|
-
|
|
143
|
+
const skills: BlockSkills = {
|
|
138
144
|
decide: supported && !exclude.has("harn-decide"),
|
|
139
145
|
council: supported && !exclude.has("harn-council"),
|
|
140
146
|
end: supported && !exclude.has("harn-end"),
|
|
141
147
|
team: supported && !exclude.has("harn-team"),
|
|
142
148
|
};
|
|
149
|
+
// Only an enabled project's block mentions the opt-in skill, so every other
|
|
150
|
+
// project's block stays byte-identical.
|
|
151
|
+
if (supported && !exclude.has("harn-computer") && readComputerConfig(projectRoot).enabled) {
|
|
152
|
+
skills.computer = true;
|
|
153
|
+
}
|
|
154
|
+
return skills;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** False for an opt-in skill whose config gate is off in this project. */
|
|
158
|
+
function skillOptedIn(projectRoot: string, skill: SkillTemplate): boolean {
|
|
159
|
+
if (skill.optIn === "computer") return readComputerConfig(projectRoot).enabled;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Delete an owned skill file and its now-empty harn-* dir (adapter root stays). */
|
|
164
|
+
function removeOwnedSkillFile(skillPath: string): void {
|
|
165
|
+
rmSync(skillPath);
|
|
166
|
+
const dir = dirname(skillPath);
|
|
167
|
+
try {
|
|
168
|
+
if (readdirSync(dir).length === 0) rmdirSync(dir);
|
|
169
|
+
} catch {
|
|
170
|
+
/* dir not empty or gone → leave it */
|
|
171
|
+
}
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
interface ApplyOpts {
|
|
@@ -170,6 +199,9 @@ export function applyInstructions(projectRoot: string, opts: ApplyOpts): ApplyRe
|
|
|
170
199
|
// aborts the whole run instead of leaving AGENTS.md half-updated.
|
|
171
200
|
const addendum = readHostAddendum(projectRoot);
|
|
172
201
|
|
|
202
|
+
const parentScope = parentScopeWarning(projectRoot);
|
|
203
|
+
if (parentScope) warnings.push(parentScope);
|
|
204
|
+
|
|
173
205
|
// ── AGENTS.md orientation block ─────────────────────────────────────────
|
|
174
206
|
const agentsPath = join(projectRoot, AGENTS_FILE);
|
|
175
207
|
const agentsExisted = existsSync(agentsPath);
|
|
@@ -272,6 +304,16 @@ export function applyInstructions(projectRoot: string, opts: ApplyOpts): ApplyRe
|
|
|
272
304
|
continue;
|
|
273
305
|
}
|
|
274
306
|
const skillPath = join(projectRoot, skillsDir, skill.relPath);
|
|
307
|
+
if (!skillOptedIn(projectRoot, skill)) {
|
|
308
|
+
// Turning the gate off takes back a skill an earlier init wrote.
|
|
309
|
+
if (existsSync(skillPath) && isOwnedFile(readFileSync(skillPath, "utf8"))) {
|
|
310
|
+
if (!opts.dryRun) removeOwnedSkillFile(skillPath);
|
|
311
|
+
actions.push(
|
|
312
|
+
`+ ${verbed("remove", "removed")} skill ${skill.id} (${skill.optIn}.enabled is off)`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
275
317
|
const content = skill.render(opts.binName);
|
|
276
318
|
const before = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : null;
|
|
277
319
|
if (before === content) {
|
|
@@ -292,6 +334,53 @@ export function applyInstructions(projectRoot: string, opts: ApplyOpts): ApplyRe
|
|
|
292
334
|
return { actions, warnings };
|
|
293
335
|
}
|
|
294
336
|
|
|
337
|
+
/** True when `dir` is inside a real Git work tree (an empty `.git` folder does not count). */
|
|
338
|
+
function insideGitWorkTree(dir: string): boolean {
|
|
339
|
+
const r = spawnSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
340
|
+
encoding: "utf8",
|
|
341
|
+
});
|
|
342
|
+
return r.status === 0 && r.stdout.trim() === "true";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const PARENT_SCAN_SKIP = new Set(["node_modules"]);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Warn when instructions are being installed in a folder that is not a Git
|
|
349
|
+
* work tree but holds projects carrying their own Harnery instructions block.
|
|
350
|
+
* Harnesses that read instruction files from parent folders (Claude Code walks
|
|
351
|
+
* up for CLAUDE.md) would load this block on top of each project's own, and
|
|
352
|
+
* with the wrong command name when a project pins a different one. Installing
|
|
353
|
+
* in a non-Git folder stays supported; this only names the overlap.
|
|
354
|
+
*/
|
|
355
|
+
export function parentScopeWarning(projectRoot: string): string | null {
|
|
356
|
+
if (insideGitWorkTree(projectRoot)) return null;
|
|
357
|
+
let entries: string[];
|
|
358
|
+
try {
|
|
359
|
+
entries = readdirSync(projectRoot, { withFileTypes: true })
|
|
360
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !PARENT_SCAN_SKIP.has(e.name))
|
|
361
|
+
.map((e) => e.name)
|
|
362
|
+
.sort();
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const marker = `harnery:begin ${INSTRUCTIONS_REGION}`;
|
|
367
|
+
const children = entries.filter((name) => {
|
|
368
|
+
try {
|
|
369
|
+
return readFileSync(join(projectRoot, name, AGENTS_FILE), "utf8").includes(marker);
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (children.length === 0) return null;
|
|
375
|
+
const listed =
|
|
376
|
+
children.slice(0, 5).join(", ") + (children.length > 5 ? `, +${children.length - 5} more` : "");
|
|
377
|
+
return (
|
|
378
|
+
`${projectRoot} is not a Git work tree and contains ${children.length} project(s) with their own ` +
|
|
379
|
+
`Harnery instructions (${listed}). Harnesses that read parent folders, such as Claude Code, will load ` +
|
|
380
|
+
`this block on top of theirs. Run init inside each project instead, or pass --project-root <project>.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
295
384
|
interface RemoveOpts {
|
|
296
385
|
adapter: string;
|
|
297
386
|
dryRun: boolean;
|
|
@@ -382,16 +471,7 @@ export function removeInstructions(projectRoot: string, opts: RemoveOpts): Apply
|
|
|
382
471
|
warnings.push(`left ${skill.relPath} (hand-edited; no harnery ownership marker)`);
|
|
383
472
|
continue;
|
|
384
473
|
}
|
|
385
|
-
if (!opts.dryRun)
|
|
386
|
-
rmSync(skillPath);
|
|
387
|
-
// Drop the now-empty harn-* dir, leaving the adapter skills root intact.
|
|
388
|
-
const dir = dirname(skillPath);
|
|
389
|
-
try {
|
|
390
|
-
if (readdirSync(dir).length === 0) rmdirSync(dir);
|
|
391
|
-
} catch {
|
|
392
|
-
/* dir not empty or gone → leave it */
|
|
393
|
-
}
|
|
394
|
-
}
|
|
474
|
+
if (!opts.dryRun) removeOwnedSkillFile(skillPath);
|
|
395
475
|
actions.push(`+ ${opts.dryRun ? "would delete" : "deleted"} skill ${skill.id}`);
|
|
396
476
|
}
|
|
397
477
|
}
|
|
@@ -453,6 +533,14 @@ export function checkInstructions(
|
|
|
453
533
|
if (exclude.has(skill.id)) continue;
|
|
454
534
|
const skillPath = join(projectRoot, skillsDir, skill.relPath);
|
|
455
535
|
const c = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : "";
|
|
536
|
+
if (!skillOptedIn(projectRoot, skill)) {
|
|
537
|
+
if (isOwnedFile(c)) {
|
|
538
|
+
issues.push(
|
|
539
|
+
`skill ${skill.id}: present but ${skill.optIn}.enabled is off (re-run init)`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
456
544
|
note(`skill ${skill.id}`, checkOwnedSkill(c, skillBody(skill.render, opts.binName)));
|
|
457
545
|
}
|
|
458
546
|
}
|
|
@@ -53,18 +53,21 @@ function regionName(event: GitHookEvent): string {
|
|
|
53
53
|
*/
|
|
54
54
|
export function renderGitHookBody(event: GitHookEvent): string {
|
|
55
55
|
const blocking = event === "pre-commit";
|
|
56
|
+
const native = process.platform === "win32";
|
|
57
|
+
const command = `${native ? "node " : ""}"$harnery_hook_bin"`;
|
|
58
|
+
const extension = native ? ".mjs" : "";
|
|
56
59
|
const invoke = blocking
|
|
57
|
-
? `
|
|
60
|
+
? ` ${command} git-hook ${event} "$@"\n` +
|
|
58
61
|
` harnery_hook_rc=$?\n` +
|
|
59
62
|
` [ "$harnery_hook_rc" -ne 0 ] && exit "$harnery_hook_rc"\n`
|
|
60
|
-
: `
|
|
63
|
+
: ` ${command} git-hook ${event} "$@" >/dev/null 2>&1 || true\n`;
|
|
61
64
|
return (
|
|
62
65
|
`# Coordination for this hook lives in harnery (agent-coord git-hook ${event});\n` +
|
|
63
66
|
`# upgrading harnery upgrades the behavior. Managed region: do not edit by hand.\n` +
|
|
64
67
|
`harnery_hook_root=$(git rev-parse --show-superproject-working-tree 2>/dev/null)\n` +
|
|
65
68
|
`[ -n "$harnery_hook_root" ] || harnery_hook_root=$(git rev-parse --show-toplevel 2>/dev/null)\n` +
|
|
66
|
-
`for harnery_hook_bin in "$harnery_hook_root/harnery/bin/agent-coord" "$harnery_hook_root/node_modules/harnery/bin/agent-coord"; do\n` +
|
|
67
|
-
` [
|
|
69
|
+
`for harnery_hook_bin in "$harnery_hook_root/harnery/bin/agent-coord${extension}" "$harnery_hook_root/node_modules/harnery/bin/agent-coord${extension}"; do\n` +
|
|
70
|
+
` [ -${native ? "f" : "x"} "$harnery_hook_bin" ] || continue\n` +
|
|
68
71
|
invoke +
|
|
69
72
|
` break\n` +
|
|
70
73
|
`done`
|
|
@@ -79,6 +82,7 @@ export function renderGitHookBody(event: GitHookEvent): string {
|
|
|
79
82
|
*/
|
|
80
83
|
export function resolveHooksDir(projectRoot: string): string | null {
|
|
81
84
|
const r = spawnSync("git", ["rev-parse", "--git-path", "hooks"], {
|
|
85
|
+
windowsHide: true,
|
|
82
86
|
cwd: projectRoot,
|
|
83
87
|
encoding: "utf8",
|
|
84
88
|
timeout: 5000,
|
|
@@ -36,6 +36,11 @@ export interface BlockSkills {
|
|
|
36
36
|
end: boolean;
|
|
37
37
|
/** the `harn-team` skill file is present */
|
|
38
38
|
team: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* the opt-in `harn-computer` skill file is present (only when the project
|
|
41
|
+
* sets `computer.enabled`); absent from the default block entirely
|
|
42
|
+
*/
|
|
43
|
+
computer?: boolean;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -78,6 +83,11 @@ export function renderInstructionsBlock(
|
|
|
78
83
|
const teamPointer = skills.team
|
|
79
84
|
? "The `harn-team` skill owns the tier choice, the governor artifacts, and the drive loop."
|
|
80
85
|
: `See \`${b} governor --help\` for the tier choice and the drive loop.`;
|
|
86
|
+
// Opt-in surface: the default block stays byte-identical unless the project
|
|
87
|
+
// enabled computer use and the skill is actually installed.
|
|
88
|
+
const computerPointer = skills.computer
|
|
89
|
+
? `\n\n**Desktop control.** This project enables \`${b} computer\` for driving native desktop apps and real browsers as a last resort. The \`harn-computer\` skill has the loop, and the desktop is one shared resource held by one agent at a time.`
|
|
90
|
+
: "";
|
|
81
91
|
// Render the journal categories from the canonical enum so this prose can
|
|
82
92
|
// never drift from what `journal add` actually accepts (the "note, plan…" list
|
|
83
93
|
// silently lagged the tool by two categories before this).
|
|
@@ -165,7 +175,7 @@ can't resolve from the repo, file it instead. \`${b} decision file "<question>"\
|
|
|
165
175
|
records it and lets you proceed on a stated default; \`${b} decision search "<terms>"\`
|
|
166
176
|
surfaces prior decisions, so check for precedent before re-deciding. ${decidePointer}
|
|
167
177
|
|
|
168
|
-
**Councils.** For a hard or contested decision, convene a council of agents. \`${b} council create "<objective>"\` runs structured rounds toward a decision. ${councilPointer}`;
|
|
178
|
+
**Councils.** For a hard or contested decision, convene a council of agents. \`${b} council create "<objective>"\` runs structured rounds toward a decision. ${councilPointer}${computerPointer}`;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
// ── Skills ──────────────────────────────────────────────────────────────────
|
|
@@ -176,6 +186,12 @@ export interface SkillTemplate {
|
|
|
176
186
|
/** path under the adapter skill dir, e.g. `harn-decide/SKILL.md` */
|
|
177
187
|
relPath: string;
|
|
178
188
|
render: (binName: string) => string;
|
|
189
|
+
/**
|
|
190
|
+
* Config gate for an opt-in skill. `"computer"` renders only when the project
|
|
191
|
+
* sets `computer.enabled: true`; ungated skills always render (minus
|
|
192
|
+
* `skills.exclude`).
|
|
193
|
+
*/
|
|
194
|
+
optIn?: "computer";
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
function decideBody(b: string): string {
|
|
@@ -518,6 +534,71 @@ single next action. If you declined to build a team because one already existed,
|
|
|
518
534
|
say that first; it is the most useful sentence in the reply.`;
|
|
519
535
|
}
|
|
520
536
|
|
|
537
|
+
function computerBody(b: string): string {
|
|
538
|
+
return `\`${b} computer\` drives this machine's desktop through a pinned desktop driver.
|
|
539
|
+
It is the last resort: use a vendor API, a CLI, or the browser tools first.
|
|
540
|
+
Reach for it only for a native desktop app, or a page that needs a real
|
|
541
|
+
browser you cannot drive any other way.
|
|
542
|
+
|
|
543
|
+
## Setup check
|
|
544
|
+
|
|
545
|
+
\`\`\`bash
|
|
546
|
+
${b} computer status
|
|
547
|
+
\`\`\`
|
|
548
|
+
|
|
549
|
+
It shows the host, the pinned driver and whether it is installed, the daemon,
|
|
550
|
+
and who holds the desktop. When the driver is missing, the project must enable
|
|
551
|
+
computer use and run \`${b} computer install --yes\`. \`${b} computer doctor\`
|
|
552
|
+
reports the driver's own health checks.
|
|
553
|
+
|
|
554
|
+
## Core loop
|
|
555
|
+
|
|
556
|
+
Every step is one call: \`${b} computer call <tool> '<json>'\`. Look up a tool's
|
|
557
|
+
arguments with \`${b} computer describe <tool>\` rather than guessing; \`${b} computer tools\`
|
|
558
|
+
lists them.
|
|
559
|
+
|
|
560
|
+
1. **Launch** the app in the background with \`launch_app\`.
|
|
561
|
+
2. **Read state** with \`get_window_state\` and \`include_screenshot: false\`. When
|
|
562
|
+
you need to see the window, pass \`screenshot_out_file\` with a file path
|
|
563
|
+
and read that file. Inline screenshots are redacted from the output unless
|
|
564
|
+
you pass \`--keep-screenshot\`, because they flood your context.
|
|
565
|
+
3. **Act by element token** \`"<snapshot_id>:<row>"\` from the latest state:
|
|
566
|
+
click, \`set_value\`, or invoke, batched through \`run_steps\` when several
|
|
567
|
+
actions belong together.
|
|
568
|
+
4. **Read back** the state after acting. Confirm the change landed before you
|
|
569
|
+
retry anything; never repeat an action blind.
|
|
570
|
+
|
|
571
|
+
## Rules
|
|
572
|
+
|
|
573
|
+
- Prefer accessibility routes (\`set_value\`, invoke) over \`type_text\` and
|
|
574
|
+
coordinate clicks.
|
|
575
|
+
- Never pass \`delivery_mode: "foreground"\` unless the driver returned
|
|
576
|
+
\`background_unavailable\` or \`delivery_failed\` for that action.
|
|
577
|
+
- For a browser, call \`browser_prepare\` with \`allow_launch: true\` and
|
|
578
|
+
\`profile: {"mode": "isolated_new"}\`. Never attach to the user's real
|
|
579
|
+
profile unless the operator asks for it.
|
|
580
|
+
- Never type a password, username, or one-time code into a sign-in page. Stop
|
|
581
|
+
and ask the operator to sign in.
|
|
582
|
+
- Only \`kill_app\` processes you launched.
|
|
583
|
+
|
|
584
|
+
## One desktop, one holder
|
|
585
|
+
|
|
586
|
+
Every agent on this machine shares one desktop. \`call\` takes a lease for you
|
|
587
|
+
and refreshes it on every call; it lapses after a short idle period.
|
|
588
|
+
|
|
589
|
+
- A refusal naming another holder means **wait**, then try once more later. Do
|
|
590
|
+
not retry in a loop. \`--wait <seconds>\` waits longer on one call.
|
|
591
|
+
- Check the holder with \`${b} computer lease status\`.
|
|
592
|
+
- Release the desktop as soon as you finish:
|
|
593
|
+
|
|
594
|
+
\`\`\`bash
|
|
595
|
+
${b} computer lease release
|
|
596
|
+
\`\`\`
|
|
597
|
+
|
|
598
|
+
Report what you did on the desktop, which apps you launched and closed, and
|
|
599
|
+
anything you left open.`;
|
|
600
|
+
}
|
|
601
|
+
|
|
521
602
|
export const SKILLS: SkillTemplate[] = [
|
|
522
603
|
{
|
|
523
604
|
id: "harn-decide",
|
|
@@ -567,4 +648,17 @@ export const SKILLS: SkillTemplate[] = [
|
|
|
567
648
|
body: teamBody(binName),
|
|
568
649
|
}),
|
|
569
650
|
},
|
|
651
|
+
{
|
|
652
|
+
id: "harn-computer",
|
|
653
|
+
relPath: "harn-computer/SKILL.md",
|
|
654
|
+
optIn: "computer",
|
|
655
|
+
render: (binName) =>
|
|
656
|
+
buildOwnedSkill({
|
|
657
|
+
name: "harn-computer",
|
|
658
|
+
description:
|
|
659
|
+
"Drive a native desktop app or a real browser through the project's pinned desktop driver, as a last resort after APIs, CLIs, and browser tools. Covers the setup check, the launch / read state / act by element token / read back loop, safety rules, and the shared desktop lease.",
|
|
660
|
+
binName,
|
|
661
|
+
body: computerBody(binName),
|
|
662
|
+
}),
|
|
663
|
+
},
|
|
570
664
|
];
|