@jagilber-org/index-server 1.28.9 → 1.28.19

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 (75) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/CONTRIBUTING.md +13 -0
  3. package/README.md +10 -14
  4. package/dist/config/featureConfig.js +4 -1
  5. package/dist/dashboard/client/admin.html +69 -29
  6. package/dist/dashboard/client/js/admin.embeddings.js +97 -5
  7. package/dist/dashboard/client/js/admin.instructions.js +1 -1
  8. package/dist/dashboard/server/AdminPanel.js +38 -0
  9. package/dist/dashboard/server/ApiRoutes.js +14 -1
  10. package/dist/dashboard/server/routes/embeddings.routes.js +76 -1
  11. package/dist/dashboard/server/routes/instructions.routes.js +4 -11
  12. package/dist/dashboard/server/routes/scripts.routes.js +35 -10
  13. package/dist/dashboard/server/routes/status.routes.js +77 -0
  14. package/dist/models/instruction.d.ts +2 -1
  15. package/dist/models/instruction.js +2 -0
  16. package/dist/schemas/index-server.code-schema.json +52478 -0
  17. package/dist/schemas/index.d.ts +7 -164
  18. package/dist/schemas/index.js +45 -63
  19. package/dist/schemas/instructionSchema.d.ts +46 -0
  20. package/dist/schemas/instructionSchema.js +159 -0
  21. package/{schemas → dist/schemas}/json-schema/instruction-content-type.schema.json +6 -4
  22. package/{schemas → dist/schemas}/json-schema/instruction-instruction-entry.schema.json +6 -4
  23. package/dist/schemas/manifest.json +78 -0
  24. package/dist/server/index-server.js +7 -1
  25. package/dist/services/bootstrapGating.js +2 -2
  26. package/dist/services/handlers/instructions.add.js +18 -0
  27. package/dist/services/handlers/instructions.groom.js +6 -1
  28. package/dist/services/handlers/instructions.import.js +42 -7
  29. package/dist/services/handlers.activation.js +3 -1
  30. package/dist/services/handlers.dashboardConfig.js +2 -1
  31. package/dist/services/handlers.feedback.d.ts +4 -4
  32. package/dist/services/handlers.feedback.js +390 -27
  33. package/dist/services/handlers.instructionSchema.js +73 -31
  34. package/dist/services/handlers.search.js +11 -6
  35. package/dist/services/indexLoader.js +7 -0
  36. package/dist/services/instructionRecordValidation.js +32 -84
  37. package/dist/services/mcpConfig/flagCatalog.d.ts +1 -1
  38. package/dist/services/mcpConfig/flagCatalog.js +2 -0
  39. package/dist/services/mcpConfig/formats.js +2 -6
  40. package/dist/services/messaging/agentMailbox.d.ts +6 -1
  41. package/dist/services/messaging/agentMailbox.js +10 -3
  42. package/dist/services/seedBootstrap.contentModel.d.ts +13 -0
  43. package/dist/services/seedBootstrap.contentModel.js +166 -0
  44. package/dist/services/seedBootstrap.contentTypes.d.ts +5 -0
  45. package/dist/services/seedBootstrap.contentTypes.js +76 -0
  46. package/dist/services/seedBootstrap.d.ts +1 -0
  47. package/dist/services/seedBootstrap.js +101 -15
  48. package/dist/services/toolRegistry.js +52 -24
  49. package/dist/services/toolRegistry.zod.js +84 -37
  50. package/dist/versioning/schemaVersion.d.ts +1 -1
  51. package/dist/versioning/schemaVersion.js +1 -13
  52. package/package.json +17 -3
  53. package/schemas/index-server.code-schema.json +31019 -25047
  54. package/schemas/instruction.schema.json +16 -6
  55. package/schemas/manifest.json +3 -3
  56. package/scripts/README.md +20 -0
  57. package/scripts/build/README.md +41 -0
  58. package/scripts/build/setup-wizard-paths.mjs +27 -0
  59. package/scripts/build/setup-wizard.mjs +7 -21
  60. package/scripts/client/README.md +26 -0
  61. package/scripts/client/index-server-client.ps1 +203 -0
  62. package/scripts/client/index-server-client.sh +149 -0
  63. package/scripts/client/powershell-mcp-server.ps1 +83 -0
  64. package/scripts/client/powershell-mcp-template.ps1 +85 -0
  65. package/scripts/hooks/README.md +40 -0
  66. package/server.json +2 -2
  67. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-admin-session.schema.json +0 -0
  68. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +0 -0
  69. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +0 -0
  70. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-config.schema.json +0 -0
  71. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-data.schema.json +0 -0
  72. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-manifest.schema.json +0 -0
  73. /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-metadata.schema.json +0 -0
  74. /package/{schemas → dist/schemas}/json-schema/instruction-audience-scope.schema.json +0 -0
  75. /package/{schemas → dist/schemas}/json-schema/instruction-requirement-level.schema.json +0 -0
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildContentModelSeed = buildContentModelSeed;
7
+ /**
8
+ * Builds the canonical "content model" seed (002-content-model) from the
9
+ * authoritative instruction JSON schema at module-load time.
10
+ *
11
+ * Single source of truth: the schema. The body here is regenerated on every
12
+ * server start, so any change to the schema's `contentType.enum`, its
13
+ * description, the top-level `required` array, or referenced field
14
+ * descriptions automatically flows into the seed. A drift test
15
+ * (src/tests/contentModelSeed.spec.ts) enforces the wiring.
16
+ *
17
+ * Constraint (constitution A-7): generalized, public-safe, environment-agnostic.
18
+ */
19
+ const instruction_schema_json_1 = __importDefault(require("../../schemas/instruction.schema.json"));
20
+ /**
21
+ * Parse the `contentType` schema description for `<name> (<desc>)` fragments
22
+ * and filter to the canonical enum members.
23
+ */
24
+ function parseContentTypeMatrix(s) {
25
+ const ct = s.properties.contentType;
26
+ if (!ct || !Array.isArray(ct.enum) || typeof ct.description !== 'string') {
27
+ throw new Error('content-model seed: schema is missing properties.contentType.enum or .description');
28
+ }
29
+ const enumSet = new Set(ct.enum);
30
+ const rows = [];
31
+ const seen = new Set();
32
+ const re = /([a-z][a-z-]*) \(([^)]+)\)/g;
33
+ let m;
34
+ while ((m = re.exec(ct.description)) !== null) {
35
+ const value = m[1];
36
+ const description = m[2].trim();
37
+ if (!enumSet.has(value))
38
+ continue;
39
+ if (seen.has(value))
40
+ continue;
41
+ seen.add(value);
42
+ rows.push({ value, description });
43
+ }
44
+ // Guarantee every enum member appears even if the description prose drifts.
45
+ for (const v of ct.enum) {
46
+ if (!seen.has(v))
47
+ rows.push({ value: v, description: '(no description in schema)' });
48
+ }
49
+ // Stable order: schema enum order
50
+ rows.sort((a, b) => ct.enum.indexOf(a.value) - ct.enum.indexOf(b.value));
51
+ return rows;
52
+ }
53
+ function fieldBullet(fieldName, s) {
54
+ const desc = s.properties[fieldName]?.description;
55
+ if (!desc) {
56
+ throw new Error(`content-model seed: schema property '${fieldName}' has no description`);
57
+ }
58
+ return `- \`${fieldName}\` — ${desc}`;
59
+ }
60
+ /**
61
+ * Common optional fields surfaced to agents. Order is curated for narrative
62
+ * flow; the *descriptions* are pulled from the schema so the seed cannot
63
+ * drift from `index_schema`. Adding/removing entries here is a deliberate
64
+ * editorial choice, but the prose is never duplicated.
65
+ */
66
+ const COMMON_OPTIONAL_FIELDS = [
67
+ 'priorityTier',
68
+ 'semanticSummary',
69
+ 'primaryCategory',
70
+ 'owner',
71
+ 'classification',
72
+ 'version',
73
+ 'status',
74
+ 'reviewIntervalDays',
75
+ 'lastReviewedAt',
76
+ 'nextReviewDue',
77
+ 'rationale'
78
+ ];
79
+ function readSchemaVersion(s) {
80
+ const sv = s.properties.schemaVersion;
81
+ if (!sv || !Array.isArray(sv.enum) || sv.enum.length === 0) {
82
+ throw new Error('content-model seed: schema.properties.schemaVersion.enum is missing or empty');
83
+ }
84
+ // Pick the highest enum value (strings compared numerically when possible).
85
+ const sorted = [...sv.enum].sort((a, b) => {
86
+ const na = Number(a);
87
+ const nb = Number(b);
88
+ if (Number.isFinite(na) && Number.isFinite(nb))
89
+ return nb - na;
90
+ return b.localeCompare(a);
91
+ });
92
+ return sorted[0];
93
+ }
94
+ /**
95
+ * Build the canonical seed object for `002-content-model` from the schema.
96
+ *
97
+ * The function is pure: same schema input → same seed output. Tests rely on
98
+ * this determinism to assert drift-safety.
99
+ *
100
+ * @returns Canonical seed `{ file, id, json }` ready to push into CANONICAL_SEEDS.
101
+ */
102
+ function buildContentModelSeed() {
103
+ const s = instruction_schema_json_1.default;
104
+ if (!Array.isArray(s.required) || s.required.length === 0) {
105
+ throw new Error('content-model seed: schema.required is missing or empty');
106
+ }
107
+ const matrix = parseContentTypeMatrix(s);
108
+ const requiredLines = s.required.map(f => fieldBullet(f, s)).join('\n');
109
+ const optionalLines = COMMON_OPTIONAL_FIELDS.map(f => fieldBullet(f, s)).join('\n');
110
+ const matrixRows = matrix
111
+ .map(r => `| \`${r.value}\` | ${r.description} |`)
112
+ .join('\n');
113
+ const schemaVersionValue = readSchemaVersion(s);
114
+ const body = `# Index Server Content Model
115
+
116
+ Knowledge for AI agents writing or evaluating instruction entries. This document is regenerated from the canonical \`schemas/instruction.schema.json\` on every server start, so it always matches the validator the index actually runs.
117
+
118
+ For the full machine-validatable schema (with current ranges, runtime limits, and the promotion-workflow checklist) call the \`index_schema\` MCP tool. Treat that tool as the validation source of truth at runtime; this seed is the conceptual knowledge guide.
119
+
120
+ ## Required fields
121
+
122
+ Every instruction entry must include:
123
+
124
+ ${requiredLines}
125
+
126
+ ## \`contentType\` decision matrix
127
+
128
+ Pick the \`contentType\` that matches what the entry is for:
129
+
130
+ | Value | Use when |
131
+ |-------|----------|
132
+ ${matrixRows}
133
+
134
+ Default is \`instruction\`.
135
+
136
+ ## Common optional fields
137
+
138
+ ${optionalLines}
139
+
140
+ Call \`index_schema\` for the authoritative list, validation rules, and minimal example.
141
+
142
+ ## Note on adjacent concepts
143
+
144
+ Some agent platforms use terms such as plugin, MCP server, or connector for deployment surfaces. When documenting those surfaces in Index Server, choose the canonical \`contentType\` by purpose: external system guidance is \`integration\`, reusable context is \`knowledge\`, a callable capability is \`skill\`, and a multi-step process is \`workflow\`.
145
+ `;
146
+ return {
147
+ file: '002-content-model.json',
148
+ id: '002-content-model',
149
+ json: {
150
+ id: '002-content-model',
151
+ title: 'Index Server Content Model & Field Reference',
152
+ body,
153
+ audience: 'all',
154
+ requirement: 'recommended',
155
+ priority: 95,
156
+ priorityTier: 'P1',
157
+ contentType: 'knowledge',
158
+ categories: ['bootstrap', 'content-model', 'reference', 'schema'],
159
+ primaryCategory: 'reference',
160
+ owner: 'system',
161
+ version: '1.0.0',
162
+ schemaVersion: schemaVersionValue,
163
+ semanticSummary: 'Knowledge for AI agents: required fields, the contentType decision matrix, and pointer to index_schema for the live JSON schema.'
164
+ }
165
+ };
166
+ }
@@ -0,0 +1,5 @@
1
+ export declare function buildContentTypesSeed(): {
2
+ file: string;
3
+ id: string;
4
+ json: Record<string, unknown>;
5
+ };
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildContentTypesSeed = buildContentTypesSeed;
7
+ const instruction_schema_json_1 = __importDefault(require("../../schemas/instruction.schema.json"));
8
+ function contentTypeRows(s) {
9
+ const ct = s.properties.contentType;
10
+ if (!ct || !Array.isArray(ct.enum) || typeof ct.description !== 'string') {
11
+ throw new Error('content-types seed: schema is missing properties.contentType.enum or .description');
12
+ }
13
+ const rows = [];
14
+ const seen = new Set();
15
+ const enumSet = new Set(ct.enum);
16
+ const re = /([a-z][a-z-]*) \(([^)]+)\)/g;
17
+ let m;
18
+ while ((m = re.exec(ct.description)) !== null) {
19
+ const value = m[1];
20
+ if (!enumSet.has(value) || seen.has(value))
21
+ continue;
22
+ seen.add(value);
23
+ rows.push({ value, description: m[2].trim() });
24
+ }
25
+ for (const value of ct.enum) {
26
+ if (!seen.has(value))
27
+ rows.push({ value, description: '(no description in schema)' });
28
+ }
29
+ rows.sort((a, b) => ct.enum.indexOf(a.value) - ct.enum.indexOf(b.value));
30
+ return rows;
31
+ }
32
+ function readSchemaVersion(s) {
33
+ const sv = s.properties.schemaVersion;
34
+ if (!sv || !Array.isArray(sv.enum) || sv.enum.length === 0) {
35
+ throw new Error('content-types seed: schema.properties.schemaVersion.enum is missing or empty');
36
+ }
37
+ return [...sv.enum].sort((a, b) => Number(b) - Number(a))[0];
38
+ }
39
+ function buildContentTypesSeed() {
40
+ const s = instruction_schema_json_1.default;
41
+ const matrixRows = contentTypeRows(s)
42
+ .map(r => `| \`${r.value}\` | ${r.description} |`)
43
+ .join('\n');
44
+ const body = `# Index Server Content Types
45
+
46
+ This knowledge seed lists the canonical \`contentType\` taxonomy from \`schemas/instruction.schema.json\`. It is regenerated from the schema on every server start so agents see the same enum values the validator enforces.
47
+
48
+ For the full machine-validatable schema, call the \`index_schema\` MCP tool.
49
+
50
+ | Value | Use when |
51
+ |-------|----------|
52
+ ${matrixRows}
53
+
54
+ Default is \`instruction\`.
55
+ `;
56
+ return {
57
+ file: '003-content-types.json',
58
+ id: '003-content-types',
59
+ json: {
60
+ id: '003-content-types',
61
+ title: 'Index Server Content Types',
62
+ body,
63
+ audience: 'all',
64
+ requirement: 'recommended',
65
+ priority: 94,
66
+ priorityTier: 'P1',
67
+ contentType: 'knowledge',
68
+ categories: ['bootstrap', 'content-types', 'schema'],
69
+ primaryCategory: 'schema',
70
+ owner: 'system',
71
+ version: '1.0.0',
72
+ schemaVersion: readSchemaVersion(s),
73
+ semanticSummary: 'Canonical Index Server contentType taxonomy generated from instruction.schema.json.',
74
+ },
75
+ };
76
+ }
@@ -14,6 +14,7 @@ export interface SeedSummary {
14
14
  created: string[];
15
15
  existing: string[];
16
16
  skipped: string[];
17
+ upgraded: string[];
17
18
  disabled: boolean;
18
19
  reason?: string;
19
20
  hash: string;
@@ -11,6 +11,8 @@ const crypto_1 = __importDefault(require("crypto"));
11
11
  const indexContext_1 = require("./indexContext");
12
12
  const logger_1 = require("./logger");
13
13
  const runtimeConfig_1 = require("../config/runtimeConfig");
14
+ const seedBootstrap_contentModel_1 = require("./seedBootstrap.contentModel");
15
+ const seedBootstrap_contentTypes_1 = require("./seedBootstrap.contentTypes");
14
16
  // Canonical seed instruction objects (kept intentionally minimal – DO NOT add environment specific data)
15
17
  const CANONICAL_SEEDS = [
16
18
  {
@@ -156,13 +158,15 @@ docker compose up # HTTP on :8787
156
158
  Restart your MCP client after configuration changes. Verify with \`health_check\`.
157
159
 
158
160
  For full configuration options: see \`docs/mcp_configuration.md\` and \`docs/configuration.md\`.`,
159
- audience: 'agents',
160
- requirement: 'required',
161
+ audience: 'all',
162
+ requirement: 'mandatory',
161
163
  priority: 100,
164
+ priorityTier: 'P1',
165
+ contentType: 'instruction',
162
166
  categories: ['bootstrap', 'mcp-activation', 'quick-start', 'documentation'],
163
167
  owner: 'system',
164
- version: 3,
165
- schemaVersion: '5',
168
+ version: '3.0.0',
169
+ schemaVersion: '6',
166
170
  semanticSummary: 'Index Server quick start: search-first workflow, knowledge contribution, copilot instructions setup, and MCP client configuration for AI agents'
167
171
  }
168
172
  },
@@ -173,17 +177,26 @@ For full configuration options: see \`docs/mcp_configuration.md\` and \`docs/con
173
177
  id: '001-lifecycle-bootstrap',
174
178
  title: 'Lifecycle Bootstrap: Local-First Instruction Strategy',
175
179
  body: 'Purpose: Early lifecycle guidance after bootstrap confirmation. Keep index minimal; prefer local-first P0/P1 additions; promote only after stability.',
176
- audience: 'agents',
180
+ audience: 'all',
177
181
  requirement: 'recommended',
178
- priorityTier: 'p1',
182
+ priority: 99,
183
+ priorityTier: 'P1',
184
+ contentType: 'instruction',
179
185
  categories: ['bootstrap', 'lifecycle'],
180
186
  owner: 'system',
181
- version: 1,
182
- schemaVersion: '5',
187
+ version: '1.0.0',
188
+ schemaVersion: '6',
183
189
  semanticSummary: 'Lifecycle and promotion guardrails after bootstrap confirmation',
184
190
  reviewIntervalDays: 120
185
191
  }
186
- }
192
+ },
193
+ // 002-content-model is generated from schemas/instruction.schema.json at
194
+ // module load. Single source of truth: any change to the schema's required
195
+ // fields, contentType enum, or referenced field descriptions automatically
196
+ // flows into the seed body. Drift is enforced by
197
+ // src/tests/contentModelSeed.spec.ts.
198
+ (0, seedBootstrap_contentModel_1.buildContentModelSeed)(),
199
+ (0, seedBootstrap_contentTypes_1.buildContentTypesSeed)()
187
200
  ];
188
201
  function computeCanonicalHash() {
189
202
  const canonical = CANONICAL_SEEDS.map(s => ({ id: s.id, file: s.file, json: s.json })).sort((a, b) => a.id.localeCompare(b.id));
@@ -198,7 +211,7 @@ function autoSeedBootstrap() {
198
211
  const cfg = (0, runtimeConfig_1.getRuntimeConfig)().bootstrapSeed;
199
212
  const disabled = !cfg.autoSeed;
200
213
  const dir = safeInstructionsDir();
201
- const summary = { dir, created: [], existing: [], skipped: [], disabled, hash: computeCanonicalHash() };
214
+ const summary = { dir, created: [], existing: [], skipped: [], upgraded: [], disabled, hash: computeCanonicalHash() };
202
215
  if (disabled) {
203
216
  summary.reason = 'disabled_by_env';
204
217
  return summary;
@@ -215,10 +228,76 @@ function autoSeedBootstrap() {
215
228
  for (const seed of CANONICAL_SEEDS) {
216
229
  const target = path_1.default.join(dir, seed.file);
217
230
  const exists = fs_1.default.existsSync(target);
231
+ // Canonical body hash (matches the sourceHash baked at write time).
232
+ // Used both to bake into freshly written seeds and to detect drift
233
+ // against existing on-disk seeds whose generated body has changed
234
+ // (e.g. 002-content-model after an instruction.schema.json edit).
235
+ const canonicalBody = typeof seed.json.body === 'string' ? seed.json.body : '';
236
+ const canonicalSourceHash = crypto_1.default.createHash('sha256').update(canonicalBody, 'utf8').digest('hex');
218
237
  if (exists) {
219
- summary.existing.push(seed.file);
220
- summary.skipped.push(seed.file);
221
- continue; // do not overwrite
238
+ // Detect schema-invalid stale seeds from earlier versions and rewrite
239
+ // them. Pre-v1.28.10 wrote `requirement: 'required'` and
240
+ // `priorityTier: 'p1'` (lowercase), both rejected by the current
241
+ // instruction.schema.json. Without this upgrade path, broken seeds
242
+ // persist forever because we previously never overwrote existing files.
243
+ // We deliberately scope the upgrade to enum violations (not arbitrary
244
+ // user edits) so hand-curated valid seeds are preserved. RCA 2026-05-07.
245
+ //
246
+ // Additionally: detect canonical-body drift (sourceHash mismatch)
247
+ // for canonical seeds. The 002-content-model seed is generated from
248
+ // instruction.schema.json on every server start; without this check
249
+ // an existing on-disk seed would silently go stale after a schema
250
+ // edit, violating the documented single-source-of-truth contract.
251
+ // RCA 2026-05-08 (PR #324 quality review).
252
+ let stale = false;
253
+ let staleReason = '';
254
+ try {
255
+ const existing = JSON.parse(fs_1.default.readFileSync(target, 'utf8'));
256
+ const validRequirement = ['mandatory', 'critical', 'recommended', 'optional', 'deprecated'];
257
+ const validPriorityTier = ['P1', 'P2', 'P3', 'P4'];
258
+ if (typeof existing.requirement === 'string' && !validRequirement.includes(existing.requirement)) {
259
+ stale = true;
260
+ staleReason = `invalid requirement="${existing.requirement}"`;
261
+ }
262
+ else if (typeof existing.priorityTier === 'string' && !validPriorityTier.includes(existing.priorityTier)) {
263
+ stale = true;
264
+ staleReason = `invalid priorityTier="${existing.priorityTier}"`;
265
+ }
266
+ else if (typeof existing.sourceHash === 'string' && existing.sourceHash !== canonicalSourceHash) {
267
+ // sourceHash on disk no longer matches the in-code canonical body.
268
+ // Refresh so canonical seeds track the current source-of-truth.
269
+ stale = true;
270
+ staleReason = `canonical body drift: on-disk sourceHash=${existing.sourceHash.slice(0, 12)}… expected=${canonicalSourceHash.slice(0, 12)}…`;
271
+ }
272
+ else if (typeof existing.sourceHash !== 'string') {
273
+ // Pre-sourceHash legacy install: an older Index Server version
274
+ // wrote canonical seeds without baking sourceHash. Without this
275
+ // branch a stale legacy body would skip refresh forever, since
276
+ // the hash-mismatch check above never trips. Treat absence as
277
+ // drift so legacy installs converge on the current canonical
278
+ // body. RCA 2026-05-08 (PR #324 reliability advisory).
279
+ stale = true;
280
+ staleReason = 'missing sourceHash (legacy pre-sourceHash install)';
281
+ }
282
+ }
283
+ catch (e) {
284
+ stale = true;
285
+ staleReason = `unparseable_json: ${(e instanceof Error) ? e.message : String(e)}`;
286
+ }
287
+ if (!stale) {
288
+ summary.existing.push(seed.file);
289
+ summary.skipped.push(seed.file);
290
+ continue; // valid existing seed; do not overwrite
291
+ }
292
+ try {
293
+ fs_1.default.unlinkSync(target);
294
+ }
295
+ catch { /* fall through to write */ }
296
+ try {
297
+ process.stderr.write(`[seed] upgrading stale seed ${seed.file}: ${staleReason}\n`);
298
+ }
299
+ catch { /* ignore */ }
300
+ // fall through to write canonical content
222
301
  }
223
302
  // Directory empty OR missing seed triggers creation.
224
303
  try {
@@ -226,10 +305,17 @@ function autoSeedBootstrap() {
226
305
  // Inject timestamps at write time so loaders never trigger
227
306
  // [invariant-repair] firstSeenTs WARN noise on subsequent reads.
228
307
  const nowIso = new Date().toISOString();
229
- const stamped = { createdAt: nowIso, firstSeenTs: nowIso, ...seed.json };
308
+ // Bake-in sourceHash so integrity_verify reports zero drift on a fresh install.
309
+ // Schema requires sha256(body) for the body field; without this seeds appear
310
+ // as drift forever (expected hash empty, actual populated).
311
+ const sourceHash = canonicalSourceHash;
312
+ const stamped = { createdAt: nowIso, updatedAt: nowIso, firstSeenTs: nowIso, sourceHash, ...seed.json };
230
313
  fs_1.default.writeFileSync(tmp, JSON.stringify(stamped, null, 2), { encoding: 'utf8' });
231
314
  fs_1.default.renameSync(tmp, target);
232
- summary.created.push(seed.file);
315
+ if (exists)
316
+ summary.upgraded.push(seed.file);
317
+ else
318
+ summary.created.push(seed.file);
233
319
  }
234
320
  catch (e) {
235
321
  summary.reason = `partial_failure ${(e instanceof Error) ? e.message : String(e)}`;
@@ -12,20 +12,9 @@ const schemas_1 = require("../schemas");
12
12
  const featureFlags_1 = require("./featureFlags");
13
13
  const envUtils_1 = require("../utils/envUtils");
14
14
  const runtimeConfig_1 = require("../config/runtimeConfig");
15
+ const instruction_1 = require("../models/instruction");
15
16
  // Input schema helpers (keep intentionally permissive if params optional)
16
17
  const stringReq = (name) => ({ type: 'object', additionalProperties: false, required: [name], properties: { [name]: { type: 'string' } } });
17
- const extensionsInputSchema = {
18
- type: 'object',
19
- additionalProperties: {
20
- anyOf: [
21
- { type: 'string' },
22
- { type: 'number' },
23
- { type: 'boolean' },
24
- { type: 'array', items: {} },
25
- { type: 'object', additionalProperties: true },
26
- ],
27
- },
28
- };
29
18
  const DANGEROUS_DIAGNOSTIC_TOOLS = new Set([
30
19
  'diagnostics_block',
31
20
  'diagnostics_microtaskFlood',
@@ -42,14 +31,21 @@ function buildInstructionBodyInputSchema(baseDescription) {
42
31
  function withDynamicInstructionBodyLimits(name, inputSchema) {
43
32
  const schema = JSON.parse(JSON.stringify(inputSchema));
44
33
  if (name === 'index_add') {
45
- const entryProps = schema.properties?.entry?.properties ?? {};
34
+ const entry = schema.properties?.entry ?? {};
35
+ const entryProps = entry.properties ?? {};
46
36
  entryProps.body = buildInstructionBodyInputSchema('Instruction body.');
37
+ // Sub-schema $id must be unique across compile cycles so Ajv (a) does
38
+ // not reject duplicate-id registration and (b) can still resolve the
39
+ // embedded "#/definitions/..." $refs scoped to this schema.
40
+ entry.$id = `tool-input/index_add/entry/${++subSchemaCounter}`;
47
41
  }
48
42
  else if (name === 'index_import') {
49
43
  const entries = schema.properties?.entries?.oneOf;
50
44
  const arrayVariant = Array.isArray(entries) ? entries.find((candidate) => candidate.type === 'array') : undefined;
51
- const itemProps = arrayVariant?.items?.properties ?? {};
45
+ const items = arrayVariant?.items ?? {};
46
+ const itemProps = items.properties ?? {};
52
47
  itemProps.body = buildInstructionBodyInputSchema('Instruction body.');
48
+ items.$id = `tool-input/index_import/entry/${++subSchemaCounter}`;
53
49
  }
54
50
  else if (name === 'index_dispatch') {
55
51
  const props = schema.properties ?? {};
@@ -59,6 +55,7 @@ function withDynamicInstructionBodyLimits(name, inputSchema) {
59
55
  }
60
56
  return schema;
61
57
  }
58
+ let subSchemaCounter = 0;
62
59
  // Explicit param schemas derived from handlers in toolHandlers.ts
63
60
  const INPUT_SCHEMAS = {
64
61
  // graph export (Phase 1 + Phase 2 enrichment). All params optional.
@@ -94,7 +91,7 @@ const INPUT_SCHEMAS = {
94
91
  keywords: { type: 'array', items: { type: 'string' }, description: 'Explicit keyword array for search action when the caller wants direct token control.' },
95
92
  ids: { type: 'array', items: { type: 'string' }, description: 'Array of instruction IDs for remove or export actions.' },
96
93
  category: { type: 'string', description: 'Filter by category for list action.' },
97
- contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'], description: 'Filter by content type for list, search, or query actions, or specify the entry content type for add action. Legacy "chat-session" write inputs are migrated to "workflow".' },
94
+ contentType: { type: 'string', enum: [...instruction_1.CONTENT_TYPES], description: 'Filter by content type for list, search, or query actions, or specify the entry content type for add action.' },
98
95
  text: { type: 'string', description: 'Full-text search within query action.' },
99
96
  includeCategories: { type: 'boolean', description: 'Search categories in addition to id/title/semanticSummary/body for search action.' },
100
97
  caseSensitive: { type: 'boolean', description: 'Enable case-sensitive matching for search action.' },
@@ -145,20 +142,27 @@ const INPUT_SCHEMAS = {
145
142
  // NOTE: instructions_query & instructions_categories removed as standalone tools.
146
143
  // They are now exclusively accessed via index_dispatch with actions 'query' and 'categories'.
147
144
  // legacy read-only instruction method schemas removed in favor of dispatcher
145
+ //
146
+ // index_import / index_add tool input schemas are DERIVED from the canonical
147
+ // instruction schema via buildToolInputEntrySchema(). The two surfaces only
148
+ // differ in their caller-required minimum (index_add accepts {id, body};
149
+ // index_import requires {id, title, body, priority, audience, requirement}).
150
+ // Both reject server-managed properties (additionalProperties:false +
151
+ // server-managed keys stripped) and share property definitions, enums and
152
+ // patterns with on-disk validation. Hand-maintaining these schemas was the
153
+ // original drift source — see src/schemas/instructionSchema.ts.
148
154
  'index_import': { type: 'object', additionalProperties: false, properties: {
149
155
  entries: { oneOf: [
150
- { type: 'array', minItems: 1, items: { type: 'object', required: ['id', 'title', 'body', 'priority', 'audience', 'requirement'], additionalProperties: false, properties: {
151
- id: { type: 'string' }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number' }, audience: { type: 'string' }, requirement: { type: 'string' }, categories: { type: 'array', items: { type: 'string' } }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'] }, extensions: extensionsInputSchema, mode: { type: 'string' }
152
- } } },
156
+ { type: 'array', minItems: 1, items: (0, schemas_1.buildToolInputEntrySchema)({
157
+ required: ['id', 'title', 'body', 'priority', 'audience', 'requirement'],
158
+ }) },
153
159
  { type: 'string', description: 'Stringified JSON array of instruction entries, or a file path to a JSON array of instruction entries' }
154
160
  ] },
155
161
  source: { type: 'string', description: 'Directory path containing .json instruction files to import' },
156
162
  mode: { enum: ['skip', 'overwrite'] }
157
163
  } },
158
164
  'index_add': { type: 'object', additionalProperties: false, required: ['entry'], properties: {
159
- entry: { type: 'object', required: ['id', 'body'], additionalProperties: false, properties: {
160
- id: { type: 'string', minLength: 1, maxLength: 120, pattern: '^[a-z0-9](?:[a-z0-9-_]{0,118}[a-z0-9])?$' }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number', minimum: 1, maximum: 100 }, audience: { type: 'string', enum: ['individual', 'group', 'all'] }, requirement: { type: 'string', enum: ['mandatory', 'critical', 'recommended', 'optional', 'deprecated'] }, categories: { type: 'array', items: { type: 'string', pattern: '^[a-z0-9][a-z0-9-_]{0,48}$' } }, deprecatedBy: { type: 'string' }, riskScore: { type: 'number' }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'] }, extensions: extensionsInputSchema
161
- } },
165
+ entry: (0, schemas_1.buildToolInputEntrySchema)({ required: ['id', 'body'] }),
162
166
  overwrite: { type: 'boolean' },
163
167
  lax: { type: 'boolean' }
164
168
  } },
@@ -208,6 +212,28 @@ const INPUT_SCHEMAS = {
208
212
  metadata: { type: 'object', additionalProperties: true },
209
213
  tags: { type: 'array', maxItems: 10, items: { type: 'string' } }
210
214
  } },
215
+ 'feedback_manage': { type: 'object', additionalProperties: false, required: ['action'], properties: {
216
+ action: { type: 'string', enum: ['submit', 'list', 'get', 'update', 'delete', 'stats'], description: 'Feedback management action to perform.' },
217
+ id: { type: 'string', description: 'Feedback entry id for get, update, and delete actions.' },
218
+ type: { type: 'string', enum: ['issue', 'status', 'security', 'feature-request', 'bug-report', 'performance', 'usability', 'other'] },
219
+ severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
220
+ status: { type: 'string', enum: ['new', 'acknowledged', 'in-progress', 'resolved', 'closed'] },
221
+ title: { type: 'string', maxLength: 200 },
222
+ description: { type: 'string', maxLength: 10000 },
223
+ context: { type: 'object', additionalProperties: true, properties: {
224
+ clientInfo: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'string' } } },
225
+ serverVersion: { type: 'string' },
226
+ environment: { type: 'object', additionalProperties: true },
227
+ sessionId: { type: 'string' },
228
+ toolName: { type: 'string' },
229
+ requestId: { type: 'string' }
230
+ } },
231
+ metadata: { type: 'object', additionalProperties: true },
232
+ tags: { type: 'array', maxItems: 10, items: { type: 'string' } },
233
+ limit: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum entries to return for list action.' },
234
+ offset: { type: 'number', minimum: 0, description: 'Pagination offset for list action.' },
235
+ since: { type: 'string', description: 'ISO date filter for list and stats actions.' }
236
+ } },
211
237
  // instructions search tool - PRIMARY discovery mechanism
212
238
  'index_search': { type: 'object', additionalProperties: false, required: ['keywords'], properties: {
213
239
  keywords: {
@@ -221,7 +247,7 @@ const INPUT_SCHEMAS = {
221
247
  limit: { type: 'number', minimum: 1, maximum: 100, default: 50, description: 'Maximum number of instruction IDs to return' },
222
248
  includeCategories: { type: 'boolean', default: false, description: 'Include categories in search scope' },
223
249
  caseSensitive: { type: 'boolean', default: false, description: 'Perform case-sensitive matching' },
224
- contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'], description: 'Filter results by content type (optional)' }
250
+ contentType: { type: 'string', enum: [...instruction_1.CONTENT_TYPES], description: 'Filter results by content type (optional)' }
225
251
  } },
226
252
  // promote_from_repo tool
227
253
  'promote_from_repo': { type: 'object', additionalProperties: false, required: ['repoPath'], properties: {
@@ -322,15 +348,16 @@ INPUT_SCHEMAS['trace_dump'] = { type: 'object', additionalProperties: false, pro
322
348
  } };
323
349
  // Stable & mutation classification lists (mirrors usage in toolHandlers; exported to remove duplication there).
324
350
  exports.STABLE = new Set(['health_check', 'feedback_submit', 'graph_export', 'index_dispatch', 'index_search', 'index_governanceHash', 'prompt_review', 'integrity_verify', 'usage_track', 'usage_hotset', 'metrics_snapshot', 'gates_evaluate', 'meta_tools', 'help_overview', 'index_schema', 'manifest_status', 'index_diagnostics', 'meta_activation_guide', 'meta_check_activation', 'bootstrap', 'bootstrap_status', 'feature_status', 'index_health', 'index_inspect', 'index_debug', 'integrity_manifest', 'messaging_read', 'messaging_list_channels', 'messaging_stats', 'messaging_get', 'messaging_thread', 'trace_dump']);
325
- exports.MUTATION = new Set(['index_add', 'index_import', 'index_repair', 'index_reload', 'index_remove', 'index_groom', 'index_enrich', 'index_governanceUpdate', 'index_normalize', 'usage_flush', 'manifest_refresh', 'manifest_repair', 'promote_from_repo', 'bootstrap_request', 'bootstrap_confirmFinalize', 'messaging_send', 'messaging_ack', 'messaging_update', 'messaging_purge', 'messaging_reply', 'diagnostics_block', 'diagnostics_microtaskFlood', 'diagnostics_memoryPressure']);
351
+ exports.MUTATION = new Set(['feedback_manage', 'index_add', 'index_import', 'index_repair', 'index_reload', 'index_remove', 'index_groom', 'index_enrich', 'index_governanceUpdate', 'index_normalize', 'usage_flush', 'manifest_refresh', 'manifest_repair', 'promote_from_repo', 'bootstrap_request', 'bootstrap_confirmFinalize', 'messaging_send', 'messaging_ack', 'messaging_update', 'messaging_purge', 'messaging_reply', 'diagnostics_block', 'diagnostics_microtaskFlood', 'diagnostics_memoryPressure']);
326
352
  // Tool tier classification (002-tool-consolidation spec)
327
353
  // core: always visible, essential daily use
328
354
  // extended: opt-in via INDEX_SERVER_FLAG_TOOLS_EXTENDED=1 or flags.json tools_extended:true
329
355
  // admin: opt-in via INDEX_SERVER_FLAG_TOOLS_ADMIN=1, rarely needed ops/debug tools
330
356
  const TOOL_TIERS = {
331
- // Core (7 after feedback_dispatch removal; feedback_submit remains always visible for agent reporting)
357
+ // Core (feedback_submit remains always visible for agent reporting; feedback_manage is the management dispatcher)
332
358
  'health_check': 'core',
333
359
  'feedback_submit': 'core',
360
+ 'feedback_manage': 'extended',
334
361
  'index_dispatch': 'core',
335
362
  'index_search': 'core',
336
363
  'prompt_review': 'core',
@@ -459,6 +486,7 @@ function describeTool(name) {
459
486
  case 'meta_tools': return 'Enumerate available tools & their metadata.';
460
487
  // feedback system descriptions
461
488
  case 'feedback_submit': return 'Submit feedback entry (issue, status report, security alert, feature request, etc.).';
489
+ case 'feedback_manage': return 'Manage feedback entries through a single action dispatcher. Actions: submit, list, get, update, delete, stats.';
462
490
  case 'bootstrap': return 'Unified bootstrap dispatcher. Actions: request, confirm, status.';
463
491
  case 'manifest_status': return 'Report index manifest presence and drift summary.';
464
492
  case 'manifest_refresh': return 'Rewrite manifest from current index state.';