@orkestrel/scaffold 0.0.54 → 0.0.56
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 +21 -6
- package/dist/bin/main.js +2 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/orchestration.md +6 -3
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +28 -2
- package/dist/host/claude/agents/orkestrel.md +12 -8
- package/dist/host/claude/rules/architecture.md +11 -0
- package/dist/host/claude/rules/portability.md +98 -0
- package/dist/host/claude/rules/quality.md +1 -0
- package/dist/host/guides/scaffold.md +187 -68
- package/dist/host/manifest.json +16 -10
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/policy.test.ts +154 -3
- package/dist/host/tests/setupPolicy.ts +540 -4
- package/dist/src/core/index.cjs +199 -43
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +113 -12
- package/dist/src/core/index.d.ts +113 -12
- package/dist/src/core/index.js +198 -44
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +140 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +108 -9
- package/dist/src/server/index.d.ts +108 -9
- package/dist/src/server/index.js +141 -15
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
1
3
|
import { describe, expect, it } from 'vitest'
|
|
2
4
|
import {
|
|
3
5
|
BRIDGE_POLICY_CONTROLS,
|
|
4
6
|
createPolicyScratch,
|
|
7
|
+
createSkillMetadata,
|
|
5
8
|
FUNCTION_SOURCE_FILES,
|
|
6
9
|
GENERIC_POLICY_SOURCES,
|
|
7
10
|
inspectPolicyControl,
|
|
11
|
+
inspectPolicyFilenamePaths,
|
|
8
12
|
inspectPolicyMirrorPaths,
|
|
13
|
+
inspectPolicyPortability,
|
|
9
14
|
inspectPolicySources,
|
|
10
15
|
inspectPolicyWorkspace,
|
|
11
16
|
inspectSkillFamily,
|
|
@@ -14,7 +19,15 @@ import {
|
|
|
14
19
|
parseSkillFrontmatter,
|
|
15
20
|
POLICY_CONTROLS,
|
|
16
21
|
POLICY_SUPPRESSION_DIRECTIVE,
|
|
22
|
+
PORTABILITY_POLICY_CONTROLS,
|
|
23
|
+
PORTABILITY_POLICY_EXCLUSION,
|
|
24
|
+
PORTABILITY_POLICY_LOCAL,
|
|
25
|
+
PORTABILITY_POLICY_SPLIT,
|
|
26
|
+
readPolicyPaths,
|
|
17
27
|
readSkillFamily,
|
|
28
|
+
RULES_POLICY_CONTROLS,
|
|
29
|
+
SKILL_BRIDGE_ROOT,
|
|
30
|
+
SKILL_FAMILY_ROOT,
|
|
18
31
|
SKILL_POLICY_APOSTROPHE,
|
|
19
32
|
SKILL_POLICY_BACKTICKED,
|
|
20
33
|
SKILL_POLICY_CONTROLS,
|
|
@@ -22,6 +35,7 @@ import {
|
|
|
22
35
|
SKILL_POLICY_FENCED,
|
|
23
36
|
SKILL_POLICY_FOLDED,
|
|
24
37
|
SKILL_POLICY_PARAGRAPHS,
|
|
38
|
+
SKILL_POLICY_TEXT,
|
|
25
39
|
stemToPolicyCandidates,
|
|
26
40
|
testToPolicyStem,
|
|
27
41
|
} from './setupPolicy.js'
|
|
@@ -337,10 +351,29 @@ describe('instrument negative controls', () => {
|
|
|
337
351
|
})
|
|
338
352
|
|
|
339
353
|
describe('skill family policy', () => {
|
|
340
|
-
|
|
354
|
+
// The family is read from the workspace it runs in, so a membership literal would
|
|
355
|
+
// bind this file to one workspace. The relationship binds in every workspace: a
|
|
356
|
+
// direct `node:fs` read of the canonical root is a second mechanism that reports
|
|
357
|
+
// the same directories, and reports none where the root is absent.
|
|
358
|
+
//
|
|
359
|
+
// The root is spelled here as literal segments rather than read from
|
|
360
|
+
// `SKILL_FAMILY_ROOT`, and that literal is what makes this read a second
|
|
361
|
+
// mechanism. Both sides reading the constant would move together when it drifts,
|
|
362
|
+
// so the case would stay green for every value the constant ever holds. Against
|
|
363
|
+
// the literal, a drifted constant desyncs the sides and reddens this case in a
|
|
364
|
+
// workspace that has the tree, while a workspace without one still passes on
|
|
365
|
+
// both readings being empty.
|
|
366
|
+
it('discovers exactly the directories the canonical skill root holds', () => {
|
|
367
|
+
const root = join(process.cwd(), '.agents', 'skills')
|
|
368
|
+
const held = existsSync(root)
|
|
369
|
+
? readdirSync(root, { withFileTypes: true })
|
|
370
|
+
.filter((entry) => entry.isDirectory())
|
|
371
|
+
.map((entry) => entry.name)
|
|
372
|
+
.sort()
|
|
373
|
+
: []
|
|
341
374
|
const family = readSkillFamily(process.cwd())
|
|
342
|
-
expect(family.length).
|
|
343
|
-
expect(family).
|
|
375
|
+
expect(family.length > 0).toBe(held.length > 0)
|
|
376
|
+
expect([...family]).toEqual(held)
|
|
344
377
|
})
|
|
345
378
|
|
|
346
379
|
it('requires every discovered skill file, metadata token, and reference', () => {
|
|
@@ -413,8 +446,126 @@ describe('skill bridge policy', () => {
|
|
|
413
446
|
}
|
|
414
447
|
})
|
|
415
448
|
|
|
449
|
+
describe('rule map policy', () => {
|
|
450
|
+
for (const control of RULES_POLICY_CONTROLS) {
|
|
451
|
+
it(`${control.label} [membership: ${control.membership}]`, () => {
|
|
452
|
+
const violations = inspectPolicyControl(control)
|
|
453
|
+
expect(violations).toHaveLength(1)
|
|
454
|
+
expect(violations[0]?.rule).toBe(control.rule)
|
|
455
|
+
expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
describe('portability policy', () => {
|
|
461
|
+
for (const control of PORTABILITY_POLICY_CONTROLS) {
|
|
462
|
+
it(`${control.label} [membership: ${control.membership}]`, () => {
|
|
463
|
+
const violations = inspectPolicyControl(control)
|
|
464
|
+
expect(violations).toHaveLength(1)
|
|
465
|
+
expect(violations[0]?.rule).toBe(control.rule)
|
|
466
|
+
expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
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
|
+
it('rejects a character Windows refuses inside a path segment', () => {
|
|
481
|
+
// A Windows host refuses to create this name, so the population itself is the control.
|
|
482
|
+
expect(inspectPolicyFilenamePaths(['src/worker/read<write>.ts'])).toEqual([
|
|
483
|
+
{
|
|
484
|
+
rule: 'portability',
|
|
485
|
+
path: 'src/worker/read<write>.ts',
|
|
486
|
+
message: 'path segments avoid the characters Windows refuses',
|
|
487
|
+
},
|
|
488
|
+
])
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('rejects sibling paths that differ by case alone', () => {
|
|
492
|
+
// A Windows host folds the pair into one file, so the population itself is the control.
|
|
493
|
+
expect(inspectPolicyFilenamePaths(['guides/Readme.md', 'guides/readme.md'])).toEqual([
|
|
494
|
+
{
|
|
495
|
+
rule: 'portability',
|
|
496
|
+
path: 'guides/readme.md',
|
|
497
|
+
message: 'path differs from guides/Readme.md by case alone',
|
|
498
|
+
},
|
|
499
|
+
])
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('accepts sibling paths that differ by more than case', () => {
|
|
503
|
+
expect(
|
|
504
|
+
inspectPolicyFilenamePaths([
|
|
505
|
+
'guides/readme.md',
|
|
506
|
+
'guides/readmes.md',
|
|
507
|
+
'src/worker/helpers.ts',
|
|
508
|
+
]),
|
|
509
|
+
).toEqual([])
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
416
513
|
describe('repository policy', () => {
|
|
417
514
|
it('enforces placement and mirrors over the real workspace', () => {
|
|
418
515
|
expect(inspectPolicyWorkspace(process.cwd())).toEqual([])
|
|
419
516
|
})
|
|
517
|
+
|
|
518
|
+
// A target reads the canon from the installed package, so its tree carries the
|
|
519
|
+
// pointer pair and no `.agents/` directory, no rule map, and no skill bridges.
|
|
520
|
+
// This vendored suite runs there, and every inspector it routes through has to
|
|
521
|
+
// stay silent on that shape.
|
|
522
|
+
it('accepts a target holding the pointer pair and no canon tree', () => {
|
|
523
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-policy-pointer-' })
|
|
524
|
+
try {
|
|
525
|
+
scratch.write(
|
|
526
|
+
'AGENTS.md',
|
|
527
|
+
'# AGENTS.md\n\nRead `node_modules/@orkestrel/scaffold/dist/host/AGENTS.md` for the canon.\n',
|
|
528
|
+
)
|
|
529
|
+
scratch.write(
|
|
530
|
+
'CLAUDE.md',
|
|
531
|
+
'# Claude Code bridge\n\nRead the `AGENTS.md` file beside this one first.\n',
|
|
532
|
+
)
|
|
533
|
+
scratch.write('.claude/settings.json', '{\n\t"permissions": {\n\t\t"allow": []\n\t}\n}\n')
|
|
534
|
+
scratch.write(
|
|
535
|
+
'.claude/agents/orkestrel.md',
|
|
536
|
+
'# Orkestrel\n\nThe agent carrying the package catalog.\n',
|
|
537
|
+
)
|
|
538
|
+
scratch.write(
|
|
539
|
+
'package.json',
|
|
540
|
+
'{\n\t"name": "target",\n\t"private": true,\n\t"scripts": {\n\t\t"test": "vitest run"\n\t}\n}\n',
|
|
541
|
+
)
|
|
542
|
+
expect(inspectPolicyWorkspace(scratch.path)).toEqual([])
|
|
543
|
+
// The control: the same workspace with one canonical skill planted and no
|
|
544
|
+
// bridge beside it reports the twin violation, so the empty result above is a
|
|
545
|
+
// sweep that ran rather than a sweep with nothing it could report.
|
|
546
|
+
scratch.write(`${SKILL_FAMILY_ROOT}/sample/SKILL.md`, SKILL_POLICY_TEXT)
|
|
547
|
+
scratch.write(`${SKILL_FAMILY_ROOT}/sample/agents/openai.yaml`, createSkillMetadata('sample'))
|
|
548
|
+
expect(inspectPolicyWorkspace(scratch.path)).toEqual([
|
|
549
|
+
{
|
|
550
|
+
rule: 'bridge',
|
|
551
|
+
path: `${SKILL_BRIDGE_ROOT}/sample`,
|
|
552
|
+
message: 'canonical skill has a matching provider bridge directory',
|
|
553
|
+
},
|
|
554
|
+
])
|
|
555
|
+
} finally {
|
|
556
|
+
scratch.destroy()
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('reaches every branch of the workspace-authored path population', () => {
|
|
561
|
+
const paths = readPolicyPaths(process.cwd())
|
|
562
|
+
expect(paths).toContain('tests/setupPolicy.ts')
|
|
563
|
+
expect(paths).toContain('.claude/settings.json')
|
|
564
|
+
expect(paths).toContain('package.json')
|
|
565
|
+
expect(paths).toContain('.gitattributes')
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('keeps every workspace path, script, and source portable', () => {
|
|
569
|
+
expect(inspectPolicyPortability(process.cwd())).toEqual([])
|
|
570
|
+
})
|
|
420
571
|
})
|