harnery 0.5.0 → 0.7.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 +16 -6
  2. package/dist/commander.d.ts +29 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +4 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +94 -33
  7. package/dist/commands/browse-ai.js +1 -1
  8. package/dist/commands/browse.d.ts.map +1 -1
  9. package/dist/commands/browse.js +41 -9
  10. package/dist/commands/cookies.js +1 -1
  11. package/dist/commands/decision.d.ts +4 -0
  12. package/dist/commands/decision.d.ts.map +1 -0
  13. package/dist/commands/decision.js +354 -0
  14. package/dist/commands/deinit.d.ts.map +1 -1
  15. package/dist/commands/deinit.js +4 -0
  16. package/dist/commands/devtools.d.ts +4 -0
  17. package/dist/commands/devtools.d.ts.map +1 -0
  18. package/dist/commands/devtools.js +239 -0
  19. package/dist/commands/docs.d.ts.map +1 -1
  20. package/dist/commands/docs.js +74 -2
  21. package/dist/commands/doctor.js +12 -4
  22. package/dist/commands/env.d.ts.map +1 -1
  23. package/dist/commands/env.js +3 -63
  24. package/dist/commands/fetch.js +1 -1
  25. package/dist/commands/init.d.ts +1 -0
  26. package/dist/commands/init.d.ts.map +1 -1
  27. package/dist/commands/init.js +54 -14
  28. package/dist/commands/scratch.js +1 -1
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +273 -62
  31. package/dist/commands/web-fetch.js +1 -1
  32. package/dist/core/agents/coord-client.d.ts.map +1 -1
  33. package/dist/core/agents/coord-client.js +32 -8
  34. package/dist/core/agents/events/consume.d.ts +25 -2
  35. package/dist/core/agents/events/consume.d.ts.map +1 -1
  36. package/dist/core/agents/events/consume.js +55 -7
  37. package/dist/core/agents/events/emit.d.ts +2 -1
  38. package/dist/core/agents/events/emit.d.ts.map +1 -1
  39. package/dist/core/agents/events/emit.js +6 -1
  40. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  41. package/dist/core/agents/rules/claim-conflict.js +16 -5
  42. package/dist/core/agents/state/scratch.d.ts +1 -1
  43. package/dist/core/agents/state/scratch.js +2 -2
  44. package/dist/core/config.d.ts +10 -0
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +13 -0
  47. package/dist/core/hooks/cli.js +3 -3
  48. package/dist/core/hooks/effects/index.d.ts +11 -7
  49. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  50. package/dist/core/hooks/effects/index.js +15 -17
  51. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  52. package/dist/core/hooks/events/emit.js +4 -0
  53. package/dist/core/hooks/events/rotate.d.ts +43 -0
  54. package/dist/core/hooks/events/rotate.d.ts.map +1 -0
  55. package/dist/core/hooks/events/rotate.js +142 -0
  56. package/dist/core/hooks/harness/events.d.ts +11 -1
  57. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  58. package/dist/core/hooks/harness/events.js +22 -3
  59. package/dist/core/hooks/harness/wiring.d.ts +8 -0
  60. package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
  61. package/dist/core/hooks/harness/wiring.js +34 -5
  62. package/dist/core/scratch/index.d.ts.map +1 -0
  63. package/dist/{lib → core}/scratch/index.js +2 -2
  64. package/dist/lib/agent-browser/client.js +1 -1
  65. package/dist/lib/browser/client.d.ts +14 -0
  66. package/dist/lib/browser/client.d.ts.map +1 -1
  67. package/dist/lib/browser/client.js +20 -0
  68. package/dist/lib/browser/index.d.ts +1 -0
  69. package/dist/lib/browser/index.d.ts.map +1 -1
  70. package/dist/lib/browser/runts.d.ts +44 -0
  71. package/dist/lib/browser/runts.d.ts.map +1 -0
  72. package/dist/lib/browser/runts.js +193 -0
  73. package/dist/lib/completion/walk.js +1 -1
  74. package/dist/lib/cookies/client.d.ts +1 -1
  75. package/dist/lib/cookies/client.d.ts.map +1 -1
  76. package/dist/lib/cookies/client.js +1 -1
  77. package/dist/lib/decision/index.d.ts +212 -0
  78. package/dist/lib/decision/index.d.ts.map +1 -0
  79. package/dist/lib/decision/index.js +523 -0
  80. package/dist/lib/devtools.d.ts +178 -0
  81. package/dist/lib/devtools.d.ts.map +1 -0
  82. package/dist/lib/devtools.js +1328 -0
  83. package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
  84. package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
  85. package/dist/lib/docs-frontmatter-migrate.js +364 -0
  86. package/dist/lib/docs-frontmatter.d.ts +33 -0
  87. package/dist/lib/docs-frontmatter.d.ts.map +1 -0
  88. package/dist/lib/docs-frontmatter.js +130 -0
  89. package/dist/lib/docs-index.d.ts +1 -0
  90. package/dist/lib/docs-index.d.ts.map +1 -1
  91. package/dist/lib/docs-index.js +4 -5
  92. package/dist/lib/docs-lint.d.ts +3 -0
  93. package/dist/lib/docs-lint.d.ts.map +1 -1
  94. package/dist/lib/docs-lint.js +67 -12
  95. package/dist/lib/docs-meta.d.ts +14 -0
  96. package/dist/lib/docs-meta.d.ts.map +1 -0
  97. package/dist/lib/docs-meta.js +34 -0
  98. package/dist/lib/docs-sweep.d.ts +12 -0
  99. package/dist/lib/docs-sweep.d.ts.map +1 -1
  100. package/dist/lib/docs-sweep.js +98 -103
  101. package/dist/lib/format.js +2 -2
  102. package/dist/lib/http/index.d.ts +1 -0
  103. package/dist/lib/http/index.d.ts.map +1 -1
  104. package/dist/lib/http/index.js +1 -0
  105. package/dist/lib/http/request.d.ts +77 -0
  106. package/dist/lib/http/request.d.ts.map +1 -0
  107. package/dist/lib/http/request.js +105 -0
  108. package/dist/lib/instructions/apply.d.ts +63 -0
  109. package/dist/lib/instructions/apply.d.ts.map +1 -0
  110. package/dist/lib/instructions/apply.js +255 -0
  111. package/dist/lib/instructions/splice.d.ts +73 -0
  112. package/dist/lib/instructions/splice.d.ts.map +1 -0
  113. package/dist/lib/instructions/splice.js +118 -0
  114. package/dist/lib/instructions/templates.d.ts +45 -0
  115. package/dist/lib/instructions/templates.d.ts.map +1 -0
  116. package/dist/lib/instructions/templates.js +258 -0
  117. package/dist/lib/tunnel/gate.d.ts +1 -0
  118. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  119. package/dist/lib/tunnel/gate.js +15 -10
  120. package/dist/lib/tunnel/state.d.ts +11 -1
  121. package/dist/lib/tunnel/state.d.ts.map +1 -1
  122. package/dist/lib/tunnel/state.js +8 -3
  123. package/package.json +9 -6
  124. package/src/commander.ts +35 -0
  125. package/src/commands/agents.ts +97 -29
  126. package/src/commands/browse-ai.ts +1 -1
  127. package/src/commands/browse.ts +63 -8
  128. package/src/commands/cookies.ts +1 -1
  129. package/src/commands/decision.ts +438 -0
  130. package/src/commands/deinit.ts +5 -0
  131. package/src/commands/devtools.ts +284 -0
  132. package/src/commands/docs.ts +86 -2
  133. package/src/commands/doctor.ts +13 -4
  134. package/src/commands/env.ts +11 -77
  135. package/src/commands/fetch.ts +1 -1
  136. package/src/commands/init.ts +66 -15
  137. package/src/commands/scratch.ts +1 -1
  138. package/src/commands/tunnel.ts +316 -65
  139. package/src/commands/web-fetch.ts +1 -1
  140. package/src/core/agents/coord-client.ts +34 -7
  141. package/src/core/agents/events/consume.ts +65 -7
  142. package/src/core/agents/events/emit.ts +7 -1
  143. package/src/core/agents/rules/claim-conflict.ts +17 -6
  144. package/src/core/agents/state/scratch.ts +2 -2
  145. package/src/core/config.ts +15 -1
  146. package/src/core/hooks/cli.ts +3 -3
  147. package/src/core/hooks/effects/index.ts +23 -16
  148. package/src/core/hooks/events/emit.ts +5 -0
  149. package/src/core/hooks/events/rotate.ts +151 -0
  150. package/src/core/hooks/harness/events.ts +30 -3
  151. package/src/core/hooks/harness/wiring.ts +46 -5
  152. package/src/{lib → core}/scratch/index.ts +2 -2
  153. package/src/lib/agent-browser/client.ts +1 -1
  154. package/src/lib/browser/client.ts +28 -0
  155. package/src/lib/browser/index.ts +4 -0
  156. package/src/lib/browser/runts.ts +218 -0
  157. package/src/lib/completion/walk.ts +1 -1
  158. package/src/lib/cookies/client.ts +2 -2
  159. package/src/lib/decision/index.ts +685 -0
  160. package/src/lib/devtools.ts +1653 -0
  161. package/src/lib/docs-frontmatter-migrate.ts +427 -0
  162. package/src/lib/docs-frontmatter.ts +151 -0
  163. package/src/lib/docs-index.ts +4 -5
  164. package/src/lib/docs-lint.ts +61 -11
  165. package/src/lib/docs-meta.ts +44 -0
  166. package/src/lib/docs-sweep.ts +104 -102
  167. package/src/lib/format.ts +2 -2
  168. package/src/lib/http/index.ts +1 -0
  169. package/src/lib/http/request.ts +154 -0
  170. package/src/lib/instructions/apply.ts +318 -0
  171. package/src/lib/instructions/splice.ts +148 -0
  172. package/src/lib/instructions/templates.ts +295 -0
  173. package/src/lib/tunnel/gate.ts +15 -10
  174. package/src/lib/tunnel/state.ts +19 -4
  175. package/dist/lib/scratch/index.d.ts.map +0 -1
  176. /package/dist/{lib → core}/scratch/index.d.ts +0 -0
