Skip to content

[rig-eslint] feat(eslint): add agents-must-be-object rule - #196

Merged
pelikhan merged 1 commit into
mainfrom
rig-eslint/agents-must-be-object-2026-07-26-a4c2472d3b3d8888
Jul 26, 2026
Merged

[rig-eslint] feat(eslint): add agents-must-be-object rule#196
pelikhan merged 1 commit into
mainfrom
rig-eslint/agents-must-be-object-2026-07-26-a4c2472d3b3d8888

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Recurring mistake

Generated Rig programs repeatedly used an array literal for the agents field instead of the required named-object shorthand. The runtime requires agents: { name } (so sub-agents can be referenced by name in prompt instructions), but models naturally reach for agents: [name] because arrays are the idiomatic multi-item JavaScript collection.

Evidence — at least 3 distinct runs

Workflow Run ID Date Excerpt
Daily Rig Task Generator 30197940742 2026-07-26T10:15:23Z "agents must be a named object: agents: { summarizer } vs agents: [summarizer] is an easy mistake. The constraint deserves a dedicated lint rule."
Daily Rig Task Generator 30205213966 2026-07-26T14:00:57Z "Rule: agents-must-be-object — Invalid: agents: [extractor]; Valid: agents: { extractor }; Why model-confusing: arrays are natural for multi-item collections. Autofix: convert array literal to shorthand object."
Daily Rig Task Generator 30185797340 2026-07-26T03:12:32Z "agents must be a named object: agents: { summarizer } vs agents: [summarizer] is an easy mistake. The constraint deserves a dedicated lint rule."

Why the autofix is safe

The rule only fires when every element of the array is a plain identifier (no computed expressions, spread operators, or function calls). In that case, [a, b]{ a, b } is a pure syntactic transformation that preserves all referenced bindings as shorthand property names. The fix is idempotent: applying it twice yields the same result.

Code examples

Invalid:

const coordinator = agent({
  agents: [extractor, summarizer],   // ← array literal
  instructions: p`Delegate to agents.`,
});

Fixed:

const coordinator = agent({
  agents: { extractor, summarizer },  // ← named object shorthand
  instructions: p`Delegate to agents.`,
});

Changed integration points

  1. skills/rig/eslint/rules/agents-must-be-object.js — new rule module with scanTokens for the dependency-free CLI and a full ESLint create() visitor.
  2. skills/rig/eslint/index.js — exports the new rule as "agents-must-be-object".
  3. skills/rig/eslint/lint.js — imports scanAgentsMustBeObject and adds it to tokenRules.
  4. src/eslint-rules.test.js — adds 12 new tests: valid cases, fix cases, idempotency, empty-array edge case, and ESLint-rule alignment checks.

Validation results

npm test        — 33 eslint-rule tests pass (+12 new); 264/266 total pass
                  (2 pre-existing failures in launcher-default-engine.test.ts unrelated to this change)
npm run lint    — exit 0, no problems
npm run typecheck — exit 0, no errors

Generated by Rig ESLint Rule Miner · sonnet46 145.7 AIC · ⌖ 9.14 AIC · ⊞ 5.3K ·

Detects and autofixes agents declared as an array literal instead of
a named object shorthand. The pattern agents: [extractor] fails at
runtime; agents: { extractor } is required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pelikhan
pelikhan marked this pull request as ready for review July 26, 2026 20:08
@pelikhan
pelikhan merged commit 4a2e211 into main Jul 26, 2026
1 check passed
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — approving with minor suggestions.

📋 Key Themes & Highlights

Findings

  • Redundant regex in scanTokens — the tokenizer contract already guarantees only identifier or single-char punctuation tokens reach that branch; a clarifying comment would be cleaner than re-testing the shape.
  • String-context test is correct but expressed as a comment in the it.each array rather than a named test case, which makes it harder to trace intent.
  • Brace spacing in fix output ({ a, b }) — worth verifying it matches the project's formatter expectations.

Positive Highlights

  • ✅ Dual implementation (token scanner + ESLint visitor) is clean and consistent with sibling rules
  • ✅ Comprehensive test suite: valid cases, fix cases, idempotency, empty-array edge case, and cross-implementation alignment
  • ✅ Safety constraint is clearly scoped: only fires when all elements are plain identifiers, no computed expressions or spreads
  • closingBracket helper handles nesting correctly and returns undefined gracefully
  • ✅ PR description is thorough: real evidence of the recurring mistake, clear code examples, validation results

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 51.3 AIC · ⌖ 7.25 AIC · ⊞ 6.3K
Comment /matt to run again

for (const token of inner) {
if (token.value === ",") continue;
if (/^[A-Za-z_$][\w$]*$/.test(token.value)) {
identifiers.push(token.value);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The identifier regex /^[A-Za-z_$][\w$]*$/ is redundant — the tokenizer in lint.js already guarantees that identifier-shaped tokens are the only word tokens emitted (lines 43–46). Only single-char punctuation tokens can ever reach this branch, and only , passes the skip check. Consider replacing the regex with a comment that names this invariant, making the guard's intent clearer.

💡 Clarified guard
// The tokenizer emits only identifier tokens (matched by /[A-Za-z_$][\w$]*/) or
// single-char punctuation tokens. Commas are separators; anything else is a
// non-identifier element (e.g. '[', '.') that can't be safely renamed.
if (token.value === ",") continue;
if (token.value.length === 1 && !/[A-Za-z_$]/.test(token.value[0])) {
  valid = false;
  break;
}
identifiers.push(token.value);

This avoids re-testing the regex on every identifier token.

Comment thread src/eslint-rules.test.js
describe("agents-must-be-object", () => {
it.each([
"agents: { extractor }",
"agents: { diagnose, fix }",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The lintSource-level valid cases include "const text = 'agents: [extractor]';" in a comment but the test string doesn't actually contain a string literal — the outer double-quotes make it just a comment note in the it.each array. The test is correct (the regex 'agents: [extractor]' inside single-quoted string content is skipped by the tokenizer), but the comment-as-code makes it harder to verify intent. Consider a dedicated it that names what is being guarded.

💡 Suggested clarifying test
it("does not flag agents array inside a string literal", () => {
  const source = `const text = 'agents: [extractor]';`;
  expect(lintSource(source).filter((p) => p.kind === "agents-must-be-object")).toEqual([]);
});

This makes the string-context protection explicit and independently verifiable.

}

export default {
meta: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The fixedText uses { a, b } (spaces inside braces), but the existing no-object-literal-record.js fix and the codebase style don't have a settled convention here. If the user's code uses {a, b} (no spaces), the autofix introduces a style difference. This is a minor inconsistency, but worth aligning with whatever convention prettier/eslint enforces in the repo.

💡 Check

Run grep -r 'agents: {' skills/rig/samples/ to see whether existing samples use spaces. If the project has Prettier configured, the fix output will be re-formatted anyway, but it's good to be consistent with the fix output of sibling rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant