affaan-m/ECC · error
${label} must be a non-empty string
Error message
${label} must be a non-empty string What it means
Thrown by ensureString() in the skill-improvement observations module, invoked from createSkillObservation() for the 'task' and 'skill.id' fields. The helper rejects anything that is not a string or is empty/whitespace-only, because observation records must be uniquely attributable to a task and a skill identifier for downstream telemetry grouping. The label in the message identifies which field failed.
Source
Thrown at scripts/lib/skill-improvement/observations.js:23
const os = require('os');
const OBSERVATION_SCHEMA_VERSION = 'ecc.skill-observation.v1';
function resolveProjectRoot(options = {}) {
return path.resolve(options.projectRoot || options.cwd || process.cwd());
}
function getSkillTelemetryRoot(options = {}) {
return path.join(resolveProjectRoot(options), '.claude', 'ecc', 'skills');
}
function getSkillObservationsPath(options = {}) {
return path.join(getSkillTelemetryRoot(options), 'observations.jsonl');
}
function ensureString(value, label) {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`${label} must be a non-empty string`);
}
return value.trim();
}
function createObservationId() {
return `obs-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2, 8)}`;
}
function createSkillObservation(input) {
const task = ensureString(input.task, 'task');
const skillId = ensureString(input.skill && input.skill.id, 'skill.id');
const skillPath = typeof input.skill.path === 'string' && input.skill.path.trim().length > 0
? input.skill.path.trim()
: null;
const success = Boolean(input.success);
const error = input.error === null || input.error === undefined ? null : String(input.error);
const feedback = input.feedback === null || input.feedback === undefined ? null : String(input.feedback);View on GitHub (pinned to 01e15490f0)
Solutions
- Ensure input.task is a non-empty trimmed string before calling createSkillObservation.
- Ensure input.skill exists and input.skill.id is a non-empty trimmed string.
- Default or reject early at your API boundary: if (!task || !skill?.id) return null; rather than letting ensureString throw deep inside the library.
- Add a runtime type check (e.g. zod schema) for the observation payload at ingress.
Example fix
// before
createSkillObservation({ task: '', skill: { id: 'tdd-workflow' }, success: true });
// -> task must be a non-empty string
// after
if (!input.task?.trim() || !input.skill?.id?.trim()) return null;
createSkillObservation({
task: input.task.trim(),
skill: { id: input.skill.id.trim(), path: input.skill.path },
success: true,
}); Defensive patterns
Strategy: validation
Validate before calling
function isValidObservationInput(input) {
return !!(
input &&
typeof input.task === 'string' && input.task.trim().length > 0 &&
input.skill &&
typeof input.skill.id === 'string' && input.skill.id.trim().length > 0
);
}
if (!isValidObservationInput(input)) return null;
createSkillObservation(input); Type guard
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isObservationInput(value) {
return !!(
value &&
isNonEmptyString(value.task) &&
value.skill &&
isNonEmptyString(value.skill.id)
);
} Try / catch
try {
createSkillObservation(input);
} catch (error) {
if (/must be a non-empty string/.test(error.message)) {
// drop the observation rather than crash the telemetry pipeline
return null;
}
throw error;
} Prevention
- Validate task and skill.id at the source (CLI parser, hook payload) rather than relying on the library.
- Use a schema (zod) for observation payloads at ingress.
- Treat empty task/skill.id as a signal of an upstream bug — log and investigate, do not silently drop in production.
- Coerce null to undefined before the call so the && chain in createSkillObservation behaves predictably.
When it happens
Trigger: Calling createSkillObservation({ task: '', skill: { id: 'foo' } }); calling it with task undefined; passing skill.id as null; passing an object whose skill property is missing entirely (input.skill && input.skill.id short-circuits to undefined); passing a numeric id like 42.
Common situations: CLI forwards an optional --task flag that was omitted; an upstream telemetry hook fires before the skill id was resolved; a JSON payload from a queue has null for empty fields instead of empty strings; refactoring renames skill.id to skill.name and forgets the call site.
Related errors
- ${label} must be a non-empty string.
- ${label} is too long (maximum ${maxChars} characters).
- ${fieldName} must be a number
- ECC_PROJECT_DIR must be a child path within /workspace.
- Unknown argument: ${arg}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/6e5fa0714e1cbd77.
Report an issue: GitHub.