@orkestrel/scaffold 0.0.21 → 0.0.22

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 (68) hide show
  1. package/dist/bin/scaffold.js +371 -140
  2. package/dist/bin/scaffold.js.map +1 -1
  3. package/dist/host/AGENTS.md +61 -42
  4. package/dist/host/CLAUDE.md +39 -408
  5. package/dist/host/agents/orchestration.md +454 -0
  6. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +25 -7
  7. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +2 -2
  8. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +42 -31
  9. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  10. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +3 -3
  11. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +2 -2
  12. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +167 -0
  13. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +98 -0
  14. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +148 -0
  15. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +4 -2
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +5 -5
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +2 -0
  18. package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +11 -6
  19. package/dist/host/claude/agents/analyst.md +59 -0
  20. package/dist/host/claude/agents/application.md +1 -1
  21. package/dist/host/claude/agents/builder.md +1 -1
  22. package/dist/host/claude/agents/checker.md +5 -1
  23. package/dist/host/claude/agents/codex.md +120 -94
  24. package/dist/host/claude/agents/grok.md +44 -32
  25. package/dist/host/claude/agents/implementer.md +1 -1
  26. package/dist/host/claude/agents/orkestrel.md +1 -1
  27. package/dist/host/claude/agents/planner.md +9 -3
  28. package/dist/host/claude/agents/researcher.md +6 -5
  29. package/dist/host/claude/agents/reviewer.md +12 -6
  30. package/dist/host/claude/agents/scout.md +7 -4
  31. package/dist/host/claude/agents/verifier.md +1 -1
  32. package/dist/host/claude/rules/application.md +17 -16
  33. package/dist/host/claude/rules/architecture.md +41 -22
  34. package/dist/host/claude/rules/documentation.md +2 -1
  35. package/dist/host/claude/rules/names.md +7 -6
  36. package/dist/host/claude/rules/quality.md +65 -18
  37. package/dist/host/claude/rules/styles.md +1 -1
  38. package/dist/host/claude/rules/tests.md +12 -6
  39. package/dist/host/claude/rules/typescript.md +1 -1
  40. package/dist/host/claude/rules/workspace.md +6 -3
  41. package/dist/host/claude/settings.json +2 -0
  42. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +10 -0
  43. package/dist/host/codex/agents/analyst.toml +13 -4
  44. package/dist/host/codex/agents/checker.toml +1 -1
  45. package/dist/host/codex/agents/grok.toml +2 -1
  46. package/dist/host/codex/agents/opus.toml +1 -1
  47. package/dist/host/codex/agents/planner.toml +1 -1
  48. package/dist/host/codex/agents/researcher.toml +2 -2
  49. package/dist/host/codex/agents/reviewer.toml +1 -1
  50. package/dist/host/codex/agents/scout.toml +2 -2
  51. package/dist/host/codex/config.toml +23 -70
  52. package/dist/host/cursor/rules/orchestration.mdc +33 -0
  53. package/dist/host/guides/src/scaffold.md +301 -123
  54. package/dist/host/manifest.json +47 -1
  55. package/dist/host/tests/setupPolicy.ts +83 -3
  56. package/dist/src/core/index.cjs +404 -63
  57. package/dist/src/core/index.cjs.map +1 -1
  58. package/dist/src/core/index.d.cts +119 -41
  59. package/dist/src/core/index.d.ts +119 -41
  60. package/dist/src/core/index.js +401 -64
  61. package/dist/src/core/index.js.map +1 -1
  62. package/dist/src/server/index.cjs +83 -33
  63. package/dist/src/server/index.cjs.map +1 -1
  64. package/dist/src/server/index.d.cts +32 -16
  65. package/dist/src/server/index.d.ts +32 -16
  66. package/dist/src/server/index.js +84 -35
  67. package/dist/src/server/index.js.map +1 -1
  68. package/package.json +4 -4
