elanous 0.2.16 → 0.2.17

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 (228) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +13 -13
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +13 -13
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +13 -13
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +13 -13
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +13 -13
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +13 -13
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +13 -13
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +13 -13
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +13 -13
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +13 -13
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +13 -13
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +13 -13
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +13 -13
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +13 -13
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +13 -13
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +14 -14
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +13 -13
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +13 -13
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +13 -13
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +13 -13
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +13 -13
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +13 -13
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +13 -13
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +13 -13
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +13 -13
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +13 -13
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +13 -13
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +13 -13
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +13 -13
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +13 -13
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +13 -13
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +13 -13
  143. package/graphs/intake/intake-daily.yaml +9 -2
  144. package/graphs/intake/news-sources.yaml +4 -0
  145. package/graphs/intake/recipes.yaml +6 -1
  146. package/package.json +1 -1
  147. package/scripts/__fixtures__/intake-news.xml +7 -0
  148. package/scripts/intake-news.ts +169 -0
  149. package/scripts/release-loop/docs-land-node.ts +69 -10
  150. package/scripts/release-loop/publish-node.ts +2 -2
  151. package/scripts/release-loop/release-docs.ts +134 -0
  152. package/scripts/release-loop/version-node.ts +7 -4
  153. package/scripts/release-story/draft.ts +42 -14
  154. package/src/agent-mission/driver.ts +53 -10
  155. package/src/autopilot/track-agent.ts +5 -3
  156. package/src/cli/autopilot-cli.ts +66 -0
  157. package/src/cli/decisions-cli.ts +4 -1
  158. package/src/cli/intake-cli.ts +1 -1
  159. package/src/cli/log-event-names.ts +1 -0
  160. package/src/cli/pr-cli.ts +12 -1
  161. package/src/cli/provider-cli.ts +14 -0
  162. package/src/cli/self-update-canary.ts +46 -0
  163. package/src/cli/self-update.ts +200 -1
  164. package/src/context-card/index.ts +45 -0
  165. package/src/context-card/task.ts +76 -0
  166. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  167. package/src/decisions/decision-ledger.ts +10 -1
  168. package/src/harness/harness-cli-command.ts +142 -10
  169. package/src/harness/harness-pod-dispatch.ts +11 -0
  170. package/src/harness/harness-queue.ts +86 -7
  171. package/src/harness/nested-elanous-policy.ts +23 -4
  172. package/src/hq/fence-wrapper.ts +123 -0
  173. package/src/hq/hq.ts +8 -0
  174. package/src/index.ts +57 -8
  175. package/src/intake-plane/digest.ts +39 -4
  176. package/src/nexus/api/grid.ts +61 -0
  177. package/src/nexus/api/http-server.ts +19 -1
  178. package/src/nexus/api/task-cards-api.ts +19 -0
  179. package/src/oauth/codex.ts +82 -2
  180. package/src/release-loop/landing-freeze.ts +18 -10
  181. package/src/seat-loop/seat-loop.ts +95 -31
  182. package/src/self-dev/ask-launch-flow.ts +26 -7
  183. package/src/self-dev/run-store.ts +5 -1
  184. package/src/self-dev/run-supervisor.ts +7 -1
  185. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  186. package/src/self-implement/frozen-merges.ts +25 -20
  187. package/src/self-implement/goal-author-clarification.ts +81 -0
  188. package/src/self-implement/goal-author-cli.ts +33 -5
  189. package/src/self-implement/goal-author.ts +88 -12
  190. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  191. package/src/self-implement/goal-document-write.ts +28 -0
  192. package/src/self-implement/heal-intake.ts +4 -0
  193. package/src/self-implement/host-regate.ts +3 -1
  194. package/src/self-implement/orchestrator.ts +19 -1
  195. package/src/task-agent/actions.ts +118 -0
  196. package/src/task-agent/judge.ts +96 -0
  197. package/src/task-cards/card-store.ts +4 -1
  198. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  199. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  200. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  201. package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
  202. package/src/telegram-commands.ts +1 -1
  203. package/src/telegram-loops-command.ts +21 -14
  204. package/src/user-config.ts +25 -3
  205. package/src/version/packed-revision.json +1 -1
  206. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  207. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  208. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  209. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  210. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  211. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
  212. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  213. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
  214. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  215. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  216. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  217. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  218. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  219. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  220. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  221. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
  227. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  228. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -1,9 +1,9 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFileSync, spawn } from 'node:child_process';
2
2
  import { dlopen, FFIType } from 'bun:ffi';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { constants as osConstants, hostname } from 'node:os';
6
- import { basename, dirname, resolve, sep } from 'node:path';
6
+ import { basename, dirname, join, resolve, sep } from 'node:path';
7
7
  import { findGitDir } from '../git-fs/locate.js';
