@orkestrel/scaffold 0.0.63 → 0.0.64

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 (52) hide show
  1. package/README.md +18 -103
  2. package/dist/bin/main.js +95 -27
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/AGENTS.md +2 -2
  5. package/dist/host/CLAUDE.md +6 -0
  6. package/dist/host/agents/orchestration.md +23 -15
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +6 -6
  9. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +4 -4
  10. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +5 -5
  11. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +15 -15
  13. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +43 -17
  14. package/dist/host/agents/skills/orkestrel-publish/references/window.md +41 -16
  15. package/dist/host/claude/agents/orkestrel.md +56 -56
  16. package/dist/host/claude/agents/reviewer.md +13 -0
  17. package/dist/host/claude/rules/architecture.md +51 -45
  18. package/dist/host/claude/rules/documentation.md +18 -1
  19. package/dist/host/claude/rules/portability.md +2 -0
  20. package/dist/host/claude/rules/quality.md +1 -1
  21. package/dist/host/claude/rules/tests.md +12 -11
  22. package/dist/host/claude/rules/typescript.md +5 -0
  23. package/dist/host/claude/rules/workspace.md +23 -18
  24. package/dist/host/claude/rules/writing.md +4 -0
  25. package/dist/host/codex/agents/orkestrel.toml +3 -3
  26. package/dist/host/codex/agents/reviewer.toml +4 -2
  27. package/dist/host/configs/helpers.ts +311 -2
  28. package/dist/host/configs/policy.ts +1100 -51
  29. package/dist/host/dotfiles/oxlintrc.json +72 -1
  30. package/dist/host/guides/guide.md +749 -222
  31. package/dist/host/guides/scaffold.md +472 -378
  32. package/dist/host/manifest.json +34 -33
  33. package/dist/host/scripts/codex.sh +0 -0
  34. package/dist/host/scripts/cursor.sh +0 -0
  35. package/dist/host/scripts/deps.sh +0 -0
  36. package/dist/host/scripts/ollama.sh +0 -0
  37. package/dist/host/tests/config.test.ts +1200 -16
  38. package/dist/host/tests/policy.test.ts +157 -173
  39. package/dist/host/tests/setupPolicy.ts +522 -1007
  40. package/dist/src/core/index.cjs +371 -277
  41. package/dist/src/core/index.cjs.map +1 -1
  42. package/dist/src/core/index.d.cts +130 -120
  43. package/dist/src/core/index.d.ts +130 -120
  44. package/dist/src/core/index.js +371 -276
  45. package/dist/src/core/index.js.map +1 -1
  46. package/dist/src/server/index.cjs +28 -21
  47. package/dist/src/server/index.cjs.map +1 -1
  48. package/dist/src/server/index.d.cts +38 -33
  49. package/dist/src/server/index.d.ts +38 -33
  50. package/dist/src/server/index.js +28 -21
  51. package/dist/src/server/index.js.map +1 -1
  52. package/package.json +15 -16
package/README.md CHANGED
@@ -1,26 +1,7 @@
1
1
  # @orkestrel/scaffold
2
2
 
