@event4u/agent-config 14.18.0 → 14.19.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +169 -130
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +420 -16
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +28 -27
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2132 -1462
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1619 -1045
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* measure_sibling_naming — how many skill descriptions name another skill.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-one-motion-authority` step 5.2. An inbox census claimed "44/299
|
|
5
|
+
* descriptions name a sibling" and shipped no definition of *naming* and no
|
|
6
|
+
* script, so the figure could be neither reproduced nor refuted. A companion
|
|
7
|
+
* claim of "218/299 bodies" reproduced under no reading of the tree at all.
|
|
8
|
+
* This script exists so the next such number is falsifiable.
|
|
9
|
+
*
|
|
10
|
+
* WHAT "NAMES A SIBLING" MEANS HERE, stated because the census did not.
|
|
11
|
+
*
|
|
12
|
+
* A description names a sibling when it contains the exact `name:` value of a
|
|
13
|
+
* DIFFERENT shipped skill, delimited so it is not a fragment of a longer
|
|
14
|
+
* identifier. `\b` is not enough: it puts a boundary between `brand` and the
|
|
15
|
+
* hyphen in `brand-identity`, so a bare `\b`-matched `brand` would score every
|
|
16
|
+
* mention of `brand-identity` as naming `brand` too. The delimiter here is
|
|
17
|
+
* "not `[A-Za-z0-9-]`" on both sides, which is the smallest rule that keeps
|
|
18
|
+
* `mcp` out of `mcp-builder` and `laravel` out of `laravel-dto`.
|
|
19
|
+
*
|
|
20
|
+
* A skill naming ITSELF does not count — that is a description, not a pointer.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THE NUMBER IS AND IS NOT.
|
|
23
|
+
*
|
|
24
|
+
* A KNOWN AND UNFIXABLE FALSE POSITIVE, named rather than left for a reader to
|
|
25
|
+
* find: some skill names are ordinary English words, so a description can score
|
|
26
|
+
* a match by accident. `canvas-design` scores `brand` on the phrase "brand
|
|
27
|
+
* assets", which is not a pointer to the `brand` skill. Excluding short or
|
|
28
|
+
* common names would trade this for a false negative on the skills whose names
|
|
29
|
+
* really are those words, and there is no rule that gets both. The count is
|
|
30
|
+
* therefore an upper bound; the per-skill list is printed so a reader can see
|
|
31
|
+
* which matches are real.
|
|
32
|
+
*
|
|
33
|
+
* It is a count of descriptions that mention a sibling by name. It is NOT a
|
|
34
|
+
* measure of routing quality: a description can name a sibling uselessly, and a
|
|
35
|
+
* description that names none can be perfectly routable on its own keywords.
|
|
36
|
+
* The reason the count is worth having is narrower — the router reads
|
|
37
|
+
* descriptions and nothing else, so a family whose members never point at each
|
|
38
|
+
* other is a family the router cannot traverse.
|
|
39
|
+
*
|
|
40
|
+
* Exit code is 0 unless `--min <n>` is given and the count falls below it.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
49
|
+
const SKILLS_REL = path.join('src', 'skills');
|
|
50
|
+
|
|
51
|
+
export interface SkillRecord { name: string; description: string }
|
|
52
|
+
|
|
53
|
+
/** Read `name:` and `description:` out of a SKILL.md frontmatter block. */
|
|
54
|
+
export function parseSkill(text: string): SkillRecord | null {
|
|
55
|
+
if (!text.startsWith('---')) return null;
|
|
56
|
+
const end = text.indexOf('\n---', 3);
|
|
57
|
+
if (end < 0) return null;
|
|
58
|
+
const fm = text.slice(3, end);
|
|
59
|
+
const name = /^name:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
|
|
60
|
+
let description = /^description:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
|
|
61
|
+
if ((description.startsWith('"') && description.endsWith('"'))
|
|
62
|
+
|| (description.startsWith("'") && description.endsWith("'"))) {
|
|
63
|
+
description = description.slice(1, -1);
|
|
64
|
+
}
|
|
65
|
+
description = description.replace(/\\"/g, '"');
|
|
66
|
+
if (name === '') return null;
|
|
67
|
+
return { name, description };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function loadSkills(root: string): SkillRecord[] {
|
|
71
|
+
const dir = path.join(root, SKILLS_REL);
|
|
72
|
+
if (!fs.existsSync(dir)) return [];
|
|
73
|
+
const out: SkillRecord[] = [];
|
|
74
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
const file = path.join(dir, entry.name, 'SKILL.md');
|
|
77
|
+
if (!fs.existsSync(file)) continue;
|
|
78
|
+
const rec = parseSkill(fs.readFileSync(file, 'utf-8'));
|
|
79
|
+
if (rec !== null) out.push(rec);
|
|
80
|
+
}
|
|
81
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Identifier-boundary containment: `mcp` is not found inside `mcp-builder`. */
|
|
85
|
+
export function namesIdentifier(haystack: string, id: string): boolean {
|
|
86
|
+
let from = 0;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const i = haystack.indexOf(id, from);
|
|
89
|
+
if (i < 0) return false;
|
|
90
|
+
const before = i === 0 ? '' : haystack[i - 1] ?? '';
|
|
91
|
+
const after = haystack[i + id.length] ?? '';
|
|
92
|
+
const isPart = (c: string): boolean => c !== '' && /[A-Za-z0-9-]/.test(c);
|
|
93
|
+
if (!isPart(before) && !isPart(after)) return true;
|
|
94
|
+
from = i + 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** For each skill, the OTHER skills its description names. Sorted, deterministic. */
|
|
99
|
+
export function siblingsNamed(skills: readonly SkillRecord[]): Map<string, string[]> {
|
|
100
|
+
const names = skills.map((s) => s.name);
|
|
101
|
+
const out = new Map<string, string[]>();
|
|
102
|
+
for (const s of skills) {
|
|
103
|
+
const hits = names
|
|
104
|
+
.filter((n) => n !== s.name && namesIdentifier(s.description, n))
|
|
105
|
+
.sort();
|
|
106
|
+
out.set(s.name, hits);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
112
|
+
const rootFlag = argv.indexOf('--root');
|
|
113
|
+
const root = rootFlag >= 0 ? (argv[rootFlag + 1] ?? REPO_ROOT) : REPO_ROOT;
|
|
114
|
+
const minFlag = argv.indexOf('--min');
|
|
115
|
+
const min = minFlag >= 0 ? Number(argv[minFlag + 1]) : null;
|
|
116
|
+
const json = argv.includes('--json');
|
|
117
|
+
|
|
118
|
+
const skills = loadSkills(root);
|
|
119
|
+
const map = siblingsNamed(skills);
|
|
120
|
+
const naming = [...map].filter(([, v]) => v.length > 0);
|
|
121
|
+
|
|
122
|
+
if (json) {
|
|
123
|
+
process.stdout.write(`${JSON.stringify({
|
|
124
|
+
total: skills.length,
|
|
125
|
+
naming: naming.length,
|
|
126
|
+
entries: Object.fromEntries(naming),
|
|
127
|
+
}, null, 1)}\n`);
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`scanned: ${String(skills.length)}\n`);
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
`sibling-naming: ${String(naming.length)}/${String(skills.length)} description(s) name at least one other shipped skill\n\n`,
|
|
132
|
+
);
|
|
133
|
+
for (const [name, hits] of naming) {
|
|
134
|
+
process.stdout.write(` ${name} → ${hits.join(', ')}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (min !== null && Number.isFinite(min) && naming.length < min) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`\n❌ measure_sibling_naming: ${String(naming.length)} below the declared floor of ${String(min)}\n`,
|
|
141
|
+
);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (process.argv[1] !== undefined) {
|
|
148
|
+
const invoked = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
149
|
+
if (invoked === import.meta.url) process.exitCode = main();
|
|
150
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `redact_hook_capture.py` (Phase 8 / Wave 8g).
|
|
6
6
|
*
|
|
7
|
-
* Reads JSON capture files written by `dispatch_hook.
|
|
7
|
+
* Reads JSON capture files written by `dispatch_hook.ts` (when
|
|
8
8
|
* `AGENT_HOOK_CAPTURE_DIR` is set) and produces a redacted version
|
|
9
9
|
* suitable for pasting into the verified-platforms roadmap.
|
|
10
10
|
*
|
|
@@ -44,7 +44,19 @@
|
|
|
44
44
|
* count descriptions against bare names. Until that runs, "most skills reach the
|
|
45
45
|
* model without a description" is a suspicion.
|
|
46
46
|
*
|
|
47
|
-
* Exit: 0 always, except a usage/IO error (1). Deliberate
|
|
47
|
+
* Exit: the REPORT is 0 always, except a usage/IO error (1). Deliberate — it
|
|
48
|
+
* gates on nothing and must not acquire a threshold.
|
|
49
|
+
*
|
|
50
|
+
* One later mode DOES carry a verdict, and it is not a threshold on the census:
|
|
51
|
+
* `--emit` exits 1 rather than writing a record from an empty store, because a
|
|
52
|
+
* zero-session reading is "no transcripts at this path" and publishing it as an
|
|
53
|
+
* activation figure is the defect this file exists to end.
|
|
54
|
+
*
|
|
55
|
+
* The assertion that the PUBLISHED figure still matches that record is a gate
|
|
56
|
+
* and lives in `check_skill_activation_claim.ts`. It is a separate script on
|
|
57
|
+
* purpose: `src/config/gate-coverage.yml` is a manifest of gates and
|
|
58
|
+
* `_lib/gate_population.ts` classifies one by prefix, so a `report_*` id
|
|
59
|
+
* registered there forks that population.
|
|
48
60
|
*/
|
|
49
61
|
|
|
50
62
|
import * as fs from 'node:fs';
|
|
@@ -53,6 +65,7 @@ import * as path from 'node:path';
|
|
|
53
65
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
66
|
|
|
55
67
|
import { projectStoreSlug } from './_lib/cc_transcript.js';
|
|
68
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
56
69
|
|
|
57
70
|
const _HERE = fileURLToPath(import.meta.url);
|
|
58
71
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -73,11 +86,17 @@ export const DETERMINISTIC_RE = /^\s*(?:[-*]\s*)?(?:\*\*)?(MUST|NEVER|ALWAYS)\b/
|
|
|
73
86
|
export interface SkillCensus {
|
|
74
87
|
total: number;
|
|
75
88
|
withTriggerKey: string[];
|
|
89
|
+
withTriggerCorpus: string[];
|
|
76
90
|
withDeterministicObligation: string[];
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
export function censusSkills(root: string): SkillCensus {
|
|
80
|
-
const out: SkillCensus = {
|
|
94
|
+
const out: SkillCensus = {
|
|
95
|
+
total: 0,
|
|
96
|
+
withTriggerKey: [],
|
|
97
|
+
withTriggerCorpus: [],
|
|
98
|
+
withDeterministicObligation: [],
|
|
99
|
+
};
|
|
81
100
|
if (!fs.existsSync(root)) {
|
|
82
101
|
return out;
|
|
83
102
|
}
|
|
@@ -98,6 +117,12 @@ export function censusSkills(root: string): SkillCensus {
|
|
|
98
117
|
if (TRIGGER_KEYS.some((k) => new RegExp(`^${k}`, 'm').test(fm))) {
|
|
99
118
|
out.withTriggerKey.push(entry.name);
|
|
100
119
|
}
|
|
120
|
+
// A test fixture, not a routing input: no host reads it at selection time.
|
|
121
|
+
// Counted because it is the second of the three populations the surface
|
|
122
|
+
// splits into, and it is the one most often mistaken for the first.
|
|
123
|
+
if (fs.existsSync(path.join(root, entry.name, 'evals', 'triggers.json'))) {
|
|
124
|
+
out.withTriggerCorpus.push(entry.name);
|
|
125
|
+
}
|
|
101
126
|
const body = fmEnd === -1 ? text : text.slice(fmEnd + 4);
|
|
102
127
|
if (DETERMINISTIC_RE.test(body)) {
|
|
103
128
|
out.withDeterministicObligation.push(entry.name);
|
|
@@ -178,7 +203,185 @@ function _pct(n: number, d: number): string {
|
|
|
178
203
|
return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
|
|
179
204
|
}
|
|
180
205
|
|
|
181
|
-
|
|
206
|
+
/** Repo-relative path of the record the published claim is derived from. */
|
|
207
|
+
export const RECORD_PATH = 'agents/evidence/metrics/skill-activation-census.json';
|
|
208
|
+
export const CLAIMS_PATH = 'docs/CLAIMS.md';
|
|
209
|
+
/** The one ledger entry this census backs. */
|
|
210
|
+
export const CLAIM_ID = 'skill-activation-census-zero';
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The three populations the shipped surface splits into, plus their overlap.
|
|
214
|
+
*
|
|
215
|
+
* They are computed from one pass rather than three greps because the whole
|
|
216
|
+
* point of 2.1 is that they reconcile: `humanNamedOnly` is a remainder, so it
|
|
217
|
+
* cannot be stated independently and then found to disagree.
|
|
218
|
+
*/
|
|
219
|
+
export interface Populations {
|
|
220
|
+
shipped: number;
|
|
221
|
+
triggerKey: number;
|
|
222
|
+
triggerCorpus: number;
|
|
223
|
+
bothKeyAndCorpus: number;
|
|
224
|
+
humanNamedOnly: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function populations(census: SkillCensus): Populations {
|
|
228
|
+
const corpus = new Set(census.withTriggerCorpus);
|
|
229
|
+
const both = census.withTriggerKey.filter((s) => corpus.has(s));
|
|
230
|
+
const eitherOne = new Set([...census.withTriggerKey, ...census.withTriggerCorpus]);
|
|
231
|
+
return {
|
|
232
|
+
shipped: census.total,
|
|
233
|
+
triggerKey: census.withTriggerKey.length,
|
|
234
|
+
triggerCorpus: census.withTriggerCorpus.length,
|
|
235
|
+
bothKeyAndCorpus: both.length,
|
|
236
|
+
humanNamedOnly: census.total - eitherOne.size,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface CensusRecord {
|
|
241
|
+
schema_version: 1;
|
|
242
|
+
measured_at: string;
|
|
243
|
+
skills_shipped: number;
|
|
244
|
+
with_trigger_key: number;
|
|
245
|
+
with_trigger_corpus: number;
|
|
246
|
+
trigger_key_and_corpus: number;
|
|
247
|
+
human_named_only: number;
|
|
248
|
+
sessions: number;
|
|
249
|
+
assistant_turns: number;
|
|
250
|
+
invocations: number;
|
|
251
|
+
distinct_skills_invoked: number;
|
|
252
|
+
stores: string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildRecord(census: SkillCensus, usage: UsageReport[], today: string): CensusRecord {
|
|
256
|
+
const pop = populations(census);
|
|
257
|
+
const distinct = new Set<string>();
|
|
258
|
+
for (const u of usage) {
|
|
259
|
+
for (const k of Object.keys(u.bySkill)) {
|
|
260
|
+
distinct.add(k);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
schema_version: 1,
|
|
265
|
+
measured_at: today,
|
|
266
|
+
skills_shipped: pop.shipped,
|
|
267
|
+
with_trigger_key: pop.triggerKey,
|
|
268
|
+
with_trigger_corpus: pop.triggerCorpus,
|
|
269
|
+
trigger_key_and_corpus: pop.bothKeyAndCorpus,
|
|
270
|
+
human_named_only: pop.humanNamedOnly,
|
|
271
|
+
sessions: usage.reduce((a, u) => a + u.sessions, 0),
|
|
272
|
+
assistant_turns: usage.reduce((a, u) => a + u.assistantTurns, 0),
|
|
273
|
+
invocations: usage.reduce((a, u) => a + u.invocations, 0),
|
|
274
|
+
distinct_skills_invoked: distinct.size,
|
|
275
|
+
stores: usage.map((u) => path.basename(u.store)),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function readRecord(repoRoot: string): CensusRecord | null {
|
|
280
|
+
const file = path.join(repoRoot, RECORD_PATH);
|
|
281
|
+
if (!fs.existsSync(file)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')) as CensusRecord;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Numbers the published claim asserts, pulled back out of the claim text.
|
|
289
|
+
*
|
|
290
|
+
* Read from prose on purpose: the claim is the surface a reader believes, so
|
|
291
|
+
* comparing the record against a separate machine field would leave the
|
|
292
|
+
* sentence free to drift from both. `null` for any field the claim no longer
|
|
293
|
+
* states in the expected shape — which `checkClaim` reports as a defect rather
|
|
294
|
+
* than passing over, because a claim that stopped stating its figures is not a
|
|
295
|
+
* claim that agrees with the record.
|
|
296
|
+
*/
|
|
297
|
+
export interface ClaimFigures {
|
|
298
|
+
sessions: number | null;
|
|
299
|
+
assistantTurns: number | null;
|
|
300
|
+
invocations: number | null;
|
|
301
|
+
distinct: number | null;
|
|
302
|
+
shipped: number | null;
|
|
303
|
+
triggerKey: number | null;
|
|
304
|
+
triggerCorpus: number | null;
|
|
305
|
+
bothKeyAndCorpus: number | null;
|
|
306
|
+
humanNamedOnly: number | null;
|
|
307
|
+
measuredAt: string | null;
|
|
308
|
+
lastVerified: string | null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _num(text: string, re: RegExp): number | null {
|
|
312
|
+
const m = re.exec(text);
|
|
313
|
+
return m?.[1] === undefined ? null : Number.parseInt(m[1].replace(/,/gu, ''), 10);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function claimBlock(claimsText: string, id: string): string | null {
|
|
317
|
+
const start = claimsText.indexOf(`### claim: ${id}\n`);
|
|
318
|
+
if (start === -1) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const rest = claimsText.slice(start + 1);
|
|
322
|
+
const end = rest.indexOf('\n### claim: ');
|
|
323
|
+
return end === -1 ? claimsText.slice(start) : claimsText.slice(start, start + 1 + end);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function parseClaimFigures(block: string): ClaimFigures {
|
|
327
|
+
return {
|
|
328
|
+
sessions: _num(block, /over ([\d,]+) sessions and/u),
|
|
329
|
+
assistantTurns: _num(block, /sessions and ([\d,]+) assistant turns/u),
|
|
330
|
+
invocations: _num(block, /records ([\d,]+) Skill invocations/u),
|
|
331
|
+
distinct: _num(block, /Skill invocations and ([\d,]+) of [\d,]+ distinct skills/u),
|
|
332
|
+
shipped: _num(block, /Skill invocations and [\d,]+ of ([\d,]+) distinct skills/u),
|
|
333
|
+
triggerKey: _num(block, /([\d,]+) declare a machine-matchable trigger key/u),
|
|
334
|
+
triggerCorpus: _num(block, /([\d,]+) carry an `evals\/triggers\.json` corpus/u),
|
|
335
|
+
bothKeyAndCorpus: _num(block, /([\d,]+) do both/u),
|
|
336
|
+
humanNamedOnly: _num(block, /([\d,]+) are reachable only by a human naming them/u),
|
|
337
|
+
measuredAt: /measured (\d{4}-\d{2}-\d{2})/u.exec(block)?.[1] ?? null,
|
|
338
|
+
lastVerified: /^- last_verified: (\d{4}-\d{2}-\d{2})/mu.exec(block)?.[1] ?? null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Disagreements between the committed record and the published claim.
|
|
344
|
+
*
|
|
345
|
+
* This is the whole of what 1.3 buys, so it is worth being exact about what it
|
|
346
|
+
* does NOT buy: it cannot tell whether the RECORD still describes the world,
|
|
347
|
+
* because the transcript store it was taken from is one machine's and is absent
|
|
348
|
+
* from CI. Re-taking the measurement is `--emit`; this only forbids the
|
|
349
|
+
* published sentence and the record from telling two different stories.
|
|
350
|
+
*/
|
|
351
|
+
export function claimProblems(rec: CensusRecord, fig: ClaimFigures): string[] {
|
|
352
|
+
const out: string[] = [];
|
|
353
|
+
const pairs: Array<[string, number, number | null]> = [
|
|
354
|
+
['sessions', rec.sessions, fig.sessions],
|
|
355
|
+
['assistant turns', rec.assistant_turns, fig.assistantTurns],
|
|
356
|
+
['Skill invocations', rec.invocations, fig.invocations],
|
|
357
|
+
['distinct skills invoked', rec.distinct_skills_invoked, fig.distinct],
|
|
358
|
+
['skills shipped', rec.skills_shipped, fig.shipped],
|
|
359
|
+
['skills with a trigger key', rec.with_trigger_key, fig.triggerKey],
|
|
360
|
+
['skills with a triggers.json corpus', rec.with_trigger_corpus, fig.triggerCorpus],
|
|
361
|
+
['skills in both populations', rec.trigger_key_and_corpus, fig.bothKeyAndCorpus],
|
|
362
|
+
['human-named-only skills', rec.human_named_only, fig.humanNamedOnly],
|
|
363
|
+
];
|
|
364
|
+
for (const [label, recorded, claimed] of pairs) {
|
|
365
|
+
if (claimed === null) {
|
|
366
|
+
out.push(`${label}: the claim no longer states this figure; the record says ${String(recorded)}`);
|
|
367
|
+
} else if (claimed !== recorded) {
|
|
368
|
+
out.push(`${label}: claim says ${String(claimed)}, record says ${String(recorded)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const [label, claimed] of [
|
|
372
|
+
['measurement date', fig.measuredAt],
|
|
373
|
+
['last_verified', fig.lastVerified],
|
|
374
|
+
] as Array<[string, string | null]>) {
|
|
375
|
+
if (claimed === null) {
|
|
376
|
+
out.push(`${label}: the claim no longer states it; the record was measured ${rec.measured_at}`);
|
|
377
|
+
} else if (claimed !== rec.measured_at) {
|
|
378
|
+
out.push(`${label}: claim says ${claimed}, record was measured ${rec.measured_at}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function render(census: SkillCensus, usage: UsageReport[], published: CensusRecord | null = null): string {
|
|
182
385
|
const lines: string[] = [];
|
|
183
386
|
lines.push('skill activation census — advisory, gates on nothing');
|
|
184
387
|
lines.push('');
|
|
@@ -193,6 +396,16 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
193
396
|
lines.push(` ${census.withDeterministicObligation.join(', ')}`);
|
|
194
397
|
}
|
|
195
398
|
lines.push('');
|
|
399
|
+
const pop = populations(census);
|
|
400
|
+
lines.push(' populations — the three ways a skill can be reached, reconciled');
|
|
401
|
+
lines.push(` machine-matchable trigger key ${pop.triggerKey}`);
|
|
402
|
+
lines.push(` evals/triggers.json corpus ${pop.triggerCorpus} (a test fixture; no host reads it at routing time)`);
|
|
403
|
+
lines.push(` both of the above ${pop.bothKeyAndCorpus}`);
|
|
404
|
+
lines.push(` human-named only (remainder) ${pop.humanNamedOnly}`);
|
|
405
|
+
lines.push(
|
|
406
|
+
` reconciles ${pop.triggerKey} + ${pop.triggerCorpus} - ${pop.bothKeyAndCorpus} + ${pop.humanNamedOnly} = ${String(pop.triggerKey + pop.triggerCorpus - pop.bothKeyAndCorpus + pop.humanNamedOnly)} of ${pop.shipped}`,
|
|
407
|
+
);
|
|
408
|
+
lines.push('');
|
|
196
409
|
let totalInv = 0;
|
|
197
410
|
const distinct = new Set<string>();
|
|
198
411
|
for (const u of usage) {
|
|
@@ -213,6 +426,65 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
213
426
|
lines.push(` ${[...distinct].sort().join(', ')}`);
|
|
214
427
|
}
|
|
215
428
|
lines.push('');
|
|
429
|
+
const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
|
|
430
|
+
if (sessionsRead === 0) {
|
|
431
|
+
// The store is keyed on the directory the process runs in, so a checkout
|
|
432
|
+
// that never hosted a session — a fresh worktree, a CI runner — yields a
|
|
433
|
+
// zero that means "no transcripts at this path". Publishing that as the
|
|
434
|
+
// activation reading would be the defect this census exists to end, so the
|
|
435
|
+
// two zeros are separated here rather than left to the reader.
|
|
436
|
+
lines.push(' EMPTY STORE: 0 sessions were read, so the zero above is "no transcripts at');
|
|
437
|
+
lines.push(' this path", NOT "no activations". It is not a measurement of the surface and');
|
|
438
|
+
lines.push(' must not be published as one. Run this where the store lives, or pass --store.');
|
|
439
|
+
lines.push('');
|
|
440
|
+
}
|
|
441
|
+
if (published !== null) {
|
|
442
|
+
// Staleness has to be readable from the run itself. Without this block the
|
|
443
|
+
// only way to tell a fresh published figure from a rotted one is to open
|
|
444
|
+
// the git log of `docs/CLAIMS.md`, which is exactly the thing 1.3 forbids.
|
|
445
|
+
const live = buildRecord(census, usage, published.measured_at);
|
|
446
|
+
// The two halves drift for different reasons and a single verdict over both
|
|
447
|
+
// would be worthless: the store is rolling, so its counts move between any
|
|
448
|
+
// two runs — including a run in the session that is adding to it. A reader
|
|
449
|
+
// who is told STALE every time stops reading the line. Only the repository
|
|
450
|
+
// half is a defect when it moves, because the published population split
|
|
451
|
+
// then no longer describes the tree it claims to count.
|
|
452
|
+
const REPO_FIELDS: Array<keyof CensusRecord> = [
|
|
453
|
+
'skills_shipped',
|
|
454
|
+
'with_trigger_key',
|
|
455
|
+
'with_trigger_corpus',
|
|
456
|
+
'trigger_key_and_corpus',
|
|
457
|
+
'human_named_only',
|
|
458
|
+
];
|
|
459
|
+
const STORE_FIELDS: Array<keyof CensusRecord> = [
|
|
460
|
+
'sessions',
|
|
461
|
+
'assistant_turns',
|
|
462
|
+
'invocations',
|
|
463
|
+
'distinct_skills_invoked',
|
|
464
|
+
];
|
|
465
|
+
const repoDrift = REPO_FIELDS.filter((k) => live[k] !== published[k]);
|
|
466
|
+
const storeDrift = STORE_FIELDS.filter((k) => live[k] !== published[k]);
|
|
467
|
+
lines.push(` PUBLISHED RECORD (${RECORD_PATH}), measured ${published.measured_at}:`);
|
|
468
|
+
lines.push(
|
|
469
|
+
` sessions=${String(published.sessions)} asst=${String(published.assistant_turns)} invocations=${String(published.invocations)} distinct=${String(published.distinct_skills_invoked)}`,
|
|
470
|
+
);
|
|
471
|
+
if (repoDrift.length > 0) {
|
|
472
|
+
lines.push(` STALE: the published population split no longer describes this tree —`);
|
|
473
|
+
lines.push(` ${repoDrift.join(', ')} moved. Re-take with --emit.`);
|
|
474
|
+
} else {
|
|
475
|
+
lines.push(' population split still describes this tree.');
|
|
476
|
+
}
|
|
477
|
+
if (sessionsRead === 0) {
|
|
478
|
+
lines.push(' Store reading: UNDECIDED — this run read no store, so it cannot say whether');
|
|
479
|
+
lines.push(' the published figures still hold. Re-run where the store lives.');
|
|
480
|
+
} else if (storeDrift.length === 0) {
|
|
481
|
+
lines.push(' Store reading: reproduces the published figures exactly.');
|
|
482
|
+
} else {
|
|
483
|
+
lines.push(` Store reading: moved since the record (${storeDrift.join(', ')}). Expected —`);
|
|
484
|
+
lines.push(' the store is rolling, not append-only, and a later reading can be lower.');
|
|
485
|
+
}
|
|
486
|
+
lines.push('');
|
|
487
|
+
}
|
|
216
488
|
if (census.withTriggerKey.length === 0) {
|
|
217
489
|
lines.push(' VERDICT: activation is not measurable as a rate. No skill declares a');
|
|
218
490
|
lines.push(' machine-matchable trigger, so a missed-activation detector has nothing to');
|
|
@@ -227,19 +499,32 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
227
499
|
return lines.join('\n');
|
|
228
500
|
}
|
|
229
501
|
|
|
502
|
+
const USAGE =
|
|
503
|
+
'usage: report_skill_activation [--root DIR] [--limit N] [--store PATH]... [--emit] [--allow-empty-store]\n';
|
|
504
|
+
|
|
230
505
|
function main(argv: string[]): number {
|
|
231
506
|
let limit = 30;
|
|
507
|
+
let emit = false;
|
|
508
|
+
let allowEmpty = false;
|
|
509
|
+
const rootIdx = argv.indexOf('--root');
|
|
510
|
+
const root = rootIdx === -1 ? REPO_ROOT : (argv[rootIdx + 1] ?? REPO_ROOT);
|
|
232
511
|
const stores: string[] = [];
|
|
233
512
|
for (let i = 0; i < argv.length; i += 1) {
|
|
234
513
|
const a = argv[i];
|
|
235
|
-
if (a === '--
|
|
514
|
+
if (a === '--root' && argv[i + 1] !== undefined) {
|
|
515
|
+
i += 1;
|
|
516
|
+
} else if (a === '--limit' && argv[i + 1] !== undefined) {
|
|
236
517
|
limit = Number.parseInt(argv[i + 1] as string, 10);
|
|
237
518
|
i += 1;
|
|
238
519
|
} else if (a === '--store' && argv[i + 1] !== undefined) {
|
|
239
520
|
stores.push(argv[i + 1] as string);
|
|
240
521
|
i += 1;
|
|
522
|
+
} else if (a === '--emit') {
|
|
523
|
+
emit = true;
|
|
524
|
+
} else if (a === '--allow-empty-store') {
|
|
525
|
+
allowEmpty = true;
|
|
241
526
|
} else if (a === '--help' || a === '-h') {
|
|
242
|
-
process.stdout.write(
|
|
527
|
+
process.stdout.write(USAGE);
|
|
243
528
|
return 0;
|
|
244
529
|
}
|
|
245
530
|
}
|
|
@@ -250,9 +535,36 @@ function main(argv: string[]): number {
|
|
|
250
535
|
if (stores.length === 0) {
|
|
251
536
|
stores.push(defaultStore(REPO_ROOT));
|
|
252
537
|
}
|
|
253
|
-
const census = censusSkills(path.join(
|
|
538
|
+
const census = censusSkills(path.join(root, SKILLS_ROOT));
|
|
254
539
|
const usage = stores.map((s) => measureUsage(s, limit));
|
|
255
|
-
process.stdout.write(`${render(census, usage)}\n`);
|
|
540
|
+
process.stdout.write(`${render(census, usage, readRecord(root))}\n`);
|
|
541
|
+
// The skills are the corpus this floor can hold: they are in the repository,
|
|
542
|
+
// so the count is the same everywhere. The transcript half is path-scoped and
|
|
543
|
+
// legitimately zero in CI, which is why it is reported above and not here — a
|
|
544
|
+
// floor over it would either red every CI run or be vacuous.
|
|
545
|
+
reportScanned({
|
|
546
|
+
gate: 'report_skill_activation',
|
|
547
|
+
scanned: census.total,
|
|
548
|
+
units: 'SKILL.md file(s)',
|
|
549
|
+
roots: [SKILLS_ROOT],
|
|
550
|
+
});
|
|
551
|
+
if (!emit) {
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
|
|
555
|
+
if (sessionsRead === 0 && !allowEmpty) {
|
|
556
|
+
process.stderr.write(
|
|
557
|
+
'report_skill_activation --emit: refusing to write a record from 0 sessions — that zero\n' +
|
|
558
|
+
'is "no transcripts at this path", not an activation reading. Run where the store lives,\n' +
|
|
559
|
+
'pass --store, or pass --allow-empty-store if an empty store is genuinely the finding.\n',
|
|
560
|
+
);
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
564
|
+
const file = path.join(root, RECORD_PATH);
|
|
565
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
566
|
+
fs.writeFileSync(file, `${JSON.stringify(buildRecord(census, usage, today), null, 2)}\n`);
|
|
567
|
+
process.stdout.write(`\nwrote ${RECORD_PATH} (measured ${today})\n`);
|
|
256
568
|
return 0;
|
|
257
569
|
}
|
|
258
570
|
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
"type": "string"
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
|
+
"no_findings_reason": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"description": "Why the finding set is empty. REQUIRED whenever a ledger file exists and `findings` is empty — enforced in check_finding_dispositions.empty_ledger_problem rather than here, because the requirement is conditional on the array length and this repo's Draft-07 subset validator resolves no `if`/`then`. The field exists because `findings: []` and a missing file made the same claim to every reader while collapsing two states that differ entirely: a review that ran and reported nothing, and a review that never ran. The reason must pick one and say how a reader can check it."
|
|
58
|
+
},
|
|
54
59
|
"findings": {
|
|
55
60
|
"type": "array",
|
|
56
61
|
"description": "May be empty. An empty array is an honest null, not a missing field — the distinction the Markdown track spells with its own §2.3 line. The item shape is INLINED rather than `$ref`-ed for the same measured reason: the subset validator does not resolve `$ref`, so a referenced definition would have validated nothing.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://github.com/event4u-app/agent-config/scripts/schemas/skill.schema.json",
|
|
4
|
-
"x-schemaVersion": "1.1.
|
|
4
|
+
"x-schemaVersion": "1.1.1",
|
|
5
5
|
"title": "Skill frontmatter",
|
|
6
6
|
"$comment": "Source: agents/reference/docs/frontmatter-contract.md#skills. Keep in sync with inventory_frontmatter.py and the linter's lint_skill().",
|
|
7
7
|
"type": "object",
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
},
|
|
277
277
|
"requires_confirmation": {
|
|
278
278
|
"type": "boolean",
|
|
279
|
-
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2
|
|
279
|
+
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 carry no pre-tool alias row), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
|
|
280
280
|
},
|
|
281
281
|
"command": {
|
|
282
282
|
"type": "array",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* rule alone is exactly the kind of surface that is dropped in fresh
|
|
17
17
|
* conversations (found RED baseline on the reply-close PR-URL contract,
|
|
18
18
|
* 2026-07-27); the session_start injection makes the contract present in every
|
|
19
|
-
* new conversation on hook-capable hosts. Copilot (
|
|
19
|
+
* new conversation on hook-capable hosts. Copilot (nothing bound here) falls back
|
|
20
20
|
* to the rule alone.
|
|
21
21
|
*
|
|
22
22
|
* Gate + name resolution (first non-empty wins — the canary is a PERSONAL,
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
*
|
|
38
38
|
* ## Honest coverage
|
|
39
39
|
*
|
|
40
|
-
* The heartbeat is a per-turn obligation and does not reach every host.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* The heartbeat is a per-turn obligation and does not reach every host. This
|
|
41
|
+
* package binds nothing on `copilot`; `cursor`'s per-turn reachability is not
|
|
42
|
+
* established here, so it is excluded rather than assumed; `cowork`'s lifecycle
|
|
43
|
+
* events are wired but inert. Those hosts
|
|
43
44
|
* are named in `HEARTBEAT_REACHABLE_PLATFORMS` by their absence, and a session
|
|
44
45
|
* there simply stops being visible after its TTL rather than being reported as
|
|
45
46
|
* covered.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted)
|
|
2
|
+
"_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted) — never added. Frozen 2026-07-08 at 221 of 264 skills.",
|
|
3
|
+
"_no_obligation_note": "Entries in `_no_obligation` are on the `skills` allowlist for a DIFFERENT reason from the rest. The list above records skills that predate the gate and owe a triggers.json; these owe none, because their description says explicit-request-only and they publish no auto-trigger keywords — there is no trigger to evaluate. Listing them without this distinction reads as a waived obligation when the obligation is absent. Read by humans only: check_trigger_eval_presence parses `skills` and ignores every other key, so this changes no verdict. Removing one from `skills` WOULD change a verdict — the gate would then demand a triggers.json for a skill that has nothing to trigger on.",
|
|
4
|
+
"_no_obligation": [
|
|
5
|
+
"sequential-thinking"
|
|
6
|
+
],
|
|
3
7
|
"skills": [
|
|
4
8
|
"accessibility-auditor",
|
|
5
9
|
"activation-design",
|
|
@@ -42,7 +42,7 @@ const SCHEMA_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'schemas', 'agent-set
|
|
|
42
42
|
const TEMPLATE_PATH = path.join(REPO_ROOT, 'src', 'config', 'agent-settings.template.yml');
|
|
43
43
|
const LOCAL_PATHS = [project_settings_path(REPO_ROOT)];
|
|
44
44
|
|
|
45
|
-
// Installer-default substitutions, mirroring
|
|
45
|
+
// Installer-default substitutions, mirroring the installer so the
|
|
46
46
|
// template validates as it would after a fresh `balanced` install.
|
|
47
47
|
const PLACEHOLDERS: Record<string, string> = {
|
|
48
48
|
__RULE_LOADING_TIER__: 'balanced',
|