@event4u/agent-config 8.6.0 → 8.8.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 +81 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- 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 +15 -0
- package/dist/agent-src/rules/active-remediation.md +92 -0
- 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/minimal-safe-diff.md +18 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +105 -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/dependency-upgrade/SKILL.md +20 -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/roadmap-writing/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +24 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +196 -16
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +12 -4
- package/dist/install/emit_host_rules_cli.js +185 -0
- package/dist/install/emit_host_rules_cli.js.map +1 -0
- package/dist/install/install.mjs +11 -2
- package/dist/install/plan.js +4 -1
- package/dist/install/plan.js.map +1 -1
- package/dist/install/rule_scope.js +109 -0
- package/dist/install/rule_scope.js.map +1 -0
- package/dist/install/rule_scope_cli.js +77 -0
- package/dist/install/rule_scope_cli.js.map +1 -0
- package/dist/install/wizard-plan.js +14 -1
- package/dist/install/wizard-plan.js.map +1 -1
- 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/agent_settings.js +963 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -0
- package/dist/scripts/_lib/agent_src.js +607 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +209 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -0
- package/dist/scripts/_lib/model_tier.js +64 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -0
- package/dist/scripts/_lib/script_output.js +140 -0
- package/dist/scripts/_lib/script_output.js.map +1 -0
- package/dist/scripts/_lib/user_global_paths.js +297 -0
- package/dist/scripts/_lib/user_global_paths.js.map +1 -0
- package/dist/scripts/condense.js +2181 -0
- package/dist/scripts/condense.js.map +1 -0
- package/dist/server/routes/install.js +18 -2
- package/dist/server/routes/install.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +14 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -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/memory-access.md +12 -0
- package/docs/proof.md +45 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +73 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/config/agent-settings.template.yml +29 -8
- package/src/install/emit_host_rules_cli.ts +190 -0
- package/src/install/plan.ts +11 -1
- package/src/install/rule_scope.ts +120 -0
- package/src/install/rule_scope_cli.ts +79 -0
- package/src/install/wizard-plan.ts +22 -1
- package/src/scripts/_lib/orchestration_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_savings.ts +127 -0
- package/src/scripts/build_proof.ts +38 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_trigger_eval_presence.ts +171 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/generate_index.ts +14 -0
- package/src/scripts/hot_context_hook.ts +47 -2
- package/src/scripts/install.sh +118 -4
- package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
- package/src/scripts/mcp_server/tools.ts +159 -1
- package/src/scripts/memory_lookup.ts +184 -6
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_savings_report.ts +100 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -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/scripts/trigger_eval_grandfather.json +226 -0
- package/src/server/schemas/settings.ts +11 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration savings aggregator (pure, no-I/O).
|
|
3
|
+
*
|
|
4
|
+
* Aggregates realized orchestration telemetry into a token-savings report.
|
|
5
|
+
* Consumes the `orchestration` sub-object of audit-log-v1 JSONL lines
|
|
6
|
+
* (contract: `src/agent-src/contexts/execution/orchestration-telemetry.md`).
|
|
7
|
+
*
|
|
8
|
+
* HONEST LIMIT — the telemetry records `token_delta` (net dispatch cost vs the
|
|
9
|
+
* in-session baseline; negative = saved) but NOT the absolute baseline. A clean
|
|
10
|
+
* "% of session saved" is therefore NOT derivable from this data alone — only
|
|
11
|
+
* ABSOLUTE net tokens saved, plus the measured-vs-estimated provenance split.
|
|
12
|
+
* A percentage would need an absolute-baseline field on the telemetry (see the
|
|
13
|
+
* report `notes` and the PR's follow-up section).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type TokenDeltaProvenance = 'measured' | 'estimated';
|
|
17
|
+
|
|
18
|
+
/** The `orchestration` sub-object shape we read (subset — additive fields only). */
|
|
19
|
+
export interface OrchestrationRecord {
|
|
20
|
+
spawn_count?: number;
|
|
21
|
+
token_delta?: number;
|
|
22
|
+
token_delta_provenance?: TokenDeltaProvenance;
|
|
23
|
+
tier_chosen?: string | null;
|
|
24
|
+
task_class?: string | null;
|
|
25
|
+
outcome?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A parsed audit-log-v1 line (only the fields this aggregator touches). */
|
|
29
|
+
export interface AuditLine {
|
|
30
|
+
input_kind?: string;
|
|
31
|
+
orchestration?: OrchestrationRecord;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProvenanceStat {
|
|
35
|
+
dispatches: number;
|
|
36
|
+
/** Σ token_delta for this provenance (negative = net saved). */
|
|
37
|
+
net_token_delta: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SavingsReport {
|
|
41
|
+
/** Lines carrying an orchestration object with spawn_count > 0. */
|
|
42
|
+
dispatches: number;
|
|
43
|
+
total_spawns: number;
|
|
44
|
+
/** Σ token_delta across all dispatches (negative = net saved). */
|
|
45
|
+
net_token_delta: number;
|
|
46
|
+
/** Magnitude of tokens saved (sum of negative deltas, as a positive number). */
|
|
47
|
+
tokens_saved: number;
|
|
48
|
+
/** Sum of positive deltas (orchestration that cost MORE than baseline). */
|
|
49
|
+
tokens_added: number;
|
|
50
|
+
by_provenance: { measured: ProvenanceStat; estimated: ProvenanceStat };
|
|
51
|
+
/** tier_chosen → Σ token_delta (negative = net saved on that tier). */
|
|
52
|
+
by_tier: Record<string, number>;
|
|
53
|
+
/** task_class → Σ token_delta. */
|
|
54
|
+
by_task_class: Record<string, number>;
|
|
55
|
+
/** measured dispatches / dispatches, 0..1 (0 when no dispatches). */
|
|
56
|
+
measured_share: number;
|
|
57
|
+
/** Honest caveats about what the number does and does not mean. */
|
|
58
|
+
notes: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emptyProvenance(): ProvenanceStat {
|
|
62
|
+
return { dispatches: 0, net_token_delta: 0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Aggregate orchestration telemetry lines into a savings report.
|
|
67
|
+
*
|
|
68
|
+
* A line counts as a dispatch only when it carries an `orchestration` object
|
|
69
|
+
* with `spawn_count > 0` (spawn_count 0 / absent = handled in-session, not a
|
|
70
|
+
* dispatch). Unknown/absent fields default safely (token_delta → 0, provenance
|
|
71
|
+
* → estimated) so a pre-extension line never throws.
|
|
72
|
+
*/
|
|
73
|
+
export function aggregateOrchestrationSavings(lines: AuditLine[]): SavingsReport {
|
|
74
|
+
const report: SavingsReport = {
|
|
75
|
+
dispatches: 0,
|
|
76
|
+
total_spawns: 0,
|
|
77
|
+
net_token_delta: 0,
|
|
78
|
+
tokens_saved: 0,
|
|
79
|
+
tokens_added: 0,
|
|
80
|
+
by_provenance: { measured: emptyProvenance(), estimated: emptyProvenance() },
|
|
81
|
+
by_tier: {},
|
|
82
|
+
by_task_class: {},
|
|
83
|
+
measured_share: 0,
|
|
84
|
+
notes: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const o = line.orchestration;
|
|
89
|
+
if (!o) continue;
|
|
90
|
+
const spawns = o.spawn_count ?? 0;
|
|
91
|
+
if (!(spawns > 0)) continue; // in-session, not a dispatch
|
|
92
|
+
|
|
93
|
+
report.dispatches += 1;
|
|
94
|
+
report.total_spawns += spawns;
|
|
95
|
+
|
|
96
|
+
const delta = o.token_delta ?? 0;
|
|
97
|
+
report.net_token_delta += delta;
|
|
98
|
+
if (delta < 0) report.tokens_saved += -delta;
|
|
99
|
+
else report.tokens_added += delta;
|
|
100
|
+
|
|
101
|
+
// Provenance defaults to `estimated` when unstated (matches the
|
|
102
|
+
// telemetry contract: estimated is the lossy fallback).
|
|
103
|
+
const prov: TokenDeltaProvenance = o.token_delta_provenance === 'measured' ? 'measured' : 'estimated';
|
|
104
|
+
report.by_provenance[prov].dispatches += 1;
|
|
105
|
+
report.by_provenance[prov].net_token_delta += delta;
|
|
106
|
+
|
|
107
|
+
const tier = o.tier_chosen ?? 'unknown';
|
|
108
|
+
report.by_tier[tier] = (report.by_tier[tier] ?? 0) + delta;
|
|
109
|
+
|
|
110
|
+
const cls = o.task_class ?? 'unclassified';
|
|
111
|
+
report.by_task_class[cls] = (report.by_task_class[cls] ?? 0) + delta;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
report.measured_share = report.dispatches === 0 ? 0 : report.by_provenance.measured.dispatches / report.dispatches;
|
|
115
|
+
|
|
116
|
+
// Honest caveats — always attached so a consumer never over-reads the number.
|
|
117
|
+
if (report.dispatches === 0) {
|
|
118
|
+
report.notes.push('No orchestration telemetry yet (0 dispatches). The number accrues as `subagents.auto: on` runs delegate real slices.');
|
|
119
|
+
} else {
|
|
120
|
+
report.notes.push('Reports ABSOLUTE net token_delta (negative = saved). A "% of session saved" is NOT derivable — the telemetry records net delta, not the absolute baseline. A percentage needs an absolute-baseline field on the telemetry.');
|
|
121
|
+
if (report.by_provenance.estimated.dispatches > 0) {
|
|
122
|
+
report.notes.push(`${report.by_provenance.estimated.dispatches}/${report.dispatches} dispatch(es) use ESTIMATED token_delta (chars/4, lossy). Prefer measured (host usage metadata).`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
@@ -107,6 +107,42 @@ function render(): string {
|
|
|
107
107
|
L.push('Benchmark results are published under `docs/` including measured nulls.');
|
|
108
108
|
}
|
|
109
109
|
L.push('');
|
|
110
|
+
L.push('**Behavioural-eval coverage — the honest baseline.** Skill *quality* is only');
|
|
111
|
+
L.push('as good as its measurement. Today **2 of 264** skills carry a behavioural');
|
|
112
|
+
L.push('`evals.json`, and the highest-traffic / highest-cost tiers (default-surface +');
|
|
113
|
+
L.push('`rich` + routers, **0 of 35**) are not yet covered. We publish that gap rather');
|
|
114
|
+
L.push('than imply "264 evaluated skills": coverage is measured per tier');
|
|
115
|
+
L.push('(`./scripts-run src/scripts/skill_eval_coverage`) and **CI-ratcheted so it can');
|
|
116
|
+
L.push('only rise** — a merged change can never lower it. Authoring the priority-tier');
|
|
117
|
+
L.push('evals is gated on per-case human ratification (a generated assertion that');
|
|
118
|
+
L.push('checks the wrong property is worse than none), so the number grows');
|
|
119
|
+
L.push('deliberately, not overnight.');
|
|
120
|
+
L.push('');
|
|
121
|
+
L.push('**Non-coding domain soundness — scoped, not proven.** The `finance` /');
|
|
122
|
+
L.push('`founder` / `ops` / `content` profiles sell concrete domain value (DCF,');
|
|
123
|
+
L.push('runway, RICE, incident command, messaging), but the skills are forged on');
|
|
124
|
+
L.push('TS/PHP — "promising, not proven" off those stacks. A disclaimer floor');
|
|
125
|
+
L.push('bounds *liability*, not *correctness*: a skill can be format-correct,');
|
|
126
|
+
L.push('disclaimered, and still embed a wrong domain assumption. Today **0 of 20**');
|
|
127
|
+
L.push('default-surface domain skills carry a sourced `domain-truth` fixture');
|
|
128
|
+
L.push('(`./scripts-run src/scripts/domain_soundness_status`); all are labeled');
|
|
129
|
+
L.push('`unvalidated` and the validated count is CI-ratcheted. Authoring the');
|
|
130
|
+
L.push('answer keys needs domain competence (a cited method, not the skill\'s own');
|
|
131
|
+
L.push('output), so validation lands deliberately — the gap is published, never');
|
|
132
|
+
L.push('implied away.');
|
|
133
|
+
L.push('');
|
|
134
|
+
L.push('**Second-brain substrate — continuity convenience, task-lift unmeasured.**');
|
|
135
|
+
L.push('The memory substrate is built (working-memory continuity across');
|
|
136
|
+
L.push('compaction, promotable knowledge cards, contradiction surfacing), but');
|
|
137
|
+
L.push('whether it beats a no-memory baseline on a reproducible task is **not yet');
|
|
138
|
+
L.push('measured**. The measurement rig exists — a deterministic multi-session');
|
|
139
|
+
L.push('recall corpus + scorer (`./scripts-run src/scripts/second_brain_score');
|
|
140
|
+
L.push('--dry-run`, no model-in-the-loop grading) — but the paired');
|
|
141
|
+
L.push('`memory-on`/`off`/`placebo` run is spend-bearing and pending. So no');
|
|
142
|
+
L.push('"second brain" capability claim is made anywhere; the substrate ships as');
|
|
143
|
+
L.push('continuity convenience until a paired run backs a lift. Boundary vs a');
|
|
144
|
+
L.push('human PKM is stated in [`docs/second-brain-scope.md`](second-brain-scope.md).');
|
|
145
|
+
L.push('');
|
|
110
146
|
L.push('## 3. Known limits (published, witness-tested)');
|
|
111
147
|
L.push('');
|
|
112
148
|
const skillGaps = collectSkillGaps();
|
|
@@ -164,6 +200,8 @@ function render(): string {
|
|
|
164
200
|
L.push('task check-refs # no broken internal references');
|
|
165
201
|
L.push('task check-skill-gaps # every logged known-limit cites a real witness test');
|
|
166
202
|
L.push('task check-comparison # every comparison-table "our evidence" pointer resolves');
|
|
203
|
+
L.push('./scripts-run src/scripts/skill_eval_coverage # behavioural-eval coverage, per tier');
|
|
204
|
+
L.push('./scripts-run src/scripts/skill_eval_coverage --check # the ratchet: coverage may not drop');
|
|
167
205
|
L.push('task build-proof-check # this page is in sync with its sources');
|
|
168
206
|
L.push('```');
|
|
169
207
|
L.push('');
|
|
@@ -43,6 +43,8 @@ import * as fs from 'node:fs';
|
|
|
43
43
|
import * as path from 'node:path';
|
|
44
44
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
45
45
|
|
|
46
|
+
import { _entry_tokens_estimate } from './memory_lookup.js';
|
|
47
|
+
|
|
46
48
|
import { load_agent_settings } from './_lib/agent_settings.js';
|
|
47
49
|
|
|
48
50
|
export const DEFAULT_FILE = 'agents/runtime/.agent-chat-history';
|
|
@@ -708,6 +710,26 @@ export function read_entries(
|
|
|
708
710
|
agent?: string | null | undefined;
|
|
709
711
|
} = {},
|
|
710
712
|
): Entry[] {
|
|
713
|
+
return read_entries_with_refs(options).map((p) => p.entry);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Like {@link read_entries} but each entry carries its `ref` — the 0-based
|
|
718
|
+
* ordinal in the file's FULL chronological entry list (header excluded,
|
|
719
|
+
* counted BEFORE any session/agent/last filter), so a ref from an index row
|
|
720
|
+
* stays valid regardless of the filters a later call applies. Refs are
|
|
721
|
+
* stable for the append-only log; file rotation invalidates them (documented
|
|
722
|
+
* — a ref is a within-file locator, not a durable id).
|
|
723
|
+
* (road-to-memory-retrieval-economy Phase 3.)
|
|
724
|
+
*/
|
|
725
|
+
export function read_entries_with_refs(
|
|
726
|
+
options: {
|
|
727
|
+
last?: number | null | undefined;
|
|
728
|
+
path?: string | null | undefined;
|
|
729
|
+
session?: string | null | undefined;
|
|
730
|
+
agent?: string | null | undefined;
|
|
731
|
+
} = {},
|
|
732
|
+
): Array<{ entry: Entry; ref: number }> {
|
|
711
733
|
const target = options.path ?? file_path();
|
|
712
734
|
const last = options.last ?? null;
|
|
713
735
|
const session = options.session ?? null;
|
|
@@ -715,8 +737,9 @@ export function read_entries(
|
|
|
715
737
|
if (!_isFile(target)) {
|
|
716
738
|
return [];
|
|
717
739
|
}
|
|
718
|
-
let
|
|
740
|
+
let pairs: Array<{ entry: Entry; ref: number }> = [];
|
|
719
741
|
const lines = _readLines(target);
|
|
742
|
+
let ref = 0;
|
|
720
743
|
for (let i = 0; i < lines.length; i++) {
|
|
721
744
|
const line = _strip(lines[i] ?? '');
|
|
722
745
|
if (!line) {
|
|
@@ -732,23 +755,72 @@ export function read_entries(
|
|
|
732
755
|
continue;
|
|
733
756
|
}
|
|
734
757
|
if (_isPlainObject(obj)) {
|
|
735
|
-
|
|
758
|
+
pairs.push({ entry: obj, ref });
|
|
759
|
+
ref += 1;
|
|
736
760
|
}
|
|
737
761
|
}
|
|
738
762
|
if (session !== null) {
|
|
739
|
-
|
|
763
|
+
pairs = pairs.filter((p) => p.entry.s === session);
|
|
740
764
|
}
|
|
741
765
|
if (agent !== null) {
|
|
742
766
|
if (agent === AGENT_UNKNOWN) {
|
|
743
|
-
|
|
767
|
+
pairs = pairs.filter((p) => !p.entry.agent);
|
|
744
768
|
} else {
|
|
745
|
-
|
|
769
|
+
pairs = pairs.filter((p) => p.entry.agent === agent);
|
|
746
770
|
}
|
|
747
771
|
}
|
|
748
772
|
if (last !== null && last >= 0) {
|
|
749
|
-
|
|
773
|
+
pairs = pairs.slice(pairs.length - last);
|
|
774
|
+
}
|
|
775
|
+
return pairs;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Timeline anchor: the entries around `ref` in the FULL chronological list —
|
|
780
|
+
* `before`/`after` neighbours plus the anchor itself. JSONL is chronological,
|
|
781
|
+
* so this is slicing, not indexing (road-to-memory-retrieval-economy P3).
|
|
782
|
+
* An out-of-range ref returns [].
|
|
783
|
+
*/
|
|
784
|
+
export function slice_around(
|
|
785
|
+
path_: string | null,
|
|
786
|
+
ref: number,
|
|
787
|
+
before = 3,
|
|
788
|
+
after = 3,
|
|
789
|
+
): Array<{ entry: Entry; ref: number }> {
|
|
790
|
+
const all = read_entries_with_refs({ path: path_ });
|
|
791
|
+
if (ref < 0 || ref >= all.length) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const start = Math.max(0, ref - Math.max(0, before));
|
|
795
|
+
const end = Math.min(all.length, ref + Math.max(0, after) + 1);
|
|
796
|
+
return all.slice(start, end);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Compact index row for a history entry: ref + type tag + timestamp-ish
|
|
801
|
+
* fields when present + a ~100-char preview + the real-tokenizer cost of
|
|
802
|
+
* fetching the full entry (same lazy-tokenizer contract as memory_lookup's
|
|
803
|
+
* index rows).
|
|
804
|
+
*/
|
|
805
|
+
export function history_index_row(pair: { entry: Entry; ref: number }): Record<string, unknown> {
|
|
806
|
+
const e = pair.entry;
|
|
807
|
+
const text = typeof e.text === 'string' ? e.text : '';
|
|
808
|
+
const row: Record<string, unknown> = {
|
|
809
|
+
ref: pair.ref,
|
|
810
|
+
t: e.t ?? '',
|
|
811
|
+
preview: text.slice(0, 100),
|
|
812
|
+
tokens_estimate: _entry_tokens_estimate(e as Record<string, unknown>),
|
|
813
|
+
};
|
|
814
|
+
if (typeof e.s === 'string') {
|
|
815
|
+
row.s = e.s;
|
|
816
|
+
}
|
|
817
|
+
for (const tsField of ['ts', 'time', 'started']) {
|
|
818
|
+
if (typeof (e as Record<string, unknown>)[tsField] === 'string') {
|
|
819
|
+
row[tsField] = (e as Record<string, unknown>)[tsField];
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
750
822
|
}
|
|
751
|
-
return
|
|
823
|
+
return row;
|
|
752
824
|
}
|
|
753
825
|
|
|
754
826
|
export function read_entries_for_current(
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Trigger-eval PRESENCE ratchet (road-to-composition-ratchet Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* `check_trigger_evals.ts` validates freshness + structure of triggers.json
|
|
6
|
+
* files that EXIST; it cannot flag a new skill that ships without one, even
|
|
7
|
+
* though the drafting protocol mandates the stub. This gate closes that gap
|
|
8
|
+
* with a shrink-only grandfather allowlist
|
|
9
|
+
* (`src/scripts/trigger_eval_grandfather.json`, frozen 2026-07-08 at 221
|
|
10
|
+
* entries):
|
|
11
|
+
*
|
|
12
|
+
* 1. Every skill under `src/skills/` must either carry
|
|
13
|
+
* `evals/triggers.json` or appear in the allowlist.
|
|
14
|
+
* 2. An allowlist entry whose skill directory no longer exists is stale
|
|
15
|
+
* and must be removed.
|
|
16
|
+
* 3. An allowlist entry whose skill HAS a triggers.json must be removed —
|
|
17
|
+
* the list may only shrink (ratchet), never regrow.
|
|
18
|
+
*
|
|
19
|
+
* Structure + freshness of existing files stay owned by
|
|
20
|
+
* `check_trigger_evals.ts`; this gate checks presence only.
|
|
21
|
+
*
|
|
22
|
+
* Exit codes: 0 ok · 1 violation(s) · 2 usage / unreadable allowlist.
|
|
23
|
+
* `--quiet` suppresses the ✅ summary (errors always print to stderr).
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
30
|
+
const SKILLS_DIR = path.join(REPO_ROOT, 'src', 'skills');
|
|
31
|
+
const ALLOWLIST_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'trigger_eval_grandfather.json');
|
|
32
|
+
|
|
33
|
+
interface Allowlist {
|
|
34
|
+
skills: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function load_allowlist(p: string): Allowlist | null {
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as unknown;
|
|
40
|
+
if (
|
|
41
|
+
typeof raw !== 'object' ||
|
|
42
|
+
raw === null ||
|
|
43
|
+
!Array.isArray((raw as { skills?: unknown }).skills) ||
|
|
44
|
+
!(raw as { skills: unknown[] }).skills.every((s) => typeof s === 'string')
|
|
45
|
+
) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { skills: (raw as { skills: string[] }).skills };
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function list_skill_dirs(root: string): string[] {
|
|
55
|
+
return fs
|
|
56
|
+
.readdirSync(root, { withFileTypes: true })
|
|
57
|
+
.filter((e) => e.isDirectory())
|
|
58
|
+
.map((e) => e.name)
|
|
59
|
+
.sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function has_triggers(skill: string): boolean {
|
|
63
|
+
return fs.existsSync(path.join(SKILLS_DIR, skill, 'evals', 'triggers.json'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Findings {
|
|
67
|
+
errors: string[];
|
|
68
|
+
covered: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function check(skills: string[], allowlisted: string[]): Findings {
|
|
73
|
+
const allow = new Set(allowlisted);
|
|
74
|
+
const skillSet = new Set(skills);
|
|
75
|
+
const errors: string[] = [];
|
|
76
|
+
let covered = 0;
|
|
77
|
+
|
|
78
|
+
for (const skill of skills) {
|
|
79
|
+
if (has_triggers(skill)) {
|
|
80
|
+
covered += 1;
|
|
81
|
+
if (allow.has(skill)) {
|
|
82
|
+
errors.push(
|
|
83
|
+
`${skill}: has evals/triggers.json but is still in the grandfather allowlist — remove the entry (shrink-only ratchet)`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} else if (!allow.has(skill)) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`${skill}: missing evals/triggers.json and not grandfathered — new/changed skills must ship a trigger-eval set (see artifact-drafting-protocol Phase C)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const entry of allowlisted) {
|
|
94
|
+
if (!skillSet.has(entry)) {
|
|
95
|
+
errors.push(
|
|
96
|
+
`allowlist entry "${entry}" has no matching skill directory — remove the stale entry`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { errors, covered, total: skills.length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parse_args(argv: string[]): { quiet: boolean } | null {
|
|
105
|
+
let quiet = false;
|
|
106
|
+
for (const a of argv) {
|
|
107
|
+
if (a === '--quiet' || a === '-q') {
|
|
108
|
+
quiet = true;
|
|
109
|
+
} else {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { quiet };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main(): number {
|
|
117
|
+
const args = parse_args(process.argv.slice(2));
|
|
118
|
+
if (args === null) {
|
|
119
|
+
process.stderr.write('usage: check_trigger_eval_presence [--quiet]\n');
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const allowlist = load_allowlist(ALLOWLIST_PATH);
|
|
124
|
+
if (allowlist === null) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`❌ check-trigger-eval-presence: unreadable or malformed allowlist at ${path.relative(REPO_ROOT, ALLOWLIST_PATH)}\n`,
|
|
127
|
+
);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { errors, covered, total } = check(list_skill_dirs(SKILLS_DIR), allowlist.skills);
|
|
132
|
+
|
|
133
|
+
if (errors.length) {
|
|
134
|
+
process.stderr.write('❌ check-trigger-eval-presence: presence-ratchet violation(s):\n');
|
|
135
|
+
for (const e of errors) {
|
|
136
|
+
process.stderr.write(` - ${e}\n`);
|
|
137
|
+
}
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!args.quiet) {
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
`✅ check-trigger-eval-presence: ${covered}/${total} skills carry evals/triggers.json (${allowlist.skills.length} grandfathered, shrink-only)\n`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
150
|
+
function _isCliEntry(): boolean {
|
|
151
|
+
if (process.argv[1] === undefined) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
155
|
+
if (import.meta.url === argvUrl) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
160
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
161
|
+
return here === argv;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
168
|
+
process.exit(main());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { ALLOWLIST_PATH, check, load_allowlist, parse_args, main };
|