hoi4-agent-tools 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +59 -57
  3. package/dist/hoi4_agent_tools/core/configuration.js +2 -0
  4. package/dist/hoi4_agent_tools/core/job-store.js +19 -40
  5. package/dist/hoi4_agent_tools/core/job-worker-host.js +57 -8
  6. package/dist/hoi4_agent_tools/core/operation-tasks.js +2 -7
  7. package/dist/hoi4_agent_tools/core/scanner.js +3 -0
  8. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +27 -13
  9. package/dist/hoi4_agent_tools/core/transactions.js +31 -2
  10. package/dist/hoi4_agent_tools/core/windows-sharing.js +42 -0
  11. package/dist/hoi4_agent_tools/core/workspace.js +114 -8
  12. package/dist/hoi4_agent_tools/event/artifact-validation.js +1 -1
  13. package/dist/hoi4_agent_tools/event/service.js +1 -1
  14. package/dist/hoi4_agent_tools/focus/service.js +2 -2
  15. package/dist/hoi4_agent_tools/gui/studio.js +16 -3
  16. package/dist/hoi4_agent_tools/map/model.js +81 -2
  17. package/dist/hoi4_agent_tools/map/service.js +1 -0
  18. package/dist/hoi4_agent_tools/map/validation.js +60 -4
  19. package/dist/hoi4_agent_tools/mcp/tools/reference.js +1 -1
  20. package/dist/hoi4_agent_tools/package-check/service.js +1 -1
  21. package/dist/hoi4_agent_tools/reference/ranking.js +309 -0
  22. package/dist/hoi4_agent_tools/reference/script-validation.js +3 -13
  23. package/dist/hoi4_agent_tools/reference/service.js +152 -116
  24. package/dist/hoi4_agent_tools/reference/source-locate.js +156 -0
  25. package/dist/hoi4_agent_tools/reference/source-lookup.js +37 -6
  26. package/dist/hoi4_agent_tools/reference/suggestions.js +38 -0
  27. package/dist/hoi4_agent_tools/schemas/reference.js +80 -2
  28. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -1
  29. package/dist/hoi4_agent_tools/technology/service.js +1 -1
  30. package/dist/hoi4_agent_tools/version.js +1 -1
  31. package/docs/development.md +6 -0
  32. package/docs/examples.md +3 -3
  33. package/docs/map.md +10 -0
  34. package/docs/reference.md +5 -3
  35. package/docs/research/reference-benchmark.md +39 -0
  36. package/docs/setup.md +15 -0
  37. package/docs/tools.md +37 -37
  38. package/npm-shrinkwrap.json +18 -5
  39. package/package.json +2 -2
  40. package/schemas/configuration.schema.json +95 -88
  41. package/schemas/continuous-focus-palette.schema.json +1 -1
  42. package/schemas/custom-weighted-pool.schema.json +1 -1
  43. package/schemas/decision-inspect-input.schema.json +1 -1
  44. package/schemas/event-feature-manifest.schema.json +1 -1
  45. package/schemas/focus-plan.schema.json +1 -1
  46. package/schemas/focus-planning-sidecar.schema.json +1 -1
  47. package/schemas/gui-animation-source.schema.json +1 -1
  48. package/schemas/gui-helper.schema.json +1 -1
  49. package/schemas/gui-scenario.schema.json +1 -1
  50. package/schemas/helper-expansion-request.schema.json +1 -1
  51. package/schemas/helper-expansion-summary.schema.json +1 -1
  52. package/schemas/impact-inspect-input.schema.json +1 -1
  53. package/schemas/map-operation.schema.json +1 -1
  54. package/schemas/mechanic-test-input.schema.json +1 -1
  55. package/schemas/operation-result.schema.json +1 -1
  56. package/schemas/package-check-input.schema.json +1 -1
  57. package/schemas/package-manifest.schema.json +1 -1
  58. package/schemas/probability-analysis-result.schema.json +1 -1
  59. package/schemas/probability-compare-input.schema.json +1 -1
  60. package/schemas/probability-evaluate-input.schema.json +1 -1
  61. package/schemas/probability-inspect-input.schema.json +1 -1
  62. package/schemas/probability-render-input.schema.json +1 -1
  63. package/schemas/probability-scenario-set.schema.json +1 -1
  64. package/schemas/probability-sequence-input.schema.json +1 -1
  65. package/schemas/probability-simulate-input.schema.json +1 -1
  66. package/schemas/probability-sweep-input.schema.json +1 -1
  67. package/schemas/reference-context-input.schema.json +1 -1
  68. package/schemas/reference-read-input.schema.json +1 -1
  69. package/schemas/reference-search-input.schema.json +1 -1
  70. package/schemas/scenario-suite.schema.json +1 -1
  71. package/schemas/scenario-test-input.schema.json +1 -1
  72. package/schemas/script-validate-input.schema.json +1 -1
  73. package/schemas/shared-scenario.schema.json +1 -1
  74. package/schemas/source-lookup-input.schema.json +113 -50
  75. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.8.1 - 2026-10-05
