@dzhechkov/harness-core 0.8.5 → 0.8.10

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 (134) hide show
  1. package/.dz-manifest.json +286 -82
  2. package/README.md +54 -2
  3. package/dist/amendment-trace.d.ts.map +1 -1
  4. package/dist/amendment-trace.js +12 -1
  5. package/dist/amendment-trace.js.map +1 -1
  6. package/dist/compounding.d.ts +54 -0
  7. package/dist/compounding.d.ts.map +1 -1
  8. package/dist/compounding.js +221 -1
  9. package/dist/compounding.js.map +1 -1
  10. package/dist/eta.d.ts +92 -0
  11. package/dist/eta.d.ts.map +1 -0
  12. package/dist/eta.js +488 -0
  13. package/dist/eta.js.map +1 -0
  14. package/dist/feature-adr-checkpoints.d.ts +11 -2
  15. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  16. package/dist/feature-adr-checkpoints.js +38 -3
  17. package/dist/feature-adr-checkpoints.js.map +1 -1
  18. package/dist/feature-adr-decision-recall.d.ts +167 -0
  19. package/dist/feature-adr-decision-recall.d.ts.map +1 -0
  20. package/dist/feature-adr-decision-recall.js +519 -0
  21. package/dist/feature-adr-decision-recall.js.map +1 -0
  22. package/dist/feature-adr-landing.d.ts +37 -0
  23. package/dist/feature-adr-landing.d.ts.map +1 -0
  24. package/dist/feature-adr-landing.js +59 -0
  25. package/dist/feature-adr-landing.js.map +1 -0
  26. package/dist/feature-adr-routing.d.ts +58 -23
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +194 -69
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard-promotion.d.ts +41 -0
  31. package/dist/guard-promotion.d.ts.map +1 -1
  32. package/dist/guard-promotion.js +218 -4
  33. package/dist/guard-promotion.js.map +1 -1
  34. package/dist/guard-volume.d.ts +108 -0
  35. package/dist/guard-volume.d.ts.map +1 -0
  36. package/dist/guard-volume.js +536 -0
  37. package/dist/guard-volume.js.map +1 -0
  38. package/dist/guard.d.ts +34 -0
  39. package/dist/guard.d.ts.map +1 -1
  40. package/dist/guard.js +113 -4
  41. package/dist/guard.js.map +1 -1
  42. package/dist/index.d.ts +18 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +12 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/integration-apply.d.ts +25 -0
  47. package/dist/integration-apply.d.ts.map +1 -0
  48. package/dist/integration-apply.js +299 -0
  49. package/dist/integration-apply.js.map +1 -0
  50. package/dist/integration-evidence.d.ts +46 -0
  51. package/dist/integration-evidence.d.ts.map +1 -0
  52. package/dist/integration-evidence.js +44 -0
  53. package/dist/integration-evidence.js.map +1 -0
  54. package/dist/integration-probe-worker.d.ts +22 -0
  55. package/dist/integration-probe-worker.d.ts.map +1 -0
  56. package/dist/integration-probe-worker.js +334 -0
  57. package/dist/integration-probe-worker.js.map +1 -0
  58. package/dist/integrations-verify.d.ts +60 -0
  59. package/dist/integrations-verify.d.ts.map +1 -0
  60. package/dist/integrations-verify.js +194 -0
  61. package/dist/integrations-verify.js.map +1 -0
  62. package/dist/lesson-generalization.d.ts +29 -0
  63. package/dist/lesson-generalization.d.ts.map +1 -0
  64. package/dist/lesson-generalization.js +84 -0
  65. package/dist/lesson-generalization.js.map +1 -0
  66. package/dist/loop-blobs.generated.js +8 -8
  67. package/dist/loop-blobs.generated.js.map +1 -1
  68. package/dist/loop-plan.d.ts +13 -1
  69. package/dist/loop-plan.d.ts.map +1 -1
  70. package/dist/loop-plan.js +15 -1
  71. package/dist/loop-plan.js.map +1 -1
  72. package/dist/loop-render.d.ts.map +1 -1
  73. package/dist/loop-render.js +51 -6
  74. package/dist/loop-render.js.map +1 -1
  75. package/dist/loop-trace.d.ts +20 -1
  76. package/dist/loop-trace.d.ts.map +1 -1
  77. package/dist/loop-trace.js +83 -1
  78. package/dist/loop-trace.js.map +1 -1
  79. package/dist/model-recommender.d.ts +8 -0
  80. package/dist/model-recommender.d.ts.map +1 -1
  81. package/dist/model-recommender.js +31 -4
  82. package/dist/model-recommender.js.map +1 -1
  83. package/dist/operations.d.ts +27 -0
  84. package/dist/operations.d.ts.map +1 -1
  85. package/dist/operations.js +178 -2
  86. package/dist/operations.js.map +1 -1
  87. package/dist/patterns.d.ts +27 -1
  88. package/dist/patterns.d.ts.map +1 -1
  89. package/dist/patterns.js +211 -45
  90. package/dist/patterns.js.map +1 -1
  91. package/dist/setup.d.ts.map +1 -1
  92. package/dist/setup.js +20 -17
  93. package/dist/setup.js.map +1 -1
  94. package/dist/target-integrations.d.ts +65 -0
  95. package/dist/target-integrations.d.ts.map +1 -0
  96. package/dist/target-integrations.js +152 -0
  97. package/dist/target-integrations.js.map +1 -0
  98. package/dist/vector-tier.d.ts +6 -1
  99. package/dist/vector-tier.d.ts.map +1 -1
  100. package/dist/vector-tier.js +32 -7
  101. package/dist/vector-tier.js.map +1 -1
  102. package/dist/workflow-run.d.ts.map +1 -1
  103. package/dist/workflow-run.js +18 -12
  104. package/dist/workflow-run.js.map +1 -1
  105. package/package.json +19 -15
  106. package/sbom.json +591 -81
  107. package/src/amendment-trace.ts +12 -1
  108. package/src/compounding.ts +300 -1
  109. package/src/eta.ts +590 -0
  110. package/src/feature-adr-checkpoints.ts +39 -3
  111. package/src/feature-adr-decision-recall.ts +652 -0
  112. package/src/feature-adr-landing.ts +109 -0
  113. package/src/feature-adr-routing.ts +224 -65
  114. package/src/guard-promotion.ts +245 -4
  115. package/src/guard-volume.ts +752 -0
  116. package/src/guard.ts +148 -4
  117. package/src/index.ts +65 -3
  118. package/src/integration-apply.ts +332 -0
  119. package/src/integration-evidence.ts +89 -0
  120. package/src/integration-probe-worker.ts +310 -0
  121. package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
  122. package/src/integrations-verify.ts +258 -0
  123. package/src/lesson-generalization.ts +115 -0
  124. package/src/loop-blobs.generated.ts +8 -8
  125. package/src/loop-plan.ts +36 -3
  126. package/src/loop-render.ts +50 -6
  127. package/src/loop-trace.ts +91 -2
  128. package/src/model-recommender.ts +35 -4
  129. package/src/operations.ts +199 -2
  130. package/src/patterns.ts +252 -43
  131. package/src/setup.ts +20 -17
  132. package/src/target-integrations.ts +225 -0
  133. package/src/vector-tier.ts +44 -14
  134. package/src/workflow-run.ts +18 -11
