hoi4-agent-tools 0.1.6 → 0.2.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 (136) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +44 -27
  3. package/SECURITY.md +4 -2
  4. package/dist/bin/setup.js +43 -6
  5. package/dist/bin/setup.js.map +1 -1
  6. package/dist/hoi4_agent_tools/core/configuration.d.ts +1 -0
  7. package/dist/hoi4_agent_tools/core/configuration.d.ts.map +1 -1
  8. package/dist/hoi4_agent_tools/core/configuration.js +3 -3
  9. package/dist/hoi4_agent_tools/core/configuration.js.map +1 -1
  10. package/dist/hoi4_agent_tools/core/engine.d.ts +1 -0
  11. package/dist/hoi4_agent_tools/core/engine.d.ts.map +1 -1
  12. package/dist/hoi4_agent_tools/core/engine.js +1 -0
  13. package/dist/hoi4_agent_tools/core/engine.js.map +1 -1
  14. package/dist/hoi4_agent_tools/core/index.d.ts.map +1 -1
  15. package/dist/hoi4_agent_tools/core/index.js +10 -2
  16. package/dist/hoi4_agent_tools/core/index.js.map +1 -1
  17. package/dist/hoi4_agent_tools/core/transactions.d.ts +2 -0
  18. package/dist/hoi4_agent_tools/core/transactions.d.ts.map +1 -1
  19. package/dist/hoi4_agent_tools/core/transactions.js +49 -6
  20. package/dist/hoi4_agent_tools/core/transactions.js.map +1 -1
  21. package/dist/hoi4_agent_tools/core/workspace.js +1 -1
  22. package/dist/hoi4_agent_tools/core/workspace.js.map +1 -1
  23. package/dist/hoi4_agent_tools/focus/compiler.d.ts +2 -1
  24. package/dist/hoi4_agent_tools/focus/compiler.d.ts.map +1 -1
  25. package/dist/hoi4_agent_tools/focus/compiler.js +12 -2
  26. package/dist/hoi4_agent_tools/focus/compiler.js.map +1 -1
  27. package/dist/hoi4_agent_tools/focus/importer.d.ts.map +1 -1
  28. package/dist/hoi4_agent_tools/focus/importer.js +13 -2
  29. package/dist/hoi4_agent_tools/focus/importer.js.map +1 -1
  30. package/dist/hoi4_agent_tools/focus/layout.d.ts.map +1 -1
  31. package/dist/hoi4_agent_tools/focus/layout.js +165 -42
  32. package/dist/hoi4_agent_tools/focus/layout.js.map +1 -1
  33. package/dist/hoi4_agent_tools/focus/lint.d.ts.map +1 -1
  34. package/dist/hoi4_agent_tools/focus/lint.js +37 -8
  35. package/dist/hoi4_agent_tools/focus/lint.js.map +1 -1
  36. package/dist/hoi4_agent_tools/focus/model.d.ts +5 -1
  37. package/dist/hoi4_agent_tools/focus/model.d.ts.map +1 -1
  38. package/dist/hoi4_agent_tools/focus/model.js +2 -0
  39. package/dist/hoi4_agent_tools/focus/model.js.map +1 -1
  40. package/dist/hoi4_agent_tools/focus/presentation.d.ts.map +1 -1
  41. package/dist/hoi4_agent_tools/focus/presentation.js +13 -5
  42. package/dist/hoi4_agent_tools/focus/presentation.js.map +1 -1
  43. package/dist/hoi4_agent_tools/focus/render.d.ts +4 -0
  44. package/dist/hoi4_agent_tools/focus/render.d.ts.map +1 -1
  45. package/dist/hoi4_agent_tools/focus/render.js +17 -3
  46. package/dist/hoi4_agent_tools/focus/render.js.map +1 -1
  47. package/dist/hoi4_agent_tools/focus/service.d.ts.map +1 -1
  48. package/dist/hoi4_agent_tools/focus/service.js +142 -97
  49. package/dist/hoi4_agent_tools/focus/service.js.map +1 -1
  50. package/dist/hoi4_agent_tools/focus/source-update.d.ts.map +1 -1
  51. package/dist/hoi4_agent_tools/focus/source-update.js +9 -4
  52. package/dist/hoi4_agent_tools/focus/source-update.js.map +1 -1
  53. package/dist/hoi4_agent_tools/gui/studio.d.ts.map +1 -1
  54. package/dist/hoi4_agent_tools/gui/studio.js +3 -2
  55. package/dist/hoi4_agent_tools/gui/studio.js.map +1 -1
  56. package/dist/hoi4_agent_tools/map/service.d.ts.map +1 -1
  57. package/dist/hoi4_agent_tools/map/service.js +11 -5
  58. package/dist/hoi4_agent_tools/map/service.js.map +1 -1
  59. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +2 -1
  60. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +1 -1
  61. package/dist/hoi4_agent_tools/mcp/prompts/register.js +35 -13
  62. package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +1 -1
  63. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +1 -1
  64. package/dist/hoi4_agent_tools/mcp/resources/register.d.ts.map +1 -1
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -0
  66. package/dist/hoi4_agent_tools/mcp/resources/register.js.map +1 -1
  67. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +1 -1
  68. package/dist/hoi4_agent_tools/mcp/security/auth.js +1 -4
  69. package/dist/hoi4_agent_tools/mcp/security/auth.js.map +1 -1
  70. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +12 -0
  71. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +1 -1
  72. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +297 -218
  73. package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +1 -1
  74. package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +1 -1
  75. package/dist/hoi4_agent_tools/mcp/server/create.js +8 -2
  76. package/dist/hoi4_agent_tools/mcp/server/create.js.map +1 -1
  77. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +5 -0
  78. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +1 -1
  79. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -0
  80. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +1 -1
  81. package/dist/hoi4_agent_tools/mcp/server/result.js +1 -1
  82. package/dist/hoi4_agent_tools/mcp/server/result.js.map +1 -1
  83. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts +31 -0
  84. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts.map +1 -0
  85. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +127 -0
  86. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js.map +1 -0
  87. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +1 -1
  88. package/dist/hoi4_agent_tools/mcp/tools/focus.js +131 -30
  89. package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +1 -1
  90. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +6 -0
  91. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +1 -1
  92. package/dist/hoi4_agent_tools/mcp/tools/gui.js +40 -12
  93. package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +1 -1
  94. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +1 -1
  95. package/dist/hoi4_agent_tools/mcp/tools/map.js +39 -12
  96. package/dist/hoi4_agent_tools/mcp/tools/map.js.map +1 -1
  97. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +1 -1
  98. package/dist/hoi4_agent_tools/mcp/transports/http.js +3 -0
  99. package/dist/hoi4_agent_tools/mcp/transports/http.js.map +1 -1
  100. package/dist/hoi4_agent_tools/schemas/focus.d.ts +1 -1
  101. package/dist/hoi4_agent_tools/schemas/focus.d.ts.map +1 -1
  102. package/dist/hoi4_agent_tools/schemas/focus.js +16 -2
  103. package/dist/hoi4_agent_tools/schemas/focus.js.map +1 -1
  104. package/dist/hoi4_agent_tools/version.d.ts +1 -1
  105. package/dist/hoi4_agent_tools/version.js +1 -1
  106. package/docs/README.md +3 -2
  107. package/docs/adr/0012-autonomous-rewrites.md +55 -0
  108. package/docs/adr/README.md +1 -0
  109. package/docs/agent-integration.md +146 -0
  110. package/docs/architecture.md +20 -4
  111. package/docs/artifacts.md +8 -5
  112. package/docs/completion-report.md +109 -79
  113. package/docs/configuration.md +30 -10
  114. package/docs/focus-workflow.md +134 -5
  115. package/docs/gui-workflow.md +7 -3
  116. package/docs/limitations.md +4 -2
  117. package/docs/map-workflow.md +15 -9
  118. package/docs/performance.md +1 -1
  119. package/docs/release.md +12 -7
  120. package/docs/research/mcp-2026-07-10.md +9 -3
  121. package/docs/security.md +12 -6
  122. package/docs/self-hosting.md +8 -6
  123. package/docs/transactions.md +45 -27
  124. package/docs/troubleshooting.md +12 -4
  125. package/package.json +7 -3
  126. package/schemas/configuration.schema.json +3 -2
  127. package/schemas/continuous-focus-palette.schema.json +1 -1
  128. package/schemas/focus-plan.schema.json +74 -49
  129. package/schemas/focus-planning-sidecar.schema.json +33 -18
  130. package/schemas/gui-animation-source.schema.json +1 -1
  131. package/schemas/gui-helper.schema.json +1 -1
  132. package/schemas/gui-scenario.schema.json +1 -1
  133. package/schemas/map-operation.schema.json +1 -1
  134. package/schemas/operation-result.schema.json +1 -1
  135. package/schemas/transaction-manifest.schema.json +1 -1
  136. package/server.json +4 -4
