fix: complete reviewed integration workflows and safety checks

This commit is contained in:
haelyra committed 2026-10-09 18:54:39 -04:00
1 parent 0cb1476f9d
commit 5981dd7092
33 files changed
+481 -64

No files matched your search

@@ -101,7 +101,7 @@ def load_custom_roles(root: Path, report: Report) -> tuple[dict[object, object],
return {}, False
try:
loaded = json.loads(mapping_source.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
report.fail(f"Cannot read .governance/docs-map.json: {diagnostic(exc)}")
return {}, False
if not isinstance(loaded, dict):
@@ -16,7 +16,7 @@ REFERENCE_DEFINITION_RE = re.compile(
"""
)
REFERENCE_USAGE_RE = re.compile(
r"(?<![!\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
r"(?<![\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
)
EXTERNAL_URI_RE = re.compile(
r"^(?:[A-Za-z][A-Za-z0-9+.-]*://|(?:data|doi|geo|irc|magnet|mailto|news|sms|tel|urn):)",
@@ -111,7 +111,18 @@ def inline_link_targets(text: str) -> list[str]:
elif character == quote:
quote = None
continue
if character in {"'", '"'}:
if escaped:
escaped = False
continue
if character == "\\":
escaped = True
continue
if (
character in {"'", '"'}
and depth == 0
and end > start
and text[end - 1].isspace()
):
quote = character
continue
if character == "(":
@@ -181,6 +192,8 @@ def normalize_link_target(raw: str) -> str | None:
)
if title:
target = target[: title.start()]
# Unescape supported Markdown punctuation before Windows path separators.
target = re.sub(r"\\([()\[\]'])", r"\1", target)
target = unquote(target.split("#", 1)[0].split("?", 1)[0]).replace("\\", "/")
if not target or (target.startswith("//") and not is_unc_path):
return None
+1 -1
View File
@@ -342,7 +342,7 @@ jobs:
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 20
steps:
- name: Checkout
+1 -1
View File
@@ -850,7 +850,7 @@ ECC/
| |-- plugin.json # Plugin metadata and component paths
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
|-- agents/ # 69 specialized subagents for delegation
|-- agents/ # 71 specialized subagents for delegation
| |-- planner.md # Feature implementation planning
| |-- architect.md # System design decisions
| |-- tdd-guide.md # Test-driven development
+1 -1
View File
@@ -1,4 +1,4 @@
**ভাষা:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Українська](../uk-UA/README.md) | [اردو](../ur/README.md) | [Polski](../pl/README.md) | [বাংলা](../bn/README.md) | বাংলা
**ভাষা:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Українська](../uk-UA/README.md) | [اردو](../ur/README.md) | [Polski](../pl/README.md) | বাংলা
# ECC
@@ -179,7 +179,6 @@ Builder msg = new Builder();
msg.put("text", "Hello, user!");
SSEPushManager.getInstance().push(sessionId, msg);
// 全員へブロードキャスト
// 全員へブロードキャスト
SSEPushManager.getInstance().broadcast(msg);
```
@@ -133,7 +133,7 @@ public class User extends AbstractData implements Serializable {
- `@Column` が付与されたフィールドのみマッピングされる。`@Table` は継承され、スーパークラスのアノテーション付きフィールドもマッピングされる。
- 同じプロパティを二重にマッピング(例:スーパークラスのアノテーション付きフィールドを隠すサブクラスフィールド)すると、そのクラスが最初に使われる際にエラーになる。
- `autoGenerated = true` は整数でないIDに新規インスタンスごとにUUIDを付与し、`autoIncrement = true` はその値をデータベースに委ねる。
- `.map.xml` の `<id>` 要素も同じ名前 `autoIncrement`・``autoGenerated` を使う。旧来の `increment`・`generate` 属性で書かれたファイルも引き続き動作する。両方の綴りが存在する場合、新しい方が優先される。
- `.map.xml` の `<id>` 要素も同じ名前 `autoIncrement`・`autoGenerated` を使う。旧来の `increment`・`generate` 属性で書かれたファイルも引き続き動作する。両方の綴りが存在する場合、新しい方が優先される。
### テーブルの自動作成
`application.properties` に1行追加することで、存在しないテーブルを自動的に作成できます:
+8 -2
View File
@@ -5381,10 +5381,13 @@ mod tests {
metrics: SessionMetrics::default(),
})?;
let low = create_remote_dispatch_request(
let low = create_remote_dispatch_request_inner(
&db,
&cfg,
RemoteDispatchKind::Standard,
&repo_root,
"Low priority cleanup",
None,
Some("lead"),
TaskPriority::Low,
"claude",
@@ -5394,10 +5397,13 @@ mod tests {
"cli",
None,
)?;
let critical = create_remote_dispatch_request(
let critical = create_remote_dispatch_request_inner(
&db,
&cfg,
RemoteDispatchKind::Standard,
&repo_root,
"Critical production incident",
None,
Some("lead"),
TaskPriority::Critical,
"claude",
+12 -1
View File
@@ -2544,8 +2544,19 @@ mod tests {
&["remote", "add", "origin", "git@github.com:example/ecc.git"],
)?;
let linked_path = root.join("worker");
run_git(
&repo,
&[
"worktree",
"add",
"-b",
"ecc/worker-123",
linked_path.to_str().expect("utf8 path"),
],
)?;
let worktree = WorktreeInfo {
path: repo.clone(),
path: linked_path,
branch: "ecc/worker-123".to_string(),
base_branch: "main".to_string(),
};
@@ -1 +1 @@
{"coverage":{"skills":302,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"0b80675288b6c632e54a2938fc314ea72e3a553cb3339b6a32f4d7dcd753d5f5","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:securityLine truncated
{"coverage":{"skills":302,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"949e47a1816d50ba7c378935b7497d0b8076cecd9102d46aa36f55785903a667","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:securityLine truncated
+4
View File
@@ -142,6 +142,10 @@
"mcp-configs",
"scripts/auto-update.js",
"scripts/setup-package-manager.js",
"scripts/lib/package-manager.js",
"scripts/lib/utils.js",
"scripts/lib/agent-data-home.js",
"scripts/lib/path-safety.js",
".hermes",
".openclaw",
".kimi",
+2 -2
View File
@@ -514,9 +514,9 @@
"test:js": "node tests/run-all.js",
"test:python": "python3 -m pytest tests/ -q",
"test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js",
"coverage:js": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 79 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"coverage:js": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"coverage:python": "python3 -m pytest tests/ -q --cov=src --cov-report=term --cov-fail-under=80",
"coverage": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 79 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"coverage": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"build:opencode": "node scripts/build-opencode.js",
"prepack": "npm run build:opencode",
"dashboard": "python3 ./ecc_dashboard.py",
@@ -101,7 +101,7 @@ def load_custom_roles(root: Path, report: Report) -> tuple[dict[object, object],
return {}, False
try:
loaded = json.loads(mapping_source.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
report.fail(f"Cannot read .governance/docs-map.json: {diagnostic(exc)}")
return {}, False
if not isinstance(loaded, dict):
@@ -16,7 +16,7 @@ REFERENCE_DEFINITION_RE = re.compile(
"""
)
REFERENCE_USAGE_RE = re.compile(
r"(?<![!\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
r"(?<![\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
)
EXTERNAL_URI_RE = re.compile(
r"^(?:[A-Za-z][A-Za-z0-9+.-]*://|(?:data|doi|geo|irc|magnet|mailto|news|sms|tel|urn):)",
@@ -111,7 +111,18 @@ def inline_link_targets(text: str) -> list[str]:
elif character == quote:
quote = None
continue
if character in {"'", '"'}:
if escaped:
escaped = False
continue
if character == "\\":
escaped = True
continue
if (
character in {"'", '"'}
and depth == 0
and end > start
and text[end - 1].isspace()
):
quote = character
continue
if character == "(":
@@ -181,6 +192,8 @@ def normalize_link_target(raw: str) -> str | None:
)
if title:
target = target[: title.start()]
# Unescape supported Markdown punctuation before Windows path separators.
target = re.sub(r"\\([()\[\]'])", r"\1", target)
target = unquote(target.split("#", 1)[0].split("?", 1)[0]).replace("\\", "/")
if not target or (target.startswith("//") and not is_unc_path):
return None
+6 -6
View File
@@ -103,16 +103,16 @@ function parseReadmeExpectations(readmeContent) {
{ category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'README.md quick-start summary' }
);
const projectTreeAgentsMatch = readmeContent.match(/^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/im);
if (!projectTreeAgentsMatch) {
const projectTreeAgentsMatches = [...readmeContent.matchAll(/^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/gim)];
if (!projectTreeAgentsMatches.length) {
throw new Error('README.md project tree is missing the agents count');
}
expectations.push({
for (const [index, match] of projectTreeAgentsMatches.entries()) expectations.push({
category: 'agents',
mode: 'exact',
expected: Number(projectTreeAgentsMatch[1]),
source: 'README.md project tree (agents)'
expected: Number(match[1]),
source: `README.md project tree ${index + 1} (agents)`
});
const tablePatterns = [
@@ -411,7 +411,7 @@ function syncEnglishReadme(content, catalog) {
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/im,
/^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/gim,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'README.md project tree (agents)'
);
+3 -3
View File
@@ -21,7 +21,6 @@
const fs = require('fs');
const path = require('path');
const http = require('http');
const os = require('os');
const {
LOOPBACK_HOSTNAMES,
buildAllowedHostnames,
@@ -30,6 +29,7 @@ const {
} = require('./lib/loopback-guard');
const { normalizeAgentTools } = require('./lib/agent-tools');
const { readHooksConfig } = require('./lib/hooks-config');
const { getClaudeDir } = require('./lib/utils');
const DEFAULT_HOST = '127.0.0.1';
@@ -867,7 +867,7 @@ handleRoute();
'<span class="act-pill act-pill-agent">Agent: ' + agentCount + '</span>' +
'</div>';
if (!entries.length) {
panel.innerHTML = summary + '<div class="act-empty">No recorded tool calls yet. This reads ~/.claude/metrics/tool-usage.jsonl, written by the session-activity-tracker PostToolUse hook. Requires that hook in the active session.</div>';
panel.innerHTML = summary + '<div class="act-empty">No recorded tool calls yet. This reads metrics/tool-usage.jsonl in the active ECC agent data directory, written by the session-activity-tracker PostToolUse hook. Requires that hook in the active session.</div>';
return;
}
panel.innerHTML = summary + '<div class="act-feed">' + entries.map(rowHtml).join('') + '</div>';
@@ -930,7 +930,7 @@ function sendHtml(res, statusCode, html) {
const MAX_ACTIVITY_LOG_BYTES = 256 * 1024;
function loadActivity(limit = 200, logPath = path.join(os.homedir(), '.claude', 'metrics', 'tool-usage.jsonl')) {
function loadActivity(limit = 200, logPath = path.join(getClaudeDir(), 'metrics', 'tool-usage.jsonl')) {
let fd;
let text;
try {
+1
View File
@@ -245,6 +245,7 @@ function globMatchesAnyPath(rootDir, scanRoot, pattern) {
return false;
}
return entries.some(entry => {
if (entry.isSymbolicLink()) return false;
if (entry.isDirectory() && IGNORED_SCAN_DIRS.has(entry.name)) return false;
return visit(path.join(current, entry.name));
});
+28 -13
View File
@@ -3,16 +3,22 @@ const path = require('path');
const {
createInstallTargetAdapter,
createManagedOperation,
createManagedScaffoldOperation,
normalizeRelativePath,
} = require('./helpers');
// GitHub Copilot CLI discovers skills from `.github/skills/` and custom agents
// from `.github/agents/`. Those two directories are the entire supported
// surface for this target: ECC's hooks target Claude Code's event model, and
// commands rely on slash-command argument substitution Copilot does not
// provide, so neither is installed here.
// from `.github/agents/`. Explicitly declared package-manager helpers live in
// `.github/ecc/scripts/`, outside Copilot's discovery roots. ECC's hooks target
// Claude Code's event model, and commands rely on slash-command argument
// substitution Copilot does not provide, so neither is installed here.
const SUPPORTED_SOURCE_PREFIXES = ['agents', 'skills'];
const COPILOT_HELPER_PATHS = new Set([
'scripts/setup-package-manager.js',
'scripts/lib/package-manager.js',
'scripts/lib/utils.js',
'scripts/lib/agent-data-home.js',
'scripts/lib/path-safety.js',
]);
function hasPrefix(normalizedPath, prefix) {
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
@@ -21,7 +27,8 @@ function hasPrefix(normalizedPath, prefix) {
function supportsCopilotSourcePath(sourceRelativePath) {
const normalizedPath = normalizeRelativePath(sourceRelativePath);
if (normalizedPath.split('/').includes('..')) return false;
return SUPPORTED_SOURCE_PREFIXES.some(prefix => hasPrefix(normalizedPath, prefix));
return COPILOT_HELPER_PATHS.has(normalizedPath)
|| SUPPORTED_SOURCE_PREFIXES.some(prefix => hasPrefix(normalizedPath, prefix));
}
function stripPrefix(normalizedPath, prefix) {
@@ -49,15 +56,23 @@ function planSourcePathOperations(module, sourceRelativePath, targetRoot) {
if (hasPrefix(normalizedSourcePath, 'skills')) {
return [
createManagedScaffoldOperation(
module.id,
normalizedSourcePath,
path.join(targetRoot, 'skills', stripPrefix(normalizedSourcePath, 'skills')),
'preserve-relative-path'
),
createManagedOperation({
moduleId: module.id,
sourceRelativePath: normalizedSourcePath,
destinationPath: path.join(targetRoot, 'skills', stripPrefix(normalizedSourcePath, 'skills')),
contentTransform: 'copilot-workflow-paths',
}),
];
}
if (COPILOT_HELPER_PATHS.has(normalizedSourcePath)) {
return [createManagedOperation({
moduleId: module.id,
sourceRelativePath: normalizedSourcePath,
destinationPath: path.join(targetRoot, 'ecc', normalizedSourcePath),
})];
}
return [];
}
@@ -70,7 +85,7 @@ module.exports = createInstallTargetAdapter({
supportsModule(module) {
// Selection gating stays permissive so modules that only act as dependency
// anchors (rules-core, commands-core, platform-configs) still resolve.
// planOperations() is what narrows the install to agents and skills, so an
// planOperations() narrows the install to agents, skills and declared helpers. An
// unsupported path contributes zero operations rather than skipping the
// module and every module that depends on it.
const paths = Array.isArray(module && module.paths) ? module.paths : [];
+12 -1
View File
@@ -4,6 +4,14 @@ const { adaptAntigravityAgent } = require('./antigravity-agent');
const { adaptCopilotAgent } = require('./copilot-agent');
const { disableOpenCodeHookPluginRegistration, getDisabledOpenCodePluginContent } = require('./hook-consent');
function rewriteCopilotWorkflowPaths(content, sourceRelativePath) {
if (!/\.md$/i.test(sourceRelativePath)) return content;
// Commands run from the installed project root. Rewrite only the supported
// helper entrypoint, leaving unrelated project scripts and URLs untouched.
return content.replace(/(^|[\s`"'(])scripts\/setup-package-manager\.js\b/g,
'$1.github/ecc/scripts/setup-package-manager.js');
}
function transformInstallContent(operation, content) {
if (!operation.contentTransform) {
return content;
@@ -12,7 +20,10 @@ function transformInstallContent(operation, content) {
return adaptAntigravityAgent(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'copilot-agent-frontmatter') {
return adaptCopilotAgent(content, operation.sourceRelativePath);
return rewriteCopilotWorkflowPaths(adaptCopilotAgent(content, operation.sourceRelativePath), operation.sourceRelativePath);
}
if (operation.contentTransform === 'copilot-workflow-paths') {
return rewriteCopilotWorkflowPaths(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
return transformInstallContent({ ...operation, contentTransform: 'opencode-home-skills-path' },
+5 -1
View File
@@ -104,6 +104,10 @@ function buildCopyFileOperation({
strategy,
contentTransform,
}) {
// Copilot's workflow reference transform is textual. Skill directories may
// also contain binary assets and executable helpers, which must keep bytes.
const applicableTransform = contentTransform === 'copilot-workflow-paths'
&& !/\.md$/i.test(sourceRelativePath) ? undefined : contentTransform;
return {
kind: 'copy-file',
moduleId,
@@ -113,7 +117,7 @@ function buildCopyFileOperation({
strategy,
ownership: 'managed',
scaffoldOnly: false,
...(contentTransform ? { contentTransform } : {}),
...(applicableTransform ? { contentTransform: applicableTransform } : {}),
};
}
+1 -1
View File
@@ -101,7 +101,7 @@ def load_custom_roles(root: Path, report: Report) -> tuple[dict[object, object],
return {}, False
try:
loaded = json.loads(mapping_source.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
report.fail(f"Cannot read .governance/docs-map.json: {diagnostic(exc)}")
return {}, False
if not isinstance(loaded, dict):
@@ -16,7 +16,7 @@ REFERENCE_DEFINITION_RE = re.compile(
"""
)
REFERENCE_USAGE_RE = re.compile(
r"(?<![!\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
r"(?<![\\])\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
)
EXTERNAL_URI_RE = re.compile(
r"^(?:[A-Za-z][A-Za-z0-9+.-]*://|(?:data|doi|geo|irc|magnet|mailto|news|sms|tel|urn):)",
@@ -111,7 +111,18 @@ def inline_link_targets(text: str) -> list[str]:
elif character == quote:
quote = None
continue
if character in {"'", '"'}:
if escaped:
escaped = False
continue
if character == "\\":
escaped = True
continue
if (
character in {"'", '"'}
and depth == 0
and end > start
and text[end - 1].isspace()
):
quote = character
continue
if character == "(":
@@ -181,6 +192,8 @@ def normalize_link_target(raw: str) -> str | None:
)
if title:
target = target[: title.start()]
# Unescape supported Markdown punctuation before Windows path separators.
target = re.sub(r"\\([()\[\]'])", r"\1", target)
target = unquote(target.split("#", 1)[0].split("?", 1)[0]).replace("\\", "/")
if not target or (target.startswith("//") and not is_unc_path):
return None
+1 -1
View File
@@ -22,7 +22,7 @@ and latency. Savings depend on model, platform, cache eligibility, writes,
retention, reuse and output volume. Runtime cache hits and savings for this skill
are **unmeasured**; the examples below validate arithmetic without provider calls.
## When to Use
## When to Activate
Use for repeated API requests that share authorized instructions, tools or
reference material. First identify the exact provider endpoint, model, service
+1 -1
View File
@@ -14,7 +14,7 @@ working tree as a file the README can reference.
The card is a committed artifact rather than a hosted image, so it renders from the
repository itself and does not depend on a third-party endpoint staying up.
## When to Use
## When to Activate
- Someone wants a usage figure visible in a README or docs page.
- Someone asks for a shareable summary of their agent activity across tools.
@@ -135,5 +135,7 @@ for (const [name, fn] of tests) {
console.error(` ${error.message}`);
}
}
if (failed) process.exit(1);
console.log(`\n${tests.length - failed - skipped}/${tests.length} passed, ${skipped} skipped`);
console.log(`\nPassed: ${tests.length - failed - skipped}`);
console.log(`Skipped: ${skipped}`);
console.log(`Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;
+2
View File
@@ -46,6 +46,7 @@ function writeEnglishReadme(root, counts, options = {}) {
fs.writeFileSync(path.join(root, 'README.md'), `Access to ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} commands.
- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} legacy command shims.
|-- agents/ # ${counts.agents} specialized subagents for delegation
|-- agents/ # ${counts.agents + 1} specialized subagents for delegation
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
| --- | --- | --- | --- | --- |
| Agents | PASS: ${tableCounts.agents} agents |
@@ -277,6 +278,7 @@ function runTests() {
assert.ok(readme.includes('Access to 1 agents, 1 skills, and 1 legacy command shims'));
assert.ok(readme.includes('actual OSS surface: 7 agents, 7 skills, and 7 legacy command shims'));
assert.ok(readme.includes('|-- agents/ # 1 specialized subagents for delegation'));
assert.strictEqual((readme.match(/# 1 specialized subagents for delegation/g) || []).length, 2);
assert.ok(readme.includes('| Skills | 42 | .agents/skills/ |'));
assert.ok(agentsDoc.includes('providing 1 specialized agents, 1+ skills, 1 commands'));
assert.ok(agentsDoc.includes('skills/ - 1+ workflow skills and domain knowledge'));
+31
View File
@@ -46,6 +46,37 @@ function run(result, filename = 'sample.test.js', actions = true) {
}
const tests = [
['named suite summaries retain their actual totals', () => {
const result = run({ status: 0, stdout: 'eval-harness package: Results: Passed: 4, Failed: 0' });
assert.strictEqual(result.status, 0);
assert.ok(result.logs.some(line => /Passed:\s+4\s/.test(line)));
}],
['named suite failure summaries cannot be lost with a successful exit', () => {
const result = run({ status: 0, stdout: 'capsule: Results: Passed: 3, Failed: 1' });
assert.strictEqual(result.status, 1);
assert.match(result.annotations[0], /reported 1 failed/);
}],
['outer summary wins over a nested runner failure summary', () => {
const result = run({ status: 0, stdout: 'Passed: 0, Failed: 1\nPASS verifies nested failure\nResults: Passed: 4, Failed: 0' });
assert.strictEqual(result.status, 0);
assert.ok(result.logs.some(line => /Passed:\s+4\s/.test(line)));
}],
['outer failure cannot be hidden by an earlier nested success', () => {
const result = run({ status: 0, stdout: 'Passed: 99, Failed: 0\nPassed: 2\nFailed: 1' });
assert.strictEqual(result.status, 1);
assert.ok(result.logs.some(line => /Passed:\s+2\s/.test(line)));
assert.match(result.annotations[0], /reported 1 failed/);
}],
['diagnostic mentions of counts are not owned summaries', () => {
const result = run({ status: 0, stdout: 'Assertion fixture: Passed: 99, Failed: 7\nPassed: 3\nFailed: 0', stderr: 'Error fixture expected "Failed: 8"' });
assert.strictEqual(result.status, 0);
assert.ok(result.logs.some(line => /Passed:\s+3\s/.test(line)));
}],
['stdout summary owns counts even when stderr quotes a nested summary', () => {
const result = run({ status: 0, stdout: 'Passed: 3, Failed: 0', stderr: 'Passed: 0, Failed: 8' });
assert.strictEqual(result.status, 0);
assert.ok(result.logs.some(line => /Passed:\s+3\s/.test(line)));
}],
['nonzero exit overrides a zero-failure summary', () => {
const result = run({ status: 1, stdout: 'Passed: 2, Failed: 0', stderr: 'Error: late crash' });
assert.strictEqual(result.status, 1);
+90 -3
View File
@@ -117,10 +117,10 @@ test('agents are planned into .github/agents with the frontmatter transform', ()
assert.strictEqual(operations[0].destinationPath, path.join('/project', '.github', 'agents'));
});
test('skills are planned into .github/skills without a transform', () => {
test('skills are planned into .github/skills with installed helper references', () => {
const operations = planFor([{ id: 'workflow-quality', paths: ['skills/tdd-workflow'] }]);
assert.strictEqual(operations.length, 1);
assert.strictEqual(operations[0].contentTransform, undefined);
assert.strictEqual(operations[0].contentTransform, 'copilot-workflow-paths');
assert.strictEqual(
operations[0].destinationPath,
path.join('/project', '.github', 'skills', 'tdd-workflow')
@@ -194,7 +194,8 @@ test('repair and uninstall preserve user-owned agents across project adapters',
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'repair must preserve user file');
const state = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
assert.ok(!state.operations.some(operation => operation.destinationPath === agent), 'repair must not claim ownership');
uninstallInstalledStates(options);
const uninstall = uninstallInstalledStates(options);
assert.strictEqual(uninstall.summary.errorCount, 0, JSON.stringify(uninstall));
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'uninstall must preserve user file');
} finally {
fs.rmSync(project, { recursive: true, force: true });
@@ -202,5 +203,91 @@ test('repair and uninstall preserve user-owned agents across project adapters',
}
});
test('normal installed Copilot TDD command runs from the project with a complete managed helper closure', () => {
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-copilot-workflow-'));
try {
const env = { ...process.env, ECC_AGENT_DATA_HOME: path.join(project, 'data') };
const homeDir = path.join(project, 'home');
fs.mkdirSync(path.join(project, 'scripts'));
fs.writeFileSync(path.join(project, 'scripts', 'setup-package-manager.js'), 'USER PROJECT SENTINEL\n');
fs.writeFileSync(path.join(project, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n');
const plan = createManifestInstallPlan({ target: 'copilot', moduleIds: ['workflow-quality'], projectRoot: project, homeDir, env });
applyInstallPlan(plan);
const skill = fs.readFileSync(path.join(project, '.github', 'skills', 'tdd-workflow', 'SKILL.md'), 'utf8');
const command = skill.match(/node (\S*setup-package-manager\.js) --detect/);
assert.ok(command, 'installed skill must give an executable detection command');
assert.strictEqual(command[1], '.github/ecc/scripts/setup-package-manager.js');
for (const helper of ['package-manager', 'utils', 'agent-data-home', 'path-safety']) {
assert.ok(fs.existsSync(path.join(project, '.github', 'ecc', 'scripts', 'lib', `${helper}.js`)), helper);
}
const detected = spawnSync(process.execPath, [command[1], '--detect'], { cwd: project, env, encoding: 'utf8', timeout: 15000 });
assert.strictEqual(detected.status, 0, detected.stderr);
assert.match(detected.stdout, /pnpm/);
assert.ok(!plan.operations.some(operation => operation.kind === 'update-claude-settings'
|| /[\\/]\.github[\\/]hooks[\\/]|[\\/]ecc[\\/]scripts[\\/]hooks[\\/]|settings\.json$|hooks\.json$/.test(operation.destinationPath)), 'Copilot must not register hooks');
assert.strictEqual(fs.readFileSync(path.join(project, 'scripts', 'setup-package-manager.js'), 'utf8'), 'USER PROJECT SENTINEL\n');
const options = { repoRoot: REPO_ROOT, projectRoot: project, homeDir, targets: ['copilot'], env };
const repair = repairInstalledStates(options);
assert.strictEqual(repair.summary.errorCount, 0, JSON.stringify(repair));
const uninstall = uninstallInstalledStates(options);
assert.strictEqual(uninstall.summary.errorCount, 0, JSON.stringify(uninstall));
assert.ok(!fs.existsSync(path.join(project, '.github', 'ecc', 'scripts', 'lib', 'package-manager.js')));
const userProject = path.join(project, 'user-project');
const userHelper = path.join(userProject, '.github', 'ecc', 'scripts', 'setup-package-manager.js');
fs.mkdirSync(path.dirname(userHelper), { recursive: true });
fs.writeFileSync(userHelper, 'USER OWNED HELPER SENTINEL\n');
const userPlan = createManifestInstallPlan({ target: 'copilot', moduleIds: ['workflow-quality'], projectRoot: userProject, homeDir, env });
applyInstallPlan(userPlan);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
const userOptions = { ...options, projectRoot: userProject };
const userRepair = repairInstalledStates(userOptions);
assert.strictEqual(userRepair.summary.errorCount, 0);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
const state = JSON.parse(fs.readFileSync(userPlan.installStatePath, 'utf8'));
assert.ok(!state.operations.some(operation => operation.destinationPath === userHelper));
const userUninstall = uninstallInstalledStates(userOptions);
assert.strictEqual(userUninstall.summary.errorCount, 0);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
} finally {
fs.rmSync(project, { recursive: true, force: true });
}
});
test('Copilot rewrites Markdown commands while preserving binary skill assets and source bytes', () => {
const fs = require('fs');
const os = require('os');
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-copilot-assets-'));
try {
const sourceRoot = path.join(temp, 'source');
fs.mkdirSync(path.join(sourceRoot, 'manifests'), { recursive: true });
fs.writeFileSync(path.join(sourceRoot, 'manifests', 'install-modules.json'), JSON.stringify({ version: 1, modules: [{
id: 'fixture', kind: 'skills', paths: ['skills/fixture'], targets: ['copilot'], dependencies: [],
}] }));
fs.writeFileSync(path.join(sourceRoot, 'manifests', 'install-profiles.json'), JSON.stringify({ version: 1, profiles: {} }));
const skillRoot = path.join(sourceRoot, 'skills', 'fixture');
fs.mkdirSync(skillRoot, { recursive: true });
const original = 'Run `node scripts/setup-package-manager.js --detect`.\nhttps://example.org/scripts/setup-package-manager.js\n';
const binary = Buffer.from([0, 255, 254, 128, 42]);
fs.writeFileSync(path.join(skillRoot, 'SKILL.md'), original);
fs.writeFileSync(path.join(skillRoot, 'asset.png'), binary);
const plan = createManifestInstallPlan({ sourceRoot, target: 'copilot', moduleIds: ['fixture'], projectRoot: path.join(temp, 'project'), homeDir: path.join(temp, 'home'), env: {} });
applyInstallPlan(plan);
const installed = path.join(plan.targetRoot, 'skills', 'fixture');
assert.strictEqual(fs.readFileSync(path.join(installed, 'SKILL.md'), 'utf8'), original.replace('node scripts/', 'node .github/ecc/scripts/'));
assert.deepStrictEqual(fs.readFileSync(path.join(installed, 'asset.png')), binary);
assert.strictEqual(fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'), original);
assert.ok(!plan.operations.find(operation => operation.sourceRelativePath.endsWith('asset.png')).contentTransform);
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed === 0 ? 0 : 1);
+19 -9
View File
@@ -65,14 +65,21 @@ function annotateFailure(displayPath, reason, output) {
}
function parseCounts(combined) {
const passedMatch = combined.match(/Passed:\s*(\d+)/);
const failedMatch = combined.match(/Failed:\s*(\d+)/);
if (passedMatch || failedMatch) {
return {
passed: passedMatch ? parseInt(passedMatch[1], 10) : 0,
failed: failedMatch ? parseInt(failedMatch[1], 10) : 0,
definite: true,
};
// Nested runner diagnostics may contain earlier summaries. The file's last
// standalone summary owns its totals; mentions inside diagnostics do not.
const summaries = [...combined.matchAll(/^[ \t]*(?:(?:[^\r\n:]+:[ \t]*)?Results:[ \t]*)?(Passed|Failed):[ \t]*(\d+)(?:[ \t]*,?[ \t]+(Passed|Failed):[ \t]*(\d+))?(?:[ \t]*,[ \t]*Skipped:[ \t]*\d+)?[ \t]*\r?$/gm)];
if (summaries.length) {
let passed = 0;
let failed = 0;
for (const summary of summaries) {
if (summary[1] === 'Passed') {
passed = Number(summary[2]);
failed = 0;
} else failed = Number(summary[2]);
if (summary[3] === 'Passed') passed = Number(summary[4]);
else if (summary[3] === 'Failed') failed = Number(summary[4]);
}
return { passed, failed, definite: true };
}
const lines = combined.split(/\r?\n/);
let tapPassed = 0;
@@ -148,7 +155,10 @@ for (const testFile of testFiles) {
if (stderr) console.log(stderr);
const combined = `${stdout}\n${stderr}`;
const counts = parseCounts(combined);
// Counts belong to stdout when a suite prints its summary there. Stderr is
// diagnostic output and may quote the failing nested process verbatim.
const stdoutCounts = parseCounts(stdout);
const counts = stdoutCounts.definite ? stdoutCounts : parseCounts(stderr);
const processFailed = Boolean(result.error) || result.status !== 0;
let failureReason;
+32
View File
@@ -18,6 +18,38 @@ let testFailed = 0;
const asyncTests = [];
const REQUEST_TIMEOUT_MS = 5000;
for (const override of ['ECC_AGENT_DATA_HOME', 'CLAUDE_CONFIG_DIR']) {
asyncTest(`activity HTTP feed reads real hook output with ${override}`, async () => {
const { spawnSync } = require('child_process');
const dir = createTempDir('ecc-activity-override-');
const saved = Object.fromEntries(['ECC_AGENT_DATA_HOME', 'CLAUDE_CONFIG_DIR', 'CURSOR_VERSION', 'CURSOR_PROJECT_DIR'].map(key => [key, process.env[key]]));
try {
for (const key of Object.keys(saved)) delete process.env[key];
process.env[override] = dir;
const payload = { session_id: `override-${override}`, tool_name: 'Read', tool_input: { file_path: 'fixture.txt' } };
const result = spawnSync(process.execPath, [path.resolve(__dirname, '../../scripts/hooks/session-activity-tracker.js')], {
input: JSON.stringify(payload), encoding: 'utf8', timeout: 10000, env: process.env,
});
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(fs.existsSync(path.join(dir, 'metrics', 'tool-usage.jsonl')));
await withDashboardServer(async port => {
const response = await requestDashboard(port, { path: '/api/activity' });
assert.strictEqual(response.statusCode, 200);
const { entries } = JSON.parse(response.body);
assert.strictEqual(entries.length, 1);
assert.strictEqual(entries[0].session_id, payload.session_id);
assert.strictEqual(entries[0].tool_name, 'Read');
});
} finally {
for (const [key, value] of Object.entries(saved)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
cleanup(dir);
}
});
}
test('renderHTML emits executable scripts without syntax errors', () => {
const { renderHTML } = require(SCRIPT);
const html = renderHTML({ agents: [], skills: [], commands: [], rules: [], mcps: [], hooks: [] });
@@ -0,0 +1,59 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { test } = require("node:test");
const { buildReport } = require("../../scripts/harness-audit");
test("consumer audit unmatched glob never enumerates linked directory targets", t => {
const temporary = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "ecc-audit-boundary-"));
t.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
const project = path.join(temporary, "project");
const outside = path.join(temporary, "outside");
fs.mkdirSync(path.join(project, "scan"), { recursive: true });
fs.mkdirSync(path.join(outside, "nested"), { recursive: true });
fs.writeFileSync(path.join(project, "pytest.ini"), "[pytest]\ntestpaths = scan/**/not-present\n");
fs.writeFileSync(path.join(project, "test_owned.py"), "def test_owned(): pass\n");
fs.writeFileSync(path.join(outside, "nested", "test_external.py"), "def test_external(): pass\n");
for (const name of ["first", "second"]) {
fs.symlinkSync(outside, path.join(project, "scan", name), "junction");
}
const original = fs.readdirSync;
const outsideReads = [];
fs.readdirSync = function (directory, ...args) {
const resolved = fs.realpathSync(directory);
if (resolved === outside || resolved.startsWith(outside + path.sep)) {
outsideReads.push(String(directory));
}
return original.call(this, directory, ...args);
};
let report;
try {
report = buildReport("repo", { rootDir: project, targetMode: "consumer" });
} finally {
fs.readdirSync = original;
}
assert.equal(report.target_mode, "consumer");
assert.ok(report.checks.find(check => check.id === "consumer-test-suite").pass);
assert.equal(report.checks.find(check => check.id === "consumer-eval-coverage").pass, false);
assert.deepEqual(outsideReads, [], "linked directories must not be enumerated, even through duplicate links");
});
test("consumer audit does not treat a linked directory as a matching testpath", t => {
const temporary = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "ecc-audit-link-match-"));
t.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
const project = path.join(temporary, "project");
fs.mkdirSync(path.join(project, "scan"), { recursive: true });
fs.writeFileSync(path.join(project, "pytest.ini"), "[pytest]\ntestpaths = scan/**/linked\n");
fs.mkdirSync(path.join(project, "owned"));
for (const name of ["test_one.py", "test_two.py", "test_three.py"]) {
fs.writeFileSync(path.join(project, "owned", name), "def test_owned(): pass\n");
}
fs.symlinkSync(path.join(project, "owned"), path.join(project, "scan", "linked"), "junction");
const report = buildReport("repo", { rootDir: project, targetMode: "consumer" });
assert.ok(report.checks.find(check => check.id === "consumer-eval-coverage").pass,
"an unmatched glob must fall back to the three regular owned test files");
});
+12 -4
View File
@@ -16,10 +16,16 @@ const rates = { input: 3, write: 3.75, read: 0.3, output: 15, storage: 1 };
const usage = { uncached: 20000, written: 10000, read: 90000, output: 20000, storageTokenHours: 0 };
const near = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-10, `${actual} != ${expected}`);
let passed = 0;
let failed = 0;
function test(name, fn) {
fn();
passed += 1;
console.log(`PASS ${name}`);
try {
fn();
passed += 1;
console.log(`PASS ${name}`);
} catch (error) {
failed += 1;
console.error(`FAIL ${name}\n${error.stack || error.message}`);
}
}
test('ten-request example charges a write premium and unchanged output', () => {
@@ -64,4 +70,6 @@ test('canonical module and package both distribute the skill', () => {
assert.ok(modules.modules.find(module => module.id === 'agentic-patterns').paths.includes('skills/prompt-caching-strategy'));
assert.ok(pkg.files.includes('skills/prompt-caching-strategy/'));
});
console.log(`${passed}/${passed} offline prompt caching checks passed; runtime cache hits unmeasured.`);
console.log('Offline prompt caching checks; runtime cache hits unmeasured.');
console.log(`Passed: ${passed}, Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;
+96
View File
@@ -786,3 +786,99 @@ def test_audit_does_not_modify_repository_files(project: Path) -> None:
}
assert result.returncode == 1
assert after == before
@pytest.mark.parametrize("link", ("[guide](it's.md)", '[guide](it\'s.md "Guide)")'))
def test_artifact_scope_reports_missing_apostrophe_destination(
project: Path, link: str
) -> None:
(project / "index.md").write_text(link + "\n", encoding="utf-8")
result = run_audit(project, "artifacts")
assert result.returncode == 1, result.stdout
assert "Broken Markdown link" in result.stdout
assert "it's.md" in result.stdout
@pytest.mark.parametrize(
"link,filename",
(
(r"[guide](file\(1\).md)", "file(1).md"),
(r"[guide](file\).md)", "file).md"),
(r'[guide](file\(1\).md "Guide)")', "file(1).md"),
(r"[guide](<file\(1\).md>)", "file(1).md"),
),
)
def test_artifact_scope_resolves_escaped_destination_parentheses(
project: Path, link: str, filename: str
) -> None:
(project / filename).write_text("# Guide\n", encoding="utf-8")
(project / "index.md").write_text(link + "\n", encoding="utf-8")
result = run_audit(project, "artifacts")
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.parametrize("scope", ("artifacts", "full"))
@pytest.mark.parametrize("usage", ("![diagram]", "![diagram][]", "![picture][diagram]"))
def test_audit_reports_missing_reference_images(
project: Path, scope: str, usage: str
) -> None:
(project / "index.md").write_text(
usage + "\n\n[diagram]: missing.png\n", encoding="utf-8"
)
result = run_audit(project, scope)
assert result.returncode == 1, result.stdout
assert "Broken Markdown link: 'index.md' -> 'missing.png'" in result.stdout
@pytest.mark.parametrize("usage", ("![diagram]", "![diagram][]"))
def test_artifact_scope_accepts_existing_reference_images(
project: Path, usage: str
) -> None:
(project / "diagram.png").write_bytes(b"image fixture")
(project / "index.md").write_text(
usage + "\n\n[diagram]: diagram.png\n", encoding="utf-8"
)
result = run_audit(project, "artifacts")
assert result.returncode == 0, result.stdout + result.stderr
def test_artifact_scope_ignores_escaped_reference_image_bracket(project: Path) -> None:
(project / "index.md").write_text(
r"!\[diagram]" + "\n\n[diagram]: missing.png\n", encoding="utf-8"
)
result = run_audit(project, "artifacts")
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.parametrize("mapping", (b"\xff", b"{invalid json"))
def test_full_audit_reports_bad_role_map_and_continues_artifact_checks(
project: Path, mapping: bytes
) -> None:
(project / ".governance").mkdir()
(project / ".governance/docs-map.json").write_bytes(mapping)
(project / "index.md").write_text("[missing](missing-guide.md)\n", encoding="utf-8")
result = run_audit(project, "full")
assert result.returncode == 1
assert "Cannot read .governance/docs-map.json" in result.stdout
assert "Broken Markdown link: 'index.md' -> 'missing-guide.md'" in result.stdout
assert "Traceback" not in result.stderr
@pytest.mark.parametrize(
"link,filename",
(
(r"[guide](file\[1\].md)", "file[1].md"),
(r"[guide](it\'s.md)", "it's.md"),
(r'[guide](file\[1\].md "Guide)")', "file[1].md"),
(r"[guide](<it\'s.md>)", "it's.md"),
("[guide][docs]\n\n[docs]: " + r"file\[1\].md", "file[1].md"),
("[guide][docs]\n\n[docs]: " + r"it\'s.md", "it's.md"),
),
)
def test_artifact_scope_resolves_escaped_bracket_and_apostrophe_filenames(
project: Path, link: str, filename: str
) -> None:
(project / filename).write_text("# Guide\n", encoding="utf-8")
(project / "index.md").write_text(link + "\n", encoding="utf-8")
result = run_audit(project, "artifacts")
assert result.returncode == 0, result.stdout + result.stderr