harnery 0.46.1 → 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.
Files changed (176) hide show
  1. package/README.md +24 -0
  2. package/bin/README.md +10 -0
  3. package/bin/agent-coord.mjs +3 -0
  4. package/bin/agent-hook.mjs +3 -0
  5. package/bin/harn.mjs +3 -0
  6. package/bin/run.mjs +43 -0
  7. package/dist/commander.d.ts +18 -6
  8. package/dist/commander.d.ts.map +1 -1
  9. package/dist/commander.js +74 -2
  10. package/dist/commands/agents.d.ts +25 -0
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/agents.js +70 -27
  13. package/dist/commands/autoupdate.d.ts +12 -0
  14. package/dist/commands/autoupdate.d.ts.map +1 -0
  15. package/dist/commands/autoupdate.js +190 -0
  16. package/dist/commands/computer.d.ts +45 -0
  17. package/dist/commands/computer.d.ts.map +1 -0
  18. package/dist/commands/computer.js +509 -0
  19. package/dist/commands/docs.js +3 -1
  20. package/dist/commands/fetch.js +11 -1
  21. package/dist/commands/grep.d.ts +72 -0
  22. package/dist/commands/grep.d.ts.map +1 -1
  23. package/dist/commands/grep.js +200 -47
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +15 -9
  26. package/dist/commands/rm.js +2 -2
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +2 -0
  29. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  30. package/dist/core/agents/canonical-emit.js +2 -3
  31. package/dist/core/agents/coord-bin.d.ts +3 -0
  32. package/dist/core/agents/coord-bin.d.ts.map +1 -1
  33. package/dist/core/agents/coord-bin.js +6 -0
  34. package/dist/core/agents/coord-client.d.ts.map +1 -1
  35. package/dist/core/agents/coord-client.js +6 -2
  36. package/dist/core/agents/render/prompt-context.d.ts +55 -7
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +122 -48
  39. package/dist/core/agents/state/proc-start.d.ts +3 -1
  40. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  41. package/dist/core/agents/state/proc-start.js +26 -4
  42. package/dist/core/autoupdate/engine.d.ts +97 -0
  43. package/dist/core/autoupdate/engine.d.ts.map +1 -0
  44. package/dist/core/autoupdate/engine.js +548 -0
  45. package/dist/core/autoupdate/fingerprint.d.ts +16 -0
  46. package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
  47. package/dist/core/autoupdate/fingerprint.js +73 -0
  48. package/dist/core/autoupdate/git.d.ts +30 -0
  49. package/dist/core/autoupdate/git.d.ts.map +1 -0
  50. package/dist/core/autoupdate/git.js +82 -0
  51. package/dist/core/autoupdate/index.d.ts +6 -0
  52. package/dist/core/autoupdate/index.d.ts.map +1 -0
  53. package/dist/core/autoupdate/index.js +4 -0
  54. package/dist/core/autoupdate/state.d.ts +59 -0
  55. package/dist/core/autoupdate/state.d.ts.map +1 -0
  56. package/dist/core/autoupdate/state.js +214 -0
  57. package/dist/core/autoupdate/types.d.ts +113 -0
  58. package/dist/core/autoupdate/types.d.ts.map +1 -0
  59. package/dist/core/autoupdate/types.js +35 -0
  60. package/dist/core/config.d.ts +53 -1
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +114 -1
  63. package/dist/core/events/v3/bootstrap.js +1 -0
  64. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  65. package/dist/core/events/v3/runtime-build.js +1 -0
  66. package/dist/core/hooks/adapter/wiring.d.ts +12 -2
  67. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  68. package/dist/core/hooks/adapter/wiring.js +38 -8
  69. package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
  70. package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
  71. package/dist/core/hooks/autoupdate-cue.js +53 -0
  72. package/dist/core/hooks/cli.js +28 -3
  73. package/dist/core/hooks/effects/index.js +4 -4
  74. package/dist/core/hooks/resolve/coord-root.js +1 -0
  75. package/dist/core/semantic/contract.d.ts +3 -3
  76. package/dist/core/semantic/service.d.ts.map +1 -1
  77. package/dist/core/semantic/service.js +4 -2
  78. package/dist/core/storage/modes.d.ts +1 -1
  79. package/dist/core/storage/modes.d.ts.map +1 -1
  80. package/dist/core/storage/modes.js +5 -1
  81. package/dist/core/workflow/durable-record.d.ts +2 -0
  82. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  83. package/dist/core/workflow/durable-record.js +13 -1
  84. package/dist/core/workflow/engine.js +4 -0
  85. package/dist/core/workflow/spawn-claude.d.ts +7 -0
  86. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  87. package/dist/core/workflow/spawn-claude.js +16 -4
  88. package/dist/core/workflow/specialists.d.ts.map +1 -1
  89. package/dist/core/workflow/specialists.js +1 -0
  90. package/dist/core/workflow/types.d.ts +10 -0
  91. package/dist/core/workflow/types.d.ts.map +1 -1
  92. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  93. package/dist/core/workflow/workspaces/leases.js +1 -9
  94. package/dist/lib/computer/cua.d.ts +50 -0
  95. package/dist/lib/computer/cua.d.ts.map +1 -0
  96. package/dist/lib/computer/cua.js +474 -0
  97. package/dist/lib/computer/host.d.ts +19 -0
  98. package/dist/lib/computer/host.d.ts.map +1 -0
  99. package/dist/lib/computer/host.js +72 -0
  100. package/dist/lib/computer/lease.d.ts +13 -0
  101. package/dist/lib/computer/lease.d.ts.map +1 -0
  102. package/dist/lib/computer/lease.js +269 -0
  103. package/dist/lib/computer/types.d.ts +163 -0
  104. package/dist/lib/computer/types.d.ts.map +1 -0
  105. package/dist/lib/computer/types.js +10 -0
  106. package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
  107. package/dist/lib/docs-metadata-v2.js +6 -5
  108. package/dist/lib/exec.d.ts.map +1 -1
  109. package/dist/lib/exec.js +1 -0
  110. package/dist/lib/guarded-remove.d.ts.map +1 -1
  111. package/dist/lib/guarded-remove.js +5 -4
  112. package/dist/lib/headless/index.d.ts +33 -0
  113. package/dist/lib/headless/index.d.ts.map +1 -1
  114. package/dist/lib/headless/index.js +38 -2
  115. package/dist/lib/http/client.d.ts +2 -9
  116. package/dist/lib/http/client.d.ts.map +1 -1
  117. package/dist/lib/http/client.js +30 -9
  118. package/dist/lib/instructions/apply.d.ts +9 -0
  119. package/dist/lib/instructions/apply.d.ts.map +1 -1
  120. package/dist/lib/instructions/apply.js +92 -14
  121. package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
  122. package/dist/lib/instructions/git-hooks.js +8 -4
  123. package/dist/lib/instructions/templates.d.ts +11 -0
  124. package/dist/lib/instructions/templates.d.ts.map +1 -1
  125. package/dist/lib/instructions/templates.js +81 -1
  126. package/package.json +5 -5
  127. package/schemas/config.schema.json +89 -1
  128. package/src/commander.ts +98 -8
  129. package/src/commands/agents.ts +100 -32
  130. package/src/commands/autoupdate.ts +250 -0
  131. package/src/commands/computer.ts +682 -0
  132. package/src/commands/docs.ts +3 -1
  133. package/src/commands/fetch.ts +11 -1
  134. package/src/commands/grep.ts +274 -59
  135. package/src/commands/init.ts +17 -7
  136. package/src/commands/rm.ts +2 -2
  137. package/src/commands/web.ts +2 -0
  138. package/src/core/agents/canonical-emit.ts +2 -3
  139. package/src/core/agents/coord-bin.ts +30 -0
  140. package/src/core/agents/coord-client.ts +6 -2
  141. package/src/core/agents/render/prompt-context.ts +152 -53
  142. package/src/core/agents/state/proc-start.ts +29 -5
  143. package/src/core/autoupdate/engine.ts +691 -0
  144. package/src/core/autoupdate/fingerprint.ts +89 -0
  145. package/src/core/autoupdate/git.ts +120 -0
  146. package/src/core/autoupdate/index.ts +29 -0
  147. package/src/core/autoupdate/state.ts +269 -0
  148. package/src/core/autoupdate/types.ts +135 -0
  149. package/src/core/config.ts +142 -1
  150. package/src/core/events/v3/bootstrap.ts +1 -0
  151. package/src/core/events/v3/runtime-build.ts +1 -0
  152. package/src/core/hooks/adapter/wiring.ts +51 -6
  153. package/src/core/hooks/autoupdate-cue.ts +72 -0
  154. package/src/core/hooks/cli.ts +27 -1
  155. package/src/core/hooks/effects/index.ts +4 -4
  156. package/src/core/hooks/resolve/coord-root.ts +1 -0
  157. package/src/core/semantic/service.ts +6 -2
  158. package/src/core/storage/modes.ts +7 -1
  159. package/src/core/workflow/durable-record.ts +15 -1
  160. package/src/core/workflow/engine.ts +3 -0
  161. package/src/core/workflow/spawn-claude.ts +16 -4
  162. package/src/core/workflow/specialists.ts +1 -0
  163. package/src/core/workflow/types.ts +10 -0
  164. package/src/core/workflow/workspaces/leases.ts +1 -9
  165. package/src/lib/computer/cua.ts +572 -0
  166. package/src/lib/computer/host.ts +87 -0
  167. package/src/lib/computer/lease.ts +299 -0
  168. package/src/lib/computer/types.ts +177 -0
  169. package/src/lib/docs-metadata-v2.ts +24 -5
  170. package/src/lib/exec.ts +1 -0
  171. package/src/lib/guarded-remove.ts +5 -3
  172. package/src/lib/headless/index.ts +66 -2
  173. package/src/lib/http/client.ts +33 -19
  174. package/src/lib/instructions/apply.ts +100 -12
  175. package/src/lib/instructions/git-hooks.ts +8 -4
  176. package/src/lib/instructions/templates.ts +95 -1
