@aiwg/cli 2026.9.21 → 2026.10.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 (210) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/cli.js +23 -18
  3. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  4. package/dist/src/artifacts/discovery-shadow.js +195 -0
  5. package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
  6. package/dist/src/artifacts/index-builder.js +47 -9
  7. package/dist/src/artifacts/query-engine.js +108 -5
  8. package/dist/src/artifacts/types.js +1 -0
  9. package/dist/src/cli/handlers/decision.js +88 -0
  10. package/dist/src/cli/handlers/gates.js +154 -0
  11. package/dist/src/cli/handlers/index.js +5 -1
  12. package/dist/src/cli/handlers/sessions.js +10 -7
  13. package/dist/src/cli/handlers/utilities.js +20 -0
  14. package/dist/src/config/aiwg-config.js +15 -2
  15. package/dist/src/decision/acceptance.js +25 -0
  16. package/dist/src/decision/adapters/jev.js +24 -2
  17. package/dist/src/decision/calibration/registry.js +9 -0
  18. package/dist/src/decision/compose.js +74 -15
  19. package/dist/src/decision/context-live-promotion.js +344 -0
  20. package/dist/src/decision/context-live-qualification.js +591 -0
  21. package/dist/src/decision/context-pruning.js +847 -0
  22. package/dist/src/decision/driver.js +430 -0
  23. package/dist/src/decision/egress-live-qualification.js +874 -0
  24. package/dist/src/decision/ensemble/contract.js +64 -1
  25. package/dist/src/decision/ensemble/index.js +1 -0
  26. package/dist/src/decision/ensemble/report.js +8 -42
  27. package/dist/src/decision/ensemble/runtime.js +571 -0
  28. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  29. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  30. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  31. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  32. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  33. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  34. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  35. package/dist/src/decision/ensemble-study/score.js +462 -0
  36. package/dist/src/decision/ensemble-study/staged.js +87 -0
  37. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  38. package/dist/src/decision/evaluate.js +227 -14
  39. package/dist/src/decision/export.js +4 -0
  40. package/dist/src/decision/feature-export.js +1074 -0
  41. package/dist/src/decision/graph-index.js +16 -0
  42. package/dist/src/decision/graph-live-qualification.js +1041 -0
  43. package/dist/src/decision/graph-live-workload.js +245 -0
  44. package/dist/src/decision/graph-skill-bridge.js +21 -3
  45. package/dist/src/decision/heldout/calibration.js +140 -0
  46. package/dist/src/decision/heldout/collector.js +430 -0
  47. package/dist/src/decision/heldout/contract.js +309 -0
  48. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  50. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  51. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  52. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  53. package/dist/src/decision/heldout/d29-pools.js +288 -0
  54. package/dist/src/decision/heldout/d29-v6.js +169 -0
  55. package/dist/src/decision/heldout/d29-v7.js +202 -0
  56. package/dist/src/decision/heldout/d29-v8.js +586 -0
  57. package/dist/src/decision/heldout/generator-registry.js +67 -0
  58. package/dist/src/decision/heldout/generators.js +696 -0
  59. package/dist/src/decision/heldout/index.js +8 -0
  60. package/dist/src/decision/heldout/journal.js +346 -0
  61. package/dist/src/decision/heldout/study-generators.js +64 -0
  62. package/dist/src/decision/heldout/types.js +2 -0
  63. package/dist/src/decision/host-policy.js +61 -0
  64. package/dist/src/decision/index.js +11 -0
  65. package/dist/src/decision/interop-profile.v1.json +112 -0
  66. package/dist/src/decision/interop.js +653 -0
  67. package/dist/src/decision/issue-triage/index.js +3 -0
  68. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  69. package/dist/src/decision/issue-triage/types.js +2 -0
  70. package/dist/src/decision/patterns/catalog.js +28 -8
  71. package/dist/src/decision/patterns/playground.js +28 -11
  72. package/dist/src/decision/patterns/specs.js +19 -10
  73. package/dist/src/decision/preprocessed-evidence.js +724 -0
  74. package/dist/src/decision/qualification/capture.js +8 -2
  75. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  76. package/dist/src/decision/qualification/load.js +8 -1
  77. package/dist/src/decision/qualification/quality.js +57 -49
  78. package/dist/src/decision/qualification/release.js +76 -3
  79. package/dist/src/decision/qualification/runner.js +5 -2
  80. package/dist/src/decision/routing/contract.js +175 -0
  81. package/dist/src/decision/routing/index.js +5 -0
  82. package/dist/src/decision/routing/report.js +216 -0
  83. package/dist/src/decision/routing/runtime.js +685 -0
  84. package/dist/src/decision/routing/types.js +2 -0
  85. package/dist/src/decision/sdlc-screening.js +1157 -0
  86. package/dist/src/decision/sensitivity/comparative.js +154 -0
  87. package/dist/src/decision/sensitivity/contract.js +218 -0
  88. package/dist/src/decision/sensitivity/index.js +5 -0
  89. package/dist/src/decision/sensitivity/runtime.js +912 -0
  90. package/dist/src/decision/sensitivity/types.js +2 -0
  91. package/dist/src/decision/validate.js +26 -0
  92. package/dist/src/extensions/commands/definitions.js +38 -0
  93. package/dist/src/extensions/manifest.js +44 -0
  94. package/dist/src/gates/discovery.js +315 -0
  95. package/dist/src/gates/driver.js +388 -0
  96. package/dist/src/gates/evaluate.js +719 -0
  97. package/dist/src/gates/floors.js +255 -0
  98. package/dist/src/gates/index.js +17 -0
  99. package/dist/src/gates/providers/evidence.js +38 -0
  100. package/dist/src/gates/providers/index.js +21 -0
  101. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  102. package/dist/src/gates/providers/loader.js +1069 -0
  103. package/dist/src/gates/providers/paired.js +52 -0
  104. package/dist/src/gates/providers/proportion.js +60 -0
  105. package/dist/src/gates/providers/registry.js +40 -0
  106. package/dist/src/gates/providers/scalar.js +41 -0
  107. package/dist/src/gates/providers/screening.js +53 -0
  108. package/dist/src/gates/providers/types.js +25 -0
  109. package/dist/src/gates/registry.js +1003 -0
  110. package/dist/src/gates/report.js +102 -0
  111. package/dist/src/gates/schema.js +56 -0
  112. package/dist/src/gates/stats/binomial.js +26 -0
  113. package/dist/src/gates/stats/bootstrap.js +62 -0
  114. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  115. package/dist/src/gates/stats/digest.js +26 -0
  116. package/dist/src/gates/stats/error.js +5 -0
  117. package/dist/src/gates/stats/index.js +9 -0
  118. package/dist/src/gates/stats/normal.js +35 -0
  119. package/dist/src/gates/stats/paired.js +120 -0
  120. package/dist/src/gates/stats/splits.js +30 -0
  121. package/dist/src/gates/types.js +8 -0
  122. package/dist/src/governance/redaction.js +14 -4
  123. package/dist/src/mcp/muse-native-config.mjs +123 -0
  124. package/dist/src/mcp/tools/decision.mjs +188 -0
  125. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  126. package/dist/src/providers/capability-matrix.yaml +2 -1
  127. package/dist/src/providers/provider-definitions.js +2 -1
  128. package/dist/src/sessions/adapters/muse.js +336 -39
  129. package/dist/src/sessions/batch-import.js +7 -0
  130. package/dist/src/sessions/workspace-discovery.js +13 -2
  131. package/package.json +1 -1
  132. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  133. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  134. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  135. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  136. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  137. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  138. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  139. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  140. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  141. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  142. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  143. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  144. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  145. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  146. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  147. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  148. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  149. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  150. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  151. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  152. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  153. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  154. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  155. package/schemas/decision/D29Study.v3.schema.json +609 -0
  156. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  157. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  158. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  159. package/schemas/decision/D29Study.v8.schema.json +671 -0
  160. package/schemas/decision/D29Study.v9.schema.json +778 -0
  161. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  162. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  163. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  164. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  165. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  166. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  167. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  168. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  169. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  170. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  171. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  172. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  173. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  174. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  175. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  176. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  177. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  178. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  179. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  180. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  181. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  182. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  183. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  184. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  185. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  186. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  187. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  188. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  189. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  190. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  191. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  192. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  193. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  194. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  195. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  196. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  197. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  198. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  199. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  200. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  201. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  202. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  203. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  204. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  205. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  206. package/tools/agents/deploy-agents.mjs +1 -1
  207. package/tools/agents/providers/base.mjs +26 -7
  208. package/tools/agents/providers/factory.mjs +10 -2
  209. package/tools/agents/providers/muse-hooks.mjs +1 -29
  210. package/tools/agents/providers/muse.mjs +6 -3