@@ -4,6 +4,7 @@ import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifa
4
4
  import { workspaceRegistrationSchema } from '../../core/configuration.js';
5
5
  import { emptyServiceResult, ServiceError } from '../../core/result.js';
6
6
  import { parseClausewitz } from '../../core/source/index.js';
7
+ import { TRANSACTION_MAX_DIAGNOSTICS } from '../../core/transaction-limits.js';
7
8
  import { importContinuousFocusPalettes, importFocusTrees, layoutFocusTreeAsync, lintContinuousFocusPalette, lintFocusTree, } from '../../focus/index.js';
8
9
  import { ScriptedGuiStudio } from '../../gui/index.js';
9
10
  import { AgentNudger, attributeMapValidationDiagnostics } from '../../map/index.js';
@@ -260,7 +261,7 @@ function focusReferences(engineSnapshot) {
260
261
  helper: active('scripted_effect'),
261
262
  };
262
263
  }
263
- async function postValidateTransaction(engine, transaction, principal, signal) {
264
+ export async function postValidateTransaction(engine, transaction, principal, signal, autonomous = false) {
264
265
  engine.invalidate(transaction.workspaceId);
265
266
  const snapshot = await engine.scan(transaction.workspaceId, {}, principal, signal);
266
267
  const changed = new Set(transaction.files.map(({ relativePath }) => sourcePathComparisonKey(relativePath)));
@@ -273,7 +274,9 @@ async function postValidateTransaction(engine, transaction, principal, signal) {
273
274
  const checks = [];
274
275
  if (transaction.operationKind === 'focus-plan-changes') {
275
276
  const catalog = focusReferences(snapshot);
276
- for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined && changed.has(sourcePathComparisonKey(relativePath)))) {
277
+ for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined &&
278
+ relativePath.toLowerCase().endsWith('.txt') &&
279
+ changed.has(sourcePathComparisonKey(relativePath)))) {
277
280
  const document = parseClausewitz(file.bytes, file.displayPath);
278
281
  const imported = importFocusTrees(document, { references: catalog });
279
282
  diagnostics.push(...imported.diagnostics);
@@ -296,7 +299,9 @@ async function postValidateTransaction(engine, transaction, principal, signal) {
296
299
  });
297
300
  }
