@event4u/agent-config 8.3.0 → 8.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +4 -426
- package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
- package/CHANGELOG.md +25 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/contracts/surface-matrix.md +103 -0
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/config/surface-matrix.yml +213 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/_cli/cmd_converge.ts +399 -0
- package/src/scripts/_cli/cmd_doctor.ts +94 -0
- package/src/scripts/_cli/cmd_upgrade.ts +40 -16
- package/src/scripts/_dispatch.bash +13 -0
- package/src/scripts/condense.ts +67 -36
- package/src/scripts/hook_manifest.yaml +16 -7
- package/src/scripts/lint_marketplace.ts +54 -56
- package/src/scripts/lint_surface_matrix.ts +200 -0
- package/src/scripts/prepack-check.mjs +108 -5
- package/src/scripts/surface_probe_hook.ts +231 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Ownership map — consumed by scripts/pr_review_routing.ts
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to `.github/ownership-map.yml` (preferred) or
|
|
4
|
+
# `agents/ownership-map.yml` and adapt the entries to your stack.
|
|
5
|
+
# Omitting the file entirely is fine — routing falls back to the
|
|
6
|
+
# generic role vocabulary from `reviewer-awareness`.
|
|
7
|
+
#
|
|
8
|
+
# First match wins per file. List narrower globs before broader ones.
|
|
9
|
+
|
|
10
|
+
version: 1
|
|
11
|
+
updated: 2026-04-21 # bump when entries change — stale maps warn after 6 months
|
|
12
|
+
|
|
13
|
+
# Applied when no entry matches a changed file. Optional.
|
|
14
|
+
defaults:
|
|
15
|
+
roles: [backend]
|
|
16
|
+
|
|
17
|
+
entries:
|
|
18
|
+
# --- Laravel ---
|
|
19
|
+
- paths:
|
|
20
|
+
- "app/Billing/**"
|
|
21
|
+
- "app/Services/Payment*.php"
|
|
22
|
+
- "database/migrations/*billing*.php"
|
|
23
|
+
roles: [finance-engineering, security]
|
|
24
|
+
focus: "tax calculation + idempotency"
|
|
25
|
+
risk: "monetary correctness — regressions bill real customers"
|
|
26
|
+
|
|
27
|
+
- paths:
|
|
28
|
+
- "app/Http/Middleware/TenantScope.php"
|
|
29
|
+
- "**/Tenant*.php"
|
|
30
|
+
roles: [platform, security]
|
|
31
|
+
focus: "cross-tenant isolation"
|
|
32
|
+
risk: "data leak across customers"
|
|
33
|
+
|
|
34
|
+
- paths:
|
|
35
|
+
- "app/Http/Controllers/Admin/**"
|
|
36
|
+
- "routes/admin.php"
|
|
37
|
+
roles: [platform, security]
|
|
38
|
+
focus: "admin authorization boundary"
|
|
39
|
+
|
|
40
|
+
- paths:
|
|
41
|
+
- "resources/views/**"
|
|
42
|
+
- "**/*.blade.php"
|
|
43
|
+
roles: [frontend]
|
|
44
|
+
focus: "a11y + responsive layout"
|
|
45
|
+
|
|
46
|
+
# --- Symfony ---
|
|
47
|
+
- paths:
|
|
48
|
+
- "src/Controller/Api/**"
|
|
49
|
+
- "src/Security/**"
|
|
50
|
+
roles: [backend, security]
|
|
51
|
+
focus: "authorization + input validation"
|
|
52
|
+
|
|
53
|
+
# --- Node / Next.js ---
|
|
54
|
+
- paths:
|
|
55
|
+
- "app/api/**"
|
|
56
|
+
- "pages/api/**"
|
|
57
|
+
roles: [backend]
|
|
58
|
+
focus: "public endpoint — rate limit + auth"
|
|
59
|
+
|
|
60
|
+
- paths:
|
|
61
|
+
- "components/**"
|
|
62
|
+
- "app/**/*.tsx"
|
|
63
|
+
roles: [frontend]
|
|
64
|
+
focus: "UX + client-state"
|
|
65
|
+
|
|
66
|
+
# --- Infra ---
|
|
67
|
+
- paths:
|
|
68
|
+
- "**/Dockerfile*"
|
|
69
|
+
- "**/docker-compose*.y*ml"
|
|
70
|
+
- "**/terraform/**"
|
|
71
|
+
- ".github/workflows/**"
|
|
72
|
+
roles: [infra, security]
|
|
73
|
+
focus: "rollout + secrets handling"
|
|
74
|
+
|
|
75
|
+
# --- Data ---
|
|
76
|
+
- paths:
|
|
77
|
+
- "**/migrations/**"
|
|
78
|
+
- "**/*.sql"
|
|
79
|
+
roles: [database, backend]
|
|
80
|
+
focus: "index strategy + rollback realism"
|
|
81
|
+
|
|
82
|
+
# --- Docs ---
|
|
83
|
+
- paths:
|
|
84
|
+
- "docs/**"
|
|
85
|
+
- "**/*.md"
|
|
86
|
+
roles: [domain-owner]
|
|
87
|
+
focus: "accuracy — reviewers confirm behavior matches text"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# PR risk config — consumed by scripts/pr_risk_review.ts
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to `.github/pr-risk-config.yml` and adapt the patterns to
|
|
4
|
+
# your stack. Omitting the file entirely is fine — the script ships with
|
|
5
|
+
# cross-stack defaults (migrations, lockfiles, auth, workflows).
|
|
6
|
+
#
|
|
7
|
+
# Matching uses `fnmatch` with glob syntax:
|
|
8
|
+
# - `**/foo/**` → any `foo/` anywhere in the tree
|
|
9
|
+
# - `composer.lock` → exact match at repo root
|
|
10
|
+
#
|
|
11
|
+
# Levels:
|
|
12
|
+
# - high → lockfiles, auth, migrations, secrets, CI, infra
|
|
13
|
+
# - medium → queues, routes, controllers, models, manifests
|
|
14
|
+
# - anything not matched → low
|
|
15
|
+
#
|
|
16
|
+
# `ignore` removes paths BEFORE classification (docs, changelog, …).
|
|
17
|
+
|
|
18
|
+
patterns:
|
|
19
|
+
high:
|
|
20
|
+
# --- universal ---
|
|
21
|
+
- "**/.env*"
|
|
22
|
+
- "**/secrets/**"
|
|
23
|
+
- ".github/workflows/**"
|
|
24
|
+
|
|
25
|
+
# --- Laravel / Symfony / generic PHP ---
|
|
26
|
+
- "**/database/migrations/**"
|
|
27
|
+
- "**/app/Policies/**"
|
|
28
|
+
- "**/config/auth.php"
|
|
29
|
+
- "**/config/services.php"
|
|
30
|
+
- "composer.lock"
|
|
31
|
+
|
|
32
|
+
# --- Node / Next.js ---
|
|
33
|
+
- "package-lock.json"
|
|
34
|
+
- "pnpm-lock.yaml"
|
|
35
|
+
- "yarn.lock"
|
|
36
|
+
- "**/middleware.ts"
|
|
37
|
+
- "**/app/api/**"
|
|
38
|
+
|
|
39
|
+
# --- Python / Django / Rails ---
|
|
40
|
+
- "poetry.lock"
|
|
41
|
+
- "Gemfile.lock"
|
|
42
|
+
- "**/migrations/*.py"
|
|
43
|
+
- "**/db/migrate/**"
|
|
44
|
+
|
|
45
|
+
# --- infra ---
|
|
46
|
+
- "**/Dockerfile*"
|
|
47
|
+
- "**/docker-compose*.y*ml"
|
|
48
|
+
- "**/terraform/**"
|
|
49
|
+
- "**/*.tf"
|
|
50
|
+
|
|
51
|
+
medium:
|
|
52
|
+
# --- Laravel ---
|
|
53
|
+
- "**/app/Http/Controllers/**"
|
|
54
|
+
- "**/app/Jobs/**"
|
|
55
|
+
- "**/app/Listeners/**"
|
|
56
|
+
- "**/routes/**"
|
|
57
|
+
|
|
58
|
+
# --- Symfony ---
|
|
59
|
+
- "**/src/Controller/**"
|
|
60
|
+
- "**/src/MessageHandler/**"
|
|
61
|
+
|
|
62
|
+
# --- Node / Next.js ---
|
|
63
|
+
- "**/app/**/route.ts"
|
|
64
|
+
- "**/pages/api/**"
|
|
65
|
+
|
|
66
|
+
# --- manifests (non-lock) ---
|
|
67
|
+
- "composer.json"
|
|
68
|
+
- "package.json"
|
|
69
|
+
- "pyproject.toml"
|
|
70
|
+
- "Gemfile"
|
|
71
|
+
|
|
72
|
+
ignore:
|
|
73
|
+
- "**/*.md"
|
|
74
|
+
- "**/CHANGELOG*"
|
|
75
|
+
- "**/docs/**"
|
|
76
|
+
- "**/*.snap"
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PR review-routing classifier.
|
|
4
|
+
*
|
|
5
|
+
* TypeScript twin of `src/agent-src/templates/scripts/pr_review_routing.py`
|
|
6
|
+
* (ADR-200). Byte-for-byte behavioural parity: same CLI flags, same
|
|
7
|
+
* glob→regex translation, same ownership / pattern matching, same role
|
|
8
|
+
* merge + sort order, same Markdown routing block, same severity level,
|
|
9
|
+
* same exit codes, same stdout/stderr split.
|
|
10
|
+
*
|
|
11
|
+
* Reads changed files between two git refs, resolves reviewer roles from
|
|
12
|
+
* `ownership-map.yml`, and matches the diff against
|
|
13
|
+
* `historical-bug-patterns.yml`. Emits a Markdown routing block plus a
|
|
14
|
+
* single-word severity level (low / medium / high) for downstream CI steps.
|
|
15
|
+
*
|
|
16
|
+
* This classifier is **informational** — it surfaces who should review and
|
|
17
|
+
* what regression tests historical patterns demand. It is not a merge gate.
|
|
18
|
+
*
|
|
19
|
+
* If --ownership-map / --patterns are omitted, the script searches
|
|
20
|
+
* `.github/` first and falls back to `agents/`.
|
|
21
|
+
* Missing data files are not an error; the script emits a generic
|
|
22
|
+
* fallback block.
|
|
23
|
+
*
|
|
24
|
+
* Exit codes: 0 = success, 2 = invalid arguments, 3 = git/config error.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import process from 'node:process';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import type * as YamlModule from 'yaml';
|
|
33
|
+
|
|
34
|
+
// Mirrors Python's top-level `import yaml` (with its ImportError → exit 3
|
|
35
|
+
// fallback). `yaml` is a hard dependency; version '1.1' matches PyYAML's
|
|
36
|
+
// safe_load tolerance, including date scalars parsed to a Date. Resolved
|
|
37
|
+
// relative to THIS module (not cwd) because the script runs `git diff` in the
|
|
38
|
+
// caller's cwd, which may carry its own (possibly invalid) package.json.
|
|
39
|
+
const YAML = createRequire(import.meta.url)('yaml') as typeof YamlModule;
|
|
40
|
+
|
|
41
|
+
export const LEVELS = ['high', 'medium', 'low'] as const;
|
|
42
|
+
export const STALE_MONTHS = 6;
|
|
43
|
+
|
|
44
|
+
type Dict = Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
export interface OwnershipHit {
|
|
47
|
+
path: string;
|
|
48
|
+
roles: string[];
|
|
49
|
+
focus: string | null;
|
|
50
|
+
risk: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PatternHit {
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
severity: string;
|
|
57
|
+
required_test: string;
|
|
58
|
+
references: string[];
|
|
59
|
+
matched_files: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const _compileCache = new Map<string, RegExp>();
|
|
63
|
+
|
|
64
|
+
/** Translate a gitignore-ish glob to a regex (same semantics as pr_risk_review.py). */
|
|
65
|
+
export function _compile(pattern: string): RegExp {
|
|
66
|
+
const cached = _compileCache.get(pattern);
|
|
67
|
+
if (cached !== undefined) {
|
|
68
|
+
return cached;
|
|
69
|
+
}
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
let i = 0;
|
|
72
|
+
const n = pattern.length;
|
|
73
|
+
const special = '.^$+{}()[]|\\';
|
|
74
|
+
while (i < n) {
|
|
75
|
+
const c = pattern[i] as string;
|
|
76
|
+
if (c === '*') {
|
|
77
|
+
if (i + 1 < n && pattern[i + 1] === '*') {
|
|
78
|
+
if (i + 2 < n && pattern[i + 2] === '/') {
|
|
79
|
+
out.push('(?:.*/)?');
|
|
80
|
+
i += 3;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (i > 0 && pattern[i - 1] === '/') {
|
|
84
|
+
out[out.length - 1] = '(?:/.*)?';
|
|
85
|
+
i += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
out.push('.*');
|
|
89
|
+
i += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
out.push('[^/]*');
|
|
93
|
+
} else if (c === '?') {
|
|
94
|
+
out.push('[^/]');
|
|
95
|
+
} else if (special.includes(c)) {
|
|
96
|
+
out.push(`\\${c}`);
|
|
97
|
+
} else {
|
|
98
|
+
out.push(c);
|
|
99
|
+
}
|
|
100
|
+
i += 1;
|
|
101
|
+
}
|
|
102
|
+
const re = new RegExp(`^${out.join('')}$`, 'su');
|
|
103
|
+
if (_compileCache.size < 512) {
|
|
104
|
+
_compileCache.set(pattern, re);
|
|
105
|
+
}
|
|
106
|
+
return re;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _match(p: string, pattern: string): boolean {
|
|
110
|
+
return _compile(pattern).test(p);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class ExitError extends Error {
|
|
114
|
+
constructor(public code: number) {
|
|
115
|
+
super(`__exit__${code}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _exists(p: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
fs.statSync(p);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _isFile(p: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
return fs.statSync(p).isFile();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _isPlainObject(v: unknown): v is Dict {
|
|
137
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _asList(v: unknown): unknown[] {
|
|
141
|
+
return Array.isArray(v) ? v : [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _yamlErr(exc: unknown): string {
|
|
145
|
+
return exc instanceof Error ? exc.message : String(exc);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function _load_yaml(p: string | null): Dict | null {
|
|
149
|
+
if (p === null || !_exists(p)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
let data: unknown;
|
|
153
|
+
try {
|
|
154
|
+
data = YAML.parse(fs.readFileSync(p, 'utf-8'), { version: '1.1' });
|
|
155
|
+
} catch (exc) {
|
|
156
|
+
process.stderr.write(`error: ${p} parse failed: ${_yamlErr(exc)}\n`);
|
|
157
|
+
throw new ExitError(3);
|
|
158
|
+
}
|
|
159
|
+
const obj: Dict = _isPlainObject(data) ? data : {};
|
|
160
|
+
if (obj['version'] !== 1) {
|
|
161
|
+
process.stderr.write(`error: ${p} missing or unsupported 'version: 1'\n`);
|
|
162
|
+
throw new ExitError(3);
|
|
163
|
+
}
|
|
164
|
+
return obj;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function changed_files(base: string, head: string): string[] {
|
|
168
|
+
const res = spawnSync('git', ['diff', '--name-only', `${base}...${head}`], {
|
|
169
|
+
encoding: 'utf-8',
|
|
170
|
+
});
|
|
171
|
+
if (res.status !== 0) {
|
|
172
|
+
process.stderr.write(`error: git diff failed: ${res.stderr ?? ''}\n`);
|
|
173
|
+
throw new ExitError(3);
|
|
174
|
+
}
|
|
175
|
+
return (res.stdout ?? '').split('\n').filter((line) => line.trim() !== '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Return per-file hits, fallback roles, and a stale flag. */
|
|
179
|
+
export function match_ownership(
|
|
180
|
+
files: string[],
|
|
181
|
+
cfg: Dict | null,
|
|
182
|
+
): [OwnershipHit[], string[], boolean] {
|
|
183
|
+
if (!cfg) {
|
|
184
|
+
return [[], [], false];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const entries = _asList(cfg['entries']);
|
|
188
|
+
const defaultsObj = _isPlainObject(cfg['defaults']) ? cfg['defaults'] : {};
|
|
189
|
+
const defaults = (_asList(defaultsObj['roles'])).map((r) => r as string);
|
|
190
|
+
|
|
191
|
+
const hits: OwnershipHit[] = [];
|
|
192
|
+
for (const p of files) {
|
|
193
|
+
for (const entryRaw of entries) {
|
|
194
|
+
const entry = _isPlainObject(entryRaw) ? entryRaw : {};
|
|
195
|
+
const globs = _asList(entry['paths']);
|
|
196
|
+
if (globs.some((g) => _match(p, g as string))) {
|
|
197
|
+
hits.push({
|
|
198
|
+
path: p,
|
|
199
|
+
roles: _asList(entry['roles']).map((r) => r as string),
|
|
200
|
+
focus: (entry['focus'] === undefined ? null : (entry['focus'] as string | null)),
|
|
201
|
+
risk: (entry['risk'] === undefined ? null : (entry['risk'] as string | null)),
|
|
202
|
+
});
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let stale = false;
|
|
209
|
+
const updated = cfg['updated'];
|
|
210
|
+
if (updated instanceof Date) {
|
|
211
|
+
// PyYAML parses a bare `YYYY-MM-DD` to date and `…THH:MM:SS` to
|
|
212
|
+
// datetime; both reduce to a Y/M comparison here. `yaml` v1.1 returns
|
|
213
|
+
// a JS Date for both; we compare in UTC to match PyYAML's naive date.
|
|
214
|
+
const now = new Date();
|
|
215
|
+
const months =
|
|
216
|
+
(now.getFullYear() - updated.getUTCFullYear()) * 12 +
|
|
217
|
+
(now.getMonth() - updated.getUTCMonth());
|
|
218
|
+
stale = months >= STALE_MONTHS;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return [hits, defaults, stale];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function match_patterns(files: string[], cfg: Dict | null): PatternHit[] {
|
|
225
|
+
if (!cfg) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
const result: PatternHit[] = [];
|
|
229
|
+
for (const patternRaw of _asList(cfg['patterns'])) {
|
|
230
|
+
const pattern = _isPlainObject(patternRaw) ? patternRaw : {};
|
|
231
|
+
const globs = _asList(pattern['paths']);
|
|
232
|
+
const matched = files.filter((f) => globs.some((g) => _match(f, g as string)));
|
|
233
|
+
if (matched.length === 0) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const id = _pyStr(pattern['id'] ?? 'unknown');
|
|
237
|
+
result.push({
|
|
238
|
+
id,
|
|
239
|
+
label: _pyStr(pattern['label'] ?? pattern['id'] ?? 'unknown'),
|
|
240
|
+
severity: _pyStr(pattern['severity'] ?? 'medium'),
|
|
241
|
+
required_test: _pyStr(pattern['required_test'] ?? ''),
|
|
242
|
+
references: _asList(pattern['references']).map((r) => r as string),
|
|
243
|
+
matched_files: matched,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Mirror Python `str(x)` for the scalar shapes that appear in a YAML config. */
|
|
250
|
+
function _pyStr(v: unknown): string {
|
|
251
|
+
if (typeof v === 'string') {
|
|
252
|
+
return v;
|
|
253
|
+
}
|
|
254
|
+
if (v === null || v === undefined) {
|
|
255
|
+
return 'None';
|
|
256
|
+
}
|
|
257
|
+
if (typeof v === 'boolean') {
|
|
258
|
+
return v ? 'True' : 'False';
|
|
259
|
+
}
|
|
260
|
+
return String(v);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function overall_level(patterns: PatternHit[]): string {
|
|
264
|
+
if (patterns.some((p) => p.severity === 'high')) {
|
|
265
|
+
return 'high';
|
|
266
|
+
}
|
|
267
|
+
if (patterns.some((p) => p.severity === 'medium')) {
|
|
268
|
+
return 'medium';
|
|
269
|
+
}
|
|
270
|
+
return 'low';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Return [(role, focus_notes, files), …] ordered by hit count then name. */
|
|
274
|
+
export function _merge_roles(
|
|
275
|
+
ownership: OwnershipHit[],
|
|
276
|
+
fallback: string[],
|
|
277
|
+
): Array<[string, string[], string[]]> {
|
|
278
|
+
// Insertion-ordered map mirroring Python dict ordering.
|
|
279
|
+
const byRole = new Map<string, [Set<string>, Set<string>]>();
|
|
280
|
+
for (const hit of ownership) {
|
|
281
|
+
for (const role of hit.roles) {
|
|
282
|
+
let pair = byRole.get(role);
|
|
283
|
+
if (pair === undefined) {
|
|
284
|
+
pair = [new Set<string>(), new Set<string>()];
|
|
285
|
+
byRole.set(role, pair);
|
|
286
|
+
}
|
|
287
|
+
const [notes, files] = pair;
|
|
288
|
+
if (hit.focus) {
|
|
289
|
+
notes.add(hit.focus);
|
|
290
|
+
}
|
|
291
|
+
files.add(hit.path);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (byRole.size === 0 && fallback.length > 0) {
|
|
295
|
+
for (const role of fallback) {
|
|
296
|
+
if (!byRole.has(role)) {
|
|
297
|
+
byRole.set(role, [new Set<string>(), new Set<string>()]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Python: sorted(items, key=lambda kv: (-len(files), name)) — stable sort,
|
|
302
|
+
// and Python's sort is stable so ties keep insertion order. JS Array.sort
|
|
303
|
+
// is stable in ES2019+; replicate the same comparator.
|
|
304
|
+
const items = [...byRole.entries()];
|
|
305
|
+
items.sort((a, b) => {
|
|
306
|
+
const la = a[1][1].size;
|
|
307
|
+
const lb = b[1][1].size;
|
|
308
|
+
if (la !== lb) {
|
|
309
|
+
return lb - la; // -len ascending == len descending
|
|
310
|
+
}
|
|
311
|
+
return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0);
|
|
312
|
+
});
|
|
313
|
+
return items.map(([role, [notes, files]]) => [
|
|
314
|
+
role,
|
|
315
|
+
_sortedStrs([...notes]),
|
|
316
|
+
_sortedStrs([...files]),
|
|
317
|
+
]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Python `sorted()` on a list of str — lexicographic by code unit. */
|
|
321
|
+
function _sortedStrs(arr: string[]): string[] {
|
|
322
|
+
return [...arr].sort((a, b) => (a < b ? -1 : (a > b ? 1 : 0)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function render(
|
|
326
|
+
level: string,
|
|
327
|
+
ownership: OwnershipHit[],
|
|
328
|
+
fallback: string[],
|
|
329
|
+
stale: boolean,
|
|
330
|
+
patterns: PatternHit[],
|
|
331
|
+
total: number,
|
|
332
|
+
have_ownership_file: boolean,
|
|
333
|
+
have_patterns_file: boolean,
|
|
334
|
+
): string {
|
|
335
|
+
const emojiMap: Record<string, string> = { high: '🔴', medium: '🟡', low: '🟢' };
|
|
336
|
+
const emoji = emojiMap[level] as string;
|
|
337
|
+
const lines: string[] = [
|
|
338
|
+
`## ${emoji} Review Routing: **${level}**`,
|
|
339
|
+
'',
|
|
340
|
+
`_${total} changed file(s), ${patterns.length} historical pattern(s) matched._`,
|
|
341
|
+
'',
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const roles = _merge_roles(ownership, fallback);
|
|
345
|
+
lines.push('### Suggested reviewers (role-based)');
|
|
346
|
+
if (roles.length === 0) {
|
|
347
|
+
lines.push('- No ownership map and no fallback roles configured.');
|
|
348
|
+
lines.push(' Fall back to generic reviewer selection per `reviewer-awareness`.');
|
|
349
|
+
} else {
|
|
350
|
+
const labels = ['primary', 'secondary'];
|
|
351
|
+
for (let k = 1; k < 20; k += 1) {
|
|
352
|
+
labels.push(`additional #${k}`);
|
|
353
|
+
}
|
|
354
|
+
const pairCount = Math.min(roles.length, labels.length);
|
|
355
|
+
for (let idx = 0; idx < pairCount; idx += 1) {
|
|
356
|
+
const [role, notes, files] = roles[idx] as [string, string[], string[]];
|
|
357
|
+
const label = labels[idx] as string;
|
|
358
|
+
const focus = notes.length > 0 ? notes.join(' / ') : 'anchored in diff';
|
|
359
|
+
lines.push(`- **${label}**: \`${role}\` — focus: ${focus}`);
|
|
360
|
+
if (files.length > 0) {
|
|
361
|
+
const preview = files.slice(0, 3).map((f) => `\`${f}\``).join(', ');
|
|
362
|
+
const suffix = files.length > 3 ? ` (+${files.length - 3} more)` : '';
|
|
363
|
+
lines.push(` - files: ${preview}${suffix}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
lines.push('');
|
|
368
|
+
|
|
369
|
+
if (patterns.length > 0) {
|
|
370
|
+
lines.push('### Historical patterns matched');
|
|
371
|
+
const order: Record<string, number> = { high: 0, medium: 1, low: 2 };
|
|
372
|
+
const sorted = [...patterns].sort(
|
|
373
|
+
(a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9),
|
|
374
|
+
);
|
|
375
|
+
const bulletMap: Record<string, string> = { high: '🔴', medium: '🟡', low: '🟢' };
|
|
376
|
+
for (const p of sorted) {
|
|
377
|
+
const bullet = bulletMap[p.severity] ?? '•';
|
|
378
|
+
lines.push(`- ${bullet} **${p.id}** — ${p.label}`);
|
|
379
|
+
if (p.required_test) {
|
|
380
|
+
lines.push(` - required test: ${p.required_test}`);
|
|
381
|
+
}
|
|
382
|
+
for (const ref of p.references) {
|
|
383
|
+
lines.push(` - ref: ${ref}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (stale) {
|
|
390
|
+
lines.push(
|
|
391
|
+
'> ⚠️ Ownership map last updated > 6 months ago — treat roles as hints.',
|
|
392
|
+
'',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const sourceBits: string[] = [];
|
|
397
|
+
if (have_ownership_file) {
|
|
398
|
+
sourceBits.push('ownership-map.yml');
|
|
399
|
+
}
|
|
400
|
+
if (have_patterns_file) {
|
|
401
|
+
sourceBits.push('historical-bug-patterns.yml');
|
|
402
|
+
}
|
|
403
|
+
const source = sourceBits.length > 0
|
|
404
|
+
? sourceBits.join(' + ')
|
|
405
|
+
: 'no project data — generic fallback';
|
|
406
|
+
lines.push(`_Data source: ${source}. Routing is informational — merge is not blocked._`);
|
|
407
|
+
|
|
408
|
+
return `${lines.join('\n')}\n`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const OWNERSHIP_CANDIDATES = [
|
|
412
|
+
'.github/ownership-map.yml',
|
|
413
|
+
'agents/ownership-map.yml',
|
|
414
|
+
];
|
|
415
|
+
const PATTERN_CANDIDATES = [
|
|
416
|
+
'.github/historical-bug-patterns.yml',
|
|
417
|
+
'agents/historical-bug-patterns.yml',
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
function _first_existing(candidates: string[]): string | null {
|
|
421
|
+
for (const p of candidates) {
|
|
422
|
+
if (_isFile(p)) {
|
|
423
|
+
return p;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
interface Args {
|
|
430
|
+
base: string;
|
|
431
|
+
head: string;
|
|
432
|
+
ownershipMap: string | null;
|
|
433
|
+
patterns: string | null;
|
|
434
|
+
output: string;
|
|
435
|
+
levelFile: string;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
class ArgError extends Error {}
|
|
439
|
+
|
|
440
|
+
function _parseArgs(argv: string[]): Args {
|
|
441
|
+
const a: Partial<Args> = { ownershipMap: null, patterns: null };
|
|
442
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
443
|
+
const tok = argv[i] as string;
|
|
444
|
+
const next = (): string => {
|
|
445
|
+
i += 1;
|
|
446
|
+
return argv[i] as string;
|
|
447
|
+
};
|
|
448
|
+
const eq = (prefix: string): string => tok.slice(prefix.length);
|
|
449
|
+
if (tok === '--base') a.base = next();
|
|
450
|
+
else if (tok === '--head') a.head = next();
|
|
451
|
+
else if (tok === '--ownership-map') a.ownershipMap = next();
|
|
452
|
+
else if (tok === '--patterns') a.patterns = next();
|
|
453
|
+
else if (tok === '--output') a.output = next();
|
|
454
|
+
else if (tok === '--level-file') a.levelFile = next();
|
|
455
|
+
else if (tok.startsWith('--base=')) a.base = eq('--base=');
|
|
456
|
+
else if (tok.startsWith('--head=')) a.head = eq('--head=');
|
|
457
|
+
else if (tok.startsWith('--ownership-map=')) a.ownershipMap = eq('--ownership-map=');
|
|
458
|
+
else if (tok.startsWith('--patterns=')) a.patterns = eq('--patterns=');
|
|
459
|
+
else if (tok.startsWith('--output=')) a.output = eq('--output=');
|
|
460
|
+
else if (tok.startsWith('--level-file=')) a.levelFile = eq('--level-file=');
|
|
461
|
+
else throw new ArgError(`unrecognized arguments: ${tok}`);
|
|
462
|
+
}
|
|
463
|
+
const missing: string[] = [];
|
|
464
|
+
if (a.base === undefined) missing.push('--base');
|
|
465
|
+
if (a.head === undefined) missing.push('--head');
|
|
466
|
+
if (a.output === undefined) missing.push('--output');
|
|
467
|
+
if (a.levelFile === undefined) missing.push('--level-file');
|
|
468
|
+
if (missing.length > 0) {
|
|
469
|
+
throw new ArgError(`the following arguments are required: ${missing.join(', ')}`);
|
|
470
|
+
}
|
|
471
|
+
return a as Args;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
475
|
+
let args: Args;
|
|
476
|
+
try {
|
|
477
|
+
args = _parseArgs(argv);
|
|
478
|
+
} catch (exc) {
|
|
479
|
+
if (exc instanceof ArgError) {
|
|
480
|
+
process.stderr.write(`error: ${exc.message}\n`);
|
|
481
|
+
return 2;
|
|
482
|
+
}
|
|
483
|
+
throw exc;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const ownership_path = args.ownershipMap ?? _first_existing(OWNERSHIP_CANDIDATES);
|
|
488
|
+
const patterns_path = args.patterns ?? _first_existing(PATTERN_CANDIDATES);
|
|
489
|
+
|
|
490
|
+
const ownership_cfg = _load_yaml(ownership_path);
|
|
491
|
+
const patterns_cfg = _load_yaml(patterns_path);
|
|
492
|
+
|
|
493
|
+
const files = changed_files(args.base, args.head);
|
|
494
|
+
const [ownership_hits, fallback_roles, stale] = match_ownership(files, ownership_cfg);
|
|
495
|
+
const pattern_hits = match_patterns(files, patterns_cfg);
|
|
496
|
+
const level = overall_level(pattern_hits);
|
|
497
|
+
|
|
498
|
+
fs.writeFileSync(
|
|
499
|
+
args.output,
|
|
500
|
+
render(
|
|
501
|
+
level, ownership_hits, fallback_roles, stale, pattern_hits,
|
|
502
|
+
files.length,
|
|
503
|
+
ownership_cfg !== null,
|
|
504
|
+
patterns_cfg !== null,
|
|
505
|
+
),
|
|
506
|
+
{ encoding: 'utf-8' },
|
|
507
|
+
);
|
|
508
|
+
fs.writeFileSync(args.levelFile, level, { encoding: 'utf-8' });
|
|
509
|
+
} catch (exc) {
|
|
510
|
+
if (exc instanceof ExitError) {
|
|
511
|
+
return exc.code;
|
|
512
|
+
}
|
|
513
|
+
throw exc;
|
|
514
|
+
}
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function _isCliEntry(): boolean {
|
|
519
|
+
if (process.argv[1] === undefined) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
523
|
+
if (import.meta.url === argvUrl) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
527
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
528
|
+
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
529
|
+
// symlink path. Compare realpaths so the entry guard still fires
|
|
530
|
+
// (without this the CLI silently no-ops when run through a symlink).
|
|
531
|
+
try {
|
|
532
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
533
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
534
|
+
return here === argv;
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const _invokedDirectly =
|
|
541
|
+
_isCliEntry();
|
|
542
|
+
if (_invokedDirectly) {
|
|
543
|
+
process.exitCode = main();
|
|
544
|
+
}
|
|
Binary file
|