@@ -92,7 +92,9 @@ export function harvestStageOutcomes(records: readonly unknown[]): Harvest {
92
92
  if (!isRecord(modelsUsed)) { skipped.noModels += 1; continue; }
93
93
  const grade = extractGrade(result);
94
94
  if (grade === null) { skipped.noGrade += 1; continue; }
95
- const runId = typeof source['runId'] === 'string' ? source['runId'] : JSON.stringify(modelsUsed).slice(0, 40);
95
+ // Full canonical JSON is intentional: the former 40-character prefix merged distinct records
96
+ // whose stage names shared a long prefix, manufacturing idempotency across different runs.
97
+ const runId = typeof source['runId'] === 'string' ? source['runId'] : `models:${JSON.stringify(modelsUsed)}`;
96
98
  const ts = typeof source['timestamp'] === 'string' ? source['timestamp'] : null;
97
99
  const tier = typeof result['tier'] === 'string' && result['tier'] !== '' ? result['tier'] : 'unknown';
98
100
  let contributed = false;
@@ -138,6 +140,8 @@ export interface Recommendation {
138
140
  readonly rule: string;
139
141
  readonly skipped: Harvest['skipped'];
140
142
  readonly crossFamilyNote: string;
143
+ readonly unfed: { readonly count: number; readonly runIds: string[] };
144
+ readonly freshness: 'current' | 'stale' | 'unfed';
141
145
  };
142
146
  }