3
- Compile a workspace specification into an ordered list of files, compare that list to a real
4
- directory, and write the difference.
5
-
6
- Every `@orkestrel` repository shares one toolchain, one set of agent instructions, and one set of
7
- root dotfiles. Scaffold ships that shared set as data inside the package and gives it verbs: create
8
- a workspace from it, report how a workspace differs from it, and write the difference back.
9
-
10
- The set splits by how a repository meets it. Each target carries its own copy of the paths it
11
- selects from the vendored set — its licence, its harness permission file, its session-start hooks,
12
- its policy register, its policy proof, its policy plugin, its configuration leaf and its proof, its
13
- root dotfiles, and the guide mirrors it starts from, never its own guide — and the verbs write
14
- them and compare them. A bench probe hook reports whether a bench CLI resolves, and the dependency
15
- hook installs the lockfile's closure in a remote session; what wires a bench stays in the canon,
16
- and a session reads it at its primary root. The instruction canon — the coding and
17
- orchestration contracts, the rules, the skills, the templates, the transport contracts, the agent
18
- roles, the bench configuration, and the MCP registrations — is published for reading instead, from a
19
- scaffold checkout sitting beside the repository, or from
20
- `node_modules/@orkestrel/scaffold/dist/host/` in the installed package. Every target carries the
21
- `AGENTS.md` and `CLAUDE.md` pointers that name where to read it, and the
22
- `.claude/agents/orkestrel.md` catalog file the `catalog` verb rewrites. Anything else a target holds
23
- at a canon path is a superseded copy, and `overwrite` deletes it.
3
+ > A compiler that turns a workspace specification into an ordered list of files, compares that list
4
+ > to a real directory, and writes the difference.
24
5
 
25
6
  ## Install
26
7
 
@@ -36,73 +17,25 @@ npx @orkestrel/scaffold --help
36
17
 
37
18
  ## Verbs
38
19
 
39
- Authority is the verb's: every verb except `audit` writes when it is typed, and no
40
- option grants a write. Exit codes are `0` clean, `1` drift or failure, and `2` usage error.
20
+ Authority is the verb's: every verb except `audit` writes when it is typed, and no option grants a
21
+ write. The guide's [Command line](guides/scaffold.md#command-line) section specifies each verb, its
22
+ options, its defaults, and the exit codes.
41
23
 
42
- `--target <path>` points any verb at another directory; the working directory is the default.
43
- `--json` replaces the report with one machine-readable value on standard output.
44
-
45
- ### `new` — scaffold a workspace
46
-
47
- ```sh
48
- npx scaffold new router --src core,server
49
- ```
50
-
51
- Writes a complete workspace into `./router`: its manifest, its build configuration, empty barrels
52
- for each selected environment, its tests, its documentation, the `AGENTS.md` and `CLAUDE.md`
53
- pointers, and every vendored file. `--app` selects private application environments on an
54
- independent axis, and `--deps` names `@orkestrel/*` runtime dependencies, each pinned to the
55
- registry's latest release. `--bin` adds the command-line entry, its test, and its scoped build
56
- configuration.
57
-
58
- ### `audit` — report how a target compares to its plan
59
-
60
- ```sh
61
- npx scaffold audit --groups configs,orchestration
62
- ```
63
-
64
- Writes nothing. Reports one row per path that differs, and exits `1` when anything does. Omit
65
- `--groups` to cover every group.
66
-
67
- ### `repair` — write back what drifted
68
-
69
- ```sh
70
- npx scaffold repair
71
- ```
72
-
73
- Restores each planned path the target is missing or has let drift, then re-audits. A file the
74
- workspace owns — its source, its own proofs, its README — is written once at creation and is never
75
- rewritten here. Not everything is owned that way: `tests/distribution.test.ts` is restored when it
76
- is absent and left alone when the workspace has replaced it, and the manifest's script region is
77
- rewritten when its chain is the one scaffold generated and refused without a write when it is not.
78
-
79
- ### `catalog` — refresh the package table and the guide mirrors
80
-
81
- ```sh
82
- npx scaffold catalog --all
83
- ```
84
-
85
- Reads the organization's published package list, rewrites the marker-bounded table in
86
- `.claude/agents/orkestrel.md`, and fetches each package's guide into its local mirror. Without
87
- `--all` it fetches only the guides the target declares as dependencies.
88
-
89
- ### `overwrite` — repair, catalog, delete, and re-pin
90
-
91
- ```sh
92
- npx scaffold overwrite --dirty
93
- ```
94
-
95
- Everything `repair` and `catalog` do, plus the steps only this verb carries: it deletes tracked
96
- files the plan does not own, and it rewrites the `@orkestrel/*` ranges in the manifest to the
97
- registry's latest releases. The deletion covers a stray beneath a vendored directory and a superseded
98
- instruction copy alike, so one run repairs the pointers and sweeps the canon paths a release moved.
99
- It needs a git repository, and it refuses a tree carrying uncommitted changes unless `--dirty` waives
100
- that refusal.
24
+ - `new` writes a whole workspace into a target that holds nothing the plan would collide with.
25
+ - `audit` writes nothing, and reports one row per path that differs.
26
+ - `repair` writes each planned path the target is missing or has let drift, and the manifest's range
27
+ and script regions.
28
+ - `catalog` rewrites the package table in the target's catalog agent file, and refetches the guide
29
+ mirrors.
30
+ - `overwrite` does everything `repair` and `catalog` do, then deletes what the plan does not own and
31
+ re-declares the dependency ranges.
101
32
 