@@ -103,20 +103,29 @@ var APP_MATRIX = Object.freeze({
103
103
  * The byte-copied host artifact paths, frozen.
104
104
  *
105
105
  * @remarks
106
- * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, `.agents`, `.claude`, `.codex`,
107
- * the four SessionStart hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` /
108
- * `scripts/codex.sh` / `scripts/ollama.sh`), the repository coding-law policy module,
109
- * the line's seven byte-identical root dotfiles, and the two guides-grouped
110
- * mirror candidates: the line-wide dev-tooling guide
111
- * (`guides/src/guide.md`) and the scaffold bin's own self-guide
112
- * (`guides/src/scaffold.md`). `stageHost` vendors both; each plan carries the
113
- * subset selected by `selectHostPaths`, omitting the target blueprint's own
114
- * guide.
106
+ * The root docs (`AGENTS.md` / `CLAUDE.md`), `LICENSE`, the canonical
107
+ * orchestration contract (`.agents/orchestration.md`) every harness bridge
108
+ * points at, `.agents`, `.claude`, `.codex`, `.cursor`, the four SessionStart
109
+ * hook scripts (`scripts/deps.sh` / `scripts/cursor.sh` / `scripts/codex.sh` /
110
+ * `scripts/ollama.sh`), the repository coding-law policy module, the line's
111
+ * seven byte-identical root dotfiles, and the two guides-grouped mirror
112
+ * candidates: the line-wide dev-tooling guide (`guides/src/guide.md`) and the
113
+ * scaffold bin's own self-guide (`guides/src/scaffold.md`). `stageHost` vendors
114
+ * both; each plan carries the subset selected by `selectHostPaths`, omitting the
115
+ * target blueprint's own guide.
116
+ *
117
+ * Three harness bridges point at `.agents/orchestration.md` and carry only their
118
+ * own harness's specifics: `CLAUDE.md`, `.codex/config.toml`, and
119
+ * `.cursor/rules`. They are meaningless without the contract they reference, but
120
+ * they do not share a `Group` — `hostGroup` keeps `CLAUDE.md` in `docs` with the
121
+ * other root documents, so a plan selecting `orchestration` carries two of the
122
+ * three and a plan selecting `docs` carries the third.
115
123
  */
116
124
  var HOST_PATHS = Object.freeze([
117
125
  "AGENTS.md",
118
126
  "CLAUDE.md",
119
127
  "LICENSE",
128
+ ".agents/orchestration.md",
120
129
  ".agents/skills",
121
130
  ".claude/agents",
122
131
  ".claude/rules",
@@ -125,6 +134,7 @@ var HOST_PATHS = Object.freeze([
125
134
  ".codex/agents",
126
135
  ".codex/config.toml",
127
136
  ".cursor/mcp.json",
137
+ ".cursor/rules",
128
138
  ".mcp.json",
129
139
  "scripts/deps.sh",
130
140
  "scripts/cursor.sh",
@@ -141,12 +151,48 @@ var HOST_PATHS = Object.freeze([
141
151
  "guides/src/guide.md",
142
152
  "guides/src/scaffold.md"
143
153
  ]);
144
- /** The consumer-owned live-service provisioner expected only by service workspaces. */
154
+ /**
155
+ * The path prefixes whose contents instruct or wire an agent, frozen.
156
+ *
157
+ * @remarks
158
+ * Group classification splits by what a path governs, not by where it sits:
159
+ * anything under these prefixes is `orchestration`, and everything else that is
160
+ * not source, tests, guides, docs, or a manifest is `configs`. Both classifiers
161
+ * — `inferGroup` for a foreign target path and `hostGroup` for a `HOST_PATHS`
162
+ * entry — read this one list, so a new harness directory is admitted once.
163
+ */
164
+ var ORCHESTRATION_PATH_PREFIXES = Object.freeze([
165
+ ".agents/",
166
+ ".claude/",
167
+ ".codex/",
168
+ ".cursor/",
169
+ ".github/",
170
+ "scripts/"
171
+ ]);
172
+ /**
173
+ * The exact root filenames that wire an agent bench rather than the toolchain, frozen.
174
+ *
175
+ * @remarks
176
+ * `.mcp.json` registers MCP servers for the harness. It sits among the root
177
+ * dotfiles but governs agents, so it groups with the harness bridges.
178
+ */
179
+ var ORCHESTRATION_PATH_NAMES = Object.freeze([".mcp.json"]);
180
+ /** The birth-only provisioner skeleton retained by workspaces with declared service vendors. */
145
181
  var SERVICE_SCRIPT_PATH = "scripts/service.sh";
146
182
  /** The consumer-owned Vitest global-setup module shared by its independently selected projects. */
147
183
  var GLOBAL_SETUP_PATH = "tests/setupGlobal.ts";
148
184
  /** The consumer-owned Vite wrapper whose physical presence enables the optional app showcase. */
149
185
  var SHOWCASE_CONFIG_PATH = "configs/app/vite.showcase.config.ts";
186
+ /**
187
+ * The catalog agent file whose bounded marker region the catalog operation alone owns.
188
+ *
189
+ * @remarks
190
+ * Vendored like every other host artifact, but presence-owned after hydration:
191
+ * `diffPlan` compares this one path by presence, so a consumer restores it while
192
+ * absent and never replaces its bytes — not from an audit, not from a repair, and
193
+ * not under `replace`.
194
+ */
195
+ var CATALOG_AGENT_PATH = ".claude/agents/orkestrel.md";
150
196
  /** The package-name RegExp — lowercase alphanumeric-with-hyphens, letter-first. */
151
197
  var NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
152
198
  /** Maximum bare workspace name length beneath the generated `@orkestrel/` scope. */
@@ -229,7 +275,7 @@ var DEFAULT_VERSION = "0.0.1";
229
275
  /** The `engines.node` range the `blueprint` builder fills. */
230
276
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
231
277
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
232
- var SCAFFOLD_RANGE = "^0.0.21";
278
+ var SCAFFOLD_RANGE = "^0.0.22";
233
279
  /** Tooling versions shared by scaffold and every generated workspace. */
234
280
  var BASE_DEV_DEPENDENCIES = Object.freeze({
235
281
  "@microsoft/api-extractor": "^7.58.12",
@@ -438,8 +484,8 @@ function member(name, category, summary, environment = "core") {
438
484
  * @remarks
439
485
  * `version` / `engines` default `DEFAULT_VERSION` / `DEFAULT_ENGINES`,
440
486
  * `src` defaults `['core']`, and `app` / `keywords` / `dependencies` /
441
- * `peers` / `extras` / `overrides` default `[]`, and `bin` / `integration` /
442
- * `service` / `global` / `showcase` default `false`. `description` is OMITTED entirely
487
+ * `peers` / `extras` / `overrides` / `services` default `[]`, and `bin` /
488
+ * `integration` / `global` / `showcase` default `false`. `description` is OMITTED entirely
443
489
  * when absent, so the result round-trips the exact-record `Blueprint` guard.
444
490
  * @returns A complete `Blueprint`.
445
491
  *
@@ -464,7 +510,7 @@ function blueprint(name, options) {
464
510
  overrides: options?.overrides ?? [],
465
511
  bin: options?.bin ?? false,
466
512
  integration: options?.integration ?? false,
467
- service: options?.service ?? false,
513
+ services: [...options?.services ?? []].sort(),
468
514
  global: options?.global ?? false,
469
515
  showcase: options?.showcase ?? false
470
516
  };
@@ -953,7 +999,7 @@ function syncToReview(report) {
953
999
  }
954
1000
  /**
955
1001
  * Project a fleet package catalog into a markdown table — the block
956
- * `.claude/agents/orkestrel.md`'s catalog markers wrap.
1002
+ * `CATALOG_AGENT_PATH`'s catalog markers wrap.
957
1003
  *
958
1004
  * @param entries - The catalog rows to render.
959
1005
  * @remarks
@@ -985,10 +1031,12 @@ function catalogToBlock(entries) {
985
1031
  *
986
1032
  * @param path - The target-relative path to classify.
987
1033
  * @remarks
988
- * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`,
989
- * then `.agents/`, `.claude/`, `.codex/`, `.github/`, and `scripts/` as
990
- * `'orchestration'`, then the two manifest files by exact name. Anything else
991
- * (a root-level, prefix-less file) falls through to `'configs'`.
1034
+ * Ordered prefix match — `src/`, `tests/`, `guides/`, `docs/`, `configs/`, then
1035
+ * `matchesOrchestrationPath`, which owns the orchestration membership rule for
1036
+ * this function and for `hostGroup` alike, then the two manifest files by exact
1037
+ * name. Anything left falls through to `'configs'`. Read the rule at
1038
+ * `matchesOrchestrationPath` rather than here; one rule with two descriptions
1039
+ * drifts exactly as fast as one rule with two implementations.
992
1040
  * @returns The inferred `Group` for `path`.
993
1041
  *
994
1042
  * @example
@@ -1006,23 +1054,43 @@ function inferGroup(path) {
1006
1054
  if (path.startsWith("guides/")) return "guides";
1007
1055
  if (path.startsWith("docs/")) return "docs";
1008
1056
  if (path.startsWith("configs/")) return "configs";
1009
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith(".github/") || path.startsWith("scripts/")) return "orchestration";
1057
+ if (matchesOrchestrationPath(path)) return "orchestration";
1010
1058
  if (path === "package.json" || path === "package-lock.json") return "manifest";
1011
1059
  return "configs";
1012
1060
  }
1013
1061
  /**
1062
+ * Test whether a path instructs or wires an agent rather than the toolchain.
1063
+ *
1064
+ * @param path - The portable path to classify.
1065
+ * @returns `true` when the path is agent orchestration.
1066
+ *
1067
+ * @example
1068
+ * ```ts
1069
+ * import { matchesOrchestrationPath } from '@orkestrel/scaffold'
1070
+ *
1071
+ * matchesOrchestrationPath('.cursor/rules/orchestration.mdc') // true
1072
+ * matchesOrchestrationPath('.mcp.json') // true
1073
+ * matchesOrchestrationPath('.oxlintrc.json') // false
1074
+ * ```
1075
+ */
1076
+ function matchesOrchestrationPath(path) {
1077
+ if (ORCHESTRATION_PATH_NAMES.includes(path)) return true;
1078
+ return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
1079
+ }
1080
+ /**
1014
1081
  * Diff a plan's artifacts against a target's current content.
1015
1082
  *
1016
1083
  * @param plan - The plan whose artifacts are the source of truth.
1017
1084
  * @param current - The target's current content, keyed by artifact-relative path.
1018
1085
  * @remarks
1019
- * Audit semantics are per-origin. A `host`-origin artifact is audited by
1020
- * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
1021
- * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
1022
- * it is content-compared exactly like a `computed` artifact and CAN be
1023
- * `stale`. `hydratePlan` expands directory-shaped host artifacts into
1024
- * content-bearing file artifacts; only an unresolved degrade-path host
1025
- * artifact stays presence-only. A `computed` artifact is content-aware canon —
1086
+ * Audit semantics are per-origin. A `host`-origin artifact with canonical
1087
+ * `hex` is content-compared exactly like a `computed` artifact and CAN be
1088
+ * `stale`. A host artifact without canonical bytes is presence-owned:
1089
+ * present is `aligned`, absent is `missing`. `CATALOG_AGENT_PATH` remains
1090
+ * presence-owned after hydration because the catalog operation alone owns its
1091
+ * bounded marker region. `hydratePlan` expands directory-shaped host artifacts
1092
+ * into byte-aware file artifacts; only an intentional dependency-guide pointer
1093
+ * may remain without `hex`. A `computed` artifact is content-aware canon —
1026
1094
  * `missing` / `aligned` / `stale` — and gates the audit like any drifted
1027
1095
  * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
1028
1096
  * always reported `aligned`, regardless of whether the target has it at all
@@ -1062,7 +1130,7 @@ function diffPlan(plan, current) {
1062
1130
  if (artifact.origin === "host") {
1063
1131
  let drift;
1064
1132
  if (seen === void 0) drift = "missing";
1065
- else if (artifact.hex === void 0) drift = "aligned";
1133
+ else if (artifact.path === ".claude/agents/orkestrel.md" || artifact.hex === void 0) drift = "aligned";
1066
1134
  else drift = seen === artifact.hex ? "aligned" : "stale";
1067
1135
  findings.push({
1068
1136
  path: artifact.path,
@@ -1419,6 +1487,35 @@ function validateBlueprint(spec) {
1419
1487
  text: "Showcase requires the app browser environment",
1420
1488
  blocking: true
1421
1489
  });
1490
+ const seenServices = /* @__PURE__ */ new Set();
1491
+ const serviceProjects = /* @__PURE__ */ new Set();
1492
+ let previousService;
1493
+ for (const service of spec.services) {
1494
+ if (!NAME_PATTERN.test(service) || service.length > 203) questions.push({
1495
+ field: "services",
1496
+ text: `Service name "${service}" must be a bounded lowercase directory name matching ${NAME_PATTERN.source}`,
1497
+ blocking: true
1498
+ });
1499
+ if (seenServices.has(service)) questions.push({
1500
+ field: "services",
1501
+ text: `Service "${service}" is declared more than once`,
1502
+ blocking: true
1503
+ });
1504
+ if (previousService !== void 0 && previousService > service) questions.push({
1505
+ field: "services",
1506
+ text: "Services must be sorted by directory name",
1507
+ blocking: true
1508
+ });
1509
+ const project = pascalCase(service);
1510
+ if (serviceProjects.has(project)) questions.push({
1511
+ field: "services",
1512
+ text: `Service "${service}" collides with another generated project name`,
1513
+ blocking: true
1514
+ });
1515
+ seenServices.add(service);
1516
+ serviceProjects.add(project);
1517
+ previousService = service;
1518
+ }
1422
1519
  if (spec.src.length > 0) {
1423
1520
  for (const environment of spec.src) if (!ENVIRONMENTS.includes(environment)) questions.push({
1424
1521
  field: "src",
@@ -1909,7 +2006,10 @@ function blueprintShape() {
1909
2006
  overrides: (0, _orkestrel_contract.arrayShape)(overrideShape(), { max: MAX_COLLECTION_ITEMS }),
1910
2007
  bin: (0, _orkestrel_contract.booleanShape)(),
1911
2008
  integration: (0, _orkestrel_contract.booleanShape)(),
1912
- service: (0, _orkestrel_contract.booleanShape)(),
2009
+ services: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)({
2010
+ min: 1,
2011
+ max: 203
2012
+ }), { max: MAX_COLLECTION_ITEMS }),
1913
2013
  global: (0, _orkestrel_contract.booleanShape)(),
1914
2014
  showcase: (0, _orkestrel_contract.booleanShape)()
1915
2015
  });
@@ -4502,6 +4602,87 @@ describe('root Vite configuration', () => {
4502
4602
  expect(environmentSourceError('app/browser', '@app/core')).toBeUndefined()
4503
4603
  }){{cases}}
4504
4604
  })
