@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
@@ -1,29 +1,40 @@
1
- import { existsSync, readdirSync } from 'node:fs'
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { describe, expect, it } from 'vitest'
4
+ import { POLICY_BANNED_TERMS, POLICY_JUDGED_TERMS } from '../configs/policy.js'
4
5
  import {
5
6
  BRIDGE_POLICY_CONTROLS,
6
7
  createPolicyScratch,
7
8
  createSkillMetadata,
8
- FUNCTION_SOURCE_FILES,
9
- GENERIC_POLICY_SOURCES,
10
9
  inspectPolicyControl,
11
10
  inspectPolicyFilenamePaths,
12
11
  inspectPolicyMirrorPaths,
13
12
  inspectPolicyPortability,
14
- inspectPolicySources,
13
+ inspectPolicyWiring,
15
14
  inspectPolicyWorkspace,
16
15
  inspectSkillFamily,
17
16
  inspectSkillBridges,
17
+ isPolicyFile,
18
+ isPolicyMirror,
19
+ isPolicyRecord,
20
+ isPolicyStray,
18
21
  matchesSkillTrigger,
19
22
  parseSkillFrontmatter,
23
+ POLICY_CATALOG_FILE,
20
24
  POLICY_CONTROLS,
25
+ POLICY_MIRROR_PATTERN,
21
26
  POLICY_SUPPRESSION_DIRECTIVE,
27
+ POLICY_TERM_FILE,
28
+ POLICY_TERM_HEADING,
22
29
  PORTABILITY_POLICY_CONTROLS,
23
- PORTABILITY_POLICY_EXCLUSION,
24
- PORTABILITY_POLICY_LOCAL,
25
- PORTABILITY_POLICY_SPLIT,
30
+ PROSE_POLICY_CONTROLS,
31
+ PROSE_POLICY_MANIFEST,
32
+ readPolicyCatalog,
33
+ readPolicyGuide,
34
+ readPolicyPackage,
26
35
  readPolicyPaths,
36
+ readPolicyProse,
37
+ readPolicyTerms,
27
38
  readSkillFamily,
28
39
  RULES_POLICY_CONTROLS,
29
40
  SKILL_BRIDGE_ROOT,
@@ -55,15 +66,6 @@ describe('policy scratch', () => {
55
66
  })
56
67
 
57
68
  describe('fleet policy register', () => {
58
- it('keeps handlers in the function set and routes out', () => {
59
- expect(FUNCTION_SOURCE_FILES).toContain('handlers.ts')
60
- expect(FUNCTION_SOURCE_FILES).not.toContain('routes.ts')
61
- })
62
-
63
- it('accepts a differently shaped workspace without a core environment', () => {
64
- expect(inspectPolicySources(GENERIC_POLICY_SOURCES)).toEqual([])
65
- })
66
-
67
69
  it('accepts matching mirrors across arbitrary axes and environments', () => {
68
70
  const tests = [
69
71
  'tests/src/worker/Worker.test.ts',
@@ -178,152 +180,6 @@ describe('policy population controls', () => {
178
180
  ).toEqual([])
179
181
  })
180
182
 
181
- it('excludes a .d.ts ambient declaration from placement', () => {
182
- expect(
183
- inspectPolicyControl({
184
- label: 'excludes a .d.ts ambient declaration from placement',
185
- membership: 'ambient TypeScript declarations',
186
- rule: 'type',
187
- files: [
188
- {
189
- path: 'app/browser/env.d.ts',
190
- content: 'export interface EnvironmentInterface {}\n',
191
- },
192
- ],
193
- }),
194
- ).toEqual([])
195
- })
196
-
197
- it('excludes a .d.mts ambient declaration from placement', () => {
198
- expect(
199
- inspectPolicyControl({
200
- label: 'excludes a .d.mts ambient declaration from placement',
201
- membership: 'ambient TypeScript declarations',
202
- rule: 'type',
203
- files: [
204
- {
205
- path: 'app/browser/env.d.mts',
206
- content: 'export interface EnvironmentInterface {}\n',
207
- },
208
- ],
209
- }),
210
- ).toEqual([])
211
- })
212
-
213
- it('excludes a .d.cts ambient declaration from placement', () => {
214
- expect(
215
- inspectPolicyControl({
216
- label: 'excludes a .d.cts ambient declaration from placement',
217
- membership: 'ambient TypeScript declarations',
218
- rule: 'type',
219
- files: [
220
- {
221
- path: 'app/browser/env.d.cts',
222
- content: 'export interface EnvironmentInterface {}\n',
223
- },
224
- ],
225
- }),
226
- ).toEqual([])
227
- })
228
-
229
- it('accepts a direct callback argument in constants.ts', () => {
230
- expect(
231
- inspectPolicyControl({
232
- label: 'accepts a direct callback argument in constants.ts',
233
- membership: 'anonymous callbacks passed directly as call arguments',
234
- rule: 'function',
235
- files: [
236
- {
237
- path: 'app/edge/constants.ts',
238
- content: 'export const LABELS = Object.freeze(COLUMNS.map((column) => column.label))\n',
239
- },
240
- ],
241
- }),
242
- ).toEqual([])
243
- })
244
-
245
- it('accepts a helper namespace in helpers.ts', () => {
246
- expect(
247
- inspectPolicyControl({
248
- label: 'accepts a helper namespace in helpers.ts',
249
- membership: 'camelCase helper namespaces containing function behavior',
250
- rule: 'data',
251
- files: [
252
- {
253
- path: 'app/edge/helpers.ts',
254
- content: 'export const formatters = Object.freeze({ money: build(fmt) })\n',
255
- },
256
- ],
257
- }),
258
- ).toEqual([])
259
- })
260
-
261
- it('accepts a concise-body direct return in constants.ts', () => {
262
- expect(
263
- inspectPolicyControl({
264
- label: 'accepts a concise-body direct return in constants.ts',
265
- membership: 'anonymous functions returned directly through a concise arrow body',
266
- rule: 'function',
267
- files: [
268
- {
269
- path: 'app/edge/constants.ts',
270
- content: 'export const WRAPPED = wrap(() => () => 1)\n',
271
- },
272
- ],
273
- }),
274
- ).toEqual([])
275
- })
276
-
277
- it('accepts a returned function inside a direct callback', () => {
278
- expect(
279
- inspectPolicyControl({
280
- label: 'accepts a returned function inside a direct callback',
281
- membership: 'anonymous functions returned directly by a return statement',
282
- rule: 'function',
283
- files: [
284
- {
285
- path: 'app/edge/constants.ts',
286
- content:
287
- 'export const LABELS = Object.freeze(COLUMNS.map((column) => { return () => column.label }))\n',
288
- },
289
- ],
290
- }),
291
- ).toEqual([])
292
- })
293
-
294
- it('accepts directly returned functions through callback control flow', () => {
295
- expect(
296
- inspectPolicyControl({
297
- label: 'accepts directly returned functions through callback control flow',
298
- membership: 'anonymous functions returned directly through control-flow branches',
299
- rule: 'function',
300
- files: [
301
- {
302
- path: 'app/edge/constants.ts',
303
- content:
304
- 'export const VALUES = Object.freeze(C.map((c) => { if (c) return () => 1; return () => 2 }))\n',
305
- },
306
- ],
307
- }),
308
- ).toEqual([])
309
- })
310
-
311
- it('excludes Vue text from the placement population', () => {
312
- expect(
313
- inspectPolicyControl({
314
- label: 'excludes Vue text from the placement population',
315
- membership: 'files outside the declared TypeScript source extensions',
316
- rule: 'type',
317
- files: [
318
- {
319
- path: 'app/browser/Panel.vue',
320
- content: '<script setup lang="ts">\nexport interface PanelInterface {}\n</script>\n',
321
- },
322
- ],
323
- }),
324
- ).toEqual([])
325
- })
326
-
327
183
  it('excludes documentation from the suppression population', () => {
328
184
  expect(
329
185
  inspectPolicyControl({
@@ -331,6 +187,10 @@ describe('policy population controls', () => {
331
187
  membership: 'files outside source, test, config, and script code',
332
188
  rule: 'suppression',
333
189
  files: [
190
+ // The manifest names the package this guide belongs to, so the prose sweep reads it
191
+ // as the workspace's own guide rather than as a top-level guide no evidence accounts
192
+ // for.
193
+ { path: 'package.json', content: PROSE_POLICY_MANIFEST },
334
194
  {
335
195
  path: 'guides/sample.md',
336
196
  content: `<!-- ${POLICY_SUPPRESSION_DIRECTIVE} -->\n`,
@@ -467,16 +327,6 @@ describe('portability policy', () => {
467
327
  })
468
328
  }
469
329
 
470
- for (const control of [
471
- PORTABILITY_POLICY_EXCLUSION,
472
- PORTABILITY_POLICY_LOCAL,
473
- PORTABILITY_POLICY_SPLIT,
474
- ]) {
475
- it(`${control.label} [membership: ${control.membership}]`, () => {
476
- expect(inspectPolicyControl(control)).toEqual([])
477
- })
478
- }
479
-
480
330
  it('rejects a character Windows refuses inside a path segment', () => {
481
331
  // A Windows host refuses to create this name, so the population itself is the control.
482
332
  expect(inspectPolicyFilenamePaths(['src/worker/read<write>.ts'])).toEqual([
@@ -510,8 +360,115 @@ describe('portability policy', () => {
510
360
  })
511
361
  })
512
362
 
363
+ describe('prose policy', () => {
364
+ for (const control of PROSE_POLICY_CONTROLS) {
365
+ it(`${control.label} [membership: ${control.membership}]`, () => {
366
+ const violations = inspectPolicyControl(control).filter(
367
+ (violation) => violation.rule === 'prose',
368
+ )
369
+ expect(violations).toHaveLength(1)
370
+ expect(violations[0]?.message).toBe(control.message)
371
+ expect(violations[0]?.line).toBe(control.line)
372
+ })
373
+ }
374
+
375
+ // A mirror is excluded from the term sweep, so the exclusion needs evidence rather than
376
+ // silence. The catalog table is that evidence, and it is a second mechanism: it is regenerated
377
+ // from the registry and names the fleet, while the guides directory names what this checkout
378
+ // holds. A guide neither side accounts for reports.
379
+ it('accounts for every top-level guide as this package, the index, or a catalog row', () => {
380
+ const root = process.cwd()
381
+ const own = readPolicyPackage(root)
382
+ if (own === undefined) throw new Error('The workspace manifest declares no name')
383
+ const guides = readPolicyProse(root).filter((path) => POLICY_MIRROR_PATTERN.test(path))
384
+ expect(guides.length).toBeGreaterThan(0)
385
+ expect(guides.filter((path) => isPolicyStray(root, path))).toEqual([])
386
+ const other = readPolicyCatalog(root).find((name) => name !== own)
387
+ if (other === undefined) throw new Error('The catalog registers no other package')
388
+ expect(isPolicyMirror(root, `guides/${other}.md`)).toBe(true)
389
+ expect(isPolicyMirror(root, `guides/${own}.md`)).toBe(false)
390
+ expect(isPolicyMirror(root, 'guides/README.md')).toBe(false)
391
+ expect(isPolicyStray(root, `guides/${own}.md`)).toBe(false)
392
+ expect(isPolicyStray(root, 'guides/README.md')).toBe(false)
393
+ // The control: a name no catalog row registers is a stray whatever the directory holds.
394
+ expect(isPolicyStray(root, 'guides/absent.md')).toBe(true)
395
+ })
396
+
397
+ it('reads the guide name a top-level path carries for another package to account for', () => {
398
+ const root = process.cwd()
399
+ const own = readPolicyPackage(root)
400
+ if (own === undefined) throw new Error('The workspace manifest declares no name')
401
+ expect(readPolicyGuide(root, 'guides/other.md')).toBe('other')
402
+ expect(readPolicyGuide(root, `guides/${own}.md`)).toBeUndefined()
403
+ expect(readPolicyGuide(root, 'guides/README.md')).toBeUndefined()
404
+ expect(readPolicyGuide(root, 'guides/nested/other.md')).toBeUndefined()
405
+ expect(readPolicyGuide(root, 'AGENTS.md')).toBeUndefined()
406
+ })
407
+
408
+ it('reads the authored Markdown population and excludes the directories it names', () => {
409
+ const paths = readPolicyProse(process.cwd())
410
+ expect(paths).toContain('README.md')
411
+ expect(paths).toContain('AGENTS.md')
412
+ expect(paths).toContain('guides/README.md')
413
+ expect(paths).toContain(POLICY_CATALOG_FILE)
414
+ expect(paths.some((path) => path.startsWith('node_modules/'))).toBe(false)
415
+ expect(paths.some((path) => path.startsWith('dist/'))).toBe(false)
416
+ expect(paths.some((path) => path.startsWith('tmp/'))).toBe(false)
417
+ expect(paths.some((path) => path.startsWith('.orkestrel/'))).toBe(false)
418
+ })
419
+ })
420
+
421
+ describe('denylist currency', () => {
422
+ // The denylist is a second copy of one column of the substitution table, so it drifts the
423
+ // moment a row is added or removed. Reading the table itself is the mechanism that could
424
+ // disagree with the copy, and every row has to land in exactly one of the two sets: matched
425
+ // unconditionally, or left to a reader because the row has a permitted sense.
426
+ // A target reads `.claude/rules/writing.md` from the installed scaffold copy rather than
427
+ // authoring it (the canon paragraph `src/core/templates.ts` emits into a generated `AGENTS.md`),
428
+ // so the table exists only where the workspace authors it. There the denylist and the table ship
429
+ // from one release; here they can drift, so the comparison runs here.
430
+ it.skipIf(!isPolicyFile(process.cwd(), POLICY_TERM_FILE))(
431
+ 'registers every substitution-table term as either matched or judged',
432
+ () => {
433
+ const registered = new Set([
434
+ ...POLICY_BANNED_TERMS.map((entry) => entry.term),
435
+ ...POLICY_JUDGED_TERMS,
436
+ ])
437
+ const table = readPolicyTerms(readFileSync(join(process.cwd(), POLICY_TERM_FILE), 'utf8'))
438
+ expect(table.length > 0).toBe(true)
439
+ expect([...registered].sort()).toEqual([...new Set(table)].sort())
440
+ },
441
+ )
442
+
443
+ it('reports a table row neither set names', () => {
444
+ // The control: a table carrying one row outside the membership, which the reader returns
445
+ // and neither set holds, so the comparison above would fail on it.
446
+ const fixture = [
447
+ '# Fixture',
448
+ '',
449
+ POLICY_TERM_HEADING,
450
+ '',
451
+ '| Term | Replacement |',
452
+ '| ---- | ----------- |',
453
+ '| `should` | `must` |',
454
+ '| `henceforth` (formal) | `after this` |',
455
+ '',
456
+ '## Not adopted',
457
+ '',
458
+ '| `unreached` | Outside the table |',
459
+ ].join('\n')
460
+ const table = readPolicyTerms(fixture)
461
+ expect(table).toEqual(['should', 'henceforth'])
462
+ const registered = new Set([
463
+ ...POLICY_BANNED_TERMS.map((entry) => entry.term),
464
+ ...POLICY_JUDGED_TERMS,
465
+ ])
466
+ expect(table.filter((term) => !registered.has(term))).toEqual(['henceforth'])
467
+ })
468
+ })
469
+
513
470
  describe('repository policy', () => {
514
- it('enforces placement and mirrors over the real workspace', () => {
471
+ it('enforces the mirror, suppression, skill, bridge, and portability laws over the real workspace', () => {
515
472
  expect(inspectPolicyWorkspace(process.cwd())).toEqual([])
516
473
  })
517
474
 
@@ -569,3 +526,30 @@ describe('repository policy', () => {
569
526
  expect(inspectPolicyPortability(process.cwd())).toEqual([])
570
527
  })
571
528
  })
529
+
530
+ describe('policy configuration wiring', () => {
531
+ it('reports a rule no top-level or override rules record enables', () => {
532
+ const configuration = { rules: {}, overrides: [{ files: ['*.ts'], rules: {} }] }
533
+ expect(inspectPolicyWiring(configuration, ['policy/no-mocking'], [])).toEqual([
534
+ 'policy/no-mocking is enabled by no top-level or override rules record',
535
+ ])
536
+ })
537
+
538
+ it('reports a population no override files list declares exactly', () => {
539
+ const configuration = { rules: {}, overrides: [{ files: ['app/**/*.ts'], rules: {} }] }
540
+ expect(inspectPolicyWiring(configuration, [], [['src/**/*.ts', 'app/**/*.ts']])).toEqual([
541
+ 'no override files list declares the population exactly: src/**/*.ts app/**/*.ts',
542
+ ])
543
+ })
544
+
545
+ it('reports a configuration that is not a record', () => {
546
+ expect(inspectPolicyWiring([], [], [])).toEqual(['Oxlint configuration must be a record'])
547
+ })
548
+
549
+ it('admits a plain record and refuses an array, null, and a primitive', () => {
550
+ expect(isPolicyRecord({})).toBe(true)
551
+ expect(isPolicyRecord([])).toBe(false)
552
+ expect(isPolicyRecord(null)).toBe(false)
553
+ expect(isPolicyRecord('record')).toBe(false)
554
+ })
555
+ })