8
8
  import { LogStore, logsDbPath } from '../mss/logging/log-store.js';
9
9
  import { loadSelfDevRun } from '../self-dev/run-store.js';
@@ -14,7 +14,7 @@ import { archiveGoals, type GoalArchiveOptions } from '../self-implement/goal-ar
14
14
  import { detectBursts, formatIncidentBurstWarning, incidentLastLines, readRunExits, recentBurst, recordRunExit } from './harness-incidents.js';
15
15
  import { Command, Option } from 'commander';
16
16
  import { runGitCommand } from '../git-fs/runner.js';
17
- import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, tracedPathReferences, type GoalType } from '../self-implement/goal-author.js';
17
+ import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, resolveGoalAuthorGrade, tracedPathReferences, type GoalAuthorGrade, type GoalAuthorGradeSelection, type GoalType } from '../self-implement/goal-author.js';
18
18
  import { templateForGoalType } from '../self-implement/graph-templates.js';
19
19
  import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, runLedgerDir, type RunLedgerMatch } from '../self-implement/run-ledger.js';
20
20
  import { resolveChildLlmEffort, resolveImplementationChildModel } from '../self-dev/dev-cli.js';
@@ -33,7 +33,8 @@ import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDep
33
33
  import { getUserConfig } from '../user-config.js';
34
34
  import { resolveRepositoryName } from './repository-name.js';
35
35
  import { installHarnessCliSinkHook } from './harness-cli-sink.js';
36
- import { addHarnessQueue, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps, type QueueSeat } from './harness-queue.js';
36
+ import { addHarnessQueue, HarnessQueueDuplicateError, harnessQueueReceiptPath, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps, type QueueItem, type QueueSeat } from './harness-queue.js';
37
+ import { writeHarnessQueueReceipt } from './harness-queue-child.js';
37
38
  import { parseDoorSince, queryLaunchDoors, renderLaunchDoors, type DoorTable } from './launch-stamp.js';
38
39
  import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
39
40
  import { runHarnessPlanRfc } from './harness-plan-rfc.js';
@@ -110,9 +111,12 @@ export interface HarnessAskSayOptions {
110
111
  supervise?: boolean;
111
112
  supervisorSource?: HarnessSupervisorSource;
112
113
  goalType?: GoalType;
114
+ authorGrade?: GoalAuthorGrade;
115
+ authorGradeSource?: GoalAuthorGradeSelection['source'];
113
116
  correlation?: string;
114
117
  /** Depth 0 only. At depth >= 1 this flag is ignored and the launch stays refused. */
115
118
  nestedElanous?: 'allow';
119
+ queue?: boolean;
116
120
  }
117
121
 