6
+
7
+ - Start normally when finished rewrite journals were recorded before the workspace roots changed, such as before installed DLC folders became source layers, and let them expire; an interrupted rewrite under changed roots is still refused.
8
+
9
+ ## 3.8.0 - 2026-10-04
10
+
11
+ - Rank documentation with field-weighted BM25 over identifier-aware terms, cite identifier table rows as exact one-line sections, return each result's best-matching `matchLine`, and split sections by CommonMark fences.
12
+ - Locate a source position with `hoi4.source_lookup` `path` and `line`: return its layer, the containing definitions, the enclosing assignment chain, and a `keyPath` that navigates back to it.
13
+ - Suggest indexed identifiers within three edits when `hoi4.source_lookup` finds no definition.
14
+ - Layer installed `dlc/` and `integrated_dlc/` folders between the base game and mods in internal-ID order, so source lookups, renders, and analyses find DLC sprites, interfaces, and portraits; `includeGameDlc: false` opts out.
15
+ - Apply `replace_path` entries from mod and dependency descriptors in addition to configured replace paths.
16
+ - Check province terrain and continents against the active `common/terrain` categories and `continent.txt`, accept any water terrain for seas and lakes, check strategic-region `naval_terrain`, and report adjacency rows after the `-1` terminator or a missing terminator.
17
+ - Search unresolved GUI asset names in grouped wildcard patterns, so windows with hundreds of missing textures render within the wildcard-pattern ceiling.
18
+ - Retry transient Windows sharing violations on job records and capacity leases, release record handles before parsing, bound delete-pending capacity slots by elapsed time, and report worker coordination failures with a fixed stage and allowlisted native code.
19
+ - Keep every production dependency inside its declared range in the published shrinkwrap, so `npm ls` succeeds in consumer installs; the `@hono/node-server` override is no longer needed.
20
+
5
21
  ## 3.7.0 - 2026-10-04
6
22
 
7
23
  - Publish an `npm-shrinkwrap.json` so every install reproduces the reviewed dependency closure, including all platforms' optional Sharp binaries (about 300 MB installed).
