@aiwg/cli 2026.9.24 → 2026.10.1

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 (209) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  3. package/dist/src/artifacts/discovery-shadow.js +195 -0
  4. package/dist/src/artifacts/index-builder.js +47 -9
  5. package/dist/src/artifacts/query-engine.js +94 -1
  6. package/dist/src/artifacts/types.js +1 -0
  7. package/dist/src/cli/handlers/decision.js +177 -0
  8. package/dist/src/cli/handlers/gates.js +154 -0
  9. package/dist/src/cli/handlers/index.js +5 -1
  10. package/dist/src/cli/handlers/sessions.js +10 -7
  11. package/dist/src/cli/handlers/utilities.js +20 -0
  12. package/dist/src/config/aiwg-config.js +15 -2
  13. package/dist/src/decision/acceptance.js +25 -0
  14. package/dist/src/decision/adapters/jev.js +29 -3
  15. package/dist/src/decision/ask.js +352 -0
  16. package/dist/src/decision/calibration/registry.js +9 -0
  17. package/dist/src/decision/compose.js +74 -15
  18. package/dist/src/decision/context-live-promotion.js +344 -0
  19. package/dist/src/decision/context-live-qualification.js +591 -0
  20. package/dist/src/decision/context-pruning.js +847 -0
  21. package/dist/src/decision/driver.js +432 -0
  22. package/dist/src/decision/egress-live-qualification.js +874 -0
  23. package/dist/src/decision/ensemble/contract.js +64 -1
  24. package/dist/src/decision/ensemble/index.js +1 -0
  25. package/dist/src/decision/ensemble/report.js +8 -42
  26. package/dist/src/decision/ensemble/runtime.js +571 -0
  27. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  28. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  29. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  30. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  31. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  32. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  33. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  34. package/dist/src/decision/ensemble-study/score.js +462 -0
  35. package/dist/src/decision/ensemble-study/staged.js +87 -0
  36. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  37. package/dist/src/decision/evaluate.js +227 -14
  38. package/dist/src/decision/export.js +4 -0
  39. package/dist/src/decision/feature-export.js +1074 -0
  40. package/dist/src/decision/graph-index.js +16 -0
  41. package/dist/src/decision/graph-live-qualification.js +1041 -0
  42. package/dist/src/decision/graph-live-workload.js +245 -0
  43. package/dist/src/decision/graph-skill-bridge.js +21 -3
  44. package/dist/src/decision/heldout/calibration.js +140 -0
  45. package/dist/src/decision/heldout/collector.js +430 -0
  46. package/dist/src/decision/heldout/contract.js +309 -0
  47. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  50. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  51. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  52. package/dist/src/decision/heldout/d29-pools.js +288 -0
  53. package/dist/src/decision/heldout/d29-v6.js +169 -0
  54. package/dist/src/decision/heldout/d29-v7.js +202 -0
  55. package/dist/src/decision/heldout/d29-v8.js +586 -0
  56. package/dist/src/decision/heldout/generator-registry.js +67 -0
  57. package/dist/src/decision/heldout/generators.js +696 -0
  58. package/dist/src/decision/heldout/index.js +8 -0
  59. package/dist/src/decision/heldout/journal.js +346 -0
  60. package/dist/src/decision/heldout/study-generators.js +64 -0
  61. package/dist/src/decision/heldout/types.js +2 -0
  62. package/dist/src/decision/host-policy.js +61 -0
  63. package/dist/src/decision/index.js +13 -0
  64. package/dist/src/decision/interop-profile.v1.json +112 -0
  65. package/dist/src/decision/interop.js +653 -0
  66. package/dist/src/decision/issue-triage/index.js +3 -0
  67. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  68. package/dist/src/decision/issue-triage/types.js +2 -0
  69. package/dist/src/decision/jev-credentials.js +121 -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/quality.js +57 -49
  77. package/dist/src/decision/qualification/release.js +76 -3
  78. package/dist/src/decision/qualification/runner.js +5 -2
  79. package/dist/src/decision/routing/contract.js +175 -0
  80. package/dist/src/decision/routing/index.js +5 -0
  81. package/dist/src/decision/routing/report.js +216 -0
  82. package/dist/src/decision/routing/runtime.js +685 -0
  83. package/dist/src/decision/routing/types.js +2 -0
  84. package/dist/src/decision/sdlc-screening.js +1157 -0
  85. package/dist/src/decision/sensitivity/comparative.js +154 -0
  86. package/dist/src/decision/sensitivity/contract.js +218 -0
  87. package/dist/src/decision/sensitivity/index.js +5 -0
  88. package/dist/src/decision/sensitivity/runtime.js +912 -0
  89. package/dist/src/decision/sensitivity/types.js +2 -0
  90. package/dist/src/decision/validate.js +26 -0
  91. package/dist/src/extensions/commands/definitions.js +38 -0
  92. package/dist/src/extensions/manifest.js +44 -0
  93. package/dist/src/gates/discovery.js +315 -0
  94. package/dist/src/gates/driver.js +388 -0
  95. package/dist/src/gates/evaluate.js +719 -0
  96. package/dist/src/gates/floors.js +255 -0
  97. package/dist/src/gates/index.js +17 -0
  98. package/dist/src/gates/providers/evidence.js +38 -0
  99. package/dist/src/gates/providers/index.js +21 -0
  100. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  101. package/dist/src/gates/providers/loader.js +1069 -0
  102. package/dist/src/gates/providers/paired.js +52 -0
  103. package/dist/src/gates/providers/proportion.js +60 -0
  104. package/dist/src/gates/providers/registry.js +40 -0
  105. package/dist/src/gates/providers/scalar.js +41 -0
  106. package/dist/src/gates/providers/screening.js +53 -0
  107. package/dist/src/gates/providers/types.js +25 -0
  108. package/dist/src/gates/registry.js +1003 -0
  109. package/dist/src/gates/report.js +102 -0
  110. package/dist/src/gates/schema.js +56 -0
  111. package/dist/src/gates/stats/binomial.js +26 -0
  112. package/dist/src/gates/stats/bootstrap.js +62 -0
  113. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  114. package/dist/src/gates/stats/digest.js +26 -0
  115. package/dist/src/gates/stats/error.js +5 -0
  116. package/dist/src/gates/stats/index.js +9 -0
  117. package/dist/src/gates/stats/normal.js +35 -0
  118. package/dist/src/gates/stats/paired.js +120 -0
  119. package/dist/src/gates/stats/splits.js +30 -0
  120. package/dist/src/gates/types.js +8 -0
  121. package/dist/src/governance/redaction.js +14 -4
  122. package/dist/src/mcp/muse-native-config.mjs +123 -0
  123. package/dist/src/mcp/tools/decision.mjs +228 -0
  124. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  125. package/dist/src/providers/capability-matrix.yaml +2 -1
  126. package/dist/src/providers/provider-definitions.js +2 -1
  127. package/dist/src/sessions/adapters/muse.js +336 -39
  128. package/dist/src/sessions/batch-import.js +7 -0
  129. package/dist/src/sessions/workspace-discovery.js +13 -2
  130. package/package.json +1 -1
  131. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  132. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  133. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  134. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  135. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  136. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  137. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  138. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  139. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  140. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  141. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  142. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  143. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  144. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  145. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  146. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  147. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  148. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  149. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  150. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  151. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  152. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  153. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  154. package/schemas/decision/D29Study.v3.schema.json +609 -0
  155. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  156. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  157. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  158. package/schemas/decision/D29Study.v8.schema.json +671 -0
  159. package/schemas/decision/D29Study.v9.schema.json +778 -0
  160. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  161. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  162. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  163. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  164. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  165. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  166. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  167. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  168. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  169. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  170. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  171. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  172. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  173. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  174. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  175. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  176. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  177. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  178. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  179. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  180. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  181. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  182. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  183. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  184. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  185. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  186. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  187. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  188. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  189. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  190. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  191. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  192. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  193. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  194. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  195. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  196. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  197. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  198. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  199. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  200. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  201. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  202. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  203. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  204. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  205. package/tools/agents/deploy-agents.mjs +1 -1
  206. package/tools/agents/providers/base.mjs +40 -18
  207. package/tools/agents/providers/factory.mjs +10 -2
  208. package/tools/agents/providers/muse-hooks.mjs +1 -29
  209. 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:
@@ -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
@@ -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,
@@ -1142,6 +1142,19 @@ export async function discoverCapability(cwd, params) {
1142
1142
  }
1143
1143
  const scored = dedupeDiscoverResults(await applyFacetFusion(Array.from(combinedByPath.values()).sort(compareDiscoverResults), candidates, params.phrase)).slice(0, limit);
1144
1144
  const relaxed = scored.some(result => !strictPaths.has(result.entry.path));
1145
+ if (params.shadowRoute) {
1146
+ try {
1147
+ await params.shadowRoute.observe(params.phrase, scored.map(result => ({
1148
+ id: discoveryIdForEntry(result.entry), name: result.entry.name ?? result.entry.title,
1149
+ type: result.entry.type, capability: result.entry.capability ?? result.entry.summary,
1150
+ score: result.score,
1151
+ })));
1152
+ }
1153
+ catch {
1154
+ // An optional observation failure must never replace deterministic discovery.
1155
+ params.shadowRoute.disable();
1156
+ }
1157
+ }
1145
1158
  const queryTimeMs = Date.now() - startTime;
1146
1159
  /**
1147
1160
  * Resolve a stored framework-graph path to an absolute AIWG_ROOT
@@ -1474,7 +1487,15 @@ function findShowMatches(entries, types, needle) {
1474
1487
  const slugLayoutMatch = ((basename === 'SKILL.md' && e.type === 'skill') ||
1475
1488
  (basename === 'BEHAVIOR.md' && e.type === 'behavior')) &&
1476
1489
  dirStem === needle;
1477
- return slugLayoutMatch || fileStem === needle || e.name === needle;
1490
+ if (slugLayoutMatch || fileStem === needle || e.name === needle)
1491
+ return true;
1492
+ if (e.type === 'gate-pack' && typeof e.name === 'string') {
1493
+ const gateStem = fileStem.replace(/\.gatepack$/i, '');
1494
+ const rest = e.name.includes(':') ? e.name.slice(e.name.indexOf(':') + 1) : e.name;
1495
+ const short = rest.includes('/') ? rest.slice(rest.indexOf('/') + 1) : rest;
1496
+ return gateStem === needle || rest === needle || short === needle;
1497
+ }
1498
+ return false;
1478
1499
  });
1479
1500
  }
1480
1501
  if (matches.length === 0) {
@@ -1526,6 +1547,68 @@ async function fortemiRecordForEntry(cwd, entry, sourcePaths) {
1526
1547
  const id = discoveryIdForEntry(entry);
1527
1548
  return loaded.exported.items.find((record) => record.id === id || record.source.path === entry.path) ?? null;
1528
1549
  }
1550
+ /**
1551
+ * Scan bundle `gate-packs/` directories for a pack matching `needle` (#2830).
1552
+ *
1553
+ * Accepts the full namespaced id, the rest-path or the short pack name and
1554
+ * probes `<bundle>/gate-packs/<pack>.gatepack.{yaml,yml,json}`. Never looks
1555
+ * under `gates/`, which agent-persistence HITL owns.
1556
+ */
1557
+ async function findGatePackInCorpus(groups, needle) {
1558
+ const path = await import('node:path');
1559
+ const fsp = (await import('node:fs')).promises;
1560
+ const trimmed = needle.trim();
1561
+ const rest = trimmed.includes(':') ? trimmed.slice(trimmed.indexOf(':') + 1) : trimmed;
1562
+ const slash = rest.indexOf('/');
1563
+ const wantedBundle = slash >= 0 ? rest.slice(0, slash) : null;
1564
+ const wantedPack = slash >= 0 ? rest.slice(slash + 1) : rest;
1565
+ if (!wantedPack || /\s/.test(wantedPack) || wantedPack.includes(':') || wantedPack.includes('/'))
1566
+ return null;
1567
+ const extensions = ['.gatepack.yaml', '.gatepack.yml', '.gatepack.json'];
1568
+ const ordered = wantedBundle
1569
+ ? groups.flatMap(group => [{ group, bundle: wantedBundle }])
1570
+ : null;
1571
+ const probe = async (group, bundle) => {
1572
+ for (const extension of extensions) {
1573
+ const candidate = path.join(group.dir, bundle, 'gate-packs', `${wantedPack}${extension}`);
1574
+ try {
1575
+ const stat = await fsp.stat(candidate);
1576
+ if (stat.isFile())
1577
+ return { path: candidate, bundleKind: group.kind, bundleId: bundle };
1578
+ }
1579
+ catch {
1580
+ // not present — continue
1581
+ }
1582
+ }
1583
+ return null;
1584
+ };
1585
+ if (ordered) {
1586
+ for (const { group, bundle } of ordered) {
1587
+ const found = await probe(group, bundle);
1588
+ if (found)
1589
+ return found;
1590
+ }
1591
+ return null;
1592
+ }
1593
+ for (const group of groups) {
1594
+ let bundles;
1595
+ try {
1596
+ bundles = (await fsp.readdir(group.dir, { withFileTypes: true }))
1597
+ .filter(d => d.isDirectory())
1598
+ .map(d => d.name)
1599
+ .sort();
1600
+ }
1601
+ catch {
1602
+ continue;
1603
+ }
1604
+ for (const bundle of bundles) {
1605
+ const found = await probe(group, bundle);
1606
+ if (found)
1607
+ return found;
1608
+ }
1609
+ }
1610
+ return null;
1611
+ }
1529
1612
  /**
1530
1613
  * Scan the AIWG_ROOT corpus for an artifact matching `name` (#1221).
1531
1614
  *
@@ -1592,6 +1675,16 @@ async function findCorpusArtifact(aiwgRoot, name, typeFilter) {
1592
1675
  }
1593
1676
  }
1594
1677
  }
1678
+ // Gate packs live in `<bundle>/gate-packs/*.gatepack.yaml|json` (never
1679
+ // `gates/`, which agent-persistence HITL uses). The needle may be the full
1680
+ // namespaced id (`aiwg:decision-engine/integrity-ceiling`), the rest-path
1681
+ // (`decision-engine/integrity-ceiling`) or the short pack name
1682
+ // (`integrity-ceiling`). (#2830)
1683
+ if (typeFilter.length === 0 || typeFilter.includes('gate-pack')) {
1684
+ const gateMatch = await findGatePackInCorpus(groups, name);
1685
+ if (gateMatch)
1686
+ return { ...gateMatch, type: 'gate-pack' };
1687
+ }
1595
1688
  // Top-level agents live at `agentic/code/agents/<category>/<name>.md`
1596
1689
  // (e.g. `personas/aiwg-writer.md`) — a flat-under-category layout the
1597
1690
  // bundle groups above do not cover. Scan it directly so persona/agent
@@ -28,6 +28,7 @@ export const OPERATIONAL_DISCOVERY_TYPES = [
28
28
  'decision-definition',
29
29
  'decision-ruleset',
30
30
  'decision-binding',
31
+ 'gate-pack',
31
32
  'flow',
32
33
  'runbook',
33
34
  'template',