@subbly/plugin-lint 0.0.5 → 0.0.8
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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/package.json +6 -6
- package/src/cron.mjs +1 -1
- package/src/eslint-plugin.mjs +1 -1
- package/src/report.mjs +16 -14
- package/src/rules.mjs +57 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @subbly/plugin-lint
|
|
2
2
|
|
|
3
|
+
## 0.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Plugin automations are now workflows, as in the builder. The manifest key is `extensions["co.subbly.builder"].workflows` and the instruction files live in `co.subbly.builder/workflows/<slug>.md`. The rule ids `automation/*` are now `workflow/*`. A manifest that still declares `automations` fails rule `manifest/namespace-unknown-key`, as the builder refresh rejects it.
|
|
8
|
+
|
|
9
|
+
## 0.0.7
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- A config field may carry `description` beside `label`: a string of 1-300 characters after trimming, shown under the input in the install dialog. Before, the linter rejected it as an unknown key (rule `manifest/namespace-unknown-key`), though the builder accepts it. A blank or over-long description fails rule `field/description`.
|
|
14
|
+
|
|
15
|
+
## 0.0.6
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 937b7e6: A connector marker may carry `liveTools` beside `auth` and `tools`: the read tools that a live page may call. It is a non-empty array of unique tool names, 1-100 characters each, as `tools` is. When `tools` is there too, each live tool must also be in `tools` (rule `connector/live-tools`). A marker with none of `auth`, `tools` and `liveTools` is rejected, as the builder refresh rejects it.
|
|
20
|
+
|
|
3
21
|
## 0.0.5
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ export default [
|
|
|
58
58
|
]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The gate is cross-file: whether a `plugin.json` is valid depends on `marketplace.json`, and a missing `
|
|
61
|
+
The gate is cross-file: whether a `plugin.json` is valid depends on `marketplace.json`, and a missing `workflows/<slug>.md` is a defect in a file that does not exist. ESLint visits one existing file at a time, so the engine runs once per invocation and hands each file the findings that belong to it. Every rule reaches the editor, absence checks included.
|
|
62
62
|
|
|
63
63
|
## Library
|
|
64
64
|
|
|
@@ -74,7 +74,7 @@ console.log(report.errors.length)
|
|
|
74
74
|
|
|
75
75
|
## What it checks
|
|
76
76
|
|
|
77
|
-
`marketplace.json` and every plugin manifest, `mcp.json` servers, skill and agent frontmatter,
|
|
77
|
+
`marketplace.json` and every plugin manifest, `mcp.json` servers, skill and agent frontmatter, workflow declarations against their files, cron floors, script lockfiles, plugin layout, and sandbox paths written in prose. Run `subbly-plugin-lint --rules` for the full list.
|
|
78
78
|
|
|
79
79
|
## Two severities
|
|
80
80
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subbly/plugin-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "The Subbly marketplace release gate, offline. Lints marketplace.json, plugin manifests, mcp.json, skills, agents and
|
|
6
|
+
"description": "The Subbly marketplace release gate, offline. Lints marketplace.json, plugin manifests, mcp.json, skills, agents and workflows.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "Subbly LTD <oleg@subbly.co> (https://www.subbly.co)",
|
|
9
9
|
"publishConfig": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"README.md",
|
|
32
32
|
"CHANGELOG.md"
|
|
33
33
|
],
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=20"
|
|
36
|
-
},
|
|
37
34
|
"scripts": {
|
|
38
35
|
"test": "node test/lint.test.mjs"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|
package/src/cron.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Cron parsing and the builder 15-minute schedule floor.
|
|
2
2
|
//
|
|
3
|
-
// Mirrors packages/core/src/lib/
|
|
3
|
+
// Mirrors packages/core/src/lib/workflows/schedule.ts: 101 firings sampled
|
|
4
4
|
// from 2026-01-01T00:00:00Z, rejected if any gap falls under the floor.
|
|
5
5
|
|
|
6
6
|
export const SCHEDULE_FLOOR_MINUTES = 15
|
package/src/eslint-plugin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ESLint bridge.
|
|
2
2
|
//
|
|
3
3
|
// The release gate is cross-file: whether plugin.json is valid depends on
|
|
4
|
-
// marketplace.json, and a missing
|
|
4
|
+
// marketplace.json, and a missing workflows/<slug>.md is a defect in a file
|
|
5
5
|
// that does not exist. ESLint visits one existing file at a time, so rules
|
|
6
6
|
// cannot be written that way directly.
|
|
7
7
|
//
|
package/src/report.mjs
CHANGED
|
@@ -41,6 +41,7 @@ export const RULES = {
|
|
|
41
41
|
'field/key-format': 'field keys must match /^[A-Z][A-Z0-9_]*$/',
|
|
42
42
|
'field/key-duplicate': 'a duplicate field key silently overrides the first',
|
|
43
43
|
'field/label': 'label is required, 1-100 characters',
|
|
44
|
+
'field/description': 'description is optional, 1-300 characters after trimming',
|
|
44
45
|
'field/type': 'type must be "text" or "select"',
|
|
45
46
|
'field/options': 'options belong to select fields and select fields require them',
|
|
46
47
|
'field/public-secret': 'public and secret are mutually exclusive',
|
|
@@ -49,24 +50,25 @@ export const RULES = {
|
|
|
49
50
|
'field/default-required': 'a default plugin cannot declare required fields',
|
|
50
51
|
'field/reserved-key': 'a field key must not restate the PLUGIN_ prefix the builder adds',
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
53
|
+
// workflows
|
|
54
|
+
'workflow/key-slug': 'workflow keys must be valid slugs',
|
|
55
|
+
'workflow/unknown-key': 'a declaration holds exactly name, schedule and model',
|
|
56
|
+
'workflow/name': 'name is required, 1-100 characters',
|
|
57
|
+
'workflow/schedule-syntax': 'schedule must be a cron expression the builder can parse',
|
|
58
|
+
'workflow/schedule-floor': 'schedule must fire at most every 15 minutes',
|
|
59
|
+
'workflow/schedule-unverified': 'this cron syntax cannot be checked offline',
|
|
60
|
+
'workflow/model': 'model must be normal or intelligent-high',
|
|
61
|
+
'workflow/missing-file': 'every declared workflow needs its markdown file',
|
|
62
|
+
'workflow/orphan-file': 'every workflow file needs a declaration',
|
|
63
|
+
'workflow/empty-file': 'a workflow file must not be empty',
|
|
64
|
+
'workflow/frontmatter': 'workflow frontmatter is not stripped and leaks into the instruction',
|
|
64
65
|
'setup/missing-install-skill': 'setup: true requires a skills/install skill',
|
|
65
66
|
|
|
66
67
|
// connectors and mcp.json
|
|
67
|
-
'connector/marker': 'a connector marker holds auth, tools
|
|
68
|
+
'connector/marker': 'a connector marker holds one or more of auth, tools and liveTools, and nothing else',
|
|
68
69
|
'connector/auth': 'auth, when present, is exactly "oauth"',
|
|
69
|
-
'connector/tools': 'tools
|
|
70
|
+
'connector/tools': 'tools and liveTools are each a non-empty array of unique tool names, 1-100 characters each',
|
|
71
|
+
'connector/live-tools': 'each live tool is also in tools, when tools is present',
|
|
70
72
|
'connector/name-length': 'connector names are 1-100 characters',
|
|
71
73
|
'connector/undefined-server': 'a declared connector needs a matching mcp.json server',
|
|
72
74
|
'connector/no-mcp': 'declaring a connector requires an mcp.json',
|
package/src/rules.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Mirrors subbly-builder branch feat/plugins-mcp:
|
|
4
4
|
// packages/plugins/src/marketplace/{marketplace-manifest,plugin-manifest,mcp-config}.ts
|
|
5
5
|
// packages/core/src/types/plugin-manifest.ts
|
|
6
|
-
// packages/core/src/lib/
|
|
6
|
+
// packages/core/src/lib/workflows/schedule.ts
|
|
7
7
|
//
|
|
8
8
|
// error = the refresh throws, so NO plugin in the marketplace releases.
|
|
9
9
|
// warn = the builder accepts it, but it is wrong, dead, or broken at runtime.
|
|
@@ -34,10 +34,10 @@ const CONNECTOR_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,49}$/
|
|
|
34
34
|
const CONTENT_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i
|
|
35
35
|
|
|
36
36
|
const MANIFEST_KEYS = ['$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'extensions']
|
|
37
|
-
const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', '
|
|
38
|
-
const FIELD_KEYS = ['key', 'label', 'type', 'required', 'secret', 'public', 'pattern', 'options']
|
|
39
|
-
const
|
|
40
|
-
const
|
|
37
|
+
const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', 'workflows', 'setup', 'connectors']
|
|
38
|
+
const FIELD_KEYS = ['key', 'label', 'description', 'type', 'required', 'secret', 'public', 'pattern', 'options']
|
|
39
|
+
const WORKFLOW_KEYS = ['name', 'schedule', 'model']
|
|
40
|
+
const WORKFLOW_MODELS = ['normal', 'intelligent-high']
|
|
41
41
|
const SERVER_KEYS = ['type', 'url', 'headers']
|
|
42
42
|
|
|
43
43
|
export const AGENT_TOOLS = ['read_file', 'write_file', 'edit_file', 'apply_patch', 'execute_command', 'get_stock_image', 'web_fetch', 'web_search', 'generate_image', 'restart_preview', 'apply_to_preview', 'tool_run', 'tool_search', 'tool_describe', 'skill']
|
|
@@ -53,7 +53,7 @@ export const DEFAULT_ENV_KEYS = [
|
|
|
53
53
|
|
|
54
54
|
const RESERVED_SLUG = 'custom'
|
|
55
55
|
|
|
56
|
-
const MISPLACED = ['agents', '
|
|
56
|
+
const MISPLACED = ['agents', 'workflows', 'scripts', 'instructions.md']
|
|
57
57
|
|
|
58
58
|
// The builder-maintained symlink to the installed version, the only stable
|
|
59
59
|
// segment prose may use in a sandbox path.
|
|
@@ -409,7 +409,7 @@ function lintPluginManifest(root, entry, report) {
|
|
|
409
409
|
check.boolean('manifest/boolean', `${ns}.setup`, ext.setup)
|
|
410
410
|
|
|
411
411
|
const fields = lintFields(report, file, at, check, ns, ext, entry.slug)
|
|
412
|
-
const
|
|
412
|
+
const workflows = lintWorkflowDeclarations(report, file, at, check, ns, ext)
|
|
413
413
|
const connectors = lintConnectors(report, file, at, check, ns, ext)
|
|
414
414
|
|
|
415
415
|
if (ext.default === true && fields.some((f) => f.required === true)) {
|
|
@@ -421,7 +421,7 @@ function lintPluginManifest(root, entry, report) {
|
|
|
421
421
|
)
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
return { fieldKeys: fields.map((f) => f.key).filter(Boolean),
|
|
424
|
+
return { fieldKeys: fields.map((f) => f.key).filter(Boolean), workflows, connectors, setup: ext.setup === true }
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
function lintFields(report, file, at, check, ns, ext, slug) {
|
|
@@ -458,6 +458,9 @@ function lintFields(report, file, at, check, ns, ext, slug) {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
check.string('field/label', `${p}.label`, field.label, { min: 1, max: 100, required: true, label: `${label}.label` })
|
|
461
|
+
if (field.description !== undefined) {
|
|
462
|
+
check.string('field/description', `${p}.description`, field.description, { min: 1, max: 300, trim: true, label: `${label}.description` })
|
|
463
|
+
}
|
|
461
464
|
|
|
462
465
|
if (field.type !== 'text' && field.type !== 'select') {
|
|
463
466
|
report.error('field/type', file, `${label}.type: expected "text" or "select", received ${JSON.stringify(field.type)}`, at(`${p}.type`))
|
|
@@ -507,39 +510,39 @@ function lintFields(report, file, at, check, ns, ext, slug) {
|
|
|
507
510
|
return ext.fields.filter(isObject)
|
|
508
511
|
}
|
|
509
512
|
|
|
510
|
-
function
|
|
511
|
-
if (ext.
|
|
512
|
-
if (!isObject(ext.
|
|
513
|
-
report.error('
|
|
513
|
+
function lintWorkflowDeclarations(report, file, at, check, ns, ext) {
|
|
514
|
+
if (ext.workflows === undefined) return []
|
|
515
|
+
if (!isObject(ext.workflows)) {
|
|
516
|
+
report.error('workflow/key-slug', file, `${ns}.workflows: expected object keyed by slug, received ${typeOf(ext.workflows)}`, at(`${ns}.workflows`))
|
|
514
517
|
return []
|
|
515
518
|
}
|
|
516
519
|
|
|
517
520
|
const slugs = []
|
|
518
|
-
for (const [slug, decl] of Object.entries(ext.
|
|
519
|
-
const p = `${ns}.
|
|
520
|
-
const label = `
|
|
521
|
+
for (const [slug, decl] of Object.entries(ext.workflows)) {
|
|
522
|
+
const p = `${ns}.workflows.${slug}`
|
|
523
|
+
const label = `workflows["${slug}"]`
|
|
521
524
|
|
|
522
525
|
if (slug.length < 1 || slug.length > 64 || !SLUG_PATTERN.test(slug)) {
|
|
523
|
-
report.error('
|
|
526
|
+
report.error('workflow/key-slug', file, `${label}: key ${SLUG_MESSAGE}`, at(p, { key: true }))
|
|
524
527
|
continue
|
|
525
528
|
}
|
|
526
529
|
if (!isObject(decl)) {
|
|
527
|
-
report.error('
|
|
530
|
+
report.error('workflow/unknown-key', file, `${label}: expected object, received ${typeOf(decl)}`, at(p))
|
|
528
531
|
continue
|
|
529
532
|
}
|
|
530
533
|
|
|
531
|
-
check.strictKeys('
|
|
532
|
-
check.string('
|
|
534
|
+
check.strictKeys('workflow/unknown-key', p, decl, WORKFLOW_KEYS, label)
|
|
535
|
+
check.string('workflow/name', `${p}.name`, decl.name, { min: 1, max: 100, trim: true, required: true, label: `${label}.name` })
|
|
533
536
|
|
|
534
|
-
if (check.string('
|
|
537
|
+
if (check.string('workflow/schedule-syntax', `${p}.schedule`, decl.schedule, { min: 1, max: 100, required: true, label: `${label}.schedule` })) {
|
|
535
538
|
lintSchedule(report, file, at(`${p}.schedule`), label, decl.schedule)
|
|
536
539
|
}
|
|
537
540
|
|
|
538
|
-
if (!
|
|
541
|
+
if (!WORKFLOW_MODELS.includes(decl.model)) {
|
|
539
542
|
report.error(
|
|
540
|
-
'
|
|
543
|
+
'workflow/model',
|
|
541
544
|
file,
|
|
542
|
-
`${label}.model: expected ${
|
|
545
|
+
`${label}.model: expected ${WORKFLOW_MODELS.join(' | ')}, received ${JSON.stringify(decl.model)}`,
|
|
543
546
|
at(`${p}.model`)
|
|
544
547
|
)
|
|
545
548
|
}
|
|
@@ -554,17 +557,17 @@ function lintSchedule(report, file, pos, label, schedule) {
|
|
|
554
557
|
cron = parseCron(schedule)
|
|
555
558
|
} catch (e) {
|
|
556
559
|
if (e instanceof CronUnsupported) {
|
|
557
|
-
report.warn('
|
|
560
|
+
report.warn('workflow/schedule-unverified', file, `${label}.schedule contains "${e.message}", which cannot be checked offline`, {
|
|
558
561
|
...pos,
|
|
559
562
|
hint: 'the builder may accept it; confirm the 15-minute floor by hand',
|
|
560
563
|
})
|
|
561
564
|
return
|
|
562
565
|
}
|
|
563
|
-
report.error('
|
|
566
|
+
report.error('workflow/schedule-syntax', file, `${label}.schedule: invalid cron expression (${e.message})`, pos)
|
|
564
567
|
return
|
|
565
568
|
}
|
|
566
569
|
if (!meetsFloor(cron)) {
|
|
567
|
-
report.error('
|
|
570
|
+
report.error('workflow/schedule-floor', file, `${label}.schedule must fire at most every ${SCHEDULE_FLOOR_MINUTES} minutes`, pos)
|
|
568
571
|
}
|
|
569
572
|
}
|
|
570
573
|
|
|
@@ -587,9 +590,9 @@ function lintConnectors(report, file, at, check, ns, ext) {
|
|
|
587
590
|
report.error('connector/marker', file, `${label}: expected object, received ${typeOf(marker)}`, at(p))
|
|
588
591
|
continue
|
|
589
592
|
}
|
|
590
|
-
check.strictKeys('connector/marker', p, marker, ['auth', 'tools'], label)
|
|
591
|
-
if (marker.auth === undefined && marker.tools === undefined) {
|
|
592
|
-
report.error('connector/marker', file, `${label}: declare auth, tools or
|
|
593
|
+
check.strictKeys('connector/marker', p, marker, ['auth', 'tools', 'liveTools'], label)
|
|
594
|
+
if (marker.auth === undefined && marker.tools === undefined && marker.liveTools === undefined) {
|
|
595
|
+
report.error('connector/marker', file, `${label}: declare auth, tools or liveTools`, at(p))
|
|
593
596
|
}
|
|
594
597
|
if (marker.auth !== undefined && marker.auth !== 'oauth') {
|
|
595
598
|
report.error('connector/auth', file, `${label}.auth: expected "oauth", received ${JSON.stringify(marker.auth)}`, at(`${p}.auth`))
|
|
@@ -597,6 +600,10 @@ function lintConnectors(report, file, at, check, ns, ext) {
|
|
|
597
600
|
if (marker.tools !== undefined) {
|
|
598
601
|
lintToolAllowlist(report, file, at, check, `${p}.tools`, `${label}.tools`, marker.tools)
|
|
599
602
|
}
|
|
603
|
+
if (marker.liveTools !== undefined) {
|
|
604
|
+
lintToolAllowlist(report, file, at, check, `${p}.liveTools`, `${label}.liveTools`, marker.liveTools)
|
|
605
|
+
lintLiveToolsInTools(report, file, at, p, label, marker)
|
|
606
|
+
}
|
|
600
607
|
names.push(name)
|
|
601
608
|
}
|
|
602
609
|
return names
|
|
@@ -624,6 +631,15 @@ function lintToolAllowlist(report, file, at, check, p, label, tools) {
|
|
|
624
631
|
}
|
|
625
632
|
}
|
|
626
633
|
|
|
634
|
+
function lintLiveToolsInTools(report, file, at, p, label, marker) {
|
|
635
|
+
if (!Array.isArray(marker.tools) || !Array.isArray(marker.liveTools)) return
|
|
636
|
+
for (const [index, tool] of marker.liveTools.entries()) {
|
|
637
|
+
if (typeof tool === 'string' && !marker.tools.includes(tool)) {
|
|
638
|
+
report.error('connector/live-tools', file, `${label}.liveTools[${index}]: live tool "${tool}" must also be in tools`, at(`${p}.liveTools.${index}`))
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
627
643
|
// --- mcp.json --------------------------------------------------------------
|
|
628
644
|
|
|
629
645
|
function lintMcp(root, entry, manifest, report) {
|
|
@@ -960,10 +976,10 @@ function lintAgents(root, entry, report) {
|
|
|
960
976
|
}
|
|
961
977
|
}
|
|
962
978
|
|
|
963
|
-
// ---
|
|
979
|
+
// --- workflows -----------------------------------------------------------
|
|
964
980
|
|
|
965
|
-
function
|
|
966
|
-
const dir = join(entry.dir, NAMESPACE, '
|
|
981
|
+
function lintWorkflowFiles(root, entry, declared, report) {
|
|
982
|
+
const dir = join(entry.dir, NAMESPACE, 'workflows')
|
|
967
983
|
const present = new Set()
|
|
968
984
|
|
|
969
985
|
for (const dirent of listDir(dir)) {
|
|
@@ -975,10 +991,10 @@ function lintAutomationFiles(root, entry, declared, report) {
|
|
|
975
991
|
const orphans = [...present].filter((s) => !declared.includes(s))
|
|
976
992
|
for (const orphan of orphans) {
|
|
977
993
|
report.error(
|
|
978
|
-
'
|
|
994
|
+
'workflow/orphan-file',
|
|
979
995
|
relative(root, join(dir, `${orphan}.md`)),
|
|
980
|
-
`Invalid
|
|
981
|
-
{ line: 1, hint: `declare it in plugin.json under extensions["${NAMESPACE}"].
|
|
996
|
+
`Invalid workflows of ${entry.slug}: files without a manifest declaration: ${orphans.join(', ')}`,
|
|
997
|
+
{ line: 1, hint: `declare it in plugin.json under extensions["${NAMESPACE}"].workflows, or delete it` }
|
|
982
998
|
)
|
|
983
999
|
}
|
|
984
1000
|
|
|
@@ -986,19 +1002,19 @@ function lintAutomationFiles(root, entry, declared, report) {
|
|
|
986
1002
|
const path = join(dir, `${slug}.md`)
|
|
987
1003
|
if (!present.has(slug)) {
|
|
988
1004
|
report.error(
|
|
989
|
-
'
|
|
1005
|
+
'workflow/missing-file',
|
|
990
1006
|
relative(root, join(entry.dir, 'plugin.json')),
|
|
991
|
-
`Invalid
|
|
992
|
-
{ hint: `create ${NAMESPACE}/
|
|
1007
|
+
`Invalid workflows of ${entry.slug}: missing workflows/${slug}.md`,
|
|
1008
|
+
{ hint: `create ${NAMESPACE}/workflows/${slug}.md` }
|
|
993
1009
|
)
|
|
994
1010
|
continue
|
|
995
1011
|
}
|
|
996
1012
|
const text = readIfExists(path) ?? ''
|
|
997
1013
|
if (text.trim() === '') {
|
|
998
|
-
report.error('
|
|
1014
|
+
report.error('workflow/empty-file', relative(root, path), `Invalid workflows of ${entry.slug}: workflows/${slug}.md is empty`, { line: 1 })
|
|
999
1015
|
}
|
|
1000
1016
|
if (text.trimStart().startsWith('---')) {
|
|
1001
|
-
report.warn('
|
|
1017
|
+
report.warn('workflow/frontmatter', relative(root, path), 'frontmatter here is not parsed or stripped, so it lands verbatim in the agent instruction', {
|
|
1002
1018
|
line: 1,
|
|
1003
1019
|
hint: 'name, schedule and model belong in plugin.json',
|
|
1004
1020
|
})
|
|
@@ -1163,7 +1179,7 @@ export function lint(root, { git = true } = {}) {
|
|
|
1163
1179
|
lintMcp(root, entry, manifest, report)
|
|
1164
1180
|
lintSkills(root, entry, report)
|
|
1165
1181
|
lintAgents(root, entry, report)
|
|
1166
|
-
|
|
1182
|
+
lintWorkflowFiles(root, entry, manifest?.workflows ?? [], report)
|
|
1167
1183
|
lintSetup(root, entry, manifest, report)
|
|
1168
1184
|
lintLayout(root, entry, report)
|
|
1169
1185
|
}
|