143
147
 
@@ -154,7 +158,7 @@ const CROSS_NOTE =
154
158
  'the qe pick is computed with the family FORCED to the cross of the code pick — a same-family qe recommendation is unrepresentable (ADR-001 D3)';
155
159
 
156
160
  /** Recommend per stage over the harvested samples (optionally one tier's slice). */
157
- export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number } = {}): Recommendation {
161
+ export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number; alreadyFed?: readonly string[] } = {}): Recommendation {
158
162
  const slice = opts.tier === undefined ? harvest.samples : harvest.samples.filter((s) => s.tier === opts.tier);
159
163
  const tierLabel = opts.tier ?? 'all';
160
164
  const byStage = new Map<string, HarvestSample[]>();
@@ -197,9 +201,21 @@ export function recommendModels(harvest: Harvest, opts: { tier?: string; quality
197
201
  }
198
202
  }
199
203
  if (!stages.includes('code')) perStage.unshift(code);
204
+ const pending = unfedRuns(harvest.samples, opts.alreadyFed ?? []);
205
+ const harvestRunIds = [...new Set(harvest.samples.map((s) => s.runId))];
206
+ const fed = new Set(opts.alreadyFed ?? []);
207
+ const fedInHarvest = harvestRunIds.filter((runId) => fed.has(runId)).length;
200
208
  return {
201
209
  perStage,
202
- basis: { runsUsed: harvest.runsUsed, window: harvest.window, rule: harvest.rule, skipped: harvest.skipped, crossFamilyNote: CROSS_NOTE },
210
+ basis: {
211
+ runsUsed: harvest.runsUsed,
212
+ window: harvest.window,
213
+ rule: harvest.rule,
214
+ skipped: harvest.skipped,
215
+ crossFamilyNote: CROSS_NOTE,
216
+ unfed: { count: pending.length, runIds: pending },
217
+ freshness: pending.length === 0 ? 'current' : fedInHarvest === 0 ? 'unfed' : 'stale',
218
+ },
203
219
  };
204
220
  }
205
221
 
@@ -213,15 +229,30 @@ export interface FeedPlan {
213
229
  readonly fedAfter: string[];
214
230
  }
215
231
 
232
+ /** One shared definition of the telemetry/store gap, used by recommendation, apply and guard. */
233
+ export function unfedRuns(samples: readonly HarvestSample[], alreadyFed: readonly string[]): string[] {
234
+ const fed = new Set(alreadyFed);
235
+ return [...new Set(samples.map((s) => s.runId))].filter((runId) => !fed.has(runId)).sort();
236
+ }
237
+
216
238
  /** A run feeds ONCE. Double-feeding the same telemetry manufactures confidence the data does not
217
239
  * contain — the second `--apply` must feed 0 and say which runs it skipped. */