@@ -1,6 +1,8 @@
1
1
  import { initDocsContext as initDocs, scanDocs } from "../lib/docs.js";
2
+ import { initDocsMigrationContext, runFrontmatterMigration, } from "../lib/docs-frontmatter-migrate.js";
2
3
  import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.js";
3
4
  import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.js";
5
+ import { readDocsMetadata, readDocsMetadataKey } from "../lib/docs-meta.js";
4
6
  import { countColdHandoffs, initDocsContext as initDocsSweep, runSweep, } from "../lib/docs-sweep.js";
5
7
  function ensureContext(context) {
6
8
  if (!context?.repoRoot || !context?.submodules) {
@@ -8,8 +10,13 @@ function ensureContext(context) {
8
10
  }
9
11
  const opts = { repoRoot: context.repoRoot, submodules: context.submodules };
10
12
  initDocs(opts);
13
+ initDocsMigrationContext(opts);
11
14
  initDocsIndex(opts);
12
- initDocsLint({ ...opts, extraExcludedPrefixes: context.extraDocsExcludedPrefixes });
15
+ initDocsLint({
16
+ ...opts,
17
+ extraExcludedPrefixes: context.extraDocsExcludedPrefixes,
18
+ docsRootAllowlist: context.docsRootAllowlist,
19
+ });
13
20
  initDocsSweep(opts);
14
21
  }
15
22
  let emit;
@@ -17,7 +24,7 @@ export function registerDocsCommand(program, emitParam, context) {
17
24
  emit = emitParam;
18
25
  const docs = program
19
26
  .command("docs")
20
- .description("Documentation tooling: freshness report, lint, sweep, index")
27
+ .description("Documentation tooling: freshness report, metadata, lint, sweep, index")
21
28
  // Options on the group itself back the default (no-subcommand) behavior.
22
29
  // See handleDocs below.
23
30
  .option("--stale <days>", "Only show files not committed in N+ days", Number.parseInt)
@@ -35,6 +42,37 @@ export function registerDocsCommand(program, emitParam, context) {
35
42
  emit.error({ code: "docs_error", message: msg });
36
43
  }
37
44
  });