4605
+ `
4606
+ }),
4607
+ serviceConformance: Object.freeze({
4608
+ id: "serviceConformance",
4609
+ name: "serviceConformance",
4610
+ summary: "The generated service-vendor structure and configuration conformance test.",
4611
+ category: "tests",
4612
+ placeholders: Object.freeze([Object.freeze({
4613
+ name: "services",
4614
+ description: "The sorted service vendor directory names."
4615
+ })]),
4616
+ content: `import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
4617
+ import { join } from 'node:path'
4618
+ import { isRecord, parseJSON } from '@orkestrel/contract'
4619
+ import { expect, it } from 'vitest'
4620
+
4621
+ it('keeps every service vendor structurally complete and exactly configured', () => {
4622
+ const declared = {{services}}
4623
+ const root = join(process.cwd(), 'tests', 'service')
4624
+ const directories = readdirSync(root, { withFileTypes: true })
4625
+ .filter((entry) => entry.isDirectory())
4626
+ .map((entry) => entry.name)
4627
+ .sort()
4628
+ const manifest = parseJSON(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
4629
+ const scripts = isRecord(manifest) && isRecord(manifest.scripts) ? manifest.scripts : {}
4630
+ const config = readFileSync(join(process.cwd(), 'vite.config.ts'), 'utf8')
4631
+ const provisioner = join(process.cwd(), 'scripts', 'service.sh')
4632
+
4633
+ expect(directories).toEqual(declared)
4634
+ if (!existsSync(provisioner) || !lstatSync(provisioner).isFile()) {
4635
+ throw new Error(
4636
+ 'Service vendors require scripts/service.sh to provision every declared vendor idempotently or exit nonzero',
4637
+ )
4638
+ }
4639
+ for (const service of declared) {
4640
+ const setup = join(root, service, 'setup.ts')
4641
+ if (!existsSync(setup) || !lstatSync(setup).isFile()) {
4642
+ throw new Error(
4643
+ \`Service vendor "\${service}" is missing tests/service/\${service}/setup.ts; add a readiness module that probes and warms the vendor and throws when unavailable\`,
4644
+ )
4645
+ }
4646
+ const label = \`name: { label: 'service:\${service}', color: 'red' }\`
4647
+ const include = \`include: ['tests/service/\${service}/**/*.test.ts']\`
4648
+ const setupFiles = \`setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', './tests/service/\${service}/setup.ts']\`
4649
+ if (!config.includes(label) || !config.includes(include) || !config.includes(setupFiles)) {
4650
+ throw new Error(\`Service vendor "\${service}" is missing its exact Vite project\`)
4651
+ }
4652
+ const command = \`vitest run --config vite.config.ts --no-cache --reporter=dot --project service:\${service}\`
4653
+ if (scripts[\`test:service:\${service}\`] !== command) {
4654
+ throw new Error(\`Service vendor "\${service}" is missing its exact npm script\`)
4655
+ }
4656
+ }
4657
+ const aggregate =
4658
+ 'vitest run --config vite.config.ts --no-cache --reporter=dot ' +
4659
+ declared.map((service) => \`--project service:\${service}\`).join(' ')
4660
+ if (scripts['test:service'] !== aggregate) {
4661
+ throw new Error('test:service must name every declared service exactly once')
4662
+ }
4663
+ if (typeof scripts.test !== 'string' || scripts.test.includes('test:service')) {
4664
+ throw new Error('The default test script must omit live service proofs')
4665
+ }
4666
+ if (
4667
+ typeof scripts.prepublishOnly !== 'string' ||
4668
+ !scripts.prepublishOnly.endsWith(' && npm run test:service')
4669
+ ) {
4670
+ throw new Error('prepublishOnly must end with the aggregate live service proof')
4671
+ }
4672
+ })
4673
+ `
4674
+ }),
4675
+ serviceProvisioner: Object.freeze({
4676
+ id: "serviceProvisioner",
4677
+ name: "serviceProvisioner",
4678
+ summary: "The birth-only workspace-owned service-vendor provisioner skeleton.",
4679
+ category: "orchestration",
4680
+ placeholders: Object.freeze([]),
4681
+ content: `#!/bin/sh
4682
+ set -eu
4683
+
4684
+ printf '%s\\n' 'Service provisioning is not configured. Implement scripts/service.sh to provision every declared vendor idempotently, then remove this failure.' >&2
4685
+ exit 1
4505
4686
  `
4506
4687
  }),
4507
4688
  policyTest: Object.freeze({
@@ -4511,15 +4692,23 @@ describe('root Vite configuration', () => {
4511
4692
  category: "tests",
4512
4693
  placeholders: Object.freeze([Object.freeze({
4513
4694
  name: "vuePolicyImport",
4514
- description: "The optional official Vue SFC compiler import."
4695
+ description: "The Vue SFC parser import, when the workspace declares app/browser."
4515
4696
  }), Object.freeze({
4516
4697
  name: "workspacePolicyAssertion",
4517
- description: "The formatter-stable workspace policy assertion."
4698
+ description: "The workspace coding-law assertion, carrying a Vue script extractor when the workspace declares app/browser."
4518
4699
  })]),
4519
4700
  content: `import { globSync } from 'node:fs'
4520
4701
  import { describe, expect, it } from 'vitest'
4521
4702
  import { isBrowserVuePath } from './setup.js'
4522
- import { inspectCodingWorkspace } from './setupPolicy.js'{{vuePolicyImport}}
4703
+ import { inspectCodingLaw, inspectCodingWorkspace, isFunctionDomainPath } from './setupPolicy.js'{{vuePolicyImport}}
4704
+
4705
+ ${CONST_KEYWORD} FUNCTION_MODULE_PATH = 'app/browser/composables/useTheme.ts'
4706
+ ${CONST_KEYWORD} FUNCTION_MODULE_VIOLATION =
4707
+ FUNCTION_MODULE_PATH + ' declarations do not form one matching exported function implementation'
4708
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_PATH = 'app/server/composables.ts'
4709
+ ${CONST_KEYWORD} FUNCTION_DOMAIN_FILE_VIOLATION =
4710
+ FUNCTION_DOMAIN_FILE_PATH +
4711
+ ' names a function domain, which belongs in a folder rather than a file'
4523
4712
 
4524
4713
  describe('repository coding law', () => {
4525
4714
  it('keeps Vue single-file components exclusively in browser environments', () => {
@@ -4531,6 +4720,125 @@ describe('repository coding law', () => {
4531
4720
  it('enforces source placement, exports, readonly contracts, and syntax law', () => {
4532
4721
  {{workspacePolicyAssertion}}
4533
4722
  })
4723
+
4724
+ it('accepts one matching exported function in a registered domain', () => {
4725
+ expect(
4726
+ inspectCodingLaw(
4727
+ FUNCTION_MODULE_PATH,
4728
+ "import type { Ref } from 'vue'\\nexport function useTheme(): Ref<undefined> { throw new Error() }",
4729
+ ),
4730
+ ).toEqual([])
4731
+ })
4732
+
4733
+ it('accepts one matching exported generator in a registered domain', () => {
4734
+ expect(
4735
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function* useTheme(): Generator<void> {}'),
4736
+ ).toEqual([])
4737
+ })
4738
+
4739
+ it('accepts overload signatures beside one matching implementation', () => {
4740
+ expect(
4741
+ inspectCodingLaw(
4742
+ FUNCTION_MODULE_PATH,
4743
+ 'export function useTheme(): void\\nexport function useTheme(mode: string): void\\nexport function useTheme(_mode?: string): void {}',
4744
+ ),
4745
+ ).toEqual([])
4746
+ })
4747
+
4748
+ it('rejects a bodyless function-domain declaration', () => {
4749
+ expect(
4750
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export declare function useTheme(): void'),
4751
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4752
+ })
4753
+
4754
+ it('rejects a mismatched bodyless declaration beside one implementation', () => {
4755
+ expect(
4756
+ inspectCodingLaw(
4757
+ FUNCTION_MODULE_PATH,
4758
+ 'declare function smuggled(secret: string): void\\nexport function useTheme(): void {}',
4759
+ ),
4760
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4761
+ })
4762
+
4763
+ it('rejects two exported functions in a function module', () => {
4764
+ expect(
4765
+ inspectCodingLaw(
4766
+ FUNCTION_MODULE_PATH,
4767
+ 'export function useTheme(): void {}\\nexport function useMode(): void {}',
4768
+ ),
4769
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4770
+ })
4771
+
4772
+ it('rejects module data beside a function-domain export', () => {
4773
+ expect(
4774
+ inspectCodingLaw(
4775
+ FUNCTION_MODULE_PATH,
4776
+ "const THEME = 'dark'\\nexport function useTheme(): string { return THEME }",
4777
+ ),
4778
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4779
+ })
4780
+
4781
+ it('rejects a function whose name differs from its filename', () => {
4782
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'export function useMode(): void {}')).toEqual([
4783
+ FUNCTION_MODULE_VIOLATION,
4784
+ ])
4785
+ })
4786
+
4787
+ it('rejects a non-exported function-domain declaration', () => {
4788
+ expect(inspectCodingLaw(FUNCTION_MODULE_PATH, 'function useTheme(): void {}')).toEqual([
4789
+ FUNCTION_MODULE_VIOLATION,
4790
+ ])
4791
+ })
4792
+
4793
+ it('rejects a default function-domain export', () => {
4794
+ expect(
4795
+ inspectCodingLaw(FUNCTION_MODULE_PATH, 'export default function useTheme(): void {}'),
4796
+ ).toEqual([FUNCTION_MODULE_VIOLATION])
4797
+ })
4798
+
4799
+ it('keeps index modules outside the function-domain shape', () => {
4800
+ const path = 'app/browser/composables/index.ts'
4801
+
4802
+ expect(isFunctionDomainPath(path)).toBe(false)
4803
+ expect(inspectCodingLaw(path, 'export function index(): void {}')).toContain(
4804
+ path + ' places module functions in their centralized kind file',
4805
+ )
4806
+ })
4807
+
4808
+ it('keeps main modules outside the function-domain shape', () => {
4809
+ const path = 'app/browser/composables/main.ts'
4810
+
4811
+ expect(isFunctionDomainPath(path)).toBe(false)
4812
+ expect(inspectCodingLaw(path, 'export function main(): void {}')).toContain(
4813
+ path + ' places module functions in their centralized kind file',
4814
+ )
4815
+ })
4816
+
4817
+ it('rejects a file named for a registered function domain', () => {
4818
+ const content =
4819
+ "import { parentPort } from 'node:worker_threads'\\nexport function start(): void { parentPort?.close() }"
4820
+
4821
+ expect(inspectCodingLaw(FUNCTION_DOMAIN_FILE_PATH, content)).toEqual([
4822
+ FUNCTION_DOMAIN_FILE_VIOLATION,
4823
+ ])
4824
+ })
4825
+
4826
+ it('keeps camelCase modules in unregistered domains under centralized placement', () => {
4827
+ const path = 'app/browser/services/normalizePath.ts'
4828
+
4829
+ expect(isFunctionDomainPath(path)).toBe(false)
4830
+ expect(inspectCodingLaw(path, 'export function normalizePath(): void {}')).toEqual([
4831
+ path + ' places module functions in their centralized kind file',
4832
+ ])
4833
+ })
4834
+
4835
+ it('preserves the self-contained Node runtime exemption', () => {
4836
+ const path = 'app/server/worker.ts'
4837
+ const content =
4838
+ "import { parentPort } from 'node:worker_threads'\\nconst port = parentPort\\nexport function start(): void { port?.close() }"
4839
+
4840
+ expect(inspectCodingLaw(path, content)).toEqual([])
4841
+ })
4534
4842
  })
4535
4843
  `
4536
4844
  }),
@@ -5677,14 +5985,30 @@ for (const entry of GUIDE_MANIFEST) {
5677
5985
  * @example
5678
5986
  * ```ts
5679
5987
  * hostGroup('AGENTS.md') // 'docs'
5680
- * hostGroup('.agents') // 'orchestration'
5681
- * hostGroup('.claude') // 'orchestration'
5682
- * hostGroup('.codex') // 'orchestration'
5988
+ * hostGroup('.agents/orchestration.md') // 'orchestration'
5989
+ * hostGroup('.claude/rules') // 'orchestration'
5990
+ * hostGroup('.cursor/rules') // 'orchestration'
5991
+ * hostGroup('.mcp.json') // 'orchestration'
5992
+ * hostGroup('.oxlintrc.json') // 'configs'
5683
5993
  * ```
5994
+ *
5995
+ * @remarks
5996
+ * Takes a `HOST_PATHS` entry, so every example above is one. A bare directory
5997
+ * name is not: `ORCHESTRATION_PATH_PREFIXES` entries carry a trailing slash, so
5998
+ * `hostGroup('.cursor')` is `configs`, and no vendored entry has that form.
5999
+ *
6000
+ * Below the `docs` branch the split is by what a path governs rather than where
6001
+ * it sits, which is why both MCP registrations — `.mcp.json` and
6002
+ * `.cursor/mcp.json` — group with the harness bridges instead of with the root
6003
+ * dotfiles beside them. The `docs` branch is checked first and is deliberately
6004
+ * positional: `AGENTS.md`, `CLAUDE.md`, and `LICENSE` are the root documents, and
6005
+ * `CLAUDE.md` stays there as a root document even though it is also a harness
6006
+ * bridge. A plan selecting `orchestration` therefore carries two of the three
6007
+ * bridges; a plan selecting `docs` carries the third.
5684
6008
  */
5685
6009
  function hostGroup(path) {
5686
6010
  if (path === "AGENTS.md" || path === "CLAUDE.md" || path === "LICENSE") return "docs";
5687
- if (path.startsWith(".agents/") || path.startsWith(".claude/") || path.startsWith(".codex/") || path.startsWith("scripts/") || path.startsWith(".github/")) return "orchestration";
6011
+ if (matchesOrchestrationPath(path)) return "orchestration";
5688
6012
  if (path.startsWith("tests/")) return "tests";
5689
6013
  if (path === "guides/src/guide.md" || path === "guides/src/scaffold.md") return "guides";
5690
6014
  return "configs";
@@ -5950,7 +6274,10 @@ function packageManifest(spec) {
5950
6274
  if (spec.bin) scripts["test:src:bin"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:bin";
5951
6275
  if (spec.integration) scripts["test:integration"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project integration";
5952
6276
  if (spec.bin && spec.integration) scripts["test:equivalence"] = "node -e \"const c=require('node:child_process'),p=process.env.npm_execpath;if(p===undefined)process.exit(1);const r=c.spawnSync(process.execPath,[p,'run','test:integration'],{stdio:'inherit',env:{...process.env,SCAFFOLD_BOUNDARY_EQUIVALENCE:'1'}});process.exit(r.status??1)\"";
5953
- if (spec.service) scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot --project service";
6277
+ if (spec.services.length > 0) {
6278
+ scripts["test:service"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.services.map((service) => `--project service:${service}`).join(" ");
6279
+ for (const service of spec.services) scripts[`test:service:${service}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project service:${service}`;
6280
+ }
5954
6281
  if (spec.app.length > 0) {
5955
6282
  scripts["test:app"] = "vitest run --config vite.config.ts --no-cache --reporter=dot " + spec.app.map((environment) => `--project ${APP_MATRIX[environment].project}`).join(" ");
5956
6283
  for (const environment of spec.app) scripts[`test:app:${environment}`] = `vitest run --config vite.config.ts --no-cache --reporter=dot --project ${APP_MATRIX[environment].project}`;
@@ -5989,7 +6316,7 @@ function packageManifest(spec) {
5989
6316
  scripts["build:src:bin"] = "vite build --config configs/src/vite.bin.config.ts";
5990
6317
  scripts["build:host"] = "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"";
5991
6318
  }
5992
- scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "");
6319
+ scripts.prepublishOnly = "npm run format:check && npm run lint:check && npm run check && npm run build && npm test" + (spec.integration ? " && npm run test:integration" : "") + (spec.services.length > 0 ? " && npm run test:service" : "");
5993
6320
  const devDependencies = devDependenciesFor(spec);
5994
6321
  const manifest = {
5995
6322
  name: hasSource ? `@orkestrel/${spec.name}` : spec.name,
@@ -5997,13 +6324,15 @@ function packageManifest(spec) {
5997
6324
  ...hasSource ? {} : { private: true },
5998
6325
  description: spec.description ?? (hasSource ? `The @orkestrel/${spec.name} package.` : `The ${spec.name} application.`),
5999
6326
  keywords: [...spec.keywords].sort(),
6000
- homepage: `https://github.com/orkestrel/${spec.name}#readme`,
6001
- bugs: `https://github.com/orkestrel/${spec.name}/issues`,
6327
+ ...hasSource ? {
6328
+ homepage: `https://github.com/orkestrel/${spec.name}#readme`,
6329
+ bugs: `https://github.com/orkestrel/${spec.name}/issues`
6330
+ } : {},
6002
6331
  license: "MIT",
6003
- repository: {
6332
+ ...hasSource ? { repository: {
6004
6333
  type: "git",
6005
6334
  url: `git+https://github.com/orkestrel/${spec.name}.git`
6006
- },
6335
+ } } : {},
6007
6336
  ...spec.bin ? { bin: { scaffold: "./dist/bin/scaffold.js" } } : {},
6008
6337
  files: spec.bin ? [
6009
6338
  "dist/src",
@@ -6153,7 +6482,7 @@ function viteProjectRegistrations(src, app = [], facts = {}) {
6153
6482
  registrations.push({ project: "policy" }, { project: "config" }, { project: "guides" });
6154
6483
  if (facts.bin === true) registrations.push({ project: "srcBin" });
6155
6484
  if (facts.integration === true) registrations.push({ project: "integration" });
6156
- if (facts.service === true) registrations.push({ project: "service" });
6485
+ for (const service of facts.services ?? []) registrations.push({ project: `service${pascalCase(service)}` });
6157
6486
  return registrations;
6158
6487
  }
6159
6488
  /**
@@ -6175,7 +6504,7 @@ function viteProjectDefinitions(facts = {}) {
6175
6504
  ];
6176
6505
  if (facts.bin === true) definitions.push(binViteProject());
6177
6506
  if (facts.integration === true) definitions.push(integrationViteProject(facts));
6178
- if (facts.service === true) definitions.push(serviceViteProject());
6507
+ for (const service of facts.services ?? []) definitions.push(serviceViteProject(service));
6179
6508
  return definitions.join("\n");
6180
6509
  }
6181
6510
  /**
@@ -8162,24 +8491,27 @@ ${facts.bin === true && facts.integration === true && facts.global === true ? `
8162
8491
  `;
8163
8492
  }
8164
8493
  /**
8165
- * Build the standalone Node-only live-service proof project.
8494
+ * Build one standalone Node-only live-service vendor proof project.
8166
8495
  *
8167
- * @returns The emitted `service` project definition.
8496
+ * @param name - The bounded vendor directory name.
8497
+ * @returns The emitted `service:<name>` project definition.
8168
8498
  *
8169
8499
  * @example
8170
8500
  * ```ts
8171
- * serviceViteProject().includes("label: 'service'") // true
8501
+ * serviceViteProject('claude').includes("label: 'service:claude'") // true
8172
8502
  * ```
8173
8503
  */
8174
- function serviceViteProject() {
8175
- return `${EXPORT_KEYWORD} const service = (options?: UserConfig): UserConfig =>
8504
+ function serviceViteProject(name) {
8505
+ if (!NAME_PATTERN.test(name) || name.length > 203) throw new Error("Service project name must be a bounded lowercase directory name");
8506
+ return `${EXPORT_KEYWORD} const ${`service${pascalCase(name)}`} = (options?: UserConfig): UserConfig =>
8176
8507
  mergeConfig(
8177
8508
  {
8178
8509
  resolve,
8510
+ plugins: [environmentBoundary('app/server')],
8179
8511
  test: {
8180
- name: { label: 'service', color: 'red' },
8181
- include: ['tests/service/**/*.test.ts'],
8182
- setupFiles: ['./tests/setup.ts', './tests/setupService.ts'],
8512
+ name: { label: ${serializeTypeScriptString(`service:${name}`)}, color: 'red' },
8513
+ include: [${serializeTypeScriptString(`tests/service/${name}/**/*.test.ts`)}],
8514
+ setupFiles: ['./tests/setup.ts', './tests/setupServer.ts', ${serializeTypeScriptString(`./tests/service/${name}/setup.ts`)}],
8183
8515
  environment: 'node',
8184
8516
  browser: { enabled: false },
8185
8517
  testTimeout: 120_000,
@@ -8329,7 +8661,7 @@ ${renderedTest}
8329
8661
  *
8330
8662
  * @param src - The declared `Environment[]`.
8331
8663
  * @param facts - Optional structural facts. `bin` appends the standalone executable
8332
- * build-and-test project; `integration` and `service` append their standalone
8664
+ * build-and-test project; `integration` and `services` append their standalone
8333
8665
  * proof projects; `global` wires the shared global-setup module.
8334
8666
  * @returns The root `vite.config.ts` file content, newline-terminated.
8335
8667
  *
@@ -9109,7 +9441,7 @@ function ciWorkflow(spec) {
9109
9441
  const tail = [];
9110
9442
  if (spec.integration) tail.push(` - name: Run live consumer integration
9111
9443
  run: npm run test:integration`);
9112
- if (spec.service) {
9444
+ if (spec.services.length > 0) {
9113
9445
  tail.push(` - name: Provision live service
9114
9446
  run: bash ${SERVICE_SCRIPT_PATH}`);
9115
9447
  tail.push(` - name: Run live service tests
@@ -9592,7 +9924,8 @@ ${EXPORT_KEYWORD} ${FUNCTION_KEYWORD} waitForEvent<TMap extends EventMap, K exte
9592
9924
  cases: configCases.join("")
9593
9925
  })
9594
9926
  ];
9595
- if (spec.src.includes("server") || spec.app.includes("server")) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9927
+ if (spec.src.includes("server") || spec.app.includes("server") || spec.services.length > 0) artifacts.push(fillArtifact("tests/setupServer.ts", "tests", "setupServer", {}, "server"));
9928
+ if (spec.services.length > 0) artifacts.push(fillArtifact("tests/config/services.test.ts", "tests", "serviceConformance", { services: renderStringArray(spec.services, " ", "const declared = ", "") }, "server"));
9596
9929
  if (spec.src.includes("browser") || spec.app.includes("browser")) artifacts.push(fillArtifact("tests/setupBrowser.ts", "tests", "setupBrowser", {}, "browser"));
9597
9930
  const hasBoundary = hasApplicationBoundary(spec);
9598
9931
  const hasShowcase = hasApplicationShowcase(spec);
@@ -10195,12 +10528,15 @@ npm install @orkestrel/${blueprint.name}
10195
10528
  \`\`\`` : "This is a private application workspace and is not published to npm.",
10196
10529
  usage: guideUsage(blueprint, pascal)
10197
10530
  }));
10198
- if (selected.includes("orchestration")) artifacts.push({
10199
- path: ".github/workflows/ci.yml",
10200
- group: "orchestration",
10201
- origin: "computed",
10202
- content: ciWorkflow(blueprint)
10203
- });
10531
+ if (selected.includes("orchestration")) {
10532
+ artifacts.push({
10533
+ path: ".github/workflows/ci.yml",
10534
+ group: "orchestration",
10535
+ origin: "computed",
10536
+ content: ciWorkflow(blueprint)
10537
+ });
10538
+ if (blueprint.services.length > 0) artifacts.push(fillArtifact(SERVICE_SCRIPT_PATH, "orchestration", "serviceProvisioner", {}));
10539
+ }
10204
10540
  for (const path of selectHostPaths(HOST_PATHS, blueprint.name)) {
10205
10541
  const group = hostGroup(path);
10206
10542
  if (!selected.includes(group)) continue;
@@ -10324,9 +10660,10 @@ var Compiler = class Compiler {
10324
10660
  this.#emitter.emit("audit", result);
10325
10661
  return result;
10326
10662
  }
10663
+ const diff = diffPlan(scaffolding.plan, current);
10327
10664
  const result = {
10328
- ...diffPlan(scaffolding.plan, current),
10329
- questions: scaffolding.questions
10665
+ ...diff,
10666
+ questions: [...scaffolding.questions, ...diff.questions]
10330
10667
  };
10331
10668
  this.#emitter.emit("audit", result);
10332
10669
  return result;
@@ -10728,6 +11065,7 @@ exports.APP_MATRIX = APP_MATRIX;
10728
11065
  exports.APP_SERVER_DEV_DEPENDENCIES = APP_SERVER_DEV_DEPENDENCIES;
10729
11066
  exports.BASE_DEV_DEPENDENCIES = BASE_DEV_DEPENDENCIES;
10730
11067
  exports.BIN_CONFIGS = BIN_CONFIGS;
11068
+ exports.CATALOG_AGENT_PATH = CATALOG_AGENT_PATH;
10731
11069
  exports.CATEGORIES = CATEGORIES;
10732
11070
  exports.CHECKOUT_ACTION_SHA = CHECKOUT_ACTION_SHA;
10733
11071
  exports.COMPILER_ID = COMPILER_ID;
@@ -10767,6 +11105,8 @@ exports.MAX_SERIALIZED_INPUT_BYTES = MAX_SERIALIZED_INPUT_BYTES;
10767
11105
  exports.MAX_TOTAL_ARTIFACT_BYTES = MAX_TOTAL_ARTIFACT_BYTES;
10768
11106
  exports.MINIMUM_NODE_VERSION = MINIMUM_NODE_VERSION;
10769
11107
  exports.NAME_PATTERN = NAME_PATTERN;
11108
+ exports.ORCHESTRATION_PATH_NAMES = ORCHESTRATION_PATH_NAMES;
11109
+ exports.ORCHESTRATION_PATH_PREFIXES = ORCHESTRATION_PATH_PREFIXES;
10770
11110
  exports.ORIGINS = ORIGINS;
10771
11111
  exports.ORKESTREL_RANGE_PATTERN = ORKESTREL_RANGE_PATTERN;
10772
11112
  exports.PlanManager = PlanManager;
@@ -10866,6 +11206,7 @@ exports.isSyncReport = isSyncReport;
10866
11206
  exports.isWorkspaceName = isWorkspaceName;
10867
11207
  exports.manifestToDependencies = manifestToDependencies;
10868
11208
  exports.manifestToName = manifestToName;
11209
+ exports.matchesOrchestrationPath = matchesOrchestrationPath;
10869
11210
  exports.member = member;
10870
11211
  exports.memberShape = memberShape;
10871
11212
  exports.override = override;