mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-09 15:02:30 +02:00
fix: use scalar Claude agent tools (#2583)
Normalize scalar Claude agent tool metadata across validators, adapters, dashboards, and generated surfaces with regression coverage.
This commit is contained in:
parent
f3afd59045
commit
6a9f075cd9
79 changed files with 515 additions and 87 deletions
|
|
@ -19,24 +19,44 @@ function extractFrontmatter(content) {
|
|||
|
||||
const frontmatter = {};
|
||||
const duplicates = [];
|
||||
const sequenceFields = [];
|
||||
let currentTopLevelKey = null;
|
||||
const lines = match[1].split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (/^\s*-\s+/.test(line)) {
|
||||
if (currentTopLevelKey) {
|
||||
sequenceFields.push(currentTopLevelKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only top-level keys are unique. Indented YAML belongs to nested values.
|
||||
if (/^\s/.test(line)) continue;
|
||||
if (!line.trim() || line.trim().startsWith('#')) continue;
|
||||
|
||||
currentTopLevelKey = null;
|
||||
const colonIdx = line.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
const key = line.slice(0, colonIdx).trim();
|
||||
const value = line.slice(colonIdx + 1).trim();
|
||||
currentTopLevelKey = key;
|
||||
if (Object.prototype.hasOwnProperty.call(frontmatter, key)) {
|
||||
duplicates.push(key);
|
||||
}
|
||||
frontmatter[key] = value;
|
||||
if (value && '[!&*{|>'.includes(value[0])) {
|
||||
sequenceFields.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(frontmatter, '__duplicates__', {
|
||||
value: duplicates,
|
||||
enumerable: false,
|
||||
});
|
||||
Object.defineProperty(frontmatter, '__sequenceFields__', {
|
||||
value: sequenceFields,
|
||||
enumerable: false,
|
||||
});
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +99,11 @@ function validateAgents() {
|
|||
}
|
||||
}
|
||||
|
||||
if (frontmatter.__sequenceFields__.includes('tools')) {
|
||||
console.error(`ERROR: ${file} - Agent tools must be a comma-separated scalar, not a YAML sequence`);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Validate model is a known value
|
||||
if (frontmatter.model && !VALID_MODELS.includes(frontmatter.model)) {
|
||||
console.error(`ERROR: ${file} - Invalid model '${frontmatter.model}'. Must be one of: ${VALID_MODELS.join(', ')}`);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const { normalizeAgentTools } = require('./lib/agent-tools');
|
||||
|
||||
function parsePort(v) {
|
||||
const n = parseInt(String(v), 10);
|
||||
|
|
@ -31,7 +32,11 @@ function readFrontmatter(p) {
|
|||
const s = l.indexOf(':'); if (s <= 0) continue;
|
||||
let k = l.slice(0, s).trim(), v = l.slice(s + 1).trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
||||
if (v.startsWith('[') && v.endsWith(']')) { try { v = JSON.parse(v); } catch { v = v.slice(1, -1).split(',').map(x => x.trim().replace(/["']/g, '')); } }
|
||||
if (k === 'tools') {
|
||||
v = normalizeAgentTools(v);
|
||||
} else if (v.startsWith('[') && v.endsWith(']')) {
|
||||
try { v = JSON.parse(v); } catch { v = v.slice(1, -1).split(',').map(x => x.trim().replace(/["']/g, '')); }
|
||||
}
|
||||
fm[k] = v;
|
||||
}
|
||||
fm._body = c.replace(/^---[\s\S]*?---\n*/, '').trim();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { normalizeAgentTools } = require('./lib/agent-tools');
|
||||
|
||||
const TOOL_NAME_MAP = new Map([
|
||||
['Read', 'read_file'],
|
||||
|
|
@ -53,25 +54,13 @@ function ensureDirectory(dirPath) {
|
|||
}
|
||||
}
|
||||
|
||||
function stripQuotes(value) {
|
||||
return value.trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
function parseToolList(line) {
|
||||
const match = line.match(/^(\s*tools\s*:\s*)\[(.*)\]\s*$/);
|
||||
const match = line.match(/^\s*tools\s*:\s*(.*)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawItems = match[2].trim();
|
||||
if (!rawItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawItems
|
||||
.split(',')
|
||||
.map(part => stripQuotes(part))
|
||||
.filter(Boolean);
|
||||
return normalizeAgentTools(match[1]);
|
||||
}
|
||||
|
||||
function adaptToolName(toolName) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { normalizeAgentTools } = require('./agent-tools');
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from a markdown string.
|
||||
|
|
@ -35,6 +36,10 @@ function parseFrontmatter(content) {
|
|||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
if (key === 'tools') {
|
||||
value = normalizeAgentTools(value);
|
||||
}
|
||||
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
|
||||
|
|
|
|||
97
scripts/lib/agent-tools.js
Normal file
97
scripts/lib/agent-tools.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
'use strict';
|
||||
|
||||
function stripSurroundingQuotes(value) {
|
||||
const trimmed = value.trim();
|
||||
const quote = trimmed[0];
|
||||
if ((quote === '"' || quote === "'") && trimmed.endsWith(quote)) {
|
||||
return trimmed.slice(1, -1).trim();
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function splitTopLevelToolList(value) {
|
||||
const items = [];
|
||||
const delimiters = [];
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
let itemStart = 0;
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === '\\') {
|
||||
escaped = true;
|
||||
} else if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'") {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '(' || character === '[' || character === '{') {
|
||||
delimiters.push(character);
|
||||
continue;
|
||||
}
|
||||
|
||||
const expectedOpener = {
|
||||
')': '(',
|
||||
']': '[',
|
||||
'}': '{',
|
||||
}[character];
|
||||
if (expectedOpener && delimiters.at(-1) === expectedOpener) {
|
||||
delimiters.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === ',' && delimiters.length === 0) {
|
||||
items.push(value.slice(itemStart, index));
|
||||
itemStart = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
items.push(value.slice(itemStart));
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize Claude agent frontmatter tools to the array shape used internally.
|
||||
*
|
||||
* Claude Code expects tools to be a comma-separated scalar. Flow sequences are
|
||||
* still accepted here so ECC can read legacy or harness-adapted agent files.
|
||||
*/
|
||||
function normalizeAgentTools(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter(item => typeof item === 'string')
|
||||
.map(stripSurroundingQuotes)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
const listValue = trimmed.startsWith('[') && trimmed.endsWith(']')
|
||||
? trimmed.slice(1, -1)
|
||||
: stripSurroundingQuotes(trimmed);
|
||||
|
||||
if (!listValue.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return splitTopLevelToolList(listValue)
|
||||
.map(stripSurroundingQuotes)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeAgentTools,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue