@event4u/agent-config 8.7.0 → 8.9.0
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 +1 -1
- package/CHANGELOG.md +109 -222
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- package/dist/agent-src/commands/research/deep.md +8 -0
- package/dist/agent-src/commands/research/report.md +7 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -0
- package/dist/agent-src/commands/roadmap/process-full.md +31 -5
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +36 -5
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
- package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
- package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
- package/dist/agent-src/rules/active-remediation.md +92 -0
- package/dist/agent-src/rules/agent-authority.md +1 -1
- package/dist/agent-src/rules/broken-access-control.md +106 -0
- package/dist/agent-src/rules/communication-through-line.md +42 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -0
- package/dist/agent-src/rules/direct-answers.md +1 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
- package/dist/agent-src/rules/git-history-discipline.md +18 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +66 -1
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +8 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
- package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +112 -0
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-review/SKILL.md +29 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
- package/dist/agent-src/skills/mcp/SKILL.md +24 -0
- package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +46 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +112 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +72 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +60 -0
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +268 -46
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +18 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/install/install.mjs +141 -3
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_builtin_names.js +145 -0
- package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
- package/dist/scripts/condense.js +14 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +58 -2
- package/docs/architecture.md +3 -3
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -5
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-verification.md +124 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -2
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/experiences/content_creator.md +2 -0
- package/docs/experiences/finance.md +2 -0
- package/docs/experiences/founder.md +2 -0
- package/docs/experiences/ops.md +2 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
- package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
- package/docs/guidelines/agent-infra/memory-access.md +12 -0
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
- package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
- package/docs/guidelines/design-canon.md +1 -0
- package/docs/proof.md +60 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +116 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/src/config/agent-settings.template.yml +25 -6
- package/src/scripts/_dispatch.bash +27 -0
- package/src/scripts/_lib/claude_builtin_names.ts +148 -0
- package/src/scripts/_lib/file_slicer.ts +117 -0
- package/src/scripts/_lib/lexical_index.ts +155 -0
- package/src/scripts/_lib/orchestration_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_record.ts +170 -0
- package/src/scripts/_lib/orchestration_savings.ts +180 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
- package/src/scripts/_lib/security_lint.ts +1 -0
- package/src/scripts/_lib/stat_index.ts +98 -0
- package/src/scripts/benchmark.ts +122 -0
- package/src/scripts/build_proof.ts +51 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +17 -0
- package/src/scripts/hot_context_hook.ts +47 -2
- package/src/scripts/install-hooks.sh +25 -0
- package/src/scripts/install.ts +35 -2
- package/src/scripts/learning_sidecar.ts +0 -0
- package/src/scripts/lint_agent_skill_names.ts +29 -0
- package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
- package/src/scripts/lint_knowledge_scale.ts +4 -3
- package/src/scripts/lint_roadmap_complexity.ts +124 -0
- package/src/scripts/lint_versioned_cache.ts +195 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +23 -3
- package/src/scripts/mcp_server/tools.ts +182 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_record.ts +140 -0
- package/src/scripts/orchestration_savings_report.ts +124 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -0
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/score_domain_truth.ts +192 -0
- package/src/scripts/second_brain_retrieval.ts +278 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/second_brain_score.ts +186 -0
- package/src/scripts/session_memory_index.ts +94 -0
- package/src/scripts/skill_eval_coverage.ts +318 -0
- package/src/server/schemas/settings.ts +5 -2
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
32
|
import * as fs from 'node:fs';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
33
34
|
import * as path from 'node:path';
|
|
34
35
|
import process from 'node:process';
|
|
35
36
|
import { fileURLToPath } from 'node:url';
|
|
@@ -309,6 +310,39 @@ export function restore_hot_context(
|
|
|
309
310
|
return { action: 'inject', reason: `restored (source=${source || 'startup'})`, context: block };
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// ---------------------------------------------------------------------
|
|
314
|
+
// opt-in memory session index (road-to-memory-retrieval-economy P5)
|
|
315
|
+
// ---------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build the compact memory index when `memory.session_index: on`; `null`
|
|
319
|
+
* on the default-off path, empty corpus, or any failure (never blocks).
|
|
320
|
+
* Memory roots are cwd-relative (same contract as the MCP tool handlers),
|
|
321
|
+
* so the build runs chdir-wrapped to the workspace root.
|
|
322
|
+
*/
|
|
323
|
+
function _session_index_block_or_null(root: string): string | null {
|
|
324
|
+
try {
|
|
325
|
+
// Lazy require (ESM-safe via createRequire) keeps the default-off
|
|
326
|
+
// fast path free of the memory_lookup + settings-cascade import cost.
|
|
327
|
+
const req = createRequire(import.meta.url);
|
|
328
|
+
const mod = req('./session_memory_index.js') as {
|
|
329
|
+
session_index_enabled: (root: string) => boolean;
|
|
330
|
+
build_session_index_block: () => string | null;
|
|
331
|
+
};
|
|
332
|
+
if (!mod.session_index_enabled(root)) return null;
|
|
333
|
+
const prev = process.cwd();
|
|
334
|
+
try {
|
|
335
|
+
process.chdir(root);
|
|
336
|
+
return mod.build_session_index_block();
|
|
337
|
+
} finally {
|
|
338
|
+
process.chdir(prev);
|
|
339
|
+
}
|
|
340
|
+
} catch (exc) {
|
|
341
|
+
process.stderr.write(`hot-context-hook: session index skipped: ${String(exc)}\n`);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
312
346
|
// ---------------------------------------------------------------------
|
|
313
347
|
// CLI — dispatcher concern entry point
|
|
314
348
|
// ---------------------------------------------------------------------
|
|
@@ -351,12 +385,23 @@ export function main(): number {
|
|
|
351
385
|
} else if (event === 'session_start') {
|
|
352
386
|
const source = String(payload.source ?? '');
|
|
353
387
|
const decision = restore_hot_context(root, source);
|
|
388
|
+
const blocks: string[] = [];
|
|
354
389
|
if (decision.action === 'inject' && decision.context) {
|
|
390
|
+
blocks.push(decision.context);
|
|
391
|
+
}
|
|
392
|
+
// Opt-in compact memory index (road-to-memory-retrieval-economy
|
|
393
|
+
// P5) — default OFF; rides the same injection surface. Memory
|
|
394
|
+
// roots are cwd-relative, so resolve from the workspace root.
|
|
395
|
+
const indexBlock = _session_index_block_or_null(root);
|
|
396
|
+
if (indexBlock !== null) {
|
|
397
|
+
blocks.push(indexBlock);
|
|
398
|
+
}
|
|
399
|
+
if (blocks.length > 0) {
|
|
355
400
|
process.stdout.write(
|
|
356
401
|
JSON.stringify({
|
|
357
402
|
decision: 'allow',
|
|
358
|
-
reason: decision.reason,
|
|
359
|
-
context:
|
|
403
|
+
reason: decision.action === 'inject' ? decision.reason : 'memory session index',
|
|
404
|
+
context: blocks.join('\n\n'),
|
|
360
405
|
}) + '\n',
|
|
361
406
|
);
|
|
362
407
|
}
|
|
@@ -42,6 +42,31 @@ if ! ./scripts-run src/scripts/check_no_conflict_markers --quiet; then
|
|
|
42
42
|
fail=1
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
|
+
# Changed-files static pass (typecheck + lint) — the deterministic backstop to
|
|
46
|
+
# the behavioural verify-before-complete rule (#818). Catches the compile/lint
|
|
47
|
+
# errors that reach remote CI "Static Checks" (e.g. TS18048). Runs only when
|
|
48
|
+
# the push touches .ts, so docs-only pushes stay fast. Skip a genuine WIP push
|
|
49
|
+
# with AGENT_CONFIG_SKIP_PREPUSH_STATIC=1 (the agent cannot use --no-verify).
|
|
50
|
+
echo "🔍 Static check on changed TypeScript (typecheck + lint)..."
|
|
51
|
+
if [ "${AGENT_CONFIG_SKIP_PREPUSH_STATIC:-}" = "1" ]; then
|
|
52
|
+
echo "⏭️ skipped via AGENT_CONFIG_SKIP_PREPUSH_STATIC=1"
|
|
53
|
+
else
|
|
54
|
+
base="$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD~1)"
|
|
55
|
+
changed_ts="$(git diff --name-only --diff-filter=d "$base"...HEAD -- '*.ts' 2>/dev/null || true)"
|
|
56
|
+
if [ -n "$changed_ts" ]; then
|
|
57
|
+
if ! npx eslint $changed_ts; then
|
|
58
|
+
echo "❌ ESLint failed on changed TypeScript. Fix before pushing (or AGENT_CONFIG_SKIP_PREPUSH_STATIC=1 for a WIP push)."
|
|
59
|
+
fail=1
|
|
60
|
+
fi
|
|
61
|
+
if ! npm run --silent typecheck; then
|
|
62
|
+
echo "❌ Typecheck (tsc) failed — this is the class of error remote CI 'Static Checks' catches (#818). Fix before pushing."
|
|
63
|
+
fail=1
|
|
64
|
+
fi
|
|
65
|
+
else
|
|
66
|
+
echo "⏭️ no changed .ts vs origin/main — skipping."
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
45
70
|
if [ $fail -ne 0 ]; then
|
|
46
71
|
echo ""
|
|
47
72
|
echo " Push blocked — fix the failures above and re-push."
|
package/src/scripts/install.ts
CHANGED
|
@@ -72,6 +72,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
72
72
|
import type * as YamlModule from 'yaml';
|
|
73
73
|
|
|
74
74
|
import { build_merge_entries } from './_lib/json_pointers.js';
|
|
75
|
+
import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
|
|
75
76
|
import * as installed_lock from './_lib/installed_lock.js';
|
|
76
77
|
import * as surface_tiers from './_lib/surface_tiers.js';
|
|
77
78
|
import * as global_deploy_inventory from './_lib/global_deploy_inventory.js';
|
|
@@ -2847,15 +2848,40 @@ function _deploy_claude_desktop(
|
|
|
2847
2848
|
* discovery (revert = one package release, no per-machine gating).
|
|
2848
2849
|
* Tier-2/internal flat commands stay command files. A command whose name
|
|
2849
2850
|
* collides with a real skill is skipped (the skill already owns `/name`).
|
|
2851
|
+
*
|
|
2852
|
+
* Reserved-name floor: a flat `/name` that equals a Claude Code BUILT-IN
|
|
2853
|
+
* command or bundled skill (see `_lib/claude_builtin_names.ts`) would shadow
|
|
2854
|
+
* the built-in — e.g. a `review` wrapper hides Claude Code's own `/review`.
|
|
2855
|
+
* Such slugs get NEITHER a skill wrapper NOR a flat command file on this
|
|
2856
|
+
* anchor (their nested `/cluster:sub` commands are untouched); the flat file
|
|
2857
|
+
* is removed so a future upstream flat-discovery fix cannot resurrect the
|
|
2858
|
+
* shadowing. The suite complements the host — it never overlays built-ins.
|
|
2850
2859
|
*/
|
|
2851
2860
|
const _CLAUDE_FLAT_WRAPPER_EXTRA = new Set(['commit']); // essential; absent from the manifest
|
|
2852
2861
|
export function _apply_claude_flat_command_wrappers(
|
|
2853
2862
|
anchor: string,
|
|
2854
2863
|
package_root: string,
|
|
2855
2864
|
current_files: Set<string>,
|
|
2856
|
-
): { wrapped: string[]; collisions: string[] } {
|
|
2865
|
+
): { wrapped: string[]; collisions: string[]; reserved: string[] } {
|
|
2857
2866
|
const wrapped: string[] = [];
|
|
2858
2867
|
const collisions: string[] = [];
|
|
2868
|
+
const reserved: string[] = [];
|
|
2869
|
+
// Reserved-name sweep over ALL flat command files (any tier): a name
|
|
2870
|
+
// shadowing a Claude Code built-in never ships on the claude-code anchor.
|
|
2871
|
+
const commands_dir = path.join(anchor, 'commands');
|
|
2872
|
+
let flat_entries: string[] = [];
|
|
2873
|
+
try {
|
|
2874
|
+
flat_entries = fs.readdirSync(commands_dir).filter((f) => f.endsWith('.md'));
|
|
2875
|
+
} catch {
|
|
2876
|
+
// No commands dir → nothing to sweep.
|
|
2877
|
+
}
|
|
2878
|
+
for (const fname of flat_entries.sort()) {
|
|
2879
|
+
const slug = fname.slice(0, -'.md'.length);
|
|
2880
|
+
if (!is_claude_builtin_name(slug)) continue;
|
|
2881
|
+
fs.rmSync(path.join(commands_dir, fname), { force: true });
|
|
2882
|
+
current_files.delete(`commands/${fname}`);
|
|
2883
|
+
reserved.push(slug);
|
|
2884
|
+
}
|
|
2859
2885
|
// Tier-0/1 visible command slugs from the locked discovery manifest.
|
|
2860
2886
|
const eligible = new Set<string>(_CLAUDE_FLAT_WRAPPER_EXTRA);
|
|
2861
2887
|
try {
|
|
@@ -2892,7 +2918,7 @@ export function _apply_claude_flat_command_wrappers(
|
|
|
2892
2918
|
current_files.add(`skills/${slug}/SKILL.md`);
|
|
2893
2919
|
wrapped.push(slug);
|
|
2894
2920
|
}
|
|
2895
|
-
return { wrapped, collisions };
|
|
2921
|
+
return { wrapped, collisions, reserved };
|
|
2896
2922
|
}
|
|
2897
2923
|
|
|
2898
2924
|
function _deploy_global_content(
|
|
@@ -2949,6 +2975,13 @@ function _deploy_global_content(
|
|
|
2949
2975
|
'skill wrappers (Claude Code flat-command discovery workaround)',
|
|
2950
2976
|
);
|
|
2951
2977
|
}
|
|
2978
|
+
if (res.reserved.length > 0 && !state.QUIET) {
|
|
2979
|
+
info(
|
|
2980
|
+
` claude-code: ${res.reserved.length} flat command(s) withheld — name is a ` +
|
|
2981
|
+
`Claude Code built-in (${res.reserved.join(', ')}); nested /cluster:sub ` +
|
|
2982
|
+
'commands remain available',
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2952
2985
|
}
|
|
2953
2986
|
|
|
2954
2987
|
const missing_targets = _verify_deploy_targets(anchor, plan);
|
|
Binary file
|
|
@@ -14,6 +14,7 @@ import * as path from 'node:path';
|
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
15
|
|
|
16
16
|
import { command_slug } from './_lib/agent_src.js';
|
|
17
|
+
import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
|
|
17
18
|
|
|
18
19
|
const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
|
|
@@ -97,6 +98,18 @@ function frontmatterNameFromFile(p: string): string | null {
|
|
|
97
98
|
return _frontmatter_name(fs.readFileSync(p, 'utf-8'));
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
const USER_INVOCABLE_FALSE_RE = /^user-invocable:\s*false\s*$/m;
|
|
102
|
+
|
|
103
|
+
/** True when the SKILL.md opts out of slash registration (`user-invocable: false`). */
|
|
104
|
+
export function _opts_out_of_slash(text: string): boolean {
|
|
105
|
+
if (!text.startsWith('---')) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const end = text.indexOf('\n---', 3);
|
|
109
|
+
const block = end !== -1 ? text.slice(3, end) : text;
|
|
110
|
+
return USER_INVOCABLE_FALSE_RE.test(block);
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
export function _spec_violation(name: string): string | null {
|
|
101
114
|
if (name.length > MAX_NAME_LEN) {
|
|
102
115
|
return `longer than ${MAX_NAME_LEN} chars`;
|
|
@@ -178,6 +191,22 @@ export function check_skills(): string[] {
|
|
|
178
191
|
'requires the folder name to match the `name:` field',
|
|
179
192
|
);
|
|
180
193
|
}
|
|
194
|
+
// Reserved-name floor: a skill named after a Claude Code built-in
|
|
195
|
+
// command / bundled skill shadows the built-in when projected as
|
|
196
|
+
// `/name` (e.g. a `mcp` skill hides Claude Code's own /mcp dialog).
|
|
197
|
+
// Opting out of slash registration (`user-invocable: false`) keeps
|
|
198
|
+
// the skill model-loadable while the host retains the /name.
|
|
199
|
+
// Reserved set: src/scripts/_lib/claude_builtin_names.ts.
|
|
200
|
+
if (
|
|
201
|
+
is_claude_builtin_name(dirname) &&
|
|
202
|
+
!_opts_out_of_slash(fs.readFileSync(md, 'utf-8'))
|
|
203
|
+
) {
|
|
204
|
+
violations.push(
|
|
205
|
+
`${rel}: \`${dirname}\` is a Claude Code built-in command name — ` +
|
|
206
|
+
'add `user-invocable: false` (model-only invocation) or rename ' +
|
|
207
|
+
'the skill; the suite must never shadow a host built-in',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
181
210
|
}
|
|
182
211
|
return violations;
|
|
183
212
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Lint behavioural-eval freshness — a behavioural `evals.json` that pins the
|
|
4
|
+
* skill body it was authored against must still match the current SKILL.md
|
|
5
|
+
* body, or it is flagged stale (road-to-skill-eval-coverage Phase 3).
|
|
6
|
+
*
|
|
7
|
+
* Sibling to `lint_eval_freshness.ts`, DIFFERENT axis: that gate keeps a
|
|
8
|
+
* corpus-backed skill's TRIGGER eval fresh against an upstream SHA; this gate
|
|
9
|
+
* keeps a skill's BEHAVIOURAL eval fresh against its own SKILL.md body. The
|
|
10
|
+
* two never conflate.
|
|
11
|
+
*
|
|
12
|
+
* A skill's `evals/evals.json` is in scope when it carries a `skill_body_sha`
|
|
13
|
+
* (SHA-256 of the SKILL.md body — post-frontmatter, trimmed — at authoring
|
|
14
|
+
* time). Absent = unpinned, OUT of scope (nothing to keep fresh), mirroring
|
|
15
|
+
* `lint_eval_freshness`'s `upstream: null` handling. So the currently-shipped
|
|
16
|
+
* evals (which carry no pin) never fail — the gate only bites once an author
|
|
17
|
+
* opts in by pinning.
|
|
18
|
+
*
|
|
19
|
+
* In scope, the gate fails when the pinned `skill_body_sha` differs from the
|
|
20
|
+
* current SKILL.md body sha: the body moved, so the eval may no longer
|
|
21
|
+
* exercise it — re-verify and re-pin (`--write-floor`-style human step) rather
|
|
22
|
+
* than let a stale eval pass silently.
|
|
23
|
+
*
|
|
24
|
+
* Deterministic, no token spend: reads on-disk files only. The live re-eval is
|
|
25
|
+
* a separate, spend-bearing step; this gate verifies the pin is current.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 clean (or none in scope) / 1 at least one stale / 2 usage.
|
|
28
|
+
*/
|
|
29
|
+
import * as crypto from 'node:crypto';
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
35
|
+
export const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
36
|
+
let SKILLS_DIR = path.join(REPO, 'src', 'skills');
|
|
37
|
+
|
|
38
|
+
export function _setSkillsDirForTest(p: string): void {
|
|
39
|
+
SKILLS_DIR = p;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Json = string | number | boolean | null | Json[] | { [k: string]: Json };
|
|
43
|
+
|
|
44
|
+
function _splitBody(text: string): string {
|
|
45
|
+
if (!text.startsWith('---\n')) return text.replace(/\s+$/, '');
|
|
46
|
+
const end = text.indexOf('\n---', 4);
|
|
47
|
+
if (end === -1) return text.replace(/\s+$/, '');
|
|
48
|
+
return text.slice(end + 4).replace(/^\n+/, '').replace(/\s+$/, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** SHA-256 of a skill's SKILL.md body (post-frontmatter, trailing-ws-trimmed). */
|
|
52
|
+
export function skillBodySha(skillMdText: string): string {
|
|
53
|
+
return crypto.createHash('sha256').update(_splitBody(skillMdText), 'utf-8').digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FreshnessResult {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
stale: Array<{ skill: string; pinned: string; current: string }>;
|
|
59
|
+
inScope: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function checkFreshness(): FreshnessResult {
|
|
63
|
+
const stale: FreshnessResult['stale'] = [];
|
|
64
|
+
let inScope = 0;
|
|
65
|
+
let entries: fs.Dirent[];
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: true, stale, inScope };
|
|
70
|
+
}
|
|
71
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
72
|
+
for (const e of entries) {
|
|
73
|
+
if (!e.isDirectory()) continue;
|
|
74
|
+
const evalsPath = path.join(SKILLS_DIR, e.name, 'evals', 'evals.json');
|
|
75
|
+
const skillMd = path.join(SKILLS_DIR, e.name, 'SKILL.md');
|
|
76
|
+
if (!fs.existsSync(evalsPath) || !fs.existsSync(skillMd)) continue;
|
|
77
|
+
let spec: Json;
|
|
78
|
+
try {
|
|
79
|
+
spec = JSON.parse(fs.readFileSync(evalsPath, 'utf-8')) as Json;
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const pinned =
|
|
84
|
+
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
85
|
+
? (spec['skill_body_sha'] as string | undefined)
|
|
86
|
+
: undefined;
|
|
87
|
+
if (typeof pinned !== 'string' || pinned === '') continue; // unpinned → out of scope
|
|
88
|
+
inScope += 1;
|
|
89
|
+
const current = skillBodySha(fs.readFileSync(skillMd, 'utf-8'));
|
|
90
|
+
if (current !== pinned) {
|
|
91
|
+
stale.push({ skill: e.name, pinned, current });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { ok: stale.length === 0, stale, inScope };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
98
|
+
let quiet = false;
|
|
99
|
+
for (const a of argv) {
|
|
100
|
+
if (a === '--quiet') quiet = true;
|
|
101
|
+
else {
|
|
102
|
+
process.stderr.write('usage: lint_behavioural_eval_freshness [--quiet]\n');
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { ok, stale, inScope } = checkFreshness();
|
|
107
|
+
if (!ok) {
|
|
108
|
+
for (const s of stale) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`stale behavioural eval: ${s.skill}/evals/evals.json pinned ${s.pinned.slice(0, 12)}… ` +
|
|
111
|
+
`but SKILL.md body is ${s.current.slice(0, 12)}… — re-verify and re-pin skill_body_sha\n`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
if (!quiet) {
|
|
117
|
+
process.stdout.write(
|
|
118
|
+
`✅ behavioural-eval freshness: ${inScope} pinned eval(s) current (unpinned evals out of scope).\n`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _isCliEntry(): boolean {
|
|
125
|
+
if (process.argv[1] === undefined) return false;
|
|
126
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
127
|
+
if (import.meta.url === argvUrl) return true;
|
|
128
|
+
try {
|
|
129
|
+
return (
|
|
130
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
131
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
132
|
+
);
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (_isCliEntry()) {
|
|
139
|
+
process.exit(main());
|
|
140
|
+
}
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* - sessions scale — `agents/knowledge/sessions/` > 50 pages
|
|
15
15
|
* → design fold-through-consolidate-gate for tracked pages
|
|
16
16
|
* - type scale — any single memory/knowledge type > 200 files
|
|
17
|
-
* →
|
|
17
|
+
* → rank via `_lib/lexical_index.ts` (hand-rolled BM25 +
|
|
18
|
+
* trigram, no engine fork / no FTS5; ADR-061 honoured)
|
|
18
19
|
* - corpus scale — > 500 files across all types
|
|
19
|
-
* → same
|
|
20
|
+
* → same lexical-index activation path
|
|
20
21
|
* - hot-context size — `agents/runtime/state/hot-context.md` > 600 tokens
|
|
21
22
|
* (estimated at 4 chars/token) → trim schema / fix the
|
|
22
23
|
* deterministic writer
|
|
@@ -151,7 +152,7 @@ export function runChecks(root: string): Warning[] {
|
|
|
151
152
|
metric: `${n}/${TYPE_FILES_MAX}`,
|
|
152
153
|
message:
|
|
153
154
|
`type '${type}' holds ${n} files (> ${TYPE_FILES_MAX}). ` +
|
|
154
|
-
'Activation path (
|
|
155
|
+
'Activation path (resolved, road-to-retrieval-substrate-hardening B2): rank via the hand-rolled BM25 + trigram index in `_lib/lexical_index.ts` (pure stdlib, NO engine fork / NO SQLite-FTS5, ADR-061 honoured). Measured lift: mean tie-set 3.333 → 1.0 (internal/bench/reports/lexical-ranking.json). Wire it lazily at first lookup + a stat-index, no vectors, no service.',
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -190,6 +190,9 @@ function lint_roadmap(
|
|
|
190
190
|
_check_execution_mode(fm, problems);
|
|
191
191
|
if (warnings) {
|
|
192
192
|
_check_autonomous_authoring(text, fm, warnings);
|
|
193
|
+
_check_human_gate_steps(text, warnings);
|
|
194
|
+
_check_human_gate_phase_headings(text, warnings);
|
|
195
|
+
_check_human_gate_exit_criteria(text, warnings);
|
|
193
196
|
}
|
|
194
197
|
return problems;
|
|
195
198
|
}
|
|
@@ -236,6 +239,127 @@ function _check_autonomous_authoring(text: string, fm: string, warnings: string[
|
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
// Human-gate step patterns (templates/roadmaps.md rule 22) — checkbox
|
|
243
|
+
// steps that park work on a human instead of the agent. Steps are
|
|
244
|
+
// agent-executable by default; genuinely user-clearable gates belong in
|
|
245
|
+
// a structured `## Blockers` entry, agent-checkable verifications become
|
|
246
|
+
// commands / targeted tests. Warnings never fail the lint; they fire for
|
|
247
|
+
// every execution mode (an interactive roadmap with sprinkled human-check
|
|
248
|
+
// steps is the same authoring bug, just slower to hurt).
|
|
249
|
+
const HUMAN_GATE_STEP_PATS: Array<[RegExp, string]> = [
|
|
250
|
+
[
|
|
251
|
+
/\b(user|maintainer|human|operator|stakeholder|product owner) (verifies|reviews|approves|confirms|signs?[- ]off(?: on)?|inspects|validates)\b/i,
|
|
252
|
+
"'<human> verifies / reviews / approves …' step",
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
/\bmanually (verify|review|check|test|confirm|inspect|validate)\b/i,
|
|
256
|
+
"'manually verify / check …' step",
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
/\bwait for (the )?(user|maintainer|stakeholders?|approval|confirmation|review|feedback)\b/i,
|
|
260
|
+
"'wait for approval / user' step",
|
|
261
|
+
],
|
|
262
|
+
[/\bask the (user|maintainer)\b/i, "'ask the user' step"],
|
|
263
|
+
[
|
|
264
|
+
/\b(get|obtain|request|await|collect) ((the|an?|final) )?((user|maintainer|stakeholder|product owner|team) )?(approval|confirmation|sign[- ]?off)\b/i,
|
|
265
|
+
"'obtain approval / sign-off' step",
|
|
266
|
+
],
|
|
267
|
+
[/\bsign[- ]?off (from|by)\b/i, "'sign-off from <human>' step"],
|
|
268
|
+
[/\bhuman review (required|needed|gate)\b/i, "'human review required' step"],
|
|
269
|
+
[
|
|
270
|
+
/\b(confirm|check|verify|review|clarify) with the (user|maintainer)\b/i,
|
|
271
|
+
"'confirm with the user' step",
|
|
272
|
+
],
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// Human-gate phase headings (template rule 22) — a phase whose TITLE is
|
|
276
|
+
// the human checkpoint ("## Phase 5 — Review / Sign-off", "## User
|
|
277
|
+
// Acceptance"). Whole-heading match after an optional "Phase N" prefix,
|
|
278
|
+
// so working headings ("## Review existing skills") never fire.
|
|
279
|
+
const HUMAN_GATE_HEADING_PAT =
|
|
280
|
+
/^#{2,3}\s+(?:phase\s+\d+\s*(?:[—–:.-])?\s*)?(?:review(?:\s*(?:[/&+]|and)\s*sign[- ]?off)?|sign[- ]?off|approval|user\s+acceptance(?:\s+testing)?|manual\s+(?:verification|review|testing|qa)|human\s+(?:validation|review))\s*$/i;
|
|
281
|
+
|
|
282
|
+
// Human-approval exit / acceptance criteria (template rule 22) — criteria
|
|
283
|
+
// must be agent-decidable (exit code, file exists, test passes), never
|
|
284
|
+
// "user approves" / "looks good". Applied inside Exit-criteria /
|
|
285
|
+
// Acceptance blocks only.
|
|
286
|
+
const HUMAN_GATE_CRITERIA_PAT =
|
|
287
|
+
/\b(user|maintainer|human|operator|stakeholder|product owner)\b[^\n]{0,60}\b(approv\w*|confirm\w*|sign[- ]?off|reviews)\b|\b(approval|confirmation|sign[- ]?off)\s+(from|by|received)\b|\blooks good\b/i;
|
|
288
|
+
|
|
289
|
+
const EXIT_CRITERIA_OPENER_PAT = /^\s*(?:[-*]\s+)?\*{0,2}(exit criteria|acceptance(?: criteria)?)\*{0,2}\s*:?/i;
|
|
290
|
+
|
|
291
|
+
const RULE22_HINT =
|
|
292
|
+
'templates/roadmaps.md rule 22: steps and criteria are agent-decidable; ' +
|
|
293
|
+
'replace with an agent-verifiable check (command / targeted test) or, ' +
|
|
294
|
+
'if only a human can decide/authorize, a structured ## Blockers entry';
|
|
295
|
+
|
|
296
|
+
function _check_human_gate_steps(text: string, warnings: string[]): void {
|
|
297
|
+
for (const rawLine of text.split('\n')) {
|
|
298
|
+
if (!/^\s*-\s*\[ \]/.test(rawLine)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
for (const [re, label] of HUMAN_GATE_STEP_PATS) {
|
|
302
|
+
if (re.test(rawLine)) {
|
|
303
|
+
warnings.push(
|
|
304
|
+
`human-gate step (${label}): ` +
|
|
305
|
+
`"${rawLine.trim().slice(0, 80)}" — ${RULE22_HINT}`,
|
|
306
|
+
);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function _check_human_gate_phase_headings(text: string, warnings: string[]): void {
|
|
314
|
+
for (const rawLine of text.split('\n')) {
|
|
315
|
+
if (!rawLine.startsWith('#')) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (HUMAN_GATE_HEADING_PAT.test(rawLine)) {
|
|
319
|
+
warnings.push(
|
|
320
|
+
`human-gate phase heading: "${rawLine.trim().slice(0, 80)}" — ` +
|
|
321
|
+
'a phase whose work only a human can finish blocks autonomous ' +
|
|
322
|
+
`execution; ${RULE22_HINT}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _check_human_gate_exit_criteria(text: string, warnings: string[]): void {
|
|
329
|
+
let inHeadingBlock = false;
|
|
330
|
+
let inInlineBlock = false;
|
|
331
|
+
for (const rawLine of text.split('\n')) {
|
|
332
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(rawLine);
|
|
333
|
+
if (heading) {
|
|
334
|
+
inHeadingBlock = /\b(acceptance|exit criteria)\b/i.test(heading[1]!);
|
|
335
|
+
inInlineBlock = false;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (rawLine.trim() === '') {
|
|
339
|
+
inInlineBlock = false;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const opener = EXIT_CRITERIA_OPENER_PAT.test(rawLine);
|
|
343
|
+
if (opener) {
|
|
344
|
+
inInlineBlock = true;
|
|
345
|
+
}
|
|
346
|
+
if (!inHeadingBlock && !inInlineBlock) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Open-checkbox lines are covered by _check_human_gate_steps;
|
|
350
|
+
// done/deferred/cancelled lines record history, not future gates.
|
|
351
|
+
if (/^\s*-\s*\[[ xX~-]\]/.test(rawLine)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (HUMAN_GATE_CRITERIA_PAT.test(rawLine)) {
|
|
355
|
+
warnings.push(
|
|
356
|
+
`human-approval exit criterion: "${rawLine.trim().slice(0, 80)}" — ` +
|
|
357
|
+
`criteria must be agent-decidable; ${RULE22_HINT}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
239
363
|
function _check_execution_mode(fm: string, problems: string[]): void {
|
|
240
364
|
if (!fm || !/^execution:/m.test(fm)) {
|
|
241
365
|
return; // absent field = interactive default — valid
|