@event4u/agent-config 9.22.0 → 9.24.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.
Files changed (90) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +84 -238
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/agent-handoff.md +10 -3
  5. package/dist/agent-src/commands/analyze/inbox.md +190 -0
  6. package/dist/agent-src/commands/analyze.md +4 -3
  7. package/dist/agent-src/commands/optimize/deep.md +13 -0
  8. package/dist/agent-src/commands/pr/create.md +6 -6
  9. package/dist/agent-src/contexts/execution/cheap-question-mechanics.md +12 -2
  10. package/dist/agent-src/contexts/execution/mandated-lines.md +180 -0
  11. package/dist/agent-src/rules/context-hygiene.md +20 -0
  12. package/dist/agent-src/rules/downstream-changes.md +26 -0
  13. package/dist/agent-src/rules/think-before-action.md +25 -0
  14. package/dist/agent-src/rules/token-budget-discipline.md +46 -3
  15. package/dist/agent-src/rules/token-efficiency.md +13 -0
  16. package/dist/agent-src/rules/ui-audit-gate.md +33 -2
  17. package/dist/agent-src/skills/adversarial-review/SKILL.md +18 -4
  18. package/dist/agent-src/skills/decision-review/SKILL.md +49 -0
  19. package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +5 -1
  20. package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +16 -0
  21. package/dist/agent-src/skills/judge-synthesis/SKILL.md +14 -1
  22. package/dist/agent-src/skills/rule-writing/SKILL.md +33 -0
  23. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +31 -0
  24. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  25. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  26. package/dist/cli/registry.js +1 -0
  27. package/dist/cli/registry.js.map +1 -1
  28. package/dist/cli-delegate/chunk-DVRS2KVR.js +4602 -0
  29. package/dist/cli-delegate/{chunk-ELMFOQPW.js → chunk-SJQMJI7M.js} +8 -4597
  30. package/dist/cli-delegate/cmd_export.js +2 -1
  31. package/dist/cli-delegate/cmd_prune.js +2 -1
  32. package/dist/cli-delegate/cmd_settings_migrate.js +2 -1
  33. package/dist/cli-delegate/cmd_settings_set.js +3370 -0
  34. package/dist/cli-delegate/cmd_sync.js +2 -1
  35. package/dist/cli-delegate/cmd_uninstall.js +2 -1
  36. package/dist/cli-delegate/cmd_validate.js +2 -1
  37. package/dist/discovery/deprecation-report.md +1 -1
  38. package/dist/discovery/discovery-manifest.json +54 -27
  39. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  40. package/dist/discovery/discovery-manifest.summary.md +3 -3
  41. package/dist/discovery/orphan-report.md +1 -1
  42. package/dist/discovery/packs.json +6 -5
  43. package/dist/discovery/trust-report.md +2 -2
  44. package/dist/discovery/workspaces.json +5 -4
  45. package/dist/hooks/dispatch.js +79 -1
  46. package/dist/mcp/registry-manifest.json +3 -3
  47. package/dist/mcp/server.json +2 -2
  48. package/dist/scripts/condense.js +47 -2
  49. package/dist/scripts/condense.js.map +1 -1
  50. package/dist/server/routes/settings.js +82 -0
  51. package/dist/server/routes/settings.js.map +1 -1
  52. package/dist/shared/settingsClasses.js +191 -0
  53. package/dist/shared/settingsClasses.js.map +1 -0
  54. package/docs/contracts/settings-classes.md +342 -0
  55. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +23 -2
  56. package/docs/guidelines/agent-infra/false-green.md +187 -0
  57. package/docs/guidelines/agent-infra/gate-authoring.md +177 -0
  58. package/package.json +3 -1
  59. package/src/config/ci-local-parity.yml +12 -0
  60. package/src/config/evaluator-budgets.json +4 -4
  61. package/src/config/gate-coverage.yml +54 -12
  62. package/src/config/gate-violation-baselines.json +6 -0
  63. package/src/config/pack-size-budget.json +4 -3
  64. package/src/scripts/_cli/cmd_settings_set.ts +509 -0
  65. package/src/scripts/_dispatch.bash +16 -0
  66. package/src/scripts/_lib/gate_ledger.ts +295 -0
  67. package/src/scripts/_lib/gate_result.ts +101 -0
  68. package/src/scripts/_lib/gate_self_test.ts +129 -0
  69. package/src/scripts/_lib/measured_render.ts +97 -0
  70. package/src/scripts/_lib/ratchet_base_ref.ts +373 -0
  71. package/src/scripts/check_gate_completeness.ts +244 -0
  72. package/src/scripts/check_gate_coverage.ts +35 -6
  73. package/src/scripts/check_references.ts +125 -15
  74. package/src/scripts/check_review_dispositions.ts +14 -1
  75. package/src/scripts/check_suppression_hygiene.ts +503 -0
  76. package/src/scripts/condense.ts +47 -2
  77. package/src/scripts/hook_manifest.yaml +23 -0
  78. package/src/scripts/hooks/dispatch_hook.ts +65 -1
  79. package/src/scripts/hooks/host_semantics.ts +140 -0
  80. package/src/scripts/lint_framework_leakage.ts +147 -15
  81. package/src/scripts/lint_mandated_lines.ts +339 -0
  82. package/src/scripts/lint_roadmap_complexity.ts +102 -11
  83. package/src/scripts/lint_roadmap_family_cap.ts +191 -0
  84. package/src/scripts/lint_settings_classes.ts +397 -0
  85. package/src/scripts/lint_token_budget_discipline.ts +109 -1
  86. package/src/scripts/measure_rule_absoluta.ts +231 -0
  87. package/src/scripts/report_imperative_density.ts +229 -0
  88. package/src/scripts/verify_physical_move.ts +10 -1
  89. package/src/server/io/yamlIO.ts +173 -0
  90. package/src/shared/settingsClasses.ts +235 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * YAML round-trip helpers for `.agent-settings.yml`.