@@ -26,6 +26,11 @@
26
26
  * PATH" is not "working", and under parallel load the first backend can
27
27
  * flake on a fraction of calls while the second runs clean.
28
28
  *
29
+ * - A claude-code call loads NO MCP servers and no claude.ai connectors by
30
+ * default (`claudeMcpLaunch`). Connectors attach about 130 tool schemas to
31
+ * every turn of a one-shot call that never uses them; a caller that needs
32
+ * one opts in per request with `mcp: "inherit"` or an explicit server map.
33
+ *
29
34
  * Env knobs (read through coordEnv, so the HARNERY_ prefix stays in one
30
35
  * place): HARNERY_HEADLESS_BACKEND forces one backend and disables the
31
36
  * fallback walk; HARNERY_HEADLESS_MODEL overrides the model passed to
@@ -38,6 +43,33 @@ import { delimiter, isAbsolute, join } from "node:path";
38
43
  import { promisify } from "node:util";
39
44
  import { coordEnv } from "../env.js";
40
45
  const execFileAsync = promisify(execFile);
46
+ /** The two guards that keep a `claude` child free of MCP servers: strict MCP
47
+ * config with an empty server map, plus the env switch that stops claude.ai
48
+ * connectors attaching mid-session (the flag alone does not cover them). */
49
+ export const CLAUDE_NO_MCP_ARGS = [
50
+ "--strict-mcp-config",
51
+ "--mcp-config",
52
+ '{"mcpServers":{}}',
53
+ ];
54
+ export const CLAUDE_NO_CONNECTORS_ENV = {
55
+ ENABLE_CLAUDEAI_MCP_SERVERS: "false",
56
+ };
57
+ /** Resolve an MCP mode to the argv and env additions for a `claude` child.
58
+ * Throws on a malformed server map rather than launching with a guess. */
59
+ export function claudeMcpLaunch(mode = "none") {
60
+ if (mode === "inherit")
61
+ return { args: [], env: {} };
62
+ if (mode === "none")
63
+ return { args: [...CLAUDE_NO_MCP_ARGS], env: { ...CLAUDE_NO_CONNECTORS_ENV } };
64
+ const servers = mode?.mcpServers;
65
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
66
+ throw new Error('mcp must be "none", "inherit", or { mcpServers: { <name>: <server config> } }');
67
+ }
68
+ return {
69
+ args: ["--strict-mcp-config", "--mcp-config", JSON.stringify({ mcpServers: servers })],
70
+ env: { ...CLAUDE_NO_CONNECTORS_ENV },
71
+ };
72
+ }
41
73
  const MAX_BUFFER = 32 * 1024 * 1024;