102
33
  ## Library
103
34
 
104
35
  The entry points split by host. `@orkestrel/scaffold` is host-independent: it compiles, gates, and
105
- compares.
36
+ compares. `@orkestrel/scaffold/server` is Node-only and holds everything that touches the filesystem
37
+ or the network: `Materializer` writes a plan into a target, `Upstream` reads the registry and the
38
+ guide host, and `WriteTransaction` stages and swaps a set of files with rollback.
106
39
 
107
40
  ```ts
108
41
  import { Compiler, createBlueprint } from '@orkestrel/scaffold'
@@ -115,26 +48,8 @@ scaffolding.questions // the advice the compile could not settle
115
48
  compiler.destroy()
116
49
  ```
117
50
 
118
- A plan says the workspace can be built. It does not decide whether to create it: a caller
119
- creating a fresh workspace refuses on any question beside the plan, blocking or not, exactly as
120
- `new` does. [`guides/scaffold.md`](guides/scaffold.md) states that rule and what it covers.
121
-
122
- `@orkestrel/scaffold/server` is Node-only and holds everything that touches the filesystem or the
123
- network: `Materializer` writes a plan into a target, `Upstream` reads the registry and
124
- the guide host, and `WriteTransaction` stages and swaps a set of files with rollback.
125
-
126
- ```ts
127
- import type { Plan } from '@orkestrel/scaffold'
128
- import { Materializer } from '@orkestrel/scaffold/server'
129
-
130
- declare const plan: Plan
131
-
132
- const materializer = new Materializer()
133
- const result = materializer.materialize(plan, './packages/router')
134
-
135
- result.written // every path created
136
- materializer.destroy()
137
- ```
51
+ A plan says the workspace can be built. It does not decide whether to create it: a caller creating a
52
+ fresh workspace refuses on any question beside the plan, blocking or not, exactly as `new` does.
138
53
 
139
54
  ## Guide
140
55
 
package/dist/bin/main.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { align, renderTable, strip, stripControls, width } from "@orkestrel/console";
3
3
  import { attempt, isError, isRecord, isString, parseJSON } from "@orkestrel/contract";
4
- import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isFloorPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
5
- import { Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readSnapshot, resolveContainedPath } from "../src/server/index.js";
4
+ import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isFloorPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
5
+ import { MAX_INVENTORY_PATHS, Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readSnapshot, resolveContainedPath } from "../src/server/index.js";
6
6
  import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
