@event4u/agent-config 14.18.0 → 14.20.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 +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- 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 +37 -4
- 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/rules/untrusted-input-defense.md +0 -2
- 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/evaluate-llm-feature/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/markitdown/SKILL.md +1 -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/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- 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/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +34 -33
- 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 +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- 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_session_recycle.ts +51 -3
- 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/audit_field_provenance.ts +44 -3
- 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_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- 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/build_rule_trigger_matrix.ts +5 -0
- 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_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- 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/handoff_context_hook.ts +54 -7
- 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/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- 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_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- 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/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -23,10 +23,48 @@
|
|
|
23
23
|
* sitting in the active tree silently counts as backlog the dashboard and
|
|
24
24
|
* ``/roadmap:process-*`` would try to execute.
|
|
25
25
|
*
|
|
26
|
-
* B. Every roadmap
|
|
27
|
-
* condition** so it never rots without a "when does it come back"
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* B. Every parked roadmap MUST record a **wake
|
|
27
|
+
* condition** so it never rots without a "when does it come back".
|
|
28
|
+
*
|
|
29
|
+
* Until 2026-09-07 this rule short-circuited on the frontmatter STATUS
|
|
30
|
+
* word: ``if (status !== 'later' && !RESUME_RE.test(body))``, so a file
|
|
31
|
+
* whose frontmatter said ``status: later`` passed with no resume text
|
|
32
|
+
* present at all. Measured over the tree at that date, 63 of 82 parked
|
|
33
|
+
* roadmaps passed on the status word alone, 8 on a real ``Blocked until``
|
|
34
|
+
* / ``Resume when``, and 11 on the bare word ``trigger`` — so 74 of 82
|
|
35
|
+
* carried no machine-readable wake condition. The word ``trigger`` was
|
|
36
|
+
* the second hole: a body that merely mentions it in passing ("the
|
|
37
|
+
* trigger fires on push") satisfied a governance gate.
|
|
38
|
+
*
|
|
39
|
+
* The corrected contract, in force regardless of ``status``:
|
|
40
|
+
*
|
|
41
|
+
* 1. **Structured** — an ``entry_condition:`` MAPPING in the
|
|
42
|
+
* frontmatter with three non-empty parts:
|
|
43
|
+
*
|
|
44
|
+
* entry_condition:
|
|
45
|
+
* what: what would change the decision
|
|
46
|
+
* when: when that could arrive
|
|
47
|
+
* who: who would have to act (``none`` is a legal answer)
|
|
48
|
+
*
|
|
49
|
+
* A condition that cannot be observed is a deferral wearing a
|
|
50
|
+
* condition's clothes, which is why all three are required and why
|
|
51
|
+
* blankness is not the same answer as ``none``.
|
|
52
|
+
*
|
|
53
|
+
* 2. **Legacy** — a body line matching ``Blocked until`` /
|
|
54
|
+
* ``Resume when`` / ``blocked-until`` / ``resume-when``. Kept so the
|
|
55
|
+
* ratchet below has a floor to walk DOWN toward rather than a tree
|
|
56
|
+
* to fail on the day it lands.
|
|
57
|
+
*
|
|
58
|
+
* A malformed ``entry_condition`` (scalar, or missing a part) is a HARD
|
|
59
|
+
* finding and is never baselined: it is a defect the author introduced in
|
|
60
|
+
* the change that wrote it, not inherited debt.
|
|
61
|
+
*
|
|
62
|
+
* C. Every parked roadmap MUST carry ``review_by``
|
|
63
|
+
* in its frontmatter. Ratcheted from the measured count, same reasoning.
|
|
64
|
+
*
|
|
65
|
+
* Both ratchets live in ``src/config/gate-violation-baselines.json`` under
|
|
66
|
+
* ``lint_roadmap_later_disposition:wake-condition`` and
|
|
67
|
+
* ``…:review-by``: the count may fall and never rise.
|
|
30
68
|
*
|
|
31
69
|
* Rationale: "roadmaps with open tasks deferred for later are always moved to
|
|
32
70
|
* ``later/``" (user directive 2026-06-16). The active tree holds only roadmaps
|
|
@@ -44,6 +82,7 @@ import * as fs from 'node:fs';
|
|
|
44
82
|
import * as path from 'node:path';
|
|
45
83
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
84
|
|
|
85
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
47
86
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
48
87
|
|
|
49
88
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -64,8 +103,17 @@ function _setRoadmapRootForTest(p: string): void {
|
|
|
64
103
|
const FRONTMATTER_RE = /^---[ \t\f\v]*\n([\s\S]*?)\n---[ \t\f\v]*\n/;
|
|
65
104
|
// STATUS_RE = re.compile(r"^status:\s*([A-Za-z0-9_-]+)\s*$", re.MULTILINE)
|
|
66
105
|
const STATUS_RE = /^status:[ \t\f\v\r\n]*([A-Za-z0-9_-]+)[ \t\f\v\r]*$/m;
|
|
67
|
-
//
|
|
68
|
-
|
|
106
|
+
// The four UNAMBIGUOUS phrases. `trigger` was dropped 2026-09-07: it matched
|
|
107
|
+
// any prose mention of the word, so 11 of 82 parked roadmaps satisfied a
|
|
108
|
+
// governance gate on a sentence about something else entirely.
|
|
109
|
+
const RESUME_RE = /\b(blocked until|resume when|blocked-until|resume-when)\b/i;
|
|
110
|
+
|
|
111
|
+
/** Ratchet keys in `src/config/gate-violation-baselines.json`. */
|
|
112
|
+
export const WAKE_GATE = 'lint_roadmap_later_disposition:wake-condition';
|
|
113
|
+
export const REVIEW_BY_GATE = 'lint_roadmap_later_disposition:review-by';
|
|
114
|
+
|
|
115
|
+
/** The three parts a structured `entry_condition` must name. */
|
|
116
|
+
export const ENTRY_CONDITION_PARTS = ['what', 'when', 'who'] as const;
|
|
69
117
|
|
|
70
118
|
// Non-roadmap files that live in the tree but are not roadmaps.
|
|
71
119
|
const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
|
|
@@ -79,6 +127,67 @@ const EXCLUDE_PREFIXES: readonly string[] = ['open-questions'];
|
|
|
79
127
|
interface Violation {
|
|
80
128
|
file: string;
|
|
81
129
|
reason: string;
|
|
130
|
+
/**
|
|
131
|
+
* `hard` fails immediately; `wake` and `review-by` are ratcheted against a
|
|
132
|
+
* committed baseline. A hard finding is one the AUTHOR introduced (a
|
|
133
|
+
* malformed structured field, a misplaced `status: later`); a ratcheted one
|
|
134
|
+
* is debt the corrected gate revealed.
|
|
135
|
+
*/
|
|
136
|
+
cls: 'hard' | 'wake' | 'review-by';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The three-part reading of a structured `entry_condition`.
|
|
141
|
+
*
|
|
142
|
+
* Deliberately parsed from the raw frontmatter text rather than through a YAML
|
|
143
|
+
* loader: this gate's contract is byte-pinned against a retired Python
|
|
144
|
+
* implementation and must not acquire a parser dependency whose error
|
|
145
|
+
* behavior differs. The shape is fixed and shallow, so a line reader is
|
|
146
|
+
* sufficient and its failure mode is legible.
|
|
147
|
+
*/
|
|
148
|
+
export function entryConditionParts(frontmatter: string): {
|
|
149
|
+
present: boolean;
|
|
150
|
+
scalar: boolean;
|
|
151
|
+
parts: Record<string, string>;
|
|
152
|
+
} {
|
|
153
|
+
const lines = frontmatter.split('\n');
|
|
154
|
+
const idx = lines.findIndex((l) => /^entry_condition:/.test(l));
|
|
155
|
+
if (idx === -1) {
|
|
156
|
+
return { present: false, scalar: false, parts: {} };
|
|
157
|
+
}
|
|
158
|
+
const head = (lines[idx] as string).slice('entry_condition:'.length).trim();
|
|
159
|
+
if (head !== '') {
|
|
160
|
+
// `entry_condition: "…"` — the pre-contract scalar form.
|
|
161
|
+
return { present: true, scalar: true, parts: {} };
|
|
162
|
+
}
|
|
163
|
+
const parts: Record<string, string> = {};
|
|
164
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
165
|
+
const line = lines[i] as string;
|
|
166
|
+
if (!/^\s+\S/.test(line)) break;
|
|
167
|
+
const m = /^\s+([a-z_]+):\s*(.*)$/.exec(line);
|
|
168
|
+
if (m === null) continue;
|
|
169
|
+
parts[m[1] as string] = _unquote((m[2] as string).trim());
|
|
170
|
+
}
|
|
171
|
+
return { present: true, scalar: false, parts };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _unquote(v: string): string {
|
|
175
|
+
if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
|
|
176
|
+
return v.slice(1, -1).trim();
|
|
177
|
+
}
|
|
178
|
+
return v.trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Missing / empty part names, `[]` when the field satisfies the contract. */
|
|
182
|
+
export function entryConditionProblems(frontmatter: string): string[] {
|
|
183
|
+
const read = entryConditionParts(frontmatter);
|
|
184
|
+
if (!read.present) return ['absent'];
|
|
185
|
+
if (read.scalar) return ['scalar'];
|
|
186
|
+
const missing: string[] = [];
|
|
187
|
+
for (const part of ENTRY_CONDITION_PARTS) {
|
|
188
|
+
if ((read.parts[part] ?? '').trim() === '') missing.push(part);
|
|
189
|
+
}
|
|
190
|
+
return missing;
|
|
82
191
|
}
|
|
83
192
|
|
|
84
193
|
function _is_roadmap(p: string): boolean {
|
|
@@ -167,6 +276,7 @@ function check(root: string): Violation[] {
|
|
|
167
276
|
if (status === 'later' && !in_later) {
|
|
168
277
|
out.push({
|
|
169
278
|
file: rel,
|
|
279
|
+
cls: 'hard',
|
|
170
280
|
reason:
|
|
171
281
|
'frontmatter `status: later` but file is not under ' +
|
|
172
282
|
'`agents/roadmaps/later/` — a blocked-for-later roadmap ' +
|
|
@@ -175,19 +285,57 @@ function check(root: string): Violation[] {
|
|
|
175
285
|
});
|
|
176
286
|
}
|
|
177
287
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
288
|
+
if (!in_later) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const fm = _frontmatter(text);
|
|
293
|
+
const body = text.slice(fm.length);
|
|
294
|
+
const problems = entryConditionProblems(fm);
|
|
295
|
+
const structured = problems.length === 0;
|
|
296
|
+
|
|
297
|
+
// Rule B(1) — a PRESENT entry_condition must satisfy the contract.
|
|
298
|
+
// Hard, never ratcheted: a malformed field is authored, not inherited.
|
|
299
|
+
if (!structured && problems[0] !== 'absent') {
|
|
300
|
+
out.push({
|
|
301
|
+
file: rel,
|
|
302
|
+
cls: 'hard',
|
|
303
|
+
reason:
|
|
304
|
+
problems[0] === 'scalar'
|
|
305
|
+
? '`entry_condition` is a scalar string — it must be a mapping naming ' +
|
|
306
|
+
'`what` (what would change the decision), `when` (when it could ' +
|
|
307
|
+
'arrive) and `who` (who would have to act; `none` is a legal answer).'
|
|
308
|
+
: '`entry_condition` is missing or blank in: ' +
|
|
309
|
+
problems.join(', ') +
|
|
310
|
+
'. A condition that cannot be observed is a deferral wearing a ' +
|
|
311
|
+
"condition's clothes; `none` is a legal answer for `who` and " +
|
|
312
|
+
'blankness is not.',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Rule B(2) — a wake condition is required REGARDLESS of `status`.
|
|
317
|
+
if (!structured && !RESUME_RE.test(body)) {
|
|
318
|
+
out.push({
|
|
319
|
+
file: rel,
|
|
320
|
+
cls: 'wake',
|
|
321
|
+
reason:
|
|
322
|
+
'roadmap under `agents/roadmaps/later/` has no wake condition — the ' +
|
|
323
|
+
'frontmatter `status` word no longer satisfies this gate, and neither ' +
|
|
324
|
+
'does the bare word `trigger`. Add a structured `entry_condition` ' +
|
|
325
|
+
'(`what` / `when` / `who`) or a `Blocked until` / `Resume when` line.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Rule C — review_by in the frontmatter of every parked roadmap.
|
|
330
|
+
if (!/^review_by:\s*\S/m.test(fm)) {
|
|
331
|
+
out.push({
|
|
332
|
+
file: rel,
|
|
333
|
+
cls: 'review-by',
|
|
334
|
+
reason:
|
|
335
|
+
'roadmap under `agents/roadmaps/later/` has no `review_by:` in its ' +
|
|
336
|
+
'frontmatter — a park with no review date is indistinguishable from ' +
|
|
337
|
+
'an abandonment.',
|
|
338
|
+
});
|
|
191
339
|
}
|
|
192
340
|
}
|
|
193
341
|
return out;
|
|
@@ -337,24 +485,65 @@ function main(argv?: readonly string[]): number {
|
|
|
337
485
|
}
|
|
338
486
|
|
|
339
487
|
const violations = check(ROADMAP_ROOT);
|
|
488
|
+
const hard = violations.filter((v) => v.cls === 'hard');
|
|
489
|
+
const wake = violations.filter((v) => v.cls === 'wake');
|
|
490
|
+
const reviewBy = violations.filter((v) => v.cls === 'review-by');
|
|
340
491
|
|
|
341
492
|
if (args.json) {
|
|
342
|
-
//
|
|
343
|
-
|
|
493
|
+
// The `file` / `reason` pair is the pinned shape; `cls` is additive, so
|
|
494
|
+
// a consumer reading the two original keys is unaffected.
|
|
495
|
+
const payload = violations.map((v) => ({ file: v.file, reason: v.reason, cls: v.cls }));
|
|
344
496
|
process.stdout.write(_jsonDumpsIndent2(payload) + '\n');
|
|
345
|
-
|
|
497
|
+
// A JSON consumer wants the findings, not the ratchet verdict; the exit
|
|
498
|
+
// code still carries it.
|
|
499
|
+
return _verdict(hard, wake, reviewBy, true);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (hard.length) {
|
|
346
503
|
process.stdout.write('❌ later/ disposition violations:\n');
|
|
347
|
-
for (const v of
|
|
504
|
+
for (const v of hard) {
|
|
348
505
|
process.stdout.write(` • ${v.file}\n ${v.reason}\n`);
|
|
349
506
|
}
|
|
350
|
-
}
|
|
507
|
+
}
|
|
508
|
+
return _verdict(hard, wake, reviewBy, false);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Ratcheted verdict. The two revealed-debt classes print their findings either
|
|
513
|
+
* way — at baseline they are the backlog this gate is walking down, and
|
|
514
|
+
* printing them is what makes the next lowering possible.
|
|
515
|
+
*/
|
|
516
|
+
function _verdict(
|
|
517
|
+
hard: readonly Violation[],
|
|
518
|
+
wake: readonly Violation[],
|
|
519
|
+
reviewBy: readonly Violation[],
|
|
520
|
+
quiet: boolean,
|
|
521
|
+
): number {
|
|
522
|
+
let failed = hard.length > 0;
|
|
523
|
+
for (const [gate, found] of [
|
|
524
|
+
[WAKE_GATE, wake],
|
|
525
|
+
[REVIEW_BY_GATE, reviewBy],
|
|
526
|
+
] as const) {
|
|
527
|
+
const verdict = checkRatchet({ gate, actual: found.length, repoRoot: REPO_ROOT });
|
|
528
|
+
if (!verdict.ok) {
|
|
529
|
+
failed = true;
|
|
530
|
+
if (!quiet) {
|
|
531
|
+
for (const v of found) {
|
|
532
|
+
process.stderr.write(` • ${v.file}\n ${v.reason}\n`);
|
|
533
|
+
}
|
|
534
|
+
process.stderr.write(`❌ ${verdict.message}\n`);
|
|
535
|
+
}
|
|
536
|
+
} else if (!quiet) {
|
|
537
|
+
process.stdout.write(`✅ ${verdict.message}\n`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!failed && !quiet) {
|
|
351
541
|
process.stdout.write(
|
|
352
542
|
'✅ later/ disposition: every blocked-for-later roadmap is parked ' +
|
|
353
|
-
'correctly
|
|
543
|
+
'correctly, and both wake-condition ratchets hold.\n',
|
|
354
544
|
);
|
|
355
545
|
}
|
|
356
|
-
|
|
357
|
-
return violations.length ? 1 : 0;
|
|
546
|
+
return failed ? 1 : 0;
|
|
358
547
|
}
|
|
359
548
|
|
|
360
549
|
function _isCliEntry(): boolean {
|
|
@@ -386,6 +575,7 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
386
575
|
export {
|
|
387
576
|
REPO_ROOT,
|
|
388
577
|
ROADMAP_ROOT,
|
|
578
|
+
type Violation,
|
|
389
579
|
LATER_DIR,
|
|
390
580
|
FRONTMATTER_RE,
|
|
391
581
|
STATUS_RE,
|
|
@@ -138,9 +138,19 @@ export function grantSurface(rel: string): GrantSurface {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// A BOUND pragma suppresses only the matches it fingerprints, so the scan
|
|
142
|
+
// runs and its output is filtered at the end. An UNBOUND one keeps the old
|
|
143
|
+
// whole-file bail and reports itself.
|
|
144
|
+
const pragmaForm = sf.pragma_form(CHECK);
|
|
145
|
+
if (pragmaForm === 'legacy') {
|
|
146
|
+
return [sl.legacy_pragma_finding(sf, CHECK)];
|
|
143
147
|
}
|
|
148
|
+
// EVERY exit from this function goes through `finish`. Wrapping only the
|
|
149
|
+
// terminal `return` is the defect that shipped for one commit here: this
|
|
150
|
+
// scan has an early return for an artifact with no `execution:` block, and a
|
|
151
|
+
// bound pragma silently did not apply to exactly those artifacts.
|
|
152
|
+
const finish = (hits: sl.Finding[]): sl.Finding[] =>
|
|
153
|
+
pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
|
|
144
154
|
const fm = _frontmatter(sf);
|
|
145
155
|
if (!fm) {
|
|
146
156
|
return [];
|
|
@@ -261,7 +271,7 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
261
271
|
}
|
|
262
272
|
|
|
263
273
|
if (Object.keys(ex).length === 0) {
|
|
264
|
-
return out;
|
|
274
|
+
return finish(out);
|
|
265
275
|
}
|
|
266
276
|
const etype = _stripQuotes((ex['type'] ?? [0, ''])[1]);
|
|
267
277
|
const safety = _stripQuotes((ex['safety_mode'] ?? [0, ''])[1]);
|
|
@@ -331,15 +341,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
|
|
|
331
341
|
),
|
|
332
342
|
);
|
|
333
343
|
}
|
|
334
|
-
return out;
|
|
344
|
+
return finish(out);
|
|
335
345
|
}
|
|
336
346
|
|
|
337
347
|
interface Args {
|
|
348
|
+
/** Scan base for the bounded root mode; absent = the package root. */
|
|
349
|
+
root?: string;
|
|
338
350
|
json: boolean;
|
|
339
351
|
}
|
|
340
352
|
|
|
341
353
|
function _argError(msg: string): never {
|
|
342
|
-
process.stderr.write('usage: lint_skill_frontmatter_safety [-h] [--json]\n');
|
|
354
|
+
process.stderr.write('usage: lint_skill_frontmatter_safety [-h] [--json] [--root DIR]\n');
|
|
343
355
|
process.stderr.write(`lint_skill_frontmatter_safety: error: ${msg}\n`);
|
|
344
356
|
process.exit(2);
|
|
345
357
|
}
|
|
@@ -347,12 +359,28 @@ function _argError(msg: string): never {
|
|
|
347
359
|
function parse_args(argv: readonly string[]): Args {
|
|
348
360
|
const out: Args = { json: false };
|
|
349
361
|
const extra: string[] = [];
|
|
362
|
+
let rootPending = false;
|
|
350
363
|
for (const a of argv) {
|
|
364
|
+
if (rootPending) {
|
|
365
|
+
out.root = a;
|
|
366
|
+
rootPending = false;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
351
369
|
if (a === '-h' || a === '--help') {
|
|
352
|
-
process.stdout.write('usage: lint_skill_frontmatter_safety [-h] [--json]\n');
|
|
370
|
+
process.stdout.write('usage: lint_skill_frontmatter_safety [-h] [--json] [--root DIR]\n');
|
|
353
371
|
process.exit(0);
|
|
354
372
|
} else if (a === '--json') {
|
|
355
373
|
out.json = true;
|
|
374
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
375
|
+
// Bounded root mode. Absent, this linter's own default roots stand;
|
|
376
|
+
// present, the same relative roots resolve against DIR instead. Used by
|
|
377
|
+
// the scout to scan a quarantined candidate, and by --self-test fixtures.
|
|
378
|
+
const eq = a.indexOf('=');
|
|
379
|
+
if (eq !== -1) {
|
|
380
|
+
out.root = a.slice(eq + 1);
|
|
381
|
+
} else {
|
|
382
|
+
rootPending = true;
|
|
383
|
+
}
|
|
356
384
|
} else {
|
|
357
385
|
extra.push(a);
|
|
358
386
|
}
|
|
@@ -365,6 +393,7 @@ function parse_args(argv: readonly string[]): Args {
|
|
|
365
393
|
|
|
366
394
|
export function main(argv: readonly string[] | null = null): number {
|
|
367
395
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
396
|
+
const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
|
|
368
397
|
|
|
369
398
|
const findings: sl.Finding[] = [];
|
|
370
399
|
// `src/subagents` is the fourth root, added because its omission is what made
|
|
@@ -372,7 +401,7 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
372
401
|
// bare shell lives there, and the gate had never read it.
|
|
373
402
|
const roots = ['src/skills', 'src/agent-src', 'src/domains', 'src/subagents'];
|
|
374
403
|
let scanned = 0;
|
|
375
|
-
for (const sf of sl.iter_corpus(roots, ['.md'])) {
|
|
404
|
+
for (const sf of sl.iter_corpus(roots, ['.md'], scanBase)) {
|
|
376
405
|
scanned += 1;
|
|
377
406
|
for (const h of _scan(sf)) {
|
|
378
407
|
findings.push(h);
|
|
@@ -400,6 +429,13 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
400
429
|
throw exc;
|
|
401
430
|
}
|
|
402
431
|
|
|
432
|
+
// Publish what this child inspected so the umbrella can aggregate a real
|
|
433
|
+
// corpus size rather than a count of children. Same number the assertion
|
|
434
|
+
// above just accepted, so the published figure cannot drift from it.
|
|
435
|
+
if (args.json) {
|
|
436
|
+
sl.report_child_scanned(scanned);
|
|
437
|
+
}
|
|
438
|
+
|
|
403
439
|
if (args.json) {
|
|
404
440
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
405
441
|
process.stdout.write(payload + '\n');
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A skill may not say "explicit request only" and publish auto-trigger keywords.
|
|
4
|
+
*
|
|
5
|
+
* The description is consumed — it is the frontmatter a catalogue and a host
|
|
6
|
+
* surface read. The keyword section is not: measured across the tree, nothing
|
|
7
|
+
* in `src/` parses `## Auto-trigger keywords`, so a skill carrying both halves
|
|
8
|
+
* is not mis-ROUTED, it is mis-DESCRIBED. A reader deciding whether they may
|
|
9
|
+
* invoke the skill meets two opposite answers in one file, and the half that
|
|
10
|
+
* looks operational is the half nothing reads.
|
|
11
|
+
*
|
|
12
|
+
* Three skills shipped in that state and a human had to notice it. The pair is
|
|
13
|
+
* mechanically decidable from the file, so noticing it again should not be
|
|
14
|
+
* anybody's job.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not a judgement about WHICH half is right. Both are legitimate
|
|
17
|
+
* designs; only the contradiction is a defect, and which way to resolve it is
|
|
18
|
+
* the author's call.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes: 0 no contradiction · 1 at least one file carries both.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as os from 'node:os';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
28
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
29
|
+
|
|
30
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
31
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
|
+
const SKILLS_REL = 'src/skills';
|
|
33
|
+
|
|
34
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
35
|
+
/** Matched in the description only — the phrase is meaningful there, prose elsewhere. */
|
|
36
|
+
const EXPLICIT_ONLY_RE = /explicit[- ]request[- ]only|explicit request only/i;
|
|
37
|
+
const AUTO_TRIGGER_RE = /^##\s+Auto-trigger keywords\s*$/m;
|
|
38
|
+
|
|
39
|
+
export function contradicts(text: string): boolean {
|
|
40
|
+
const fm = FRONTMATTER_RE.exec(text);
|
|
41
|
+
if (fm === null) return false;
|
|
42
|
+
// `description:` may be a folded or quoted scalar spanning lines, so the
|
|
43
|
+
// slice runs to the next top-level key rather than to the next newline.
|
|
44
|
+
const desc = /^description:([\s\S]*?)(?=\n[a-z_]+:|$)/im.exec(fm[1] as string);
|
|
45
|
+
if (desc === null) return false;
|
|
46
|
+
if (!EXPLICIT_ONLY_RE.test(desc[1] as string)) return false;
|
|
47
|
+
return AUTO_TRIGGER_RE.test(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function main(): number {
|
|
51
|
+
if (process.argv.slice(2).includes('--self-test')) return selfTest();
|
|
52
|
+
const rootIdx = process.argv.indexOf('--root');
|
|
53
|
+
const root = rootIdx === -1 ? ROOT : (process.argv[rootIdx + 1] ?? ROOT);
|
|
54
|
+
const dir = path.join(root, SKILLS_REL);
|
|
55
|
+
let names: string[] = [];
|
|
56
|
+
try {
|
|
57
|
+
names = fs.readdirSync(dir).sort();
|
|
58
|
+
} catch {
|
|
59
|
+
process.stderr.write(`❌ no skills directory under ${root}/${SKILLS_REL}\n`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const offenders: string[] = [];
|
|
63
|
+
let scanned = 0;
|
|
64
|
+
for (const n of names) {
|
|
65
|
+
const f = path.join(dir, n, 'SKILL.md');
|
|
66
|
+
if (!fs.existsSync(f)) continue;
|
|
67
|
+
scanned += 1;
|
|
68
|
+
if (contradicts(fs.readFileSync(f, 'utf-8'))) {
|
|
69
|
+
offenders.push(`${SKILLS_REL}/${n}/SKILL.md`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
reportScanned({
|
|
73
|
+
gate: 'lint_skill_trigger_contradiction',
|
|
74
|
+
scanned,
|
|
75
|
+
units: 'SKILL.md file(s)',
|
|
76
|
+
roots: [SKILLS_REL],
|
|
77
|
+
});
|
|
78
|
+
if (offenders.length > 0) {
|
|
79
|
+
for (const o of offenders) {
|
|
80
|
+
process.stdout.write(
|
|
81
|
+
`❌ ${o}: description says explicit-request-only AND the body publishes ` +
|
|
82
|
+
'`## Auto-trigger keywords` — the two halves say opposite things ' +
|
|
83
|
+
'and nothing in src/ parses the keyword half, so the operational-looking ' +
|
|
84
|
+
'one is the inert one.\n',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
`\n${String(offenders.length)} contradicting skill(s). Resolve by removing whichever ` +
|
|
89
|
+
'half is not true; the check does not decide which.\n',
|
|
90
|
+
);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`✅ lint_skill_trigger_contradiction: ${String(scanned)} skill(s) inspected, ` +
|
|
95
|
+
'none claims explicit-request-only while publishing auto-trigger keywords.\n',
|
|
96
|
+
);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selfTest(): number {
|
|
101
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'trigger-contradiction-selftest-'));
|
|
102
|
+
const write = (name: string, body: string): string => {
|
|
103
|
+
const dir = path.join(tmp, name, SKILLS_REL, 'sample');
|
|
104
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), body, 'utf-8');
|
|
106
|
+
return path.join(tmp, name);
|
|
107
|
+
};
|
|
108
|
+
const run = (root: string): number =>
|
|
109
|
+
runGateCli(ROOT, 'src/scripts/lint_skill_trigger_contradiction.ts', ['--root', root], root);
|
|
110
|
+
|
|
111
|
+
const both =
|
|
112
|
+
'---\nname: sample\ndescription: Does a thing; explicit request only.\n---\n\n## Auto-trigger keywords\n\nfoo\n';
|
|
113
|
+
const descOnly = '---\nname: sample\ndescription: Does a thing; explicit request only.\n---\n\nbody\n';
|
|
114
|
+
const autoOnly = '---\nname: sample\ndescription: Does a thing.\n---\n\n## Auto-trigger keywords\n\nfoo\n';
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return runSelfTest({
|
|
118
|
+
gate: 'lint_skill_trigger_contradiction',
|
|
119
|
+
minCases: 3,
|
|
120
|
+
minRejectCases: 1,
|
|
121
|
+
cases: [
|
|
122
|
+
{
|
|
123
|
+
name: 'a skill carrying BOTH halves is rejected',
|
|
124
|
+
expect: 'reject',
|
|
125
|
+
run: () => run(write('both', both)),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'explicit-request-only with no keyword section passes',
|
|
129
|
+
expect: 'accept',
|
|
130
|
+
run: () => run(write('desc-only', descOnly)),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'auto-trigger keywords with no explicit-only claim passes',
|
|
134
|
+
expect: 'accept',
|
|
135
|
+
run: () => run(write('auto-only', autoOnly)),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
} finally {
|
|
140
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _isCliEntry(): boolean {
|
|
145
|
+
if (process.argv[1] === undefined) return false;
|
|
146
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
147
|
+
try {
|
|
148
|
+
return (
|
|
149
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
150
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
158
|
+
process.exit(main());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { SKILLS_REL, main };
|
|
@@ -41,6 +41,10 @@ export interface Resource {
|
|
|
41
41
|
readonly source: string;
|
|
42
42
|
readonly mime_type: string;
|
|
43
43
|
readonly kind: ResourceKind;
|
|
44
|
+
/** Frontmatter `type:` (`always` / `auto` / `manual`) where the asset has one. */
|
|
45
|
+
readonly rule_type?: string;
|
|
46
|
+
/** File mtime, ISO-8601, for the protocol's `lastModified` annotation. */
|
|
47
|
+
readonly last_modified?: string;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// Python: re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
|
|
@@ -160,9 +164,20 @@ function _load(
|
|
|
160
164
|
source: meta.source ?? 'package',
|
|
161
165
|
mime_type: MIME_MARKDOWN,
|
|
162
166
|
kind,
|
|
167
|
+
...(meta.type === undefined ? {} : { rule_type: meta.type.trim() }),
|
|
168
|
+
...(_mtimeIso(p) === null ? {} : { last_modified: _mtimeIso(p) as string }),
|
|
163
169
|
};
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
/** File mtime as ISO-8601, or `null` when it cannot be read. */
|
|
173
|
+
function _mtimeIso(p: string): string | null {
|
|
174
|
+
try {
|
|
175
|
+
return fs.statSync(p).mtime.toISOString();
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
166
181
|
export function scan_rules(root?: string | null): [Resource[], string[]] {
|
|
167
182
|
const base = root ?? _project_root();
|
|
168
183
|
const rules_root = path.join(base, 'dist/agent-src', 'rules');
|
|
@@ -256,6 +271,25 @@ function _strCmp(a: string, b: string): number {
|
|
|
256
271
|
return 0;
|
|
257
272
|
}
|
|
258
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Priority in the protocol's 0..1 range.
|
|
276
|
+
*
|
|
277
|
+
* The roadmap that asked for this said to derive it "from the existing `tier`".
|
|
278
|
+
* There is no `tier` on a Resource — measured, not assumed — so it derives from
|
|
279
|
+
* what does exist: the frontmatter `type:` an always-loaded rule carries, then
|
|
280
|
+
* the asset kind. A rule the host loads on every turn outranks one it loads on
|
|
281
|
+
* a trigger, which outranks reference material.
|
|
282
|
+
*
|
|
283
|
+
* PROTOCOL CONFORMANCE ONLY. No shipped host is known to consume `priority` for
|
|
284
|
+
* selection, and this function must not be cited as evidence that one does.
|
|
285
|
+
*/
|
|
286
|
+
export function resourcePriority(resource: Resource): number {
|
|
287
|
+
if (resource.rule_type === 'always') return 1;
|
|
288
|
+
if (resource.rule_type === 'auto') return 0.7;
|
|
289
|
+
if (resource.rule_type === 'manual') return 0.3;
|
|
290
|
+
return resource.kind === 'rule' ? 0.7 : resource.kind === 'context' ? 0.5 : 0.3;
|
|
291
|
+
}
|
|
292
|
+
|
|
259
293
|
/** Project a Resource into MCP `Resource` constructor kwargs. */
|
|
260
294
|
export function to_mcp_resource_meta(resource: Resource): Record<string, unknown> {
|
|
261
295
|
return {
|
|
@@ -263,6 +297,13 @@ export function to_mcp_resource_meta(resource: Resource): Record<string, unknown
|
|
|
263
297
|
name: resource.name,
|
|
264
298
|
description: resource.description,
|
|
265
299
|
mimeType: resource.mime_type,
|
|
300
|
+
// `audience` is the model, not the end user: these are governance
|
|
301
|
+
// assets a host injects, never something a person reads in a client.
|
|
302
|
+
audience: ['assistant'],
|
|
303
|
+
priority: resourcePriority(resource),
|
|
304
|
+
...(resource.last_modified === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { lastModified: resource.last_modified }),
|
|
266
307
|
_meta: { source: resource.source, kind: resource.kind },
|
|
267
308
|
};
|
|
268
309
|
}
|