42
74
  const DEFAULT_TIMEOUT_MS = 240_000;
43
75
  const DEFAULT_MAX_TURNS = 4;
@@ -90,15 +122,17 @@ function stageCall(req) {
90
122
  model: coordEnv("HEADLESS_MODEL") ?? req.model,
91
123
  timeoutMs: req.timeoutMs ?? DEFAULT_TIMEOUT_MS,
92
124
  maxTurns: req.maxTurns ?? DEFAULT_MAX_TURNS,
125
+ ...(req.mcp ? { mcp: req.mcp } : {}),
93
126
  };
94
127
  }
95
128
  /** Run a harness CLI from the neutral dir with stdin CLOSED (see module
96
129
  * comment: an open stdin pipe deadlocks codex). */
97
- function execHarness(bin, args, call) {
130
+ function execHarness(bin, args, call, extraEnv = {}) {
98
131
  const pending = execFileAsync(bin, args, {
99
132
  cwd: call.dir,
100
133
  timeout: call.timeoutMs,
101
134
  maxBuffer: MAX_BUFFER,
135
+ ...(Object.keys(extraEnv).length ? { env: { ...process.env, ...extraEnv } } : {}),
102
136
  });
103
137
  pending.child.stdin?.end();
104
138
  return pending;
@@ -114,7 +148,9 @@ async function runClaudeCode(bin, call) {
114
148
  args.push("--allowedTools", "Read");
115
149
  if (call.model)
116
150
  args.push("--model", call.model);
117
- const { stdout } = await execHarness(bin, args, call);
151
+ const mcp = claudeMcpLaunch(call.mcp);
152
+ args.push(...mcp.args);
153
+ const { stdout } = await execHarness(bin, args, call, mcp.env);
118
154
  const envelope = JSON.parse(stdout);
119
155
  if (envelope.is_error) {
120
156
  throw new Error(typeof envelope.result === "string" ? envelope.result : "claude-code: headless error");
@@ -44,15 +44,6 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
44
44
  extraCookies?: ExtraCookies;
45
45
  /** Response body representation. Default `text`; use `bytes` for lossless file output. */
46
46
  responseType?: TResponseType;
47
- /**
48
- * With `redirect: "follow"`, follow redirects in this client instead of the
49
- * runtime's fetch. Every hop then gets its own cookie header, `extraHeaders`
50
- * call, and Set-Cookie persistence, so a caller that inspects those can stop
51
- * a chain between hops. Credential headers are dropped on a cross-origin hop.
52
- * Default: on whenever a `jar` is passed (host-only cookies must follow the
53
- * jar's exact-host rule on every hop), off without one.
54
- */
55
- clientRedirects?: boolean;
56
47
  }
57
48
  type FetchBody<TResponseType extends FetchResponseType> = TResponseType extends "bytes" ? Uint8Array : string;
58
49
  export interface FetchResult<TBody = string> {
@@ -64,6 +55,8 @@ export interface FetchResult<TBody = string> {
64
55
  /** Number of cookies persisted back into the jar (0 if no jar passed). */
65
56
  cookiesSaved: number;
66
57
  }
58
+ /** Caller headers that survive a redirect to a different origin (lowercase). */
59
+ export declare const CROSS_ORIGIN_REDIRECT_HEADERS: ReadonlySet<string>;
67
60
  export declare function fetchWithJar<TResponseType extends FetchResponseType = "text">(url: string, opts?: FetchOptions<TResponseType>): Promise<FetchResult<FetchBody<TResponseType>>>;
68
61
  export {};
69
62
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAaD,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAsChD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AA2BD,gFAAgF;AAChF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAK5D,CAAC;AAEH,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAuChD"}
@@ -6,19 +6,35 @@ import { applyExtraCookies, domainMatches, violatesCookiePrefix, } from "../cook
6
6
  * Pass `responseType: "bytes"` when the caller must preserve the body exactly.
7
7
  * Streams aren't supported; this is a CLI helper, not a streaming HTTP
8
8
  * client. Use Bun's `fetch` directly for streaming workloads.
9
+ *
10
+ * With `redirect: "follow"` (the default) this client follows redirects
11
+ * itself instead of the runtime's fetch, with or without a jar, so one
12
+ * policy governs every hop:
13
+ * - Each hop gets its own jar Cookie header (exact-host for host-only
14
+ * cookies), its own `extraHeaders(url)` call for the hop's URL, and its
15
+ * own Set-Cookie persistence. A callback that throws stops the chain
16
+ * before the next request.
17
+ * - A same-origin hop keeps every caller header.
18
+ * - A cross-origin hop keeps only the caller headers in
19
+ * `CROSS_ORIGIN_REDIRECT_HEADERS`; credentials, bypass tokens, and custom
20
+ * `X-*` headers are dropped for the rest of the chain.
21
+ * - A cross-origin 307 or 308 on a request that is not GET or HEAD throws
22
+ * instead of resending the body to the new origin.
9
23
  */
10
24
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
11
25
  const MAX_CLIENT_REDIRECTS = 20;
26
+ /** Caller headers that survive a redirect to a different origin (lowercase). */
27
+ export const CROSS_ORIGIN_REDIRECT_HEADERS = new Set([
28
+ "user-agent",
29
+ "accept",
30
+ "accept-language",
31
+ "range",
32
+ ]);
12
33
  export async function fetchWithJar(url, opts = {}) {
13
- // With a jar, redirects are followed here so every hop gets its own Cookie
14
- // header from the jar (exact-host for host-only cookies) and its own
15
- // Set-Cookie persistence; the runtime would otherwise carry or drop the
16
- // first hop's header on its own rules. Pass `clientRedirects: false` to opt out.
17
- const clientRedirects = opts.clientRedirects ?? Boolean(opts.jar);
18
- if (!clientRedirects || (opts.redirect ?? "follow") !== "follow")
34
+ if ((opts.redirect ?? "follow") !== "follow")
19
35
  return fetchOnce(url, opts);
20
36
  let current = url;
21
- let method = opts.method ?? "GET";
37
+ let method = (opts.method ?? "GET").toUpperCase();
22
38
  let body = opts.body;
23
39
  let headers = opts.headers;
24
40
  let cookiesSaved = 0;
@@ -32,13 +48,18 @@ export async function fetchWithJar(url, opts = {}) {
32
48
  throw new Error(`Too many redirects (more than ${MAX_CLIENT_REDIRECTS}) starting at ${url}`);
33
49
  }
34
50
  const next = new URL(location, current);
51
+ const crossOrigin = next.origin !== new URL(current).origin;
35
52
  if (result.status === 303 ||
36
53
  ((result.status === 301 || result.status === 302) && method !== "GET" && method !== "HEAD")) {
37
54
  method = "GET";
38
55
  body = undefined;
39
56
  }
40
- if (next.origin !== new URL(current).origin && headers) {
41
- headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !["authorization", "cookie"].includes(name.toLowerCase())));
57
+ else if (crossOrigin && method !== "GET" && method !== "HEAD") {
58
+ throw new Error(`Refusing to follow a ${result.status} redirect of a ${method} request from ${new URL(current).origin} to ${next.origin}: ` +
59
+ "it would resend the request body to a different origin. Send the request to the new URL directly if that is intended.");
60
+ }
61
+ if (crossOrigin && headers) {
62
+ headers = Object.fromEntries(Object.entries(headers).filter(([name]) => CROSS_ORIGIN_REDIRECT_HEADERS.has(name.toLowerCase())));
42
63
  }
43
64
  current = next.href;
44
65
  }
@@ -37,6 +37,15 @@ export interface ApplyResult {
37
37
  * writes nothing. `dryRun` reports without touching the fs.
38
38
  */
39
39
  export declare function applyInstructions(projectRoot: string, opts: ApplyOpts): ApplyResult;
40
+ /**
41
+ * Warn when instructions are being installed in a folder that is not a Git
42
+ * work tree but holds projects carrying their own Harnery instructions block.
43
+ * Harnesses that read instruction files from parent folders (Claude Code walks
44
+ * up for CLAUDE.md) would load this block on top of each project's own, and
45
+ * with the wrong command name when a project pins a different one. Installing
46
+ * in a non-Git folder stays supported; this only names the overlap.
47
+ */
48
+ export declare function parentScopeWarning(projectRoot: string): string | null;
40
49
  interface RemoveOpts {
41
50
  adapter: string;
42
51
  dryRun: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA2FH,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAYlE;AAmBD,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,CAoInF;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,WAAW,CA8FrF;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzC,WAAW,CA2Db"}
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAiGH,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAYlE;AA0CD,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,CAiJnF;AAYD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2BrE;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,WAAW,CAqFrF;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzC,WAAW,CAmEb"}
@@ -20,9 +20,10 @@
20
20
  * `skills.exclude` in `.harnery/config.jsonc`. Claude Code uses
21
21
  * `.claude/skills/`; Cursor and Codex use `.agents/skills/`.
22
22
  */
23
+ import { spawnSync } from "node:child_process";
23
24
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync, } from "node:fs";
24
25
  import { dirname, join } from "node:path";
25
- import { endOfTurnStatusCommand, stripJsonComments } from "../../core/config.js";
26
+ import { endOfTurnStatusCommand, readComputerConfig, stripJsonComments, } from "../../core/config.js";
26
27
  import { readHostAddendum } from "./host-addendum.js";
27
28
  import { checkOwnedSkill, checkRegion, isOwnedFile, removeRegion, shortHash, spliceRegion, } from "./splice.js";
28
29
  import { HOST_ADDENDUM_REGION, IMPORT_REGION, INSTRUCTIONS_REGION, renderInstructionsBlock, SKILLS, } from "./templates.js";
@@ -101,12 +102,36 @@ export function readSkillsExclude(projectRoot) {
101
102
  function blockSkills(projectRoot, adapter) {
102
103
  const supported = adapterSkillsDir(adapter) !== null;
103
104
  const exclude = readSkillsExclude(projectRoot);
104
- return {
105
+ const skills = {
105
106
  decide: supported && !exclude.has("harn-decide"),
106
107
  council: supported && !exclude.has("harn-council"),
107
108
  end: supported && !exclude.has("harn-end"),
108
109
  team: supported && !exclude.has("harn-team"),
109
110
  };
111
+ // Only an enabled project's block mentions the opt-in skill, so every other
112
+ // project's block stays byte-identical.
113
+ if (supported && !exclude.has("harn-computer") && readComputerConfig(projectRoot).enabled) {
114
+ skills.computer = true;
115
+ }
116
+ return skills;
117
+ }
118
+ /** False for an opt-in skill whose config gate is off in this project. */
119
+ function skillOptedIn(projectRoot, skill) {
120
+ if (skill.optIn === "computer")
121
+ return readComputerConfig(projectRoot).enabled;
122
+ return true;
123
+ }
124
+ /** Delete an owned skill file and its now-empty harn-* dir (adapter root stays). */
125
+ function removeOwnedSkillFile(skillPath) {
126
+ rmSync(skillPath);
127
+ const dir = dirname(skillPath);
128
+ try {
129
+ if (readdirSync(dir).length === 0)
130
+ rmdirSync(dir);
131
+ }
132
+ catch {
133
+ /* dir not empty or gone → leave it */
134
+ }
110
135
  }
111
136
  /**
112
137
  * Inject / refresh the instructions block, the CLAUDE.md import shim (claude-code),
@@ -123,6 +148,9 @@ export function applyInstructions(projectRoot, opts) {
123
148
  // Resolve the host addendum before touching a single file, so a bad path
124
149
  // aborts the whole run instead of leaving AGENTS.md half-updated.
125
150
  const addendum = readHostAddendum(projectRoot);
151
+ const parentScope = parentScopeWarning(projectRoot);
152
+ if (parentScope)
153
+ warnings.push(parentScope);
126
154
  // ── AGENTS.md orientation block ─────────────────────────────────────────
127
155
  const agentsPath = join(projectRoot, AGENTS_FILE);
128
156
  const agentsExisted = existsSync(agentsPath);
@@ -221,6 +249,15 @@ export function applyInstructions(projectRoot, opts) {
221
249
  continue;
222
250
  }
223
251
  const skillPath = join(projectRoot, skillsDir, skill.relPath);
252
+ if (!skillOptedIn(projectRoot, skill)) {
253
+ // Turning the gate off takes back a skill an earlier init wrote.
254
+ if (existsSync(skillPath) && isOwnedFile(readFileSync(skillPath, "utf8"))) {
255
+ if (!opts.dryRun)
256
+ removeOwnedSkillFile(skillPath);
257
+ actions.push(`+ ${verbed("remove", "removed")} skill ${skill.id} (${skill.optIn}.enabled is off)`);
258
+ }
259
+ continue;
260
+ }
224
261
  const content = skill.render(opts.binName);
225
262
  const before = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : null;
226
263
  if (before === content) {
@@ -237,6 +274,51 @@ export function applyInstructions(projectRoot, opts) {
237
274
  }
238
275
  return { actions, warnings };
239
276
  }
277
+ /** True when `dir` is inside a real Git work tree (an empty `.git` folder does not count). */
278
+ function insideGitWorkTree(dir) {
279
+ const r = spawnSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
280
+ encoding: "utf8",
281
+ });
282
+ return r.status === 0 && r.stdout.trim() === "true";
283
+ }
284
+ const PARENT_SCAN_SKIP = new Set(["node_modules"]);
285
+ /**
286
+ * Warn when instructions are being installed in a folder that is not a Git
287
+ * work tree but holds projects carrying their own Harnery instructions block.
288
+ * Harnesses that read instruction files from parent folders (Claude Code walks
289
+ * up for CLAUDE.md) would load this block on top of each project's own, and
290
+ * with the wrong command name when a project pins a different one. Installing
291
+ * in a non-Git folder stays supported; this only names the overlap.
292
+ */
293
+ export function parentScopeWarning(projectRoot) {
294
+ if (insideGitWorkTree(projectRoot))
295
+ return null;
296
+ let entries;
297
+ try {
298
+ entries = readdirSync(projectRoot, { withFileTypes: true })
299
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && !PARENT_SCAN_SKIP.has(e.name))
300
+ .map((e) => e.name)
301
+ .sort();
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ const marker = `harnery:begin ${INSTRUCTIONS_REGION}`;
307
+ const children = entries.filter((name) => {
308
+ try {
309
+ return readFileSync(join(projectRoot, name, AGENTS_FILE), "utf8").includes(marker);
310
+ }
311
+ catch {
312
+ return false;
313
+ }
314
+ });
315
+ if (children.length === 0)
316
+ return null;
317
+ const listed = children.slice(0, 5).join(", ") + (children.length > 5 ? `, +${children.length - 5} more` : "");
318
+ return (`${projectRoot} is not a Git work tree and contains ${children.length} project(s) with their own ` +
319
+ `Harnery instructions (${listed}). Harnesses that read parent folders, such as Claude Code, will load ` +
320
+ `this block on top of theirs. Run init inside each project instead, or pass --project-root <project>.`);
321
+ }
240
322
  /**
241
323
  * Reverse {@link applyInstructions}: strip the AGENTS.md block, the CLAUDE.md
242
324
  * import shim, and delete the shipped skill files (only ones harnery generated).
@@ -317,18 +399,8 @@ export function removeInstructions(projectRoot, opts) {
317
399
  warnings.push(`left ${skill.relPath} (hand-edited; no harnery ownership marker)`);
318
400
  continue;
319
401
  }
320
- if (!opts.dryRun) {
321
- rmSync(skillPath);
322
- // Drop the now-empty harn-* dir, leaving the adapter skills root intact.
323
- const dir = dirname(skillPath);
324
- try {
325
- if (readdirSync(dir).length === 0)
326
- rmdirSync(dir);
327
- }
328
- catch {
329
- /* dir not empty or gone → leave it */
330
- }
331
- }
402
+ if (!opts.dryRun)
403
+ removeOwnedSkillFile(skillPath);
332
404
  actions.push(`+ ${opts.dryRun ? "would delete" : "deleted"} skill ${skill.id}`);
333
405
  }
334
406
  }
