@mikenguyen69/harness 0.1.0-beta.1 → 0.1.0-beta.2

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.
@@ -29,6 +29,29 @@ export function packageProfilesDir(fromUrl = import.meta.url) {
29
29
  export function packageTemplatesDir(fromUrl = import.meta.url) {
30
30
  return resolvePackageAsset(fromUrl, "templates", "ci-templates");
31
31
  }
32
+ /**
33
+ * Intake kit templates for `harness init`.
34
+ * Prefer staged `templates/target-kit`; fall back to the portfolio docs path
35
+ * when developing against the system-harness workspace checkout.
36
+ */
37
+ export function packageTargetKitDir(fromUrl = import.meta.url) {
38
+ const here = dirname(fileURLToPath(fromUrl));
39
+ let dir = here;
40
+ while (true) {
41
+ const staged = join(dir, "templates", "target-kit");
42
+ if (existsSync(join(staged, "component.toml")))
43
+ return resolve(staged);
44
+ const parent = dirname(dir);
45
+ if (parent === dir)
46
+ break;
47
+ dir = parent;
48
+ }
49
+ // Workspace: repos/system-harness/{src,dist}/… → orchestration docs kit
50
+ const workspaceKit = resolve(here, "..", "..", "..", "orchestration-harness", "docs", "target-kit");
51
+ if (existsSync(join(workspaceKit, "component.toml")))
52
+ return workspaceKit;
53
+ throw new Error(`target-kit templates not found (looked for templates/target-kit from ${fileURLToPath(fromUrl)})`);
54
+ }
32
55
  export function targetPath(repoRoot, ...segments) {
33
56
  return resolve(repoRoot, ...segments);
34
57
  }
@@ -1,20 +1,28 @@
1
1
  /**
2
- * init — scaffold hooks (`harness hook pre-tool-use`).
2
+ * init — scaffold hooks + target intake kit (TypeScript profile by default).
3
3
  *
4
4
  * Named owner of init-composes-hook-install: system-harness `init` invokes the
5
5
  * agent-harness hook-install path (`adapter/install`) directly as a subprocess
6
6
  * composition. system-harness does not import `@harness/agent`. The published
7
7
  * facade only routes `harness init` here and adds no hook-install logic.
8
+ *
9
+ * Default init also copies package-owned target-kit templates and the chosen
10
+ * language profile into the target repo (skip existing files). `--hooks-only`
11
+ * restores the previous hooks-only behaviour.
8
12
  */
9
13
  import { spawnSync } from "node:child_process";
10
- import { existsSync, mkdirSync } from "node:fs";
14
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
11
15
  import { dirname, join, resolve } from "node:path";
12
16
  import { fileURLToPath } from "node:url";
13
- import { packageProfilesDir, packageTemplatesDir, targetOwnedPaths, targetPath, } from "../assets.js";
17
+ import { packageProfilesDir, packageTargetKitDir, packageTemplatesDir, targetOwnedPaths, targetPath, } from "../assets.js";
14
18
  /** Public facade hook command — never an internal binary path. */
15
19
  export const HOOK_COMMAND = "harness hook pre-tool-use";
16
20
  /** Documented composition: system init → agent-harness adapter/install. */
17
21
  export const HOOK_INSTALL_OWNER = "system-harness init composes agent-harness adapter/install";
22
+ /** Default language profile scaffolded into the target. */
23
+ export const DEFAULT_INIT_PROFILE = "typescript";
24
+ /** Kit files that stay package-local documentation, not target config. */
25
+ const KIT_SKIP = new Set(["README.md"]);
18
26
  function resolveAgentHookInstall() {
19
27
  const here = dirname(fileURLToPath(import.meta.url));
20
28
  const fromEnv = process.env.AGENT_HARNESS_BIN;
@@ -48,13 +56,97 @@ export function writeInitHooks(repoRoot, _command = HOOK_COMMAND) {
48
56
  const written = (result.stdout ?? "").trim();
49
57
  return written || targetPath(repoRoot, ".claude", "settings.json");
50
58
  }
51
- export function runInit(repoRoot) {
59
+ function walkFiles(root, prefix = "") {
60
+ const out = [];
61
+ if (!existsSync(root))
62
+ return out;
63
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
64
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
65
+ const abs = join(root, entry.name);
66
+ if (entry.isDirectory())
67
+ out.push(...walkFiles(abs, rel));
68
+ else if (entry.isFile())
69
+ out.push(rel.replaceAll("\\", "/"));
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * Copy intake kit + language profile into the target. Never overwrites.
75
+ */
76
+ export function scaffoldTargetKit(repoRoot, options = {}) {
77
+ const root = resolve(repoRoot);
78
+ const profile = options.profile ?? DEFAULT_INIT_PROFILE;
79
+ const fromUrl = options.fromUrl ?? import.meta.url;
80
+ const kitDir = packageTargetKitDir(fromUrl);
81
+ const packageProfiles = packageProfilesDir(fromUrl);
82
+ const profileSrc = join(packageProfiles, profile, "bindings.toml");
83
+ if (!existsSync(profileSrc)) {
84
+ throw new Error(`profile "${profile}" not found under ${packageProfiles} (expected ${profile}/bindings.toml)`);
85
+ }
86
+ const written = [];
87
+ const skipped = [];
88
+ const copyOne = (src, destRel) => {
89
+ const dest = join(root, destRel);
90
+ if (existsSync(dest)) {
91
+ skipped.push(destRel.replaceAll("\\", "/"));
92
+ return;
93
+ }
94
+ mkdirSync(dirname(dest), { recursive: true });
95
+ copyFileSync(src, dest);
96
+ written.push(destRel.replaceAll("\\", "/"));
97
+ };
98
+ for (const rel of walkFiles(kitDir)) {
99
+ const base = rel.split("/").pop() ?? rel;
100
+ if (KIT_SKIP.has(base))
101
+ continue;
102
+ // Old kit layout may still ship profiles/target — skip; we install the
103
+ // language profile from package profiles/ instead.
104
+ if (rel.startsWith("profiles/"))
105
+ continue;
106
+ copyOne(join(kitDir, rel), rel);
107
+ }
108
+ copyOne(profileSrc, join("profiles", profile, "bindings.toml"));
109
+ ensureGitignoreHarness(root, written, skipped);
110
+ return {
111
+ written,
112
+ skipped,
113
+ profile,
114
+ kitDir,
115
+ profilesDir: packageProfiles,
116
+ };
117
+ }
118
+ function ensureGitignoreHarness(repoRoot, written, skipped) {
119
+ const rel = ".gitignore";
120
+ const dest = join(repoRoot, rel);
121
+ const marker = ".harness/";
122
+ if (!existsSync(dest)) {
123
+ writeFileSync(dest, `${marker}\n`, "utf8");
124
+ written.push(rel);
125
+ return;
126
+ }
127
+ const body = readFileSync(dest, "utf8");
128
+ if (body.split(/\r?\n/).some((line) => line.trim() === marker || line.trim() === ".harness")) {
129
+ skipped.push(rel);
130
+ return;
131
+ }
132
+ const next = body.endsWith("\n") || body.length === 0 ? `${body}${marker}\n` : `${body}\n${marker}\n`;
133
+ writeFileSync(dest, next, "utf8");
134
+ written.push(`${rel} (+${marker})`);
135
+ }
136
+ export function runInit(repoRoot, options = {}) {
52
137
  const root = resolve(repoRoot);
138
+ const hooksPath = writeInitHooks(root);
139
+ const profilesDir = packageProfilesDir();
140
+ const templatesDir = packageTemplatesDir();
141
+ const scaffold = options.hooksOnly
142
+ ? null
143
+ : scaffoldTargetKit(root, { profile: options.profile ?? DEFAULT_INIT_PROFILE });
53
144
  return {
54
145
  repoRoot: root,
55
- hooksPath: writeInitHooks(root),
56
- profilesDir: packageProfilesDir(),
57
- templatesDir: packageTemplatesDir(),
146
+ hooksPath,
147
+ profilesDir,
148
+ templatesDir,
149
+ scaffold,
58
150
  };
59
151
  }
60
152
  function flag(args, name) {
@@ -63,16 +155,41 @@ function flag(args, name) {
63
155
  }
64
156
  export function initCommand(args, cwd = process.cwd()) {
65
157
  if (args.includes("--help") || args.includes("-h")) {
66
- process.stdout.write("harness init [--cwd <dir>] scaffold hooks (harness hook pre-tool-use)\n");
158
+ process.stdout.write([
159
+ "harness init [--cwd <dir>] [--profile <name>] [--hooks-only]",
160
+ "",
161
+ "Scaffold the target intake kit (TypeScript profile by default), ensure",
162
+ ".harness/ is gitignored, and install the agent PreToolUse hook",
163
+ `(${HOOK_COMMAND}). Existing files are left untouched.`,
164
+ "",
165
+ " --hooks-only only install hooks (skip kit / profile copy)",
166
+ " --profile language profile to copy (default: typescript)",
167
+ "",
168
+ ].join("\n"));
67
169
  return 0;
68
170
  }
69
171
  const repo = resolve(flag(args, "cwd") ?? cwd);
70
172
  mkdirSync(targetOwnedPaths(repo).harnessDir, { recursive: true });
71
173
  try {
72
- const result = runInit(repo);
174
+ const result = runInit(repo, {
175
+ hooksOnly: args.includes("--hooks-only"),
176
+ ...(flag(args, "profile") ? { profile: flag(args, "profile") } : {}),
177
+ });
73
178
  process.stdout.write(`✓ hooks ${result.hooksPath}\n`);
74
- process.stdout.write(` (profiles from ${result.profilesDir})\n`);
75
- process.stdout.write(` (templates from ${result.templatesDir})\n`);
179
+ if (result.scaffold) {
180
+ const { written, skipped, profile } = result.scaffold;
181
+ process.stdout.write(`✓ kit profile=${profile}\n`);
182
+ for (const rel of written)
183
+ process.stdout.write(` + ${rel}\n`);
184
+ for (const rel of skipped)
185
+ process.stdout.write(` = ${rel} (exists)\n`);
186
+ process.stdout.write(" Next: replace every REPLACE marker, then harness doctor / spec check.\n");
187
+ }
188
+ else {
189
+ process.stdout.write(" (hooks-only — kit not scaffolded)\n");
190
+ }
191
+ process.stdout.write(` (package profiles ${result.profilesDir})\n`);
192
+ process.stdout.write(` (package templates ${result.templatesDir})\n`);
76
193
  return 0;
77
194
  }
78
195
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikenguyen69/harness",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "One installed `harness` command over the portfolio — routing facade, not a fourth control loop.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,15 +10,16 @@ materializing a unit.
10
10
  [docs/onboarding.md](../../../../docs/onboarding.md). Do not wire compiled
11
11
  portfolio entrypoints for target work.
12
12
 
13
- Expected target layout:
13
+ Expected target layout (written by `harness init`):
14
14
 
15
15
  ```text
16
16
  component.toml
17
17
  system.toml
18
18
  routes.toml
19
19
  orchestration.json
20
- profiles/target/bindings.toml
20
+ profiles/typescript/bindings.toml
21
21
  specs/first-unit.yaml
22
+ .gitignore # ensures .harness/ is ignored
22
23
  ```
23
24
 
24
25
  Before dispatch, record:
@@ -39,18 +40,20 @@ Before dispatch, record:
39
40
 
40
41
  Then:
41
42
 
42
- 1. Copy and customize all templates.
43
- 2. Run `rg -n "REPLACE" component.toml system.toml routes.toml orchestration.json profiles specs`; zero matches are required.
44
- 3. From the target root, scaffold hooks (no separate hook-install step):
43
+ 1. From the target root, scaffold the kit + hooks (no manual copy of this directory):
45
44
 
46
45
  ```sh
47
46
  harness init
47
+ # or: harness init --hooks-only # hooks only, if the kit is already present
48
48
  ```
49
49
 
50
- Hook installation is one step of `harness init`. Owner: system-harness
50
+ `harness init` copies these templates and `profiles/typescript/` from the
51
+ installed package, ensures `.harness/` is gitignored, and installs the
52
+ PreToolUse hook. Existing files are left untouched. Owner: system-harness
51
53
  `init` composing the agent-harness hook-install path; generated commands call
52
54
  `harness hook pre-tool-use`.
53
- 4. Read-only preflight, then materialize once:
55
+ 2. Customize every scaffolded file. Run `rg -n "REPLACE" component.toml system.toml routes.toml orchestration.json profiles specs`; zero matches are required.
56
+ 3. Read-only preflight, then materialize once:
54
57
 
55
58
  ```sh
56
59
  harness spec check --specs ./specs --system ./system.toml
@@ -58,8 +61,8 @@ harness spec materialize --specs ./specs --system ./system.toml --ledger ./.harn
58
61
  harness doctor --repo . --routes ./routes.toml --spool ./.harness/spool --json
59
62
  ```
60
63
 
61
- 5. Review doctor’s resolved runner/model, commands, budgets, base branch, and dirty-tree decision.
62
- 6. Obtain separate authorization for the target dispatch and its merge policy.
64
+ 4. Review doctor’s resolved runner/model, commands, budgets, base branch, and dirty-tree decision.
65
+ 5. Obtain separate authorization for the target dispatch and its merge policy.
63
66
  Installing `@mikenguyen69/harness` authorizes neither a dry run nor a real target
64
67
  run. `doctor` / `spec check` do not dispatch agents.
65
68
 
@@ -1,7 +1,8 @@
1
1
  # REPLACE every command/decline rationale with the target's reviewed policy.
2
+ # Defaults come from profiles/typescript/bindings.toml; entries here override.
2
3
  [harness]
3
4
  policy_version = "0.0.1"
4
- profile = "target"
5
+ profile = "typescript"
5
6
  mode = "standard"
6
7
 
7
8
  [gates]
@@ -1,5 +1,5 @@
1
1
  [[route]]
2
- profile = "target"
2
+ profile = "typescript"
3
3
  runner = "claude"
4
4
  model = "REPLACE-approved-model"
5
5
  mode = "standard"
@@ -4,4 +4,4 @@ id = "REPLACE-target-system-id"
4
4
  [[component]]
5
5
  id = "app"
6
6
  root = "."
7
- profile = "target"
7
+ profile = "typescript"
@@ -1,6 +0,0 @@
1
- [harness]
2
- profile_version = "0.0.1"
3
-
4
- # The component contract owns the initial bindings. Move stable, shared target
5
- # bindings here only after the first dry run proves them.
6
- [gates]