45
+ docs
46
+ .command("meta")
47
+ .description("Read YAML frontmatter from a documentation file")
48
+ .argument("<path>", "Markdown file path, relative to the project root or absolute")
49
+ .argument("[key]", "Optional top-level frontmatter key")
50
+ .option("--json", "Emit a requested key as JSON even in an interactive terminal")
51
+ .action(async (path, key, opts) => {
52
+ try {
53
+ ensureContext(context);
54
+ handleMeta(context.repoRoot, path, key, opts);
55
+ }
56
+ catch (err) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ emit.error({ code: "docs_error", message: msg });
59
+ }
60
+ });
61
+ docs
62
+ .command("frontmatter-migrate")
63
+ .description("Convert lifecycle docs from bold metadata headers to YAML frontmatter")
64
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
65
+ .option("--yes", "Apply changes; without this flag the command is a dry-run")
66
+ .action(async (opts) => {
67
+ try {
68
+ ensureContext(context);
69
+ handleFrontmatterMigration(opts);
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ emit.error({ code: "docs_error", message: msg });
74
+ }
75
+ });
38
76
  docs
39
77
  .command("lint")
40
78
  .description("Verify every repo matches the documentation contract (directory layout + naming rules)")
@@ -82,6 +120,40 @@ export function registerDocsCommand(program, emitParam, context) {
82
120
  }
