elanous 0.2.4 → 0.2.6

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 (455) hide show
  1. package/README.md +7 -6
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -1,7 +1,9 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { callNcsTool } from '../connectors/ncs/server.js';
4
5
  import { courseLinks } from './course-links.js';
6
+ import { gapPrompt, judgeGaps, MAX_GAP_PROMPT_CHARS, type GapUnit } from './gap-judge.js';
5
7
 
6
8
  type Data = Record<string, unknown>;
7
9
  const value = (object: unknown): Data => object && typeof object === 'object' && !Array.isArray(object) ? object as Data : {};
@@ -9,6 +11,30 @@ const text = (item: unknown) => typeof item === 'string' ? item.trim() : '';
9
11
  const list = (item: unknown): string[] => Array.isArray(item) ? item.filter((v): v is string => typeof v === 'string' && !!v.trim()) : [];
10
12
  const line = (item: unknown) => text(item).replace(/[\r\n|]/g, ' ');
11
13
  const json = (data: Data) => console.log(JSON.stringify(data));
14
+ const items = (raw: unknown): Data[] => {
15
+ const item = value(raw).items ? value(value(raw).items).item : value(raw).item ?? raw;
16
+ return (Array.isArray(item) ? item : item ? [item] : []).map(value);
17
+ };
18
+ const pick = (record: Data, ...names: string[]) => names.map(name => line(record[name])).find(Boolean) ?? '';
19
+ const MAX_DETAIL_UNITS = 6;
20
+ const DETAIL_CONCURRENCY = 2;
21
+ const gapUnit = (unit: Data): GapUnit => ({
22
+ code: line(unit.code), name: line(unit.name), definition: line(unit.definition), level: line(unit.level),
23
+ elements: Array.isArray(unit.elements) ? unit.elements.map(value).map(element => ({ name: line(element.name), criteria: line(element.criteria) })) : [],
24
+ });
25
+ const detailFields = (raw: unknown) => {
26
+ const body = value(value(raw).body);
27
+ const records = items(body);
28
+ const detail = records[0] ?? body;
29
+ const elements = items(detail.COMPE_UNIT_ELEM ?? detail.competencyUnitElements ?? detail.elements ?? body.COMPE_UNIT_ELEM);
30
+ return {
31
+ level: pick(detail, 'COMPE_UNIT_LVL', 'COMPE_UNIT_LEVEL', 'NCS_LVL', 'level'),
32
+ elements: elements.map(element => ({
33
+ name: pick(element, 'COMPE_UNIT_ELEM_NAME', 'NCS_COMPE_UNIT_ELEM_NAME', 'name'),
34
+ criteria: pick(element, 'PERF_CRIT', 'PERF_CRITERIA', 'PERFORMANCE_CRITERIA', 'criteria'),
35
+ })).filter(element => element.name || element.criteria),
36
+ };
37
+ };
12
38
  const context = value(JSON.parse(readFileSync(process.env.ELANOUS_GRAPH_CONTEXT ?? '', 'utf8')));
13
39
  const input = value(context.input);
14
40
  const outputs = value(context.outputs);