298
301
  else if (transaction.operationKind === 'continuous-focus-plan-changes') {
299
- for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined && changed.has(sourcePathComparisonKey(relativePath)))) {
302
+ for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined &&
303
+ relativePath.toLowerCase().endsWith('.txt') &&
304
+ changed.has(sourcePathComparisonKey(relativePath)))) {
300
305
  const document = parseClausewitz(file.bytes, file.displayPath);
301
306
  const imported = importContinuousFocusPalettes(document);
302
307
  diagnostics.push(...imported.diagnostics);
@@ -353,7 +358,71 @@ async function postValidateTransaction(engine, transaction, principal, signal) {
353
358
  ? 'Changed source reparsed and reindexed successfully'
354
359
  : 'Changed source has blocking post-write diagnostics',
355
360
  });
356
- return { diagnostics, checks };
361
+ const availableDiagnostics = Math.max(0, TRANSACTION_MAX_DIAGNOSTICS - transaction.diagnostics.length);
362
+ if (diagnostics.length > availableDiagnostics) {
363
+ const workspace = engine.resolver.get(transaction.workspaceId, principal);
364
+ const sourceEvidence = boundedSourceHashEvidence(Object.fromEntries(transaction.files.flatMap(({ relativePath, afterSha256 }) => afterSha256 === null ? [] : [[relativePath, afterSha256]])));
365
+ const artifact = await engine.artifacts.putChunked(workspace, autonomous
366
+ ? `${transaction.operationKind}.post-validation.json`
367
+ : `${transaction.transactionId}.post-validation.json`, 'application/json', `${canonicalJson({
368
+ schemaVersion: 1,
369
+ ...(autonomous
370
+ ? { execution: 'autonomous-rewrite' }
371
+ : {
372
+ transactionId: transaction.transactionId,
373
+ planHash: transaction.planHash,
374
+ }),
375
+ operationKind: transaction.operationKind,
376
+ diagnosticCount: diagnostics.length,
377
+ checks,
378
+ diagnostics,
379
+ })}\n`, {
380
+ kind: autonomous ? 'autonomous-rewrite-post-validation' : 'transaction-post-validation',
381
+ toolVersion: PACKAGE_VERSION,
382
+ schemaVersion: autonomous
383
+ ? 'autonomous-rewrite-post-validation.v1'
384
+ : 'transaction-post-validation.v1',
385
+ sourceHashes: sourceEvidence.sourceHashes,
386
+ metadata: { sourceHashInventory: sourceEvidence.inventory },
387
+ }, 'Complete source-linked diagnostics from post-write transaction validation', signal);
388
+ const retainedLimit = Math.max(0, availableDiagnostics - 1);
389
+ const hard = diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
390
+ const retainedHard = hard.slice(0, retainedLimit);
391
+ const retainedSoft = diagnostics
392
+ .filter(({ severity }) => severity !== 'error' && severity !== 'blocker')
393
+ .slice(0, retainedLimit - retainedHard.length);
394
+ const omittedHard = hard.length - retainedHard.length;
395
+ const artifactLink = publicArtifactLink(artifact);
396
+ checks.push({
397
+ id: 'post-write-diagnostics-resource',
398
+ passed: omittedHard === 0,
399
+ message: `Complete post-write diagnostics are stored at ${artifactLink.uri}`,
400
+ });
401
+ return {
402
+ diagnostics: availableDiagnostics === 0
403
+ ? []
404
+ : [
405
+ ...retainedHard,
406
+ ...retainedSoft,
407
+ {
408
+ code: 'POST_VALIDATION_DIAGNOSTICS_IN_RESOURCE',
409
+ severity: omittedHard > 0 ? 'blocker' : 'warning',
410
+ category: 'transaction',
411
+ message: `Complete post-write validation contains ${diagnostics.length} diagnostics and is stored as an MCP resource`,
412
+ details: {
413
+ total: diagnostics.length,
414
+ returned: availableDiagnostics,
415
+ omitted: diagnostics.length - retainedLimit,
416
+ omittedHard,
417
+ artifact: artifactLink,
418
+ },
419
+ },
420
+ ],
421
+ checks,
422
+ artifacts: [artifactLink],
423
+ };
424
+ }
425
+ return { diagnostics, checks, artifacts: [] };
357
426
  }