83
121
  });
84
122
  }
123
+ // --- `harn docs frontmatter-migrate` ---
124
+ function handleFrontmatterMigration(opts) {
125
+ const rows = runFrontmatterMigration({ repo: opts.repo, apply: !!opts.yes });
126
+ const counts = {
127
+ would_update: rows.filter((row) => row.status === "would-update").length,
128
+ updated: rows.filter((row) => row.status === "updated").length,
129
+ skipped: rows.filter((row) => row.status === "skipped").length,
130
+ errors: rows.filter((row) => row.status === "error").length,
131
+ };
132
+ emit.data({
133
+ dry_run: !opts.yes,
134
+ applied: !!opts.yes && counts.errors === 0,
135
+ aborted: !!opts.yes && counts.errors > 0,
136
+ repo: opts.repo ?? null,
137
+ counts,
138
+ rows,
139
+ });
140
+ if (counts.errors > 0)
141
+ emit.setExitCode(1);
142
+ }
143
+ // --- `harn docs meta` ---
144
+ function handleMeta(repoRoot, path, key, opts) {
145
+ const metadata = readDocsMetadata(repoRoot, path).data;
146
+ if (!key) {
147
+ emit.data(metadata);
148
+ return;
149
+ }
150
+ const value = readDocsMetadataKey(metadata, key, path);
151
+ if (opts.json || !process.stdout.isTTY || typeof value === "object") {
152
+ emit.data(value);
153
+ return;
154
+ }
155
+ emit.text(String(value));
156
+ }
85
157
  // --- Default `harn docs` (freshness report) ---
