hoi4-agent-tools 0.1.7 → 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 (99) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +31 -25
  3. package/SECURITY.md +4 -2
  4. package/dist/bin/setup.js +11 -4
  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/transactions.d.ts +2 -0
  15. package/dist/hoi4_agent_tools/core/transactions.d.ts.map +1 -1
  16. package/dist/hoi4_agent_tools/core/transactions.js +49 -6
  17. package/dist/hoi4_agent_tools/core/transactions.js.map +1 -1
  18. package/dist/hoi4_agent_tools/core/workspace.js +1 -1
  19. package/dist/hoi4_agent_tools/core/workspace.js.map +1 -1
  20. package/dist/hoi4_agent_tools/focus/service.d.ts.map +1 -1
  21. package/dist/hoi4_agent_tools/focus/service.js +4 -18
  22. package/dist/hoi4_agent_tools/focus/service.js.map +1 -1
  23. package/dist/hoi4_agent_tools/gui/studio.d.ts.map +1 -1
  24. package/dist/hoi4_agent_tools/gui/studio.js +3 -2
  25. package/dist/hoi4_agent_tools/gui/studio.js.map +1 -1
  26. package/dist/hoi4_agent_tools/map/service.d.ts.map +1 -1
  27. package/dist/hoi4_agent_tools/map/service.js +11 -5
  28. package/dist/hoi4_agent_tools/map/service.js.map +1 -1
  29. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +2 -1
  30. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +1 -1
  31. package/dist/hoi4_agent_tools/mcp/prompts/register.js +35 -13
  32. package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +1 -1
  33. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +1 -1
  34. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +1 -1
  35. package/dist/hoi4_agent_tools/mcp/security/auth.js +1 -4
  36. package/dist/hoi4_agent_tools/mcp/security/auth.js.map +1 -1
  37. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +12 -0
  38. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +1 -1
  39. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +239 -220
  40. package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +1 -1
  41. package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +1 -1
  42. package/dist/hoi4_agent_tools/mcp/server/create.js +8 -2
  43. package/dist/hoi4_agent_tools/mcp/server/create.js.map +1 -1
  44. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +5 -0
  45. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +1 -1
  46. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -0
  47. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +1 -1
  48. package/dist/hoi4_agent_tools/mcp/server/result.js +1 -1
  49. package/dist/hoi4_agent_tools/mcp/server/result.js.map +1 -1
  50. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts +31 -0
  51. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.d.ts.map +1 -0
  52. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +127 -0
  53. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js.map +1 -0
  54. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +1 -1
  55. package/dist/hoi4_agent_tools/mcp/tools/focus.js +69 -23
  56. package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +1 -1
  57. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +6 -0
  58. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +1 -1
  59. package/dist/hoi4_agent_tools/mcp/tools/gui.js +40 -12
  60. package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +1 -1
  61. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +1 -1
  62. package/dist/hoi4_agent_tools/mcp/tools/map.js +39 -12
  63. package/dist/hoi4_agent_tools/mcp/tools/map.js.map +1 -1
  64. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +1 -1
  65. package/dist/hoi4_agent_tools/mcp/transports/http.js +3 -0
  66. package/dist/hoi4_agent_tools/mcp/transports/http.js.map +1 -1
  67. package/dist/hoi4_agent_tools/version.d.ts +1 -1
  68. package/dist/hoi4_agent_tools/version.js +1 -1
  69. package/docs/README.md +2 -2
  70. package/docs/adr/0012-autonomous-rewrites.md +55 -0
  71. package/docs/adr/README.md +1 -0
  72. package/docs/agent-integration.md +33 -23
  73. package/docs/architecture.md +20 -4
  74. package/docs/artifacts.md +8 -5
  75. package/docs/completion-report.md +85 -125
  76. package/docs/configuration.md +24 -8
  77. package/docs/focus-workflow.md +16 -14
  78. package/docs/gui-workflow.md +7 -3
  79. package/docs/limitations.md +3 -2
  80. package/docs/map-workflow.md +15 -9
  81. package/docs/performance.md +1 -1
  82. package/docs/release.md +11 -9
  83. package/docs/research/mcp-2026-07-10.md +9 -3
  84. package/docs/security.md +12 -6
  85. package/docs/self-hosting.md +8 -6
  86. package/docs/transactions.md +45 -33
  87. package/docs/troubleshooting.md +12 -4
  88. package/package.json +2 -2
  89. package/schemas/configuration.schema.json +3 -2
  90. package/schemas/continuous-focus-palette.schema.json +1 -1
  91. package/schemas/focus-plan.schema.json +1 -1
  92. package/schemas/focus-planning-sidecar.schema.json +1 -1
  93. package/schemas/gui-animation-source.schema.json +1 -1
  94. package/schemas/gui-helper.schema.json +1 -1
  95. package/schemas/gui-scenario.schema.json +1 -1
  96. package/schemas/map-operation.schema.json +1 -1
  97. package/schemas/operation-result.schema.json +1 -1
  98. package/schemas/transaction-manifest.schema.json +1 -1
  99. package/server.json +3 -3
