426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
388 lines
12 KiB
JavaScript
Executable File
388 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Default-pack prompt-content linter.
|
|
*
|
|
* Per IMPL §7.2 + GAP §8.9: scans `src/default-packs/*` `promptInstructions`
|
|
* for the corpus documented in `docs/audit/prompt-content-lint.md`:
|
|
* - PII names, email addresses, phone numbers
|
|
* - absolute filesystem paths
|
|
* - hardcoded ISO times / datetimes outside owner-fact references
|
|
* - embedded conditional logic
|
|
* - hardcoded URLs
|
|
* - Wave-N / W<N>-<L> narrative leaks
|
|
* - leftover slop markers (`to` + `do`, `fix` + `me`, `xx` + `x`, `ha` + `ck`)
|
|
*
|
|
* W3-B promotion: this runner exits non-zero on any finding by default.
|
|
* `--allow-warnings` opts back into the legacy warnings-only behavior; it
|
|
* exists only so a maintainer can re-run without CI failing while triaging
|
|
* a wave of changes locally.
|
|
*
|
|
* Usage:
|
|
* node scripts/lint-default-packs.mjs # CI-fail mode (default)
|
|
* node scripts/lint-default-packs.mjs --allow-warnings # warnings-only
|
|
*
|
|
* The script reads each `src/default-packs/*.ts` file as text and runs the
|
|
* same regex corpus the runtime `lintPromptText` uses. Reading the source
|
|
* files directly (instead of importing the registered packs) keeps the
|
|
* linter independent of the W1-A spine and avoids needing a TS runtime in
|
|
* `bun run verify`.
|
|
*
|
|
* It also rejects direct `ScheduledTaskSeed`/`ScheduledTask` construction from
|
|
* pack files. Pack authors define typed task definitions and compile them
|
|
* through `task-definitions.ts`.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const packsDir = path.resolve(here, "..", "src", "default-packs");
|
|
|
|
const PII_NAMES = ["Jill", "Marco", "Sarah", "Suran", "Sam"];
|
|
const PII_REGEX = new RegExp(`\\b(${PII_NAMES.join("|")})\\b`, "g");
|
|
|
|
const PHONE_REGEX =
|
|
/(?:\+\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}\b/g;
|
|
|
|
const ABSOLUTE_PATH_REGEX =
|
|
/(?:^|[\s"'`(])(?:\/[A-Za-z0-9_.\-/]{2,}|~\/[A-Za-z0-9_.\-/]{2,}|[A-Z]:\\[A-Za-z0-9_.\\-]{2,})/g;
|
|
|
|
const ISO_TIME_REGEX =
|
|
/\b(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?(?:Z|[+-]\d{2}:\d{2})?\b/g;
|
|
const ISO_DATE_REGEX =
|
|
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?\b/g;
|
|
|
|
const OWNER_FACT_TIME_PATTERNS = [
|
|
/morningWindow/i,
|
|
/eveningWindow/i,
|
|
/quietHours/i,
|
|
/HH:MM/,
|
|
];
|
|
|
|
function basicEmailValid(value) {
|
|
const at = value.indexOf("@");
|
|
if (at <= 0 || at !== value.lastIndexOf("@")) return false;
|
|
if (/\s/.test(value)) return false;
|
|
const domain = value.slice(at + 1);
|
|
return domain.length >= 3 && domain.slice(1, -1).includes(".");
|
|
}
|
|
|
|
function isEmailAtomChar(code) {
|
|
return (
|
|
(code >= 48 && code <= 57) ||
|
|
(code >= 65 && code <= 90) ||
|
|
(code >= 97 && code <= 122) ||
|
|
code === 33 ||
|
|
code === 35 ||
|
|
code === 36 ||
|
|
code === 37 ||
|
|
code === 38 ||
|
|
code === 39 ||
|
|
code === 42 ||
|
|
code === 43 ||
|
|
code === 45 ||
|
|
code === 47 ||
|
|
code === 63 ||
|
|
code === 94 ||
|
|
code === 95 ||
|
|
code === 96 ||
|
|
code === 123 ||
|
|
code === 124 ||
|
|
code === 125 ||
|
|
code === 126 ||
|
|
code === 46
|
|
);
|
|
}
|
|
|
|
function isDomainChar(code) {
|
|
return (
|
|
(code >= 48 && code <= 57) ||
|
|
(code >= 65 && code <= 90) ||
|
|
(code >= 97 && code <= 122) ||
|
|
code === 45 ||
|
|
code === 46
|
|
);
|
|
}
|
|
|
|
function findBasicEmailSpans(text) {
|
|
const spans = [];
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
const at = text.indexOf("@", i);
|
|
if (at === -1) break;
|
|
let start = at - 1;
|
|
while (start >= 0 && isEmailAtomChar(text.charCodeAt(start))) start -= 1;
|
|
start += 1;
|
|
let end = at + 1;
|
|
while (end < text.length && isDomainChar(text.charCodeAt(end))) end += 1;
|
|
while (end > at + 1) {
|
|
const last = text.charCodeAt(end - 1);
|
|
if (last !== 45 && last !== 46) break;
|
|
end -= 1;
|
|
}
|
|
if (start < at && end > at + 1) {
|
|
const value = text.slice(start, end);
|
|
if (basicEmailValid(value)) spans.push({ value, start, end });
|
|
}
|
|
i = Math.max(end, at + 1);
|
|
}
|
|
return spans;
|
|
}
|
|
|
|
const CONDITIONAL_REGEX =
|
|
/\b(if user(?:'s)?|when [A-Za-z_]+\s*[=:]+|if owner|if the user is|if name is|when name is|unless owner|unless user|else if\b|case [A-Za-z_]+ when\b)/gi;
|
|
|
|
const URL_REGEX = /\bhttps?:\/\/[^\s'"`)<>]+/g;
|
|
|
|
const WAVE_NARRATIVE_REGEX = /\b(?:Wave[\s-]?\d+|W[1-9]\d*-[A-Z])\b/g;
|
|
|
|
const SLOP_TO_DO_TOKEN = "TO" + "DO";
|
|
const SLOP_FIX_ME_TOKEN = "FIX" + "ME";
|
|
const SLOP_TRIPLE_X_TOKEN = "XX" + "X";
|
|
const SLOP_HA_CK_TOKEN = "HA" + "CK";
|
|
const SLOP_REGEX = new RegExp(
|
|
`\\b(${SLOP_TO_DO_TOKEN}|${SLOP_FIX_ME_TOKEN}|${SLOP_TRIPLE_X_TOKEN}|${SLOP_HA_CK_TOKEN})\\b`,
|
|
"g",
|
|
);
|
|
|
|
const RAW_SCHEDULED_TASK_IMPORT_REGEX =
|
|
/import\s+type\s+\{[^}]*\bScheduledTask(?:Seed)?\b[^}]*\}\s+from\s+["']\.\/contract-stubs\.js["']/g;
|
|
const RAW_SCHEDULED_TASK_ANNOTATION_REGEX =
|
|
/:\s*(?:ReadonlyArray<\s*)?ScheduledTask(?:Seed)?\b/g;
|
|
|
|
function lintTaskDefinitionAuthorship(packKey, source) {
|
|
const findings = [];
|
|
for (const m of source.matchAll(RAW_SCHEDULED_TASK_IMPORT_REGEX)) {
|
|
findings.push({
|
|
rule: "raw_scheduled_task",
|
|
packKey,
|
|
recordIndex: "source",
|
|
message:
|
|
"Default packs must import typed task definitions and compiler helpers instead of authoring ScheduledTask/ScheduledTaskSeed directly.",
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of source.matchAll(RAW_SCHEDULED_TASK_ANNOTATION_REGEX)) {
|
|
findings.push({
|
|
rule: "raw_scheduled_task",
|
|
packKey,
|
|
recordIndex: "source",
|
|
message:
|
|
"Default packs must not type raw ScheduledTask records; compile a TaskDefinition instead.",
|
|
match: m[0],
|
|
});
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
/**
|
|
* Extract every `promptInstructions: "..."` string from a TS source file.
|
|
* Handles single-line strings and `+`-concatenated multi-line literals.
|
|
*
|
|
* Crude on purpose — the script must run without a TS runtime, and
|
|
* `promptInstructions` values are by convention plain string literals
|
|
* (single-quoted, double-quoted, or backtick-quoted). The runtime
|
|
* `lintPromptText` is what backs in-process pack registration.
|
|
*/
|
|
function extractPrompts(source) {
|
|
const prompts = [];
|
|
// Match `promptInstructions:` followed by an optional literal (single, double,
|
|
// or backtick-quoted). Support `+`-concatenated string literals on subsequent
|
|
// lines.
|
|
const re = /promptInstructions:\s*([\s\S]+?)(?=,\s*\n\s*(?:[a-zA-Z_]+:|\}))/g;
|
|
let match;
|
|
while ((match = re.exec(source)) !== null) {
|
|
const slice = match[1];
|
|
// Pull every quoted literal in `slice` and concatenate.
|
|
const literalRe = /(["'`])((?:\\.|(?!\1).)*)\1/g;
|
|
let literalMatch;
|
|
let combined = "";
|
|
while ((literalMatch = literalRe.exec(slice)) !== null) {
|
|
combined += literalMatch[2].replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
}
|
|
if (combined.length > 0) {
|
|
prompts.push(combined);
|
|
}
|
|
}
|
|
return prompts;
|
|
}
|
|
|
|
function lintPromptText(packKey, recordIndex, prompt) {
|
|
const findings = [];
|
|
for (const m of prompt.matchAll(PII_REGEX)) {
|
|
findings.push({
|
|
rule: "pii_name",
|
|
packKey,
|
|
recordIndex,
|
|
message: `PII name "${m[1]}" embedded in prompt; reference owner facts via contextRequest.includeOwnerFacts.preferredName instead.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of findBasicEmailSpans(prompt)) {
|
|
findings.push({
|
|
rule: "email_pii",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Concrete email address "${m.value}" embedded in prompt; reference the owner or an EntityStore contact instead.`,
|
|
match: m.value,
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(PHONE_REGEX)) {
|
|
findings.push({
|
|
rule: "phone_pii",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Phone number "${m[0]}" embedded in prompt; reference the owner or an EntityStore contact instead.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(ABSOLUTE_PATH_REGEX)) {
|
|
findings.push({
|
|
rule: "absolute_path",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Absolute path embedded in prompt; default packs ship across hosts and must not bake in filesystem paths.`,
|
|
match: m[0].trim(),
|
|
});
|
|
}
|
|
const hasOwnerFactReference = OWNER_FACT_TIME_PATTERNS.some((re) =>
|
|
re.test(prompt),
|
|
);
|
|
if (!hasOwnerFactReference) {
|
|
for (const m of prompt.matchAll(ISO_TIME_REGEX)) {
|
|
findings.push({
|
|
rule: "hardcoded_iso_time",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Hardcoded clock time "${m[0]}" in prompt; reference ownerFact.morningWindow / eveningWindow instead.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(ISO_DATE_REGEX)) {
|
|
findings.push({
|
|
rule: "hardcoded_iso_time",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Hardcoded ISO datetime "${m[0]}" in prompt; reference owner facts or trigger anchors instead.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
}
|
|
for (const m of prompt.matchAll(CONDITIONAL_REGEX)) {
|
|
findings.push({
|
|
rule: "embedded_conditional",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Conditional logic in prompt ("${m[0]}"); express as a registered gate or completionCheck rather than a content branch.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(URL_REGEX)) {
|
|
findings.push({
|
|
rule: "hardcoded_url",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Concrete URL "${m[0]}" embedded in prompt; reference a connector capability rather than baking in a host-specific URL.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(WAVE_NARRATIVE_REGEX)) {
|
|
findings.push({
|
|
rule: "wave_narrative",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Internal milestone reference "${m[0]}" in prompt; Wave/W-prefixed labels belong in comments and docs, not in runtime prompt content.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
for (const m of prompt.matchAll(SLOP_REGEX)) {
|
|
findings.push({
|
|
rule: "prompt_slop",
|
|
packKey,
|
|
recordIndex,
|
|
message: `Leftover marker "${m[0]}" in prompt; finish the prompt or remove the placeholder.`,
|
|
match: m[0],
|
|
});
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function packKeyFromFilename(filename) {
|
|
// Pack files are named like `daily-rhythm.ts`; the key matches the filename
|
|
// stem. Skip helper / contract / registry / lint / index / consolidation /
|
|
// escalation files.
|
|
const stem = path.basename(filename, ".ts");
|
|
const skip = new Set([
|
|
"index",
|
|
"registry-types",
|
|
"contract-stubs",
|
|
"consolidation-policies",
|
|
"escalation-ladders",
|
|
"lint",
|
|
"task-definitions",
|
|
// Registry bridge, not a pack: reconciles already-compiled packs against
|
|
// the first-run seed and registers them on the scheduling spine, so it
|
|
// legitimately references the ScheduledTaskSeed shape.
|
|
"spine-registration",
|
|
]);
|
|
return skip.has(stem) ? null : stem;
|
|
}
|
|
|
|
function main() {
|
|
if (!fs.existsSync(packsDir)) {
|
|
console.error(
|
|
`[lint-default-packs] FAIL: no default-packs directory found at ${packsDir}.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const allowWarnings = process.argv.includes("--allow-warnings");
|
|
|
|
const files = fs
|
|
.readdirSync(packsDir)
|
|
.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"));
|
|
const allFindings = [];
|
|
|
|
for (const file of files) {
|
|
const packKey = packKeyFromFilename(file);
|
|
if (!packKey) continue;
|
|
const fullPath = path.join(packsDir, file);
|
|
const source = fs.readFileSync(fullPath, "utf8");
|
|
allFindings.push(...lintTaskDefinitionAuthorship(packKey, source));
|
|
const prompts = extractPrompts(source);
|
|
prompts.forEach((prompt, recordIndex) => {
|
|
const findings = lintPromptText(packKey, recordIndex, prompt);
|
|
allFindings.push(...findings);
|
|
});
|
|
}
|
|
|
|
if (allFindings.length === 0) {
|
|
console.log(
|
|
"[lint-default-packs] clean — 0 findings across default packs.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const prefix = allowWarnings ? "WARN" : "FAIL";
|
|
console.error(
|
|
`[lint-default-packs] ${prefix}: ${allFindings.length} finding(s) across default packs:`,
|
|
);
|
|
for (const finding of allFindings) {
|
|
console.error(
|
|
` [${finding.rule}] ${finding.packKey}#${finding.recordIndex}: ${finding.message} (matched: ${JSON.stringify(finding.match)})`,
|
|
);
|
|
}
|
|
if (allowWarnings) {
|
|
console.error(
|
|
"[lint-default-packs] --allow-warnings: exiting 0 despite findings.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
console.error(
|
|
"[lint-default-packs] failing CI on findings (W3-B). Re-run with --allow-warnings only for local triage.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
main();
|