@subbly/plugin-lint 0.0.7 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.0.7
4
10
 
5
11
  ### 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 `automations/<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.
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, automation 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.
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.7",
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 automations.",
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/automations/schedule.ts: 101 firings sampled
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
@@ -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 automations/<slug>.md is a defect in a file
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
@@ -50,18 +50,18 @@ export const RULES = {
50
50
  'field/default-required': 'a default plugin cannot declare required fields',
51
51
  'field/reserved-key': 'a field key must not restate the PLUGIN_ prefix the builder adds',
52
52
 
53
- // automations
54
- 'automation/key-slug': 'automation keys must be valid slugs',
55
- 'automation/unknown-key': 'a declaration holds exactly name, schedule and model',
56
- 'automation/name': 'name is required, 1-100 characters',
57
- 'automation/schedule-syntax': 'schedule must be a cron expression the builder can parse',
58
- 'automation/schedule-floor': 'schedule must fire at most every 15 minutes',
59
- 'automation/schedule-unverified': 'this cron syntax cannot be checked offline',
60
- 'automation/model': 'model must be normal or intelligent-high',
61
- 'automation/missing-file': 'every declared automation needs its markdown file',
62
- 'automation/orphan-file': 'every automation file needs a declaration',
63
- 'automation/empty-file': 'an automation file must not be empty',
64
- 'automation/frontmatter': 'automation frontmatter is not stripped and leaks into the instruction',
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',
65
65
  'setup/missing-install-skill': 'setup: true requires a skills/install skill',
66
66
 
67
67
  // connectors and 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/automations/schedule.ts
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', 'automations', 'setup', 'connectors']
37
+ const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', 'workflows', 'setup', 'connectors']
38
38
  const FIELD_KEYS = ['key', 'label', 'description', 'type', 'required', 'secret', 'public', 'pattern', 'options']
39
- const AUTOMATION_KEYS = ['name', 'schedule', 'model']
40
- const AUTOMATION_MODELS = ['normal', 'intelligent-high']
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', 'automations', 'scripts', 'instructions.md']
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 automations = lintAutomationDeclarations(report, file, at, check, ns, ext)
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), automations, connectors, setup: ext.setup === true }
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) {
@@ -510,39 +510,39 @@ function lintFields(report, file, at, check, ns, ext, slug) {
510
510
  return ext.fields.filter(isObject)
511
511
  }
512
512
 
513
- function lintAutomationDeclarations(report, file, at, check, ns, ext) {
514
- if (ext.automations === undefined) return []
515
- if (!isObject(ext.automations)) {
516
- report.error('automation/key-slug', file, `${ns}.automations: expected object keyed by slug, received ${typeOf(ext.automations)}`, at(`${ns}.automations`))
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`))
517
517
  return []
518
518
  }
519
519
 
520
520
  const slugs = []
521
- for (const [slug, decl] of Object.entries(ext.automations)) {
522
- const p = `${ns}.automations.${slug}`
523
- const label = `automations["${slug}"]`
521
+ for (const [slug, decl] of Object.entries(ext.workflows)) {
522
+ const p = `${ns}.workflows.${slug}`
523
+ const label = `workflows["${slug}"]`
524
524
 
525
525
  if (slug.length < 1 || slug.length > 64 || !SLUG_PATTERN.test(slug)) {
526
- report.error('automation/key-slug', file, `${label}: key ${SLUG_MESSAGE}`, at(p, { key: true }))
526
+ report.error('workflow/key-slug', file, `${label}: key ${SLUG_MESSAGE}`, at(p, { key: true }))
527
527
  continue
528
528
  }
529
529
  if (!isObject(decl)) {
530
- report.error('automation/unknown-key', file, `${label}: expected object, received ${typeOf(decl)}`, at(p))
530
+ report.error('workflow/unknown-key', file, `${label}: expected object, received ${typeOf(decl)}`, at(p))
531
531
  continue
532
532
  }
533
533
 
534
- check.strictKeys('automation/unknown-key', p, decl, AUTOMATION_KEYS, label)
535
- check.string('automation/name', `${p}.name`, decl.name, { min: 1, max: 100, trim: true, required: true, label: `${label}.name` })
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` })
536
536
 
537
- if (check.string('automation/schedule-syntax', `${p}.schedule`, decl.schedule, { min: 1, max: 100, required: true, label: `${label}.schedule` })) {
537
+ if (check.string('workflow/schedule-syntax', `${p}.schedule`, decl.schedule, { min: 1, max: 100, required: true, label: `${label}.schedule` })) {
538
538
  lintSchedule(report, file, at(`${p}.schedule`), label, decl.schedule)
539
539
  }
540
540
 
541
- if (!AUTOMATION_MODELS.includes(decl.model)) {
541
+ if (!WORKFLOW_MODELS.includes(decl.model)) {
542
542
  report.error(
543
- 'automation/model',
543
+ 'workflow/model',
544
544
  file,
545
- `${label}.model: expected ${AUTOMATION_MODELS.join(' | ')}, received ${JSON.stringify(decl.model)}`,
545
+ `${label}.model: expected ${WORKFLOW_MODELS.join(' | ')}, received ${JSON.stringify(decl.model)}`,
546
546
  at(`${p}.model`)
547
547
  )
548
548
  }
@@ -557,17 +557,17 @@ function lintSchedule(report, file, pos, label, schedule) {
557
557
  cron = parseCron(schedule)
558
558
  } catch (e) {
559
559
  if (e instanceof CronUnsupported) {
560
- report.warn('automation/schedule-unverified', file, `${label}.schedule contains "${e.message}", which cannot be checked offline`, {
560
+ report.warn('workflow/schedule-unverified', file, `${label}.schedule contains "${e.message}", which cannot be checked offline`, {
561
561
  ...pos,
562
562
  hint: 'the builder may accept it; confirm the 15-minute floor by hand',
563
563
  })
564
564
  return
565
565
  }
566
- report.error('automation/schedule-syntax', file, `${label}.schedule: invalid cron expression (${e.message})`, pos)
566
+ report.error('workflow/schedule-syntax', file, `${label}.schedule: invalid cron expression (${e.message})`, pos)
567
567
  return
568
568
  }
569
569
  if (!meetsFloor(cron)) {
570
- report.error('automation/schedule-floor', file, `${label}.schedule must fire at most every ${SCHEDULE_FLOOR_MINUTES} minutes`, pos)
570
+ report.error('workflow/schedule-floor', file, `${label}.schedule must fire at most every ${SCHEDULE_FLOOR_MINUTES} minutes`, pos)
571
571
  }
572
572
  }
573
573
 
@@ -976,10 +976,10 @@ function lintAgents(root, entry, report) {
976
976
  }
977
977
  }
978
978
 
979
- // --- automations -----------------------------------------------------------
979
+ // --- workflows -----------------------------------------------------------
980
980
 
981
- function lintAutomationFiles(root, entry, declared, report) {
982
- const dir = join(entry.dir, NAMESPACE, 'automations')
981
+ function lintWorkflowFiles(root, entry, declared, report) {
982
+ const dir = join(entry.dir, NAMESPACE, 'workflows')
983
983
  const present = new Set()
984
984
 
985
985
  for (const dirent of listDir(dir)) {
@@ -991,10 +991,10 @@ function lintAutomationFiles(root, entry, declared, report) {
991
991
  const orphans = [...present].filter((s) => !declared.includes(s))
992
992
  for (const orphan of orphans) {
993
993
  report.error(
994
- 'automation/orphan-file',
994
+ 'workflow/orphan-file',
995
995
  relative(root, join(dir, `${orphan}.md`)),
996
- `Invalid automations of ${entry.slug}: files without a manifest declaration: ${orphans.join(', ')}`,
997
- { line: 1, hint: `declare it in plugin.json under extensions["${NAMESPACE}"].automations, or delete it` }
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` }
998
998
  )
999
999
  }
1000
1000
 
@@ -1002,19 +1002,19 @@ function lintAutomationFiles(root, entry, declared, report) {
1002
1002
  const path = join(dir, `${slug}.md`)
1003
1003
  if (!present.has(slug)) {
1004
1004
  report.error(
1005
- 'automation/missing-file',
1005
+ 'workflow/missing-file',
1006
1006
  relative(root, join(entry.dir, 'plugin.json')),
1007
- `Invalid automations of ${entry.slug}: missing automations/${slug}.md`,
1008
- { hint: `create ${NAMESPACE}/automations/${slug}.md` }
1007
+ `Invalid workflows of ${entry.slug}: missing workflows/${slug}.md`,
1008
+ { hint: `create ${NAMESPACE}/workflows/${slug}.md` }
1009
1009
  )
1010
1010
  continue
1011
1011
  }
1012
1012
  const text = readIfExists(path) ?? ''
1013
1013
  if (text.trim() === '') {
1014
- report.error('automation/empty-file', relative(root, path), `Invalid automations of ${entry.slug}: automations/${slug}.md is empty`, { line: 1 })
1014
+ report.error('workflow/empty-file', relative(root, path), `Invalid workflows of ${entry.slug}: workflows/${slug}.md is empty`, { line: 1 })
1015
1015
  }
1016
1016
  if (text.trimStart().startsWith('---')) {
1017
- report.warn('automation/frontmatter', relative(root, path), 'frontmatter here is not parsed or stripped, so it lands verbatim in the agent instruction', {
1017
+ report.warn('workflow/frontmatter', relative(root, path), 'frontmatter here is not parsed or stripped, so it lands verbatim in the agent instruction', {
1018
1018
  line: 1,
1019
1019
  hint: 'name, schedule and model belong in plugin.json',
1020
1020
  })
@@ -1179,7 +1179,7 @@ export function lint(root, { git = true } = {}) {
1179
1179
  lintMcp(root, entry, manifest, report)
1180
1180
  lintSkills(root, entry, report)
1181
1181
  lintAgents(root, entry, report)
1182
- lintAutomationFiles(root, entry, manifest?.automations ?? [], report)
1182
+ lintWorkflowFiles(root, entry, manifest?.workflows ?? [], report)
1183
1183
  lintSetup(root, entry, manifest, report)
1184
1184
  lintLayout(root, entry, report)
1185
1185
  }