118
122
  export interface ResolvedHarnessSupervisor {
@@ -263,6 +267,7 @@ function registerHarnessAskSayOptions(command: Command): Command {
263
267
  .option('--yes', 'git 아닌 프로젝트에 git init 및 첫 커밋을 만들도록 동의')
264
268
  .option('--correlation <id>', '요청과 런을 잇는 불투명 correlation 값')
265
269
  .addOption(new Option('--seat <seat>', '런 귀속 자리 (OP|TC|MK|UX)').choices(['OP', 'TC', 'MK', 'UX']))
270
+ .option('--no-queue', '비상시 자리별 대기열을 건너뛰고 직접 발사')
266
271
  .addOption(new Option('--goal-type <type>', `골 종류 (${GOAL_TYPES.join('|')})`).choices([...GOAL_TYPES]))
267
272
  .option('--force-preflight', '전제 검사 막힘을 명시 요청으로 우회(관측에 남음)')
268
273
  .option('--force-gate', '같은 골 실행 중 막힘을 명시 요청으로 우회(관측에 남음; 예산 막힘은 우회 불가)')
@@ -302,6 +307,8 @@ function normalizeHarnessCommonOptions(opts: HarnessAskSayOptions): HarnessAskSa
302
307
  ...(opts.mergeByHost === true ? { mergeByHost: true } : {}),
303
308
  ...(opts.observeOnly ? { observeOnly: true } : {}),
304
309
  ...(opts.goalType !== undefined ? { goalType: opts.goalType } : {}),
310
+ ...(opts.authorGrade !== undefined ? { authorGrade: opts.authorGrade } : {}),
311
+ ...(opts.authorGradeSource !== undefined ? { authorGradeSource: opts.authorGradeSource } : {}),
305
312
  ...resolveHarnessSupervisor(opts),
306
313
  ...resolveNestedElanousOption(opts),
307
314
  };
@@ -589,6 +596,16 @@ function announceLocalChildProvider(opts: unknown): void {
589
596
  console.error(`child provider 요청 = ${child.childLlmProvider.trim()}${child.childLlmModel?.trim() ? `/${child.childLlmModel.trim()}` : ''} (local)`);
590
597
  }
591
598
 
599
+ /** `harness.queue.directSay` (ONEDOOR-2 · OP 10-06 기본 끔): config > env ELANOUS_HARNESS_QUEUE_DIRECT_SAY > false. */
600
+ export function harnessQueueDirectSay(env: NodeJS.ProcessEnv = process.env): boolean {
601
+ try {
602
+ const configured = getUserConfig().harness?.queue?.directSay;
603
+ if (typeof configured === 'boolean') return configured;
604
+ } catch { /* An unreadable config keeps the old direct path. */ }
605
+ const raw = env.ELANOUS_HARNESS_QUEUE_DIRECT_SAY?.trim().toLowerCase();
606
+ return raw === '1' || raw === 'true';
607
+ }
608
+
592
609
  /** `harness.authorOnPod` — 명시 true 만 켠다. 읽기 실패는 끔(호스트 저작)으로 둔다. */
593
610
  function harnessAuthorOnPod(): boolean {
594
611
  try { return getUserConfig().harness?.authorOnPod === true; }
@@ -633,12 +650,17 @@ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-s
633
650
  let output = '';
634
651
  let runId: string | undefined;
635
652
  const child = opts as HarnessAskSayChildLlmOptions;
653
+ const podAuthorGrade = authorOnPod && entrance === 'cli-harness-say'
654
+ ? child.authorGradeSource && child.authorGrade
655
+ ? { grade: child.authorGrade, source: child.authorGradeSource }
656
+ : resolveGoalAuthorGrade(child.authorGrade, getUserConfig().harness?.authorGrade)
657
+ : undefined;
636
658
  // 10-05 PODPROVIDER: say which child provider the Pod will use when the launch named one.
637
659
  if (child.childLlmProvider?.trim()) console.error(`child provider 요청 = ${child.childLlmProvider.trim()}${child.childLlmModel?.trim() ? `/${child.childLlmModel.trim()}` : ''} (pod · 자격 없으면 발사 전 거부)`);
638
660
  const status = await dispatchHarnessOnPod({ entrance, input, podPool: pool, ...(authorOnPod && entrance === 'cli-harness-say' ? { authorOnPod: true } : {}),
639
661
  ...(child.childLlmProvider?.trim() ? { childLlmProvider: child.childLlmProvider.trim() } : {}),
640
662
  ...(child.childLlmModel?.trim() ? { childLlmModel: child.childLlmModel.trim() } : {}),
641
- ...(child.childLlmEffort?.trim() ? { childLlmEffort: child.childLlmEffort.trim() } : {}), ...(dispatchRecorded ? { dispatchRecorded: true } : {}), ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...((opts as HarnessAskSayChildLlmOptions).goalType ? { goalType: (opts as HarnessAskSayChildLlmOptions).goalType } : {}), ...(o.after ? { after: o.after } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}), ...(o.seat ? { seat: o.seat } : {}) },
663
+ ...(child.childLlmEffort?.trim() ? { childLlmEffort: child.childLlmEffort.trim() } : {}), ...(authorOnPod && podAuthorGrade ? { authorGrade: podAuthorGrade.grade, authorGradeSource: podAuthorGrade.source } : {}), ...(dispatchRecorded ? { dispatchRecorded: true } : {}), ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...((opts as HarnessAskSayChildLlmOptions).goalType ? { goalType: (opts as HarnessAskSayChildLlmOptions).goalType } : {}), ...(o.after ? { after: o.after } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}), ...(o.seat ? { seat: o.seat } : {}) },
642
664
  { onOutput: (text) => {
643
665
  runId ??= harnessPodRunId(output + text);
644
666
  output = (output + text).slice(-16_000);
@@ -716,22 +738,102 @@ async function dispatchHarnessAskSay(
716
738
  },
717
739
  dispatch: (resolved: ResolvedHarnessSubstrate, stampedOpts: HarnessAskSayChildLlmOptions & HarnessDryRunOpts) => Promise<void>,
718
740
  gate: PreLaunchGateDeps & { readBudget?: () => Promise<BudgetDecision | 'unknown'> } = {},
741
+ queueDeps?: HarnessQueueDeps,
742
+ immediateLaunch?: HarnessQueueDeps['launch'],
743
+ immediateExit?: () => Promise<number>,
719
744
  ): Promise<void> {
720
745
  await runInjectedHarnessHandler(async () => {
721
746
  assertHarnessChildLlmModel(opts);
722
- const resolved = resolveLaunchSubstrate(opts);
723
747
  if (isHarnessDryRun(opts)) {
748
+ resolveLaunchSubstrate(opts);
724
749
  printHarnessLaunchDryRun(dryRunPreview);
725
750
  return;
726
751
  }
727
752
  // Resolve the launch directory once; descendants and the checkpoint retain the same identity.
728
753
  const queued = process.env.ELANOUS_HARNESS_QUEUE_LAUNCH && process.env.ELANOUS_HARNESS_SEAT;
729
754
  const queueSeat = queued === 'OP' || queued === 'TC' || queued === 'MK' || queued === 'UX' ? queued : undefined;
730
- const assigned = opts.seat ?? queueSeat ?? queueSeatForCwd(process.cwd(), getUserConfig().loops?.orchestrator?.seatTrees);
755
+ let cwdSeatMemo: { seat: QueueSeat | undefined } | undefined;
756
+ const cwdSeat = (): QueueSeat | undefined =>
757
+ (cwdSeatMemo ??= { seat: queueSeatForCwd(process.cwd(), getUserConfig().loops?.orchestrator?.seatTrees) }).seat;
758
+ const assigned = opts.seat ?? queueSeat ?? cwdSeat();
731
759
  const inheritedSeat = process.env.ELANOUS_HARNESS_SEAT;
760
+ const directSay = harnessQueueDirectSay();
761
+ const shouldQueue = directSay && opts.queue !== false && !process.env.ELANOUS_HARNESS_QUEUE_LAUNCH
762
+ && Boolean(opts.seat ?? cwdSeat());
763
+ const resolved = shouldQueue
764
+ ? resolveHarnessSubstrate({ flag: opts as HarnessSubstrateOpts, config: getUserConfig(), env: process.env })
765
+ : resolveLaunchSubstrate(opts);
732
766
  const podSeat = resolved.substrate === 'pod' && ['OP', 'TC', 'MK', 'UX'].includes(inheritedSeat ?? '')
733
767
  ? inheritedSeat as QueueSeat : undefined;
734
768
  const stampedSeat = resolved.substrate === 'pod' ? opts.seat ?? queueSeat ?? podSeat ?? assigned : assigned;
769
+ if (directSay && opts.queue === false) {
770
+ try { debug.log('harness.queue', 'bypass', { seat: stampedSeat ?? null, reason: '--no-queue' }); }
771
+ catch { /* Observation must not prevent an emergency launch. */ }
772
+ } else if (shouldQueue && stampedSeat) {
773
+ resolveNestedElanousOption(opts);
774
+ const flags: string[] = [];
775
+ const values: Array<[string, string | undefined]> = [
776
+ ['--base', opts.base], ['--target', opts.target], ['--correlation', opts.correlation],
777
+ ['--goal-type', opts.goalType], ...(dryRunPreview.entrance === 'cli-harness-say' ? [['--author-grade', opts.authorGradeSource === 'flag' ? opts.authorGrade : undefined] as [string, string | undefined]] : []), ['--child-llm-provider', opts.childLlmProvider],
778
+ ['--child-llm-model', opts.childLlmModel], ['--child-llm-effort', opts.childLlmEffort],
779
+ ['--pod-pool', (opts as HarnessSubstrateOpts).podPool], ['--pod-memory', (opts as HarnessSubstrateOpts).podMemory],
780
+ ['--after', (opts as HarnessSubstrateOpts).after], ['--source', (opts as HarnessSubstrateOpts).source],
781
+ ];
782
+ for (const [flag, value] of values) if (value !== undefined) flags.push(flag, value);
783
+ if ((opts as HarnessSubstrateOpts).source !== undefined && resolved.substrate !== 'pod') {
784
+ console.error('`--source` 는 `--substrate pod` 와 함께');
785
+ process.exitCode = 2;
786
+ return;
787
+ }
788
+ if (resolved.substrate === 'pod' && opts.target !== undefined) {
789
+ console.error('`--target` 은 Pod 경로에서 아직 지원하지 않는다 — 로컬로 돌리거나 `--target` 을 빼라');
790
+ console.error('Pod 로 특정 원천을 주려면 `--source`');
791
+ process.exitCode = 2;
792
+ return;
793
+ }
794
+ for (const [enabled, flag] of [
795
+ [opts.json, '--json'], [opts.yes, '--yes'], [opts.autoMerge === false, '--no-auto-merge'],
796
+ [opts.mergeByHost, '--merge-by-host'], [opts.observeOnly, '--observe-only'],
797
+ [opts.supervise === false, '--no-supervise'], [opts.forcePreflight, '--force-preflight'],
798
+ [opts.forceGate, '--force-gate'],
799
+ ] as const) if (enabled) flags.push(flag);
800
+ let row: QueueItem;
801
+ try {
802
+ row = await addHarnessQueue({ seat: stampedSeat, launchCwd: process.cwd(), refuseDuplicate: true,
803
+ ...(dryRunPreview.goalPath ? { ask: dryRunPreview.goalPath } : { say: dryRunPreview.input }),
804
+ launchArgs: ['harness', dryRunPreview.goalPath ? 'ask' : 'say', dryRunPreview.goalPath ?? dryRunPreview.input,
805
+ '--seat', stampedSeat, ...((opts as HarnessSubstrateOpts).substrate ? ['--substrate', (opts as HarnessSubstrateOpts).substrate!] : []), ...flags,
806
+ ...(opts.nestedElanous ? ['--nested-elanous', opts.nestedElanous] : [])],
807
+ }, queueDeps);
808
+ await queueDeps?.afterEnqueue?.(row);
809
+ } catch (error) {
810
+ if (!(error instanceof HarnessQueueDuplicateError)) throw error;
811
+ console.error(`발사 거절 — ${error.message.replace(/[\r\n]+/g, ' ')}`);
812
+ process.exitCode = 1;
813
+ return;
814
+ }
815
+ const result = await tickHarnessQueue({ ...queueDeps, ...(immediateLaunch ? { launch: immediateLaunch } : {}) }, row.id);
816
+ if (result.outcome === 'launched' && result.item?.id === row.id) {
817
+ if (immediateExit) process.exitCode = await immediateExit();
818
+ } else if (result.outcome === 'waiting') {
819
+ const reason = listHarnessQueue(queueDeps).find((item) => item.id === row.id)?.waitingReason ?? result.reason;
820
+ console.log(`대기열에 들어갔다 — ${reason.replace(/[\r\n]+/g, ' ')}`);
821
+ process.exitCode = 0;
822
+ } else if (result.outcome === 'skipped' && result.item?.id === row.id && result.item.status !== 'queued') {
823
+ // Another tick won the race for this row. Only a confirmed launch (pid recorded) is reported as launched;
824
+ // a row still `launching` may yet fail, so the direct call ends non-zero instead of claiming success.
825
+ const confirmed = (result.item.status === 'launched' || result.item.status === 'finished') && result.item.pid !== undefined;
826
+ const log = join(queueDeps?.root ?? effectiveInstanceRoot(), 'harness', `${row.id}.log`);
827
+ if (confirmed) console.log(`다른 대기열 tick 이 이미 발사했다 — ${row.id} (pid ${result.item.pid}) · 출력: ${log}`);
828
+ else console.error(`다른 대기열 tick 이 발사 중이다(확정 전) — ${row.id} (${result.item.status}) · harness queue list 로 확인`);
829
+ try { debug.log('harness.queue', 'raced-launch', { id: row.id, seat: row.seat, status: result.item.status, confirmed }); }
830
+ catch { /* Observation cannot change the launch outcome. */ }
831
+ process.exitCode = confirmed ? 0 : 1;
832
+ } else {
833
+ throw new Error(`harness queue: ${result.reason}`);
834
+ }
835
+ return;
836
+ }
735
837
  // Depth >= 1 cannot re-allow. Drop the flag here so the launch sees the refusal, not the raw argv.
736
838
  const nested = resolveNestedElanousOption(opts);
737
839
  const stampedOpts = {
@@ -2413,6 +2515,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2413
2515
  installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
2414
2516
  const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
2415
2517
  const queueDeps = deps.queue ?? {};
2518
+ let launchedExit: Promise<number> | undefined;
2519
+ const immediateLaunch: NonNullable<HarnessQueueDeps['launch']> = queueDeps.launch ?? (async (item: QueueItem, args: string[], root: string) => {
2520
+ const child = spawn(process.execPath, [join(import.meta.dir, 'harness-queue-child.ts'),
2521
+ harnessQueueReceiptPath(root, item.launchId!), queueDeps.launchCommand ?? join(import.meta.dir, '../../bin/elanous.mjs'),
2522
+ ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
2523
+ cwd: item.launchCwd ?? resolve(import.meta.dir, '../..'), stdio: 'inherit',
2524
+ env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
2525
+ ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
2526
+ });
2527
+ // Capture the end at spawn time: a child that already closed (or died by a signal) must not leave the parent waiting.
2528
+ launchedExit = new Promise<number>((done) => child.once('close', (code, signal) => {
2529
+ const signo = signal ? (osConstants.signals as Record<string, number>)[signal] : undefined;
2530
+ done(code ?? (signo ? 128 + signo : 1));
2531
+ }));
2532
+ return await new Promise<number>((done, reject) => {
2533
+ child.once('spawn', () => done(child.pid!));
2534
+ child.once('error', (error) => {
2535
+ try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
2536
+ catch { /* An unreadable receipt leaves the reservation indeterminate. */ }
2537
+ reject(error);
2538
+ });
2539
+ });
2540
+ });
2541
+ const immediateExit = async (): Promise<number> => launchedExit ? await launchedExit : 0;
2416
2542
  const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
2417
2543
  queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
2418
2544
  .option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
@@ -2458,7 +2584,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2458
2584
  }
2459
2585
  return resolved.substrate === 'pod' ? onPod(stampedOpts, 'cli-harness-ask', goalPath, resolved.pool!, deps.podDispatchTask) : (announceLocalChildProvider(stampedOpts), ask(goalPath, normalizeHarnessAskSayOptions(stampedOpts)));
2460
2586
  },
2461
- deps.launchGate,
2587
+ deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
2462
2588
  );
2463
2589
  });