3
+ *
4
+ * Important contract: the template ships with comments that explain
5
+ * every key — we MUST NOT discard them when the wizard writes back.
6
+ * `js-yaml.dump` cannot preserve comments, so we use a key-level
7
+ * replace strategy that mirrors `scripts/install.py::_replace_template_value`:
8
+ * scan the rendered template line-by-line, replace only the scalar
9
+ * value at each dotted path, leave comments and indentation alone.
10
+ *
11
+ * For arrays and nested objects we fall back to re-serializing the
12
+ * subtree (`js-yaml.dump`) because line-level replace can't safely
13
+ * splice multi-line blocks. The form renderer keeps depth ≤ 2, so
14
+ * the subtree footprint is small.
15
+ */
16
+
17
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
18
+
19
+ export interface ReadResult {
20
+ values: Record<string, unknown>;
21
+ raw: string;
22
+ }
23
+
24
+ export function parseYaml(raw: string): Record<string, unknown> {
25
+ const parsed = yamlLoad(raw);
26
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
27
+ throw new Error('settings file did not parse to an object');
28
+ }
29
+ return parsed as Record<string, unknown>;
30
+ }
31
+
32
+ function formatScalar(value: unknown): string {
33
+ if (typeof value === 'string') {
34
+ // js-yaml safely quotes strings that need it; trim the trailing newline.
35
+ return yamlDump(value, { lineWidth: -1 }).replace(/\n$/, '').trim();
36
+ }
37
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
38
+ if (value === null || value === undefined) return '""';
39
+ // Arrays / nested objects: dump inline. Form keeps depth ≤ 2 so this is rare.
40
+ return yamlDump(value, { lineWidth: -1, flowLevel: 0 }).replace(/\n$/, '').trim();
41
+ }
42
+
43
+ interface FlatEntry {
44
+ path: string[];
45
+ value: unknown;
46
+ }
47
+
48
+ function flatten(value: unknown, prefix: string[] = []): FlatEntry[] {
49
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
50
+ return [{ path: prefix, value }];
51
+ }
52
+ const entries: FlatEntry[] = [];
53
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
54
+ entries.push(...flatten(v, [...prefix, k]));
55
+ }
56
+ return entries;
57
+ }
58
+
59
+ /**
60
+ * Replace the scalar at `dottedPath` in `template` with `value`.
61
+ * Comments and unrelated keys are preserved verbatim. Returns the
62
+ * template unchanged when the path cannot be located (matches the
63
+ * Python installer's tolerant behaviour).
64
+ */
65
+ export function replaceScalar(template: string, dottedPath: string[], value: unknown): string {
66
+ if (dottedPath.length === 0) return template;
67
+ const sections = dottedPath.slice(0, -1);
68
+ const key = dottedPath[dottedPath.length - 1];
69
+ const targetIndent = ' '.repeat(sections.length);
70
+
71
+ const lines = template.split('\n');
72
+ const currentPath: (string | null)[] = new Array<string | null>(sections.length).fill(null);
73
+ const formatted = formatScalar(value);
74
+
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const line = lines[i];
77
+ if (line === undefined) continue;
78
+ const stripped = line.trim();
79
+ if (stripped === '' || stripped.startsWith('#')) continue;
80
+ const indentLen = line.length - line.trimStart().length;
81
+ if (indentLen % 2 !== 0) continue;
82
+ const level = indentLen / 2;
83
+ if (level > sections.length) continue;
84
+
85
+ const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(stripped);
86
+ if (!m) continue;
87
+ const lineKey = m[1];
88
+ if (lineKey === undefined) continue;
89
+ if (level < sections.length) {
90
+ if (lineKey === sections[level]) {
91
+ currentPath[level] = lineKey;
92
+ for (let j = level + 1; j < currentPath.length; j++) currentPath[j] = null;
93
+ } else if (currentPath[level] !== null && lineKey !== currentPath[level]) {
94
+ currentPath[level] = null;
95
+ }
96
+ continue;
97
+ }
98
+ // level === sections.length → leaf candidate
99
+ const parentsMatch = sections.every((s, idx) => currentPath[idx] === s);
100
+ if (!parentsMatch) continue;
101
+ if (lineKey !== key) continue;
102
+ lines[i] = `${targetIndent}${key}: ${formatted}`;
103
+ return lines.join('\n');
104
+ }
105
+ return template;
106
+ }
107
+
108
+ /**
109
+ * Apply every leaf change from `newValues` to `templateBody`. Paths that
110
+ * are not present in the template are appended at the end. Comments are
111
+ * preserved for every key that already exists in the template.
112
+ */
113
+ export function mergeIntoTemplate(templateBody: string, newValues: Record<string, unknown>): string {
114
+ let body = templateBody;
115
+ const appended: string[] = [];
116
+ for (const entry of flatten(newValues)) {
117
+ const before = body;
118
+ body = replaceScalar(body, entry.path, entry.value);
119
+ if (body === before) {
120
+ // Path is not in the template — append a flat key=value at EOF.
121
+ // Form coverage is asserted by parity test, so this branch only
122
+ // fires on hand-edited templates.
123
+ appended.push(`${entry.path.join('.')}: ${formatScalar(entry.value)}`);
124
+ }
125
+ }
126
+ if (appended.length > 0) {
127
+ if (!body.endsWith('\n')) body += '\n';
128
+ body += `\n# Wizard-added keys (no template entry)\n${appended.join('\n')}\n`;
129
+ }
130
+ return body;
131
+ }
132
+
133
+ export function diffValues(
134
+ before: Record<string, unknown>,
135
+ after: Record<string, unknown>,
136
+ ): Array<{ path: string; from: unknown; to: unknown }> {
137
+ const out: Array<{ path: string; from: unknown; to: unknown }> = [];
138
+ const bMap = new Map(flatten(before).map((e) => [e.path.join('.'), e.value]));
139
+ const aMap = new Map(flatten(after).map((e) => [e.path.join('.'), e.value]));
140
+ const paths = new Set<string>([...bMap.keys(), ...aMap.keys()]);
141
+ for (const path of paths) {
142
+ const fromV = bMap.get(path);
143
+ const toV = aMap.get(path);
144
+ if (JSON.stringify(fromV) !== JSON.stringify(toV)) out.push({ path, from: fromV, to: toV });
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
150
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
151
+ }
152
+
153
+ /**
154
+ * Recursive dict merge — overlay wins, nested dicts are merged, lists
155
+ * are replaced (not concatenated). Mirrors `scripts/install.py::deep_merge`
156
+ * so the Python installer and the Fastify server produce the same
157
+ * three-layer settings tree (`defaults < global < project`).
158
+ */
159
+ export function deepMerge(
160
+ base: Record<string, unknown>,
161
+ overlay: Record<string, unknown>,
162
+ ): Record<string, unknown> {
163
+ const result: Record<string, unknown> = { ...base };
164
+ for (const [key, value] of Object.entries(overlay)) {
165
+ const existing = result[key];
166
+ if (isPlainObject(existing) && isPlainObject(value)) {
167
+ result[key] = deepMerge(existing, value);
168
+ } else {
169
+ result[key] = value;
170
+ }
171
+ }
172
+ return result;
173
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Settings A/B/C classes — the pure half.
3
+ *
4
+ * `docs/contracts/settings-classes.md` is the single source: one markdown table
5
+ * row per template leaf, each carrying the class that decides **who may write
6
+ * that key**. This module parses that table and nothing else. There is
7
+ * deliberately no generated JSON twin — a second machine-readable copy is a
8
+ * second thing to drift, and the whole point of the contract is that the fence
9
+ * and the prose explaining it cannot disagree.
10
+ *
11
+ * Pure module — no I/O. Consumed by the lint (`src/scripts/lint_settings_classes.ts`),
12
+ * the `settings set` writer, and the Fastify settings route, so all three
13
+ * refuse the same keys for the same reason.
14
+ *
15
+ * @see docs/contracts/settings-classes.md
16
+ */
17
+
18
+ /** The closed class vocabulary. A row outside it is a defect, not a new class. */
19
+ export const SETTINGS_CLASSES = ['A', 'B', 'C'] as const;
20
+
21
+ export type SettingsClass = (typeof SETTINGS_CLASSES)[number];
22
+
23
+ export function isSettingsClass(value: string): value is SettingsClass {
24
+ return (SETTINGS_CLASSES as readonly string[]).includes(value);
25
+ }
26
+
27
+ /** One parsed row of the contract's key table. */
28
+ export interface SettingsClassRow {
29
+ /** Dotted leaf path, e.g. `personal.autonomy`. */
30
+ key: string;
31
+ /** Raw class cell — NOT yet validated, so the lint can report a bad one. */
32
+ cls: string;
33
+ /** 1-indexed line in the contract, so a finding points at something. */
34
+ line: number;
35
+ }
36
+
37
+ /** What the contract's own `## Counts` table claims about itself. */
38
+ export interface DeclaredClassCounts {
39
+ A: number | null;
40
+ B: number | null;
41
+ C: number | null;
42
+ total: number | null;
43
+ }
44
+
45
+ /**
46
+ * `| \`some.key\` | C | \`false\` | why |` → one row.
47
+ *
48
+ * The class column is what separates the key table from the prose tables above
49
+ * it, several of which also open with a backticked key. A second cell that is
50
+ * not a single bare letter is not a key row.
51
+ */
52
+ const ROW_RE = /^\|\s*`([^`]+)`\s*\|\s*([^|]*?)\s*\|/;
53
+
54
+ export function parseSettingsClassRows(markdown: string): SettingsClassRow[] {
55
+ const rows: SettingsClassRow[] = [];
56
+ const lines = markdown.split('\n');
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const m = ROW_RE.exec(lines[i] ?? '');
59
+ if (m === null) {
60
+ continue;
61
+ }
62
+ const key = m[1] ?? '';
63
+ const cls = m[2] ?? '';
64
+ if (!/^[A-Za-z]$/.test(cls)) {
65
+ continue;
66
+ }
67
+ rows.push({ key, cls, line: i + 1 });
68
+ }
69
+ return rows;
70
+ }
71
+
72
+ /** `| A — preference | 27 |` and `| **Total** | **140** |` → the declared tallies. */
73
+ export function parseDeclaredClassCounts(markdown: string): DeclaredClassCounts {
74
+ const read = (re: RegExp): number | null => {
75
+ const m = re.exec(markdown);
76
+ const raw = m?.[1];
77
+ return raw === undefined ? null : Number.parseInt(raw, 10);
78
+ };
79
+ return {
80
+ A: read(/^\|\s*A\s+—\s+preference\s*\|\s*(\d+)\s*\|/m),
81
+ B: read(/^\|\s*B\s+—\s+consent\s*\|\s*(\d+)\s*\|/m),
82
+ C: read(/^\|\s*C\s+—\s+guarded\s*\|\s*(\d+)\s*\|/m),
83
+ total: read(/^\|\s*\*\*Total\*\*\s*\|\s*\*\*(\d+)\*\*\s*\|/m),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Dotted key → class, for the rows whose class is valid.
89
+ *
90
+ * A row with an unparseable class is DROPPED rather than defaulted, so a
91
+ * caller asking "may I write this?" gets `undefined` — which every caller
92
+ * treats as refuse. A typo in the contract must never widen the fence.
93
+ */
94
+ export function buildSettingsClassIndex(rows: readonly SettingsClassRow[]): Map<string, SettingsClass> {
95
+ const index = new Map<string, SettingsClass>();
96
+ for (const row of rows) {
97
+ if (!isSettingsClass(row.cls) || index.has(row.key)) {
98
+ continue;
99
+ }
100
+ index.set(row.key, row.cls);
101
+ }
102
+ return index;
103
+ }
104
+
105
+ /**
106
+ * Conservative-default test for class B (half one of the B invariant).
107
+ *
108
+ * A sparse settings file means absent = default. A B key whose default is the
109
+ * permissive value makes "never asked" indistinguishable from "answered yes",
110
+ * which turns the ask into decoration on a decision already taken in the
111
+ * user's name. These are the values that cannot carry a permission.
112
+ *
113
+ * Half two of the invariant — that the ask fires on the user's own request and
114
+ * never at a moment the agent chose — is prose in the contract, not code. No
115
+ * gate can read the agent's reason for picking a moment.
116
+ */
117
+ export function isConservativeDefault(value: unknown): boolean {
118
+ if (value === null || value === false || value === '' || value === 0) {
119
+ return true;
120
+ }
121
+ if (Array.isArray(value)) {
122
+ return value.length === 0;
123
+ }
124
+ if (typeof value === 'object' && value !== null) {
125
+ return Object.keys(value).length === 0;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ /**
131
+ * Dotted leaf paths of a parsed settings tree.
132
+ *
133
+ * A leaf is anything that is not a NON-EMPTY map. An empty map is a real
134
+ * configurable value with a real default (`subagents.host_capabilities: {}`),
135
+ * so it is a leaf — one more than the template↔schema parity walk produces.
136
+ * That divergence is deliberate: a key the parity walk skips is exactly the
137
+ * key that would otherwise reach the writer with no class.
138
+ */
139
+ export function settingsLeafPaths(value: unknown, prefix = ''): string[] {
140
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
141
+ return [prefix];
142
+ }
143
+ const entries = Object.entries(value as Record<string, unknown>);
144
+ if (entries.length === 0) {
145
+ return [prefix];
146
+ }
147
+ return entries.flatMap(([k, v]) => settingsLeafPaths(v, prefix === '' ? k : `${prefix}.${k}`));
148
+ }
149
+
150
+ /** Read one dotted path out of a parsed settings tree; `undefined` when absent. */
151
+ export function getSettingsLeaf(root: unknown, dotted: string): unknown {
152
+ let node: unknown = root;
153
+ for (const part of dotted.split('.')) {
154
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) {
155
+ return undefined;
156
+ }
157
+ node = (node as Record<string, unknown>)[part];
158
+ }
159
+ return node;
160
+ }
161
+
162
+ /**
163
+ * The class governing a dotted path, walking to the nearest classified ancestor.
164
+ *
165
+ * An exact row wins. Failing that, the longest classified prefix does: a
166
+ * class-C key whose value is a MAP has children that never appear in a flattened
167
+ * diff under the key's own name, so `subagents.host_capabilities.subagent_spawn`
168
+ * must inherit the C on `subagents.host_capabilities` rather than read as
169
+ * unclassified. Returns `undefined` when nothing on the path is classified —
170
+ * and every caller treats that as guarded, not as free.
171
+ */
172
+ export function classOfPath(
173
+ index: ReadonlyMap<string, SettingsClass>,
174
+ dotted: string,
175
+ ): SettingsClass | undefined {
176
+ const exact = index.get(dotted);
177
+ if (exact !== undefined) {
178
+ return exact;
179
+ }
180
+ const parts = dotted.split('.');
181
+ for (let end = parts.length - 1; end > 0; end--) {
182
+ const ancestor = index.get(parts.slice(0, end).join('.'));
183
+ if (ancestor !== undefined) {
184
+ return ancestor;
185
+ }
186
+ }
187
+ return undefined;
188
+ }
189
+
190
+ /** Absent / null / empty-scalar — the three spellings of "not set" in a settings file. */
191
+ export function isUnsetSettingValue(value: unknown): boolean {
192
+ return value === undefined || value === null || value === '';
193
+ }
194
+
195
+ /** One entry of a settings diff, as `diffValues` produces them. */
196
+ export interface SettingsChange {
197
+ path: string;
198
+ from: unknown;
199
+ to: unknown;
200
+ }
201
+
202
+ /**
203
+ * The changed paths a human must confirm before the write lands.
204
+ *
205
+ * Extracted from the Fastify write route so the fail-closed branch is testable
206
+ * without booting a server around it. That branch is the one a single inverted
207
+ * operator turns into a fail-OPEN, and a gate whose most dangerous line has no
208
+ * test is a gate that passes review once and rots after.
209
+ *
210
+ * `classes === null` means the contract could not be read. Every changed key is
211
+ * then guarded: unverifiable is not the same as unguarded. With a contract in
212
+ * hand, a key is guarded when its nearest classified ancestor is C, and ALSO
213
+ * when nothing on its path is classified at all — the same per-key refusal the
214
+ * CLI writer applies.
215
+ *
216
+ * Pairs where both sides are unset are dropped first: a key written as `null`
217
+ * reads back as `''` through the comment-preserving merge, and a confirmation
218
+ * that appears on every save is one nobody reads.
219
+ */
220
+ export function guardedChangedKeys(
221
+ classes: ReadonlyMap<string, SettingsClass> | null,
222
+ changed: readonly SettingsChange[],
223
+ ): string[] {
224
+ return changed
225
+ .filter((c) => !(isUnsetSettingValue(c.from) && isUnsetSettingValue(c.to)))
226
+ .map((c) => c.path)
227
+ .filter((key) => {
228
+ if (classes === null) {
229
+ return true;
230
+ }
231
+ const cls = classOfPath(classes, key);
232
+ return cls === 'C' || cls === undefined;
233
+ })
234
+ .sort();
235
+ }