86
158
  async function handleDocs(opts) {
87
159
  if (opts.format === "json")
@@ -220,17 +220,25 @@ function checkHarnessHooks() {
220
220
  const bin = resolveBinName(root);
221
221
  const parts = drift.map((d) => {
222
222
  const bits = [];
223
+ if (d.parseError)
224
+ bits.push(`invalid JSON (${d.parseError})`);
223
225
  if (d.missing.length > 0) {
224
226
  bits.push(`${d.missing.length} missing (${d.missing.map((m) => m.subcommand).join(", ")})`);
225
227
  }
226
228
  if (d.orphans.length > 0)
227
229
  bits.push(`${d.orphans.length} orphaned (${d.orphans.join(", ")})`);
230
+ if (d.invalidTopLevelKeys.length > 0) {
231
+ bits.push(`invalid fields (${d.invalidTopLevelKeys.join(", ")})`);
232
+ }
233
+ if (d.invalidEventKeys.length > 0) {
234
+ bits.push(`unsupported events (${d.invalidEventKeys.join(", ")})`);
235
+ }
228
236
  return `${d.settingsFile}: ${bits.join("; ")}`;
229
237
  });
230
- const hasOrphans = drift.some((d) => d.orphans.length > 0);
231
- const hint = hasOrphans
232
- ? `run \`${bin} init\` to wire missing hooks; remove orphaned entries (renamed/dropped events) with \`${bin} deinit\` then \`${bin} init\``
233
- : `run \`${bin} init\` to wire the new hook(s) (idempotent)`;
238
+ const needsManualRepair = drift.some((d) => d.parseError || d.invalidTopLevelKeys.length > 0 || d.invalidEventKeys.length > 0);
239
+ const hint = needsManualRepair
240
+ ? `repair the invalid harness settings, then run \`${bin} init\` to migrate harnery hooks`
241
+ : `run \`${bin} init\` to migrate the hook set (idempotent)`;
234
242
  return { name: "harness hooks", severity: "warn", detail: parts.join(" | "), hint };
235
243
  }
236
244
  function macOrLinux(mac, linux) {
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAqB1E,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAaN"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAwB,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAqBhG,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAaN"}
@@ -4,7 +4,7 @@ import { exec, sh } from "../lib/exec.js";
4
4
  export function registerEnvCommand(program, emit, context) {
5
5
  program
6
6
  .command("env [section]")
7
- .description("Show environment status (docker, gcp, bq, node, python, git)")
7
+ .description("Show environment status (runtimes, docker, git; hosts can add sections)")
8
8
  .action(async (section) => {
9
9
  try {
10
10
  await handleEnv(section, emit, context);
@@ -20,9 +20,9 @@ async function handleEnv(section, emit, context) {
20
20
  const sections = {
21
21
  runtimes: checkRuntimes,
22
22
  docker: checkDocker,
23
- gcp: checkGcp,
24
- bq: checkBigQuery,
25
23
  git: () => checkGit(context),
24
+ // Host-registered sections (e.g. gcp/bq) merge in after the generic ones.
25
+ ...(context?.envSections ?? {}),
26
26
  };
27
27
  if (section) {
28
28
  const fn = sections[section];
@@ -110,66 +110,6 @@ async function checkDocker() {
110
110
  }
111
111
  return checks;
112
112
  }
113
- async function checkGcp() {
114
- const checks = [];
115
- const account = await sh("gcloud config get-value account 2>/dev/null").catch(() => ({
116
- stdout: "",
117
- exitCode: 1,
118
- stderr: "",
119
- }));
120
- const project = await sh("gcloud config get-value project 2>/dev/null").catch(() => ({
121
- stdout: "",
122
- exitCode: 1,
123
- stderr: "",
124
- }));
125
- if (account.exitCode === 0 && account.stdout) {
126
- checks.push({ label: "GCP Account", value: account.stdout, status: "ok" });
127
- }
128
- else {
129
- checks.push({ label: "GCP Account", value: "not authenticated", status: "missing" });
130
- }
131
- if (project.exitCode === 0 && project.stdout) {
132
- checks.push({ label: "GCP Project", value: project.stdout, status: "ok" });
133
- }
134
- else {
135
- checks.push({ label: "GCP Project", value: "not set", status: "missing" });
136
- }
137
- return checks;
138
- }
139
- async function checkBigQuery() {
140
- const checks = [];
141
- const result = await sh("bq ls --max_results=1 2>/dev/null").catch(() => ({
142
- stdout: "",
143
- exitCode: 1,
144
- stderr: "",
145
- }));
146
- if (result.exitCode === 0) {
147
- checks.push({ label: "BigQuery", value: "connected", status: "ok" });
148
- }
149
- else {
150
- checks.push({ label: "BigQuery", value: "not connected (check GCP auth)", status: "missing" });
151
- }
152
- const datasets = await sh("bq ls --format=json --max_results=20 2>/dev/null").catch(() => ({
153
- stdout: "",
154
- exitCode: 1,
155
- stderr: "",
156
- }));
157
- if (datasets.exitCode === 0 && datasets.stdout) {
158
- try {
159
- const ds = JSON.parse(datasets.stdout);
160
- const names = ds
161
- .map((d) => d.datasetReference?.datasetId)
162
- .filter(Boolean)
163
- .join(", ");
164
- if (names)
165
- checks.push({ label: "Datasets", value: names, status: "info" });
166
- }
167
- catch {
168
- // Ignore parse failures
169
- }
170
- }
171
- return checks;
172
- }
173
113
  async function checkGit(context) {
174
114
  const checks = [];
175
115
  const cwd = context?.repoRoot ?? process.cwd();
@@ -48,7 +48,7 @@ async function runFetch(url, opts, emit, context) {
48
48
  headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
49
49
  }
50
50
  const jar = opts.cookies !== false
51
- ? new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "bp-fetch" })
51
+ ? new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-fetch" })
52
52
  : null;
53
53
  const timeoutMs = Number.parseInt(opts.timeout, 10);
54
54
  const ac = new AbortController();
@@ -35,6 +35,7 @@ export declare function registerInitCommand(program: Command, emit: EmitContext,
35
35
  export declare function wireHooks(settings: SettingsFile, spec: HarnessSpec, agentHookPath: string, harness: HarnessId): {
36
36
  wired: number;
37
37
  already: number;
38
+ removed: number;
38
39
  };
39
40
  /**
40
41
  * Inverse of {@link wireHooks}: strip every harnery-owned hook entry from a
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAClG,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,iCAAiC,CAAC;AAIzC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAWxC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAsF/F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,SAAS,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAsBpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAmB1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmDhG"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAClG,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,iCAAiC,CAAC;AAKzC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAYxC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAsH/F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,SAAS,GACjB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAiCrD;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAmB1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmDhG"}
@@ -19,18 +19,22 @@ import { spawnSync } from "node:child_process";
19
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
20
  import { dirname, relative, resolve } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
- import { DEFAULT_BIN_NAME, stripJsonComments } from "../core/config.js";
22
+ import { DEFAULT_BIN_NAME, pinnedBinName, stripJsonComments } from "../core/config.js";
23
23
  import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
24
24
  import { commandWiresSubcommand, groupCommands, makeEntry, } from "../core/hooks/harness/wiring.js";
25
+ import { applyInstructions, checkInstructions } from "../lib/instructions/apply.js";
25
26
  // This file is src/commands/init.ts → harnery package root is two levels up.
26
27
  const HARNERY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
27
28
  export function registerInitCommand(program, emit, binName) {
28
29
  program
29
30
  .command("init")
30
- .description("Bootstrap harnery in this project: create .harnery/ and wire the harness " +
31
- "hooks (idempotent; safe to re-run). Use --dry-run to preview.")
31
+ .description("Bootstrap harnery in this project: create .harnery/, wire the harness " +
32
+ "hooks, and inject the agent-facing instructions block + skills " +
33
+ "(idempotent; safe to re-run). Use --dry-run to preview, --check to " +
34
+ "report drift without writing (exit 0 fresh / 2 drift / 1 error).")
32
35
  .option("--harness <id>", "claude-code | cursor | codex", "claude-code")
33
36
  .option("--dry-run", "Show what would change without writing")
37
+ .option("--check", "Report instructions/skills drift without writing; exit 0/2/1")
34
38
  .option("--project-root <path>", "Project root (default: git toplevel, else cwd)")
35
39
  .action((opts) => {
36
40
  const harness = opts.harness;
@@ -41,6 +45,25 @@ export function registerInitCommand(program, emit, binName) {
41
45
  return;
42
46
  }
43
47
  const projectRoot = resolve(opts.projectRoot ?? gitTopLevel() ?? process.cwd());
48
+ // A binName already pinned in this project's config.jsonc beats the
49
+ // invoking CLI's name: the pin is a committed, deliberate declaration;
50
+ // the invoking bin is circumstantial (any embedding host's CLI can run
51
+ // init inside any checkout). Without this, re-running init from a host
52
+ // CLI re-stamps the host's name into committed agent-facing surfaces.
53
+ const bin = pinnedBinName(projectRoot) ?? (binName?.trim() ? binName : DEFAULT_BIN_NAME);
54
+ // ── --check: read-only drift report on the block + skills ──────────────
55
+ if (opts.check === true) {
56
+ const { status, issues } = checkInstructions(projectRoot, { binName: bin, harness });
57
+ const head = status === "fresh"
58
+ ? "harn init --check: instructions + skills are current"
59
+ : status === "drift"
60
+ ? "harn init --check: drift found (re-run `init` to refresh)"
61
+ : "harn init --check: error";
62
+ const lines = issues.length ? `\n${issues.map((i) => ` ✗ ${i}`).join("\n")}` : "";
63
+ emit.text(`${head}${lines}`);
64
+ emit.setExitCode(status === "fresh" ? 0 : status === "drift" ? 2 : 1);
65
+ return;
66
+ }
44
67
  const dryRun = opts.dryRun === true;
45
68
  const actions = [];
46
69
  // ── 1. coord root ──────────────────────────────────────────────────────
@@ -61,10 +84,11 @@ export function registerInitCommand(program, emit, binName) {
61
84
  // The coord binaries (agent-hook/agent-coord) and web UI run as harnery
62
85
  // itself, so they can't see a consumer CLI's name; they read it back from
63
86
  // config.jsonc. Standalone `harn` is the resolver's default, so only a
64
- // consumer (binName ≠ "harn") needs the stamp.
65
- if (binName && binName !== DEFAULT_BIN_NAME) {
87
+ // consumer (bin ≠ "harn") needs the stamp. `bin` already honors an
88
+ // existing pin, so this never overwrites a deliberate config value.
89
+ if (bin !== DEFAULT_BIN_NAME) {
66
90
  const configPath = resolve(coordDir, "config.jsonc");
67
- const stamp = stampBinName(configPath, binName, dryRun);
91
+ const stamp = stampBinName(configPath, bin, dryRun);
68
92
  if (stamp)
69
93
  actions.push(stamp);
70
94
  }
@@ -86,19 +110,24 @@ export function registerInitCommand(program, emit, binName) {
86
110
  else {
87
111
  settings = {};
88
112
  }
89
- const { wired, already } = wireHooks(settings, spec, agentHook, harness);
90
- if (wired === 0) {
113
+ const { wired, already, removed } = wireHooks(settings, spec, agentHook, harness);
114
+ if (wired === 0 && removed === 0) {
91
115
  actions.push(`· all ${spec.events.length} ${harness} hooks already wired in ${rel(projectRoot, settingsPath)}`);
92
116
  }
93
117
  else if (dryRun) {
94
- actions.push(`+ would wire ${wired} hook(s) into ${rel(projectRoot, settingsPath)} (${already} already present)`);
118
+ actions.push(`+ would wire ${wired} hook(s) and remove ${removed} legacy hook(s) in ` +
119
+ `${rel(projectRoot, settingsPath)} (${already} already present)`);
95
120
  }
96
121
  else {
97
122
  mkdirSync(dirname(settingsPath), { recursive: true });
98
123
  writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
99
- actions.push(`+ wired ${wired} hook(s) into ${rel(projectRoot, settingsPath)} (${already} already present)`);
124
+ actions.push(`+ wired ${wired} hook(s) and removed ${removed} legacy hook(s) in ` +
125
+ `${rel(projectRoot, settingsPath)} (${already} already present)`);
100
126
  }
101
- emit.text(render(projectRoot, dryRun, actions));
127
+ // ── 3. agent-facing instructions block + skills ────────────────────────
128
+ const applied = applyInstructions(projectRoot, { binName: bin, harness, dryRun });
129
+ actions.push(...applied.actions);
130
+ emit.text(render(projectRoot, dryRun, actions, applied.warnings));
102
131
  });
103
132
  }
104
133
  /**
@@ -120,6 +149,16 @@ export function wireHooks(settings, spec, agentHookPath, harness) {
120
149
  settings.hooks = {};
121
150
  let wired = 0;
122
151
  let already = 0;
152
+ let removed = 0;
153
+ for (const { settingsKey, subcommand } of spec.legacyEvents ?? []) {
154
+ const groups = settings.hooks[settingsKey] ?? [];
155
+ const kept = groups.filter((group) => !groupCommands(group).some((command) => commandWiresSubcommand(command, subcommand)));
156
+ removed += groups.length - kept.length;
157
+ if (kept.length === 0)
158
+ delete settings.hooks[settingsKey];
159
+ else
160
+ settings.hooks[settingsKey] = kept;
161
+ }
123
162
  for (const { settingsKey, subcommand } of spec.events) {
124
163
  const command = `bash ${agentHookPath} ${subcommand} --harness ${harness}`;
125
164
  const groups = settings.hooks[settingsKey] ?? [];
@@ -132,7 +171,7 @@ export function wireHooks(settings, spec, agentHookPath, harness) {
132
171
  settings.hooks[settingsKey] = groups;
133
172
  wired++;
134
173
  }
135
- return { wired, already };
174
+ return { wired, already, removed };
136
175
  }
137
176
  /**
138
177
  * Inverse of {@link wireHooks}: strip every harnery-owned hook entry from a
@@ -258,12 +297,13 @@ function firstBraceIndex(raw) {
258
297
  }
259
298
  return -1;
260
299
  }
261
- function render(projectRoot, dryRun, actions) {
300
+ function render(projectRoot, dryRun, actions, warnings = []) {
262
301
  const head = dryRun ? "harn init (dry run): no changes written" : "harn init";
302
+ const warnBlock = warnings.length ? `\n${warnings.map((w) => ` ! ${w}`).join("\n")}` : "";
263
303
  const tail = dryRun
264
304
  ? "\nRe-run without --dry-run to apply."
265
305
  : "\nDone. Start a session and check `harn agents whoami`.";
266
- return `${head}\n root: ${projectRoot}\n${actions.map((a) => ` ${a}`).join("\n")}${tail}`;
306
+ return `${head}\n root: ${projectRoot}\n${actions.map((a) => ` ${a}`).join("\n")}${warnBlock}${tail}`;
267
307
  }
268
308
  function gitTopLevel() {
269
309
  const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, watch } fr
2
2
  import { resolve as resolvePath } from "node:path";
3
3
  import { emitCanonical, normalizeHarness, readHeartbeat } from "../core/agents/index.js";
4
4
  import { resolveBinName } from "../core/config.js";
5
- import { appendEntry, archiveScratch, currentOwnerOrThrow, lintScratch, listArchives, loadScratch, parseScratch, pruneArchives, resolveOwnerByName, SCRATCH_CATEGORIES, scratchDir, scratchPath, sweepOrphanScratchpads, } from "../lib/scratch/index.js";
5
+ import { appendEntry, archiveScratch, currentOwnerOrThrow, lintScratch, listArchives, loadScratch, parseScratch, pruneArchives, resolveOwnerByName, SCRATCH_CATEGORIES, scratchDir, scratchPath, sweepOrphanScratchpads, } from "../core/scratch/index.js";
6
6
  /**
7
7
  * `harn scratch`: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
8
8
  * Used both for self-notes (surviving compaction) and peer coordination (other
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA4jB1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,YAAY,CAAC,EAAE,qBAAqB,GACnC,IAAI,CAuDN"}
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA2yB1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,YAAY,CAAC,EAAE,qBAAqB,GACnC,IAAI,CAmEN"}