2464
2590
  }
@@ -2466,9 +2592,15 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2466
2592
  const say = deps.say;
2467
2593
  if (say) {
2468
2594
  registerHarnessAskSayOptions(harnessCmd.command('say <sentence...>').description('문장을 받아 구동한다'))
2595
+ .addOption(new Option('--author-grade <grade>', 'say 저작 등급 (full|lite)').choices(['full', 'lite']))
2469
2596
  .action(async (sentence: string[], opts: HarnessAskSayChildLlmOptions & HarnessDryRunOpts) => {
2597
+ const grade = resolveGoalAuthorGrade(opts.authorGrade, getUserConfig().harness?.authorGrade);
2598
+ const podSource = process.env.ELANOUS_POD_AUTHOR_ON_POD === '1' && process.env.ELANOUS_POD_AUTHOR_GRADE === grade.grade
2599
+ ? process.env.ELANOUS_POD_AUTHOR_GRADE_SOURCE : undefined;
2600
+ const gradedOpts = grade.source === 'default' && podSource === undefined ? opts
2601
+ : { ...opts, authorGrade: grade.grade, authorGradeSource: podSource === 'config' || podSource === 'default' || podSource === 'flag' ? podSource : grade.source };
2470
2602
  await dispatchHarnessAskSay(
2471
- opts,
2603
+ gradedOpts,
2472
2604
  {
2473
2605
  input: sentence.join(' '),
2474
2606
  entrance: 'cli-harness-say',
@@ -2485,7 +2617,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2485
2617
  ? onPod(stampedOpts, 'cli-harness-say', sentence.join(' '), resolved.pool!, deps.podDispatchTask, harnessAuthorOnPod())
2486
2618
  : (announceLocalChildProvider(stampedOpts), say(sentence, normalizeHarnessAskSayOptions(stampedOpts)));
2487
2619
  },
2488
- deps.launchGate,
2620
+ deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
2489
2621
  );
2490
2622
  });
2491
2623
  }
