@structor-dev/cli 0.2.1 → 0.2.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/README.md +13 -11
- package/bin/structor.mjs +336 -32
- package/docs/INIT.md +19 -11
- package/docs/adr/0002-store-harness-configuration-in-generated-harness.md +42 -0
- package/package.json +1 -1
- package/schemas/harness-config.schema.json +8 -0
- package/scripts/init-harness.mjs +81 -13
- package/scripts/lib.mjs +16 -2
- package/scripts/rendered-config.mjs +9 -6
- package/scripts/setup-contributor.mjs +1 -1
- package/template/scripts/bootstrap-workspace.mjs.tpl +1 -1
- package/template/scripts/check-workspace.mjs.tpl +1 -1
- package/template/workspace/AGENTS.md.tpl +4 -5
- package/template/workspace/CLAUDE.md.tpl +5 -5
package/README.md
CHANGED
|
@@ -55,10 +55,11 @@ During local development from a clone of this repo, use
|
|
|
55
55
|
`node ./structor/bin/structor.mjs init` from the parent workspace instead.
|
|
56
56
|
|
|
57
57
|
`init` is local-only and deterministic. It detects sibling repos, asks a few
|
|
58
|
-
questions,
|
|
59
|
-
generated
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
questions, previews the full setup transaction with a dry run, persists
|
|
59
|
+
`harness.config.json` inside the generated harness only after confirmation, and
|
|
60
|
+
does not report success until consumer entrypoints, workspace entrypoints, and
|
|
61
|
+
completion gates have passed. No network calls, no LLM calls, no telemetry, no
|
|
62
|
+
package installs, and no remote service mutation.
|
|
62
63
|
|
|
63
64
|
`structor init` remains the normal setup flow for users creating generated
|
|
64
65
|
harnesses for their own target repositories. Contributing to Structor itself is
|
|
@@ -222,9 +223,9 @@ These boundaries are intentional in the current template:
|
|
|
222
223
|
The supported happy path is:
|
|
223
224
|
|
|
224
225
|
1. Clone this template repo into the same workspace folder as the consumer repos.
|
|
225
|
-
2.
|
|
226
|
-
3.
|
|
227
|
-
4.
|
|
226
|
+
2. Run `structor init` from the workspace folder.
|
|
227
|
+
3. Let Structor generate the project harness as a sibling of the consumer repos.
|
|
228
|
+
4. Let Structor install or verify consumer and workspace entrypoints before success.
|
|
228
229
|
5. Start Codex or Claude Code from the workspace, generated harness, or a
|
|
229
230
|
bootstrapped consumer repo.
|
|
230
231
|
|
|
@@ -241,10 +242,11 @@ workspace/
|
|
|
241
242
|
|
|
242
243
|
With that layout, the current flow can bootstrap consumer repos out of the box
|
|
243
244
|
when their agent pointer files are missing. For safety, existing consumer
|
|
244
|
-
`AGENTS.md`, `CLAUDE.md`, and `.claude/CLAUDE.md` files
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
workspace
|
|
245
|
+
`AGENTS.md`, `CLAUDE.md`, and `.claude/CLAUDE.md` files must already match the
|
|
246
|
+
expected Structor-managed pointer surfaces or `init` fails unless `--force` is
|
|
247
|
+
explicitly passed. If you generate the harness somewhere else, move or copy it
|
|
248
|
+
into the sibling workspace layout before running the generated workspace
|
|
249
|
+
bootstrap scripts.
|
|
248
250
|
|
|
249
251
|
## Agent-Assisted Manual Setup
|
|
250
252
|
|
package/bin/structor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { access, mkdir, readdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { access, mkdir, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { constants as fsConstants } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
@@ -13,6 +13,15 @@ import {
|
|
|
13
13
|
resolveHarnessConfig,
|
|
14
14
|
validateConfigShape,
|
|
15
15
|
} from "../scripts/lib.mjs";
|
|
16
|
+
import {
|
|
17
|
+
generateHarness,
|
|
18
|
+
installConsumerEntrypoints,
|
|
19
|
+
render,
|
|
20
|
+
} from "../scripts/init-harness.mjs";
|
|
21
|
+
import {
|
|
22
|
+
consumerEntrypointValues,
|
|
23
|
+
harnessTemplateValues,
|
|
24
|
+
} from "../scripts/rendered-config.mjs";
|
|
16
25
|
import {
|
|
17
26
|
consumerEntrypointsForSettings,
|
|
18
27
|
requiredHarnessRepoFilesForWorkspaceCheck,
|
|
@@ -453,11 +462,67 @@ async function loadExistingConfig(configPath) {
|
|
|
453
462
|
}
|
|
454
463
|
}
|
|
455
464
|
|
|
465
|
+
async function discoverWorkspaceConfigPath(workspaceRoot, explicitConfigPath = null) {
|
|
466
|
+
if (explicitConfigPath) return path.resolve(workspaceRoot, explicitConfigPath);
|
|
467
|
+
|
|
468
|
+
const workspaceConfigPath = path.join(workspaceRoot, configFileName);
|
|
469
|
+
const matches = [];
|
|
470
|
+
const skipDirectoryNames = new Set([".git", "node_modules"]);
|
|
471
|
+
|
|
472
|
+
async function visitDirectory(directoryPath, depth = 0) {
|
|
473
|
+
if (depth > 4) return;
|
|
474
|
+
|
|
475
|
+
const candidatePath = path.join(directoryPath, configFileName);
|
|
476
|
+
const candidate = await maybeReadJson(candidatePath);
|
|
477
|
+
const resolvedWorkspaceRoot = candidate?.workspace?.root
|
|
478
|
+
? path.resolve(path.dirname(candidatePath), candidate.workspace.root)
|
|
479
|
+
: null;
|
|
480
|
+
if (resolvedWorkspaceRoot === workspaceRoot) {
|
|
481
|
+
matches.push(candidatePath);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
485
|
+
for (const entry of entries) {
|
|
486
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || skipDirectoryNames.has(entry.name)) continue;
|
|
487
|
+
await visitDirectory(path.join(directoryPath, entry.name), depth + 1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await visitDirectory(workspaceRoot);
|
|
492
|
+
return matches.length === 1 ? matches[0] : workspaceConfigPath;
|
|
493
|
+
}
|
|
494
|
+
|
|
456
495
|
async function writeConfig(configPath, config) {
|
|
457
496
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
458
497
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
459
498
|
}
|
|
460
499
|
|
|
500
|
+
function configContent(config) {
|
|
501
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function durableConfigPathFor(workspaceRoot, outputPath) {
|
|
505
|
+
return path.join(path.resolve(workspaceRoot, outputPath), configFileName);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function initConfigWithWorkspaceRoot(config, workspaceRoot) {
|
|
509
|
+
const configPath = durableConfigPathFor(workspaceRoot, config.output.path);
|
|
510
|
+
return {
|
|
511
|
+
workspace: {
|
|
512
|
+
root: relativeFrom(path.dirname(configPath), workspaceRoot),
|
|
513
|
+
},
|
|
514
|
+
...config,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function initCompletionCommands(harnessRoot) {
|
|
519
|
+
return [
|
|
520
|
+
commandText(process.execPath, ["scripts/validate-governance.mjs"]),
|
|
521
|
+
commandText(process.execPath, ["scripts/check-workspace.mjs"]),
|
|
522
|
+
`Config: ${path.join(harnessRoot, configFileName)}`,
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
|
|
461
526
|
export function nextValidationCommands(config) {
|
|
462
527
|
const commands = [
|
|
463
528
|
`cd ${config.output.path}`,
|
|
@@ -629,7 +694,7 @@ function printDoctorCheck(results, status, label, detail = "") {
|
|
|
629
694
|
async function doctor(options) {
|
|
630
695
|
const results = [];
|
|
631
696
|
const workspaceRoot = path.resolve(options.workspace ?? process.cwd());
|
|
632
|
-
const configPath =
|
|
697
|
+
const configPath = await discoverWorkspaceConfigPath(workspaceRoot, options.config);
|
|
633
698
|
section("Structor doctor");
|
|
634
699
|
note("Diagnosis only. No files will be repaired or written.");
|
|
635
700
|
|
|
@@ -796,6 +861,162 @@ function printNextSteps(config) {
|
|
|
796
861
|
}
|
|
797
862
|
}
|
|
798
863
|
|
|
864
|
+
function printSetupTransactionPreview(config, configPath) {
|
|
865
|
+
const settings = {
|
|
866
|
+
models: config.models,
|
|
867
|
+
clientSupport: {
|
|
868
|
+
codexHooks: config.clientSupport?.codex?.hooks ?? config.models.openai,
|
|
869
|
+
claudeRules: config.clientSupport?.claude?.rules ?? config.models.anthropic,
|
|
870
|
+
claudeHooks: false,
|
|
871
|
+
claudeSkills: false,
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
section("Setup transaction preview");
|
|
876
|
+
console.log(`Durable config: ${configPath}`);
|
|
877
|
+
console.log(`Generated harness: ${config.output.path}`);
|
|
878
|
+
console.log("Consumer entrypoints:");
|
|
879
|
+
for (const consumer of config.consumers) {
|
|
880
|
+
for (const entrypoint of consumerEntrypointsForSettings(settings)) {
|
|
881
|
+
console.log(` - ${consumer.path}/${entrypoint.path}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
console.log("Workspace entrypoints:");
|
|
885
|
+
for (const entrypoint of workspaceEntrypointsForSettings(settings)) {
|
|
886
|
+
console.log(` - ${entrypoint.path}`);
|
|
887
|
+
}
|
|
888
|
+
console.log("Completion gates:");
|
|
889
|
+
console.log(" - node scripts/validate-governance.mjs");
|
|
890
|
+
console.log(" - node scripts/check-workspace.mjs");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function runGeneratedNodeScript({ harnessRoot, relativeScriptPath, args = [], failureLabel }) {
|
|
894
|
+
const result = spawnSync(process.execPath, [relativeScriptPath, ...args], {
|
|
895
|
+
cwd: harnessRoot,
|
|
896
|
+
encoding: "utf8",
|
|
897
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
898
|
+
});
|
|
899
|
+
printCommandOutput(result);
|
|
900
|
+
if (result.status !== 0) {
|
|
901
|
+
throw new Error(failureLabel);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function assertGeneratedScriptsReady(generatedFiles, scriptPaths) {
|
|
906
|
+
const filesByPath = new Map(generatedFiles.map((file) => [file.targetRelative, file]));
|
|
907
|
+
const notReady = [];
|
|
908
|
+
for (const scriptPath of scriptPaths) {
|
|
909
|
+
const file = filesByPath.get(scriptPath);
|
|
910
|
+
if (!file || file.action === "skipped" || !file.rendered) {
|
|
911
|
+
notReady.push(scriptPath);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (notReady.length > 0) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
`Generated setup scripts were not refreshed or verified:\n${notReady.map((item) => `- ${item}`).join("\n")}\nInspect the existing files and re-run with --force if they should be replaced.`,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function assertNoEntrypointConflicts({ config, resolvedConfig, harnessRoot, force }) {
|
|
923
|
+
if (force) return;
|
|
924
|
+
|
|
925
|
+
const settings = { models: config.models, clientSupport: resolvedConfig.support };
|
|
926
|
+
const conflicts = [];
|
|
927
|
+
const templateWorkspaceRoot = config.workspace?.root
|
|
928
|
+
? path.resolve(harnessRoot, config.workspace.root)
|
|
929
|
+
: path.dirname(harnessRoot);
|
|
930
|
+
const harnessValues = harnessTemplateValues(
|
|
931
|
+
config,
|
|
932
|
+
resolvedConfig.support,
|
|
933
|
+
resolvedConfig.consumers,
|
|
934
|
+
harnessRoot,
|
|
935
|
+
templateWorkspaceRoot,
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
for (const entrypoint of workspaceEntrypointsForSettings(settings)) {
|
|
939
|
+
const targetPath = path.join(resolvedConfig.workspaceRoot, entrypoint.path);
|
|
940
|
+
if (!(await exists(targetPath))) continue;
|
|
941
|
+
|
|
942
|
+
const templatePath = path.join(packageRoot, "template", entrypoint.template);
|
|
943
|
+
const [actual, template] = await Promise.all([
|
|
944
|
+
readFile(targetPath, "utf8"),
|
|
945
|
+
readFile(templatePath, "utf8"),
|
|
946
|
+
]);
|
|
947
|
+
const expected = render(template, harnessValues);
|
|
948
|
+
if (actual !== expected) {
|
|
949
|
+
conflicts.push(`workspace:${entrypoint.path}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
for (const resolvedConsumer of resolvedConfig.consumers) {
|
|
954
|
+
const consumer = resolvedConsumer.config;
|
|
955
|
+
const consumerRoot = resolvedConsumer.confirmedRoot ?? resolvedConsumer.root;
|
|
956
|
+
const harnessRelativePath = path.relative(consumerRoot, harnessRoot).replaceAll(path.sep, "/") || ".";
|
|
957
|
+
const values = consumerEntrypointValues(config, consumer, harnessRelativePath);
|
|
958
|
+
|
|
959
|
+
for (const entrypoint of consumerEntrypointsForSettings(settings)) {
|
|
960
|
+
const targetPath = path.join(consumerRoot, entrypoint.path);
|
|
961
|
+
if (!(await exists(targetPath))) continue;
|
|
962
|
+
|
|
963
|
+
const templatePath = path.join(packageRoot, "template", entrypoint.template);
|
|
964
|
+
const [actual, template] = await Promise.all([
|
|
965
|
+
readFile(targetPath, "utf8"),
|
|
966
|
+
readFile(templatePath, "utf8"),
|
|
967
|
+
]);
|
|
968
|
+
const expected = render(template, values);
|
|
969
|
+
if (actual !== expected) {
|
|
970
|
+
conflicts.push(`consumer:${consumer.name}:${entrypoint.path}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (conflicts.length > 0) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`Entrypoint conflicts detected before bootstrap:\n${conflicts.map((item) => `- ${item}`).join("\n")}\nRe-run with --force to overwrite known Structor pointer surfaces.`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function removeEmptyParents(startPath, stopPath) {
|
|
983
|
+
let current = path.dirname(startPath);
|
|
984
|
+
const resolvedStop = path.resolve(stopPath);
|
|
985
|
+
while (current.startsWith(resolvedStop) && current !== resolvedStop) {
|
|
986
|
+
try {
|
|
987
|
+
await rm(current);
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (error?.code === "ENOTEMPTY" || error?.code === "ENOENT") return;
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
current = path.dirname(current);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function cleanupFailedInit({
|
|
997
|
+
harnessRoot,
|
|
998
|
+
harnessRootExisted,
|
|
999
|
+
workspaceRoot,
|
|
1000
|
+
workspaceCreatedPaths,
|
|
1001
|
+
consumerCreatedPaths,
|
|
1002
|
+
harnessCreatedPaths,
|
|
1003
|
+
}) {
|
|
1004
|
+
for (const targetPath of [...workspaceCreatedPaths, ...consumerCreatedPaths]) {
|
|
1005
|
+
await rm(targetPath, { force: true });
|
|
1006
|
+
await removeEmptyParents(targetPath, workspaceRoot);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!harnessRootExisted) {
|
|
1010
|
+
await rm(harnessRoot, { recursive: true, force: true });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
for (const targetPath of harnessCreatedPaths) {
|
|
1015
|
+
await rm(targetPath, { force: true });
|
|
1016
|
+
await removeEmptyParents(targetPath, harnessRoot);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
799
1020
|
async function printContributorPlan(plan, options, sourceReady) {
|
|
800
1021
|
section(options.dryRun ? "Contributor workspace preview" : "Contributor workspace");
|
|
801
1022
|
console.log(`Workspace: ${plan.workspaceRoot}`);
|
|
@@ -926,11 +1147,11 @@ async function init(options) {
|
|
|
926
1147
|
|
|
927
1148
|
const workspaceDefault = options.workspace ? path.resolve(options.workspace) : process.cwd();
|
|
928
1149
|
const workspaceRoot = path.resolve(await askLine(rl, "Workspace folder", workspaceDefault));
|
|
929
|
-
const
|
|
930
|
-
const existingConfig = await loadExistingConfig(
|
|
1150
|
+
const legacyConfigPath = await discoverWorkspaceConfigPath(workspaceRoot, options.config);
|
|
1151
|
+
const existingConfig = await loadExistingConfig(legacyConfigPath);
|
|
931
1152
|
let startingConfig = null;
|
|
932
1153
|
if (existingConfig) {
|
|
933
|
-
printConfigSummary(existingConfig,
|
|
1154
|
+
printConfigSummary(existingConfig, legacyConfigPath);
|
|
934
1155
|
if (await askYesNo(rl, "Use this existing config as the starting point?", true)) {
|
|
935
1156
|
startingConfig = existingConfig;
|
|
936
1157
|
} else {
|
|
@@ -1010,47 +1231,130 @@ async function init(options) {
|
|
|
1010
1231
|
consumers,
|
|
1011
1232
|
};
|
|
1012
1233
|
|
|
1013
|
-
|
|
1234
|
+
const initConfig = initConfigWithWorkspaceRoot(config, workspaceRoot);
|
|
1235
|
+
const configPath = durableConfigPathFor(workspaceRoot, initConfig.output.path);
|
|
1236
|
+
printConfigSummary(initConfig, configPath);
|
|
1014
1237
|
warnIfOutputIsNotWorkspaceChild(workspaceRoot, config.output.path);
|
|
1015
|
-
note("harness.config.json
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1238
|
+
note("harness.config.json will be persisted inside the generated harness so init can finish with a fully bootstrapped workspace.");
|
|
1239
|
+
printSetupTransactionPreview(initConfig, configPath);
|
|
1240
|
+
const canContinue = existingConfig
|
|
1241
|
+
? await askYesNo(rl, "Replace the generated harness config with this setup?", false)
|
|
1242
|
+
: await askYesNo(rl, "Continue with this setup?", true);
|
|
1243
|
+
if (!canContinue) {
|
|
1244
|
+
warn("Stopped before generation.");
|
|
1021
1245
|
return;
|
|
1022
1246
|
}
|
|
1023
|
-
await writeConfig(configPath, config);
|
|
1024
|
-
success(`Wrote ${configPath}`);
|
|
1025
1247
|
|
|
1026
1248
|
section("Dry-run preview");
|
|
1027
|
-
note("The initializer dry-run renders the plan
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1249
|
+
note("The initializer dry-run renders the generated harness plan before any files are written.");
|
|
1250
|
+
const renderedConfig = configContent(initConfig);
|
|
1251
|
+
const dryRunGenerated = await generateHarness(initConfig, {
|
|
1252
|
+
configPath,
|
|
1253
|
+
configContent: renderedConfig,
|
|
1254
|
+
requireExistingConsumers: true,
|
|
1255
|
+
dryRun: true,
|
|
1256
|
+
});
|
|
1031
1257
|
|
|
1032
1258
|
const apply = options.yes || await askYesNo(rl, "Generate harness now?", true);
|
|
1033
1259
|
if (!apply) {
|
|
1034
1260
|
warn("Stopped after dry-run preview.");
|
|
1035
|
-
printNextSteps(config);
|
|
1036
1261
|
return;
|
|
1037
1262
|
}
|
|
1038
1263
|
|
|
1039
|
-
const generateArgs = ["--config", configPath];
|
|
1040
|
-
if (options.force) generateArgs.push("--force");
|
|
1041
|
-
const installEntrypoints = options.installConsumerEntrypoints || await askYesNo(
|
|
1042
|
-
rl,
|
|
1043
|
-
"Install consumer entrypoint pointer files? These are thin AGENTS.md/CLAUDE.md files that route agents to the generated Structor repo.",
|
|
1044
|
-
true,
|
|
1045
|
-
);
|
|
1046
|
-
if (installEntrypoints) generateArgs.push("--install-consumer-entrypoints");
|
|
1047
|
-
|
|
1048
1264
|
section("Generate");
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1265
|
+
const harnessRoot = path.dirname(configPath);
|
|
1266
|
+
const harnessRootExisted = await exists(harnessRoot);
|
|
1267
|
+
const workspaceCreatedPaths = [];
|
|
1268
|
+
const consumerCreatedPaths = [];
|
|
1269
|
+
const harnessCreatedPaths = [];
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
await assertNoEntrypointConflicts({
|
|
1273
|
+
config: initConfig,
|
|
1274
|
+
resolvedConfig: dryRunGenerated.resolvedConfig,
|
|
1275
|
+
harnessRoot,
|
|
1276
|
+
force: options.force,
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const generated = await generateHarness(initConfig, {
|
|
1280
|
+
configPath,
|
|
1281
|
+
configContent: renderedConfig,
|
|
1282
|
+
requireExistingConsumers: true,
|
|
1283
|
+
force: options.force,
|
|
1284
|
+
dryRun: false,
|
|
1285
|
+
});
|
|
1286
|
+
harnessCreatedPaths.push(
|
|
1287
|
+
...generated.generatedFiles
|
|
1288
|
+
.filter((file) => file.action === "created")
|
|
1289
|
+
.map((file) => file.targetPath),
|
|
1290
|
+
);
|
|
1291
|
+
if (generated.manifestFile?.action === "created") {
|
|
1292
|
+
harnessCreatedPaths.push(generated.manifestFile.targetPath);
|
|
1293
|
+
}
|
|
1294
|
+
assertGeneratedScriptsReady(generated.generatedFiles, [
|
|
1295
|
+
"scripts/bootstrap-workspace.mjs",
|
|
1296
|
+
"scripts/validate-governance.mjs",
|
|
1297
|
+
"scripts/check-workspace.mjs",
|
|
1298
|
+
]);
|
|
1299
|
+
|
|
1300
|
+
const durableConfigExisted = await exists(configPath);
|
|
1301
|
+
await writeConfig(configPath, initConfig);
|
|
1302
|
+
success(`${durableConfigExisted ? "Updated" : "Wrote"} ${configPath}`);
|
|
1303
|
+
if (!durableConfigExisted) harnessCreatedPaths.push(configPath);
|
|
1304
|
+
|
|
1305
|
+
const consumerEntrypoints = await installConsumerEntrypoints(generated.resolvedConfig, {
|
|
1306
|
+
dryRun: false,
|
|
1307
|
+
force: options.force,
|
|
1308
|
+
});
|
|
1309
|
+
consumerCreatedPaths.push(
|
|
1310
|
+
...consumerEntrypoints
|
|
1311
|
+
.filter((entrypoint) => entrypoint.action === "created")
|
|
1312
|
+
.map((entrypoint) => path.join(generated.resolvedConfig.workspaceRoot, entrypoint.consumerPath, entrypoint.path)),
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
const settings = { models: initConfig.models, clientSupport: generated.resolvedConfig.support };
|
|
1316
|
+
for (const entrypoint of workspaceEntrypointsForSettings(settings)) {
|
|
1317
|
+
const targetPath = path.join(generated.resolvedConfig.workspaceRoot, entrypoint.path);
|
|
1318
|
+
if (!(await exists(targetPath))) workspaceCreatedPaths.push(targetPath);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
section("Workspace bootstrap");
|
|
1322
|
+
await runGeneratedNodeScript({
|
|
1323
|
+
harnessRoot,
|
|
1324
|
+
relativeScriptPath: "scripts/bootstrap-workspace.mjs",
|
|
1325
|
+
args: options.force ? ["--force"] : [],
|
|
1326
|
+
failureLabel: "Workspace bootstrap failed.",
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
section("Completion gates");
|
|
1330
|
+
await runGeneratedNodeScript({
|
|
1331
|
+
harnessRoot,
|
|
1332
|
+
relativeScriptPath: "scripts/validate-governance.mjs",
|
|
1333
|
+
failureLabel: "Generated governance validation failed.",
|
|
1334
|
+
});
|
|
1335
|
+
await runGeneratedNodeScript({
|
|
1336
|
+
harnessRoot,
|
|
1337
|
+
relativeScriptPath: "scripts/check-workspace.mjs",
|
|
1338
|
+
failureLabel: "Workspace completion check failed.",
|
|
1339
|
+
});
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
await cleanupFailedInit({
|
|
1342
|
+
harnessRoot,
|
|
1343
|
+
harnessRootExisted,
|
|
1344
|
+
workspaceRoot,
|
|
1345
|
+
workspaceCreatedPaths,
|
|
1346
|
+
consumerCreatedPaths,
|
|
1347
|
+
harnessCreatedPaths,
|
|
1348
|
+
});
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1052
1352
|
success("Structor setup complete.");
|
|
1053
|
-
|
|
1353
|
+
section("Setup ready");
|
|
1354
|
+
note("No post-success bootstrap steps are required.");
|
|
1355
|
+
for (const command of initCompletionCommands(harnessRoot)) {
|
|
1356
|
+
console.log(` ${command}`);
|
|
1357
|
+
}
|
|
1054
1358
|
} finally {
|
|
1055
1359
|
rl.close();
|
|
1056
1360
|
}
|
package/docs/INIT.md
CHANGED
|
@@ -33,10 +33,12 @@ The default first-run path is:
|
|
|
33
33
|
1. Run `npx @structor-dev/cli init` from the parent workspace folder.
|
|
34
34
|
2. Confirm the workspace, project name, generated harness path, agent clients,
|
|
35
35
|
and consumer repos.
|
|
36
|
-
3.
|
|
37
|
-
|
|
36
|
+
3. Review the setup summary, including the durable harness-local
|
|
37
|
+
`harness.config.json` path, entrypoint writes, and completion gates.
|
|
38
|
+
4. Review the dry-run preview of the generated harness plan.
|
|
38
39
|
5. Confirm generation only if the preview is correct.
|
|
39
|
-
6.
|
|
40
|
+
6. Let Structor install or verify consumer and workspace entrypoints, then run
|
|
41
|
+
generated governance and workspace completion gates before success.
|
|
40
42
|
|
|
41
43
|
During development from this repository, the equivalent local command is
|
|
42
44
|
`npm run init -- --workspace ..`.
|
|
@@ -59,13 +61,16 @@ service; generated files remain local until you review and commit them.
|
|
|
59
61
|
|
|
60
62
|
Only after confirmation, it can write:
|
|
61
63
|
|
|
62
|
-
- `harness.config.json`
|
|
64
|
+
- `harness.config.json` inside the generated harness.
|
|
63
65
|
- A generated Structor repo at the configured `output.path`.
|
|
64
|
-
-
|
|
65
|
-
`.claude/CLAUDE.md
|
|
66
|
+
- Required consumer entrypoint pointer files: `AGENTS.md`, `CLAUDE.md`, and
|
|
67
|
+
`.claude/CLAUDE.md` when the selected model support enables them.
|
|
68
|
+
- Required workspace entrypoint pointer files owned by the generated harness
|
|
69
|
+
bootstrap contract.
|
|
66
70
|
|
|
67
|
-
Existing generated harness files and
|
|
68
|
-
|
|
71
|
+
Existing generated harness files and known Structor-managed pointer surfaces are
|
|
72
|
+
updated only when they already match the expected content or the user passes
|
|
73
|
+
`--force`. Conflicting user-owned or stale pointer files block setup.
|
|
69
74
|
|
|
70
75
|
Consumer entrypoints are thin pointer files. They route Codex and Claude Code
|
|
71
76
|
back to the generated harness; they are not copies of the canonical harness
|
|
@@ -118,14 +123,17 @@ implementation.
|
|
|
118
123
|
|
|
119
124
|
`harness.config.json` is Structor's project-specific input file. It records:
|
|
120
125
|
|
|
126
|
+
- workspace root semantics for workspace-relative topology paths when the
|
|
127
|
+
config lives inside the generated harness
|
|
121
128
|
- project name, slug, and generated repo name
|
|
122
129
|
- output path
|
|
123
130
|
- Codex and Claude support flags
|
|
124
131
|
- consumer repo paths, purposes, and validation commands
|
|
125
132
|
|
|
126
|
-
Consumer repo paths
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
Consumer repo paths and the durable init `output.path` remain workspace-relative
|
|
134
|
+
even when the config is stored inside the generated harness. The generator
|
|
135
|
+
rejects absolute consumer paths, `..` traversal, symlinked consumer paths, and
|
|
136
|
+
entrypoint writes to directories that do not look like repositories.
|
|
129
137
|
|
|
130
138
|
`structor generate --config harness.config.json` uses this file to render the
|
|
131
139
|
generated harness deterministically.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ADR 0002: Store Harness Configuration In Generated Harness
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`structor init` used to persist `harness.config.json` at the workspace root,
|
|
10
|
+
generate the harness, and then print manual bootstrap commands. That left two
|
|
11
|
+
problems:
|
|
12
|
+
|
|
13
|
+
- setup could report success before workspace entrypoints were actually
|
|
14
|
+
installed and verified
|
|
15
|
+
- a loose workspace-root config was fragile when the parent workspace was not a
|
|
16
|
+
repository and the generated harness was the durable artifact users kept
|
|
17
|
+
|
|
18
|
+
We also needed a way for paths such as `output.path` and consumer repo paths to
|
|
19
|
+
stay workspace-relative after moving the durable config into the generated
|
|
20
|
+
harness.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
- New `structor init` runs persist `harness.config.json` inside the generated
|
|
25
|
+
harness repo.
|
|
26
|
+
- Init-authored configs include explicit `workspace.root` semantics so
|
|
27
|
+
workspace-relative topology paths keep resolving correctly even though the
|
|
28
|
+
config file now lives inside the harness repo.
|
|
29
|
+
- `structor init` does not print `Structor setup complete.` until consumer
|
|
30
|
+
entrypoints, workspace entrypoints, `node scripts/validate-governance.mjs`,
|
|
31
|
+
and `node scripts/check-workspace.mjs` have completed successfully.
|
|
32
|
+
- Existing manual `structor generate --config <path>` flows remain supported for
|
|
33
|
+
workspace-root configs that do not declare `workspace.root`.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
- The generated harness becomes the durable home for both policy and the init
|
|
38
|
+
recipe that produced it.
|
|
39
|
+
- Failed init attempts can clean up files created during that same setup
|
|
40
|
+
transaction without depending on a loose workspace-root config.
|
|
41
|
+
- Tooling that looks for a config from the workspace should prefer the explicit
|
|
42
|
+
harness-local config when it is the only unambiguous match.
|
package/package.json
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
"required": ["project", "output", "models", "consumers"],
|
|
7
7
|
"properties": {
|
|
8
8
|
"$schema": { "type": "string" },
|
|
9
|
+
"workspace": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"required": ["root"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"root": { "type": "string", "minLength": 1 }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
9
17
|
"project": {
|
|
10
18
|
"type": "object",
|
|
11
19
|
"additionalProperties": false,
|
package/scripts/init-harness.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import path from "node:path";
|
|
@@ -109,6 +109,26 @@ async function collectTemplateFiles() {
|
|
|
109
109
|
return files.sort();
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async function collectExistingFiles(basePath) {
|
|
113
|
+
const files = new Set();
|
|
114
|
+
if (!(await exists(basePath))) return files;
|
|
115
|
+
|
|
116
|
+
async function walk(currentPath) {
|
|
117
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const absolute = path.join(currentPath, entry.name);
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
await walk(absolute);
|
|
122
|
+
} else if (entry.isFile()) {
|
|
123
|
+
files.add(absolute);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await walk(basePath);
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
async function generatedScriptHashes(templateFiles, config, values) {
|
|
113
133
|
const hashes = {};
|
|
114
134
|
const trustedScriptTemplates = new Set(trustedGeneratedScriptTemplatesForSettings(config));
|
|
@@ -137,6 +157,11 @@ export async function writeRenderedFile(sourceRelative, targetRoot, values, opti
|
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
if ((await exists(targetPath)) && !options.force) {
|
|
160
|
+
const actual = await readFile(targetPath, "utf8");
|
|
161
|
+
if (actual === content) {
|
|
162
|
+
console.log(`verified existing ${targetPath}`);
|
|
163
|
+
return { action: "verified", rendered: true, targetPath, targetRelative };
|
|
164
|
+
}
|
|
140
165
|
console.log(`skipped existing ${targetPath}`);
|
|
141
166
|
return { action: "skipped", rendered: false, targetPath, targetRelative };
|
|
142
167
|
}
|
|
@@ -198,9 +223,10 @@ export async function installConsumerEntrypoints(resolvedConfig, options) {
|
|
|
198
223
|
label: `Consumer entrypoint ${targetRelative}`,
|
|
199
224
|
});
|
|
200
225
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
226
|
+
const existed = await exists(targetPath);
|
|
201
227
|
await writeFile(targetPath, content);
|
|
202
228
|
console.log(`wrote consumer entrypoint ${targetPath}`);
|
|
203
|
-
records.push({ ...record, action: "wrote", rendered: true });
|
|
229
|
+
records.push({ ...record, action: existed ? "wrote" : "created", rendered: true });
|
|
204
230
|
}
|
|
205
231
|
}
|
|
206
232
|
|
|
@@ -255,9 +281,16 @@ async function writeGenerationManifest({
|
|
|
255
281
|
rootPath: outputRoot,
|
|
256
282
|
label: "Generation manifest .structor/manifest.json",
|
|
257
283
|
});
|
|
284
|
+
const existed = await exists(manifestPath);
|
|
258
285
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
259
286
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
260
287
|
console.log(`wrote ${manifestPath}`);
|
|
288
|
+
return {
|
|
289
|
+
action: existed ? "wrote" : "created",
|
|
290
|
+
rendered: true,
|
|
291
|
+
targetPath: manifestPath,
|
|
292
|
+
targetRelative: path.relative(outputRoot, manifestPath).replaceAll(path.sep, "/"),
|
|
293
|
+
};
|
|
261
294
|
}
|
|
262
295
|
|
|
263
296
|
export async function generateHarness(config, {
|
|
@@ -268,6 +301,7 @@ export async function generateHarness(config, {
|
|
|
268
301
|
dryRun = false,
|
|
269
302
|
force = false,
|
|
270
303
|
installConsumerEntrypoints: shouldInstallConsumerEntrypoints = false,
|
|
304
|
+
requireExistingConsumers = false,
|
|
271
305
|
allowAbsoluteOutput = false,
|
|
272
306
|
allowTemplateRepoConsumer = false,
|
|
273
307
|
} = {}) {
|
|
@@ -279,11 +313,15 @@ export async function generateHarness(config, {
|
|
|
279
313
|
configDir,
|
|
280
314
|
outputPath,
|
|
281
315
|
allowAbsoluteOutput,
|
|
282
|
-
requireExistingConsumers: shouldInstallConsumerEntrypoints,
|
|
316
|
+
requireExistingConsumers: requireExistingConsumers || shouldInstallConsumerEntrypoints,
|
|
283
317
|
allowTemplateRepoConsumer,
|
|
284
318
|
});
|
|
285
319
|
const { outputRoot, support } = resolvedConfig;
|
|
286
|
-
const
|
|
320
|
+
const templateWorkspaceRoot = config.workspace?.root
|
|
321
|
+
? path.resolve(configDir, config.workspace.root)
|
|
322
|
+
: path.resolve(configDir);
|
|
323
|
+
const templateOutputRoot = path.resolve(templateWorkspaceRoot, outputPath);
|
|
324
|
+
const values = harnessTemplateValues(config, support, resolvedConfig.consumers, templateOutputRoot, templateWorkspaceRoot);
|
|
287
325
|
values.GENERATED_HARNESS_CONTRACT_MODULE = await readFile(
|
|
288
326
|
path.join(repoRoot, "scripts/generated-harness-contract.mjs"),
|
|
289
327
|
"utf8",
|
|
@@ -304,11 +342,36 @@ export async function generateHarness(config, {
|
|
|
304
342
|
}
|
|
305
343
|
}
|
|
306
344
|
|
|
345
|
+
const htmlViewsRoot = path.join(outputRoot, "ai", "views");
|
|
346
|
+
const htmlViewFilesBefore = renderedHtmlViewsScript
|
|
347
|
+
? await collectExistingFiles(htmlViewsRoot)
|
|
348
|
+
: new Set();
|
|
307
349
|
if (!dryRun && renderedHtmlViewsScript) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
let htmlViewFilesAfter;
|
|
351
|
+
try {
|
|
352
|
+
execFileSync(process.execPath, [path.join(outputRoot, "scripts/generate-html-views.mjs")], {
|
|
353
|
+
cwd: outputRoot,
|
|
354
|
+
stdio: "inherit",
|
|
355
|
+
});
|
|
356
|
+
htmlViewFilesAfter = await collectExistingFiles(htmlViewsRoot);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
htmlViewFilesAfter = await collectExistingFiles(htmlViewsRoot);
|
|
359
|
+
for (const targetPath of htmlViewFilesAfter) {
|
|
360
|
+
if (!htmlViewFilesBefore.has(targetPath)) {
|
|
361
|
+
await rm(targetPath, { force: true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
for (const targetPath of htmlViewFilesAfter) {
|
|
367
|
+
if (htmlViewFilesBefore.has(targetPath)) continue;
|
|
368
|
+
generatedFiles.push({
|
|
369
|
+
action: "created",
|
|
370
|
+
rendered: true,
|
|
371
|
+
targetPath,
|
|
372
|
+
targetRelative: path.relative(outputRoot, targetPath).replaceAll(path.sep, "/"),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
312
375
|
} else if (!dryRun) {
|
|
313
376
|
console.log("skipped HTML view generation because scripts/generate-html-views.mjs was not freshly rendered");
|
|
314
377
|
}
|
|
@@ -317,8 +380,8 @@ export async function generateHarness(config, {
|
|
|
317
380
|
? await installConsumerEntrypoints(resolvedConfig, { dryRun, force, config: configPath })
|
|
318
381
|
: [];
|
|
319
382
|
|
|
320
|
-
|
|
321
|
-
await writeGenerationManifest({
|
|
383
|
+
const manifestFile = !dryRun
|
|
384
|
+
? await writeGenerationManifest({
|
|
322
385
|
config,
|
|
323
386
|
configContent: manifestConfigContent,
|
|
324
387
|
configPath,
|
|
@@ -327,10 +390,15 @@ export async function generateHarness(config, {
|
|
|
327
390
|
outputRoot,
|
|
328
391
|
resolvedConfig,
|
|
329
392
|
support,
|
|
330
|
-
})
|
|
331
|
-
|
|
393
|
+
})
|
|
394
|
+
: null;
|
|
332
395
|
|
|
333
|
-
return
|
|
396
|
+
return {
|
|
397
|
+
resolvedConfig,
|
|
398
|
+
generatedFiles,
|
|
399
|
+
consumerEntrypoints,
|
|
400
|
+
manifestFile,
|
|
401
|
+
};
|
|
334
402
|
}
|
|
335
403
|
|
|
336
404
|
async function main() {
|
package/scripts/lib.mjs
CHANGED
|
@@ -102,6 +102,15 @@ export function workspaceRootForConfig(configDir, templateRepoRoot = repoRoot) {
|
|
|
102
102
|
: resolvedConfigDir;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function resolveWorkspaceRoot(config, configDir, templateRepoRoot = repoRoot) {
|
|
106
|
+
const configuredRoot = config.workspace?.root;
|
|
107
|
+
if (typeof configuredRoot !== "string" || configuredRoot.trim() === "") {
|
|
108
|
+
return workspaceRootForConfig(configDir, templateRepoRoot);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return path.resolve(configDir, configuredRoot);
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
export async function canonicalPathForWrite(targetPath) {
|
|
106
115
|
let currentPath = path.resolve(targetPath);
|
|
107
116
|
const missingSegments = [];
|
|
@@ -355,10 +364,15 @@ export async function resolveHarnessConfig(config, {
|
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
const resolvedConfigDir = path.resolve(configDir);
|
|
358
|
-
const workspaceRoot =
|
|
359
|
-
const
|
|
367
|
+
const workspaceRoot = resolveWorkspaceRoot(config, resolvedConfigDir, templateRepoRoot);
|
|
368
|
+
const topologyRoot = config.workspace?.root ? workspaceRoot : resolvedConfigDir;
|
|
369
|
+
const requestedOutputRoot = path.resolve(topologyRoot, outputPath);
|
|
360
370
|
const consumerRoots = [];
|
|
361
371
|
|
|
372
|
+
if (!isSameOrInsidePath(resolvedConfigDir, workspaceRoot)) {
|
|
373
|
+
errors.push(`${label}: config path ${resolvedConfigDir} must stay inside the workspace root ${workspaceRoot}.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
362
376
|
for (const consumer of config.consumers) {
|
|
363
377
|
try {
|
|
364
378
|
consumerRoots.push(assertSafeConsumerPath({
|
|
@@ -57,13 +57,12 @@ function consumerNames(consumers) {
|
|
|
57
57
|
return consumers.map((consumer) => rawSlug(consumer.name, "consumer.name"));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function consumerConfig(resolvedConsumers,
|
|
61
|
-
|
|
62
|
-
return resolvedConsumers.map(({ config: consumer, root: consumerRoot }) => {
|
|
60
|
+
function consumerConfig(resolvedConsumers, workspaceRoot) {
|
|
61
|
+
return resolvedConsumers.map(({ config: consumer, requestedRoot, root: consumerRoot }) => {
|
|
63
62
|
return {
|
|
64
63
|
...consumer,
|
|
65
64
|
name: rawSlug(consumer.name, "consumer.name"),
|
|
66
|
-
workspacePath: path.relative(
|
|
65
|
+
workspacePath: path.relative(workspaceRoot, requestedRoot ?? consumerRoot).replaceAll(path.sep, "/") || ".",
|
|
67
66
|
};
|
|
68
67
|
});
|
|
69
68
|
}
|
|
@@ -72,16 +71,20 @@ export function renderedGeneratedScriptHashes(hashes) {
|
|
|
72
71
|
return jsonLiteral(hashes);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
export function harnessTemplateValues(config, support, resolvedConsumers, outputRoot) {
|
|
74
|
+
export function harnessTemplateValues(config, support, resolvedConsumers, outputRoot, workspaceRoot = path.dirname(outputRoot)) {
|
|
75
|
+
const workspaceHarnessPath = path.relative(workspaceRoot, outputRoot).replaceAll(path.sep, "/") || ".";
|
|
76
|
+
|
|
76
77
|
return {
|
|
77
78
|
PROJECT_NAME: markdownText(config.project.name),
|
|
78
79
|
PROJECT_NAME_CODE: markdownCodeSpan(config.project.name),
|
|
79
80
|
PROJECT_NAME_JSON: javascriptLiteral(config.project.name),
|
|
80
81
|
PROJECT_SLUG: rawSlug(config.project.slug, "project.slug"),
|
|
81
82
|
HARNESS_REPO_NAME: rawSlug(config.project.harnessRepoName, "project.harnessRepoName"),
|
|
83
|
+
WORKSPACE_HARNESS_PATH: workspaceHarnessPath.startsWith(".") ? workspaceHarnessPath : `./${workspaceHarnessPath}`,
|
|
84
|
+
WORKSPACE_ROOT_FROM_HARNESS_JSON: javascriptLiteral(path.relative(outputRoot, workspaceRoot).replaceAll(path.sep, "/") || "."),
|
|
82
85
|
CONSUMER_REPOS_LIST: consumerList(config.consumers),
|
|
83
86
|
CONSUMER_REPO_NAMES_JSON: javascriptLiteral(consumerNames(config.consumers)),
|
|
84
|
-
CONSUMER_CONFIG_JSON: jsonLiteral(consumerConfig(resolvedConsumers,
|
|
87
|
+
CONSUMER_CONFIG_JSON: jsonLiteral(consumerConfig(resolvedConsumers, workspaceRoot)),
|
|
85
88
|
PRIMARY_CONSUMER_NAME: rawSlug(config.consumers[0].name, "consumer.name"),
|
|
86
89
|
MODEL_OPENAI_ENABLED: javascriptBoolean(config.models.openai),
|
|
87
90
|
MODEL_ANTHROPIC_ENABLED: javascriptBoolean(config.models.anthropic),
|
|
@@ -75,7 +75,7 @@ async function main() {
|
|
|
75
75
|
console.log(`Workspace: ${workspaceRoot}`);
|
|
76
76
|
console.log(`Preset: ${presetConfigPath}`);
|
|
77
77
|
|
|
78
|
-
const resolvedConfig = await generateHarness(config, {
|
|
78
|
+
const { resolvedConfig } = await generateHarness(config, {
|
|
79
79
|
configPath: presetConfigPath,
|
|
80
80
|
configDir: workspaceRoot,
|
|
81
81
|
dryRun: options.dryRun,
|
|
@@ -8,7 +8,7 @@ import { workspaceEntrypointsForSettings } from "./generated-harness-contract.mj
|
|
|
8
8
|
import { assertSafeWriteTarget, exists } from "./lib/path-safety.mjs";
|
|
9
9
|
|
|
10
10
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const workspaceRoot = path.resolve(repoRoot,
|
|
11
|
+
const workspaceRoot = path.resolve(repoRoot, {{WORKSPACE_ROOT_FROM_HARNESS_JSON}});
|
|
12
12
|
const consumers = {{CONSUMER_CONFIG_JSON}};
|
|
13
13
|
const models = {
|
|
14
14
|
openai: {{MODEL_OPENAI_ENABLED}},
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./generated-harness-contract.mjs";
|
|
14
14
|
|
|
15
15
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
-
const workspaceRoot = path.resolve(repoRoot,
|
|
16
|
+
const workspaceRoot = path.resolve(repoRoot, {{WORKSPACE_ROOT_FROM_HARNESS_JSON}});
|
|
17
17
|
const harnessRepoName = "{{HARNESS_REPO_NAME}}";
|
|
18
18
|
const consumers = {{CONSUMER_CONFIG_JSON}};
|
|
19
19
|
const models = {
|
|
@@ -4,14 +4,13 @@ This workspace is governed by the {{PROJECT_NAME}} engineering harness.
|
|
|
4
4
|
|
|
5
5
|
Read the harness first:
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
7
|
+
1. `{{WORKSPACE_HARNESS_PATH}}/AGENTS.md`
|
|
8
|
+
2. `{{WORKSPACE_HARNESS_PATH}}/ai/AGENTS.md`
|
|
9
|
+
3. `{{WORKSPACE_HARNESS_PATH}}/ai/HUB.md`
|
|
10
|
+
4. `{{WORKSPACE_HARNESS_PATH}}/ai/context.md`
|
|
11
11
|
|
|
12
12
|
Consumer repositories:
|
|
13
13
|
|
|
14
14
|
{{CONSUMER_REPOS_LIST}}
|
|
15
15
|
|
|
16
16
|
Keep this file short. Canonical policy belongs in the harness repo.
|
|
17
|
-
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This workspace is governed by the {{PROJECT_NAME}} engineering harness.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
@{{WORKSPACE_HARNESS_PATH}}/ai/context.md
|
|
6
6
|
|
|
7
7
|
Read the harness first:
|
|
8
8
|
|
|
9
|
-
1.
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
9
|
+
1. `{{WORKSPACE_HARNESS_PATH}}/CLAUDE.md`
|
|
10
|
+
2. `{{WORKSPACE_HARNESS_PATH}}/ai/AGENTS.md`
|
|
11
|
+
3. `{{WORKSPACE_HARNESS_PATH}}/ai/HUB.md`
|
|
12
|
+
4. `{{WORKSPACE_HARNESS_PATH}}/ai/context.md`
|
|
13
13
|
|
|
14
14
|
Consumer repositories:
|
|
15
15
|
|