@event4u/agent-config 16.1.0 → 16.2.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 +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- 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 +4 -4
- 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 +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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 +10 -10
- 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 +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A hook's exit code comes from the table, never from a numeral.
|
|
4
|
+
*
|
|
5
|
+
* road-to-a-kernel-that-guards-its-plumbing 4.1, the enforcing half.
|
|
6
|
+
* `hooks/exit_codes.ts` is the table; this is what keeps it the only one.
|
|
7
|
+
*
|
|
8
|
+
* WHAT THE NUMERALS COST. Thirty-three files under `src/scripts/hooks/` each
|
|
9
|
+
* declared their own `EXIT_ALLOW = 0` / `EXIT_BLOCK = 1` / `EXIT_WARN = 2`, two
|
|
10
|
+
* of them under a third name. The danger is not a typo: 1 and 2 mean the
|
|
11
|
+
* OPPOSITE things on Claude Code from what they mean in this tree's internal
|
|
12
|
+
* language, which is why `host_semantics.ts` exists to lower one onto the
|
|
13
|
+
* other. A bare `process.exit(1)` in a concern is a claim about which of the
|
|
14
|
+
* two dialects the writer had in mind, and the source does not record which.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT SCANS. `.ts` under `src/scripts/hooks/`, refusing a call whose
|
|
17
|
+
* argument is a bare numeric literal. An identifier is accepted whatever it is
|
|
18
|
+
* called — this gate does not verify that the identifier came from the table,
|
|
19
|
+
* because a gate that resolved imports would be a type-checker, and the thing
|
|
20
|
+
* it would catch (a local `const EXIT_BLOCK = 1` shadowing the import) is
|
|
21
|
+
* already visible in review as a duplicate declaration. Stated rather than left
|
|
22
|
+
* for a reader to assume the green covers it.
|
|
23
|
+
*
|
|
24
|
+
* WHY THE DISPATCHERS ARE OUT OF SCOPE. The `*-dispatcher.sh` trampolines carry
|
|
25
|
+
* `process.exit(0)` inside a `node -e` one-liner that reads a workspace root
|
|
26
|
+
* off stdin. They are shell, they cannot import, and their exit code is the
|
|
27
|
+
* one-liner's, never a verdict. The table's own header says so; this gate scans
|
|
28
|
+
* `.ts` only, which is the same boundary stated once more where it is enforced.
|
|
29
|
+
*
|
|
30
|
+
* THE TABLE FILE ITSELF is excluded, and that is the one exclusion: it is where
|
|
31
|
+
* the numerals are DEFINED, so a gate that refused them there would forbid the
|
|
32
|
+
* fix it exists to require.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 clean · 1 a bare numeral reached `process.exit` · 2 the corpus
|
|
35
|
+
* could not be read.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
|
|
43
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
44
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
45
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
46
|
+
|
|
47
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
48
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
49
|
+
|
|
50
|
+
export const GATE = 'lint_exit_codes';
|
|
51
|
+
|
|
52
|
+
const HOOKS_REL = path.join('src', 'scripts', 'hooks');
|
|
53
|
+
|
|
54
|
+
/** The file that DEFINES the numerals. Excluding it is not a carve-out. */
|
|
55
|
+
const TABLE_BASENAME = 'exit_codes.ts';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The fixture import line, ASSEMBLED rather than written out.
|
|
59
|
+
*
|
|
60
|
+
* `prepack-check.mjs` scans source text for relative imports and cannot tell a
|
|
61
|
+
* string literal from a real one, so a fixture containing the words
|
|
62
|
+
* `from './exit_codes.js'` reads as this file importing a sibling that does not
|
|
63
|
+
* exist beside it — the table lives under `hooks/`. It reported exactly that
|
|
64
|
+
* before this was assembled. Splitting the specifier is the smallest change
|
|
65
|
+
* that keeps the fixture honest about what a passing hook file looks like
|
|
66
|
+
* without asserting an import this file does not make.
|
|
67
|
+
*/
|
|
68
|
+
const FIXTURE_IMPORT = (name: string): string => `import { ${name} } from '.` + `/${TABLE_BASENAME.replace('.ts', '.js')}';\n`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `process.exit(<digits>)`, with optional whitespace.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately anchored on `process.exit` rather than on any `exit(`: a
|
|
74
|
+
* concern's own helper named `exit` is not the process boundary, and widening
|
|
75
|
+
* the pattern to catch it would red on ordinary code.
|
|
76
|
+
*/
|
|
77
|
+
const BARE_EXIT = /process\s*\.\s*exit\s*\(\s*(-?\d+)\s*\)/g;
|
|
78
|
+
|
|
79
|
+
export interface ExitFinding {
|
|
80
|
+
readonly file: string;
|
|
81
|
+
readonly line: number;
|
|
82
|
+
readonly code: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Findings in one file's text. Pure, so a test reads the decision. */
|
|
86
|
+
export function findBareExits(rel: string, text: string): ExitFinding[] {
|
|
87
|
+
const out: ExitFinding[] = [];
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i] as string;
|
|
91
|
+
// A commented example is prose about the rule, not an exit.
|
|
92
|
+
if (/^\s*(\*|\/\/)/.test(line)) continue;
|
|
93
|
+
BARE_EXIT.lastIndex = 0;
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = BARE_EXIT.exec(line)) !== null) {
|
|
96
|
+
out.push({ file: rel, line: i + 1, code: m[1] as string });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hookFiles(root: string): string[] {
|
|
103
|
+
const dir = path.join(root, HOOKS_REL);
|
|
104
|
+
let entries: string[];
|
|
105
|
+
try {
|
|
106
|
+
entries = fs.readdirSync(dir);
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return entries
|
|
111
|
+
.filter((f) => f.endsWith('.ts') && f !== TABLE_BASENAME)
|
|
112
|
+
.map((f) => path.join(HOOKS_REL, f))
|
|
113
|
+
.sort();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
117
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
118
|
+
const i = argv.indexOf('--root');
|
|
119
|
+
if (i !== -1 && (argv[i + 1] === undefined || argv[i + 1]?.startsWith('-') === true)) {
|
|
120
|
+
process.stderr.write(`${GATE}: \`--root\` needs a directory.\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
const root = i === -1 ? ROOT : (argv[i + 1] as string);
|
|
124
|
+
|
|
125
|
+
const files = hookFiles(root);
|
|
126
|
+
const findings: ExitFinding[] = [];
|
|
127
|
+
// A file the gate cannot READ is not a file it cleared. The first version
|
|
128
|
+
// of this loop skipped it and then counted it complete, which is a false
|
|
129
|
+
// green with a number behind it — the exact shape `assertScanned` exists to
|
|
130
|
+
// refuse one level up, reintroduced inside the loop. Reported by an
|
|
131
|
+
// independent review before this landed.
|
|
132
|
+
const unreadable: string[] = [];
|
|
133
|
+
for (const rel of files) {
|
|
134
|
+
let text: string;
|
|
135
|
+
try {
|
|
136
|
+
text = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
137
|
+
} catch {
|
|
138
|
+
unreadable.push(rel);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
findings.push(...findBareExits(rel, text));
|
|
142
|
+
}
|
|
143
|
+
if (unreadable.length > 0) {
|
|
144
|
+
for (const rel of unreadable) {
|
|
145
|
+
process.stderr.write(`❌ ${GATE}: cannot read ${rel} — the corpus is incomplete.\n`);
|
|
146
|
+
}
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Before the verdict: a corpus of zero is a gate that read nothing, and a
|
|
151
|
+
// green over it says the tree is clean when nobody looked.
|
|
152
|
+
try {
|
|
153
|
+
reportScanned({ gate: GATE, scanned: files.length, units: 'hook file(s)', roots: [HOOKS_REL] });
|
|
154
|
+
} catch (exc) {
|
|
155
|
+
process.stderr.write(`❌ ${String((exc as Error).message)}\n`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ledger = new GateLedger(GATE);
|
|
160
|
+
ledger.plan(files);
|
|
161
|
+
const failed = new Set(findings.map((f) => f.file));
|
|
162
|
+
for (const f of findings) {
|
|
163
|
+
ledger.fail(f.file, `bare exit numeral ${f.code} at line ${String(f.line)}`);
|
|
164
|
+
}
|
|
165
|
+
for (const rel of files) if (!failed.has(rel)) ledger.complete(rel);
|
|
166
|
+
ledger.report();
|
|
167
|
+
|
|
168
|
+
if (findings.length > 0) {
|
|
169
|
+
for (const f of findings) {
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`❌ ${GATE}: ${f.file}:${String(f.line)} exits with the bare numeral ` +
|
|
172
|
+
`\`${f.code}\`. Import the name from \`hooks/exit_codes.ts\` — the numbers ` +
|
|
173
|
+
'mean opposite things in this tree and on the host, and a numeral records ' +
|
|
174
|
+
'neither.\n',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`✅ ${GATE}: ${String(files.length)} hook file(s), no bare exit numeral.\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function selfTest(): number {
|
|
186
|
+
const plant = (name: string, body: string): string => {
|
|
187
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `exit-codes-${name}-`));
|
|
188
|
+
fs.mkdirSync(path.join(dir, HOOKS_REL), { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(dir, HOOKS_REL, 'probe_hook.ts'), body, 'utf-8');
|
|
190
|
+
// The table itself, so the exclusion is exercised rather than assumed.
|
|
191
|
+
fs.writeFileSync(
|
|
192
|
+
path.join(dir, HOOKS_REL, TABLE_BASENAME),
|
|
193
|
+
'export const EXIT_ALLOW = 0;\nprocess.exit(0);\n',
|
|
194
|
+
'utf-8',
|
|
195
|
+
);
|
|
196
|
+
return dir;
|
|
197
|
+
};
|
|
198
|
+
const run = (root: string): number =>
|
|
199
|
+
runGateCli(ROOT, path.join('src', 'scripts', 'lint_exit_codes.ts'), ['--root', root], ROOT);
|
|
200
|
+
|
|
201
|
+
return runSelfTest({
|
|
202
|
+
gate: GATE,
|
|
203
|
+
minCases: 6,
|
|
204
|
+
minRejectCases: 4,
|
|
205
|
+
cases: [
|
|
206
|
+
{
|
|
207
|
+
name: 'an empty corpus is refused, not certified green',
|
|
208
|
+
expect: 'reject',
|
|
209
|
+
run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'exit-codes-empty-'))),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'a bare deny numeral fails',
|
|
213
|
+
expect: 'reject',
|
|
214
|
+
run: () => run(plant('block', 'process.exit(1);\n')),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'a bare allow numeral fails too — 0 is the one people think is harmless',
|
|
218
|
+
expect: 'reject',
|
|
219
|
+
run: () => run(plant('allow', 'process.exit(0);\n')),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'whitespace inside the call does not hide it',
|
|
223
|
+
expect: 'reject',
|
|
224
|
+
run: () => run(plant('spaced', 'process . exit ( 2 );\n')),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'an imported name passes',
|
|
228
|
+
expect: 'accept',
|
|
229
|
+
run: () =>
|
|
230
|
+
run(
|
|
231
|
+
plant(
|
|
232
|
+
'named',
|
|
233
|
+
`${FIXTURE_IMPORT('EXIT_ALLOW')}process.exit(EXIT_ALLOW);\n`,
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
// The table defines the numerals; refusing them there would
|
|
239
|
+
// forbid the fix this gate exists to require. Exercised, not
|
|
240
|
+
// assumed — the plant writes a bare exit into it every time.
|
|
241
|
+
name: 'the table file is excluded even though it carries a numeral',
|
|
242
|
+
expect: 'accept',
|
|
243
|
+
run: () =>
|
|
244
|
+
run(
|
|
245
|
+
plant(
|
|
246
|
+
'table',
|
|
247
|
+
`${FIXTURE_IMPORT('EXIT_WARN')}process.exit(EXIT_WARN);\n`,
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _isCliEntry(): boolean {
|
|
256
|
+
const entry = process.argv[1];
|
|
257
|
+
return entry !== undefined && import.meta.url === pathToFileURL(entry).href;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (_isCliEntry()) {
|
|
261
|
+
process.exit(main());
|
|
262
|
+
}
|
|
@@ -716,6 +716,61 @@ export function _check_host_lowering(
|
|
|
716
716
|
}
|
|
717
717
|
}
|
|
718
718
|
}
|
|
719
|
+
_check_slot_answers(table, errors);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Every (host, slot) pair carries a date, and a citation to go with it.
|
|
724
|
+
*
|
|
725
|
+
* WHY AN ERROR AND NOT A WARNING. `block_exit: null` is the value the table
|
|
726
|
+
* carries on 29 of its 32 pairs, and until this check it meant two different
|
|
727
|
+
* things at once: "read the host's contract, it honours no refusal here" and
|
|
728
|
+
* "nobody has ever looked at this pair". Those are opposite epistemic states
|
|
729
|
+
* printed as the same byte, and the file's own header promises the first one
|
|
730
|
+
* ("`verified: null` means nobody established anything") without giving the
|
|
731
|
+
* per-slot layer any way to say it. `answered_at` is that way, and a soft
|
|
732
|
+
* warning would let the ambiguity back in one undated slot at a time — the
|
|
733
|
+
* exact drift the row-level `verified` gate was added to stop one level up.
|
|
734
|
+
*
|
|
735
|
+
* THE CITATION MAY BE INHERITED. A host publishes ONE hooks page, so requiring
|
|
736
|
+
* a per-slot `docs_url` would mean five copies of the same URL per host and
|
|
737
|
+
* five places for it to rot. The row's `verified.docs_url` is the citation
|
|
738
|
+
* unless the slot overrides it; what the check refuses is a dated answer with
|
|
739
|
+
* no reachable citation at all.
|
|
740
|
+
*
|
|
741
|
+
* A slot on a row with NO `verified` block is exempt from the citation half
|
|
742
|
+
* and not from the date: an undated row is already an error wherever it carries
|
|
743
|
+
* a blocking binding, and demanding a URL from a row that has none would report
|
|
744
|
+
* the same missing provenance twice in different words.
|
|
745
|
+
*/
|
|
746
|
+
export function _check_slot_answers(table: HostLowering, errors: string[]): void {
|
|
747
|
+
for (const [host, surfaces] of table) {
|
|
748
|
+
for (const [surface, row] of surfaces) {
|
|
749
|
+
for (const [slot, s] of row.slots) {
|
|
750
|
+
const where = `host_lowering ${host}/${surface}/${slot}`;
|
|
751
|
+
if (s.answered_at === null) {
|
|
752
|
+
errors.push(
|
|
753
|
+
`${where}: no \`answered_at\`. Every host-slot pair carries the date it was ` +
|
|
754
|
+
"last answered, so that a `block_exit: null` is a finding with a date on it " +
|
|
755
|
+
"rather than a slot nobody looked at.",
|
|
756
|
+
);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s.answered_at)) {
|
|
760
|
+
errors.push(`${where}: \`answered_at: ${s.answered_at}\` is not an ISO \`YYYY-MM-DD\` date.`);
|
|
761
|
+
}
|
|
762
|
+
// The row-level exemption the block comment above states: a row with no
|
|
763
|
+
// `verified` block already reports its missing provenance once, at row level.
|
|
764
|
+
if (row.verified !== null && s.docs_url === null && row.verified.docs_url === null) {
|
|
765
|
+
errors.push(
|
|
766
|
+
`${where}: dated \`${s.answered_at}\` with no citation — the slot carries no ` +
|
|
767
|
+
"`docs_url` and the row's `verified.docs_url` is absent. Cite the page the " +
|
|
768
|
+
"answer was read off, on the slot or on the row.",
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
719
774
|
}
|
|
720
775
|
|
|
721
776
|
export function lint(
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Hard-gate linter for the roadmap `## Blockers` contract
|
|
4
4
|
* (`templates/roadmaps.md` rule 20 / `roadmap-ci-steps-policy` siblings).
|
|
5
5
|
*
|
|
6
|
-
* Validates, for every active roadmap
|
|
6
|
+
* Validates, for every active roadmap and every parked idea one directory
|
|
7
|
+
* down in `stubs/`:
|
|
7
8
|
*
|
|
8
9
|
* 1. Every `### blocker: <id>` entry declares all five required fields
|
|
9
10
|
* (Status, Owner, Blocks, What to do, Resolved when).
|
|
@@ -22,11 +23,14 @@
|
|
|
22
23
|
* road-to-asked-not-parked exists to stop; without this field a marker
|
|
23
24
|
* cannot distinguish a declined decision from an unoffered one.
|
|
24
25
|
*
|
|
25
|
-
* HARD, not ratcheted, and it can be: no
|
|
26
|
-
* checkbox annotation today, so on the day it ships
|
|
27
|
-
* nothing — the same "no backlog to grandfather"
|
|
28
|
-
* contract above makes.
|
|
29
|
-
*
|
|
26
|
+
* HARD, not ratcheted, and it can be: no file in scope carries a real
|
|
27
|
+
* checkbox annotation pointing at a blocker today, so on the day it ships
|
|
28
|
+
* the rule fires on nothing — the same "no backlog to grandfather"
|
|
29
|
+
* argument the `Class:` contract above makes. Re-measured when `stubs/`
|
|
30
|
+
* entered scope on 2026-09-28: 7 stubs carry checkboxes and 0 of them
|
|
31
|
+
* carry a `blocked-by:` marker, so the rule stays latent there too.
|
|
32
|
+
* `later/`, `archive/` and `skipped/` are outside this gate's glob and
|
|
33
|
+
* are untouched by it; `stubs/` is inside it — see § SCOPE.
|
|
30
34
|
*
|
|
31
35
|
* 5. An entry that declares `- **Ownership:**` declares one of the three
|
|
32
36
|
* OWNER-OWNED classes (`product-owned`, `business-owned`,
|
|
@@ -41,10 +45,49 @@
|
|
|
41
45
|
* field, so on the day it ships no entry in the tree declares one and the
|
|
42
46
|
* rule fires on nothing.
|
|
43
47
|
*
|
|
48
|
+
* SCOPE — the active tree plus `stubs/`, and deliberately nothing else.
|
|
49
|
+
*
|
|
50
|
+
* Widened from the active tree alone by an owner ruling on 2026-09-28. Before
|
|
51
|
+
* it, a stub's hold was unreachable by every blocker gate: a parked idea is
|
|
52
|
+
* where a hold sits LONGEST, so excluding it exempted exactly the entries most
|
|
53
|
+
* likely to rot. The rejected alternative was to keep the scope and reword the
|
|
54
|
+
* acceptance criterion that named this gate — refused because a criterion
|
|
55
|
+
* edited to match what was achieved stops being an acceptance test.
|
|
56
|
+
*
|
|
57
|
+
* Measured on the tree the widening shipped against: 121 files under `stubs/`
|
|
58
|
+
* (120 stubs plus the directory README), of which 6 carry a `### blocker:`
|
|
59
|
+
* heading and 4 declare one open inside a `## Blockers` section. Zero hard
|
|
60
|
+
* violations, zero additions to the decidability ratchet, zero new
|
|
61
|
+
* active/archived overlaps. So it lands on nothing — the same "no backlog to
|
|
62
|
+
* grandfather" argument the `Class:` and `Ownership:` contracts make.
|
|
63
|
+
*
|
|
64
|
+
* REACH, stated honestly, because the obvious reading of the paragraph above
|
|
65
|
+
* is wrong: this does NOT hold every future stub blocker to the contract. Both
|
|
66
|
+
* scanners require the entry to sit under a `## Blockers` heading, and 2 of
|
|
67
|
+
* today's 6 put it under `## State` instead — so a third of the population is
|
|
68
|
+
* in the glob and still unread. Stub headings are free-form, unlike the
|
|
69
|
+
* template-driven roadmaps this gate was written for, so that is the likely
|
|
70
|
+
* shape rather than an edge case. Widening the glob fixes WHERE the gate looks
|
|
71
|
+
* and not WHAT it can parse; the second half is the sibling defect recorded in
|
|
72
|
+
* `stubs/road-to-blocker-parse-visibility.md`, and it is untouched here.
|
|
73
|
+
*
|
|
74
|
+
* `later/`, `archive/` and `skipped/` stay OUT, and the reason is not symmetry:
|
|
75
|
+
* those record decisions already taken (parked, closed, dropped), so a blocker
|
|
76
|
+
* left unresolved there is history rather than debt. A stub records a decision
|
|
77
|
+
* still to take. The polarity is pinned in both directions by
|
|
78
|
+
* `tests/scripts/lint_roadmap_blockers.test.ts` § the scanned scope, so
|
|
79
|
+
* widening further cannot happen by accident.
|
|
80
|
+
*
|
|
81
|
+
* One consequence worth naming rather than discovering: `_archiveOverlap`
|
|
82
|
+
* defaults its ACTIVE corpus to this glob, so a stub declaring a blocker open
|
|
83
|
+
* that an archived roadmap also declares open now fails. That is the same
|
|
84
|
+
* self-contradiction the assertion already caught one directory up, and it
|
|
85
|
+
* measured 0 on the widening tree.
|
|
86
|
+
*
|
|
44
87
|
* Fenced code blocks are stripped before scanning so a roadmap that shows
|
|
45
88
|
* the `## Blockers` shape as a documentation example is not flagged.
|
|
46
89
|
*
|
|
47
|
-
* Exit codes: 0 = clean /
|
|
90
|
+
* Exit codes: 0 = clean / nothing in scope, 1 = violations.
|
|
48
91
|
*/
|
|
49
92
|
|
|
50
93
|
import * as fs from 'node:fs';
|
|
@@ -58,7 +101,14 @@ const _HERE = fileURLToPath(import.meta.url);
|
|
|
58
101
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
59
102
|
|
|
60
103
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
61
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The scanned scope, for display. Two plain globs rather than one brace
|
|
106
|
+
* expression: nothing globs with this string, and a `{,stubs/}` empty
|
|
107
|
+
* alternative is bash-only syntax that would mislead anyone who tried.
|
|
108
|
+
*/
|
|
109
|
+
const ROADMAP_GLOB = 'agents/roadmaps/*.md + agents/roadmaps/stubs/*.md';
|
|
110
|
+
/** The one subdirectory inside the glob. See § SCOPE for why only this one. */
|
|
111
|
+
const SCANNED_SUBDIRS: readonly string[] = ['stubs'];
|
|
62
112
|
|
|
63
113
|
const FENCED_CODE_RE = /^[ \t]*```[^\n]*\n[\s\S]*?^[ \t]*```[ \t]*$/gm;
|
|
64
114
|
const BLOCKERS_SECTION_RE = /^##[ \t]+Blockers[ \t]*$/im;
|
|
@@ -370,13 +420,14 @@ function _scanBoth(rawText: string): ScanResult {
|
|
|
370
420
|
return { hard: violations, decidability };
|
|
371
421
|
}
|
|
372
422
|
|
|
373
|
-
/** Sorted
|
|
374
|
-
function
|
|
375
|
-
const dir = path.join(REPO_ROOT, 'agents', 'roadmaps');
|
|
423
|
+
/** Sorted `*.md` directly inside `dir` — never recursive. */
|
|
424
|
+
function _globFlat(dir: string): string[] {
|
|
376
425
|
let entries: fs.Dirent[];
|
|
377
426
|
try {
|
|
378
427
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
379
428
|
} catch {
|
|
429
|
+
// A missing directory is not an error: `stubs/` is optional, and the
|
|
430
|
+
// dead-scope assertion in `main` is what catches a MOVED root.
|
|
380
431
|
return [];
|
|
381
432
|
}
|
|
382
433
|
const out: string[] = [];
|
|
@@ -388,6 +439,21 @@ function _globRoadmaps(): string[] {
|
|
|
388
439
|
return out.sort();
|
|
389
440
|
}
|
|
390
441
|
|
|
442
|
+
/**
|
|
443
|
+
* The files this gate judges: active roadmaps, plus `stubs/`. See § SCOPE.
|
|
444
|
+
*
|
|
445
|
+
* Takes its root so the scope can be driven from a fixture tree. Asserting the
|
|
446
|
+
* rule against the real tree would only restate today's file list; the test
|
|
447
|
+
* pins which DIRECTORIES are in and which are out, in both directions.
|
|
448
|
+
*/
|
|
449
|
+
function _globRoadmaps(roadmapRoot: string = path.join(REPO_ROOT, 'agents', 'roadmaps')): string[] {
|
|
450
|
+
const out = _globFlat(roadmapRoot);
|
|
451
|
+
for (const sub of SCANNED_SUBDIRS) {
|
|
452
|
+
out.push(..._globFlat(path.join(roadmapRoot, sub)));
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
391
457
|
function _relPosix(target: string, root: string): string {
|
|
392
458
|
return path.relative(root, target).split(path.sep).join('/');
|
|
393
459
|
}
|
|
@@ -561,7 +627,7 @@ function main(): number {
|
|
|
561
627
|
const roadmaps = _globRoadmaps();
|
|
562
628
|
if (roadmaps.length === 0) {
|
|
563
629
|
if (!QUIET) {
|
|
564
|
-
process.stdout.write(`✅
|
|
630
|
+
process.stdout.write(`✅ nothing in scope under ${ROADMAP_GLOB}\n`);
|
|
565
631
|
}
|
|
566
632
|
return 0;
|
|
567
633
|
}
|
|
@@ -69,6 +69,22 @@ export interface SkillPrompt {
|
|
|
69
69
|
readonly kind: PromptKind;
|
|
70
70
|
readonly recommended_for_user_types: readonly string[];
|
|
71
71
|
readonly user_type_match: UserTypeMatch;
|
|
72
|
+
/**
|
|
73
|
+
* Declared parameters, from the frontmatter `inputs:` block.
|
|
74
|
+
*
|
|
75
|
+
* Empty for every artifact that declares none, which is the whole corpus
|
|
76
|
+
* today — so `arguments: []` on the wire stays the behaviour for an
|
|
77
|
+
* undeclared prompt rather than becoming a special case
|
|
78
|
+
* (road-to-an-invocation-contract-that-reaches-the-wire 3.2).
|
|
79
|
+
*/
|
|
80
|
+
readonly inputs: readonly PromptInput[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One declared parameter, reduced to what the MCP `arguments` shape carries. */
|
|
84
|
+
export interface PromptInput {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly description: string;
|
|
87
|
+
readonly required: boolean;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
/** Walk up from this file to the repo root (parent of `scripts/`). */
|
|
@@ -92,6 +108,124 @@ export function _project_root(): string {
|
|
|
92
108
|
* Avoids a YAML dependency for Phase 1; the frontmatter shape is
|
|
93
109
|
* enforced by `task lint-skills` upstream.
|
|
94
110
|
*/
|
|
111
|
+
/** The raw frontmatter block of a document, or `''` when it has none. */
|
|
112
|
+
export function _frontmatter_text(text: string): string {
|
|
113
|
+
if (!text.startsWith('---\n')) return '';
|
|
114
|
+
const parts = _splitMax(text, '---\n', 2);
|
|
115
|
+
return parts.length < 3 ? '' : (parts[1] as string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse the frontmatter `inputs:` block out of raw frontmatter text.
|
|
120
|
+
*
|
|
121
|
+
* WHY THIS EXISTS RATHER THAN A YAML CALL. `_strip_frontmatter` is a flat
|
|
122
|
+
* `key: value` line scanner and cannot represent a nested list — `inputs:`
|
|
123
|
+
* would land as an empty string and its `- name: x` lines as stray keys. The
|
|
124
|
+
* MCP server is deliberately stdlib-only and is bundled for the wire, so
|
|
125
|
+
* pulling a YAML parser in to read one optional block would pay a dependency
|
|
126
|
+
* on every install for a block almost nothing declares yet.
|
|
127
|
+
*
|
|
128
|
+
* IT READS ONE SHAPE AND REFUSES THE REST. Accepted:
|
|
129
|
+
*
|
|
130
|
+
* inputs:
|
|
131
|
+
* - name: target
|
|
132
|
+
* type: string
|
|
133
|
+
* required: true
|
|
134
|
+
* description: What to act on.
|
|
135
|
+
*
|
|
136
|
+
* Refused, by yielding NOTHING for the whole block: the flow form
|
|
137
|
+
* (`inputs: [{name: a}]`), a block scalar (`description: >`), and any nested
|
|
138
|
+
* mapping under an item key. Refusing is not silent — `check_inputs_parity`
|
|
139
|
+
* compares this reader against a real YAML parse for every declaring artifact
|
|
140
|
+
* and FAILS the build on any difference, so an unsupported form is a red gate
|
|
141
|
+
* with a named remedy rather than a wrong argument on the wire.
|
|
142
|
+
*
|
|
143
|
+
* That gate is the answer to the defect an independent review found here: the
|
|
144
|
+
* key scan used to run at any depth, so a `default:` MAPPING whose first child
|
|
145
|
+
* was `name:` overwrote the parameter's own name and the server offered a host
|
|
146
|
+
* an argument that did not exist. The header promised "no guessed input" while
|
|
147
|
+
* the code guessed. Depth is now tracked, and where this reader cannot be sure
|
|
148
|
+
* it declines and the parity gate makes the decline visible.
|
|
149
|
+
*/
|
|
150
|
+
export function _parse_inputs(fm: string): PromptInput[] {
|
|
151
|
+
const lines = fm.split('\n');
|
|
152
|
+
let i = lines.findIndex((l) => /^inputs:/.test(l));
|
|
153
|
+
if (i === -1) return [];
|
|
154
|
+
// The flow form carries its payload on the `inputs:` line itself. This
|
|
155
|
+
// reader does not parse it; the parity gate turns that into a failure.
|
|
156
|
+
if (!/^inputs:\s*$/.test(lines[i] as string)) return [];
|
|
157
|
+
|
|
158
|
+
const out: PromptInput[] = [];
|
|
159
|
+
let current: { name?: string; description?: string; required?: boolean } | null = null;
|
|
160
|
+
/** Indent of the key lines belonging to the item being read. */
|
|
161
|
+
let keyIndent: number | null = null;
|
|
162
|
+
let refused = false;
|
|
163
|
+
|
|
164
|
+
const flush = (): void => {
|
|
165
|
+
if (current?.name !== undefined) {
|
|
166
|
+
out.push({
|
|
167
|
+
name: current.name,
|
|
168
|
+
description: current.description ?? '',
|
|
169
|
+
required: current.required === true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
current = null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const clean = (raw: string): string => {
|
|
176
|
+
const v = raw.trim();
|
|
177
|
+
// An unquoted trailing comment is not part of the value — the same strip
|
|
178
|
+
// `_strip_frontmatter` applies, kept consistent deliberately.
|
|
179
|
+
const hashed = /^(?!["'])([^#]*?)\s+#.*$/.exec(v);
|
|
180
|
+
return _stripQuotes((hashed === null ? v : (hashed[1] ?? '')).trim());
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
for (i += 1; i < lines.length && !refused; i += 1) {
|
|
184
|
+
const line = lines[i] as string;
|
|
185
|
+
if (line.trim() === '') continue;
|
|
186
|
+
// A comment at any column is not block content and does not end the block.
|
|
187
|
+
if (/^\s*#/.test(line)) continue;
|
|
188
|
+
// A non-indented line ends the block.
|
|
189
|
+
if (!/^\s/.test(line)) break;
|
|
190
|
+
|
|
191
|
+
const indent = (/^(\s*)/.exec(line) as RegExpExecArray)[1]!.replace(/\t/g, ' ').length;
|
|
192
|
+
|
|
193
|
+
const item = /^\s*-\s+([A-Za-z_]\w*):\s*(.*)$/.exec(line);
|
|
194
|
+
if (item !== null) {
|
|
195
|
+
flush();
|
|
196
|
+
current = {};
|
|
197
|
+
keyIndent = line.indexOf('-') + 2;
|
|
198
|
+
if (item[1] === 'name') current.name = clean(item[2] ?? '');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const kv = /^\s+([A-Za-z_]\w*):\s*(.*)$/.exec(line);
|
|
203
|
+
if (kv === null || current === null || keyIndent === null) continue;
|
|
204
|
+
// Deeper than the item's own keys: a nested mapping or sequence this
|
|
205
|
+
// reader does not model. Decline the whole block rather than absorb it.
|
|
206
|
+
if (indent > keyIndent) {
|
|
207
|
+
refused = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
const key = kv[1] as string;
|
|
211
|
+
const rawValue = (kv[2] ?? '').trim();
|
|
212
|
+
// A block scalar or an empty value means the real content is on the
|
|
213
|
+
// following lines, which this reader does not join.
|
|
214
|
+
if (rawValue === '' || rawValue === '>' || rawValue === '|') {
|
|
215
|
+
refused = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
const value = clean(rawValue);
|
|
219
|
+
if (key === 'name') current.name = value;
|
|
220
|
+
else if (key === 'description') current.description = value;
|
|
221
|
+
else if (key === 'required') current.required = value.toLowerCase() === 'true';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (refused) return [];
|
|
225
|
+
flush();
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
95
229
|
export function _strip_frontmatter(text: string): [Record<string, string>, string] {
|
|
96
230
|
if (!text.startsWith('---\n')) {
|
|
97
231
|
return [{}, text];
|
|
@@ -365,6 +499,7 @@ function _load_file(p: string, kind: PromptKind, fallback_name: string): SkillPr
|
|
|
365
499
|
kind,
|
|
366
500
|
recommended_for_user_types: _parse_inline_array(meta.recommended_for_user_types ?? ''),
|
|
367
501
|
user_type_match: '',
|
|
502
|
+
inputs: _parse_inputs(_frontmatter_text(text)),
|
|
368
503
|
};
|
|
369
504
|
}
|
|
370
505
|
|
|
@@ -548,7 +683,13 @@ export function to_mcp_prompt_meta(prompt: SkillPrompt): Record<string, unknown>
|
|
|
548
683
|
name: wire,
|
|
549
684
|
title: prompt.name,
|
|
550
685
|
description: prompt.description,
|
|
551
|
-
|
|
686
|
+
// Derived from the declaration, never invented. An artifact that
|
|
687
|
+
// declares nothing still serves `[]` — see `_parse_inputs`.
|
|
688
|
+
arguments: prompt.inputs.map((i) => ({
|
|
689
|
+
name: i.name,
|
|
690
|
+
description: i.description,
|
|
691
|
+
required: i.required,
|
|
692
|
+
})),
|
|
552
693
|
_meta: meta,
|
|
553
694
|
};
|
|
554
695
|
}
|