@@ -49,6 +49,8 @@ export interface HarnessPodDispatchInput {
49
49
  readonly childLlmEffort?: string;
50
50
  /** `harness.authorOnPod` — say 문장을 Pod 안에서 저작부터 돌린다. 호스트는 영수증만 남긴다. */
51
51
  readonly authorOnPod?: boolean;
52
+ readonly authorGrade?: import('../self-implement/goal-author.js').GoalAuthorGrade;
53
+ readonly authorGradeSource?: import('../self-implement/goal-author.js').GoalAuthorGradeSource;
52
54
  }
53
55
 
54
56
  export function podOrchestrateArgs(input: HarnessPodDispatchInput, goalFile: string): string[] {
@@ -146,6 +148,15 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
146
148
  if (input.after) env.ELANOUS_POD_AFTER = input.after;
147
149
  if (input.authorOnPod) env.ELANOUS_POD_AUTHOR_ON_POD = '1';
148
150
  else delete env.ELANOUS_POD_AUTHOR_ON_POD;
151
+ if (input.authorOnPod && input.authorGrade) {
152
+ env.ELANOUS_POD_AUTHOR_GRADE = input.authorGrade;
153
+ // 출처는 호스트가 알 때만 싣는다 — 모르면 «flag» 로 지어내지 않고 Pod 가 자기가 본 출처를 쓰게 둔다.
154
+ if (input.authorGradeSource) env.ELANOUS_POD_AUTHOR_GRADE_SOURCE = input.authorGradeSource;
155
+ else delete env.ELANOUS_POD_AUTHOR_GRADE_SOURCE;
156
+ } else {
157
+ delete env.ELANOUS_POD_AUTHOR_GRADE;
158
+ delete env.ELANOUS_POD_AUTHOR_GRADE_SOURCE;
159
+ }
149
160
  if (input.entrance === 'cli-harness-ask') {
150
161
  const root = findGitDir(cwd)?.root;
151
162
  const path = resolve(cwd, input.input);
@@ -22,6 +22,7 @@ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
22
22
  export type QueueItem = {
23
23
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
24
24
  at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
25
+ launchArgs?: string[]; launchCwd?: string; cellId?: string;
25
26
  };
26
27
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
27
28
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
@@ -53,6 +54,10 @@ export interface HarnessQueueDeps {
53
54
  cap?: (seat: QueueSeat) => number;
54
55
  pool?: () => QueuePool;
55
56
  launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
57
+ /** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
58
+ afterEnqueue?: (item: QueueItem) => void | Promise<void>;
59
+ /** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
60
+ launchCommand?: string;
56
61
  alive?: (pid: number) => boolean;
57
62
  processes?: () => readonly QueueProcess[];
58
63
  receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
@@ -143,6 +148,9 @@ function read(path: string): QueueItem[] {
143
148
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
144
149
  && (row.launchId === undefined || typeof row.launchId === 'string')
145
150
  && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
151
+ && (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
152
+ && (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
153
+ && (row.cellId === undefined || typeof row.cellId === 'string')
146
154
  && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
147
155
  throw new Error('harness queue: invalid queue file (no launch)');
148
156
  }
@@ -207,7 +215,24 @@ function seat(value: string): QueueSeat {
207
215
  return value as QueueSeat;
208
216
  }
209
217
 
210
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
218
+ /** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
219
+ export function queueCellId(text: string): string | undefined {
220
+ return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
221
+ }
222
+
223
+ /** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
224
+ function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
225
+ if (kind === 'say') return queueCellId(input);
226
+ try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
227
+ }
228
+
229
+ export class HarnessQueueDuplicateError extends Error {
230
+ constructor(existing: QueueItem, key: string) {
231
+ super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
232
+ }
233
+ }
234
+
235
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
211
236
  const assigned = seat(input.seat);
212
237
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
213
238
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -228,8 +253,24 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
228
253
  return existing;
229
254
  }
230
255
  }
256
+ // Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
257
+ const cellId = queueRowCellId(kind, value);
258
+ if (input.refuseDuplicate) {
259
+ const cell = cellId;
260
+ const existing = rows.find((row) => row.status !== 'finished'
261
+ && !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
262
+ && ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
263
+ if (existing) {
264
+ const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
265
+ observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
266
+ throw new HarnessQueueDuplicateError(existing, key);
267
+ }
268
+ }
231
269
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
232
270
  hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
271
+ ...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
272
+ ...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
273
+ ...(cellId === undefined ? {} : { cellId }),
233
274
  ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
234
275
  save(path, [...rows, item]);
235
276
  observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
@@ -318,6 +359,7 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
318
359
  }
319
360
 
320
361
  export function queueLaunchArgs(item: QueueItem): string[] {
362
+ if (item.launchArgs) return item.launchArgs;
321
363
  return ['harness', item.kind, item.input, '--substrate', 'pod',
322
364
  ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
323
365
  }
@@ -468,15 +510,19 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
468
510
  }
469
511
  }
470
512
 
471
- async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
513
+ function defaultLaunch(command?: string) {
514
+ return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
515
+ }
516
+
517
+ async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
472
518
  const repo = resolve(import.meta.dir, '../..');
473
519
  const logPath = join(root, 'harness', `${item.id}.log`);
474
520
  const fd = openSync(logPath, 'a', 0o600);
475
521
  try {
476
522
  const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
477
- harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
523
+ harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
478
524
  ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
479
- cwd: repo, detached: true, stdio: ['ignore', fd, fd],
525
+ cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
480
526
  env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
481
527
  ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
482
528
  });
@@ -675,7 +721,7 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
675
721
  }
676
722
  }
677
723
 
678
- export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
724
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
679
725
  const root = deps.root ?? effectiveInstanceRoot();
680
726
  return locked(root, async (path) => {
681
727
  try {
@@ -699,10 +745,20 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
699
745
  }
700
746
  const heads = new Map<QueueSeat, QueueItem>();
701
747
  for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
748
+ if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
749
+ const row = items.find((item) => item.id === requestedId);
750
+ if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
751
+ const reason = '앞선 대기열 항목 차례';
752
+ if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
753
+ observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
754
+ return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
755
+ }
702
756
  if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
703
757
  const lastSeat = lastLaunchedSeat(path);
704
758
  const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
705
759
  : queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
760
+ // A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
761
+ // head with room; an earlier seat's head with room means the requested row waits for its turn.
706
762
  const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
707
763
  .flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
708
764
  const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
@@ -729,7 +785,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
729
785
  ...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
730
786
  attributed, unattributed }, deps);
731
787
  }
732
- return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
788
+ const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
789
+ const subjectReason = blocked.get(subject.seat)!.reason;
790
+ return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
733
791
  };
734
792
  for (const item of candidates) {
735
793
  const injectedCap = deps.cap?.(item.seat);
@@ -799,11 +857,32 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
799
857
  + current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
800
858
  }
801
859
  if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
860
+ if (requestedId && item.id !== requestedId) {
861
+ const requested = current.find((row) => row.id === requestedId)!;
862
+ // The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
863
+ // evaluated yet; «another seat's turn» only when the requested seat itself has room.
864
+ const own = blocked.get(requested.seat)?.reason ?? (() => {
865
+ const ownCap = deps.cap?.(requested.seat);
866
+ const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
867
+ const charged = runs.filter((run) => !run.seat || run.seat === requested.seat);
868
+ const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
869
+ const active = charged.length + current.filter((row) => row.seat === requested.seat &&
870
+ (row.status === 'launching' || row.status === 'launched')
871
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
872
+ if (active >= details.cap) return seatCapReason(requested.seat, active, details);
873
+ const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
874
+ return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
875
+ })();
876
+ const reason = own ?? `다른 자리 차례 — ${item.seat}`;
877
+ if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
878
+ observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
879
+ return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
880
+ }
802
881
  const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
803
882
  delete launching.waitingReason;
804
883
  save(path, current.map((row) => row.id === item.id ? launching : row));
805
884
  try {
806
- const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
885
+ const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
807
886
  if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
808
887
  const launched: QueueItem = { ...launching, pid, status: 'launched' };
809
888
  save(path, current.map((row) => row.id === item.id ? launched : row));
@@ -13,6 +13,7 @@
13
13
  // an allow flag is ignored and the launch stays refused.
14
14
 
15
15
  import { debug } from '../debug/log.js';
16
+ import { getUserConfig } from '../user-config.js';
16
17
 
17
18
  export const ELANOUS_NESTED_DEPTH_ENV = 'ELANOUS_NESTED_DEPTH';
18
19
 
@@ -26,7 +27,7 @@ export interface NestedElanousDecision {
26
27
  readonly depth: number;
27
28
  /** True when depth >= 1 supplied allow and it was ignored. */
28
29
  readonly allowIgnored: boolean;
29
- readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored';
30
+ readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored' | 'depth-cap';
30
31
  }
31
32
 
32
33
  /** Current process depth from the nested-depth marker. Unset or unreadable = 0 (outer launcher). */
@@ -56,11 +57,25 @@ export function nestedDepthCodexArgs(backendName: string, childDepth: number): s
56
57
  * Allow decision. Depth 0 allows only when `allow` is true.
57
58
  * Depth >= 1 always refuses; a supplied allow is ignored and logged.
58
59
  */
60
+ /** Nested depth cap: explicit value, else `harness.nestedElanousMaxDepth`, else 2. */
61
+ export function resolveNestedDepthCap(maxDepth?: number): number {
62
+ const configured = maxDepth ?? getUserConfig().harness?.nestedElanousMaxDepth;
63
+ return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 2;
64
+ }
65
+
59
66
  export function decideNestedElanousLaunch(input: {
60
67
  readonly depth: number;
61
68
  readonly allow?: boolean;
69
+ readonly maxDepth?: number;
70
+ /** False for an ordinary mission spawn that did not request a nested launch — no cap log. */
71
+ readonly logCap?: boolean;
62
72
  }): NestedElanousDecision {
63
73
  const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
74
+ const cap = resolveNestedDepthCap(input.maxDepth);
75
+ if (depth >= cap) {
76
+ if (input.logCap !== false) debug.log('agent-mission.nested', 'depth-cap', { depth, cap });
77
+ return { allowed: false, depth, allowIgnored: input.allow === true, reason: 'depth-cap' };
78
+ }
64
79
  if (depth >= 1) {
65
80
  if (input.allow === true) {
66
81
  debug.log('agent-mission.nested', 'allow-ignored', { depth });
@@ -101,14 +116,18 @@ export function gateNestedElanousLaunch(input: {
101
116
  readonly nestedLaunchRequested: boolean;
102
117
  readonly allow?: boolean;
103
118
  readonly configAllow?: boolean;
119
+ readonly maxDepth?: number;
104
120
  }): NestedElanousDecision {
105
121
  const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
122
+ const allow = input.nestedLaunchRequested && (input.allow === true || (depth === 0 && input.configAllow === true));
106
123
  if (!input.nestedLaunchRequested) {
107
- return decideNestedElanousLaunch({ depth, allow: false });
124
+ return decideNestedElanousLaunch({ depth, allow: false, logCap: false, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
108
125
  }
109
126
  if (!backendCarriesNestedDepth(input.backendName)) {
127
+ // The depth-unknown refusal stands; reaching the cap is still recorded.
128
+ const cap = resolveNestedDepthCap(input.maxDepth);
129
+ if (depth >= cap) debug.log('agent-mission.nested', 'depth-cap', { depth, cap, backend: input.backendName });
110
130
  return { allowed: false, depth, allowIgnored: false, reason: 'depth-unknown' };
111
131
  }
112
- const allow = input.allow === true || (depth === 0 && input.configAllow === true);
113
- return decideNestedElanousLaunch({ depth, allow });
132
+ return decideNestedElanousLaunch({ depth, allow, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
114
133
  }