7
- import { executeSync } from "@orkestrel/process/server";
7
+ import { createSession } from "@orkestrel/process/server";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/bin/constants.ts
10
10
  /**
@@ -1033,40 +1033,108 @@ function catalogToNames(target) {
1033
1033
  return names;
1034
1034
  }
1035
1035
  /**
1036
+ * Collects the complete NUL-delimited records from one raw git output chunk.
1037
+ *
1038
+ * @param records - The complete records retained in arrival order.
1039
+ * @param pending - The mutable cell carrying the unfinished record between chunks.
1040
+ * @param decoder - The streaming UTF-8 decoder shared by the query.
1041
+ * @param controller - The query controller aborted on a bound refusal.
1042
+ * @param target - The repository the query addresses.
1043
+ * @param chunk - The next owned output bytes.
1044
+ * @returns Nothing.
1045
+ */
1046
+ function collectGitRecords(records, pending, decoder, controller, target, chunk) {
1047
+ if (controller.signal.aborted) return;
1048
+ const parts = `${pending[0] ?? ""}${decoder.decode(chunk, { stream: true })}`.split("\0");
1049
+ pending[0] = parts.pop() ?? "";
1050
+ for (const record of parts) {
1051
+ if (record.length === 0) continue;
1052
+ if (records.length >= MAX_INVENTORY_PATHS) {
1053
+ refuseGitRecords(controller, target, `The git query for target at ${target} exceeded the working-tree inventory limit.`);
1054
+ return;
1055
+ }
1056
+ records.push(record);
1057
+ }
1058
+ if ((pending[0] ?? "").length > MAX_PATH_LENGTH + 3) refuseGitRecords(controller, target, `The git query for target at ${target} returned an unfinished record beyond the supported path limit.`);
1059
+ }
1060
+ /**
1061
+ * Aborts a git query with its first target refusal.
1062
+ *
1063
+ * @param controller - The query controller whose reason retains the refusal.
1064
+ * @param target - The repository the query addresses.
1065
+ * @param message - The refusal message.
1066
+ * @returns Nothing.
1067
+ */
1068
+ function refuseGitRecords(controller, target, message) {
1069
+ if (!controller.signal.aborted) controller.abort(new ScaffoldError("TARGET", message, { target }));
1070
+ }
1071
+ /**
1036
1072
  * Reads one git query as its NUL-separated records.
1037
1073
  *
1038
1074
  * @param target - The repository the query runs in.
1039
1075
  * @param args - The git arguments, without the program name.
1040
1076
  * @returns The non-empty records git wrote, in git's own order.
1041
- * @throws `ScaffoldError('TARGET', …)` when the query fails, which is what a
1042
- * directory that is not a git repository answers with.
1077
+ * @throws `ScaffoldError('TARGET', …)` when the query fails or its output is
1078
+ * incomplete or outside the working-tree inventory bounds.
1043
1079
  *
1044
1080
  * @remarks
1045
1081
  * Git is asked rather than reimplemented, because the tracked set and the dirty
1046
- * set are git's own answers and nothing else can give them. `executeSync`
1047
- * resolves the bare `git` name against `PATH` and `PATHEXT` on Windows, never
1048
- * through a shell, so the query runs without an extension of its own. A failed
1049
- * run resolves instead of throwing there, so git's own refusal is buffered into
1050
- * the result rather than written onto a stream nobody chose.
1082
+ * set are git's own answers and nothing else can give them. The raw session
1083
+ * streams complete NUL-delimited records without retaining the whole query as a
1084
+ * manifest-sized text buffer. Its initial hooks are installed before the eager
1085
+ * spawn, so no output or child fault can precede its observer.
1051
1086
  *
1052
1087
  * @example
1053
1088
  * ```ts
1054
1089
  * import { readGitRecords } from './helpers.js'
1055
1090
  *
1056
- * readGitRecords('./packages/router', ['ls-files', '-z']) // ['package.json', 'src/core/index.ts']
1091
+ * await readGitRecords('./packages/router', ['ls-files', '-z']) // ['package.json', 'src/core/index.ts']
1057
1092
  * ```
1058
1093
  */