@@ -488,12 +488,13 @@ providers:
488
488
 
489
489
  muse:
490
490
  display_name: Muse Code
491
- status: experimental
491
+ status: stable
492
492
  # Meta's terminal/CI coding agent (Muse Spark-powered). Explicitly not a
493
493
  # model-API surface: Glimmer/Image/Voice are out of scope.
494
494
  # Skill writer (.agents/skills + XDG user root), discover-first AGENTS.md
495
495
  # bridge, managed .muse/hooks.json, and opt-in MCP settings profile.
496
496
  # ADR: docs/architecture/adr-muse-provider-target.md · epic #223.
497
+ # #231: promoted stable — Linux PUW + security review; maintainer Linux-only waiver 2026-10-02.
497
498
  daemon_tier: unsupported
498
499
  daemon_pty_adapter: false
499
500
  artifact_paths:
@@ -526,22 +526,21 @@ async function handleBuild(args) {
526
526
  loadUserGraphConfigs(cwd);
527
527
  loadGlobalGraphConfigs();
528
528
  let jsonBuilt = false;
529
- let projectGraphBuilt = false;
529
+ const builtGraphs = new Set();
530
530
  if (graph) {
531
531
  // --graph X: build the JSON graph if X is one; otherwise X may be a
532
532
  // research-corpus markdown view, rendered below.
533
533
  if (graph in GRAPH_CONFIGS) {
534
534
  await buildIndex(cwd, { force, verbose, scope, graph, explicit: true });
535
535
  jsonBuilt = true;
536
- projectGraphBuilt = graph === 'project';
536
+ builtGraphs.add(graph);
537
537
  }
538
538
  }
539
539
  else if (all) {
540
540
  // Build all known graphs — user asked for everything, but don't hard-error on missing dirs
541
541
  for (const [name] of orderedGraphEntries(Object.entries(GRAPH_CONFIGS))) {
542
542
  await buildIndex(cwd, { force, verbose, graph: name, explicit: false });
543
- if (name === 'project')
544
- projectGraphBuilt = true;
543
+ builtGraphs.add(name);
545
544
  }
546
545
  jsonBuilt = true;
547
546
  }
@@ -550,8 +549,7 @@ async function handleBuild(args) {
550
549
  for (const [name, config] of orderedGraphEntries(Object.entries(GRAPH_CONFIGS))) {
551
550
  if (config.defaultBuild) {
552
551
  await buildIndex(cwd, { force, verbose, scope: name === Object.keys(GRAPH_CONFIGS)[0] ? scope : undefined, graph: name, explicit: false });
553
- if (name === 'project')
554
- projectGraphBuilt = true;
552
+ builtGraphs.add(name);
555
553
  }
556
554
  }
557
555
  jsonBuilt = true;
@@ -583,21 +581,28 @@ async function handleBuild(args) {
583
581
  if (viewResults.some((r) => r.status === 'unsupported')) {
584
582
  process.exit(1);
585
583
  }
586
- if (projectGraphBuilt) {
587
- await syncProjectFortemiCoreCacheAfterBuild(cwd, verbose);
584
+ if (builtGraphs.size > 0) {
585
+ await syncFortemiCoreCachesAfterBuild(cwd, builtGraphs, verbose);
588
586
  }
589
587
  }
590
- async function syncProjectFortemiCoreCacheAfterBuild(cwd, verbose) {
591
- try {
592
- const { syncFortemiCoreIndex } = await import('./fortemi-core-sync.js');
593
- const manifest = syncFortemiCoreIndex(cwd, { graph: 'project' });
594
- if (verbose) {
595
- console.log(`Fortemi Core project sync: ${manifest.status}, ${manifest.item_count} item(s) → ${manifest.export_path}`);
588
+ async function syncFortemiCoreCachesAfterBuild(cwd, graphs, verbose) {
589
+ const { syncFortemiCoreIndex } = await import('./fortemi-core-sync.js');
590
+ const { loadGraphIndexFile } = await import('./index-reader.js');
591
+ for (const graph of graphs) {
592
+ // Some default-build graphs skip cleanly when their scan roots are absent.
593
+ // Only materialize a cache when this build left an actual source index.
594
+ if (!loadGraphIndexFile(cwd, 'metadata.json', graph))
595
+ continue;
596
+ try {
597
+ const manifest = syncFortemiCoreIndex(cwd, { graph });
598
+ if (verbose) {
599
+ console.log(`Fortemi Core ${graph} sync: ${manifest.status}, ${manifest.item_count} item(s) → ${manifest.export_path}`);
600
+ }
601
+ }
602
+ catch (err) {
603
+ console.error(`Warning: Fortemi Core ${graph} sync failed: ${err instanceof Error ? err.message : String(err)}`);
604
+ console.error(`Run \`aiwg index sync --graph ${graph}\` before using Fortemi-backed commands for '${graph}' artifacts.`);
596
605
  }
597
- }
598
- catch (err) {
599
- console.error(`Warning: Fortemi Core project sync failed: ${err instanceof Error ? err.message : String(err)}`);
600
- console.error('Run `aiwg index sync --graph project` before using Fortemi-backed `aiwg discover` or `aiwg show` for project-local artifacts.');
601
606
  }
602
607
  }
603
608
  /**
@@ -0,0 +1,177 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { types as utilTypes } from 'node:util';
3
+ import { canonicalJson } from '../security/artifact-trust.js';
4
+ import { admitEntry } from '../decision/entry.js';
5
+ import { evaluateDecisionRuleset } from '../decision/evaluate.js';
6
+ import { validateProjectionPolicy } from '../decision/projection.js';
7
+ import { artifactPin, assertArtifactPin, validateAgainstSchema, validateBinding, validateDefinition, validateRuleset } from '../decision/validate.js';
8
+ const hash = (value) => `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
9
+ const pinEqual = (a, b) => canonicalJson(a) === canonicalJson(b);
10
+ const validPin = (pin) => Boolean(pin?.id && pin.version && /^sha256:[a-f0-9]{64}$/.test(pin.digest));
11
+ const requestFields = ['query', 'candidates', 'options', 'definition', 'binding'];
12
+ /** Strip only the trusted cancellation handle; reject accessors/proxies before reading data. */
13
+ function payloadOf(request) {
14
+ if (!request || typeof request !== 'object' || utilTypes.isProxy(request)
15
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(request)))
16
+ throw new Error('Invalid shadow request');
17
+ const descriptors = Object.getOwnPropertyDescriptors(request);
18
+ if (Reflect.ownKeys(request).some(key => typeof key !== 'string' || ![...requestFields, 'signal'].includes(key))) {
19
+ throw new Error('Unsupported shadow request field');
20
+ }
21
+ const data = {};
22
+ for (const key of requestFields) {
23
+ const descriptor = descriptors[key];
24
+ if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
25
+ throw new Error('Invalid shadow request field');
26
+ data[key] = descriptor.value;
27
+ }
28
+ admitEntry(data);
29
+ return structuredClone(data);
30
+ }
31
+ /** Concrete Decision runtime bridge, deliberately without an executor or transport shortcut.
32
+ * It supports one pinned option cardinality, one evaluation and one non-retrying target.
33
+ * Other candidate cardinalities fail closed and retain deterministic discovery. */
34
+ export class DiscoveryShadowEvaluator {
35
+ profile;
36
+ calibration;
37
+ scheduler;
38
+ runtime;
39
+ registry;
40
+ alias;
41
+ options;
42
+ profileDigest;
43
+ constructor(config) {
44
+ const profile = { shadowPolicy: config.shadowPolicy, definition: config.definition, ruleset: config.ruleset, binding: config.binding,
45
+ projectionPolicy: config.projectionPolicy, policyPin: config.policyPin, receiptProjectId: config.receiptProjectId,
46
+ runId: config.runId, invocationScope: config.invocationScope };
47
+ admitEntry(profile);
48
+ this.profile = structuredClone(profile);
49
+ const p = this.profile;
50
+ validateDefinition(p.definition);
51
+ validateRuleset(p.ruleset);
52
+ validateBinding(p.binding, p.ruleset);
53
+ assertArtifactPin(p.definition, p.shadowPolicy.definition, 'discovery definition');
54
+ assertArtifactPin(p.binding, p.shadowPolicy.binding, 'discovery binding');
55
+ if (![p.runId, p.invocationScope, p.receiptProjectId].every(value => typeof value === 'string' && value.trim())
56
+ || !validPin(p.policyPin) || p.ruleset.spec.evaluations.length !== 1 || p.definition.spec.answer.kind !== 'choice') {
57
+ throw new Error('Shadow evaluator requires an identified single Choice profile');
58
+ }
59
+ if (!Number.isSafeInteger(p.shadowPolicy.ambiguity.maximumCandidates) || p.shadowPolicy.ambiguity.maximumCandidates < 1
60
+ || p.shadowPolicy.ambiguity.maximumCandidates > 100 || !Number.isSafeInteger(p.shadowPolicy.ambiguity.maximumTextBytes)
61
+ || p.shadowPolicy.ambiguity.maximumTextBytes < 1 || p.shadowPolicy.ambiguity.maximumTextBytes > 1_000_000
62
+ || !Number.isSafeInteger(p.shadowPolicy.maximumLatencyMs) || p.shadowPolicy.maximumLatencyMs < 1
63
+ || !Number.isSafeInteger(p.shadowPolicy.maximumTokens) || p.shadowPolicy.maximumTokens < 1
64
+ || !Number.isFinite(p.shadowPolicy.maximumCostUsd) || p.shadowPolicy.maximumCostUsd < 0) {
65
+ throw new Error('Invalid bounded shadow profile');
66
+ }
67
+ if (p.projectionPolicy.fields.length !== 2
68
+ || canonicalJson(p.projectionPolicy.fields.map(field => field.output).sort()) !== canonicalJson(['candidates', 'query'])
69
+ || p.projectionPolicy.fields.some(field => field.trust !== 'untrusted' || field.pointer !== `/${field.output}`)) {
70
+ throw new Error('Discovery query and candidates require the untrusted D10 projection');
71
+ }
72
+ const evaluation = p.ruleset.spec.evaluations[0];
73
+ this.alias = evaluation.alias;
74
+ if (evaluation.inputPointer !== '' || !pinEqual(evaluation.decision, artifactPin(p.definition)))
75
+ throw new Error('Shadow evaluation definition mismatch');
76
+ this.options = p.definition.spec.answer.options.map(option => option.id);
77
+ const candidates = this.options.slice(0, -1);
78
+ if (candidates.length < 1 || candidates.length > p.shadowPolicy.ambiguity.maximumCandidates
79
+ || this.options.at(-1) !== 'none' || candidates.some((id, i) => id !== `candidate_${i}`))
80
+ throw new Error('Invalid pinned shadow option domain');
81
+ const binding = p.binding.spec.evaluations[this.alias];
82
+ const target = binding.targets[0];
83
+ if (binding.targets.length !== 1 || binding.fallbackOn.length || target.retry.maxRetries !== 0
84
+ || p.binding.spec.maxAttempts !== 1 || p.binding.spec.concurrency !== 1
85
+ || p.binding.spec.totalTimeoutMs > p.shadowPolicy.maximumLatencyMs || target.timeoutMs > p.shadowPolicy.maximumLatencyMs
86
+ || target.acceptance.mode !== 'primitive-policy'
87
+ || !target.acceptance.compatibleUncertaintyProfiles.includes(p.shadowPolicy.uncertaintyProfile)) {
88
+ throw new Error('Shadow evaluator requires bounded single-target primitive acceptance');
89
+ }
90
+ if (!config.scheduler.enabled || !config.scheduler.estimate)
91
+ throw new Error('Shadow evaluator requires existing admission and cost/token estimates');
92
+ const { estimate, onEvidence, ...schedulerData } = config.scheduler;
93
+ admitEntry(schedulerData);
94
+ this.scheduler = { ...structuredClone(schedulerData), estimate, ...(onEvidence ? { onEvidence } : {}) };
95
+ // Shared host limits may be broader than this observer's per-invocation budget.
96
+ // Narrow every admission scope before resolution/dispatch, preserving stricter limits.
97
+ const bounded = (limits) => ({ ...limits,
98
+ maxTokens: Math.min(limits.maxTokens ?? p.shadowPolicy.maximumTokens, p.shadowPolicy.maximumTokens),
99
+ maxCostUsd: Math.min(limits.maxCostUsd ?? p.shadowPolicy.maximumCostUsd, p.shadowPolicy.maximumCostUsd),
100
+ allowUnknownCost: false,
101
+ });
102
+ this.scheduler.workspace.limits = bounded(this.scheduler.workspace.limits);
103
+ this.scheduler.principal.limits = bounded(this.scheduler.principal.limits);
104
+ this.scheduler.providers = Object.fromEntries(Object.entries(this.scheduler.providers).map(([id, limits]) => [id, bounded(limits)]));
105
+ admitEntry(config.calibration.artifact);
106
+ this.calibration = structuredClone(config.calibration.artifact);
107
+ this.registry = config.calibration.registry;
108
+ const registered = this.registry.artifactHistory().find(item => item.id === this.calibration.id);
109
+ const identity = this.calibration.identity;
110
+ if (!registered || hash(registered) !== hash(this.calibration) || this.calibration.digest !== p.shadowPolicy.calibrationDigest
111
+ || identity.definitionDigest !== artifactPin(p.definition).digest || identity.primitive !== 'choice'
112
+ || identity.provider !== target.adapter || identity.adapterVersion !== target.adapterVersion
113
+ || !p.shadowPolicy.allowedModels.includes(identity.actualModel))
114
+ throw new Error('Shadow calibration is not the registered pinned profile');
115
+ this.runtime = { adapters: { ...config.adapters }, resolveCredential: config.resolveCredential, receiptStore: config.receiptStore,
116
+ record: config.record, ...(config.now ? { now: config.now } : {}), ...(config.onProjectionEvidence ? { onProjectionEvidence: config.onProjectionEvidence } : {}) };
117
+ this.profileDigest = hash({ profile: p, calibration: this.calibration.digest, scheduler: schedulerData });
118
+ }
119
+ authorize(request) {
120
+ try {
121
+ this.checkedPayload(request);
122
+ validateProjectionPolicy(this.profile.projectionPolicy);
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ record(receipt) { this.runtime.record(structuredClone(receipt)); }
130
+ async evaluate(request) {
131
+ const payload = this.checkedPayload(request);
132
+ const descriptor = Object.getOwnPropertyDescriptor(request, 'signal');
133
+ if (!descriptor || !('value' in descriptor) || !(descriptor.value instanceof AbortSignal))
134
+ throw new Error('Shadow evaluation requires a cancellation signal');
135
+ const signal = descriptor.value;
136
+ if (signal.aborted)
137
+ throw new Error('Shadow evaluation cancelled');
138
+ const p = this.profile;
139
+ const input = { query: payload.query, candidates: payload.candidates };
140
+ const invocationId = `discovery-shadow:${hash({ scope: p.invocationScope, profile: this.profileDigest, input }).slice(7)}`;
141
+ const evaluated = await evaluateDecisionRuleset({
142
+ ruleset: structuredClone(p.ruleset), binding: structuredClone(p.binding), definitions: { [p.definition.metadata.id]: structuredClone(p.definition) },
143
+ input, runId: p.runId, invocationId, signal, adapters: this.runtime.adapters, resolveCredential: this.runtime.resolveCredential,
144
+ receiptStore: this.runtime.receiptStore, receiptProjectId: p.receiptProjectId, policyPin: p.policyPin,
145
+ calibrationPin: { id: this.calibration.id, version: this.calibration.identity.calibrator.version, digest: this.calibration.digest },
146
+ calibrationCompatibility: { registry: this.registry, calibrationArtifactId: this.calibration.id,
147
+ policy: { unknown: 'defer', incompatible: 'fail', shadowRequired: 'shadow', unusableCalibration: 'require-approval' },
148
+ identityFor: ({ actualModel }) => ({ ...structuredClone(this.calibration.identity), actualModel }) },
149
+ projection: { resolve: () => structuredClone(p.projectionPolicy), ...(this.runtime.onProjectionEvidence ? { onEvidence: this.runtime.onProjectionEvidence } : {}) },
150
+ scheduler: this.scheduler, ...(this.runtime.now ? { now: this.runtime.now } : {}),
151
+ });
152
+ const receipt = await this.runtime.receiptStore.read(invocationId, p.receiptProjectId);
153
+ const decision = evaluated.spec.evaluations[this.alias];
154
+ if (!receipt || receipt.state !== 'completed' || !receipt.result || !decision
155
+ || !receipt.evaluations[this.alias] || hash(receipt.result) !== hash(evaluated) || hash(receipt.evaluations[this.alias]) !== hash(decision)) {
156
+ throw new Error('Shadow evaluation has no verified terminal receipt');
157
+ }
158
+ return { result: structuredClone(decision), receiptDigest: hash(receipt) };
159
+ }
160
+ checkedPayload(request) {
161
+ const payload = payloadOf(request);
162
+ const p = this.profile;
163
+ if (p.shadowPolicy.mode !== 'shadow' || !pinEqual(payload.definition, p.shadowPolicy.definition) || !pinEqual(payload.binding, p.shadowPolicy.binding)
164
+ || canonicalJson(payload.options) !== canonicalJson(this.options) || !Array.isArray(payload.candidates)
165
+ || payload.candidates.length !== this.options.length - 1 || typeof payload.query !== 'string' || !payload.query.trim()
166
+ || payload.candidates.some((c, i) => !c || c.option !== this.options[i] || Object.keys(c).sort().join(',') !== 'capability,id,name,option,type'
167
+ || [c.id, c.name, c.type, c.capability].some(value => typeof value !== 'string') || !c.id)
168
+ || new Set(payload.candidates.map(c => c.id)).size !== payload.candidates.length
169
+ || Buffer.byteLength(canonicalJson({ query: payload.query, candidates: payload.candidates })) > p.shadowPolicy.ambiguity.maximumTextBytes) {
170
+ throw new Error('Shadow request does not match pinned bounded domain');
171
+ }
172
+ validateAgainstSchema(p.ruleset.spec.inputSchema, { query: payload.query, candidates: payload.candidates }, 'shadow input');
173
+ validateAgainstSchema(p.definition.spec.inputSchema, { query: payload.query, candidates: payload.candidates }, 'shadow input');
174
+ return payload;
175
+ }
176
+ }
177
+ //# sourceMappingURL=discovery-shadow-evaluator.js.map
@@ -0,0 +1,195 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from '../security/artifact-trust.js';
3
+ const finite = (n) => Number.isFinite(n) && n >= 0;
4
+ function validatePolicy(policy) {
5
+ if (policy.version !== 'discovery-ambiguity/v1' || !finite(policy.minimumScore)
6
+ || !Number.isFinite(policy.minimumMargin) || policy.minimumMargin <= 0
7
+ || !Number.isSafeInteger(policy.maximumCandidates) || policy.maximumCandidates < 1 || policy.maximumCandidates > 100
8
+ || !Number.isSafeInteger(policy.maximumTextBytes) || policy.maximumTextBytes < 1 || policy.maximumTextBytes > 1_000_000) {
9
+ throw new Error('Invalid discovery ambiguity policy');
10
+ }
11
+ }
12
+ /** Retrieval order is authoritative, including ties; this function never sorts it. */
13
+ export function discoveryAmbiguity(query, candidates, policy) {
14
+ validatePolicy(policy);
15
+ const answer = (reason, eligible = false) => ({ version: policy.version, eligible, reason });
16
+ if (typeof query !== 'string' || !query.trim() || !Array.isArray(candidates)
17
+ || candidates.some(c => !c || typeof c.id !== 'string' || !c.id || typeof c.name !== 'string'
18
+ || typeof c.type !== 'string' || typeof c.capability !== 'string' || !finite(c.score))
19
+ || new Set(candidates.map(c => c.id)).size !== candidates.length
20
+ || candidates.some((c, i) => i > 0 && c.score > candidates[i - 1].score)
21
+ || Buffer.byteLength(query + candidates.map(c => c.name + c.type + c.capability + c.id).join('')) > policy.maximumTextBytes) {
22
+ return answer('invalid-input');
23
+ }
24
+ if (!candidates.length)
25
+ return answer('no-candidates');
26
+ const normalized = query.trim().toLowerCase();
27
+ if (candidates.some(c => c.name.trim().toLowerCase() === normalized || c.id.toLowerCase() === normalized))
28
+ return answer('exact-name');
29
+ const first = candidates[0];
30
+ if (first.score < policy.minimumScore)
31
+ return answer('low-score', true);
32
+ const margin = first.score - (candidates[1]?.score ?? 0);
33
+ if (margin >= policy.minimumMargin)
34
+ return answer('high-margin');
35
+ return answer(candidates[1] && first.type !== candidates[1].type ? 'cross-type' : 'low-margin', true);
36
+ }
37
+ const digest = (value) => `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
38
+ const samePin = (a, b) => canonicalJson(a) === canonicalJson(b);
39
+ /** One host-owned circuit per workload. Drift/quality controls can trip it without changing
40
+ * any index or active decision pins. Re-enabling requires a newly constructed route. */
41
+ export class DiscoveryShadowRoute {
42
+ host;
43
+ policy;
44
+ stopped = false;
45
+ active = new Set();
46
+ constructor(policy, host) {
47
+ this.host = host;
48
+ validatePolicy(policy.ambiguity);
49
+ if (!['disabled', 'shadow'].includes(policy.mode) || !policy.allowedModels.length
50
+ || !policy.uncertaintyProfile || !/^sha256:[0-9a-f]{64}$/.test(policy.calibrationDigest)
51
+ || ![policy.minimumProbability, policy.minimumMargin].every(n => finite(n) && n <= 1)
52
+ || !Number.isSafeInteger(policy.maximumLatencyMs) || policy.maximumLatencyMs < 1
53
+ || !Number.isSafeInteger(policy.maximumTokens) || policy.maximumTokens < 1
54
+ || !finite(policy.maximumCostUsd)
55
+ || ![policy.definition, policy.binding].every(pin => pin && typeof pin.id === 'string' && pin.id.length > 0
56
+ && typeof pin.version === 'string' && pin.version.length > 0 && /^sha256:[0-9a-f]{64}$/.test(pin.digest))
57
+ || policy.allowedModels.some(model => typeof model !== 'string' || !model))
58
+ throw new Error('Invalid discovery shadow policy');
59
+ this.policy = structuredClone(policy);
60
+ }
61
+ disable() { this.stopped = true; for (const controller of this.active)
62
+ controller.abort(); }
63
+ get disabled() { return this.stopped || this.policy.mode === 'disabled'; }
64
+ /** Return metadata via the governed sink. Caller results always retain baseline order. */
65
+ async observe(query, baseline) {
66
+ const p = this.policy;
67
+ const gate = discoveryAmbiguity(query, baseline, p.ambiguity);
68
+ const selected = gate.reason === 'invalid-input' ? [] : baseline.slice(0, p.ambiguity.maximumCandidates);
69
+ const projection = {
70
+ query,
71
+ candidates: selected.map((c, i) => ({ option: `candidate_${i}`, id: c.id, name: c.name, type: c.type, capability: c.capability })),
72
+ options: [...selected.map((_, i) => `candidate_${i}`), 'none'],
73
+ definition: structuredClone(p.definition), binding: structuredClone(p.binding),
74
+ };
75
+ const receipt = {
76
+ schemaVersion: 'discovery-shadow/v1', route: 'deterministic-only', reason: 'privacy-denied',
77
+ ambiguityReason: gate.reason, candidatesDigest: digest(selected.map(c => c.id)), policyDigest: digest(p),
78
+ selection: null, resultDigest: null, servedModel: null, latencyMs: 0, usage: { tokens: null, costUsd: null },
79
+ };
80
+ const record = () => { try {
81
+ this.host.record(structuredClone(receipt));
82
+ }
83
+ catch {
84
+ this.disable();
85
+ } };
86
+ if (this.disabled || !gate.eligible) {
87
+ receipt.reason = this.stopped ? 'circuit-open' : p.mode === 'disabled' ? 'disabled' : gate.reason;
88
+ record();
89
+ return;
90
+ }
91
+ let authorized = false;
92
+ try {
93
+ authorized = this.host.authorize(structuredClone(projection)) === true;
94
+ }
95
+ catch { /* fail closed */ }
96
+ if (!authorized) {
97
+ record();
98
+ return;
99
+ }
100
+ const clock = this.host.clock ?? Date.now;
101
+ const start = clock();
102
+ if (!Number.isFinite(start)) {
103
+ this.disable();
104
+ return;
105
+ }
106
+ const controller = new AbortController();
107
+ this.active.add(controller);
108
+ let timedOut = false;
109
+ let onAbort;
110
+ let timer;
111
+ try {
112
+ const timeout = new Promise((_, reject) => {
113
+ timer = setTimeout(() => { timedOut = true; controller.abort(); reject(new Error('deadline')); }, p.maximumLatencyMs);
114
+ });
115
+ const cancelled = new Promise((_, reject) => {
116
+ onAbort = () => reject(new Error('cancelled'));
117
+ controller.signal.addEventListener('abort', onAbort, { once: true });
118
+ });
119
+ receipt.route = 'shadow';
120
+ const response = await Promise.race([this.host.evaluate({ ...structuredClone(projection), signal: controller.signal }), timeout, cancelled]);
121
+ if (this.stopped)
122
+ throw new Error('circuit-open');
123
+ const elapsed = clock() - start;
124
+ receipt.latencyMs = finite(elapsed) ? elapsed : p.maximumLatencyMs;
125
+ if (!finite(elapsed) || elapsed >= p.maximumLatencyMs) {
126
+ timedOut = true;
127
+ controller.abort();
128
+ throw new Error('deadline');
129
+ }
130
+ const r = response.result.spec;
131
+ receipt.resultDigest = /^sha256:[0-9a-f]{64}$/.test(response.receiptDigest) ? response.receiptDigest : null;
132
+ const attempts = r.attempts;
133
+ // Shared request accounting must remain at its owner; a per-item route cannot charge it again.
134
+ const ordinary = attempts.length > 0 && attempts.every(a => !a.batch);
135
+ const tokenKnown = ordinary && attempts.every(a => Number.isSafeInteger(a.usage.inputTokens) && a.usage.inputTokens >= 0
136
+ && Number.isSafeInteger(a.usage.outputTokens) && a.usage.outputTokens >= 0);
137
+ const costKnown = ordinary && attempts.every(a => a.usage.costUsd !== null && finite(a.usage.costUsd));
138
+ receipt.usage.tokens = tokenKnown ? attempts.reduce((n, a) => n + a.usage.inputTokens + a.usage.outputTokens, 0) : null;
139
+ receipt.usage.costUsd = costKnown ? attempts.reduce((n, a) => n + a.usage.costUsd, 0) : null;
140
+ if (receipt.usage.tokens === null || receipt.usage.costUsd === null || !Number.isSafeInteger(receipt.usage.tokens)
141
+ || receipt.usage.tokens > p.maximumTokens || receipt.usage.costUsd > p.maximumCostUsd) {
142
+ receipt.reason = 'budget-breach';
143
+ this.disable();
144
+ record();
145
+ return;
146
+ }
147
+ const last = attempts.at(-1);
148
+ const uncertainty = r.uncertainty;
149
+ const distribution = uncertainty?.distribution;
150
+ const probabilities = distribution && projection.options.every(option => Object.hasOwn(distribution, option))
151
+ && Object.keys(distribution).length === projection.options.length ? Object.values(distribution) : [];
152
+ const knownModel = last.actualModel !== null && p.allowedModels.includes(last.actualModel);
153
+ receipt.servedModel = knownModel ? last.actualModel : null;
154
+ const option = r.value;
155
+ let accept = r.status === 'success' && samePin(r.decision, p.definition) && samePin(r.binding, p.binding)
156
+ && knownModel && receipt.resultDigest !== null && r.acceptance?.disposition === 'act'
157
+ && r.calibrationCompatibility?.action === 'allow' && ['exact', 'approved-compatible'].includes(r.calibrationCompatibility.state)
158
+ && r.calibrationCompatibility.artifactDigest === p.calibrationDigest
159
+ && r.calibrationCompatibility.actualModel === last.actualModel
160
+ && r.calibrationCompatibility.runId === `${r.runId}:${r.invocationId}:${r.alias}`
161
+ && uncertainty?.source === 'provider' && uncertainty.profile === p.uncertaintyProfile
162
+ && probabilities.length > 0 && probabilities.every(v => finite(v) && v <= 1)
163
+ && Math.abs(probabilities.reduce((sum, v) => sum + v, 0) - 1) < 1e-6
164
+ && typeof option === 'string' && projection.options.includes(option);
165
+ if (accept && distribution && typeof option === 'string') {
166
+ const probability = distribution[option];
167
+ const competitor = Math.max(...Object.entries(distribution).filter(([key]) => key !== option).map(([, value]) => value));
168
+ accept = probability > competitor && probability >= p.minimumProbability && probability - competitor >= p.minimumMargin;
169
+ }
170
+ receipt.reason = accept ? 'accepted-shadow' : 'rejected-evidence';
171
+ if (!accept)
172
+ this.disable(); // quality/model/calibration breach restores deterministic-only routing
173
+ if (accept)
174
+ receipt.selection = option === 'none' ? { kind: 'none' } : { kind: 'candidate', ordinal: projection.options.indexOf(option) };
175
+ }
176
+ catch {
177
+ receipt.reason = timedOut ? 'budget-breach' : this.stopped ? 'circuit-open' : 'evaluation-failed';
178
+ const elapsed = clock() - start;
179
+ receipt.latencyMs = timedOut ? Math.max(p.maximumLatencyMs, finite(elapsed) ? elapsed : 0) : finite(elapsed) ? elapsed : 0;
180
+ receipt.selection = null;
181
+ if (timedOut)
182
+ this.disable();
183
+ }
184
+ finally {
185
+ if (timer)
186
+ clearTimeout(timer);
187
+ if (onAbort)
188
+ controller.signal.removeEventListener('abort', onAbort);
189
+ this.active.delete(controller);
190
+ }
191
+ record();
192
+ }
193
+ }
194
+ export { DiscoveryShadowEvaluator } from './discovery-shadow-evaluator.js';
195
+ //# sourceMappingURL=discovery-shadow.js.map
@@ -168,7 +168,11 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
168
168
  }
169
169
  }
170
170
  let status = getFortemiCoreSyncStatus(cwd, graph);
171
- if ((!status.optedIn || !status.built || status.stale) && graph === "framework") {
171
+ // The packaged framework index is a bootstrap fallback for an absent shared
172
+ // cache. Once a shared cache exists, its stale state proves that the local
173
+ // source index is newer; silently replacing it with an older packaged index
174
+ // would hide newly authored capabilities.
175
+ if ((!status.optedIn || !status.built) && !status.stale && graph === "framework") {
172
176
  const prebuilt = getFortemiCorePrebuiltStatus(graph);
173
177
  if (prebuilt.optedIn && prebuilt.built && !prebuilt.stale) {
174
178
  status = prebuilt;
@@ -182,7 +186,7 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
182
186
  if (!status.built || status.stale) {
183
187
  return {
184
188
  reason: `${status.reason ??
185
- `Fortemi Core static index for graph '${graph}' is stale or incomplete`}. Re-run 'aiwg index sync' or pass '--backend local' to use the legacy local index.`,
189
+ `Fortemi Core static index for graph '${graph}' is stale or incomplete`}. Re-run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' or pass '--backend local' to use the legacy local index.`,
186
190
  };
187
191
  }
188
192
  try {
@@ -15,6 +15,7 @@ import { createHash } from 'crypto';
15
15
  import { load as loadYaml } from 'js-yaml';
16
16
  import { validateDecisionDocument } from '../decision/validate.js';
17
17
  import { parseDecisionJson, parseDecisionYaml } from '../decision/entry.js';
18
+ import { validateGateDocument } from '../gates/schema.js';
18
19
  import { DEFAULT_INDEX_EXTENSIONS, INDEX_EXTRACTOR_VERSION, INDEX_VERSION, INDEX_DIR, OPERATIONAL_DISCOVERY_TYPES, PHASE_DIRECTORIES, GRAPH_CONFIGS, TEMPLATE_INDEX_EXTENSIONS, resolveGraphScanDir, loadUserGraphConfigs, loadGlobalGraphConfigs, } from './types.js';
19
20
  import { parseCitationSidecar, citationResultToEdges, buildRefToPathMap } from './citation-parser.js';
20
21
  import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
@@ -197,6 +198,42 @@ export function parseDecisionDoc(content, relativePath) {
197
198
  capability: purpose.slice(0, 240), tags: ['decision-system', value.kind], searchTerms: [...terms],
198
199
  };
199
200
  }
201
+ /**
202
+ * Classify and schema-check authored gate-pack documents (#2830).
203
+ *
204
+ * Packs live in `<bundle>/gate-packs/*.gatepack.yaml|json` (never `gates/`,
205
+ * which agent-persistence HITL uses). The `.gatepack.` extension is the
206
+ * boundary: HITL `gates/*.yaml` files must never classify as `gate-pack`.
207
+ * Invalid packs return null so they are never discoverable; the bundle
208
+ * loader rejects them at load with a diagnostic.
209
+ */
210
+ export function parseGatePackDoc(content, relativePath) {
211
+ if (!/\.gatepack\.(json|ya?ml)$/i.test(relativePath))
212
+ return null;
213
+ if (Buffer.byteLength(content, 'utf8') > 262_144)
214
+ return null;
215
+ let document;
216
+ try {
217
+ document = /\.json$/i.test(relativePath) ? parseDecisionJson(content) : parseDecisionYaml(content);
218
+ validateGateDocument(document);
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ const value = document;
224
+ if (value.kind !== 'GatePack')
225
+ return null;
226
+ const description = value.metadata.description;
227
+ const terms = new Set([
228
+ value.kind, value.metadata.id, value.metadata.version, description,
229
+ ...(value.spec.gates ?? []).map(gate => String(gate.id ?? '')).filter(Boolean),
230
+ ...Object.keys(value.spec.metrics ?? {}),
231
+ ]);
232
+ return {
233
+ type: 'gate-pack', kind: 'GatePack', name: value.metadata.id, description,
234
+ capability: description.slice(0, 240), tags: ['gates', value.kind], searchTerms: [...terms],
235
+ };
236
+ }
200
237
  function parseSchemaDoc(content, relativePath) {
201
238
  if (!/\.(json|ya?ml)$/i.test(relativePath))
202
239
  return null;
@@ -964,15 +1001,16 @@ export async function buildIndex(cwd, options = {}) {
964
1001
  // become discoverable (#1540).
965
1002
  const flow = parseFlowDoc(content, relativePath);
966
1003
  const decision = flow ? null : parseDecisionDoc(content, relativePath);
1004
+ const gatePack = flow || decision ? null : parseGatePackDoc(content, relativePath);
967
1005
  const inferredType = inferType(data, relativePath);
968
1006
  const physicalType = inferType({ ...data, type: undefined }, relativePath);
969
- const runbook = flow || decision ? null : parseRunbookDoc(data, body, relativePath);
1007
+ const runbook = flow || decision || gatePack ? null : parseRunbookDoc(data, body, relativePath);
970
1008
  const schemaDoc = inferredType === 'schema' ? parseSchemaDoc(content, relativePath) : null;
971
- const title = flow?.name ?? decision?.name ?? schemaDoc?.title ?? extractTitle(data, body);
1009
+ const title = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.title ?? extractTitle(data, body);
972
1010
  const phase = typeof data.phase === 'string' ? data.phase : inferPhase(relativePath);
973
- const type = flow?.type ?? decision?.type ?? (runbook ? 'runbook' : inferredType);
974
- const tags = flow?.tags ?? decision?.tags ?? (Array.isArray(data.tags) ? data.tags.map(String) : []);
975
- const summary = flow?.description ?? decision?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
1011
+ const type = flow?.type ?? decision?.type ?? gatePack?.type ?? (runbook ? 'runbook' : inferredType);
1012
+ const tags = flow?.tags ?? decision?.tags ?? gatePack?.tags ?? (Array.isArray(data.tags) ? data.tags.map(String) : []);
1013
+ const summary = flow?.description ?? decision?.description ?? gatePack?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
976
1014
  const dependencies = extractMentions(content);
977
1015
  const markdownLinks = extractMarkdownLinks(content);
978
1016
  // Discovery metadata (#1214, #1540, #1792) — meaningful for operational
@@ -982,10 +1020,10 @@ export async function buildIndex(cwd, options = {}) {
982
1020
  // Declarative processes have no trigger phrases — they rely on their
983
1021
  // capability and structure-aware search terms.
984
1022
  const triggers = isDiscoverable && !flow ? extractTriggers(body, data) : undefined;
985
- const capability = flow?.capability ?? decision?.capability ?? schemaDoc?.capability ?? runbook?.capability ?? (isDiscoverable ? extractCapability(data, body) : undefined);
986
- const kind = flow?.kind ?? decision?.kind ?? runbook?.kind;
1023
+ const capability = flow?.capability ?? decision?.capability ?? gatePack?.capability ?? schemaDoc?.capability ?? runbook?.capability ?? (isDiscoverable ? extractCapability(data, body) : undefined);
1024
+ const kind = flow?.kind ?? decision?.kind ?? gatePack?.kind ?? runbook?.kind;
987
1025
  const sourceType = runbook && physicalType !== 'runbook' ? physicalType : undefined;
988
- const searchTerms = flow?.searchTerms ?? decision?.searchTerms ?? schemaDoc?.searchTerms ?? runbook?.searchTerms;
1026
+ const searchTerms = flow?.searchTerms ?? decision?.searchTerms ?? gatePack?.searchTerms ?? schemaDoc?.searchTerms ?? runbook?.searchTerms;
989
1027
  const kernel = data.kernel === true || data.kernel === 'true' ? true : undefined;
990
1028
  // Script entrypoint metadata is meaningful for skills only (#1227).
991
1029
  const script = type === 'skill' ? extractSkillScript(data) : undefined;
@@ -994,7 +1032,7 @@ export async function buildIndex(cwd, options = {}) {
994
1032
  // Canonical short name (#1233) — used by the scorer to floor exact-name
995
1033
  // queries to 1.0 so hyphenated kernel-skill names like `aiwg-doctor`
996
1034
  // remain searchable even when the rendered title strips the hyphen.
997
- const name = flow?.name ?? decision?.name ?? schemaDoc?.name ?? (isDiscoverable ? extractCanonicalName(data, relativePath) : undefined);
1035
+ const name = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.name ?? (isDiscoverable ? extractCanonicalName(data, relativePath) : undefined);
998
1036
  entry = {
999
1037
  path: relativePath,
1000
1038
  type,