@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.
- package/dist/internal/system/assets.js +23 -0
- package/dist/internal/system/init/index.js +128 -11
- package/package.json +1 -1
- package/templates/target-kit/README.md +12 -9
- package/templates/target-kit/component.toml +2 -1
- package/templates/target-kit/routes.toml +1 -1
- package/templates/target-kit/system.toml +1 -1
- package/templates/target-kit/profiles/target/bindings.toml +0 -6
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
56
|
-
profilesDir
|
|
57
|
-
templatesDir
|
|
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(
|
|
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
|
-
|
|
75
|
-
|
|
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
|
@@ -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/
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 = "
|
|
5
|
+
profile = "typescript"
|
|
5
6
|
mode = "standard"
|
|
6
7
|
|
|
7
8
|
[gates]
|