package/README.md CHANGED
@@ -14,6 +14,7 @@ hoi4-agent-tools-setup --print-client-config
14
14
  ```
15
15
 
16
16
  Add the printed configuration to your MCP client and open your mod as its workspace.
17
+ The install is about 280 MiB, because npm fetches the image library's binaries for every platform to keep each install identical.
17
18
  Keep an installed copy of HOI4 available for vanilla examples, artwork and fonts.
18
19
  The [setup guide](docs/setup.md) covers connecting your agent and choosing game or mod paths.
19
20
 
@@ -26,11 +27,11 @@ The examples below show the results.
26
27
 
27
28
  ### Focus trees and alternative prerequisites
28
29
 
29
- | Tree or branch | Supplied in-game capture | MCP source preview |
30
- | ---------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
31
- | Fury | [![Fury in game](docs/images/comparisons/fury-ingame.png)](docs/images/comparisons/fury-ingame.png) | [![Fury focus tree](docs/images/comparisons/fury-mcp.png)](docs/images/comparisons/fury-mcp.png) |
32
- | Holy Realm | [![Holy Realm in game](docs/images/comparisons/holy-realm-ingame.png)](docs/images/comparisons/holy-realm-ingame.png) | [![Holy Realm focus tree](docs/images/comparisons/holy-realm-mcp.png)](docs/images/comparisons/holy-realm-mcp.png) |
33
- | Utopia Manifesto | [![Utopia in game](docs/images/comparisons/utopia-ingame.png)](docs/images/comparisons/utopia-ingame.png) | [![Utopia focus tree](docs/images/comparisons/utopia-mcp.png)](docs/images/comparisons/utopia-mcp.png) |
30
+ | Focus tree | Supplied in-game capture | MCP source preview |
31
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
32
+ | 52 focuses | [![Medium focus tree in game](docs/images/comparisons/fury-ingame.png)](docs/images/comparisons/fury-ingame.png) | [![Medium focus tree preview](docs/images/comparisons/fury-mcp.png)](docs/images/comparisons/fury-mcp.png) |
33
+ | 111 focuses | [![Large focus tree in game](docs/images/comparisons/holy-realm-ingame.png)](docs/images/comparisons/holy-realm-ingame.png) | [![Large focus tree preview](docs/images/comparisons/holy-realm-mcp.png)](docs/images/comparisons/holy-realm-mcp.png) |
34
+ | 124 focuses | [![Branching focus tree in game](docs/images/comparisons/utopia-ingame.png)](docs/images/comparisons/utopia-ingame.png) | [![Branching focus tree preview](docs/images/comparisons/utopia-mcp.png)](docs/images/comparisons/utopia-mcp.png) |
34
35
 
35
36
  Dotted connectors in the MCP preview mark focuses where either preceding focus satisfies one prerequisite group.
36
37
  The tree previews show route structure; the game's focus screen has different framing, labels and icons.
@@ -40,28 +41,29 @@ The [focus guide](docs/focus.md) explains inspection, rendering and edits, and t
40
41
 
41
42
  > Render a technology folder. Check card sizes, artwork, links and missing placements.
42
43
 
43
- | Folder | Supplied in-game capture | MCP source preview |
44
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
45
- | Infantry | [![Infantry in game](docs/images/comparisons/infantry-ingame.png)](docs/images/comparisons/infantry-ingame.png) | [![Infantry technology folder](docs/images/comparisons/infantry-mcp.png)](docs/images/comparisons/infantry-mcp.png) |
46
- | Chemical warfare | [![Chemical folder in game](docs/images/comparisons/chemical-recent-ingame.png)](docs/images/comparisons/chemical-recent-ingame.png) | [![Chemical folder preview](docs/images/comparisons/chemical-historical-mcp.png)](docs/images/comparisons/chemical-historical-mcp.png) |
44
+ | Folder | Supplied in-game capture | MCP source preview |
45
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | Vanilla infantry | [![Infantry folder in game](docs/images/comparisons/infantry-ingame.png)](docs/images/comparisons/infantry-ingame.png) | [![Infantry technology folder](docs/images/comparisons/infantry-mcp.png)](docs/images/comparisons/infantry-mcp.png) |
47
+ | Mod wide cards | [![Mod technology folder in game](docs/images/comparisons/chemical-recent-ingame.png)](docs/images/comparisons/chemical-recent-ingame.png) | [![Mod technology folder preview](docs/images/comparisons/chemical-historical-mcp.png)](docs/images/comparisons/chemical-historical-mcp.png) |
47
48
 
48
- The chemical comparison uses the source revision recorded with the screenshot and includes wide equipment cards.
49
+ The mod folder preview uses the source revision recorded with its screenshot and includes wide equipment cards.
50
+ Armored-car artwork that installed DLC folders provide appears in the infantry preview.
49
51
  The [technology comparison](docs/visual-comparisons.md#technology-folders) records its source revision, and the [technology guide](docs/technology.md) covers inspection and doctrine paths.
50
52
 
51
53
  ### Scripted interfaces
52
54
 
53
55
  > Preview an interface with its game assets and fonts. Check states, text clipping and clickable areas at 100% and 125% UI scale.
54
56
 
55
- | Interface | Supplied in-game capture | MCP source preview |
56
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
57
- | Video options | [![Options in game](docs/images/comparisons/gui/options-video-ingame.png)](docs/images/comparisons/gui/options-video-ingame.png) | [![Options preview](docs/images/comparisons/gui/options-video-mcp.png)](docs/images/comparisons/gui/options-video-mcp.png) |
58
- | Škoda Priority | [![Škoda Priority in game](docs/images/comparisons/gui/skoda-priority-ingame.png)](docs/images/comparisons/gui/skoda-priority-ingame.png) | [![Škoda Priority preview](docs/images/comparisons/gui/skoda-priority-mcp.png)](docs/images/comparisons/gui/skoda-priority-mcp.png) |
59
- | Chaos Redux Tag Manager | [![Tag Manager in game](docs/images/comparisons/gui/tag-manager-ingame.png)](docs/images/comparisons/gui/tag-manager-ingame.png) | [![Tag Manager preview](docs/images/comparisons/gui/tag-manager-mcp.png)](docs/images/comparisons/gui/tag-manager-mcp.png) |
57
+ | Interface | Supplied in-game capture | MCP source preview |
58
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
59
+ | Vanilla video options | [![Options in game](docs/images/comparisons/gui/options-video-ingame.png)](docs/images/comparisons/gui/options-video-ingame.png) | [![Options preview](docs/images/comparisons/gui/options-video-mcp.png)](docs/images/comparisons/gui/options-video-mcp.png) |
60
+ | Vanilla priority panel | [![Priority panel in game](docs/images/comparisons/gui/skoda-priority-ingame.png)](docs/images/comparisons/gui/skoda-priority-ingame.png) | [![Priority panel preview](docs/images/comparisons/gui/skoda-priority-mcp.png)](docs/images/comparisons/gui/skoda-priority-mcp.png) |
61
+ | Mod management window | [![Management window in game](docs/images/comparisons/gui/tag-manager-ingame.png)](docs/images/comparisons/gui/tag-manager-ingame.png) | [![Management window preview](docs/images/comparisons/gui/tag-manager-mcp.png)](docs/images/comparisons/gui/tag-manager-mcp.png) |
60
62
 
61
- The Tag Manager screenshot comes from an older layout.
62
- The Options preview has no observed gamma value, and Škoda's game capture frames its background differently.
63
+ The management window capture comes from an older layout.
64
+ The video options preview has no observed gamma value, and the priority panel's capture frames its background differently.
63
65
 
64
- | Event Log | Chaos Meter: status | Chaos Meter: deaths |
66
+ | Mod event log | Mod status panel | Mod status panel, second tab |
65
67
  | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
66
68
  | [![Event Log preview](docs/images/comparisons/gui/event-log-mcp.png)](docs/images/comparisons/gui/event-log-mcp.png) | [![Chaos Meter status](docs/images/comparisons/gui/chaos-meter-status-mcp.png)](docs/images/comparisons/gui/chaos-meter-status-mcp.png) | [![Chaos Meter deaths](docs/images/comparisons/gui/chaos-meter-deaths-mcp.png)](docs/images/comparisons/gui/chaos-meter-deaths-mcp.png) |
67
69
 
@@ -69,7 +71,7 @@ The Options preview has no observed gamma value, and Škoda's game capture frame
69
71
  | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
72
  | [![Decision window](docs/images/comparisons/native-ui/countrydecisionview-window.png)](docs/images/comparisons/native-ui/countrydecisionview-window.png) | [![Occupation window](docs/images/comparisons/native-ui/countryoccupationview-window.png)](docs/images/comparisons/native-ui/countryoccupationview-window.png) |
71
73
 
72
- The Event Log, Chaos Meter, decision and occupation previews use declared states or positions for layout review.
74
+ The event log, status panel, decision and occupation previews use declared states or positions for layout review.
73
75
  They are source previews, not observations of a running campaign.
74
76
  See the [interface gallery](docs/gui-comparisons.md) for full images and fidelity notes, and the [GUI guide](docs/gui.md) for scenario inputs.
75
77
 
@@ -79,7 +81,7 @@ See the [interface gallery](docs/gui-comparisons.md) for full images and fidelit
79
81
 
80
82
  > Trace an event's options and follow-up events. Point out unresolved calls and link each route to its source.
81
83
 
82
- | Source map | Chaos Redux event chain |
84
+ | Source map | Mod event chain |
83
85
  | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
84
86
  | [![Map rendered from installed source](docs/images/comparisons/source/map-mcp.png)](docs/images/comparisons/source/map-mcp.png) | [![Chaos Redux event options](docs/images/comparisons/source/event-chain-mcp.png)](docs/images/comparisons/source/event-chain-mcp.png) |
85
87
 
@@ -101,43 +103,43 @@ The [examples guide](docs/examples.md) has more prompt details.
101
103
  The default server exposes 35 tools.
102
104
  Your agent can select them from ordinary language; use the live tool schema for call parameters.
103
105
 
104
- | Tool | Purpose |
105
- | --------------------------- | -------------------------------------------------------------------------------------------- |
106
- | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
107
- | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
108
- | `hoi4.focus_raster` | Produce a PNG preview with decoded source icons. |
109
- | `hoi4.focus_rewrite` | Create or update a focus tree. |
110
- | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
111
- | `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
112
- | `hoi4.gui_rewrite` | Create or update a GUI source package. |
113
- | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
114
- | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
115
- | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
116
- | `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
117
- | `hoi4.reference_search` | Search bounded local documentation sections. |
118
- | `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
119
- | `hoi4.source_lookup` | Find exact definitions, overrides, usages, and narrow source blocks. |
120
- | `hoi4.script_validate` | Check snippet syntax, native command kinds, and declared scopes against local documentation. |
121
- | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
122
- | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
123
- | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
124
- | `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
125
- | `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
126
- | `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
127
- | `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
128
- | `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
129
- | `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
130
- | `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
131
- | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
132
- | `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
133
- | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
134
- | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
135
- | `hoi4.probability_evaluate` | Evaluate supported weights, probability and timing models, and missing inputs. |
136
- | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
137
- | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
138
- | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
139
- | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
140
- | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
106
+ | Tool | Purpose |
107
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
108
+ | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
109
+ | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
110
+ | `hoi4.focus_raster` | Produce a PNG preview with decoded source icons. |
111
+ | `hoi4.focus_rewrite` | Create or update a focus tree. |
112
+ | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
113
+ | `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
114
+ | `hoi4.gui_rewrite` | Create or update a GUI source package. |
115
+ | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
116
+ | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
117
+ | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
118
+ | `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
119
+ | `hoi4.reference_search` | Search local documentation in plain words or script names; each result names its answering line. |
120
+ | `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
121
+ | `hoi4.source_lookup` | Find exact definitions, overrides and usages across game, DLC and mod, or the definition containing a file line. |
122
+ | `hoi4.script_validate` | Check snippet syntax, native command kinds, and declared scopes against local documentation. |
123
+ | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
124
+ | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
125
+ | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
126
+ | `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
127
+ | `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
128
+ | `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
129
+ | `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
130
+ | `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
131
+ | `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
132
+ | `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
133
+ | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
134
+ | `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
135
+ | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
136
+ | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
137
+ | `hoi4.probability_evaluate` | Evaluate supported weights, probability and timing models, and missing inputs. |
138
+ | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
139
+ | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
140
+ | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
141
+ | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
142
+ | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
141
143
 
142
144
  Set `HOI4_AGENT_TOOLS_CHAOSX=1` to expose two optional Chaos Redux workflow tools: `chaosx.focus_country_assets` and `chaosx.visual_revision`.
143
145
  The [tool guide](docs/tools.md) explains results, linked artifacts and long operations.
@@ -81,6 +81,8 @@ export const workspaceRegistrationSchema = z
81
81
  root: z.string().min(1),
82
82
  kind: z.enum(['mod', 'game', 'dependency']).default('mod'),
83
83
  gameRoot: z.string().min(1).optional(),
84
+ /** Layer installed `dlc/` and `integrated_dlc/` folders between the game and mods. */
85
+ includeGameDlc: z.boolean().default(true),
84
86
  dependencyRoots: z.array(z.string().min(1)).max(WORKSPACE_MAX_SOURCE_ROOTS).default([]),
85
87
  dependencies: z.array(dependencyRegistrationSchema).max(WORKSPACE_MAX_SOURCE_ROOTS).default([]),
86
88
  replacePaths: z.array(relativeRootPathSchema).max(WORKSPACE_MAX_PATHS).default([]),
@@ -1,10 +1,10 @@
1
1
  import { lstat, mkdir, open, opendir, rename, unlink } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { hostname } from 'node:os';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
4
  import { z } from 'zod/v4';
6
5
  import { canonicalJson, hashCanonical, secureId } from './canonical.js';
7
6
  import { ServiceError } from './result.js';
7
+ import { retryWindowsSharing } from './windows-sharing.js';
8
8
  import { SharedRequestCapacity } from './shared-request-capacity.js';
9
9
  import { containedGeneratedPath } from './workspace.js';
10
10
  import { PACKAGE_VERSION } from '../version.js';
@@ -248,26 +248,8 @@ async function assertUnlinked(file) {
248
248
  }
249
249
  /** Atomic replacement with a finite retry for Windows sharing violations. */
250
250
  export async function publishJobRecord(temporary, target, options = {}) {
251
- const platform = options.platform ?? process.platform;
252
- const publish = options.publish ?? rename;
253
- const wait = options.wait ?? delay;
254
- const maxAttempts = options.maxAttempts ?? 100;
255
- if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
256
- throw new RangeError('Job record publication attempts must be a positive integer');
257
- for (let attempt = 1;; attempt += 1) {
258
- try {
259
- await publish(temporary, target);
260
- return;
261
- }
262
- catch (error) {
263
- const code = error.code ?? '';
264
- if (platform !== 'win32' ||
265
- !['EPERM', 'EBUSY', 'EACCES'].includes(code) ||
266
- attempt >= maxAttempts)
267
- throw error;
268
- await wait(10);
269
- }
270
- }
251
+ const { publish = rename, ...retry } = options;
252
+ await retryWindowsSharing(() => publish(temporary, target), retry);
271
253
  }
272
254
  /** Internal durable job records. Callers must obtain scopes from the authorized workspace resolver. */
273
255
  export class JobStore {
@@ -376,22 +358,14 @@ export class JobStore {
376
358
  if (!['completed', 'cancelled', 'failed'].includes(record.status))
377
359
  throw new ServiceError('JOB_STATE_CONFLICT', 'Only a terminal job record can be removed by retention cleanup');
378
360
  const file = await this.recordPath(scope, id);
379
- for (let attempt = 1;; attempt += 1) {
380
- signal.throwIfAborted();
381
- try {
382
- await unlink(file);
383
- return true;
384
- }
385
- catch (error) {
386
- const code = error.code ?? '';
387
- if (code === 'ENOENT')
388
- return false;
389
- if (process.platform !== 'win32' ||
390
- !['EPERM', 'EBUSY', 'EACCES'].includes(code) ||
391
- attempt >= 100)
392
- throw error;
393
- await delay(10);
394
- }
361
+ try {
362
+ await retryWindowsSharing(() => unlink(file), { signal });
363
+ return true;
364
+ }
365
+ catch (error) {
366
+ if (error.code === 'ENOENT')
367
+ return false;
368
+ throw error;
395
369
  }
396
370
  });
397
371
  }
@@ -573,11 +547,16 @@ export class JobStore {
573
547
  async readEnvelope(file, id, scopeHash) {
574
548
  let handle;
575
549
  try {
576
- handle = await open(file, 'r');
550
+ // A record being replaced is briefly delete-pending on Windows. Release the
551
+ // handle before parsing so a slow reader never blocks a concurrent publication.
552
+ handle = await retryWindowsSharing(() => open(file, 'r'));
577
553
  const metadata = await handle.stat();
578
554
  if (!metadata.isFile() || metadata.size > this.maxRecordBytes)
579
555
  throw new ServiceError('JOB_RECORD_INVALID', 'Persistent job record exceeds its storage budget or is not a file');
580
- const parsed = envelopeSchema.safeParse(JSON.parse(await handle.readFile('utf8')));
556
+ const text = await handle.readFile('utf8');
557
+ await handle.close();
558
+ handle = undefined;
559
+ const parsed = envelopeSchema.safeParse(JSON.parse(text));
581
560
  if (!parsed.success ||
582
561
  parsed.data.record.id !== id ||
583
562
  hashCanonical(parsed.data.record.scope) !== scopeHash ||
@@ -632,7 +611,7 @@ export class JobStore {
632
611
  }
633
612
  finally {
634
613
  await handle?.close();
635
- await unlink(temporary).catch((error) => {
614
+ await retryWindowsSharing(() => unlink(temporary)).catch((error) => {
636
615
  if (error.code !== 'ENOENT')
637
616
  throw error;
638
617
  });
@@ -10,6 +10,53 @@ import { containedGeneratedPath } from './workspace.js';
10
10
  import { ServiceError } from './result.js';
11
11
  const maximumCheckpointRecoveries = 2;
12
12
  const maximumPreDispatchRecoveries = 2;
13
+ // Native and IPC codes are fixed vocabulary. Error messages can contain paths or source
14
+ // text, so only an allowlisted code and the fixed coordination stage are reported.
15
+ const nativeFailureCodes = new Set([
16
+ 'EACCES',
17
+ 'EAGAIN',
18
+ 'EBADF',
19
+ 'EBUSY',
20
+ 'ECHILD',
21
+ 'ECONNRESET',
22
+ 'EEXIST',
23
+ 'EIO',
24
+ 'EMFILE',
25
+ 'ENFILE',
26
+ 'ENOENT',
27
+ 'ENOMEM',
28
+ 'ENOSPC',
29
+ 'ENOTDIR',
30
+ 'ENOTEMPTY',
31
+ 'EPERM',
32
+ 'EPIPE',
33
+ 'ESRCH',
34
+ 'ETIMEDOUT',
35
+ 'ERR_IPC_CHANNEL_CLOSED',
36
+ 'ERR_IPC_DISCONNECTED',
37
+ ]);
38
+ function nativeFailureCode(error) {
39
+ if (typeof error !== 'object' || error === null || !('code' in error))
40
+ return undefined;
41
+ return typeof error.code === 'string' && nativeFailureCodes.has(error.code)
42
+ ? error.code
43
+ : undefined;
44
+ }
45
+ /** Map a launcher failure to a stable, path-free job failure. */
46
+ export function workerFailure(error, stage) {
47
+ if (error instanceof ServiceError)
48
+ return { code: error.code, message: error.message };
49
+ const nativeCode = nativeFailureCode(error);
50
+ return nativeCode === undefined
51
+ ? {
52
+ code: 'JOB_WORKER_FAILED',
53
+ message: `The isolated worker stopped without publishing a result (stage ${stage})`,
54
+ }
55
+ : {
56
+ code: `JOB_WORKER_${nativeCode}`,
57
+ message: `Worker coordination could not complete (${nativeCode}, stage ${stage}); inspect the retained job outcome`,
58
+ };
59
+ }
13
60
  /** Bounded, fixed-entry child execution; client cancellation/disconnection never kills a worker. */
14
61
  export class JobWorkerHost {
15
62
  engine;
@@ -69,10 +116,13 @@ export class JobWorkerHost {
69
116
  throw new ServiceError('JOB_OPERATION_UNAVAILABLE', 'This domain has not yet been registered for worker execution');
70
117
  const signal = new AbortController().signal;
71
118
  const recovered = new Set();
119
+ const progress = { stage: 'admission' };
72
120
  let preDispatchRecoveries = 0;
73
121
  for (;;) {
74
122
  try {
75
- await this.scheduler.run(this.owner, 1024, signal, () => this.capacity.run(signal, (lease) => this.launch(workspaceId, id, lease, principal)));
123
+ progress.stage = 'admission';
124
+ await this.scheduler.run(this.owner, 1024, signal, () => this.capacity.run(signal, (lease) => this.launch(workspaceId, id, lease, progress, principal)));
125
+ progress.stage = 'supervision';
76
126
  const current = await this.jobs.get(workspaceId, id, principal);
77
127
  if (['completed', 'cancelled', 'failed'].includes(current.status))
78
128
  return current;
@@ -104,12 +154,7 @@ export class JobWorkerHost {
104
154
  recovered.add(checkpoint);
105
155
  continue;
106
156
  }
107
- return await this.jobs.failInterrupted(workspaceId, id, {
108
- code: error instanceof ServiceError ? error.code : 'JOB_WORKER_FAILED',
109
- message: error instanceof ServiceError
110
- ? error.message
111
- : 'The isolated worker stopped without publishing a result',
112
- }, principal);
157
+ return await this.jobs.failInterrupted(workspaceId, id, workerFailure(error, progress.stage), principal);
113
158
  }
114
159
  }
115
160
  }
@@ -126,7 +171,7 @@ export class JobWorkerHost {
126
171
  await delay(100);
127
172
  }
128
173
  }
129
- async launch(workspaceId, id, lease, principal) {
174
+ async launch(workspaceId, id, lease, progress, principal) {
130
175
  // Resolve the same configured workspace and grants again immediately before dispatch.
131
176
  await this.jobs.get(workspaceId, id, principal);
132
177
  const sourceMode = import.meta.url.endsWith('.ts');
@@ -143,6 +188,7 @@ export class JobWorkerHost {
143
188
  workspaceIds: [...new Set([...grant.workspaceIds, ...discoveredWorkspaceIds])],
144
189
  }
145
190
  : grant;
191
+ progress.stage = 'startup';
146
192
  const child = spawn(process.execPath, [...(sourceMode ? ['--import', import.meta.resolve('tsx')] : []), entry], {
147
193
  detached: true,
148
194
  cwd: process.cwd(),
@@ -162,7 +208,9 @@ export class JobWorkerHost {
162
208
  value.type !== 'ready' ||
163
209
  child.pid === undefined)
164
210
  throw new ServiceError('JOB_WORKER_PROTOCOL', 'The worker did not provide its readiness handshake');
211
+ progress.stage = 'handoff';
165
212
  await lease.handoffToProcess(child.pid);
213
+ progress.stage = 'dispatch';
166
214
  await new Promise((resolve, reject) => {
167
215
  phase = 'waiting_for_acceptance';
168
216
  acknowledgmentTimer = setTimeout(() => reject(new ServiceError('JOB_WORKER_ACCEPT_TIMEOUT', 'The worker did not acknowledge receipt within its startup window')), 30_000);
@@ -216,6 +264,7 @@ export class JobWorkerHost {
216
264
  finally {
217
265
  clearTimeout(acknowledgmentTimer);
218
266
  }
267
+ progress.stage = 'supervision';
219
268
  for (;;) {
220
269
  const record = await this.jobs.get(workspaceId, id, principal);
221
270
  if (['completed', 'failed', 'cancelled'].includes(record.status))
@@ -9,7 +9,7 @@ import { probabilityCompareRequestSchema, probabilityEvaluateRequestSchema, prob
9
9
  import { technologyCompareRequestSchema, technologyInspectRequestSchema, technologyRenderRequestSchema, } from '../schemas/technology.js';
10
10
  import { currentJobOwner, jobOwnerLiveness, jobRetentionExpired, } from './job-store.js';
11
11
  import { JobService } from './job-service.js';
12
- import { JobWorkerHost } from './job-worker-host.js';
12
+ import { JobWorkerHost, workerFailure } from './job-worker-host.js';
13
13
  import { compareCodeUnits, hashCanonical, secureId } from './canonical.js';
14
14
  import { ServiceError } from './result.js';
15
15
  import { errorResult } from './operation-result.js';
@@ -378,12 +378,7 @@ export class OperationTaskService {
378
378
  .then(() => undefined)
379
379
  .catch(async (error) => {
380
380
  const jobs = await this.jobs();
381
- await jobs.failInterrupted(record.scope.workspaceId, record.id, {
382
- code: error instanceof ServiceError ? error.code : 'JOB_WORKER_FAILED',
383
- message: error instanceof ServiceError
384
- ? error.message
385
- : 'The isolated worker could not be started',
386
- }, principal);
381
+ await jobs.failInterrupted(record.scope.workspaceId, record.id, workerFailure(error, 'admission'), principal);
387
382
  })
388
383
  .finally(() => {
389
384
  this.launches.delete(key);
@@ -52,6 +52,8 @@ function normalizeRelative(value) {
52
52
  return value.replaceAll('\\', '/').replace(/^\.\//u, '');
53
53
  }
54
54
  function rootLabel(root) {
55
+ if (root.kind === 'dlc' && root.label !== undefined)
56
+ return root.label;
55
57
  return root.kind === 'dependency' ? `dependency-${root.loadOrder}` : root.kind;
56
58
  }
57
59
  function hiddenByReplacePath(workspace, root, relativePath) {
@@ -130,6 +132,7 @@ export class WorkspaceScanner {
130
132
  }
131
133
  const roots = workspace.roots
132
134
  .filter((root) => (root.kind === 'game' ||
135
+ root.kind === 'dlc' ||
133
136
  root.kind === 'dependency' ||
134
137
  root.kind === 'mod' ||
135
138
  root.kind === 'fixture') &&
@@ -6,6 +6,7 @@ import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { sha256Bytes } from './canonical.js';
7
7
  import { canonicalPath, containedGeneratedPath } from './workspace.js';
8
8
  import { ServiceError } from './result.js';
9
+ import { retryWindowsSharing, sharingRetryDelay } from './windows-sharing.js';
9
10
  function processAlive(pid) {
10
11
  try {
11
12
  process.kill(pid, 0);
@@ -18,9 +19,13 @@ function processAlive(pid) {
18
19
  export class SharedRequestCapacity {
19
20
  stateRoot;
20
21
  capacity;
21
- constructor(stateRoot, capacity = 4) {
22
+ deletePendingLimitMs;
23
+ now;
24
+ constructor(stateRoot, capacity = 4, options = {}) {
22
25
  this.stateRoot = stateRoot;
23
26
  this.capacity = capacity;
27
+ this.deletePendingLimitMs = options.deletePendingLimitMs ?? 60_000;
28
+ this.now = options.now ?? Date.now;
24
29
  }
25
30
  async run(signal, action) {
26
31
  signal.throwIfAborted();
@@ -33,7 +38,9 @@ export class SharedRequestCapacity {
33
38
  const root = await containedGeneratedPath(await canonicalPath(this.stateRoot, signal), 'request-capacity', sha256Bytes(hostname().toLowerCase()).slice(0, 16));
34
39
  await mkdir(root, { recursive: true });
35
40
  const lease = `${process.pid}-${randomUUID()}.lease`;
36
- const pendingDeletionFailures = new Map();
41
+ // First observation time of each slot that Windows still reports as delete-pending.
42
+ const pendingDeletionSince = new Map();
43
+ let waits = 0;
37
44
  for (;;) {
38
45
  signal.throwIfAborted();
39
46
  for (let index = 0; index < this.capacity; index += 1) {
@@ -43,31 +50,35 @@ export class SharedRequestCapacity {
43
50
  const slot = path.join(root, String(index));
44
51
  try {
45
52
  await mkdir(slot);
46
- pendingDeletionFailures.delete(index);
53
+ pendingDeletionSince.delete(index);
47
54
  }
48
55
  catch (error) {
49
56
  const code = error.code;
50
57
  // Windows can retain a delete-pending directory handle after rmdir
51
58
  // returns. Recreating that slot reports EPERM/EBUSY, not EEXIST.
52
- // No work is admitted until a later mkdir actually succeeds.
59
+ // Contending waiters keep such a slot pending while they inspect it, so the
60
+ // bound is elapsed time rather than a count of observations. No work is
61
+ // admitted until a later mkdir actually succeeds.
53
62
  if (process.platform === 'win32' && ['EPERM', 'EBUSY'].includes(code ?? '')) {
54
- const failures = (pendingDeletionFailures.get(index) ?? 0) + 1;
55
- pendingDeletionFailures.set(index, failures);
56
- if (failures >= 50)
57
- throw error;
63
+ const since = pendingDeletionSince.get(index) ?? this.now();
64
+ pendingDeletionSince.set(index, since);
65
+ if (this.now() - since >= this.deletePendingLimitMs)
66
+ throw new ServiceError('REQUEST_CAPACITY_UNAVAILABLE', `An execution capacity slot stayed unavailable (${code}) beyond its admission window`);
58
67
  await this.reap(slot);
59
68
  continue;
60
69
  }
61
70
  if (code !== 'EEXIST')
62
71
  throw error;
63
- pendingDeletionFailures.delete(index);
72
+ pendingDeletionSince.delete(index);
64
73
  await this.reap(slot);
65
74
  continue;
66
75
  }
67
76
  let owner;
68
77
  try {
69
78
  owner = path.join(slot, lease);
70
- await writeFile(owner, '', { flag: 'wx', mode: 0o600 });
79
+ await retryWindowsSharing(() => writeFile(owner, '', { flag: 'wx', mode: 0o600 }), {
80
+ signal,
81
+ });
71
82
  }
72
83
  catch (error) {
73
84
  // A competing stale-owner cleanup can remove an empty slot before our
@@ -91,7 +102,7 @@ export class SharedRequestCapacity {
91
102
  throw new ServiceError('REQUEST_LEASE_OWNER_INVALID', 'Capacity can only transfer to another live process');
92
103
  const next = path.join(slot, `${pid}-${randomUUID()}.lease`);
93
104
  try {
94
- await rename(owner, next);
105
+ await retryWindowsSharing(() => rename(owner, next));
95
106
  }
96
107
  catch (error) {
97
108
  if (error.code === 'ENOENT')
@@ -107,7 +118,7 @@ export class SharedRequestCapacity {
107
118
  // A launcher can fail while its admitted child still runs. Keep that child's
108
119
  // lease until process-exit evidence permits a later reaper to reclaim it.
109
120
  if (transferredPid === undefined || !processAlive(transferredPid)) {
110
- await unlink(owner).catch((error) => {
121
+ await retryWindowsSharing(() => unlink(owner)).catch((error) => {
111
122
  if (error.code !== 'ENOENT')
112
123
  throw error;
113
124
  });
@@ -120,7 +131,10 @@ export class SharedRequestCapacity {
120
131
  }
121
132
  }
122
133
  }
123
- await delay(100, undefined, { signal });
134
+ // Short first waits keep a briefly held lock responsive; the cap and jitter keep
135
+ // many contending processes from polling the shared directory in lockstep.
136
+ waits += 1;
137
+ await delay(sharingRetryDelay(waits) * (0.75 + Math.random() / 2), undefined, { signal });
124
138
  }
125
139
  }
126
140
  async reap(slot) {