mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix: complete reviewed integration workflows and safety checks
This commit is contained in:
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
|
||||
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -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
@@ -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行追加することで、存在しないテーブルを自動的に作成できます:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)'
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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 : [];
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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
|
||||
Reference in new issue
Block a user