@@ -261,7 +261,7 @@ function focusReferences(engineSnapshot) {
261
261
  helper: active('scripted_effect'),
262
262
  };
263
263
  }
264
- async function postValidateTransaction(engine, transaction, principal, signal) {
264
+ export async function postValidateTransaction(engine, transaction, principal, signal, autonomous = false) {
265
265
  engine.invalidate(transaction.workspaceId);
266
266
  const snapshot = await engine.scan(transaction.workspaceId, {}, principal, signal);
267
267
  const changed = new Set(transaction.files.map(({ relativePath }) => sourcePathComparisonKey(relativePath)));
@@ -362,18 +362,26 @@ async function postValidateTransaction(engine, transaction, principal, signal) {
362
362
  if (diagnostics.length > availableDiagnostics) {
363
363
  const workspace = engine.resolver.get(transaction.workspaceId, principal);
364
364
  const sourceEvidence = boundedSourceHashEvidence(Object.fromEntries(transaction.files.flatMap(({ relativePath, afterSha256 }) => afterSha256 === null ? [] : [[relativePath, afterSha256]])));
365
- const artifact = await engine.artifacts.putChunked(workspace, `${transaction.transactionId}.post-validation.json`, 'application/json', `${canonicalJson({
365
+ const artifact = await engine.artifacts.putChunked(workspace, autonomous
366
+ ? `${transaction.operationKind}.post-validation.json`
367
+ : `${transaction.transactionId}.post-validation.json`, 'application/json', `${canonicalJson({
366
368
  schemaVersion: 1,
367
- transactionId: transaction.transactionId,
368
- planHash: transaction.planHash,
369
+ ...(autonomous
370
+ ? { execution: 'autonomous-rewrite' }
371
+ : {
372
+ transactionId: transaction.transactionId,
373
+ planHash: transaction.planHash,
374
+ }),
369
375
  operationKind: transaction.operationKind,
370
376
  diagnosticCount: diagnostics.length,
371
377
  checks,
372
378
  diagnostics,
373
379
  })}\n`, {
374
- kind: 'transaction-post-validation',
380
+ kind: autonomous ? 'autonomous-rewrite-post-validation' : 'transaction-post-validation',
375
381
  toolVersion: PACKAGE_VERSION,
376
- schemaVersion: 'transaction-post-validation.v1',
382
+ schemaVersion: autonomous
383
+ ? 'autonomous-rewrite-post-validation.v1'
384
+ : 'transaction-post-validation.v1',
377
385
  sourceHashes: sourceEvidence.sourceHashes,
378
386
  metadata: { sourceHashInventory: sourceEvidence.inventory },
379
387
  }, 'Complete source-linked diagnostics from post-write transaction validation', signal);
@@ -411,9 +419,10 @@ async function postValidateTransaction(engine, transaction, principal, signal) {
411
419
  },
412
420
  ],
413
421
  checks,
422
+ artifacts: [artifactLink],
414
423
  };
415
424
  }
416
- return { diagnostics, checks };
425
+ return { diagnostics, checks, artifacts: [] };
417
426
  }
418
427
  export function registerBaseTools(server, engine, context) {
419
428
  server.registerTool('hoi4.project_register', {
@@ -570,229 +579,239 @@ export function registerBaseTools(server, engine, context) {
570
579
  return errorResult(error, workspaceId);
571
580
  }
572
581
  });
573
- server.registerTool('hoi4.transaction_status', {
574
- title: 'Inspect transaction',
575
- description: 'Return compact dry-run/apply/rollback counts and current state with a complete manifest resource.',
576
- inputSchema: workspaceInput.extend({ transactionId: transactionIdSchema }).strict(),
577
- outputSchema: transactionStatusOutputSchema,
578
- annotations: readOnly,
579
- }, async ({ workspaceId, transactionId }, extra) => {
580
- try {
581
- const progress = progressReporter(extra);
582
- progress.signal.throwIfAborted();
583
- const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
584
- progress.signal.throwIfAborted();
585
- const result = emptyServiceResult(workspaceId, { manifest: transactionSummary(manifest) });
586
- result.code = 'TRANSACTION_STATUS';
587
- result.transactionId = manifest.transactionId;
588
- result.planHash = manifest.planHash;
589
- result.proposedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
590
- result.changedFiles = manifest.appliedFiles.slice(0, 100);
591
- result.artifacts = [transactionResourceLink(manifest)];
592
- result.validation = manifest.validation;
593
- result.rollbackStatus = manifest.rollbackStatus;
594
- return toolResult(result);
595
- }
596
- catch (error) {
597
- return errorResult(error, workspaceId);
598
- }
599
- });
600
- server.registerTool('hoi4.transaction_diff', {
601
- title: 'Review transaction diff',
602
- description: 'Page through the complete affected-file set, operation summaries, and source/binary artifact links before apply.',
603
- inputSchema: workspaceInput
604
- .extend({
605
- transactionId: transactionIdSchema,
606
- limit: z.number().int().min(1).max(20).default(20),
607
- cursor: z.string().min(1).max(4096).optional(),
608
- })
609
- .strict(),
610
- outputSchema: transactionDiffOutputSchema,
611
- annotations: readOnly,
612
- }, async ({ workspaceId, transactionId, limit, cursor }, extra) => {
613
- try {
614
- const progress = progressReporter(extra);
615
- progress.signal.throwIfAborted();
616
- const manifest = await engine.transactions.status(workspaceId, transactionId, context.principal, progress.signal);
617
- progress.signal.throwIfAborted();
618
- let fileOffset = 0;
619
- let operationOffset = 0;
620
- let artifactOffset = 0;
621
- if (cursor !== undefined) {
622
- const decoded = decodeTransactionDiffCursor(cursor);
623
- if (decoded.planHash !== manifest.planHash ||
624
- decoded.fileOffset > manifest.files.length ||
625
- decoded.operationOffset > manifest.operations.length ||
626
- decoded.artifactOffset > manifest.artifacts.length) {
627
- 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;
628
646
  }
629
- fileOffset = decoded.fileOffset;
630
- operationOffset = decoded.operationOffset;
631
- 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);
632
689
  }
633
- const manifestFiles = manifest.files.slice(fileOffset, fileOffset + limit);
634
- const files = manifestFiles.map(({ relativePath, operationIds, beforeSha256, afterSha256, beforeSize, afterSize, mediaType, diffArtifact, }) => ({
635
- relativePath,
636
- operationIdCount: operationIds.length,
637
- operationIds: operationIds.slice(0, 20),
638
- beforeSha256,
639
- afterSha256,
640
- beforeSize,
641
- afterSize,
642
- mediaType,
643
- ...(diffArtifact === undefined ? {} : { diffArtifact }),
644
- }));
645
- const operations = manifest.operations.slice(operationOffset, operationOffset + limit);
646
- const artifacts = manifest.artifacts.slice(artifactOffset, artifactOffset + limit);
647
- const nextFileOffset = fileOffset + manifestFiles.length;
648
- const nextOperationOffset = operationOffset + operations.length;
649
- const nextArtifactOffset = artifactOffset + artifacts.length;
650
- const nextCursor = nextFileOffset < manifest.files.length ||
651
- nextOperationOffset < manifest.operations.length ||
652
- nextArtifactOffset < manifest.artifacts.length
653
- ? encodeTransactionDiffCursor(manifest.planHash, nextFileOffset, nextOperationOffset, nextArtifactOffset)
654
- : undefined;
655
- const result = emptyServiceResult(workspaceId, {
656
- files,
657
- operations: operations.map(({ id, kind, summary }) => ({ id, kind, summary })),
658
- fileCount: manifest.files.length,
659
- operationCount: manifest.operations.length,
660
- artifactCount: manifest.artifacts.length,
661
- returnedFiles: files.length,
662
- returnedOperations: operations.length,
663
- returnedArtifacts: artifacts.length,
664
- ...(nextCursor === undefined ? {} : { nextCursor }),
665
- expiresAt: manifest.expiresAt,
666
- });
667
- result.code = 'TRANSACTION_DIFF';
668
- result.transactionId = manifest.transactionId;
669
- result.planHash = manifest.planHash;
670
- result.proposedFiles = files.map(({ relativePath }) => relativePath);
671
- result.artifacts = [transactionResourceLink(manifest), ...artifacts];
672
- result.validation = manifest.validation;
673
- result.rollbackStatus = manifest.rollbackStatus;
674
- return toolResult(result);
675
- }
676
- catch (error) {
677
- return errorResult(error, workspaceId);
678
- }
679
- });
680
- server.registerTool('hoi4.transaction_apply', {
681
- title: 'Apply reviewed transaction',
682
- description: 'Apply a completed dry run only when transaction ID, workspace, principal, source hashes, and expected plan hash still match.',
683
- inputSchema: transactionInput,
684
- outputSchema: transactionMutationOutputSchema,
685
- annotations: {
686
- readOnlyHint: false,
687
- destructiveHint: true,
688
- idempotentHint: false,
689
- openWorldHint: false,
690
- },
691
- }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
692
- try {
693
- requireServerScope(context, 'hoi4:write');
694
- const progress = progressReporter(extra);
695
- await progress.report(0, 3, 'Checking transaction preconditions');
696
- const manifest = await engine.transactions.apply(workspaceId, transactionId, expectedPlanHash, {
697
- ...(context.principal === undefined ? {} : { principal: context.principal }),
698
- signal: progress.signal,
699
- postValidate: (transaction, signal) => postValidateTransaction(engine, transaction, context.principal, signal),
700
- });
701
- engine.invalidate(workspaceId);
702
- await engine.scan(workspaceId, {}, context.principal, progress.signal);
703
- await progress.report(3, 3, 'Apply and index rebuild complete');
704
- const result = emptyServiceResult(workspaceId, {
705
- state: manifest.state,
706
- fileCount: manifest.files.length,
707
- appliedFileCount: manifest.appliedFiles.length,
708
- artifactCount: manifest.artifacts.length,
709
- });
710
- result.code = 'TRANSACTION_APPLIED';
711
- result.transactionId = manifest.transactionId;
712
- result.planHash = manifest.planHash;
713
- result.proposedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
714
- result.changedFiles = manifest.appliedFiles.slice(0, 100);
715
- result.artifacts = [transactionResourceLink(manifest)];
716
- result.validation = manifest.validation;
717
- result.rollbackStatus = manifest.rollbackStatus;
718
- return toolResult(result);
719
- }
720
- catch (error) {
721
- 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) => {
722
706
  try {
723
- const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
724
- return errorResult(error, workspaceId, {
725
- transactionId: failed.transactionId,
726
- planHash: failed.planHash,
727
- proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
728
- changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
729
- artifacts: [transactionResourceLink(failed)],
730
- validation: failed.validation,
731
- rollbackStatus: failed.rollbackStatus,
732
- 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),
733
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,
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);
734
735
  }
735
- catch {
736
- // 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);
737
755
  }
738
- return errorResult(error, workspaceId);
739
- }
740
- });
741
- server.registerTool('hoi4.transaction_rollback', {
742
- title: 'Roll back applied transaction',
743
- description: 'Restore exact original bytes when the applied result still matches rollback preconditions.',
744
- inputSchema: transactionInput,
745
- outputSchema: transactionMutationOutputSchema,
746
- annotations: {
747
- readOnlyHint: false,
748
- destructiveHint: true,
749
- idempotentHint: false,
750
- openWorldHint: false,
751
- },
752
- }, async ({ workspaceId, transactionId, expectedPlanHash }, extra) => {
753
- try {
754
- requireServerScope(context, 'hoi4:write');
755
- const progress = progressReporter(extra);
756
- await progress.report(0, 2, 'Checking rollback preconditions');
757
- progress.signal.throwIfAborted();
758
- const manifest = await engine.transactions.rollback(workspaceId, transactionId, expectedPlanHash, context.principal, progress.signal);
759
- engine.invalidate(workspaceId);
760
- const result = emptyServiceResult(workspaceId, {
761
- state: manifest.state,
762
- fileCount: manifest.files.length,
763
- appliedFileCount: manifest.appliedFiles.length,
764
- artifactCount: manifest.artifacts.length,
765
- });
766
- result.code = 'TRANSACTION_ROLLED_BACK';
767
- result.transactionId = manifest.transactionId;
768
- result.planHash = manifest.planHash;
769
- result.changedFiles = manifest.files.slice(0, 100).map(({ relativePath }) => relativePath);
770
- result.artifacts = [transactionResourceLink(manifest)];
771
- result.validation = manifest.validation;
772
- result.rollbackStatus = manifest.rollbackStatus;
773
- await progress.report(2, 2, 'Atomic rollback complete');
774
- return toolResult(result);
775
- }
776
- 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) => {
777
769
  try {
778
- const failed = await engine.transactions.status(workspaceId, transactionId, context.principal);
779
- return errorResult(error, workspaceId, {
780
- transactionId: failed.transactionId,
781
- planHash: failed.planHash,
782
- proposedFiles: failed.files.slice(0, 100).map(({ relativePath }) => relativePath),
783
- changedFiles: failed.state === 'applied' ? failed.appliedFiles.slice(0, 100) : [],
784
- artifacts: [transactionResourceLink(failed)],
785
- validation: failed.validation,
786
- rollbackStatus: failed.rollbackStatus,
787
- 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,
788
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);
789
793
  }
790
- catch {
791
- // 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);
792
812
  }
793
- return errorResult(error, workspaceId);
794
- }
795
- });
813
+ });
814
+ }
796
815
  server.registerTool('hoi4.artifact_list', {
797
816
  title: 'List workspace artifacts',
798
817
  description: 'List content-addressed artifacts without exposing filesystem paths.',