218
240
  export function planFeed(samples: readonly HarvestSample[], alreadyFed: readonly string[]): FeedPlan {
219
241
  const fed = new Set(alreadyFed);
220
242
  const toFeed: HarvestSample[] = [];
221
243
  const skippedRuns = new Set<string>();
244
+ const seenSamples = new Set<string>();
222
245
  for (const s of samples) {
223
246
  if (fed.has(s.runId)) skippedRuns.add(s.runId);
224
- else toFeed.push(s);
247
+ else {
248
+ // A multi-stage run legitimately feeds one sample per stage. Only a repeated sample from a
249
+ // duplicated record is suppressed within this harvest.
250
+ const sampleKey = JSON.stringify([s.runId, s.stage]);
251
+ if (!seenSamples.has(sampleKey)) {
252
+ seenSamples.add(sampleKey);
253
+ toFeed.push(s);
254
+ }
255
+ }
225
256
  }
226
257
  const fedAfter = [...new Set([...alreadyFed, ...toFeed.map((s) => s.runId)])].sort();
227
258
  return { toFeed, skippedRuns: [...skippedRuns].sort(), fedAfter };
package/src/operations.ts CHANGED
@@ -57,6 +57,17 @@ import { describeSkillLoadFailure, discoverSkillIds, loadSkillFromDir } from './
57
57
  import type { SkillApplyFailure, SkillLoadFailure } from './skills.js';
58
58
  import { TARGETS } from './targets.js';
59
59
  import type { TargetName } from './targets.js';
60
+ import {
61
+ TARGET_INTEGRATIONS,
62
+ aggregateIntegrationManifests,
63
+ notRequestedOutcomes,
64
+ refusedOutcome,
65
+ staticPolicyOutcomes,
66
+ type IntegrationOutcome,
67
+ type IntegrationManifestSource,
68
+ } from './target-integrations.js';
69
+ import { applyIntegrationFragments, IntegrationApplyError, type IntegrationApplyFault } from './integration-apply.js';
70
+ import { verifyTargetIntegration, type IntegrationProcessPort } from './integrations-verify.js';
60
71
  import {
61
72
  AGENTS_MD_BUDGET_WARN_FRACTION,
62
73
  CODEX_PROJECT_DOC_MAX_BYTES,
@@ -87,6 +98,24 @@ export interface InitOptions {
87
98
  readonly select?: readonly string[];
88
99
  /** When set, generate platform-specific enrichment files alongside SKILL.md. */
89
100
  readonly enrich?: boolean;
101
+ /** Exact content-bound digest printed by the first integration-aware run. */
102
+ readonly allowIntegrations?: string;
103
+ /** Explicit hook opt-out; maps hooks to not-requested. */
104
+ readonly noHooks?: boolean;
105
+ /** Explicit skills-only opt-out, even when selected skills carry manifests. */
106
+ readonly noIntegrations?: boolean;
107
+ /** Live verification is load-bearing; requesting this flag makes requested integrations refuse. */
108
+ readonly noVerify?: boolean;
109
+ /** Injectable process boundary for deterministic integration tests. */
110
+ readonly integrationProcessPort?: IntegrationProcessPort;
111
+ /** Test-only fault at the carrier/ownership-journal durability boundary. */
112
+ readonly integrationApplyFault?: IntegrationApplyFault;
113
+ /**
114
+ * Install-level manifest sources. The CLI supplies this only on the first
115
+ * per-directory run so companion planning, authorization, and probing happen
116
+ * exactly once across every discovered skill pack.
117
+ */
118
+ readonly integrationManifestSources?: readonly IntegrationManifestSource[];
90
119
  }
91
120
 
92
121
  /** Per-skill outcome of {@link runInit}. */
@@ -130,6 +159,10 @@ export interface InitReport {
130
159
  * any per-skill loop.
131
160
  */
132
161
  readonly applyFailures: readonly SkillApplyFailure[];
162
+ /** Exactly two ordered companion outcomes: MCP, then hooks. */
163
+ readonly integrations: readonly [IntegrationOutcome, IntegrationOutcome];
164
+ /** Safe digest used by `--allow-integrations`; absent when no manifest exists. */
165
+ readonly integrationDigest?: string;
133
166
  }
134
167
 
135
168
  // ---------------------------------------------------------------------------
@@ -266,6 +299,8 @@ export interface AgentsMdInitOptions {
266
299
  readonly projectRoot: string;
267
300
  /** When set, install only these skill ids (a preset selection). */
268
301
  readonly select?: readonly string[] | undefined;
302
+ readonly noHooks?: boolean;
303
+ readonly noIntegrations?: boolean;
269
304
  }
270
305
 
271
306
  /** The parts of a single-file managed-Markdown target that vary by filename. */
@@ -367,6 +402,28 @@ function runInitSingleFileMd(options: AgentsMdInitOptions, config: SingleFileMdC
367
402
  }
368
403
  const missing = selection === undefined ? [] : selection.filter((id) => !discovered.has(id));
369
404
 
405
+ let aggregate = { manifest: undefined, digest: undefined } as ReturnType<typeof aggregateIntegrationManifests>;
406
+ try {
407
+ if (options.noIntegrations !== true) {
408
+ aggregate = aggregateIntegrationManifests(picked.map(({ id, skillsDir }) => ({ skillId: id, skillDir: skillsDir })));
409
+ }
410
+ } catch (error) {
411
+ const remediation = error instanceof Error ? error.message : String(error);
412
+ return {
413
+ target: config.target,
414
+ skillsDir: joinedDir,
415
+ projectRoot: options.projectRoot,
416
+ skills: [],
417
+ missing,
418
+ failures: [],
419
+ applyFailures: [],
420
+ integrations: [
421
+ refusedOutcome(config.target, 'mcp', 'MANIFEST_INVALID', remediation),
422
+ refusedOutcome(config.target, 'hooks', 'MANIFEST_INVALID', remediation),
423
+ ],
424
+ };
425
+ }
426
+
370
427
  // Skip-and-collect: one unloadable skill must not discard the whole aggregation.
371
428
  const failures: SkillLoadFailure[] = [];
372
429
  const loaded: { id: string; section: string }[] = [];
@@ -397,7 +454,19 @@ function runInitSingleFileMd(options: AgentsMdInitOptions, config: SingleFileMdC
397
454
 
398
455
  // No apply failures are possible here: the single write is outside every per-skill
399
456
  // loop, so a write error propagates as itself rather than being attributed to a skill.
400
- return { target: config.target, skillsDir: joinedDir, projectRoot: options.projectRoot, skills, missing, failures, applyFailures: [] };
457
+ return {
458
+ target: config.target,
459
+ skillsDir: joinedDir,
460
+ projectRoot: options.projectRoot,
461
+ skills,
462
+ missing,
463
+ failures,
464
+ applyFailures: [],
465
+ integrations: options.noIntegrations === true
466
+ ? notRequestedOutcomes(config.target)
467
+ : staticPolicyOutcomes(config.target, aggregate.manifest, options.noHooks === true),
468
+ ...(aggregate.digest !== undefined ? { integrationDigest: aggregate.digest } : {}),
469
+ };
401
470
  }
402
471
 
403
472
  /**
@@ -546,6 +615,8 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
546
615
  skillsDirs: [options.skillsDir],
547
616
  projectRoot: options.projectRoot,
548
617
  select: options.select,
618
+ ...(options.noHooks !== undefined ? { noHooks: options.noHooks } : {}),
619
+ ...(options.noIntegrations !== undefined ? { noIntegrations: options.noIntegrations } : {}),
549
620
  });
550
621
  }
551
622
  // gemini is likewise a flattening single-file target — aggregate into ONE root
@@ -555,6 +626,8 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
555
626
  skillsDirs: [options.skillsDir],
556
627
  projectRoot: options.projectRoot,
557
628
  select: options.select,
629
+ ...(options.noHooks !== undefined ? { noHooks: options.noHooks } : {}),
630
+ ...(options.noIntegrations !== undefined ? { noIntegrations: options.noIntegrations } : {}),
558
631
  });
559
632
  }
560
633
 
@@ -568,6 +641,78 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
568
641
  const missing = selection === undefined
569
642
  ? []
570
643
  : selection.filter((id) => !discovered.includes(id));
644
+ const manifestSources: readonly IntegrationManifestSource[] = options.integrationManifestSources
645
+ ?? ids.map((id) => ({ skillId: id, skillDir: options.skillsDir }));
646
+ let aggregate = { manifest: undefined, digest: undefined } as ReturnType<typeof aggregateIntegrationManifests>;
647
+ try {
648
+ if (options.noIntegrations !== true) aggregate = aggregateIntegrationManifests(manifestSources);
649
+ } catch (error) {
650
+ const remediation = error instanceof Error ? error.message : String(error);
651
+ return {
652
+ target: options.target,
653
+ skillsDir: options.skillsDir,
654
+ projectRoot: options.projectRoot,
655
+ skills: [],
656
+ missing,
657
+ failures: [],
658
+ applyFailures: [],
659
+ integrations: [
660
+ refusedOutcome(options.target, 'mcp', 'MANIFEST_INVALID', remediation),
661
+ refusedOutcome(options.target, 'hooks', 'MANIFEST_INVALID', remediation),
662
+ ],
663
+ };
664
+ }
665
+
666
+ let integrations: readonly [IntegrationOutcome, IntegrationOutcome] = notRequestedOutcomes(options.target);
667
+ let eligibleClaudePlan: ReturnType<(typeof TARGET_INTEGRATIONS)['claude-code']['plan']> | undefined;
668
+ if (aggregate.manifest !== undefined && options.noIntegrations !== true) {
669
+ if (options.target !== 'claude-code') {
670
+ integrations = staticPolicyOutcomes(options.target, aggregate.manifest, options.noHooks === true);
671
+ } else {
672
+ const mcpRequested = Object.keys(aggregate.manifest.mcpServers ?? {}).length > 0;
673
+ const hooksRequested = (aggregate.manifest.hooks?.length ?? 0) > 0 && options.noHooks !== true;
674
+ let mcpOutcome: IntegrationOutcome = { target: options.target, component: 'mcp', status: 'not-requested', registrations: [] };
675
+ if (mcpRequested) {
676
+ if (options.noVerify === true) {
677
+ mcpOutcome = refusedOutcome(options.target, 'mcp', 'NO_QUALIFYING_LIVE_RECEIPT', '--no-verify cannot authorize integration emission; rerun with live verification enabled');
678
+ } else if (aggregate.digest === undefined || options.allowIntegrations !== aggregate.digest) {
679
+ mcpOutcome = refusedOutcome(options.target, 'mcp', 'INTEGRATION_AUTHORIZATION_REQUIRED', `rerun with --allow-integrations ${aggregate.digest ?? '<missing-digest>'}`);
680
+ } else {
681
+ const plan = TARGET_INTEGRATIONS['claude-code'].plan(aggregate.manifest, { target: 'claude-code' });
682
+ const refusal = plan.refusals.find((row) => row.component === 'mcp');
683
+ if (refusal !== undefined) {
684
+ mcpOutcome = refusedOutcome(options.target, 'mcp', refusal.reasonCode, refusal.remediation);
685
+ } else {
686
+ const preflight = verifyTargetIntegration({
687
+ target: 'claude-code',
688
+ component: 'mcp',
689
+ projectRoot: options.projectRoot,
690
+ phase: 'preflight',
691
+ ...(options.integrationProcessPort !== undefined ? { processPort: options.integrationProcessPort } : {}),
692
+ });
693
+ if (!preflight.ok) {
694
+ mcpOutcome = refusedOutcome(options.target, 'mcp', preflight.reasonCode ?? 'LIVE_PROBE_FAILED', preflight.remediation ?? 'live preflight did not qualify');
695
+ } else {
696
+ eligibleClaudePlan = plan;
697
+ mcpOutcome = {
698
+ target: options.target,
699
+ component: 'mcp',
700
+ status: 'emitted',
701
+ registrations: [],
702
+ carrier: { scope: 'project', path: '.mcp.json' },
703
+ ...(preflight.runtimeVersion !== undefined ? { runtimeVersion: preflight.runtimeVersion } : {}),
704
+ ...(preflight.evidenceVersion !== undefined ? { evidenceVersion: preflight.evidenceVersion } : {}),
705
+ };
706
+ }
707
+ }
708
+ }
709
+ }
710
+ const hookOutcome: IntegrationOutcome = hooksRequested
711
+ ? refusedOutcome(options.target, 'hooks', 'NO_ACTIVATION_RECEIPT', 'record a nonce canary and negative-control activation receipt')
712
+ : { target: options.target, component: 'hooks', status: 'not-requested', registrations: [] };
713
+ integrations = [mcpOutcome, hookOutcome];
714
+ }
715
+ }
571
716
  // Skip-and-collect (D1): one unparseable SKILL.md must not discard the whole install.
572
717
  // Same shape as `runVerify`'s long-standing per-id try/catch below.
573
718
  //
@@ -602,7 +747,59 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
602
747
  applyFailures.push({ id, reason: error instanceof Error ? error.message : String(error) });
603
748
  }
604
749
  }
605
- return { target: options.target, skillsDir: options.skillsDir, projectRoot: options.projectRoot, skills, missing, failures, applyFailures };
750
+ if (eligibleClaudePlan !== undefined && integrations[0].status === 'emitted') {
751
+ try {
752
+ applyIntegrationFragments({
753
+ projectRoot: options.projectRoot,
754
+ fragments: eligibleClaudePlan.fragments,
755
+ ...(options.integrationApplyFault !== undefined ? { injectFault: options.integrationApplyFault } : {}),
756
+ });
757
+ const registrations = Object.keys(aggregate.manifest?.mcpServers ?? {});
758
+ const observed = [];
759
+ let failedObservation: ReturnType<typeof verifyTargetIntegration> | undefined;
760
+ for (const registrationId of registrations) {
761
+ const result = verifyTargetIntegration({
762
+ target: 'claude-code',
763
+ component: 'mcp',
764
+ projectRoot: options.projectRoot,
765
+ registrationId,
766
+ phase: 'post-write',
767
+ ...(options.integrationProcessPort !== undefined ? { processPort: options.integrationProcessPort } : {}),
768
+ });
769
+ if (!result.ok) { failedObservation = result; break; }
770
+ observed.push(...result.registrations);
771
+ }
772
+ if (failedObservation !== undefined) {
773
+ integrations = [
774
+ {
775
+ ...refusedOutcome(options.target, 'mcp', failedObservation.reasonCode ?? 'POST_WRITE_REGISTRATION_NOT_OBSERVED', failedObservation.remediation ?? 'registration was written but not observed'),
776
+ carrier: { scope: 'project', path: '.mcp.json' },
777
+ applied: true,
778
+ },
779
+ integrations[1],
780
+ ];
781
+ } else {
782
+ integrations = [{ ...integrations[0], registrations: observed }, integrations[1]];
783
+ }
784
+ } catch (error) {
785
+ const reasonCode = error instanceof IntegrationApplyError ? error.reasonCode : 'APPLY_FAILED';
786
+ integrations = [{
787
+ ...refusedOutcome(options.target, 'mcp', reasonCode, error instanceof Error ? error.message : String(error)),
788
+ ...(error instanceof IntegrationApplyError && error.applied ? { applied: true } : {}),
789
+ }, integrations[1]];
790
+ }
791
+ }
792
+ return {
793
+ target: options.target,
794
+ skillsDir: options.skillsDir,
795
+ projectRoot: options.projectRoot,
796
+ skills,
797
+ missing,
798
+ failures,
799
+ applyFailures,
800
+ integrations,
801
+ ...(aggregate.digest !== undefined ? { integrationDigest: aggregate.digest } : {}),
802
+ };
606
803
  }
607
804
 
608
805
  // ---------------------------------------------------------------------------