1059
- function readGitRecords(target, args) {
1060
- const result = executeSync({
1061
- file: "git",
1062
- arguments: [...args]
1063
- }, {
1064
- workspace: target,
1065
- limit: MAX_MANIFEST_BYTES,
1066
- strict: false
1067
- });
1068
- if (result.failed) throw new ScaffoldError("TARGET", `The target at ${target} is not a git repository.`, { target });
1069
- return result.stdout.split("\0").filter((record) => record.length > 0);
1094
+ async function readGitRecords(target, args) {
1095
+ const records = [];
1096
+ const pending = [""];
1097
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
1098
+ const controller = new AbortController();
1099
+ let session;
1100
+ try {
1101
+ session = createSession({
1102
+ command: {
1103
+ file: "git",
1104
+ arguments: [
1105
+ "-C",
1106
+ target,
1107
+ ...args
1108
+ ]
1109
+ },
1110
+ workspace: ".",
1111
+ signal: controller.signal,
1112
+ on: {
1113
+ stdout: collectGitRecords.bind(void 0, records, pending, decoder, controller, target),
1114
+ error: refuseGitRecords.bind(void 0, controller, target, `The git query for target at ${target} failed before it completed.`)
1115
+ },
1116
+ error: refuseGitRecords.bind(void 0, controller, target, `The git query for target at ${target} failed while reading its output.`)
1117
+ });
1118
+ const exit = await session.exit;
1119
+ pending[0] = `${pending[0] ?? ""}${decoder.decode()}`;
1120
+ if (controller.signal.aborted) throw controller.signal.reason;
1121
+ if (exit.signal !== null) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended from signal ${exit.signal}.`, {
1122
+ target,
1123
+ signal: exit.signal
1124
+ });
1125
+ if (exit.code !== 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} failed with exit code ${String(exit.code)}.`, {
1126
+ target,
1127
+ code: exit.code
1128
+ });
1129
+ if (!exit.drained) throw new ScaffoldError("TARGET", `The git query for target at ${target} ended before its output drained.`, { target });
1130
+ if ((pending[0] ?? "").length > 0) throw new ScaffoldError("TARGET", `The git query for target at ${target} returned an incomplete record.`, { target });
1131
+ return records;
1132
+ } catch (error) {
1133
+ if (isScaffoldError(error)) throw error;
1134
+ throw new ScaffoldError("TARGET", `The git query for target at ${target} could not start.`, { target });
1135
+ } finally {
1136
+ if (session !== void 0) await session.destroy();
1137
+ }
1070
1138
  }
1071
1139
  /**
1072
1140
  * Reads the environments a comma-separated selection names.
@@ -1446,7 +1514,7 @@ var CLI = class {
1446
1514
  const groups = selectionToGroups(command.groups);
1447
1515
  const blueprint = this.#derive(target);
1448
1516
  this.#assertTarget(target, blueprint, groups);
1449
- const worktree = this.#worktree(target);
1517
+ const worktree = await this.#worktree(target);
1450
1518
  if (worktree.dirty.length > 0 && command.dirty !== true) throw new ScaffoldError("TARGET", `The target at ${target} carries ${String(worktree.dirty.length)} uncommitted change${worktree.dirty.length === 1 ? "" : "s"}. Commit them, or pass --dirty to waive the refusal.`, {
1451
1519
  target,
1452
1520
  dirty: worktree.dirty.length
@@ -1971,14 +2039,14 @@ var CLI = class {
1971
2039
  if (manifest === void 0) throw new ScaffoldError("TARGET", `The target at ${target} carries no readable manifest.`, { target });
1972
2040
  return manifest;
1973
2041
  }
1974
- #worktree(target) {
1975
- const tracked = readGitRecords(target, ["ls-files", "-z"]);
1976
- const dirty = readGitRecords(target, [
2042
+ async #worktree(target) {
2043
+ const tracked = await readGitRecords(target, ["ls-files", "-z"]);
2044
+ const dirty = (await readGitRecords(target, [
1977
2045
  "status",
1978
2046
  "--porcelain=v1",
1979
2047
  "--untracked-files=all",
1980
2048
  "-z"
1981
- ]).map((record) => record.length > 3 && record[2] === " " ? record.slice(3) : record);
2049
+ ])).map((record) => record.length > 3 && record[2] === " " ? record.slice(3) : record);
1982
2050
  const state = {
1983
2051
  tracked,
1984
2052
  dirty