open-multi-agent-kit 0.96.1 → 0.96.2

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 (167) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +12 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/resource-doctor-cli.d.ts +22 -0
  7. package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
  8. package/dist/commands/resource-doctor-cli.js +98 -0
  9. package/dist/commands/resource-doctor-cli.js.map +1 -0
  10. package/dist/core/agent-session.d.ts +37 -1
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +289 -5
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/completion-sound.d.ts +109 -0
  15. package/dist/core/completion-sound.d.ts.map +1 -0
  16. package/dist/core/completion-sound.js +165 -0
  17. package/dist/core/completion-sound.js.map +1 -0
  18. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  19. package/dist/core/context-budget-system-prompt-items.js +2 -2
  20. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  21. package/dist/core/host-resource-probe.d.ts +63 -0
  22. package/dist/core/host-resource-probe.d.ts.map +1 -0
  23. package/dist/core/host-resource-probe.js +126 -0
  24. package/dist/core/host-resource-probe.js.map +1 -0
  25. package/dist/core/host-resource-snapshot.d.ts +54 -0
  26. package/dist/core/host-resource-snapshot.d.ts.map +1 -0
  27. package/dist/core/host-resource-snapshot.js +139 -0
  28. package/dist/core/host-resource-snapshot.js.map +1 -0
  29. package/dist/core/prompt-settlement.d.ts +81 -0
  30. package/dist/core/prompt-settlement.d.ts.map +1 -0
  31. package/dist/core/prompt-settlement.js +84 -0
  32. package/dist/core/prompt-settlement.js.map +1 -0
  33. package/dist/core/provider-resilience.d.ts.map +1 -1
  34. package/dist/core/provider-resilience.js +1 -1
  35. package/dist/core/provider-resilience.js.map +1 -1
  36. package/dist/core/provider-retry.d.ts +7 -0
  37. package/dist/core/provider-retry.d.ts.map +1 -1
  38. package/dist/core/provider-retry.js +24 -0
  39. package/dist/core/provider-retry.js.map +1 -1
  40. package/dist/core/provider-usage.d.ts +15 -2
  41. package/dist/core/provider-usage.d.ts.map +1 -1
  42. package/dist/core/provider-usage.js +110 -2
  43. package/dist/core/provider-usage.js.map +1 -1
  44. package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
  45. package/dist/core/reasoning-router-resolver.js +1 -1
  46. package/dist/core/reasoning-router-resolver.js.map +1 -1
  47. package/dist/core/resource-admission-config.d.ts +55 -0
  48. package/dist/core/resource-admission-config.d.ts.map +1 -0
  49. package/dist/core/resource-admission-config.js +97 -0
  50. package/dist/core/resource-admission-config.js.map +1 -0
  51. package/dist/core/resource-admission.d.ts +81 -0
  52. package/dist/core/resource-admission.d.ts.map +1 -0
  53. package/dist/core/resource-admission.js +128 -0
  54. package/dist/core/resource-admission.js.map +1 -0
  55. package/dist/core/resource-governor-format.d.ts +50 -0
  56. package/dist/core/resource-governor-format.d.ts.map +1 -0
  57. package/dist/core/resource-governor-format.js +113 -0
  58. package/dist/core/resource-governor-format.js.map +1 -0
  59. package/dist/core/resource-governor-settings.d.ts +56 -0
  60. package/dist/core/resource-governor-settings.d.ts.map +1 -0
  61. package/dist/core/resource-governor-settings.js +89 -0
  62. package/dist/core/resource-governor-settings.js.map +1 -0
  63. package/dist/core/resource-observation-journal.d.ts +74 -0
  64. package/dist/core/resource-observation-journal.d.ts.map +1 -0
  65. package/dist/core/resource-observation-journal.js +198 -0
  66. package/dist/core/resource-observation-journal.js.map +1 -0
  67. package/dist/core/resource-safety-gate.d.ts +54 -0
  68. package/dist/core/resource-safety-gate.d.ts.map +1 -0
  69. package/dist/core/resource-safety-gate.js +51 -0
  70. package/dist/core/resource-safety-gate.js.map +1 -0
  71. package/dist/core/run-journal.d.ts.map +1 -1
  72. package/dist/core/run-journal.js +13 -1
  73. package/dist/core/run-journal.js.map +1 -1
  74. package/dist/core/run-resource-lease.d.ts +73 -0
  75. package/dist/core/run-resource-lease.d.ts.map +1 -0
  76. package/dist/core/run-resource-lease.js +78 -0
  77. package/dist/core/run-resource-lease.js.map +1 -0
  78. package/dist/core/session-bash-service.d.ts +12 -0
  79. package/dist/core/session-bash-service.d.ts.map +1 -1
  80. package/dist/core/session-bash-service.js +14 -0
  81. package/dist/core/session-bash-service.js.map +1 -1
  82. package/dist/core/session-system-prompt.d.ts +2 -0
  83. package/dist/core/session-system-prompt.d.ts.map +1 -1
  84. package/dist/core/session-system-prompt.js +2 -0
  85. package/dist/core/session-system-prompt.js.map +1 -1
  86. package/dist/core/session-termination.d.ts +7 -2
  87. package/dist/core/session-termination.d.ts.map +1 -1
  88. package/dist/core/session-termination.js +29 -1
  89. package/dist/core/session-termination.js.map +1 -1
  90. package/dist/core/settings-manager.d.ts +12 -0
  91. package/dist/core/settings-manager.d.ts.map +1 -1
  92. package/dist/core/settings-manager.js +36 -0
  93. package/dist/core/settings-manager.js.map +1 -1
  94. package/dist/core/slash-commands.d.ts.map +1 -1
  95. package/dist/core/slash-commands.js +1 -0
  96. package/dist/core/slash-commands.js.map +1 -1
  97. package/dist/core/subagent-lane-launcher.d.ts +64 -0
  98. package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
  99. package/dist/core/subagent-lane-launcher.js +107 -0
  100. package/dist/core/subagent-lane-launcher.js.map +1 -0
  101. package/dist/core/system-cpu-sampler.d.ts +45 -0
  102. package/dist/core/system-cpu-sampler.d.ts.map +1 -0
  103. package/dist/core/system-cpu-sampler.js +95 -0
  104. package/dist/core/system-cpu-sampler.js.map +1 -0
  105. package/dist/core/system-prompt.d.ts.map +1 -1
  106. package/dist/core/system-prompt.js +5 -8
  107. package/dist/core/system-prompt.js.map +1 -1
  108. package/dist/core/workload-classifier.d.ts +32 -0
  109. package/dist/core/workload-classifier.d.ts.map +1 -0
  110. package/dist/core/workload-classifier.js +66 -0
  111. package/dist/core/workload-classifier.js.map +1 -0
  112. package/dist/core/workload-family-matcher.d.ts +3 -0
  113. package/dist/core/workload-family-matcher.d.ts.map +1 -0
  114. package/dist/core/workload-family-matcher.js +126 -0
  115. package/dist/core/workload-family-matcher.js.map +1 -0
  116. package/dist/core/workload-permit-pool.d.ts +78 -0
  117. package/dist/core/workload-permit-pool.d.ts.map +1 -0
  118. package/dist/core/workload-permit-pool.js +138 -0
  119. package/dist/core/workload-permit-pool.js.map +1 -0
  120. package/dist/core/workload-shard-executor.d.ts +68 -0
  121. package/dist/core/workload-shard-executor.d.ts.map +1 -0
  122. package/dist/core/workload-shard-executor.js +156 -0
  123. package/dist/core/workload-shard-executor.js.map +1 -0
  124. package/dist/core/workload-shard-plan.d.ts +81 -0
  125. package/dist/core/workload-shard-plan.d.ts.map +1 -0
  126. package/dist/core/workload-shard-plan.js +183 -0
  127. package/dist/core/workload-shard-plan.js.map +1 -0
  128. package/dist/core/workload-shard-runner.d.ts +11 -0
  129. package/dist/core/workload-shard-runner.d.ts.map +1 -0
  130. package/dist/core/workload-shard-runner.js +98 -0
  131. package/dist/core/workload-shard-runner.js.map +1 -0
  132. package/dist/core/workload-shard-store.d.ts +44 -0
  133. package/dist/core/workload-shard-store.d.ts.map +1 -0
  134. package/dist/core/workload-shard-store.js +189 -0
  135. package/dist/core/workload-shard-store.js.map +1 -0
  136. package/dist/core/workload-sharders.d.ts +47 -0
  137. package/dist/core/workload-sharders.d.ts.map +1 -0
  138. package/dist/core/workload-sharders.js +162 -0
  139. package/dist/core/workload-sharders.js.map +1 -0
  140. package/dist/core/workload-shell-scan.d.ts +12 -0
  141. package/dist/core/workload-shell-scan.d.ts.map +1 -0
  142. package/dist/core/workload-shell-scan.js +79 -0
  143. package/dist/core/workload-shell-scan.js.map +1 -0
  144. package/dist/main.d.ts.map +1 -1
  145. package/dist/main.js +6 -0
  146. package/dist/main.js.map +1 -1
  147. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.js +81 -0
  150. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  151. package/docs/providers.md +1 -1
  152. package/docs/sdk.md +4 -0
  153. package/docs/settings.md +64 -2
  154. package/docs/skills.md +16 -0
  155. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  156. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  157. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  158. package/examples/extensions/gondolin/package-lock.json +2 -2
  159. package/examples/extensions/gondolin/package.json +1 -1
  160. package/examples/extensions/sandbox/package-lock.json +2 -2
  161. package/examples/extensions/sandbox/package.json +1 -1
  162. package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
  163. package/examples/extensions/subagent/domain-profiles.ts +0 -1
  164. package/examples/extensions/with-deps/package-lock.json +2 -2
  165. package/examples/extensions/with-deps/package.json +1 -1
  166. package/npm-shrinkwrap.json +18 -18
  167. package/package.json +7 -9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-shard-runner.js","sourceRoot":"","sources":["../../src/core/workload-shard-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAShE,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,KAAoC,EACpC,WAAiD,EACjD,OAAe,EACf,GAAe,EACC;IAChB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7C,IAAI,aAAuC,CAAC;IAC5C,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACtB,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC;gBAC7C,SAAS,EAAE,SAAS,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,EAAE;gBAClD,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;gBACnC,aAAa,EAAE,OAAO;gBACtB,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,SAAS,EAAE,KAAK,CAAC,mBAAmB,IAAI,OAAO;aAC/C,CAAC,CAAC;YACH,aAAa,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,OAAO,GAAG,KAAK,YAAY,mBAAmB,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;YACjF,oEAAoE;YACpE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACd,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;gBACjE,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;YACnG,CAAC;YACD,OAAO;QACR,CAAC;IACF,CAAC;IACD,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IACtC,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5E,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;YACjC,KAAK;YACL,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,OAAO,IAAI,CAAC;YAC/C,MAAM,EAAE,KAAK,CAAC,MAAM;SACpB,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9D,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE;YACnD,GAAG;YACH,SAAS;YACT,YAAY,EAAE,CAAC,aAAa,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;YAC9E,UAAU,EAAE,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC;SACrD,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,KAAK,GAAuB,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC/E,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE;YACnD,GAAG;YACH,SAAS;YACT,UAAU,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc;YAChE,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;SACtD,CAAC,CAAC;IACJ,CAAC;YAAS,CAAC;QACV,aAAa,EAAE,EAAE,CAAC;IACnB,CAAC;AAAA,CACD;AAED,MAAM,UAAU,eAAe,CAC9B,KAAoC,EACpC,WAAiD,EACjD,OAAe,EACf,KAAyB,EACzB,OAKC,EACM;IACP,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAClC,MAAM,OAAO,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IAC5E,MAAM,QAAQ,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC;IAC5D,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC;QAC5B,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;QACzB,OAAO;QACP,OAAO;QACP,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,EAAE;QACxC,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/E,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE;QACxB,OAAO;QACP,KAAK;QACL,OAAO;QACP,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY;QACxF,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;KACxF,CAAC,CAAC;AAAA,CACH;AAED,SAAS,gBAAgB,CAAC,MAA+B,EAAE,QAAgB,EAAsB;IAChG,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,SAAS,CAAC;IACtC,OAAO,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AAAA,CAC5C;AAED,SAAS,iBAAiB,CAAC,KAAyB,EAAE,QAAgB,EAAsB;IAC3F,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,QAAQ,QAAQ,EAAE,CAAC;IAClD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACvD;AAED,MAAM,UAAU,SAAS,CAAC,IAAuB,EAAE,OAAe,EAAqB;IACtF,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;IAC7E,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,SAAS,OAAO,gBAAgB,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["import { WorkloadPermitError } from \"./workload-permit-pool.ts\";\nimport type { ExecuteWorkloadShardPlanInput } from \"./workload-shard-executor.ts\";\nimport type {\n\tWorkloadShardPlan,\n\tWorkloadShardProjection,\n\tWorkloadShardSpec,\n\tWorkloadShardState,\n} from \"./workload-shard-plan.ts\";\n\nexport async function runOneShard(\n\tinput: ExecuteWorkloadShardPlanInput,\n\tprojections: Map<string, WorkloadShardProjection>,\n\tshardId: string,\n\tnow: () => Date,\n): Promise<void> {\n\tconst shard = shardById(input.plan, shardId);\n\tlet releasePermit: (() => void) | undefined;\n\tif (input.permitPool) {\n\t\ttry {\n\t\t\tconst permit = await input.permitPool.acquire({\n\t\t\t\trequestId: `shard-${input.plan.planId}-${shardId}`,\n\t\t\t\tpromptRunId: input.plan.promptRunId,\n\t\t\t\tworkloadClass: \"heavy\",\n\t\t\t\tweight: 1,\n\t\t\t\tsignal: input.signal,\n\t\t\t\ttimeoutMs: input.permitWaitTimeoutMs ?? 120_000,\n\t\t\t});\n\t\t\treleasePermit = () => permit.release();\n\t\t} catch (error) {\n\t\t\tconst aborted = error instanceof WorkloadPermitError && error.code === \"aborted\";\n\t\t\t// Never ran: stays pending; an aborted wait is not a shard failure.\n\t\t\tif (!aborted) {\n\t\t\t\tapplyTransition(input, projections, shardId, \"running\", { now });\n\t\t\t\tapplyTransition(input, projections, shardId, \"failed\", { reasonCode: \"permit.unavailable\", now });\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t}\n\tconst startedAt = now().toISOString();\n\tapplyTransition(input, projections, shardId, \"running\", { now, startedAt });\n\ttry {\n\t\tconst result = await input.runner({\n\t\t\tshard,\n\t\t\tattempt: projections.get(shardId)?.attempt ?? 1,\n\t\t\tsignal: input.signal,\n\t\t});\n\t\tconst state = terminalStateFor(input.signal, result.exitCode);\n\t\tapplyTransition(input, projections, shardId, state, {\n\t\t\tnow,\n\t\t\tstartedAt,\n\t\t\tevidenceRefs: [`exit-code:${result.exitCode}`, ...(result.evidenceRefs ?? [])],\n\t\t\treasonCode: terminalReasonFor(state, result.exitCode),\n\t\t});\n\t} catch (error) {\n\t\tconst state: WorkloadShardState = input.signal?.aborted ? \"aborted\" : \"failed\";\n\t\tapplyTransition(input, projections, shardId, state, {\n\t\t\tnow,\n\t\t\tstartedAt,\n\t\t\treasonCode: state === \"aborted\" ? \"run.aborted\" : \"runner.error\",\n\t\t\tevidenceRefs: [`error:${String(error).slice(0, 120)}`],\n\t\t});\n\t} finally {\n\t\treleasePermit?.();\n\t}\n}\n\nexport function applyTransition(\n\tinput: ExecuteWorkloadShardPlanInput,\n\tprojections: Map<string, WorkloadShardProjection>,\n\tshardId: string,\n\tstate: WorkloadShardState,\n\toptions: {\n\t\treadonly now: () => Date;\n\t\treadonly startedAt?: string;\n\t\treadonly evidenceRefs?: readonly string[];\n\t\treadonly reasonCode?: string;\n\t},\n): void {\n\tconst current = projections.get(shardId);\n\tif (current === undefined) return;\n\tconst attempt = state === \"running\" ? current.attempt + 1 : current.attempt;\n\tconst terminal = state !== \"pending\" && state !== \"running\";\n\tinput.store.appendTransition({\n\t\tschemaVersion: 1,\n\t\tplanId: input.plan.planId,\n\t\tshardId,\n\t\tattempt,\n\t\tstate,\n\t\t...(options.startedAt !== undefined && state === \"running\" ? { startedAt: options.startedAt } : {}),\n\t\t...(terminal ? { finishedAt: options.now().toISOString() } : {}),\n\t\tevidenceRefs: options.evidenceRefs ?? [],\n\t\t...(options.reasonCode !== undefined ? { reasonCode: options.reasonCode } : {}),\n\t});\n\tprojections.set(shardId, {\n\t\tshardId,\n\t\tstate,\n\t\tattempt,\n\t\tevidenceRefs: options.evidenceRefs?.length ? options.evidenceRefs : current.evidenceRefs,\n\t\treasonCode: options.reasonCode ?? (state === \"pending\" ? undefined : current.reasonCode),\n\t});\n}\n\nfunction terminalStateFor(signal: AbortSignal | undefined, exitCode: number): WorkloadShardState {\n\tif (signal?.aborted) return \"aborted\";\n\treturn exitCode === 0 ? \"passed\" : \"failed\";\n}\n\nfunction terminalReasonFor(state: WorkloadShardState, exitCode: number): string | undefined {\n\tif (state === \"failed\") return `exit.${exitCode}`;\n\treturn state === \"aborted\" ? \"run.aborted\" : undefined;\n}\n\nexport function shardById(plan: WorkloadShardPlan, shardId: string): WorkloadShardSpec {\n\tconst shard = plan.shards.find((candidate) => candidate.shardId === shardId);\n\tif (shard === undefined) {\n\t\tthrow new Error(`shard ${shardId} not in plan ${plan.planId}`);\n\t}\n\treturn shard;\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { type WorkloadShardPlan, type WorkloadShardProjection, type WorkloadShardRecord } from "./workload-shard-plan.ts";
2
+ /**
3
+ * Durable workload shard journal (OMK v0.97.x roadmap §13.3/§13.5, M5/PR8).
4
+ *
5
+ * Append-only JSONL at `.omk/runs/<promptRunId>/workload-shards.jsonl` with
6
+ * a per-line hash chain. Every append is validated against the in-memory
7
+ * projection BEFORE it is written, and every load re-verifies parse, chain,
8
+ * and §13.4 transition legality. Corruption fails closed (§21, §32 kill
9
+ * criteria: never resume over a corrupt journal): the caller quarantines
10
+ * the file and requires explicit repair; the journal itself is preserved
11
+ * for §30.4 rollback.
12
+ */
13
+ export declare const WORKLOAD_SHARD_JOURNAL_FILE = "workload-shards.jsonl";
14
+ export type WorkloadShardJournalErrorCode = "io" | "parse" | "chain" | "plan" | "transition";
15
+ export declare class WorkloadShardJournalError extends Error {
16
+ readonly code: WorkloadShardJournalErrorCode;
17
+ readonly line?: number;
18
+ constructor(code: WorkloadShardJournalErrorCode, message: string, line?: number);
19
+ }
20
+ export declare function workloadShardJournalPath(cwd: string, promptRunId: string): string;
21
+ export declare class WorkloadShardStore {
22
+ private readonly journalPath;
23
+ private state;
24
+ constructor(journalPath: string);
25
+ static open(cwd: string, promptRunId: string): WorkloadShardStore;
26
+ get filePath(): string;
27
+ /** Record the plan as the first line. Fails closed if any plan already exists. */
28
+ appendPlan(plan: WorkloadShardPlan): void;
29
+ /** Validate one §13.4 transition against the projection, then append it. */
30
+ appendTransition(record: WorkloadShardRecord): void;
31
+ /** Load and fully verify the journal. A missing file is an empty journal. */
32
+ load(): {
33
+ readonly plan: WorkloadShardPlan | null;
34
+ readonly records: readonly WorkloadShardRecord[];
35
+ readonly projections: ReadonlyMap<string, WorkloadShardProjection> | null;
36
+ };
37
+ /** §21 fail-closed path: move a corrupt journal aside; explicit repair required. */
38
+ quarantine(): string | null;
39
+ private ensureLoaded;
40
+ private parseLine;
41
+ private verifyChain;
42
+ private appendLine;
43
+ }
44
+ //# sourceMappingURL=workload-shard-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-shard-store.d.ts","sourceRoot":"","sources":["../../src/core/workload-shard-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAGN,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EAExB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,2BAA2B,0BAA0B,CAAC;AAInE,MAAM,MAAM,6BAA6B,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;AAE7F,qBAAa,yBAA0B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,IAAI,EAAE,6BAA6B,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAK9E;CACD;AAWD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKjF;AASD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,KAAK,CAA6B;IAE1C,YAAY,WAAW,EAAE,MAAM,EAE9B;IAED,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAEhE;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,kFAAkF;IAClF,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAWxC;IAED,6EAA4E;IAC5E,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAgBlD;IAED,6EAA6E;IAC7E,IAAI,IAAI;QACP,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;QACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;QACjD,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,uBAAuB,CAAC,GAAG,IAAI,CAAC;KAC1E,CAIA;IAED,qFAAoF;IACpF,UAAU,IAAI,MAAM,GAAG,IAAI,CAY1B;IAED,OAAO,CAAC,YAAY;IAqCpB,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;CAmBlB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n\treduceShardRecords,\n\tvalidateWorkloadShardPlan,\n\ttype WorkloadShardPlan,\n\ttype WorkloadShardProjection,\n\ttype WorkloadShardRecord,\n\tWorkloadShardTransitionError,\n} from \"./workload-shard-plan.ts\";\n\n/**\n * Durable workload shard journal (OMK v0.97.x roadmap §13.3/§13.5, M5/PR8).\n *\n * Append-only JSONL at `.omk/runs/<promptRunId>/workload-shards.jsonl` with\n * a per-line hash chain. Every append is validated against the in-memory\n * projection BEFORE it is written, and every load re-verifies parse, chain,\n * and §13.4 transition legality. Corruption fails closed (§21, §32 kill\n * criteria: never resume over a corrupt journal): the caller quarantines\n * the file and requires explicit repair; the journal itself is preserved\n * for §30.4 rollback.\n */\n\nexport const WORKLOAD_SHARD_JOURNAL_FILE = \"workload-shards.jsonl\";\nconst GENESIS_DIGEST = \"workload-shard-genesis\";\nconst RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;\n\nexport type WorkloadShardJournalErrorCode = \"io\" | \"parse\" | \"chain\" | \"plan\" | \"transition\";\n\nexport class WorkloadShardJournalError extends Error {\n\treadonly code: WorkloadShardJournalErrorCode;\n\treadonly line?: number;\n\tconstructor(code: WorkloadShardJournalErrorCode, message: string, line?: number) {\n\t\tsuper(line === undefined ? message : `${message} (line ${line})`);\n\t\tthis.name = \"WorkloadShardJournalError\";\n\t\tthis.code = code;\n\t\tthis.line = line;\n\t}\n}\n\ntype JournalLine =\n\t| { readonly seq: number; readonly digest: string; readonly kind: \"plan\"; readonly plan: WorkloadShardPlan }\n\t| {\n\t\t\treadonly seq: number;\n\t\t\treadonly digest: string;\n\t\t\treadonly kind: \"transition\";\n\t\t\treadonly record: WorkloadShardRecord;\n\t };\n\nexport function workloadShardJournalPath(cwd: string, promptRunId: string): string {\n\tif (!RUN_ID_PATTERN.test(promptRunId)) {\n\t\tthrow new WorkloadShardJournalError(\"io\", `invalid promptRunId for journal path: ${JSON.stringify(promptRunId)}`);\n\t}\n\treturn path.join(cwd, \".omk\", \"runs\", promptRunId, WORKLOAD_SHARD_JOURNAL_FILE);\n}\n\ninterface JournalState {\n\tplan: WorkloadShardPlan | null;\n\trecords: WorkloadShardRecord[];\n\tlastDigest: string;\n\tnextSeq: number;\n}\n\nexport class WorkloadShardStore {\n\tprivate readonly journalPath: string;\n\tprivate state: JournalState | null = null;\n\n\tconstructor(journalPath: string) {\n\t\tthis.journalPath = journalPath;\n\t}\n\n\tstatic open(cwd: string, promptRunId: string): WorkloadShardStore {\n\t\treturn new WorkloadShardStore(workloadShardJournalPath(cwd, promptRunId));\n\t}\n\n\tget filePath(): string {\n\t\treturn this.journalPath;\n\t}\n\n\t/** Record the plan as the first line. Fails closed if any plan already exists. */\n\tappendPlan(plan: WorkloadShardPlan): void {\n\t\tconst state = this.ensureLoaded();\n\t\tif (state.plan !== null) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"journal already contains a plan\");\n\t\t}\n\t\tconst planErrors = validateWorkloadShardPlan(plan);\n\t\tif (planErrors.length > 0) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", `invalid plan: ${planErrors.join(\"; \")}`);\n\t\t}\n\t\tthis.appendLine(state, { kind: \"plan\", plan });\n\t\tstate.plan = plan;\n\t}\n\n\t/** Validate one §13.4 transition against the projection, then append it. */\n\tappendTransition(record: WorkloadShardRecord): void {\n\t\tconst state = this.ensureLoaded();\n\t\tif (state.plan === null) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"cannot append a transition before the plan\");\n\t\t}\n\t\ttry {\n\t\t\t// Replaying with the candidate appended proves legality before any write.\n\t\t\treduceShardRecords(state.plan, [...state.records, record]);\n\t\t} catch (error) {\n\t\t\tif (error instanceof WorkloadShardTransitionError) {\n\t\t\t\tthrow new WorkloadShardJournalError(\"transition\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tthis.appendLine(state, { kind: \"transition\", record });\n\t\tstate.records.push(record);\n\t}\n\n\t/** Load and fully verify the journal. A missing file is an empty journal. */\n\tload(): {\n\t\treadonly plan: WorkloadShardPlan | null;\n\t\treadonly records: readonly WorkloadShardRecord[];\n\t\treadonly projections: ReadonlyMap<string, WorkloadShardProjection> | null;\n\t} {\n\t\tconst state = this.ensureLoaded();\n\t\tconst projections = state.plan === null ? null : reduceShardRecords(state.plan, state.records);\n\t\treturn { plan: state.plan, records: [...state.records], projections };\n\t}\n\n\t/** §21 fail-closed path: move a corrupt journal aside; explicit repair required. */\n\tquarantine(): string | null {\n\t\ttry {\n\t\t\tif (!fs.existsSync(this.journalPath)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst target = `${this.journalPath}.corrupt-${Date.now()}`;\n\t\t\tfs.renameSync(this.journalPath, target);\n\t\t\tthis.state = null;\n\t\t\treturn target;\n\t\t} catch (error) {\n\t\t\tthrow new WorkloadShardJournalError(\"io\", `quarantine failed: ${String(error)}`);\n\t\t}\n\t}\n\n\tprivate ensureLoaded(): JournalState {\n\t\tif (this.state !== null) {\n\t\t\treturn this.state;\n\t\t}\n\t\tconst state: JournalState = { plan: null, records: [], lastDigest: GENESIS_DIGEST, nextSeq: 0 };\n\t\tif (fs.existsSync(this.journalPath)) {\n\t\t\tconst raw = fs.readFileSync(this.journalPath, \"utf8\");\n\t\t\tconst lines = raw.split(\"\\n\").filter((line) => line.trim() !== \"\");\n\t\t\tfor (let index = 0; index < lines.length; index++) {\n\t\t\t\tconst parsed = this.parseLine(lines[index], index + 1);\n\t\t\t\tthis.verifyChain(state, parsed, index + 1);\n\t\t\t\tif (parsed.kind === \"plan\") {\n\t\t\t\t\tif (state.plan !== null) {\n\t\t\t\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"duplicate plan line\", index + 1);\n\t\t\t\t\t}\n\t\t\t\t\tstate.plan = parsed.plan;\n\t\t\t\t} else {\n\t\t\t\t\tif (state.plan === null) {\n\t\t\t\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"transition before plan\", index + 1);\n\t\t\t\t\t}\n\t\t\t\t\tstate.records.push(parsed.record);\n\t\t\t\t}\n\t\t\t\tstate.lastDigest = parsed.digest;\n\t\t\t\tstate.nextSeq = parsed.seq + 1;\n\t\t\t}\n\t\t\tif (state.plan !== null) {\n\t\t\t\ttry {\n\t\t\t\t\treduceShardRecords(state.plan, state.records);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow new WorkloadShardJournalError(\"transition\", `replay failed: ${String(error)}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.state = state;\n\t\treturn state;\n\t}\n\n\tprivate parseLine(line: string, lineNumber: number): JournalLine {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch {\n\t\t\tthrow new WorkloadShardJournalError(\"parse\", \"journal line is not valid JSON\", lineNumber);\n\t\t}\n\t\tconst candidate = parsed as Partial<JournalLine>;\n\t\tif (\n\t\t\ttypeof candidate !== \"object\" ||\n\t\t\tcandidate === null ||\n\t\t\ttypeof candidate.seq !== \"number\" ||\n\t\t\ttypeof candidate.digest !== \"string\" ||\n\t\t\t(candidate.kind !== \"plan\" && candidate.kind !== \"transition\")\n\t\t) {\n\t\t\tthrow new WorkloadShardJournalError(\"parse\", \"journal line has an unknown shape\", lineNumber);\n\t\t}\n\t\treturn candidate as JournalLine;\n\t}\n\n\tprivate verifyChain(state: JournalState, line: JournalLine, lineNumber: number): void {\n\t\tif (line.seq !== state.nextSeq) {\n\t\t\tthrow new WorkloadShardJournalError(\"chain\", `sequence ${line.seq} != expected ${state.nextSeq}`, lineNumber);\n\t\t}\n\t\tconst expected = chainDigest(state.lastDigest, line);\n\t\tif (line.digest !== expected) {\n\t\t\tthrow new WorkloadShardJournalError(\"chain\", \"digest chain mismatch\", lineNumber);\n\t\t}\n\t}\n\n\tprivate appendLine(\n\t\tstate: JournalState,\n\t\tbody: { kind: \"plan\"; plan: WorkloadShardPlan } | { kind: \"transition\"; record: WorkloadShardRecord },\n\t): void {\n\t\tconst seq = state.nextSeq;\n\t\tconst digest = chainDigest(state.lastDigest, { seq, ...body });\n\t\tconst line: JournalLine =\n\t\t\tbody.kind === \"plan\"\n\t\t\t\t? { seq, digest, kind: \"plan\", plan: body.plan }\n\t\t\t\t: { seq, digest, kind: \"transition\", record: body.record };\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.journalPath), { recursive: true });\n\t\t\tfs.appendFileSync(this.journalPath, `${JSON.stringify(line)}\\n`, \"utf8\");\n\t\t} catch (error) {\n\t\t\tthrow new WorkloadShardJournalError(\"io\", `append failed: ${String(error)}`);\n\t\t}\n\t\tstate.lastDigest = digest;\n\t\tstate.nextSeq = seq + 1;\n\t}\n}\n\n/** Digest over the previous digest plus the canonical payload (digest field excluded). */\nfunction chainDigest(\n\tpreviousDigest: string,\n\tline: { readonly seq: number } & (\n\t\t| { readonly kind: \"plan\"; readonly plan: WorkloadShardPlan }\n\t\t| { readonly kind: \"transition\"; readonly record: WorkloadShardRecord }\n\t),\n): string {\n\tconst payload =\n\t\tline.kind === \"plan\"\n\t\t\t? JSON.stringify({ seq: line.seq, kind: line.kind, plan: line.plan })\n\t\t\t: JSON.stringify({ seq: line.seq, kind: line.kind, record: line.record });\n\treturn createHash(\"sha256\").update(`${previousDigest}\\n${payload}`, \"utf8\").digest(\"hex\");\n}\n"]}
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { reduceShardRecords, validateWorkloadShardPlan, WorkloadShardTransitionError, } from "./workload-shard-plan.js";
5
+ /**
6
+ * Durable workload shard journal (OMK v0.97.x roadmap §13.3/§13.5, M5/PR8).
7
+ *
8
+ * Append-only JSONL at `.omk/runs/<promptRunId>/workload-shards.jsonl` with
9
+ * a per-line hash chain. Every append is validated against the in-memory
10
+ * projection BEFORE it is written, and every load re-verifies parse, chain,
11
+ * and §13.4 transition legality. Corruption fails closed (§21, §32 kill
12
+ * criteria: never resume over a corrupt journal): the caller quarantines
13
+ * the file and requires explicit repair; the journal itself is preserved
14
+ * for §30.4 rollback.
15
+ */
16
+ export const WORKLOAD_SHARD_JOURNAL_FILE = "workload-shards.jsonl";
17
+ const GENESIS_DIGEST = "workload-shard-genesis";
18
+ const RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
19
+ export class WorkloadShardJournalError extends Error {
20
+ code;
21
+ line;
22
+ constructor(code, message, line) {
23
+ super(line === undefined ? message : `${message} (line ${line})`);
24
+ this.name = "WorkloadShardJournalError";
25
+ this.code = code;
26
+ this.line = line;
27
+ }
28
+ }
29
+ export function workloadShardJournalPath(cwd, promptRunId) {
30
+ if (!RUN_ID_PATTERN.test(promptRunId)) {
31
+ throw new WorkloadShardJournalError("io", `invalid promptRunId for journal path: ${JSON.stringify(promptRunId)}`);
32
+ }
33
+ return path.join(cwd, ".omk", "runs", promptRunId, WORKLOAD_SHARD_JOURNAL_FILE);
34
+ }
35
+ export class WorkloadShardStore {
36
+ journalPath;
37
+ state = null;
38
+ constructor(journalPath) {
39
+ this.journalPath = journalPath;
40
+ }
41
+ static open(cwd, promptRunId) {
42
+ return new WorkloadShardStore(workloadShardJournalPath(cwd, promptRunId));
43
+ }
44
+ get filePath() {
45
+ return this.journalPath;
46
+ }
47
+ /** Record the plan as the first line. Fails closed if any plan already exists. */
48
+ appendPlan(plan) {
49
+ const state = this.ensureLoaded();
50
+ if (state.plan !== null) {
51
+ throw new WorkloadShardJournalError("plan", "journal already contains a plan");
52
+ }
53
+ const planErrors = validateWorkloadShardPlan(plan);
54
+ if (planErrors.length > 0) {
55
+ throw new WorkloadShardJournalError("plan", `invalid plan: ${planErrors.join("; ")}`);
56
+ }
57
+ this.appendLine(state, { kind: "plan", plan });
58
+ state.plan = plan;
59
+ }
60
+ /** Validate one §13.4 transition against the projection, then append it. */
61
+ appendTransition(record) {
62
+ const state = this.ensureLoaded();
63
+ if (state.plan === null) {
64
+ throw new WorkloadShardJournalError("plan", "cannot append a transition before the plan");
65
+ }
66
+ try {
67
+ // Replaying with the candidate appended proves legality before any write.
68
+ reduceShardRecords(state.plan, [...state.records, record]);
69
+ }
70
+ catch (error) {
71
+ if (error instanceof WorkloadShardTransitionError) {
72
+ throw new WorkloadShardJournalError("transition", error.message);
73
+ }
74
+ throw error;
75
+ }
76
+ this.appendLine(state, { kind: "transition", record });
77
+ state.records.push(record);
78
+ }
79
+ /** Load and fully verify the journal. A missing file is an empty journal. */
80
+ load() {
81
+ const state = this.ensureLoaded();
82
+ const projections = state.plan === null ? null : reduceShardRecords(state.plan, state.records);
83
+ return { plan: state.plan, records: [...state.records], projections };
84
+ }
85
+ /** §21 fail-closed path: move a corrupt journal aside; explicit repair required. */
86
+ quarantine() {
87
+ try {
88
+ if (!fs.existsSync(this.journalPath)) {
89
+ return null;
90
+ }
91
+ const target = `${this.journalPath}.corrupt-${Date.now()}`;
92
+ fs.renameSync(this.journalPath, target);
93
+ this.state = null;
94
+ return target;
95
+ }
96
+ catch (error) {
97
+ throw new WorkloadShardJournalError("io", `quarantine failed: ${String(error)}`);
98
+ }
99
+ }
100
+ ensureLoaded() {
101
+ if (this.state !== null) {
102
+ return this.state;
103
+ }
104
+ const state = { plan: null, records: [], lastDigest: GENESIS_DIGEST, nextSeq: 0 };
105
+ if (fs.existsSync(this.journalPath)) {
106
+ const raw = fs.readFileSync(this.journalPath, "utf8");
107
+ const lines = raw.split("\n").filter((line) => line.trim() !== "");
108
+ for (let index = 0; index < lines.length; index++) {
109
+ const parsed = this.parseLine(lines[index], index + 1);
110
+ this.verifyChain(state, parsed, index + 1);
111
+ if (parsed.kind === "plan") {
112
+ if (state.plan !== null) {
113
+ throw new WorkloadShardJournalError("plan", "duplicate plan line", index + 1);
114
+ }
115
+ state.plan = parsed.plan;
116
+ }
117
+ else {
118
+ if (state.plan === null) {
119
+ throw new WorkloadShardJournalError("plan", "transition before plan", index + 1);
120
+ }
121
+ state.records.push(parsed.record);
122
+ }
123
+ state.lastDigest = parsed.digest;
124
+ state.nextSeq = parsed.seq + 1;
125
+ }
126
+ if (state.plan !== null) {
127
+ try {
128
+ reduceShardRecords(state.plan, state.records);
129
+ }
130
+ catch (error) {
131
+ throw new WorkloadShardJournalError("transition", `replay failed: ${String(error)}`);
132
+ }
133
+ }
134
+ }
135
+ this.state = state;
136
+ return state;
137
+ }
138
+ parseLine(line, lineNumber) {
139
+ let parsed;
140
+ try {
141
+ parsed = JSON.parse(line);
142
+ }
143
+ catch {
144
+ throw new WorkloadShardJournalError("parse", "journal line is not valid JSON", lineNumber);
145
+ }
146
+ const candidate = parsed;
147
+ if (typeof candidate !== "object" ||
148
+ candidate === null ||
149
+ typeof candidate.seq !== "number" ||
150
+ typeof candidate.digest !== "string" ||
151
+ (candidate.kind !== "plan" && candidate.kind !== "transition")) {
152
+ throw new WorkloadShardJournalError("parse", "journal line has an unknown shape", lineNumber);
153
+ }
154
+ return candidate;
155
+ }
156
+ verifyChain(state, line, lineNumber) {
157
+ if (line.seq !== state.nextSeq) {
158
+ throw new WorkloadShardJournalError("chain", `sequence ${line.seq} != expected ${state.nextSeq}`, lineNumber);
159
+ }
160
+ const expected = chainDigest(state.lastDigest, line);
161
+ if (line.digest !== expected) {
162
+ throw new WorkloadShardJournalError("chain", "digest chain mismatch", lineNumber);
163
+ }
164
+ }
165
+ appendLine(state, body) {
166
+ const seq = state.nextSeq;
167
+ const digest = chainDigest(state.lastDigest, { seq, ...body });
168
+ const line = body.kind === "plan"
169
+ ? { seq, digest, kind: "plan", plan: body.plan }
170
+ : { seq, digest, kind: "transition", record: body.record };
171
+ try {
172
+ fs.mkdirSync(path.dirname(this.journalPath), { recursive: true });
173
+ fs.appendFileSync(this.journalPath, `${JSON.stringify(line)}\n`, "utf8");
174
+ }
175
+ catch (error) {
176
+ throw new WorkloadShardJournalError("io", `append failed: ${String(error)}`);
177
+ }
178
+ state.lastDigest = digest;
179
+ state.nextSeq = seq + 1;
180
+ }
181
+ }
182
+ /** Digest over the previous digest plus the canonical payload (digest field excluded). */
183
+ function chainDigest(previousDigest, line) {
184
+ const payload = line.kind === "plan"
185
+ ? JSON.stringify({ seq: line.seq, kind: line.kind, plan: line.plan })
186
+ : JSON.stringify({ seq: line.seq, kind: line.kind, record: line.record });
187
+ return createHash("sha256").update(`${previousDigest}\n${payload}`, "utf8").digest("hex");
188
+ }
189
+ //# sourceMappingURL=workload-shard-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-shard-store.js","sourceRoot":"","sources":["../../src/core/workload-shard-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n\treduceShardRecords,\n\tvalidateWorkloadShardPlan,\n\ttype WorkloadShardPlan,\n\ttype WorkloadShardProjection,\n\ttype WorkloadShardRecord,\n\tWorkloadShardTransitionError,\n} from \"./workload-shard-plan.ts\";\n\n/**\n * Durable workload shard journal (OMK v0.97.x roadmap §13.3/§13.5, M5/PR8).\n *\n * Append-only JSONL at `.omk/runs/<promptRunId>/workload-shards.jsonl` with\n * a per-line hash chain. Every append is validated against the in-memory\n * projection BEFORE it is written, and every load re-verifies parse, chain,\n * and §13.4 transition legality. Corruption fails closed (§21, §32 kill\n * criteria: never resume over a corrupt journal): the caller quarantines\n * the file and requires explicit repair; the journal itself is preserved\n * for §30.4 rollback.\n */\n\nexport const WORKLOAD_SHARD_JOURNAL_FILE = \"workload-shards.jsonl\";\nconst GENESIS_DIGEST = \"workload-shard-genesis\";\nconst RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;\n\nexport type WorkloadShardJournalErrorCode = \"io\" | \"parse\" | \"chain\" | \"plan\" | \"transition\";\n\nexport class WorkloadShardJournalError extends Error {\n\treadonly code: WorkloadShardJournalErrorCode;\n\treadonly line?: number;\n\tconstructor(code: WorkloadShardJournalErrorCode, message: string, line?: number) {\n\t\tsuper(line === undefined ? message : `${message} (line ${line})`);\n\t\tthis.name = \"WorkloadShardJournalError\";\n\t\tthis.code = code;\n\t\tthis.line = line;\n\t}\n}\n\ntype JournalLine =\n\t| { readonly seq: number; readonly digest: string; readonly kind: \"plan\"; readonly plan: WorkloadShardPlan }\n\t| {\n\t\t\treadonly seq: number;\n\t\t\treadonly digest: string;\n\t\t\treadonly kind: \"transition\";\n\t\t\treadonly record: WorkloadShardRecord;\n\t };\n\nexport function workloadShardJournalPath(cwd: string, promptRunId: string): string {\n\tif (!RUN_ID_PATTERN.test(promptRunId)) {\n\t\tthrow new WorkloadShardJournalError(\"io\", `invalid promptRunId for journal path: ${JSON.stringify(promptRunId)}`);\n\t}\n\treturn path.join(cwd, \".omk\", \"runs\", promptRunId, WORKLOAD_SHARD_JOURNAL_FILE);\n}\n\ninterface JournalState {\n\tplan: WorkloadShardPlan | null;\n\trecords: WorkloadShardRecord[];\n\tlastDigest: string;\n\tnextSeq: number;\n}\n\nexport class WorkloadShardStore {\n\tprivate readonly journalPath: string;\n\tprivate state: JournalState | null = null;\n\n\tconstructor(journalPath: string) {\n\t\tthis.journalPath = journalPath;\n\t}\n\n\tstatic open(cwd: string, promptRunId: string): WorkloadShardStore {\n\t\treturn new WorkloadShardStore(workloadShardJournalPath(cwd, promptRunId));\n\t}\n\n\tget filePath(): string {\n\t\treturn this.journalPath;\n\t}\n\n\t/** Record the plan as the first line. Fails closed if any plan already exists. */\n\tappendPlan(plan: WorkloadShardPlan): void {\n\t\tconst state = this.ensureLoaded();\n\t\tif (state.plan !== null) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"journal already contains a plan\");\n\t\t}\n\t\tconst planErrors = validateWorkloadShardPlan(plan);\n\t\tif (planErrors.length > 0) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", `invalid plan: ${planErrors.join(\"; \")}`);\n\t\t}\n\t\tthis.appendLine(state, { kind: \"plan\", plan });\n\t\tstate.plan = plan;\n\t}\n\n\t/** Validate one §13.4 transition against the projection, then append it. */\n\tappendTransition(record: WorkloadShardRecord): void {\n\t\tconst state = this.ensureLoaded();\n\t\tif (state.plan === null) {\n\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"cannot append a transition before the plan\");\n\t\t}\n\t\ttry {\n\t\t\t// Replaying with the candidate appended proves legality before any write.\n\t\t\treduceShardRecords(state.plan, [...state.records, record]);\n\t\t} catch (error) {\n\t\t\tif (error instanceof WorkloadShardTransitionError) {\n\t\t\t\tthrow new WorkloadShardJournalError(\"transition\", error.message);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tthis.appendLine(state, { kind: \"transition\", record });\n\t\tstate.records.push(record);\n\t}\n\n\t/** Load and fully verify the journal. A missing file is an empty journal. */\n\tload(): {\n\t\treadonly plan: WorkloadShardPlan | null;\n\t\treadonly records: readonly WorkloadShardRecord[];\n\t\treadonly projections: ReadonlyMap<string, WorkloadShardProjection> | null;\n\t} {\n\t\tconst state = this.ensureLoaded();\n\t\tconst projections = state.plan === null ? null : reduceShardRecords(state.plan, state.records);\n\t\treturn { plan: state.plan, records: [...state.records], projections };\n\t}\n\n\t/** §21 fail-closed path: move a corrupt journal aside; explicit repair required. */\n\tquarantine(): string | null {\n\t\ttry {\n\t\t\tif (!fs.existsSync(this.journalPath)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst target = `${this.journalPath}.corrupt-${Date.now()}`;\n\t\t\tfs.renameSync(this.journalPath, target);\n\t\t\tthis.state = null;\n\t\t\treturn target;\n\t\t} catch (error) {\n\t\t\tthrow new WorkloadShardJournalError(\"io\", `quarantine failed: ${String(error)}`);\n\t\t}\n\t}\n\n\tprivate ensureLoaded(): JournalState {\n\t\tif (this.state !== null) {\n\t\t\treturn this.state;\n\t\t}\n\t\tconst state: JournalState = { plan: null, records: [], lastDigest: GENESIS_DIGEST, nextSeq: 0 };\n\t\tif (fs.existsSync(this.journalPath)) {\n\t\t\tconst raw = fs.readFileSync(this.journalPath, \"utf8\");\n\t\t\tconst lines = raw.split(\"\\n\").filter((line) => line.trim() !== \"\");\n\t\t\tfor (let index = 0; index < lines.length; index++) {\n\t\t\t\tconst parsed = this.parseLine(lines[index], index + 1);\n\t\t\t\tthis.verifyChain(state, parsed, index + 1);\n\t\t\t\tif (parsed.kind === \"plan\") {\n\t\t\t\t\tif (state.plan !== null) {\n\t\t\t\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"duplicate plan line\", index + 1);\n\t\t\t\t\t}\n\t\t\t\t\tstate.plan = parsed.plan;\n\t\t\t\t} else {\n\t\t\t\t\tif (state.plan === null) {\n\t\t\t\t\t\tthrow new WorkloadShardJournalError(\"plan\", \"transition before plan\", index + 1);\n\t\t\t\t\t}\n\t\t\t\t\tstate.records.push(parsed.record);\n\t\t\t\t}\n\t\t\t\tstate.lastDigest = parsed.digest;\n\t\t\t\tstate.nextSeq = parsed.seq + 1;\n\t\t\t}\n\t\t\tif (state.plan !== null) {\n\t\t\t\ttry {\n\t\t\t\t\treduceShardRecords(state.plan, state.records);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow new WorkloadShardJournalError(\"transition\", `replay failed: ${String(error)}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.state = state;\n\t\treturn state;\n\t}\n\n\tprivate parseLine(line: string, lineNumber: number): JournalLine {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch {\n\t\t\tthrow new WorkloadShardJournalError(\"parse\", \"journal line is not valid JSON\", lineNumber);\n\t\t}\n\t\tconst candidate = parsed as Partial<JournalLine>;\n\t\tif (\n\t\t\ttypeof candidate !== \"object\" ||\n\t\t\tcandidate === null ||\n\t\t\ttypeof candidate.seq !== \"number\" ||\n\t\t\ttypeof candidate.digest !== \"string\" ||\n\t\t\t(candidate.kind !== \"plan\" && candidate.kind !== \"transition\")\n\t\t) {\n\t\t\tthrow new WorkloadShardJournalError(\"parse\", \"journal line has an unknown shape\", lineNumber);\n\t\t}\n\t\treturn candidate as JournalLine;\n\t}\n\n\tprivate verifyChain(state: JournalState, line: JournalLine, lineNumber: number): void {\n\t\tif (line.seq !== state.nextSeq) {\n\t\t\tthrow new WorkloadShardJournalError(\"chain\", `sequence ${line.seq} != expected ${state.nextSeq}`, lineNumber);\n\t\t}\n\t\tconst expected = chainDigest(state.lastDigest, line);\n\t\tif (line.digest !== expected) {\n\t\t\tthrow new WorkloadShardJournalError(\"chain\", \"digest chain mismatch\", lineNumber);\n\t\t}\n\t}\n\n\tprivate appendLine(\n\t\tstate: JournalState,\n\t\tbody: { kind: \"plan\"; plan: WorkloadShardPlan } | { kind: \"transition\"; record: WorkloadShardRecord },\n\t): void {\n\t\tconst seq = state.nextSeq;\n\t\tconst digest = chainDigest(state.lastDigest, { seq, ...body });\n\t\tconst line: JournalLine =\n\t\t\tbody.kind === \"plan\"\n\t\t\t\t? { seq, digest, kind: \"plan\", plan: body.plan }\n\t\t\t\t: { seq, digest, kind: \"transition\", record: body.record };\n\t\ttry {\n\t\t\tfs.mkdirSync(path.dirname(this.journalPath), { recursive: true });\n\t\t\tfs.appendFileSync(this.journalPath, `${JSON.stringify(line)}\\n`, \"utf8\");\n\t\t} catch (error) {\n\t\t\tthrow new WorkloadShardJournalError(\"io\", `append failed: ${String(error)}`);\n\t\t}\n\t\tstate.lastDigest = digest;\n\t\tstate.nextSeq = seq + 1;\n\t}\n}\n\n/** Digest over the previous digest plus the canonical payload (digest field excluded). */\nfunction chainDigest(\n\tpreviousDigest: string,\n\tline: { readonly seq: number } & (\n\t\t| { readonly kind: \"plan\"; readonly plan: WorkloadShardPlan }\n\t\t| { readonly kind: \"transition\"; readonly record: WorkloadShardRecord }\n\t),\n): string {\n\tconst payload =\n\t\tline.kind === \"plan\"\n\t\t\t? JSON.stringify({ seq: line.seq, kind: line.kind, plan: line.plan })\n\t\t\t: JSON.stringify({ seq: line.seq, kind: line.kind, record: line.record });\n\treturn createHash(\"sha256\").update(`${previousDigest}\\n${payload}`, \"utf8\").digest(\"hex\");\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import type { WorkloadClassification } from "./workload-classifier.ts";
2
+ import { type WorkloadShardPlan } from "./workload-shard-plan.ts";
3
+ /**
4
+ * Initial sharder adapter registry (OMK v0.97.x roadmap §12.2, M5/PR9).
5
+ *
6
+ * Automatic sharding happens only when semantic equivalence is guaranteed
7
+ * (§12.1): a known sharder, simple argv, shard-local side effects, an
8
+ * order-independent result, defined aggregation, per-shard evidence, and
9
+ * resume-skip support. Everything else — §12.3's forbidden list, complex
10
+ * shell, unknown scripts — returns `unsupported` and the command is NEVER
11
+ * rewritten (§32 kill criteria).
12
+ *
13
+ * Adapters are pure: runtime facts they cannot know statically (upstream
14
+ * `--shard` capability from package metadata, the workspace list, the
15
+ * `go list ./...` package list) are injected by the M5 executor. A missing
16
+ * fact degrades to `unsupported`, never to a guess (§4.3).
17
+ */
18
+ export interface WorkloadSharderFacts {
19
+ /** §12.2 Vitest/Jest condition: upstream version supports `--shard` (package metadata). */
20
+ readonly vitestShardSupport?: boolean;
21
+ readonly jestShardSupport?: boolean;
22
+ /** Workspace names from package.json for `npm run test --workspace=<name>` fan-out. */
23
+ readonly workspaces?: readonly string[];
24
+ /** Deterministic `go list ./...` output; sorted lexically before chunking (§12.2). */
25
+ readonly goPackages?: readonly string[];
26
+ }
27
+ export interface ShardPlanRequest {
28
+ readonly promptRunId: string;
29
+ readonly command: string;
30
+ readonly classification: WorkloadClassification;
31
+ /** Desired shard fan-out (admission-derived); adapters clamp to what stays equivalent. */
32
+ readonly desiredShards: number;
33
+ readonly facts?: WorkloadSharderFacts;
34
+ /** Injectable identity/clock for deterministic tests. */
35
+ readonly planId?: string;
36
+ readonly createdAt?: string;
37
+ }
38
+ export type ShardPlanOutcome = {
39
+ readonly kind: "planned";
40
+ readonly plan: WorkloadShardPlan;
41
+ } | {
42
+ readonly kind: "unsupported";
43
+ readonly reasonCodes: readonly string[];
44
+ };
45
+ /** Build a shard plan for one already-classified command, or refuse (§12.3: no rewrite). */
46
+ export declare function planWorkloadShards(request: ShardPlanRequest): ShardPlanOutcome;
47
+ //# sourceMappingURL=workload-sharders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-sharders.d.ts","sourceRoot":"","sources":["../../src/core/workload-sharders.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAIN,KAAK,iBAAiB,EAEtB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,oBAAoB;IACpC,4FAA2F;IAC3F,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,uFAAsF;IACtF,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,sBAAsB,CAAC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IACtC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,gBAAgB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAC9D;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAK7E,6FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAmB9E","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { analyzeShellCommandV2 } from \"./command-safety-parser.ts\";\nimport type { WorkloadClassification } from \"./workload-classifier.ts\";\nimport {\n\tbuildShardCommandDescriptor,\n\tvalidateWorkloadShardPlan,\n\tWORKLOAD_SHARD_PLAN_VERSION,\n\ttype WorkloadShardPlan,\n\ttype WorkloadShardSpec,\n} from \"./workload-shard-plan.ts\";\n\n/**\n * Initial sharder adapter registry (OMK v0.97.x roadmap §12.2, M5/PR9).\n *\n * Automatic sharding happens only when semantic equivalence is guaranteed\n * (§12.1): a known sharder, simple argv, shard-local side effects, an\n * order-independent result, defined aggregation, per-shard evidence, and\n * resume-skip support. Everything else — §12.3's forbidden list, complex\n * shell, unknown scripts — returns `unsupported` and the command is NEVER\n * rewritten (§32 kill criteria).\n *\n * Adapters are pure: runtime facts they cannot know statically (upstream\n * `--shard` capability from package metadata, the workspace list, the\n * `go list ./...` package list) are injected by the M5 executor. A missing\n * fact degrades to `unsupported`, never to a guess (§4.3).\n */\n\nexport interface WorkloadSharderFacts {\n\t/** §12.2 Vitest/Jest condition: upstream version supports `--shard` (package metadata). */\n\treadonly vitestShardSupport?: boolean;\n\treadonly jestShardSupport?: boolean;\n\t/** Workspace names from package.json for `npm run test --workspace=<name>` fan-out. */\n\treadonly workspaces?: readonly string[];\n\t/** Deterministic `go list ./...` output; sorted lexically before chunking (§12.2). */\n\treadonly goPackages?: readonly string[];\n}\n\nexport interface ShardPlanRequest {\n\treadonly promptRunId: string;\n\treadonly command: string;\n\treadonly classification: WorkloadClassification;\n\t/** Desired shard fan-out (admission-derived); adapters clamp to what stays equivalent. */\n\treadonly desiredShards: number;\n\treadonly facts?: WorkloadSharderFacts;\n\t/** Injectable identity/clock for deterministic tests. */\n\treadonly planId?: string;\n\treadonly createdAt?: string;\n}\n\nexport type ShardPlanOutcome =\n\t| { readonly kind: \"planned\"; readonly plan: WorkloadShardPlan }\n\t| { readonly kind: \"unsupported\"; readonly reasonCodes: readonly string[] };\n\nconst MIN_SHARDS = 2;\nconst MAX_SHARDS = 16;\n\n/** Build a shard plan for one already-classified command, or refuse (§12.3: no rewrite). */\nexport function planWorkloadShards(request: ShardPlanRequest): ShardPlanOutcome {\n\tif (request.classification.complexity !== \"simple-argv\") {\n\t\treturn unsupported(\"shard.complex-shell\");\n\t}\n\tconst argv = extractArgv(request.command);\n\tif (argv.length === 0) {\n\t\treturn unsupported(\"shard.empty-command\");\n\t}\n\tconst desired = Math.floor(request.desiredShards);\n\tif (!Number.isFinite(desired) || desired < MIN_SHARDS) {\n\t\treturn unsupported(\"shard.fanout.insufficient\");\n\t}\n\tconst shardCount = Math.min(desired, MAX_SHARDS);\n\tfor (const adapter of SHARDER_ADAPTERS) {\n\t\tif (adapter.canHandle(argv, request.classification)) {\n\t\t\treturn adapter.plan({ request, argv, shardCount });\n\t\t}\n\t}\n\treturn unsupported(\"sharder.unknown\");\n}\n\ninterface AdapterInput {\n\treadonly request: ShardPlanRequest;\n\treadonly argv: readonly string[];\n\treadonly shardCount: number;\n}\n\ninterface SharderAdapter {\n\treadonly strategy: string;\n\tcanHandle(argv: readonly string[], classification: WorkloadClassification): boolean;\n\tplan(input: AdapterInput): ShardPlanOutcome;\n}\n\n/** §12.2 Vitest / Jest: append `--shard=i/n`, preserving every existing option. */\nfunction shardFlagAdapter(\n\tstrategy: \"vitest-shard\" | \"jest-shard\",\n\texecutable: \"vitest\" | \"jest\",\n\tsupportFact: (facts: WorkloadSharderFacts | undefined) => boolean | undefined,\n): SharderAdapter {\n\treturn {\n\t\tstrategy,\n\t\tcanHandle: (argv, classification) =>\n\t\t\tclassification.commandFamily === \"node-test\" && basename(argv[0] ?? \"\") === executable,\n\t\tplan: ({ request, argv, shardCount }) => {\n\t\t\t// §12.1 condition 2 + conflicting-flag guards live in the classifier.\n\t\t\tif (!request.classification.safeToAutoShard) {\n\t\t\t\treturn unsupported(\"shard.preconditions.failed\");\n\t\t\t}\n\t\t\tconst support = supportFact(request.facts);\n\t\t\tif (support === undefined) {\n\t\t\t\treturn unsupported(\"shard.capability.unverified\");\n\t\t\t}\n\t\t\tif (!support) {\n\t\t\t\treturn unsupported(\"shard.capability.unsupported\");\n\t\t\t}\n\t\t\tconst shards = Array.from({ length: shardCount }, (_, index) => {\n\t\t\t\tconst shardArgv = [...argv, `--shard=${index + 1}/${shardCount}`];\n\t\t\t\treturn shardSpec(`${executable}-${index + 1}-of-${shardCount}`, shardArgv, []);\n\t\t\t});\n\t\t\treturn planned(request, strategy, shardCount, shards);\n\t\t},\n\t};\n}\n\n/** §12.2 monorepo workspaces: only independent `test` scripts fan out; build keeps order. */\nconst workspaceAdapter: SharderAdapter = {\n\tstrategy: \"npm-workspace-test\",\n\tcanHandle: (argv, classification) =>\n\t\tclassification.commandFamily === \"node-test\" && basename(argv[0] ?? \"\") === \"npm\",\n\tplan: ({ request, argv, shardCount }) => {\n\t\tconst script = argv[1] === \"run\" ? argv[2] : argv[1];\n\t\tif (script !== \"test\" && script !== \"t\") {\n\t\t\t// §12.2: builds must preserve dependency order — never auto-fanned.\n\t\t\treturn unsupported(\"shard.workspace.non-test-script\");\n\t\t}\n\t\tif (argv.some((token) => token.startsWith(\"--workspace\"))) {\n\t\t\treturn unsupported(\"shard.workspace.already-scoped\");\n\t\t}\n\t\tconst workspaces = [...(request.facts?.workspaces ?? [])].sort(compareLexical);\n\t\tif (workspaces.length < MIN_SHARDS) {\n\t\t\treturn unsupported(\"shard.workspace.insufficient\");\n\t\t}\n\t\tconst chunkCount = Math.min(shardCount, workspaces.length);\n\t\tconst chunks: string[][] = Array.from({ length: chunkCount }, () => []);\n\t\tworkspaces.forEach((workspace, index) => {\n\t\t\tchunks[index % chunkCount].push(workspace);\n\t\t});\n\t\tconst shards = chunks.map((chunk, index) =>\n\t\t\tshardSpec(\n\t\t\t\t`workspace-chunk-${index + 1}-of-${chunkCount}`,\n\t\t\t\t[\"npm\", \"run\", \"test\", ...chunk.map((workspace) => `--workspace=${workspace}`)],\n\t\t\t\t[],\n\t\t\t),\n\t\t);\n\t\treturn planned(request, \"npm-workspace-test\", chunkCount, shards);\n\t},\n};\n\n/** §12.2 Go: chunk the deterministic package list; race/cache flags are preserved. */\nconst goTestAdapter: SharderAdapter = {\n\tstrategy: \"go-package-chunks\",\n\tcanHandle: (argv, classification) =>\n\t\tclassification.commandFamily === \"go-test\" && basename(argv[0] ?? \"\") === \"go\" && argv[1] === \"test\",\n\tplan: ({ request, argv, shardCount }) => {\n\t\tif (!request.classification.safeToAutoShard) {\n\t\t\treturn unsupported(\"shard.preconditions.failed\");\n\t\t}\n\t\tconst packages = [...(request.facts?.goPackages ?? [])].sort(compareLexical);\n\t\tif (packages.length < MIN_SHARDS) {\n\t\t\treturn unsupported(\"shard.go.package-list-unavailable\");\n\t\t}\n\t\tconst flags = argv.slice(2).filter((token) => token.startsWith(\"-\"));\n\t\tconst chunkCount = Math.min(shardCount, packages.length);\n\t\tconst chunks: string[][] = Array.from({ length: chunkCount }, () => []);\n\t\tpackages.forEach((packageName, index) => {\n\t\t\tchunks[index % chunkCount].push(packageName);\n\t\t});\n\t\tconst shards = chunks.map((chunk, index) =>\n\t\t\tshardSpec(`go-chunk-${index + 1}-of-${chunkCount}`, [\"go\", \"test\", ...flags, ...chunk], []),\n\t\t);\n\t\treturn planned(request, \"go-package-chunks\", chunkCount, shards);\n\t},\n};\n\nconst SHARDER_ADAPTERS: readonly SharderAdapter[] = [\n\tshardFlagAdapter(\"vitest-shard\", \"vitest\", (facts) => facts?.vitestShardSupport),\n\tshardFlagAdapter(\"jest-shard\", \"jest\", (facts) => facts?.jestShardSupport),\n\tworkspaceAdapter,\n\tgoTestAdapter,\n];\n\nfunction shardSpec(shardId: string, argv: readonly string[], dependencyIds: readonly string[]): WorkloadShardSpec {\n\treturn {\n\t\tshardId,\n\t\tdependencyIds,\n\t\tcommandDescriptor: buildShardCommandDescriptor(argv),\n\t\t// §12.1 condition 6: every shard yields its own exit evidence; the\n\t\t// aggregate receipt is evaluated by the M5 executor (§13.6).\n\t\texpectedEvidence: [\"exit-code\"],\n\t};\n}\n\nfunction planned(\n\trequest: ShardPlanRequest,\n\tstrategy: string,\n\tmaxConcurrency: number,\n\tshards: readonly WorkloadShardSpec[],\n): ShardPlanOutcome {\n\tconst plan: WorkloadShardPlan = {\n\t\tschemaVersion: WORKLOAD_SHARD_PLAN_VERSION,\n\t\tplanId: request.planId ?? `shard-plan-${randomUUID()}`,\n\t\tpromptRunId: request.promptRunId,\n\t\tcommandDigest: createHash(\"sha256\").update(request.command, \"utf8\").digest(\"hex\"),\n\t\tstrategy,\n\t\tcreatedAt: request.createdAt ?? new Date().toISOString(),\n\t\tmaxConcurrency,\n\t\tshards,\n\t};\n\tconst errors = validateWorkloadShardPlan(plan);\n\tif (errors.length > 0) {\n\t\t// Fail closed: a structurally invalid plan is never handed to the executor.\n\t\treturn unsupported(\"shard.plan.invalid\");\n\t}\n\treturn { kind: \"planned\", plan };\n}\n\nfunction unsupported(...reasonCodes: string[]): ShardPlanOutcome {\n\treturn { kind: \"unsupported\", reasonCodes };\n}\n\nfunction extractArgv(command: string): readonly string[] {\n\ttry {\n\t\tconst analysis = analyzeShellCommandV2(command);\n\t\tconst tokens = analysis.segments[0]?.tokens ?? [];\n\t\tlet start = 0;\n\t\twhile (start < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[start])) {\n\t\t\tstart += 1;\n\t\t}\n\t\treturn tokens.slice(start);\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** §12.2: deterministic lexical (code-unit) order, independent of runtime locale. */\nfunction compareLexical(a: string, b: string): number {\n\tif (a < b) {\n\t\treturn -1;\n\t}\n\treturn a > b ? 1 : 0;\n}\n\nfunction basename(token: string): string {\n\tconst slash = token.lastIndexOf(\"/\");\n\treturn slash === -1 ? token : token.slice(slash + 1);\n}\n"]}
@@ -0,0 +1,162 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { analyzeShellCommandV2 } from "./command-safety-parser.js";
3
+ import { buildShardCommandDescriptor, validateWorkloadShardPlan, WORKLOAD_SHARD_PLAN_VERSION, } from "./workload-shard-plan.js";
4
+ const MIN_SHARDS = 2;
5
+ const MAX_SHARDS = 16;
6
+ /** Build a shard plan for one already-classified command, or refuse (§12.3: no rewrite). */
7
+ export function planWorkloadShards(request) {
8
+ if (request.classification.complexity !== "simple-argv") {
9
+ return unsupported("shard.complex-shell");
10
+ }
11
+ const argv = extractArgv(request.command);
12
+ if (argv.length === 0) {
13
+ return unsupported("shard.empty-command");
14
+ }
15
+ const desired = Math.floor(request.desiredShards);
16
+ if (!Number.isFinite(desired) || desired < MIN_SHARDS) {
17
+ return unsupported("shard.fanout.insufficient");
18
+ }
19
+ const shardCount = Math.min(desired, MAX_SHARDS);
20
+ for (const adapter of SHARDER_ADAPTERS) {
21
+ if (adapter.canHandle(argv, request.classification)) {
22
+ return adapter.plan({ request, argv, shardCount });
23
+ }
24
+ }
25
+ return unsupported("sharder.unknown");
26
+ }
27
+ /** §12.2 Vitest / Jest: append `--shard=i/n`, preserving every existing option. */
28
+ function shardFlagAdapter(strategy, executable, supportFact) {
29
+ return {
30
+ strategy,
31
+ canHandle: (argv, classification) => classification.commandFamily === "node-test" && basename(argv[0] ?? "") === executable,
32
+ plan: ({ request, argv, shardCount }) => {
33
+ // §12.1 condition 2 + conflicting-flag guards live in the classifier.
34
+ if (!request.classification.safeToAutoShard) {
35
+ return unsupported("shard.preconditions.failed");
36
+ }
37
+ const support = supportFact(request.facts);
38
+ if (support === undefined) {
39
+ return unsupported("shard.capability.unverified");
40
+ }
41
+ if (!support) {
42
+ return unsupported("shard.capability.unsupported");
43
+ }
44
+ const shards = Array.from({ length: shardCount }, (_, index) => {
45
+ const shardArgv = [...argv, `--shard=${index + 1}/${shardCount}`];
46
+ return shardSpec(`${executable}-${index + 1}-of-${shardCount}`, shardArgv, []);
47
+ });
48
+ return planned(request, strategy, shardCount, shards);
49
+ },
50
+ };
51
+ }
52
+ /** §12.2 monorepo workspaces: only independent `test` scripts fan out; build keeps order. */
53
+ const workspaceAdapter = {
54
+ strategy: "npm-workspace-test",
55
+ canHandle: (argv, classification) => classification.commandFamily === "node-test" && basename(argv[0] ?? "") === "npm",
56
+ plan: ({ request, argv, shardCount }) => {
57
+ const script = argv[1] === "run" ? argv[2] : argv[1];
58
+ if (script !== "test" && script !== "t") {
59
+ // §12.2: builds must preserve dependency order — never auto-fanned.
60
+ return unsupported("shard.workspace.non-test-script");
61
+ }
62
+ if (argv.some((token) => token.startsWith("--workspace"))) {
63
+ return unsupported("shard.workspace.already-scoped");
64
+ }
65
+ const workspaces = [...(request.facts?.workspaces ?? [])].sort(compareLexical);
66
+ if (workspaces.length < MIN_SHARDS) {
67
+ return unsupported("shard.workspace.insufficient");
68
+ }
69
+ const chunkCount = Math.min(shardCount, workspaces.length);
70
+ const chunks = Array.from({ length: chunkCount }, () => []);
71
+ workspaces.forEach((workspace, index) => {
72
+ chunks[index % chunkCount].push(workspace);
73
+ });
74
+ const shards = chunks.map((chunk, index) => shardSpec(`workspace-chunk-${index + 1}-of-${chunkCount}`, ["npm", "run", "test", ...chunk.map((workspace) => `--workspace=${workspace}`)], []));
75
+ return planned(request, "npm-workspace-test", chunkCount, shards);
76
+ },
77
+ };
78
+ /** §12.2 Go: chunk the deterministic package list; race/cache flags are preserved. */
79
+ const goTestAdapter = {
80
+ strategy: "go-package-chunks",
81
+ canHandle: (argv, classification) => classification.commandFamily === "go-test" && basename(argv[0] ?? "") === "go" && argv[1] === "test",
82
+ plan: ({ request, argv, shardCount }) => {
83
+ if (!request.classification.safeToAutoShard) {
84
+ return unsupported("shard.preconditions.failed");
85
+ }
86
+ const packages = [...(request.facts?.goPackages ?? [])].sort(compareLexical);
87
+ if (packages.length < MIN_SHARDS) {
88
+ return unsupported("shard.go.package-list-unavailable");
89
+ }
90
+ const flags = argv.slice(2).filter((token) => token.startsWith("-"));
91
+ const chunkCount = Math.min(shardCount, packages.length);
92
+ const chunks = Array.from({ length: chunkCount }, () => []);
93
+ packages.forEach((packageName, index) => {
94
+ chunks[index % chunkCount].push(packageName);
95
+ });
96
+ const shards = chunks.map((chunk, index) => shardSpec(`go-chunk-${index + 1}-of-${chunkCount}`, ["go", "test", ...flags, ...chunk], []));
97
+ return planned(request, "go-package-chunks", chunkCount, shards);
98
+ },
99
+ };
100
+ const SHARDER_ADAPTERS = [
101
+ shardFlagAdapter("vitest-shard", "vitest", (facts) => facts?.vitestShardSupport),
102
+ shardFlagAdapter("jest-shard", "jest", (facts) => facts?.jestShardSupport),
103
+ workspaceAdapter,
104
+ goTestAdapter,
105
+ ];
106
+ function shardSpec(shardId, argv, dependencyIds) {
107
+ return {
108
+ shardId,
109
+ dependencyIds,
110
+ commandDescriptor: buildShardCommandDescriptor(argv),
111
+ // §12.1 condition 6: every shard yields its own exit evidence; the
112
+ // aggregate receipt is evaluated by the M5 executor (§13.6).
113
+ expectedEvidence: ["exit-code"],
114
+ };
115
+ }
116
+ function planned(request, strategy, maxConcurrency, shards) {
117
+ const plan = {
118
+ schemaVersion: WORKLOAD_SHARD_PLAN_VERSION,
119
+ planId: request.planId ?? `shard-plan-${randomUUID()}`,
120
+ promptRunId: request.promptRunId,
121
+ commandDigest: createHash("sha256").update(request.command, "utf8").digest("hex"),
122
+ strategy,
123
+ createdAt: request.createdAt ?? new Date().toISOString(),
124
+ maxConcurrency,
125
+ shards,
126
+ };
127
+ const errors = validateWorkloadShardPlan(plan);
128
+ if (errors.length > 0) {
129
+ // Fail closed: a structurally invalid plan is never handed to the executor.
130
+ return unsupported("shard.plan.invalid");
131
+ }
132
+ return { kind: "planned", plan };
133
+ }
134
+ function unsupported(...reasonCodes) {
135
+ return { kind: "unsupported", reasonCodes };
136
+ }
137
+ function extractArgv(command) {
138
+ try {
139
+ const analysis = analyzeShellCommandV2(command);
140
+ const tokens = analysis.segments[0]?.tokens ?? [];
141
+ let start = 0;
142
+ while (start < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[start])) {
143
+ start += 1;
144
+ }
145
+ return tokens.slice(start);
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ }
151
+ /** §12.2: deterministic lexical (code-unit) order, independent of runtime locale. */
152
+ function compareLexical(a, b) {
153
+ if (a < b) {
154
+ return -1;
155
+ }
156
+ return a > b ? 1 : 0;
157
+ }
158
+ function basename(token) {
159
+ const slash = token.lastIndexOf("/");
160
+ return slash === -1 ? token : token.slice(slash + 1);
161
+ }
162
+ //# sourceMappingURL=workload-sharders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-sharders.js","sourceRoot":"","sources":["../../src/core/workload-sharders.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { analyzeShellCommandV2 } from \"./command-safety-parser.ts\";\nimport type { WorkloadClassification } from \"./workload-classifier.ts\";\nimport {\n\tbuildShardCommandDescriptor,\n\tvalidateWorkloadShardPlan,\n\tWORKLOAD_SHARD_PLAN_VERSION,\n\ttype WorkloadShardPlan,\n\ttype WorkloadShardSpec,\n} from \"./workload-shard-plan.ts\";\n\n/**\n * Initial sharder adapter registry (OMK v0.97.x roadmap §12.2, M5/PR9).\n *\n * Automatic sharding happens only when semantic equivalence is guaranteed\n * (§12.1): a known sharder, simple argv, shard-local side effects, an\n * order-independent result, defined aggregation, per-shard evidence, and\n * resume-skip support. Everything else — §12.3's forbidden list, complex\n * shell, unknown scripts — returns `unsupported` and the command is NEVER\n * rewritten (§32 kill criteria).\n *\n * Adapters are pure: runtime facts they cannot know statically (upstream\n * `--shard` capability from package metadata, the workspace list, the\n * `go list ./...` package list) are injected by the M5 executor. A missing\n * fact degrades to `unsupported`, never to a guess (§4.3).\n */\n\nexport interface WorkloadSharderFacts {\n\t/** §12.2 Vitest/Jest condition: upstream version supports `--shard` (package metadata). */\n\treadonly vitestShardSupport?: boolean;\n\treadonly jestShardSupport?: boolean;\n\t/** Workspace names from package.json for `npm run test --workspace=<name>` fan-out. */\n\treadonly workspaces?: readonly string[];\n\t/** Deterministic `go list ./...` output; sorted lexically before chunking (§12.2). */\n\treadonly goPackages?: readonly string[];\n}\n\nexport interface ShardPlanRequest {\n\treadonly promptRunId: string;\n\treadonly command: string;\n\treadonly classification: WorkloadClassification;\n\t/** Desired shard fan-out (admission-derived); adapters clamp to what stays equivalent. */\n\treadonly desiredShards: number;\n\treadonly facts?: WorkloadSharderFacts;\n\t/** Injectable identity/clock for deterministic tests. */\n\treadonly planId?: string;\n\treadonly createdAt?: string;\n}\n\nexport type ShardPlanOutcome =\n\t| { readonly kind: \"planned\"; readonly plan: WorkloadShardPlan }\n\t| { readonly kind: \"unsupported\"; readonly reasonCodes: readonly string[] };\n\nconst MIN_SHARDS = 2;\nconst MAX_SHARDS = 16;\n\n/** Build a shard plan for one already-classified command, or refuse (§12.3: no rewrite). */\nexport function planWorkloadShards(request: ShardPlanRequest): ShardPlanOutcome {\n\tif (request.classification.complexity !== \"simple-argv\") {\n\t\treturn unsupported(\"shard.complex-shell\");\n\t}\n\tconst argv = extractArgv(request.command);\n\tif (argv.length === 0) {\n\t\treturn unsupported(\"shard.empty-command\");\n\t}\n\tconst desired = Math.floor(request.desiredShards);\n\tif (!Number.isFinite(desired) || desired < MIN_SHARDS) {\n\t\treturn unsupported(\"shard.fanout.insufficient\");\n\t}\n\tconst shardCount = Math.min(desired, MAX_SHARDS);\n\tfor (const adapter of SHARDER_ADAPTERS) {\n\t\tif (adapter.canHandle(argv, request.classification)) {\n\t\t\treturn adapter.plan({ request, argv, shardCount });\n\t\t}\n\t}\n\treturn unsupported(\"sharder.unknown\");\n}\n\ninterface AdapterInput {\n\treadonly request: ShardPlanRequest;\n\treadonly argv: readonly string[];\n\treadonly shardCount: number;\n}\n\ninterface SharderAdapter {\n\treadonly strategy: string;\n\tcanHandle(argv: readonly string[], classification: WorkloadClassification): boolean;\n\tplan(input: AdapterInput): ShardPlanOutcome;\n}\n\n/** §12.2 Vitest / Jest: append `--shard=i/n`, preserving every existing option. */\nfunction shardFlagAdapter(\n\tstrategy: \"vitest-shard\" | \"jest-shard\",\n\texecutable: \"vitest\" | \"jest\",\n\tsupportFact: (facts: WorkloadSharderFacts | undefined) => boolean | undefined,\n): SharderAdapter {\n\treturn {\n\t\tstrategy,\n\t\tcanHandle: (argv, classification) =>\n\t\t\tclassification.commandFamily === \"node-test\" && basename(argv[0] ?? \"\") === executable,\n\t\tplan: ({ request, argv, shardCount }) => {\n\t\t\t// §12.1 condition 2 + conflicting-flag guards live in the classifier.\n\t\t\tif (!request.classification.safeToAutoShard) {\n\t\t\t\treturn unsupported(\"shard.preconditions.failed\");\n\t\t\t}\n\t\t\tconst support = supportFact(request.facts);\n\t\t\tif (support === undefined) {\n\t\t\t\treturn unsupported(\"shard.capability.unverified\");\n\t\t\t}\n\t\t\tif (!support) {\n\t\t\t\treturn unsupported(\"shard.capability.unsupported\");\n\t\t\t}\n\t\t\tconst shards = Array.from({ length: shardCount }, (_, index) => {\n\t\t\t\tconst shardArgv = [...argv, `--shard=${index + 1}/${shardCount}`];\n\t\t\t\treturn shardSpec(`${executable}-${index + 1}-of-${shardCount}`, shardArgv, []);\n\t\t\t});\n\t\t\treturn planned(request, strategy, shardCount, shards);\n\t\t},\n\t};\n}\n\n/** §12.2 monorepo workspaces: only independent `test` scripts fan out; build keeps order. */\nconst workspaceAdapter: SharderAdapter = {\n\tstrategy: \"npm-workspace-test\",\n\tcanHandle: (argv, classification) =>\n\t\tclassification.commandFamily === \"node-test\" && basename(argv[0] ?? \"\") === \"npm\",\n\tplan: ({ request, argv, shardCount }) => {\n\t\tconst script = argv[1] === \"run\" ? argv[2] : argv[1];\n\t\tif (script !== \"test\" && script !== \"t\") {\n\t\t\t// §12.2: builds must preserve dependency order — never auto-fanned.\n\t\t\treturn unsupported(\"shard.workspace.non-test-script\");\n\t\t}\n\t\tif (argv.some((token) => token.startsWith(\"--workspace\"))) {\n\t\t\treturn unsupported(\"shard.workspace.already-scoped\");\n\t\t}\n\t\tconst workspaces = [...(request.facts?.workspaces ?? [])].sort(compareLexical);\n\t\tif (workspaces.length < MIN_SHARDS) {\n\t\t\treturn unsupported(\"shard.workspace.insufficient\");\n\t\t}\n\t\tconst chunkCount = Math.min(shardCount, workspaces.length);\n\t\tconst chunks: string[][] = Array.from({ length: chunkCount }, () => []);\n\t\tworkspaces.forEach((workspace, index) => {\n\t\t\tchunks[index % chunkCount].push(workspace);\n\t\t});\n\t\tconst shards = chunks.map((chunk, index) =>\n\t\t\tshardSpec(\n\t\t\t\t`workspace-chunk-${index + 1}-of-${chunkCount}`,\n\t\t\t\t[\"npm\", \"run\", \"test\", ...chunk.map((workspace) => `--workspace=${workspace}`)],\n\t\t\t\t[],\n\t\t\t),\n\t\t);\n\t\treturn planned(request, \"npm-workspace-test\", chunkCount, shards);\n\t},\n};\n\n/** §12.2 Go: chunk the deterministic package list; race/cache flags are preserved. */\nconst goTestAdapter: SharderAdapter = {\n\tstrategy: \"go-package-chunks\",\n\tcanHandle: (argv, classification) =>\n\t\tclassification.commandFamily === \"go-test\" && basename(argv[0] ?? \"\") === \"go\" && argv[1] === \"test\",\n\tplan: ({ request, argv, shardCount }) => {\n\t\tif (!request.classification.safeToAutoShard) {\n\t\t\treturn unsupported(\"shard.preconditions.failed\");\n\t\t}\n\t\tconst packages = [...(request.facts?.goPackages ?? [])].sort(compareLexical);\n\t\tif (packages.length < MIN_SHARDS) {\n\t\t\treturn unsupported(\"shard.go.package-list-unavailable\");\n\t\t}\n\t\tconst flags = argv.slice(2).filter((token) => token.startsWith(\"-\"));\n\t\tconst chunkCount = Math.min(shardCount, packages.length);\n\t\tconst chunks: string[][] = Array.from({ length: chunkCount }, () => []);\n\t\tpackages.forEach((packageName, index) => {\n\t\t\tchunks[index % chunkCount].push(packageName);\n\t\t});\n\t\tconst shards = chunks.map((chunk, index) =>\n\t\t\tshardSpec(`go-chunk-${index + 1}-of-${chunkCount}`, [\"go\", \"test\", ...flags, ...chunk], []),\n\t\t);\n\t\treturn planned(request, \"go-package-chunks\", chunkCount, shards);\n\t},\n};\n\nconst SHARDER_ADAPTERS: readonly SharderAdapter[] = [\n\tshardFlagAdapter(\"vitest-shard\", \"vitest\", (facts) => facts?.vitestShardSupport),\n\tshardFlagAdapter(\"jest-shard\", \"jest\", (facts) => facts?.jestShardSupport),\n\tworkspaceAdapter,\n\tgoTestAdapter,\n];\n\nfunction shardSpec(shardId: string, argv: readonly string[], dependencyIds: readonly string[]): WorkloadShardSpec {\n\treturn {\n\t\tshardId,\n\t\tdependencyIds,\n\t\tcommandDescriptor: buildShardCommandDescriptor(argv),\n\t\t// §12.1 condition 6: every shard yields its own exit evidence; the\n\t\t// aggregate receipt is evaluated by the M5 executor (§13.6).\n\t\texpectedEvidence: [\"exit-code\"],\n\t};\n}\n\nfunction planned(\n\trequest: ShardPlanRequest,\n\tstrategy: string,\n\tmaxConcurrency: number,\n\tshards: readonly WorkloadShardSpec[],\n): ShardPlanOutcome {\n\tconst plan: WorkloadShardPlan = {\n\t\tschemaVersion: WORKLOAD_SHARD_PLAN_VERSION,\n\t\tplanId: request.planId ?? `shard-plan-${randomUUID()}`,\n\t\tpromptRunId: request.promptRunId,\n\t\tcommandDigest: createHash(\"sha256\").update(request.command, \"utf8\").digest(\"hex\"),\n\t\tstrategy,\n\t\tcreatedAt: request.createdAt ?? new Date().toISOString(),\n\t\tmaxConcurrency,\n\t\tshards,\n\t};\n\tconst errors = validateWorkloadShardPlan(plan);\n\tif (errors.length > 0) {\n\t\t// Fail closed: a structurally invalid plan is never handed to the executor.\n\t\treturn unsupported(\"shard.plan.invalid\");\n\t}\n\treturn { kind: \"planned\", plan };\n}\n\nfunction unsupported(...reasonCodes: string[]): ShardPlanOutcome {\n\treturn { kind: \"unsupported\", reasonCodes };\n}\n\nfunction extractArgv(command: string): readonly string[] {\n\ttry {\n\t\tconst analysis = analyzeShellCommandV2(command);\n\t\tconst tokens = analysis.segments[0]?.tokens ?? [];\n\t\tlet start = 0;\n\t\twhile (start < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[start])) {\n\t\t\tstart += 1;\n\t\t}\n\t\treturn tokens.slice(start);\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** §12.2: deterministic lexical (code-unit) order, independent of runtime locale. */\nfunction compareLexical(a: string, b: string): number {\n\tif (a < b) {\n\t\treturn -1;\n\t}\n\treturn a > b ? 1 : 0;\n}\n\nfunction basename(token: string): string {\n\tconst slash = token.lastIndexOf(\"/\");\n\treturn slash === -1 ? token : token.slice(slash + 1);\n}\n"]}
@@ -0,0 +1,12 @@
1
+ interface ComplexityScan {
2
+ readonly reasons: readonly string[];
3
+ }
4
+ /**
5
+ * Detect the §9.3 rewrite-forbidding tokens outside quoted literals:
6
+ * `| > >> < && || ; & $( \`` plus newlines. Inside single quotes everything
7
+ * is literal; inside double quotes command substitution (`$(`, backtick)
8
+ * stays active per POSIX, other operators are literal.
9
+ */
10
+ export declare function scanShellComplexity(command: string): ComplexityScan;
11
+ export {};
12
+ //# sourceMappingURL=workload-shell-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload-shell-scan.d.ts","sourceRoot":"","sources":["../../src/core/workload-shell-scan.ts"],"names":[],"mappings":"AAAA,UAAU,cAAc;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAsEnE","sourcesContent":["interface ComplexityScan {\n\treadonly reasons: readonly string[];\n}\n\n/**\n * Detect the §9.3 rewrite-forbidding tokens outside quoted literals:\n * `| > >> < && || ; & $( \\`` plus newlines. Inside single quotes everything\n * is literal; inside double quotes command substitution (`$(`, backtick)\n * stays active per POSIX, other operators are literal.\n */\nexport function scanShellComplexity(command: string): ComplexityScan {\n\tconst reasons = new Set<string>();\n\tlet inSingle = false;\n\tlet inDouble = false;\n\tfor (let index = 0; index < command.length; index++) {\n\t\tconst ch = command[index];\n\t\tif (inSingle) {\n\t\t\tif (ch === \"'\") {\n\t\t\t\tinSingle = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (ch === \"\\\\\") {\n\t\t\t// Escaped character is literal both bare and inside double quotes.\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inDouble) {\n\t\t\tif (ch === '\"') {\n\t\t\t\tinDouble = false;\n\t\t\t} else if (ch === \"`\" || (ch === \"$\" && command[index + 1] === \"(\")) {\n\t\t\t\treasons.add(\"shell.operator.substitution\");\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tswitch (ch) {\n\t\t\tcase \"'\":\n\t\t\t\tinSingle = true;\n\t\t\t\tbreak;\n\t\t\tcase '\"':\n\t\t\t\tinDouble = true;\n\t\t\t\tbreak;\n\t\t\tcase \"`\":\n\t\t\t\treasons.add(\"shell.operator.substitution\");\n\t\t\t\tbreak;\n\t\t\tcase \"$\":\n\t\t\t\tif (command[index + 1] === \"(\") {\n\t\t\t\t\treasons.add(\"shell.operator.substitution\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"|\":\n\t\t\t\treasons.add(command[index + 1] === \"|\" ? \"shell.operator.list\" : \"shell.operator.pipe\");\n\t\t\t\tif (command[index + 1] === \"|\") {\n\t\t\t\t\tindex += 1;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"&\":\n\t\t\t\treasons.add(command[index + 1] === \"&\" ? \"shell.operator.list\" : \"shell.operator.background\");\n\t\t\t\tif (command[index + 1] === \"&\") {\n\t\t\t\t\tindex += 1;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \";\":\n\t\t\t\treasons.add(\"shell.operator.list\");\n\t\t\t\tbreak;\n\t\t\tcase \">\":\n\t\t\tcase \"<\":\n\t\t\t\treasons.add(\"shell.operator.redirect\");\n\t\t\t\tbreak;\n\t\t\tcase \"\\n\":\n\t\t\t\treasons.add(\"shell.multiline\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\tif (inSingle || inDouble) {\n\t\treasons.add(\"shell.unbalanced-quote\");\n\t}\n\treturn { reasons: [...reasons] };\n}\n"]}