@@ -370,6 +442,12 @@ export function checkInstructions(projectRoot, opts) {
370
442
  continue;
371
443
  const skillPath = join(projectRoot, skillsDir, skill.relPath);
372
444
  const c = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : "";
445
+ if (!skillOptedIn(projectRoot, skill)) {
446
+ if (isOwnedFile(c)) {
447
+ issues.push(`skill ${skill.id}: present but ${skill.optIn}.enabled is off (re-run init)`);
448
+ }
449
+ continue;
450
+ }
373
451
  note(`skill ${skill.id}`, checkOwnedSkill(c, skillBody(skill.render, opts.binName)));
374
452
  }
375
453
  }
@@ -1 +1 @@
1
- {"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/git-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,eAAe,yDAIgB,CAAC;AAC7C,YAAY,EAAE,YAAY,EAAE,CAAC;AAM7B;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAkB7D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAoE7F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAa9D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2B9F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CA8B9F"}
1
+ {"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/git-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,eAAe,yDAIgB,CAAC;AAC7C,YAAY,EAAE,YAAY,EAAE,CAAC;AAM7B;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAqB7D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWlE;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAoE7F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAa9D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2B9F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CA8B9F"}
@@ -41,17 +41,20 @@ function regionName(event) {
41
41
  */
42
42
  export function renderGitHookBody(event) {
43
43
  const blocking = event === "pre-commit";
44
+ const native = process.platform === "win32";
45
+ const command = `${native ? "node " : ""}"$harnery_hook_bin"`;
46
+ const extension = native ? ".mjs" : "";
44
47
  const invoke = blocking
45
- ? ` "$harnery_hook_bin" git-hook ${event} "$@"\n` +
48
+ ? ` ${command} git-hook ${event} "$@"\n` +
46
49
  ` harnery_hook_rc=$?\n` +
47
50
  ` [ "$harnery_hook_rc" -ne 0 ] && exit "$harnery_hook_rc"\n`
48
- : ` "$harnery_hook_bin" git-hook ${event} "$@" >/dev/null 2>&1 || true\n`;
51
+ : ` ${command} git-hook ${event} "$@" >/dev/null 2>&1 || true\n`;
49
52
  return (`# Coordination for this hook lives in harnery (agent-coord git-hook ${event});\n` +
50
53
  `# upgrading harnery upgrades the behavior. Managed region: do not edit by hand.\n` +
51
54
  `harnery_hook_root=$(git rev-parse --show-superproject-working-tree 2>/dev/null)\n` +
52
55
  `[ -n "$harnery_hook_root" ] || harnery_hook_root=$(git rev-parse --show-toplevel 2>/dev/null)\n` +
53
- `for harnery_hook_bin in "$harnery_hook_root/harnery/bin/agent-coord" "$harnery_hook_root/node_modules/harnery/bin/agent-coord"; do\n` +
54
- ` [ -x "$harnery_hook_bin" ] || continue\n` +
56
+ `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` +
57
+ ` [ -${native ? "f" : "x"} "$harnery_hook_bin" ] || continue\n` +
55
58
  invoke +
56
59
  ` break\n` +
57
60
  `done`);
@@ -64,6 +67,7 @@ export function renderGitHookBody(event) {
64
67
  */
65
68
  export function resolveHooksDir(projectRoot) {
66
69
  const r = spawnSync("git", ["rev-parse", "--git-path", "hooks"], {
70
+ windowsHide: true,
67
71
  cwd: projectRoot,
68
72
  encoding: "utf8",
69
73
  timeout: 5000,
@@ -31,6 +31,11 @@ export interface BlockSkills {
31
31
  end: boolean;
32
32
  /** the `harn-team` skill file is present */
33
33
  team: boolean;
34
+ /**
35
+ * the opt-in `harn-computer` skill file is present (only when the project
36
+ * sets `computer.enabled`); absent from the default block entirely
37
+ */
38
+ computer?: boolean;
34
39
  }
35
40
  /**
36
41
  * The always-on orientation spliced into `AGENTS.md`. Target ≤ 80 rendered
@@ -51,6 +56,12 @@ export interface SkillTemplate {
51
56
  /** path under the adapter skill dir, e.g. `harn-decide/SKILL.md` */
52
57
  relPath: string;
53
58
  render: (binName: string) => string;
59
+ /**
60
+ * Config gate for an opt-in skill. `"computer"` renders only when the project
61
+ * sets `computer.enabled: true`; ungated skills always render (minus
62
+ * `skills.exclude`).
63
+ */
64
+ optIn?: "computer";
54
65
  }
55
66
  export declare const SKILLS: SkillTemplate[];
56
67
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AACtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,OAAO,CAAC;IACb,4CAA4C;IAC5C,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAAoE,GAC3E,MAAM,CAiHR;AAID,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAsVD,eAAO,MAAM,MAAM,EAAE,aAAa,EAiDjC,CAAC"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AACtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,OAAO,CAAC;IACb,4CAA4C;IAC5C,IAAI,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAAoE,GAC3E,MAAM,CAsHR;AAID,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAuZD,eAAO,MAAM,MAAM,EAAE,aAAa,EA8DjC,CAAC"}
@@ -58,6 +58,11 @@ export function renderInstructionsBlock(binName, skills = { decide: true, counci
58
58
  const teamPointer = skills.team
59
59
  ? "The `harn-team` skill owns the tier choice, the governor artifacts, and the drive loop."
60
60
  : `See \`${b} governor --help\` for the tier choice and the drive loop.`;
61
+ // Opt-in surface: the default block stays byte-identical unless the project
62
+ // enabled computer use and the skill is actually installed.
63
+ const computerPointer = skills.computer
64
+ ? `\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.`
65
+ : "";
61
66
  // Render the journal categories from the canonical enum so this prose can
62
67
  // never drift from what `journal add` actually accepts (the "note, plan…" list
63
68
  // silently lagged the tool by two categories before this).
@@ -143,7 +148,7 @@ can't resolve from the repo, file it instead. \`${b} decision file "<question>"\
143
148
  records it and lets you proceed on a stated default; \`${b} decision search "<terms>"\`
144
149
  surfaces prior decisions, so check for precedent before re-deciding. ${decidePointer}
145
150
 
146
- **Councils.** For a hard or contested decision, convene a council of agents. \`${b} council create "<objective>"\` runs structured rounds toward a decision. ${councilPointer}`;
151
+ **Councils.** For a hard or contested decision, convene a council of agents. \`${b} council create "<objective>"\` runs structured rounds toward a decision. ${councilPointer}${computerPointer}`;
147
152
  }
148
153
  function decideBody(b) {
149
154
  return `The decision docket is a persistent queue for decisions you would otherwise
@@ -481,6 +486,70 @@ Tell the operator which tier you chose and why, the goal id, the roles, and the
481
486
  single next action. If you declined to build a team because one already existed,
482
487
  say that first; it is the most useful sentence in the reply.`;
483
488
  }
489
+ function computerBody(b) {
490
+ return `\`${b} computer\` drives this machine's desktop through a pinned desktop driver.
491
+ It is the last resort: use a vendor API, a CLI, or the browser tools first.
492
+ Reach for it only for a native desktop app, or a page that needs a real
493
+ browser you cannot drive any other way.
494
+
495
+ ## Setup check
496
+
497
+ \`\`\`bash
498
+ ${b} computer status
499
+ \`\`\`
500
+
501
+ It shows the host, the pinned driver and whether it is installed, the daemon,
502
+ and who holds the desktop. When the driver is missing, the project must enable
503
+ computer use and run \`${b} computer install --yes\`. \`${b} computer doctor\`
504
+ reports the driver's own health checks.
505
+
506
+ ## Core loop
507
+
508
+ Every step is one call: \`${b} computer call <tool> '<json>'\`. Look up a tool's
509
+ arguments with \`${b} computer describe <tool>\` rather than guessing; \`${b} computer tools\`
510
+ lists them.
511
+
512
+ 1. **Launch** the app in the background with \`launch_app\`.
513
+ 2. **Read state** with \`get_window_state\` and \`include_screenshot: false\`. When
514
+ you need to see the window, pass \`screenshot_out_file\` with a file path
515
+ and read that file. Inline screenshots are redacted from the output unless
516
+ you pass \`--keep-screenshot\`, because they flood your context.
517
+ 3. **Act by element token** \`"<snapshot_id>:<row>"\` from the latest state:
518
+ click, \`set_value\`, or invoke, batched through \`run_steps\` when several
519
+ actions belong together.
520
+ 4. **Read back** the state after acting. Confirm the change landed before you
521
+ retry anything; never repeat an action blind.
522
+
523
+ ## Rules
524
+
525
+ - Prefer accessibility routes (\`set_value\`, invoke) over \`type_text\` and
526
+ coordinate clicks.
527
+ - Never pass \`delivery_mode: "foreground"\` unless the driver returned
528
+ \`background_unavailable\` or \`delivery_failed\` for that action.
529
+ - For a browser, call \`browser_prepare\` with \`allow_launch: true\` and
530
+ \`profile: {"mode": "isolated_new"}\`. Never attach to the user's real
531
+ profile unless the operator asks for it.
532
+ - Never type a password, username, or one-time code into a sign-in page. Stop
533
+ and ask the operator to sign in.
534
+ - Only \`kill_app\` processes you launched.
535
+
536
+ ## One desktop, one holder
537
+
538
+ Every agent on this machine shares one desktop. \`call\` takes a lease for you
539
+ and refreshes it on every call; it lapses after a short idle period.
540
+
541
+ - A refusal naming another holder means **wait**, then try once more later. Do
542
+ not retry in a loop. \`--wait <seconds>\` waits longer on one call.
543
+ - Check the holder with \`${b} computer lease status\`.
544
+ - Release the desktop as soon as you finish:
545
+
546
+ \`\`\`bash
547
+ ${b} computer lease release
548
+ \`\`\`
549
+
550
+ Report what you did on the desktop, which apps you launched and closed, and
551
+ anything you left open.`;
552
+ }
484
553
  export const SKILLS = [
485
554
  {
486
555
  id: "harn-decide",
@@ -522,4 +591,15 @@ export const SKILLS = [
522
591
  body: teamBody(binName),
523
592
  }),
524
593
  },
594
+ {
595
+ id: "harn-computer",
596
+ relPath: "harn-computer/SKILL.md",
597
+ optIn: "computer",
598
+ render: (binName) => buildOwnedSkill({
599
+ name: "harn-computer",
600
+ description: "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.",
601
+ binName,
602
+ body: computerBody(binName),
603
+ }),
604
+ },
525
605
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.46.1",
3
+ "version": "0.47.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -224,9 +224,9 @@
224
224
  }
225
225
  },
226
226
  "bin": {
227
- "harn": "./bin/harn",
228
- "agent-coord": "./bin/agent-coord",
229
- "agent-hook": "./bin/agent-hook"
227
+ "harn": "./bin/harn.mjs",
228
+ "agent-coord": "./bin/agent-coord.mjs",
229
+ "agent-hook": "./bin/agent-hook.mjs"
230
230
  },
231
231
  "//files": "src ships as TS so the `bun` export condition resolves to source; negate co-located tests, fixture directories, and shared test helpers so they don't ride into the tarball (a .npmignore is inert for paths already inside the files allowlist).",
232
232
  "files": [
@@ -256,7 +256,7 @@
256
256
  "scripts": {
257
257
  "build": "npm run clean && tsc -p tsconfig.build.json && node scripts/rewrite-dts-extensions.mjs",
258
258
  "build:openclaw-plugin": "mkdir -p openclaw-plugin/dist && bun build openclaw-plugin/src/index.ts openclaw-plugin/src/record-worker.ts --target=node --format=esm --external openclaw --outdir openclaw-plugin/dist --entry-naming '[name].js' && cp openclaw-plugin/package.json openclaw-plugin/openclaw.plugin.json openclaw-plugin/dist/",
259
- "clean": "rm -rf dist",
259
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
260
260
  "prepublishOnly": "npm run build",
261
261
  "typecheck": "bun x tsc --noEmit",
262
262
  "lint": "bun x @biomejs/biome check src openclaw-plugin opencode-plugin",