@@ -17,6 +43,11 @@ if (!contextsDir.endsWith('.json.contexts')) throw new Error('invalid graph run
17
43
  const runStateFile = contextsDir.slice(0, -'.contexts'.length);
18
44
  const reportDir = runStateFile.slice(0, -'.json'.length);
19
45
  const reportFile = join(reportDir, 'report.md');
46
+ const enterpriseHeadings = ['기업 개요 및 교육 대상', '교육 니즈', '직무·과제 및 NCS', 'AI 적합성', '역량 진단', '교육 로드맵', '교육 명세·운영 경로'];
47
+ const interviewField = (interview: string, heading: string) => interview.split(`## ${heading}\n`)[1]?.split(/\n## /)[0]?.trim() ?? '';
48
+ const brief = (raw: string) => line(raw.replace(/^[-*]\s*/gm, '')).slice(0, 240) || '미확인';
49
+ const enterpriseOutput = (name: string) => value(outputs[name]);
50
+ const enterpriseLine = (label: string, raw: unknown) => `- ${label}: ${line(raw) || '미확인'}`;
20
51
 
21
52
  async function run(step: string) {
22
53
  if (step === 'profile') {
@@ -31,21 +62,170 @@ async function run(step: string) {
31
62
  const skills = field('보유 역량').split(/[,\n]/).map(s => s.replace(/^[-*] /, '').trim()).filter(Boolean);
32
63
  if (!jobs.length || !experiences.length || !skills.length) throw new Error('interview requires 희망 직무, 경험, 보유 역량 headings');
33
64
  json({ jobs, skills, experienceCount: experiences.length });
65
+ } else if (step === 'intake') {
66
+ const path = text(input.interview);
67
+ if (!path || !path.endsWith('.md')) throw new Error('input.interview must name a Markdown interview file');
68
+ const interview = readFileSync(path, 'utf8').replace(/\r\n?/g, '\n');
69
+ const headings = ['기업 개요', '교육 대상', '현업 과제', '교육 목표', '현재 역량', '교육 제약'];
70
+ if (headings.some(heading => !interview.includes(`## ${heading}\n`))) throw new Error(`enterprise interview requires ${headings.join(', ')} headings`);
71
+ const task = interviewField(interview, '현업 과제').split(/\n|[,;]/).map(s => s.replace(/^[-*]\s*/, '').trim()).find(Boolean) ?? '';
72
+ if (!task || task === '미확인') throw new Error('missing enterprise task for NCS search');
73
+ for (const heading of ['교육 대상', '교육 목표']) {
74
+ const field = brief(interviewField(interview, heading));
75
+ if (field === '미확인') throw new Error(`missing enterprise ${heading}`);
76
+ }
77
+ const jobs = [line(task).slice(0, 80)];
78
+ json({ jobs, sections: Object.fromEntries(headings.map(heading => [heading, brief(interviewField(interview, heading))])) });
79
+ } else if (step === 'needs') {
80
+ const sections = value(enterpriseOutput('intake').sections);
81
+ if (!text(sections['현업 과제'])) throw new Error('missing enterprise intake');
82
+ json({ learnerGroup: sections['교육 대상'], task: sections['현업 과제'], requestedOutcome: sections['교육 목표'], evidence: '인터뷰 자기보고', baseline: '미확인' });
83
+ } else if (step === 'task') {
84
+ const needs = enterpriseOutput('needs');
85
+ if (!text(needs.task)) throw new Error('missing enterprise needs');
86
+ json({ work: needs.task, performanceCriterion: text(needs.requestedOutcome) || '미확인', source: '현업 과제·교육 목표 (인터뷰 자기보고)' });
34
87
  } else if (step === 'ncs-match') {
35
- const profile = value(outputs.profile);
88
+ const profile = value(outputs.profile ?? outputs.intake);
36
89
  const jobs = list(profile.jobs);
37
90
  if (!jobs.length) throw new Error('missing profile jobs');
38
91
  const results = await Promise.all(jobs.slice(0, 3).map(async (job) => ({ job, result: await callNcsTool('ncs_search_units', { keyword: job }) })));
39
- const units = results.flatMap(({ job, result }) => {
40
- const body = value(value(result).body);
41
- const raw = value(body.items).item;
42
- return (Array.isArray(raw) ? raw : raw ? [raw] : []).map(item => {
43
- const unit = value(item);
44
- return { job, code: line(unit.NCS_CL_CD ?? unit.NCS_COMPE_UNIT_CD), name: line(unit.COMPE_UNIT_NAME), definition: line(unit.COMPE_UNIT_DEF), source: text(value(result).source) };
45
- }).filter(unit => unit.name && unit.code);
46
- });
47
- if (!units.length) throw new Error('NCS search returned no named competency units; cannot invent a match');
92
+ const named = results.flatMap(({ job, result }) => items(value(result).body).map(unit => ({
93
+ job, code: pick(unit, 'NCS_COMPE_UNIT_CD'), name: pick(unit, 'COMPE_UNIT_NAME'),
94
+ definition: pick(unit, 'COMPE_UNIT_DEF'), source: text(value(result).source),
95
+ })).filter(unit => unit.name));
96
+ // A hit without NCS_COMPE_UNIT_CD cannot be looked up (its classification code is not a unit code) — skip it, keep the rest.
97
+ const matches = named.filter(unit => unit.code);
98
+ if (named.length && !matches.length) throw new Error('NCS search returned named competency units only without NCS_COMPE_UNIT_CD; cannot invent a match');
99
+ if (!matches.length) throw new Error('NCS search returned no named competency units; cannot invent a match');
100
+ // Interleave jobs in search order; deduplicate codes before the bounded detail lookup.
101
+ const perJob = jobs.slice(0, 3).map(job => matches.filter(unit => unit.job === job));
102
+ const candidates: typeof matches = [];
103
+ const seen = new Set<string>();
104
+ for (let index = 0; candidates.length < MAX_DETAIL_UNITS && perJob.some(group => index < group.length); index++) {
105
+ for (const group of perJob) {
106
+ const unit = group[index];
107
+ if (unit && !seen.has(unit.code)) {
108
+ candidates.push(unit);
109
+ seen.add(unit.code);
110
+ if (candidates.length === MAX_DETAIL_UNITS) break;
111
+ }
112
+ }
113
+ }
114
+ const details: (typeof matches[number] & ReturnType<typeof detailFields> & { source: string })[] = [];
115
+ for (let index = 0; index < candidates.length; index += DETAIL_CONCURRENCY) {
116
+ details.push(...await Promise.all(candidates.slice(index, index + DETAIL_CONCURRENCY).map(async unit => {
117
+ const detail = await callNcsTool('ncs_unit', { code: unit.code });
118
+ const fields = detailFields(detail);
119
+ if (!fields.level || !fields.elements.length || fields.elements.some(element => !element.name || !element.criteria)) {
120
+ throw new Error(`NCS unit ${unit.code} has no complete elements, performance criteria and level`);
121
+ }
122
+ return { ...unit, ...fields, source: text(value(detail).source) };
123
+ })));
124
+ }
125
+ const interview = readFileSync(text(input.interview), 'utf8');
126
+ const units: typeof details = [];
127
+ for (const detail of details) {
128
+ if (gapPrompt([...units, detail].map(unit => gapUnit(unit)), interview).length <= MAX_GAP_PROMPT_CHARS) units.push(detail);
129
+ }
130
+ if (!units.length) throw new Error('interview or NCS detail exceeds gap prompt input limit');
48
131
  json({ units });
132
+ } else if (step === 'ai-fit') {
133
+ const task = enterpriseOutput('task');
134
+ const ncs = enterpriseOutput('ncs-match');
135
+ const units = Array.isArray(ncs.units) ? ncs.units.map(value) : [];
136
+ if (!text(task.work) || !units.length) throw new Error('missing task or NCS evidence for AI fit');
137
+ json({ task: task.work, ncsCodes: units.map(unit => text(unit.code)),
138
+ opportunity: '해당 과제의 AI 보조 가능성 검토 (효과 미확인)',
139
+ alternative: '업무 절차 정리 및 담당자 검토',
140
+ feasibility: '데이터 접근·보안·사내 정책 승인 미확인', decision: '조건부 검토' });
141
+ } else if (step === 'competency') {
142
+ const sections = value(enterpriseOutput('intake').sections);
143
+ const units = enterpriseOutput('ncs-match').units;
144
+ if (!Array.isArray(units) || !units.length) throw new Error('missing NCS units for competency assessment');
145
+ json({ current: sections['현재 역량'] || '미확인', status: '인터뷰 자기보고만 확인; 객관적 수준 미확인',
146
+ requirements: units.map(value).map(unit => ({ code: text(unit.code), name: text(unit.name), level: text(unit.level) || '미확인' })) });
147
+ } else if (step === 'roadmap') {
148
+ const competency = enterpriseOutput('competency');
149
+ const aiFit = enterpriseOutput('ai-fit');
150
+ if (!Array.isArray(competency.requirements) || !text(aiFit.decision)) throw new Error('missing competency or AI fit');
151
+ json({ phases: ['현재 수준 확인', '업무 기반 실습', '담당자 검토를 동반한 시범 적용', '업무 산출물 평가'],
152
+ assessment: '실제 업무 산출물의 수행 기준에 따른 관찰 평가', adoption: '제안 단계; 승인 미확인' });
153
+ } else if (step === 'spec') {
154
+ const roadmap = enterpriseOutput('roadmap');
155
+ const sections = value(enterpriseOutput('intake').sections);
156
+ if (!Array.isArray(roadmap.phases)) throw new Error('missing enterprise roadmap');
157
+ json({ audience: sections['교육 대상'] || '미확인', outcome: sections['교육 목표'] || '미확인',
158
+ activities: roadmap.phases, assessment: roadmap.assessment, constraints: sections['교육 제약'] || '미확인',
159
+ duration: '미확인', courses: '미확인' });
160
+ } else if (step === 'routing') {
161
+ const spec = enterpriseOutput('spec');
162
+ if (!Array.isArray(spec.activities)) throw new Error('missing enterprise training spec');
163
+ const sections = value(enterpriseOutput('intake').sections);
164
+ const ncs = enterpriseOutput('ncs-match');
165
+ const units = Array.isArray(ncs.units) ? ncs.units.map(value) : [];
166
+ if (!units.length) throw new Error('cannot report without verified NCS units');
167
+ const requirements = enterpriseOutput('competency').requirements;
168
+ if (!Array.isArray(requirements) || !requirements.length) throw new Error('missing competency requirements');
169
+ const md = ['# 기업 HRD 교육 제안 보고서', '',
170
+ `## ${enterpriseHeadings[0]}`, enterpriseLine('기업 개요 (인터뷰 자기보고)', sections['기업 개요']), enterpriseLine('교육 대상 (인터뷰 자기보고)', sections['교육 대상']), '',
171
+ `## ${enterpriseHeadings[1]}`, enterpriseLine('교육 대상', enterpriseOutput('needs').learnerGroup), enterpriseLine('현업 니즈 (인터뷰 자기보고)', enterpriseOutput('needs').task), enterpriseLine('교육 목표', enterpriseOutput('needs').requestedOutcome), enterpriseLine('근거', enterpriseOutput('needs').evidence), enterpriseLine('현재 수준', enterpriseOutput('needs').baseline), '',
172
+ `## ${enterpriseHeadings[2]}`, enterpriseLine('직무 과제', enterpriseOutput('task').work), enterpriseLine('관찰할 수행 기준', enterpriseOutput('task').performanceCriterion), enterpriseLine('과제 출처', enterpriseOutput('task').source),
173
+ ...units.map(unit => `- NCS ${line(unit.code)} — ${line(unit.name)} · 수준 ${line(unit.level) || '미확인'} · 출처 ${line(unit.source) || '미확인'}`), '',
174
+ `## ${enterpriseHeadings[3]}`, enterpriseLine('적용 검토 과제', enterpriseOutput('ai-fit').task), enterpriseLine('관련 NCS 코드', list(enterpriseOutput('ai-fit').ncsCodes).join(', ')), enterpriseLine('적용 가능성', enterpriseOutput('ai-fit').opportunity), enterpriseLine('비AI 대안', enterpriseOutput('ai-fit').alternative), enterpriseLine('도입 판정', enterpriseOutput('ai-fit').decision), enterpriseLine('정책 및 데이터 접근', enterpriseOutput('ai-fit').feasibility), '',
175
+ `## ${enterpriseHeadings[4]}`, enterpriseLine('현재 역량 (자기보고)', enterpriseOutput('competency').current), enterpriseLine('검증 상태', enterpriseOutput('competency').status),
176
+ ...requirements.map(raw => value(raw)).map(unit => `- 요구 역량: NCS ${line(unit.code)} — ${line(unit.name)} · 수준 ${line(unit.level) || '미확인'}`), '',
177
+ `## ${enterpriseHeadings[5]}`, ...list(enterpriseOutput('roadmap').phases).map((phase, i) => `- ${i + 1}. ${line(phase)}`), enterpriseLine('평가', enterpriseOutput('roadmap').assessment), enterpriseLine('채택 상태', enterpriseOutput('roadmap').adoption), '',
178
+ `## ${enterpriseHeadings[6]}`, enterpriseLine('교육 대상', spec.audience), enterpriseLine('교육 목표', spec.outcome), enterpriseLine('평가 명세', spec.assessment), enterpriseLine('시간·예산·접근 제약', spec.constraints),
179
+ enterpriseLine('기간', spec.duration), enterpriseLine('추천 과정·URL', spec.courses), '- 운영 경로: 담당자와 비AI/AI 시범 방식을 검토; 데이터 접근·정책 승인은 미확인', '',
180
+ '※ 인터뷰는 자기보고이며 NCS는 확인된 조회 결과만 인용합니다. 제안은 회사의 승인된 정책이나 확정 교육 과정이 아닙니다.', ''].join('\n');
181
+ mkdirSync(reportDir, { recursive: true });
182
+ writeFileSync(reportFile, md, { mode: 0o600 });
183
+ json({ report: reportFile });
184
+ } else if (step === 'judge-enterprise') {
185
+ const report = text(enterpriseOutput('routing').report);
186
+ if (report !== reportFile) throw new Error('missing enterprise report path');
187
+ const content = readFileSync(reportFile, 'utf8');
188
+ const needs = enterpriseOutput('needs');
189
+ const task = enterpriseOutput('task');
190
+ const aiFit = enterpriseOutput('ai-fit');
191
+ const competency = enterpriseOutput('competency');
192
+ const roadmap = enterpriseOutput('roadmap');
193
+ const spec = enterpriseOutput('spec');
194
+ const sections = value(enterpriseOutput('intake').sections);
195
+ const units = Array.isArray(enterpriseOutput('ncs-match').units) ? (enterpriseOutput('ncs-match').units as unknown[]).map(value) : [];
196
+ const phases = list(roadmap.phases);
197
+ const requirements = Array.isArray(competency.requirements) ? competency.requirements.map(value) : [];
198
+ const substantive = (raw: unknown) => !!text(raw) && text(raw) !== '미확인';
199
+ const required: string[][] = [
200
+ [enterpriseLine('기업 개요 (인터뷰 자기보고)', sections['기업 개요']), enterpriseLine('교육 대상 (인터뷰 자기보고)', sections['교육 대상'])],
201
+ [enterpriseLine('교육 대상', needs.learnerGroup), enterpriseLine('현업 니즈 (인터뷰 자기보고)', needs.task), enterpriseLine('교육 목표', needs.requestedOutcome), enterpriseLine('근거', needs.evidence), enterpriseLine('현재 수준', needs.baseline)],
202
+ [enterpriseLine('직무 과제', task.work), enterpriseLine('관찰할 수행 기준', task.performanceCriterion), enterpriseLine('과제 출처', task.source),
203
+ ...units.map(unit => `- NCS ${line(unit.code)} — ${line(unit.name)} · 수준 ${line(unit.level) || '미확인'} · 출처 ${line(unit.source) || '미확인'}`)],
204
+ [enterpriseLine('적용 검토 과제', aiFit.task), enterpriseLine('관련 NCS 코드', list(aiFit.ncsCodes).join(', ')), enterpriseLine('적용 가능성', aiFit.opportunity), enterpriseLine('비AI 대안', aiFit.alternative), enterpriseLine('도입 판정', aiFit.decision), enterpriseLine('정책 및 데이터 접근', aiFit.feasibility)],
205
+ [enterpriseLine('현재 역량 (자기보고)', competency.current), enterpriseLine('검증 상태', competency.status),
206
+ ...requirements.map(unit => `- 요구 역량: NCS ${line(unit.code)} — ${line(unit.name)} · 수준 ${line(unit.level) || '미확인'}`)],
207
+ [...phases.map((phase, i) => `- ${i + 1}. ${line(phase)}`), enterpriseLine('평가', roadmap.assessment), enterpriseLine('채택 상태', roadmap.adoption)],
208
+ [enterpriseLine('교육 대상', spec.audience), enterpriseLine('교육 목표', spec.outcome), enterpriseLine('평가 명세', spec.assessment), enterpriseLine('시간·예산·접근 제약', spec.constraints), enterpriseLine('기간', spec.duration), enterpriseLine('추천 과정·URL', spec.courses), '- 운영 경로: 담당자와 비AI/AI 시범 방식을 검토; 데이터 접근·정책 승인은 미확인'],
209
+ ];
210
+ const complete = enterpriseHeadings.every((heading, index) => {
211
+ const marker = `## ${heading}\n`;
212
+ const start = content.indexOf(marker);
213
+ const end = index < enterpriseHeadings.length - 1 ? content.indexOf(`## ${enterpriseHeadings[index + 1]}\n`, start + marker.length) : content.length;
214
+ if (start < 0 || end <= start || content.indexOf(marker, start + marker.length) !== -1) return false;
215
+ const lines = new Set(content.slice(start + marker.length, end).split('\n'));
216
+ return required[index]!.every(expected => lines.has(expected));
217
+ });
218
+ const hasNcs = units.length > 0 && units.every(unit => substantive(unit.code) && substantive(unit.name)) &&
219
+ list(aiFit.ncsCodes).join(', ') === units.map(unit => text(unit.code)).join(', ');
220
+ const hasRequiredInput = [sections['교육 대상'], sections['교육 목표'], needs.learnerGroup, needs.requestedOutcome, spec.audience, spec.outcome].every(substantive);
221
+ const hasRequirements = requirements.length === units.length && requirements.every((unit, index) =>
222
+ substantive(unit.code) && substantive(unit.name) && substantive(unit.level) &&
223
+ ['code', 'name', 'level'].every(field => text(unit[field]) === text(units[index]?.[field])));
224
+ const hasDecision = [aiFit.task, aiFit.opportunity, aiFit.alternative, aiFit.decision, roadmap.assessment].every(substantive) &&
225
+ text(aiFit.task) === text(task.work) && phases.length >= 4 && phases.every(substantive);
226
+ const path = (JSON.parse(readFileSync(runStateFile, 'utf8')) as { path?: string[] }).path ?? [];
227
+ const visits = path.filter(node => node === 'judge' || node === 'judge-enterprise').length;
228
+ json({ outcome: complete && hasRequiredInput && hasNcs && hasRequirements && hasDecision ? 'ok' : visits < 2 ? 'retry' : 'fail', report });
49
229
  } else if (step === 'research') {
50
230
  const profile = value(outputs.profile);
51
231
  const query = `${list(profile.jobs).join(' ')} 역량 교육 과정 강의 공식 출처`;
@@ -61,13 +241,27 @@ async function run(step: string) {
61
241
  const courses = await courseLinks(result, units);
62
242
  json({ courses });
63
243
  } else if (step === 'gap') {
64
- const profile = value(outputs.profile);
65
244
  const ncs = value(outputs['ncs-match']);
66
- const skills = list(profile.skills);
67
- const units = Array.isArray(ncs.units) ? ncs.units.map(value) : [];
245
+ const units = Array.isArray(ncs.units) ? ncs.units.map(value).map(gapUnit) : [];
68
246
  if (!units.length) throw new Error('missing NCS units');
69
- const gaps = units.map(unit => ({ code: line(unit.code), name: line(unit.name), status: skills.some(skill => line(skill) === line(unit.name) && !!line(unit.name)) ? '보유(인터뷰 자기보고)' : '갭(증거 미확인)' }));
70
- json({ gaps });
247
+ const interview = readFileSync(text(input.interview), 'utf8');
248
+ // A research retry revisits gap with the same units and interview — reuse only a verdict made from exactly those.
249
+ const basis = createHash('sha256').update(gapPrompt(units, interview)).digest('hex');
250
+ const previous = value(outputs.gap);
251
+ if (previous.basis === basis && Array.isArray(previous.gaps)) {
252
+ json({ gaps: previous.gaps, basis });
253
+ return;
254
+ }
255
+ const gaps = await judgeGaps(units, interview, async prompt => {
256
+ const cli = Bun.spawn(['elanous', '--test', 'ask', '--json', prompt], { env: process.env, stdout: 'pipe', stderr: 'pipe' });
257
+ const stdout = await new Response(cli.stdout).text();
258
+ await new Response(cli.stderr).text();
259
+ if (await cli.exited !== 0) throw new Error('gap LLM failed');
260
+ const response = value(JSON.parse(stdout));
261
+ if (typeof response.reply !== 'string') throw new Error('gap LLM returned no reply');
262
+ return response.reply;
263
+ });
264
+ json({ gaps, basis });
71
265
  } else if (step === 'report') {
72
266
  const profile = value(outputs.profile);
73
267
  const ncs = value(outputs['ncs-match']);
@@ -78,8 +272,8 @@ async function run(step: string) {
78
272
  const gaps = Array.isArray(gap.gaps) ? gap.gaps.map(value) : [];
79
273
  if (!units.length || !gaps.length) throw new Error('cannot report without NCS units and gaps');
80
274
  const md = ['# AI 직무코치 보고서', '', '## 직무', ...list(profile.jobs).map(x => `- ${line(x)}`), '',
81
- '## NCS 능력단위 매칭', ...units.map(x => `- ${line(x.code)} — ${line(x.name)} (${line(x.job)})`), '',
82
- '## 역량 갭', ...gaps.map(x => `- ${line(x.code)} ${line(x.name)}: ${line(x.status)}`), '',
275
+ '## NCS 능력단위 매칭', ...units.map(x => `- ${line(x.code)} — ${line(x.name)} (${line(x.job)}) · 수준 ${line(x.level) || '미확인'}${Array.isArray(x.elements) ? x.elements.map(value).map(element => ` · ${line(element.name)}: ${line(element.criteria)}`).join('') : ''}`), '',
276
+ '## 역량 갭', ...gaps.map(x => `- ${line(x.code)} ${line(x.name)}: ${line(x.status)}${text(x.quote) ? ` — 인터뷰 인용: “${line(x.quote)}”` : ''}`), '',
83
277
  '## 추천 코스', ...(courses.length ? courses.map(x => `- [${line(x.title).replace(/[\[\]]/g, '')}](${text(x.url)}) — ${line(x.matchedUnit)}; ${line(x.evidence)}`) : ['- 확인된 관련 교육 과정 없음 (추천 미확인)']), '',
84
278
  '## 출처', '- [한국산업인력공단 NCS 기준정보 조회](https://www.data.go.kr/data/15128213/openapi.do)',
85
279
  ...courses.map(x => `- ${text(x.url)}`), '', '※ 보유 역량은 인터뷰 자기보고이며, 확인되지 않은 역량은 갭으로 표시합니다.', ''].join('\n');
@@ -3,11 +3,24 @@
3
3
  "name": "job-coach",
4
4
  "version": "0.1.0",
5
5
  "description": "Interview-to-NCS competency gap and cited course report.",
6
+ "skills": "./skills/",
7
+ "contributes": {
8
+ "skills": [
9
+ { "name": "interview-to-profile" },
10
+ { "name": "career-report" },
11
+ { "name": "run-report" },
12
+ { "name": "enterprise-needs" },
13
+ { "name": "enterprise-task-analysis" },
14
+ { "name": "enterprise-ai-fit" },
15
+ { "name": "enterprise-competency" },
16
+ { "name": "enterprise-roadmap" }
17
+ ]
18
+ },
6
19
  "extensions": {
7
20
  "ai.elanous": {
8
- "graphs": ["./graphs/report.yaml"],
9
- "capabilities": ["network:fetch"],
10
- "connectors": [{ "id": "ncs", "fields": [{ "name": "serviceKey", "secret": true }] }]
21
+ "graphs": ["./graphs/report.yaml", "./graphs/report-enterprise.yaml"],
22
+ "capabilities": ["fs:workdir", "net:apis.data.go.kr", "proc:bun", "proc:elanous", "secret:ncs"],
23
+ "connectors": [{ "id": "ncs", "fields": [{ "name": "serviceKey", "secret": true, "env": "NCS_SERVICE_KEY" }] }]
11
24
  }
12
25
  }
13
26
  }
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: enterprise-ai-fit
3
+ description: Assess AI applicability to verified enterprise tasks, including non-AI alternatives and policy unknowns.
4
+ ---
5
+
6
+ Use at the enterprise `ai-fit` stage after `task` and the existing `ncs-match` stage. For each concrete task, compare a plausible AI-assisted workflow with a non-AI alternative. Record the input data type, expected benefit, failure mode, human review point, testable outcome and evidence from the task or confirmed NCS unit. Separate suitability from a promise of productivity gain. If data rights, access, privacy, security or organization approval are not documented, mark policy and deployment feasibility `미확인`; do not assert permission to upload internal information to an external model. When AI adds no justified value or a task is insufficiently described, recommend the non-AI or evidence-gathering route rather than an AI course.
7
+
8
+ Use only fictional company examples. Summarize the interview instead of copying quotations; any indispensable quotation must be brief, relevant and de-identified. Never fabricate an NCS code, course name, URL or internal policy; mark unknown details `미확인`. Hand the supported AI-fit and unresolved constraints to `competency`.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: enterprise-competency
3
+ description: Compare task and verified NCS requirements to self-reported enterprise competencies without inferring incapacity.
4
+ ---
5
+
6
+ Use after `enterprise-ai-fit` at the `competency` stage. Map each verified NCS unit's official code, name, level and criteria to the relevant task, keeping its source. Compare those requirements with `## 현재 역량` as self-report only. Classify each requirement as evidenced by interview, gap in evidence, or `미확인` where baseline evidence is absent; never equate missing evidence with inability or claim that an interview verifies mastery. For AI-suitable tasks, include human review, data handling and evaluation skills only insofar as the task and evidence require them. Link each proposed learning outcome to a task and an observable assessment; flag unverified NCS detail `미확인`, not an invented official unit.
7
+
8
+ Use fictional people and employers in examples. Do not persist interview quotations or confidential descriptions; at most use a short, de-identified excerpt when necessary for a specific finding. Course availability and URL, approval to use company data and applicable company policy remain `미확인` until checked. Send the evidence-backed competencies and uncertainties to `roadmap`.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: enterprise-needs
3
+ description: Extract evidence-backed organizational learning needs from a fictional enterprise interview without claiming that a requested course is the solution.
4
+ ---
5
+
6
+ Use after `interview-to-profile` in enterprise mode (`intake` → `needs`). Read the enterprise interview headings `## 기업 개요`, `## 교육 대상`, `## 현업 과제`, `## 교육 목표`, `## 현재 역량`, `## 교육 제약`. Distinguish what the organization requested from what its workflow evidence supports. For each need, record the affected learner group, task or bottleneck, desired observable outcome, evidence section and missing information. Keep unsupported causes and target levels `미확인`; ask for missing evidence rather than invent a baseline or learner count. A request for AI training is not proof that AI is the best intervention.
7
+
8
+ Use only fictional employer and learner details in examples. Summarize or cite interview sections instead of reproducing interview paragraphs; if an excerpt is essential, quote only a short relevant fragment without names or identifiers. Never print the raw interview or credentials into graph stdout. Company approval and data-use policy, NCS mappings, named courses and course URLs remain `미확인` until independently verified. Hand needs and unresolved questions to `task`, not a prematurely selected course.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: enterprise-roadmap
3
+ description: Sequence evidence-backed enterprise learning and assessment into a conditional roadmap, not an approved company policy.
4
+ ---
5
+
6
+ Use after `enterprise-competency` at the `roadmap` stage. Order learning activities by prerequisite and task: baseline check, guided practice, task-based pilot with human review, then observable assessment. For each phase, show the learner group, supported task or verified NCS requirement, expected work artifact, success measure, responsible reviewer if known and prerequisites. If time, budget, data access, approval or baseline are absent from `## 교육 제약`, mark them `미확인` and describe the recommendation as conditional; do not invent durations, headcounts, policy approvals or guaranteed outcomes. Pass the ordered phases to `spec` for training specification and `routing` for delivery options. A course recommendation needs an inspected publisher page describing an actual course, direct URL and relevance to the cited unit; otherwise record course and URL as `미확인`, not a plausible link.
7
+
8
+ All examples must be fictional. Cite interview headings instead of copying raw interview text; any essential quote must be short, relevant and de-identified. NCS codes, course details and organization policy not independently confirmed remain `미확인`. Do not label this proposed roadmap an adopted company plan.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: enterprise-task-analysis
3
+ description: Turn enterprise learning needs into traceable tasks and evidence requirements for NCS matching.
4
+ ---
5
+
6
+ Use at the enterprise `task` stage after `enterprise-needs`. For each interview-backed work task, capture the learner role, input, action, output, observable performance criterion and interview section that supports it. Separate a concrete work description from a hypothesized task; mark absent frequency, tools, access permissions and proficiency `미확인`. Return job candidate keywords and bounded task descriptions for the existing `ncs-match` stage. A job title is a search candidate, not a confirmed NCS classification. Let `ncs-match` verify a code, name, level and source from the NCS service; never manufacture a code or promote a loose search hit to a confirmed unit.
7
+
8
+ In examples use fictional roles and organizations. Do not pass raw interview paragraphs into graph stdout: cite the heading or use a brief de-identified excerpt only where necessary. Do not invent internal company policy, course names or course URLs; write `미확인` if absent. If no concrete task is documented, state that the task basis is `미확인` and request clarification instead of drawing an unsupported NCS match.
@@ -3,4 +3,21 @@ name: interview-to-profile
3
3
  description: Extract career candidates, experience and self-reported skills from a Korean interview.
4
4
  ---
5
5
 
6
- Read the interview Markdown. Extract, without invention, `jobs` (target job candidates), `skills` (self-reported capabilities) and `experienceCount` (number of concrete work or project descriptions). Do not persist the raw descriptions in graph stdout. Record unknowns as missing rather than extrapolating. Never print or log the original interview. For the packaged graph, prepare headings `## 희망 직무`, `## 경험`, `## 보유 역량`; list items for experience and comma-separated skills. A job is a candidate, not a confirmed NCS code.
6
+ Read the interview Markdown without inventing evidence. Never print or log the original interview; keep quotations out of graph stdout and persistent run context. Where a report needs a quotation, use only a short, relevant excerpt necessary to support the claim, without identifying details; otherwise paraphrase and cite the interview section. Mark unknown NCS codes or units, course names or URLs, and company policy details `미확인` rather than guessing.
7
+
8
+ ## Personal mode (existing contract)
9
+
10
+ Extract `jobs` (target job candidates), `skills` (self-reported capabilities) and `experienceCount` (number of concrete work or project descriptions). Do not persist the raw descriptions in graph stdout. Record unknowns as missing rather than extrapolating. For the personal graph, prepare headings `## 희망 직무`, `## 경험`, `## 보유 역량`; list items for experience and comma-separated skills. A job is a candidate, not a confirmed NCS code.
11
+
12
+ ## Enterprise mode (interview-to-profile input)
13
+
14
+ Use a fictional organization in packaged examples. Prepare the interview under these headings, separating statements made by the organization from independently confirmed facts:
15
+
16
+ - `## 기업 개요`: fictional organization, sector and team; omit real employer and employee identifiers.
17
+ - `## 교육 대상`: roles, learner group and scope; do not infer headcount or proficiency.
18
+ - `## 현업 과제`: concrete tasks and workflow bottlenecks, not a presumed diagnosis.
19
+ - `## 교육 목표`: requested outcomes and observable success measures, if stated.
20
+ - `## 현재 역량`: self-reported capabilities and evidence; not a verified competency rating.
21
+ - `## 교육 제약`: time, budget, data access and approvals only when actually supplied; otherwise `미확인`.
22
+
23
+ For the enterprise graph, summarize only the minimum necessary organization context, job candidates, tasks, goals, current skills and constraints for downstream steps. Do not copy interview paragraphs or identifying quotations into run outputs. Keep a stated need distinct from an observed task and from a recommended intervention. An interview alone does not verify NCS classifications, course existence or URLs, applicability of AI to confidential data, or internal policy approval: mark each unknown `미확인` until independently verified. Do not present a suggested training plan as an approved company policy.
@@ -505,7 +505,10 @@ BACKUP_OK=1
505
505
  if [ "${ELANOUS_BACKUP_NO_HEARTBEAT:-0}" = "1" ]; then
506
506
  echo " (심박 끔 — ELANOUS_BACKUP_NO_HEARTBEAT=1 · 시험 전용)" >&2
507
507
  elif [ -n "$HB_REPO" ] && [ -f "$HB_REPO/scripts/botlab/heartbeat-emit.sh" ]; then
508
- bash "$HB_REPO/scripts/botlab/heartbeat-emit.sh" backup "$BACKUP_OK" "$HB_REPO" "$BACKUP_SOURCE"
508
+ # 심박은 «운영» 우주로 — 백업이 뜨는 것이 운영 ~/.elanous 이다. 크론은 환경변수가 없고 소스 트리는
509
+ # 트리 파생 시험 우주로 풀려(`where` layer=tree-derived) 심박이 «격리라 안 밀었다»로 3일 멎었다(09-30).
510
+ ELANOUS_STATE_DIR="${ELANOUS_STATE_DIR:-$HOME/.elanous}" ELANOUS_CONFIG_DIR="${ELANOUS_CONFIG_DIR:-$HOME/.elanous}" \
511
+ bash "$HB_REPO/scripts/botlab/heartbeat-emit.sh" backup "$BACKUP_OK" "$HB_REPO" "$BACKUP_SOURCE"
509
512
  else
510
513
  echo " ⚠️ 심박을 «못 보냈다» — 저장소 자리를 못 찾았다(HB_REPO=${HB_REPO:-없음})" >&2
511
514
  fi
@@ -60,11 +60,12 @@ export const TRACK_SCRUBS: { pattern: RegExp; replace: string; why: string }[] =
60
60
  /** 잔여 표식 — 하나라도 남으면 rc=1 */
61
61
  const TRACK_REGISTRY = JSON.parse(
62
62
  readFileSync(join(import.meta.dir, 'coord-tracks.json'), 'utf8'),
63
- ) as { tracks: { id: string; mark: string }[] };
63
+ ) as { tracks: { id: string; mark: string; alias?: string }[] };
64
64
  const TRACK_MARKS = TRACK_REGISTRY.tracks.map((t) => t.mark).join('');
65
- const TRACK_IDS = TRACK_REGISTRY.tracks.map((t) => t.id).join('');
65
+ /** 2글자 신원(OP·MK·TC·UX) ⊕ 옛 한 글자 별칭(S·T·O·F) — 둘 다 공개 문서에 남으면 안 된다. */
66
+ const TRACK_NAMES = [...new Set(TRACK_REGISTRY.tracks.flatMap((t) => [t.id, t.alias ?? '']).filter(Boolean))].join('|');
66
67
  /** 잔여 표식 — 트랙 정본(`coord-tracks.json`)에서 파생한다. 트랙을 더해도 여기는 고치지 않는다. */
67
- export const TRACK_RESIDUE = new RegExp(`[${TRACK_MARKS}]|(?<![\\w\\]])\\[[${TRACK_IDS}]\\](?!\\()`, 'gu');
68
+ export const TRACK_RESIDUE = new RegExp(`[${TRACK_MARKS}]|(?<![\\w\\]])\\[(?:${TRACK_NAMES})\\](?!\\()`, 'gu');
68
69
 
69
70
  export function scrubTrackMarks(md: string): { text: string; applied: string[] } {
70
71
  let text = md;
@@ -107,13 +107,20 @@ fi
107
107
  # 🆕 2026-09-24 (🅞 보고): 「나에게 온 «요청»」 표지가 🅢 로 고정돼 있었다 — `--track O` 감시에서 🅢·🅣 앞 요청이
108
108
  # «🅞 에게 온 요청»으로 뜨고, 🅞 앞 요청은 일반어(부탁드립니다)가 없으면 안 떴다.
109
109
  # ⇒ 표지를 트랙 정본(scripts/coord-tracks.json)에서 $TRACK 으로 찾아 식을 짓는다. 일반어는 «내 표지와 함께»일 때만.
110
- MARK=$(grep -o "\"id\": *\"$TRACK\", *\"mark\": *\"[^\"]*\"" "$(dirname "$0")/coord-tracks.json" 2>/dev/null | sed 's/.*"mark": *"//; s/"$//')
110
+ # 🆕 09-30 역할 재편: 신원이 2글자 id(TC) ⊕ 직함(CTO) ⊕ 옛 alias(O) 셋이다 — --track 은 id·alias 어느 쪽도 받고,
111
+ # «내 글» 거르기와 «나에게 온 요청» 표지는 셋을 다 본다(전환기엔 옛 접두로 쓴 글도 내 글이다).
112
+ ENTRY=$(jq -c --arg t "$TRACK" '[.tracks[] | select(.id == $t or .alias == $t)][0] // empty' "$(dirname "$0")/coord-tracks.json" 2>/dev/null)
113
+ MARK=$(printf '%s' "$ENTRY" | jq -r '.mark // empty' 2>/dev/null)
114
+ SELF_ID=$(printf '%s' "$ENTRY" | jq -r '.id // empty' 2>/dev/null); SELF_ID=${SELF_ID:-$TRACK}
115
+ SELF_ALIAS=$(printf '%s' "$ENTRY" | jq -r '.alias // empty' 2>/dev/null); SELF_ALIAS=${SELF_ALIAS:-$SELF_ID}
116
+ SELF_TITLE=$(printf '%s' "$ENTRY" | jq -r '.title // empty' 2>/dev/null)
117
+ NAMES_ADDR="\\\\[$SELF_ID\\\\] 님|\\\\[$SELF_ALIAS\\\\] 님|$SELF_ID 님|$SELF_ID 께${SELF_TITLE:+|$SELF_TITLE 님|$SELF_TITLE 께}"
111
118
  if [ -n "$MARK" ]; then
112
- REQ_ADDR="$MARK 께|$MARK 에게|$MARK 께서|$MARK 에 요청|$MARK 님|\\\\[$TRACK\\\\] 님"
119
+ REQ_ADDR="$MARK 께|$MARK 에게|$MARK 께서|$MARK 에 요청|$MARK 님|$NAMES_ADDR"
113
120
  REQ_MARK="$MARK"
114
121
  else
115
122
  echo "⚠️ 트랙 정본에서 $TRACK 의 표지를 못 찾았다 — 요청 표지는 [$TRACK] 님 만 본다" >&2
116
- REQ_ADDR="\\\\[$TRACK\\\\] 님"
123
+ REQ_ADDR="$NAMES_ADDR"
117
124
  REQ_MARK="\\\\[$TRACK\\\\]"
118
125
  fi
119
126
 
@@ -123,6 +130,9 @@ LOCKDIR="$TMP/ch${PR_NUM}-$TRACK.lock"
123
130
  # 본문 기준선 — ⭐ 프로세스가 아니라 «파일»에 둔다. 그래서 감시가 죽어 있던 구간의 본문 변경도
124
131
  # 부활 «첫 주기»에 뜬다(매뉴얼 §「부활 구간은 못 본다」의 **본문 축만** 닫힌다 — 코멘트 축은 그대로).
125
132
  BODYFILE="$TMP/ch${PR_NUM}-$TRACK.body"
133
+ # 코멘트 기준선(since)도 «파일»에 둔다(RFC-coordination-cost §7a · C1 · 09-30) — 재기동·30분 만기 재무장 뒤에도
134
+ # 끊긴 자리부터 이어 읽는다. 24시간보다 오래된 기준선은 버리고 지금부터(한 번에 쏟아지는 것 방지).
135
+ SINCEFILE="$TMP/ch${PR_NUM}-$TRACK.since"
126
136
 
127
137
  # 기동시각 — 소유 판정의 유일한 근거(PID 는 재사용된다). 공백은 정규화해서 비교한다.
128
138
  proc_started() {
@@ -416,7 +426,7 @@ fetch_comments() { # $1=since(ISO8601) → stdout=포맷된 신규 행 · rc=0
416
426
  # 그 글은 필터를 «통과»했고 창에도 떴다 — 그런데 다른 글 사이에 섞여 «요청»으로 안 보였다.
417
427
  # 🔑 그러므로 결손은 「고르기」가 아니라 ***「고른 뒤 «구분»하지 않은 것」***이다.
418
428
  # ⛔ 표지를 «본문 앞»에 둔다 — 뒤에 두면 BODY_CHARS 절단에 잘려 사라진다.
419
- fmt="select(((.body | sub(\"^[[:space:]#]+\"; \"\")) | (startswith(\"**[$TRACK]\") or startswith(\"[$TRACK]\"))) | not)
429
+ fmt="select(((.body | sub(\"^[[:space:]#]+\"; \"\")) | (startswith(\"**[$SELF_ID]\") or startswith(\"[$SELF_ID]\") or startswith(\"**[$SELF_ALIAS]\") or startswith(\"[$SELF_ALIAS]\"))) | not)
420
430
  | \"[#$PR_NUM 신규 id=\" + (.id|tostring) + \" \" + .created_at + \" @\" + .login + \"]\"
421
431
  + (if ((.body | test(\"$REQ_ADDR\")) or ((.body | test(\"$REQ_MARK\")) and (.body | test(\"부탁드립니다|부탁합니다\")))) then \" 🙋‼️ 나에게 온 «요청»\" else \"\" end)
422
432
  + \" \" + (.body[:$BODY_CHARS] | gsub(\"\n\"; \" ⏎ \"))"
@@ -501,6 +511,14 @@ watch_loop() {
501
511
  echo " 📄 «본문» 감시 ON · 기준선 «없음» ⇒ 첫 주기를 기준선으로 삼고 이벤트를 안 낸다"
502
512
  fi
503
513
  last=$(date -u +%Y-%m-%dT%H:%M:%SZ)
514
+ if [ -s "$SINCEFILE" ]; then
515
+ local saved; saved=$(head -1 "$SINCEFILE" 2>/dev/null)
516
+ if [[ "$saved" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] \
517
+ && [ -z "$(find "$SINCEFILE" -mmin +1440 2>/dev/null)" ]; then
518
+ last="$saved"
519
+ echo " 🔁 코멘트 기준선 이어 읽기 since=$last (파일 $SINCEFILE) — 죽어 있던 구간도 첫 주기에 뜬다"
520
+ fi
521
+ fi
504
522
  while true; do
505
523
  sleep "$INTERVAL"
506
524
  now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -530,6 +548,7 @@ watch_loop() {
530
548
  fi
531
549
  [ -n "$out" ] && printf '%s\n' "$out"
532
550
  last="$now"
551
+ printf '%s\n' "$last" > "$SINCEFILE" 2>/dev/null || true
533
552
  # ⭐ 실패하다 «돌아왔으면» 한 줄로 말한다 — 조용히 복구되면 아무도 «공백 구간»이 있었음을 모른다.
534
553
  if [ "$WATCH_FAILS" -gt 0 ]; then
535
554
  printf '✅ [#%s 조회 «회복» %s] 연속 실패 %s회 뒤 성공 — 그 사이도 since=%s 로 «이어 붙였다»(놓친 것 없음)\n' \
@@ -41,25 +41,44 @@ grep -q '{{TS}}' "$BODY" || echo "⚠️ 본문에 {{TS}} 자리표시가 «없
41
41
 
42
42
  # 트랙 신원은 «정본 한 곳»에서 읽는다 — scripts/coord-tracks.json. 못 읽으면 추측하지 않고 선다.
43
43
  TRACKS_FILE="$(dirname "$0")/coord-tracks.json"
44
- TRACKS=$(grep -o '"id": *"[A-Z]"' "$TRACKS_FILE" 2>/dev/null | grep -o '[A-Z]"$' | tr -d '"\n')
45
- [ -n "$TRACKS" ] || { echo "⛔ 트랙 정본을 읽지 못했다: $TRACKS_FILE — 발신하지 않는다" >&2; exit 4; }
44
+ # 신원 = 2글자 id(OP·MK·TC·UX…) ⊕ 옛 한 글자 alias(S·T·O·F — 전환기엔 받되 경고). 목록은 정본에서만.
45
+ IDS=$(grep -o '"id": *"[A-Z][A-Z]*"' "$TRACKS_FILE" 2>/dev/null | sed 's/.*"\([A-Z]*\)"$/\1/' | tr '\n' ' ')
46
+ ALIASES=$(grep -o '"alias": *"[A-Z][A-Z]*"' "$TRACKS_FILE" 2>/dev/null | sed 's/.*"\([A-Z]*\)"$/\1/' | tr '\n' ' ')
47
+ [ -n "$IDS" ] || { echo "⛔ 트랙 정본을 읽지 못했다: $TRACKS_FILE — 발신하지 않는다" >&2; exit 4; }
46
48
  ID="${COORD_ID:-S}"
47
- if [ "${#ID}" -ne 1 ] || [ "${TRACKS#*"$ID"}" = "$TRACKS" ]; then
48
- echo "⛔ COORD_ID 는 트랙 정본($TRACKS_FILE)의 신원 [$TRACKS] 중 하나여야 한다(받은 값: $ID)" >&2; exit 4
49
- fi
49
+ case " $IDS " in
50
+ *" $ID "*) ;;
51
+ *) case " $ALIASES " in
52
+ *" $ID "*) NEW=$(grep -o "\"id\": *\"[A-Z]*\", *\"title\": *\"[A-Z]*\", *\"alias\": *\"$ID\"" "$TRACKS_FILE" | sed 's/^"id": *"\([A-Z]*\)".*/\1/')
53
+ echo "⚠️ 옛 한 글자 신원 [$ID] — 새 신원은 [${NEW:-?}] 다(전환기라 받는다)" >&2 ;;
54
+ *) echo "⛔ COORD_ID 는 트랙 정본($TRACKS_FILE)의 신원 [$IDS] (옛 [$ALIASES]) 중 하나여야 한다(받은 값: $ID)" >&2; exit 4 ;;
55
+ esac ;;
56
+ esac
50
57
  FIRST=$(head -1 "$BODY")
58
+ OTHER=""
59
+ for T in $IDS $ALIASES; do
60
+ [ "$T" = "$ID" ] && continue
61
+ case "$FIRST" in "**[$T]**"*) OTHER="$T" ;; esac
62
+ done
51
63
  case "$FIRST" in
52
64
  "**[$ID]**"*) ;;
53
- \*\*\[[$TRACKS]\]\*\**)
54
- echo "⛔ 신원 접두가 «내 것이 아니다» — 첫 줄이 ${FIRST%%\*\* *}** 로 시작한다(내 신원 = [$ID])." >&2
55
- echo " ⛔ 감시자는 startswith 로 고른다 ⇒ 상대는 이 글을 «자기 글»로 보고 건너뛴다. 발신하지 않는다." >&2
56
- echo " ✅ 수신자는 본문에 적어라: **[$ID]** … [T] 님께 …" >&2
57
- exit 5 ;;
58
65
  *)
66
+ if [ -n "$OTHER" ]; then
67
+ echo "⛔ 신원 접두가 «내 것이 아니다» — 첫 줄이 **[$OTHER]** 로 시작한다(내 신원 = [$ID])." >&2
68
+ echo " ⛔ 감시자는 startswith 로 고른다 ⇒ 상대는 이 글을 «자기 글»로 보고 건너뛴다. 발신하지 않는다." >&2
69
+ echo " ✅ 수신자는 본문에 적어라: **[$ID]** … MK 님께 …" >&2
70
+ exit 5
71
+ fi
59
72
  echo "⛔ 첫 줄이 «**[$ID]**» 신원 접두로 시작하지 않는다 — 상대 감시자가 이 글을 못 고른다. 발신하지 않는다." >&2
60
73
  exit 5 ;;
61
74
  esac
62
75
 
76
+ # 봉투 v2(RFC-coordination-cost §2 · C1 · 09-30): 첫 줄에 종류 다섯 중 하나 — 막지 않고 «알린다».
77
+ case "$FIRST" in
78
+ *요청*|*결정*|*사고*|*보고*|*정정*) ;;
79
+ *) echo "⚠️ 첫 줄에 글 종류(요청·결정·사고·보고·정정)가 없다 — 받는 쪽 감시자가 즉시/모아 읽기를 못 가른다(발신은 한다)" >&2 ;;
80
+ esac
81
+
63
82
  TS=$(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M KST')
64
83
  TMP=$(mktemp "${TMPDIR:-/tmp}/coord-post.XXXXXX") || { echo "⛔ mktemp 실패 — 발신하지 않는다" >&2; exit 7; }
65
84
  DATA=$(mktemp "${TMPDIR:-/tmp}/coord-post-data.XXXXXX") || { rm -f "$TMP"; echo "⛔ mktemp 실패 — 발신하지 않는다" >&2; exit 7; }
@@ -125,8 +144,26 @@ fi
125
144
  POST_OUTPUT=$(bun bin/elanous.mjs gh pr comment "$PR" --body-file "$TMP" 2>&1)
126
145
  RC=$?
127
146
  printf '%s\n' "$POST_OUTPUT" | tail -2
147
+ VIA=""
148
+ # `gh pr comment` goes through GraphQL; its secondary rate limit can block while REST still answers (GIT-S83).
149
+ # 09-30: 채널 코멘트 1,500 뒤 GraphQL 이 «Something went wrong» 으로 연속 실패하고 REST 는 201 — 이 내부 오류도 REST 로 물러선다.
150
+ # ⛔ 다른 실패(PR 못 찾음 등)는 물러서지 않는다. 물러서기 전에 같은 첫 줄이 이미 올라갔는지 본다(부분 성공 → 이중 발신 방지).
151
+ if [ "$RC" -ne 0 ] && printf '%s' "$POST_OUTPUT" | grep -qiE 'rate limit|Something went wrong'; then
152
+ echo "⚠️ GraphQL 실패(한도·내부 오류) — REST 로 한 번 물러선다(GIT-S83)" >&2
153
+ FIRSTOUT=$(head -1 "$TMP")
154
+ # issues 코멘트 목록은 오래된 순이고 정렬 인자를 안 받는다 ⇒ 최근 10분만 since 로 자른다.
155
+ SINCE10=$(date -u -v-10M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '-10 min' +%Y-%m-%dT%H:%M:%SZ)
156
+ if bun bin/elanous.mjs gh api "repos/{owner}/{repo}/issues/$PR/comments?since=$SINCE10&per_page=100" --jq '.[].body | split("\n")[0]' 2>/dev/null | grep -qxF -- "$FIRSTOUT"; then
157
+ echo "[coord-post] 같은 첫 줄이 이미 채널에 있다 — GraphQL 이 실제로는 올렸다(이중 발신 안 함)" >&2
158
+ exit 0
159
+ fi
160
+ POST_OUTPUT=$(bun bin/elanous.mjs gh api -X POST "repos/{owner}/{repo}/issues/$PR/comments" -F "body=@$TMP" -q .html_url 2>&1)
161
+ RC=$?
162
+ printf '%s\n' "$POST_OUTPUT" | tail -2
163
+ VIA=" · REST 폴백"
164
+ fi
128
165
  if [ "$RC" -ne 0 ]; then
129
- echo "⛔ 발신 실패 rc=$RC — «보냈다고 읽지 마라»" >&2
166
+ echo "⛔ 발신 실패 rc=$RC$VIA — «보냈다고 읽지 마라»" >&2
130
167
  exit "$RC"
131
168
  fi
132
- echo "[coord-post] 발신 성공 · 채널 #$PR · 신원 [$ID] · 시각 $TS" >&2
169
+ echo "[coord-post] 발신 성공$VIA · 채널 #$PR · 신원 [$ID] · 시각 $TS" >&2
@@ -1,10 +1,10 @@
1
1
  {
2
- "_comment": "조율 채널 트랙 신원의 정본. 트랙을 더하려면 여기 한 줄만 더한다 — scripts/coord-post.sh 와 scripts/build-docs-site.ts 가 이 파일을 읽는다.",
2
+ "_comment": "조율 채널 세션 신원의 정본(09-30 역할 재편). id = 2글자 신원(발신 접두 **[TC]**) · title = 직함 · alias = 옛 한 글자(기록 읽기·전환기 발신용) · mark = 옛 표지. 세션을 더하려면 여기 한 줄만 더한다 — coord-post.sh · coord-channel-watch.sh · build-docs-site.ts · mission-phase-track.ts 가 이 파일을 읽는다.",
3
3
  "tracks": [
4
- { "id": "S", "mark": "🅢", "owns": ["기획", "총괄", "전략", "소구점", "로드맵", "plan", "strategy"] },
5
- { "id": "T", "mark": "🅣", "owns": ["영상", "비디오", "티저", "하이퍼프레임", "HyperFrames", "렌더", "video", "teaser", "render", "B-roll"] },
6
- { "id": "F", "mark": "🅕", "owns": ["PWA", "웹", "웹사이트", "elanous.ai", "탭", "화면", "UI", "프론트엔드", "frontend", "web", "tab"] },
7
- { "id": "O", "mark": "🅞", "owns": ["블로그", "매뉴얼", "문서", "흡수", "Obsidian", "승인", "blog", "manual", "docs"] },
4
+ { "id": "OP", "title": "COO", "alias": "S", "mark": "🅢", "owns": ["판올림", "컷", "체크리스트", "분배", "통합", "로드맵", "기획", "총괄", "전략", "인계", "release cut", "roadmap", "plan", "strategy"] },
5
+ { "id": "MK", "title": "CMO", "alias": "T", "mark": "🅣", "owns": ["블로그", "콘텐츠", "발행", "광고", "프로모션", "소구점", "마케팅", "영상", "비디오", "티저", "하이퍼프레임", "렌더", "blog", "content", "marketing", "ads", "video", "teaser", "render", "B-roll", "상표", "변리사", "브랜드", "trademark", "brand", "홈페이지", "웹사이트", "elanous.ai", "elanvital.ai", "website"] },
6
+ { "id": "TC", "title": "CTO", "alias": "O", "mark": "🅞", "owns": ["하니스", "게이트", "원장", "리뷰", "관측", "릴리스 루프", "흡수", "Obsidian", "매뉴얼", "문서", "기술", "harness", "gate", "ledger", "review", "observability", "intake", "manual", "docs"] },
7
+ { "id": "UX", "title": "CXO", "alias": "F", "mark": "🅕", "owns": ["PWA", "웹", "탭", "화면", "UI", "프론트엔드", "안드로이드", "아이폰", "데스크톱", "텔레그램", "디스코드", "메신저", "봇", "frontend", "web", "tab", "android", "ios", "desktop", "telegram", "discord", "bot"] },
8
8
  { "id": "E", "mark": "🅔", "owns": [] }
9
9
  ]
10
10
  }