358
427
  export function registerBaseTools(server, engine, context) {
359
428
  server.registerTool('hoi4.project_register', {
@@ -499,7 +568,7 @@ export function registerBaseTools(server, engine, context) {
499
568
  passed: true,
500
569
  message: snapshot.complete
501
570
  ? `${snapshot.index.symbols.length} symbols indexed from a complete inventory`
502
- : `${snapshot.index.symbols.length} symbols indexed; ${snapshot.skippedSourceCount} over-limit source(s) were skipped`,
571
+ : `${snapshot.index.symbols.length} symbols indexed from an incomplete inventory; ${snapshot.skippedSourceCount} over-limit source(s) were recorded and aggregate index ceilings may also apply`,
503
572
  },
504
573
  ],
505
574
  };
@@ -510,229 +579,239 @@ export function registerBaseTools(server, engine, context) {
510
579
  return errorResult(error, workspaceId);
511
580
  }
512
581
  });
513
- server.registerTool('hoi4.transaction_status', {
514
- title: 'Inspect transaction',
515
- description: 'Return compact dry-run/apply/rollback counts and current state with a complete manifest resource.',
516
- inputSchema: workspaceInput.extend({ transactionId: transactionIdSchema }).strict(),
517
- outputSchema: transactionStatusOutputSchema,
518
- annotations: readOnly,
519
- }, async ({ workspaceId, transactionId }, extra) => {
520
- try {
521
- const progress = progressReporter(extra);
522
- progress.signal.throwIfAborted();
523
- const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
524
- progress.signal.throwIfAborted();
525
- const result = emptyServiceResult(workspaceId, { manifest: transactionSummary(manifest) });
526
- result.code = 'TRANSACTION_STATUS';
527
- result.transactionId = manifest.transactionId;
528
- result.planHash = manifest.planHash;
529
- result.proposedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
530
- result.changedFiles = manifest.appliedFiles.slice(0, 100);
531
- result.artifacts = [transactionResourceLink(manifest)];
532
- result.validation = manifest.validation;
533
- result.rollbackStatus = manifest.rollbackStatus;
534
- return toolResult(result);
535
- }
536
- catch (error) {
537
- return errorResult(error, workspaceId);
538
- }
539
- });
540
- server.registerTool('hoi4.transaction_diff', {
541
- title: 'Review transaction diff',
542
- description: 'Page through the complete affected-file set, operation summaries, and source/binary artifact links before apply.',
543
- inputSchema: workspaceInput
544
- .extend({
545
- transactionId: transactionIdSchema,
546
- limit: z.number().int().min(1).max(20).default(20),
547
- cursor: z.string().min(1).max(4096).optional(),
548
- })
549
- .strict(),
550
- outputSchema: transactionDiffOutputSchema,
551
- annotations: readOnly,
552
- }, async ({ workspaceId, transactionId, limit, cursor }, extra) => {
553
- try {
554
- const progress = progressReporter(extra);
555
- progress.signal.throwIfAborted();
556
- const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
557
- progress.signal.throwIfAborted();
558
- let fileOffset = 0;
559
- let operationOffset = 0;
560
- let artifactOffset = 0;
561
- if (cursor !== undefined) {
562
- const decoded = decodeTransactionDiffCursor(cursor);
563
- if (decoded.planHash !== manifest.planHash ||
564
- decoded.fileOffset > manifest.files.length ||
565
- decoded.operationOffset > manifest.operations.length ||
566
- decoded.artifactOffset > manifest.artifacts.length) {
567
- throw new ServiceError('TRANSACTION_CURSOR_STALE', 'Transaction plan changed or the diff cursor is stale');
582
+ if (engine.resolver.config().writePolicy !== 'autonomous') {
583
+ server.registerTool('hoi4.transaction_status', {
584
+ title: 'Inspect transaction',
585
+ description: 'Return compact dry-run/apply/rollback counts and current state with a complete manifest resource.',
586
+ inputSchema: workspaceInput.extend({ transactionId: transactionIdSchema }).strict(),
587
+ outputSchema: transactionStatusOutputSchema,
588
+ annotations: readOnly,
589
+ }, async ({ workspaceId, transactionId }, extra) => {
590
+ try {
591
+ const progress = progressReporter(extra);
592
+ progress.signal.throwIfAborted();
593
+ const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
594
+ progress.signal.throwIfAborted();
595
+ const result = emptyServiceResult(workspaceId, {
596
+ manifest: transactionSummary(manifest),
597
+ });
598
+ result.code = 'TRANSACTION_STATUS';
599
+ result.transactionId = manifest.transactionId;
600
+ result.planHash = manifest.planHash;
601
+ result.proposedFiles = manifest.files
602
+ .slice(0, 100)
603
+ .map(({ relativePath }) => relativePath);
604
+ result.changedFiles = manifest.appliedFiles.slice(0, 100);
605
+ result.artifacts = [transactionResourceLink(manifest)];
606
+ result.validation = manifest.validation;
607
+ result.rollbackStatus = manifest.rollbackStatus;
608
+ return toolResult(result);
609
+ }
610
+ catch (error) {
611
+ return errorResult(error, workspaceId);
612
+ }
613
+ });
614
+ server.registerTool('hoi4.transaction_diff', {
615
+ title: 'Review transaction diff',
616
+ description: 'Page through the complete affected-file set, operation summaries, and source/binary artifact links before apply.',
617
+ inputSchema: workspaceInput
618
+ .extend({
619
+ transactionId: transactionIdSchema,
620
+ limit: z.number().int().min(1).max(20).default(20),
621
+ cursor: z.string().min(1).max(4096).optional(),
622
+ })
623
+ .strict(),
624
+ outputSchema: transactionDiffOutputSchema,
625
+ annotations: readOnly,
626
+ }, async ({ workspaceId, transactionId, limit, cursor }, extra) => {
627
+ try {
628
+ const progress = progressReporter(extra);
629
+ progress.signal.throwIfAborted();
630
+ const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
631
+ progress.signal.throwIfAborted();
632
+ let fileOffset = 0;
633
+ let operationOffset = 0;
634
+ let artifactOffset = 0;
635
+ if (cursor !== undefined) {
636
+ const decoded = decodeTransactionDiffCursor(cursor);
637
+ if (decoded.planHash !== manifest.planHash ||
638
+ decoded.fileOffset > manifest.files.length ||
639
+ decoded.operationOffset > manifest.operations.length ||
640
+ decoded.artifactOffset > manifest.artifacts.length) {
641
+ throw new ServiceError('TRANSACTION_CURSOR_STALE', 'Transaction plan changed or the diff cursor is stale');
642
+ }
643
+ fileOffset = decoded.fileOffset;
644
+ operationOffset = decoded.operationOffset;
645
+ artifactOffset = decoded.artifactOffset;
568
646
  }
569
- fileOffset = decoded.fileOffset;
570
- operationOffset = decoded.operationOffset;
571
- artifactOffset = decoded.artifactOffset;
647
+ const manifestFiles = manifest.files.slice(fileOffset, fileOffset + limit);
648
+ const files = manifestFiles.map(({ relativePath, operationIds, beforeSha256, afterSha256, beforeSize, afterSize, mediaType, diffArtifact, }) => ({
649
+ relativePath,
650
+ operationIdCount: operationIds.length,
651
+ operationIds: operationIds.slice(0, 20),
652
+ beforeSha256,
653
+ afterSha256,
654
+ beforeSize,
655
+ afterSize,
656
+ mediaType,
657
+ ...(diffArtifact === undefined ? {} : { diffArtifact }),
658
+ }));
659
+ const operations = manifest.operations.slice(operationOffset, operationOffset + limit);
660
+ const artifacts = manifest.artifacts.slice(artifactOffset, artifactOffset + limit);
661
+ const nextFileOffset = fileOffset + manifestFiles.length;
662
+ const nextOperationOffset = operationOffset + operations.length;
663
+ const nextArtifactOffset = artifactOffset + artifacts.length;
664
+ const nextCursor = nextFileOffset < manifest.files.length ||
665
+ nextOperationOffset < manifest.operations.length ||
666
+ nextArtifactOffset < manifest.artifacts.length
667
+ ? encodeTransactionDiffCursor(manifest.planHash, nextFileOffset, nextOperationOffset, nextArtifactOffset)
668
+ : undefined;
669
+ const result = emptyServiceResult(workspaceId, {
670
+ files,
671
+ operations: operations.map(({ id, kind, summary }) => ({ id, kind, summary })),
672
+ fileCount: manifest.files.length,
673
+ operationCount: manifest.operations.length,
674
+ artifactCount: manifest.artifacts.length,
675
+ returnedFiles: files.length,
676
+ returnedOperations: operations.length,
677
+ returnedArtifacts: artifacts.length,
678
+ ...(nextCursor === undefined ? {} : { nextCursor }),
679
+ expiresAt: manifest.expiresAt,
680
+ });
681
+ result.code = 'TRANSACTION_DIFF';
682
+ result.transactionId = manifest.transactionId;
683
+ result.planHash = manifest.planHash;
684
+ result.proposedFiles = files.map(({ relativePath }) => relativePath);
685
+ result.artifacts = [transactionResourceLink(manifest), ...artifacts];
686
+ result.validation = manifest.validation;
687
+ result.rollbackStatus = manifest.rollbackStatus;
688
+ return toolResult(result);
572
689
  }
573
- const manifestFiles = manifest.files.slice(fileOffset, fileOffset + limit);
574
- const files = manifestFiles.map(({ relativePath, operationIds, beforeSha256, afterSha256, beforeSize, afterSize, mediaType, diffArtifact, }) => ({
575
- relativePath,
576
- operationIdCount: operationIds.length,
577
- operationIds: operationIds.slice(0, 20),
578
- beforeSha256,
579
- afterSha256,
580
- beforeSize,
581
- afterSize,
582
- mediaType,
583
- ...(diffArtifact === undefined ? {} : { diffArtifact }),
584
- }));
585
- const operations = manifest.operations.slice(operationOffset, operationOffset + limit);
586
- const artifacts = manifest.artifacts.slice(artifactOffset, artifactOffset + limit);
587
- const nextFileOffset = fileOffset + manifestFiles.length;
588
- const nextOperationOffset = operationOffset + operations.length;
589
- const nextArtifactOffset = artifactOffset + artifacts.length;
590
- const nextCursor = nextFileOffset < manifest.files.length ||
591
- nextOperationOffset < manifest.operations.length ||
592
- nextArtifactOffset < manifest.artifacts.length
593
- ? encodeTransactionDiffCursor(manifest.planHash, nextFileOffset, nextOperationOffset, nextArtifactOffset)
594
- : undefined;
595
- const result = emptyServiceResult(workspaceId, {
596
- files,
597
- operations: operations.map(({ id, kind, summary }) => ({ id, kind, summary })),
598
- fileCount: manifest.files.length,
599
- operationCount: manifest.operations.length,
600
- artifactCount: manifest.artifacts.length,
601
- returnedFiles: files.length,
602
- returnedOperations: operations.length,
603
- returnedArtifacts: artifacts.length,
604
- ...(nextCursor === undefined ? {} : { nextCursor }),
605
- expiresAt: manifest.expiresAt,
606
- });
607
- result.code = 'TRANSACTION_DIFF';
608
- result.transactionId = manifest.transactionId;
609
- result.planHash = manifest.planHash;
610
- result.proposedFiles = files.map(({ relativePath }) => relativePath);
611
- result.artifacts = [transactionResourceLink(manifest), ...artifacts];
612
- result.validation = manifest.validation;
613
- result.rollbackStatus = manifest.rollbackStatus;
614
- return toolResult(result);
615
- }
616
- catch (error) {
617
- return errorResult(error, workspaceId);
618
- }
619
- });
620
- server.registerTool('hoi4.transaction_apply', {
621
- title: 'Apply reviewed transaction',
622
- description: 'Apply a completed dry run only when transaction ID, workspace, principal, source hashes, and expected plan hash still match.',
623
- inputSchema: transactionInput,
624
- outputSchema: transactionMutationOutputSchema,
625
- annotations: {
626
- readOnlyHint: false,
627
- destructiveHint: true,
628
- idempotentHint: false,
629
- openWorldHint: false,
630
- },
631
- }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
632
- try {
633
- requireServerScope(context, 'hoi4:write');
634
- const progress = progressReporter(extra);
635
- await progress.report(0, 3, 'Checking transaction preconditions');
636
- const manifest = await engine.transactions.apply(workspaceId, transactionId, expectedPlanHash, {
637
- ...(context.principal === undefined ? {} : { principal: context.principal }),
638
- signal: progress.signal,
639
- postValidate: (transaction, signal) => postValidateTransaction(engine, transaction, context.principal, signal),
640
- });
641
- engine.invalidate(workspaceId);
642
- await engine.scan(workspaceId, {}, context.principal, progress.signal);
643
- await progress.report(3, 3, 'Apply and index rebuild complete');
644
- const result = emptyServiceResult(workspaceId, {
645
- state: manifest.state,
646
- fileCount: manifest.files.length,
647
- appliedFileCount: manifest.appliedFiles.length,
648
- artifactCount: manifest.artifacts.length,
649
- });
650
- result.code = 'TRANSACTION_APPLIED';
651
- result.transactionId = manifest.transactionId;
652
- result.planHash = manifest.planHash;
653
- result.proposedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
654
- result.changedFiles = manifest.appliedFiles.slice(0, 100);
655
- result.artifacts = [transactionResourceLink(manifest)];
656
- result.validation = manifest.validation;
657
- result.rollbackStatus = manifest.rollbackStatus;
658
- return toolResult(result);
659
- }
660
- catch (error) {
661
- engine.invalidate(workspaceId);
690
+ catch (error) {
691
+ return errorResult(error, workspaceId);
692
+ }
693
+ });
694
+ server.registerTool('hoi4.transaction_apply', {
695
+ title: 'Apply reviewed transaction',
696
+ description: 'Apply a completed dry run only when transaction ID, workspace, principal, source hashes, and expected plan hash still match.',
697
+ inputSchema: transactionInput,
698
+ outputSchema: transactionMutationOutputSchema,
699
+ annotations: {
700
+ readOnlyHint: false,
701
+ destructiveHint: true,
702
+ idempotentHint: false,
703
+ openWorldHint: false,
704
+ },
705
+ }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
662
706
  try {
663
- const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
664
- return errorResult(error, workspaceId, {
665
- transactionId: failed.transactionId,
666
- planHash: failed.planHash,
667
- proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
668
- changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
669
- artifacts: [transactionResourceLink(failed)],
670
- validation: failed.validation,
671
- rollbackStatus: failed.rollbackStatus,
672
- data: { state: failed.state, failure: failed.failure ?? null },
707
+ requireServerScope(context, 'hoi4:write');
708
+ const progress = progressReporter(extra);
709
+ await progress.report(0, 3, 'Checking transaction preconditions');
710
+ const manifest = await engine.transactions.apply(workspaceId, transactionId, expectedPlanHash, {
711
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
712
+ signal: progress.signal,
713
+ postValidate: (transaction, signal) => postValidateTransaction(engine, transaction, context.principal, signal),
714
+ });
715
+ engine.invalidate(workspaceId);
716
+ await engine.scan(workspaceId, {}, context.principal, progress.signal);
717
+ await progress.report(3, 3, 'Apply and index rebuild complete');
718
+ const result = emptyServiceResult(workspaceId, {
719
+ state: manifest.state,
720
+ fileCount: manifest.files.length,
721
+ appliedFileCount: manifest.appliedFiles.length,
722
+ artifactCount: manifest.artifacts.length,
673
723
  });
724
+ result.code = 'TRANSACTION_APPLIED';
725
+ result.transactionId = manifest.transactionId;
726
+ result.planHash = manifest.planHash;
727
+ result.proposedFiles = manifest.files
728
+ .slice(0, 100)
729
+ .map(({ relativePath }) => relativePath);
730
+ result.changedFiles = manifest.appliedFiles.slice(0, 100);
731
+ result.artifacts = [transactionResourceLink(manifest)];
732
+ result.validation = manifest.validation;
733
+ result.rollbackStatus = manifest.rollbackStatus;
734
+ return toolResult(result);
674
735
  }
675
- catch {
676
- // A forged or cross-workspace transaction ID intentionally reveals no manifest details.
736
+ catch (error) {
737
+ engine.invalidate(workspaceId);
738
+ try {
739
+ const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
740
+ return errorResult(error, workspaceId, {
741
+ transactionId: failed.transactionId,
742
+ planHash: failed.planHash,
743
+ proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
744
+ changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
745
+ artifacts: [transactionResourceLink(failed)],
746
+ validation: failed.validation,
747
+ rollbackStatus: failed.rollbackStatus,
748
+ data: { state: failed.state, failure: failed.failure ?? null },
749
+ });
750
+ }
751
+ catch {
752
+ // A forged or cross-workspace transaction ID intentionally reveals no manifest details.
753
+ }
754
+ return errorResult(error, workspaceId);
677
755
  }
678
- return errorResult(error, workspaceId);
679
- }
680
- });
681
- server.registerTool('hoi4.transaction_rollback', {
682
- title: 'Roll back applied transaction',
683
- description: 'Restore exact original bytes when the applied result still matches rollback preconditions.',
684
- inputSchema: transactionInput,
685
- outputSchema: transactionMutationOutputSchema,
686
- annotations: {
687
- readOnlyHint: false,
688
- destructiveHint: true,
689
- idempotentHint: false,
690
- openWorldHint: false,
691
- },
692
- }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
693
- try {
694
- requireServerScope(context, 'hoi4:write');
695
- const progress = progressReporter(extra);
696
- await progress.report(0, 2, 'Checking rollback preconditions');
697
- progress.signal.throwIfAborted();
698
- const manifest = await engine.transactions.rollback(workspaceId, transactionId, expectedPlanHash, context.principal, progress.signal);
699
- engine.invalidate(workspaceId);
700
- const result = emptyServiceResult(workspaceId, {
701
- state: manifest.state,
702
- fileCount: manifest.files.length,
703
- appliedFileCount: manifest.appliedFiles.length,
704
- artifactCount: manifest.artifacts.length,
705
- });
706
- result.code = 'TRANSACTION_ROLLED_BACK';
707
- result.transactionId = manifest.transactionId;
708
- result.planHash = manifest.planHash;
709
- result.changedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
710
- result.artifacts = [transactionResourceLink(manifest)];
711
- result.validation = manifest.validation;
712
- result.rollbackStatus = manifest.rollbackStatus;
713
- await progress.report(2, 2, 'Atomic rollback complete');
714
- return toolResult(result);
715
- }
716
- catch (error) {
756
+ });
757
+ server.registerTool('hoi4.transaction_rollback', {
758
+ title: 'Roll back applied transaction',
759
+ description: 'Restore exact original bytes when the applied result still matches rollback preconditions.',
760
+ inputSchema: transactionInput,
761
+ outputSchema: transactionMutationOutputSchema,
762
+ annotations: {
763
+ readOnlyHint: false,
764
+ destructiveHint: true,
765
+ idempotentHint: false,
766
+ openWorldHint: false,
767
+ },
768
+ }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
717
769
  try {
718
- const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
719
- return errorResult(error, workspaceId, {
720
- transactionId: failed.transactionId,
721
- planHash: failed.planHash,
722
- proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
723
- changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
724
- artifacts: [transactionResourceLink(failed)],
725
- validation: failed.validation,
726
- rollbackStatus: failed.rollbackStatus,
727
- data: { state: failed.state, failure: failed.failure ?? null },
770
+ requireServerScope(context, 'hoi4:write');
771
+ const progress = progressReporter(extra);
772
+ await progress.report(0, 2, 'Checking rollback preconditions');
773
+ progress.signal.throwIfAborted();
774
+ const manifest = await engine.transactions.rollback(workspaceId, transactionId, expectedPlanHash, context.principal, progress.signal);
775
+ engine.invalidate(workspaceId);
776
+ const result = emptyServiceResult(workspaceId, {
777
+ state: manifest.state,
778
+ fileCount: manifest.files.length,
779
+ appliedFileCount: manifest.appliedFiles.length,
780
+ artifactCount: manifest.artifacts.length,
728
781
  });
782
+ result.code = 'TRANSACTION_ROLLED_BACK';
783
+ result.transactionId = manifest.transactionId;
784
+ result.planHash = manifest.planHash;
785
+ result.changedFiles = manifest.files
786
+ .slice(0, 100)
787
+ .map(({ relativePath }) => relativePath);
788
+ result.artifacts = [transactionResourceLink(manifest)];
789
+ result.validation = manifest.validation;
790
+ result.rollbackStatus = manifest.rollbackStatus;
791
+ await progress.report(2, 2, 'Atomic rollback complete');
792
+ return toolResult(result);
729
793
  }
730
- catch {
731
- // Invalid or inaccessible transaction IDs intentionally reveal no manifest details.
794
+ catch (error) {
795
+ try {
796
+ const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
797
+ return errorResult(error, workspaceId, {
798
+ transactionId: failed.transactionId,
799
+ planHash: failed.planHash,
800
+ proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
801
+ changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
802
+ artifacts: [transactionResourceLink(failed)],
803
+ validation: failed.validation,
804
+ rollbackStatus: failed.rollbackStatus,
805
+ data: { state: failed.state, failure: failed.failure ?? null },
806
+ });
807
+ }
808
+ catch {
809
+ // Invalid or inaccessible transaction IDs intentionally reveal no manifest details.
810
+ }
811
+ return errorResult(error, workspaceId);
732
812
  }
733
- return errorResult(error, workspaceId);
734
- }
735
- });
813
+ });
814
+ }
736
815
  server.registerTool('hoi4.artifact_list', {
737
816
  title: 'List workspace artifacts',
738
817
  description: 'List content-addressed artifacts without exposing filesystem paths.',