cuelo 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +11 -11
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +2 -2
  6. package/.next/required-server-files.json +2 -2
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/server/app/api/updates/route.js +1 -1
  26. package/.next/server/app/index.html +1 -1
  27. package/.next/server/app/index.rsc +4 -4
  28. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  29. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  30. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/.next/server/app/office/page_client-reference-manifest.js +1 -1
  34. package/.next/server/app/office-preview/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/office-preview.html +1 -1
  36. package/.next/server/app/office-preview.rsc +2 -2
  37. package/.next/server/app/office-preview.segments/_full.segment.rsc +2 -2
  38. package/.next/server/app/office-preview.segments/_head.segment.rsc +1 -1
  39. package/.next/server/app/office-preview.segments/_index.segment.rsc +2 -2
  40. package/.next/server/app/office-preview.segments/_tree.segment.rsc +1 -1
  41. package/.next/server/app/office-preview.segments/office-preview/__PAGE__.segment.rsc +1 -1
  42. package/.next/server/app/office-preview.segments/office-preview.segment.rsc +1 -1
  43. package/.next/server/app/office.html +1 -1
  44. package/.next/server/app/office.rsc +4 -4
  45. package/.next/server/app/office.segments/_full.segment.rsc +4 -4
  46. package/.next/server/app/office.segments/_head.segment.rsc +1 -1
  47. package/.next/server/app/office.segments/_index.segment.rsc +2 -2
  48. package/.next/server/app/office.segments/_tree.segment.rsc +1 -1
  49. package/.next/server/app/office.segments/office/__PAGE__.segment.rsc +3 -3
  50. package/.next/server/app/office.segments/office.segment.rsc +1 -1
  51. package/.next/server/app/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/recover.html +1 -1
  54. package/.next/server/app/recover.rsc +2 -2
  55. package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
  56. package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
  57. package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
  58. package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
  59. package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
  60. package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
  61. package/.next/server/app-paths-manifest.json +11 -11
  62. package/.next/server/chunks/5489.js +2 -2
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/pages/404.html +1 -1
  65. package/.next/server/pages/500.html +1 -1
  66. package/.next/server/server-reference-manifest.json +1 -1
  67. package/.next/static/chunks/{9575-2c3b8252837b1934.js → 9575-9196474bb9e5c752.js} +2 -2
  68. package/.next/static/chunks/app/{layout-330b42182f46978a.js → layout-713017c65de14601.js} +1 -1
  69. package/.next/trace +108 -108
  70. package/.next/trace-build +1 -1
  71. package/README.md +1 -1
  72. package/Tools/CUELO_Setup/files/native-runtime-patch.js +3 -3
  73. package/Tools/OMP_Global_Config/agent/RULES.md +4 -0
  74. package/Tools/OMP_Global_Config/agent/agents/maker.md +12 -6
  75. package/Tools/OMP_Global_Config/agent/extensions/character-voice.ts +3 -3
  76. package/Tools/OMP_Global_Config/agent/extensions/command-guard/index.ts +16 -1
  77. package/Tools/OMP_Global_Config/agent/extensions/command-guard/powershell-syntax.ts +5 -4
  78. package/Tools/OMP_Global_Config/agent/extensions/draft/markdown.ts +226 -0
  79. package/Tools/OMP_Global_Config/agent/extensions/draft/ooxml.ts +222 -0
  80. package/Tools/OMP_Global_Config/agent/extensions/draft/pdf.ts +63 -0
  81. package/Tools/OMP_Global_Config/agent/extensions/draft/zip.ts +73 -0
  82. package/Tools/OMP_Global_Config/agent/extensions/draft.ts +211 -0
  83. package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +1 -1
  84. package/Tools/OMP_Global_Config/agent/extensions/skim.ts +33 -22
  85. package/Tools/OMP_Global_Config/agent/prompt-copies/RULES.en.md +2 -1
  86. package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +2 -2
  87. package/Tools/OMP_Global_Config/agent/rules/subagent.md +14 -6
  88. package/Tools/OMP_Global_Config/agent/sop/_common.md +4 -4
  89. package/Tools/OMP_Global_Config/agent/sop/_writer.md +6 -0
  90. package/Tools/OMP_Global_Config/agent/sop/maker.md +1 -1
  91. package/Tools/OMP_Global_Config/agent/tools/git-finalizer/index.ts +26 -1
  92. package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +18 -0
  93. package/package.json +7 -7
  94. /package/.next/static/{mxPvGJ9HlZ_4ew2eZ7ps7 → S8tJkE71e2D0DgH4X1s0S}/_buildManifest.js +0 -0
  95. /package/.next/static/{mxPvGJ9HlZ_4ew2eZ7ps7 → S8tJkE71e2D0DgH4X1s0S}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-webpack","duration":112423190,"timestamp":700170,"id":14,"parentId":1,"tags":{},"startTime":1791473713913,"traceId":"3606727d2701de14"},{"name":"run-typescript","duration":20687094,"timestamp":113483877,"id":11084,"parentId":1,"tags":{},"startTime":1791473826697,"traceId":"3606727d2701de14"},{"name":"static-check","duration":4251042,"timestamp":134266247,"id":11087,"parentId":1,"tags":{},"startTime":1791473847479,"traceId":"3606727d2701de14"},{"name":"static-generation","duration":12640866,"timestamp":138637262,"id":11259,"parentId":1,"tags":{},"startTime":1791473851850,"traceId":"3606727d2701de14"},{"name":"collect-build-traces","duration":21550937,"timestamp":138595291,"id":11256,"parentId":1,"tags":{},"startTime":1791473851808,"traceId":"3606727d2701de14"},{"name":"telemetry-flush","duration":89,"timestamp":160150808,"id":11268,"parentId":1,"tags":{},"startTime":1791473873364,"traceId":"3606727d2701de14"},{"name":"next-build","duration":159572212,"timestamp":578707,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1791473713792,"traceId":"3606727d2701de14"}]
1
+ [{"name":"run-webpack","duration":114018786,"timestamp":687805,"id":14,"parentId":1,"tags":{},"startTime":1791507921870,"traceId":"6c75260e77b7370d"},{"name":"run-typescript","duration":24733849,"timestamp":115113546,"id":11084,"parentId":1,"tags":{},"startTime":1791508036296,"traceId":"6c75260e77b7370d"},{"name":"static-check","duration":4561356,"timestamp":139975975,"id":11087,"parentId":1,"tags":{},"startTime":1791508061159,"traceId":"6c75260e77b7370d"},{"name":"static-generation","duration":12317561,"timestamp":144649546,"id":11259,"parentId":1,"tags":{},"startTime":1791508065832,"traceId":"6c75260e77b7370d"},{"name":"collect-build-traces","duration":21852595,"timestamp":144608304,"id":11256,"parentId":1,"tags":{},"startTime":1791508065791,"traceId":"6c75260e77b7370d"},{"name":"telemetry-flush","duration":109,"timestamp":166472512,"id":11268,"parentId":1,"tags":{},"startTime":1791508087655,"traceId":"6c75260e77b7370d"},{"name":"next-build","duration":165897246,"timestamp":575399,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1791507921758,"traceId":"6c75260e77b7370d"}]
package/README.md CHANGED
@@ -16,7 +16,7 @@ Use the same `~/.omp/agent` sessions, credentials and model configuration from t
16
16
 
17
17
  **한국어:** CUELO는 omp(oh-my-pi)를 위한 오픈소스 AI 코딩 에이전트 작업 공간이자 하네스입니다. 웹 앱은 omp SDK를 서버 안에서 실행하고, `omp` CLI와 같은 `~/.omp/agent` 디렉터리를 씁니다. 터미널에서 하던 세션을 브라우저에서 이어 가고, 다시 터미널로 돌아가도 기록·계정·모델 설정을 공유합니다.
18
18
 
19
- 내장 OMP 코어는 **18.8.5**입니다. 별도로 설치한 `omp` CLI의 버전은 웹 업데이트와 독립적이며, 앱에 포함된 CLI를 `omp` 명령으로 사용하는 설치에서는 웹과 같은 코어를 사용합니다.
19
+ 내장 OMP 코어는 **18.8.6**입니다. 별도로 설치한 `omp` CLI의 버전은 웹 업데이트와 독립적이며, 앱에 포함된 CLI를 `omp` 명령으로 사용하는 설치에서는 웹과 같은 코어를 사용합니다.
20
20
 
21
21
  CUELO의 목적은 작업 중 확인한 실패와 막힘을 기억·작업 규칙·실행 절차에 반영해, 사람이 같은 문제를 다시 지적하지 않아도 다음 작업에서 실수를 줄이는 것입니다. 원인을 확인하고 필요한 부분만 바꾸며, 다음 작업에서 적용과 효과를 확인하는 것까지를 지향합니다. 기록을 저장했다는 이유만으로 자동 개선이 끝났다고 보지는 않습니다.
22
22
 
@@ -9,9 +9,9 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
9
9
  const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
10
10
  const EXPECTED = {
11
11
  name: 'cuelo',
12
- version: '0.10.1',
12
+ version: '0.10.2',
13
13
  distribution: 'cuelo',
14
- coreVersion: '18.8.5',
14
+ coreVersion: '18.8.6',
15
15
  };
16
16
 
17
17
  // OMP core 18.4.12 `src/sdk.ts`: session-scoped AsyncJobManager for a process
@@ -177,7 +177,7 @@ function readNativePackage(target) {
177
177
  throw new Error(`unexpected native package ${packageJson.name}@${packageJson.version}`);
178
178
  }
179
179
  if (!build || build.distribution !== EXPECTED.distribution || build.coreVersion !== EXPECTED.coreVersion) {
180
- throw new Error('native cueloBuild metadata does not match cuelo / core 18.8.5');
180
+ throw new Error('native cueloBuild metadata does not match cuelo / core 18.8.6');
181
181
  }
182
182
  return packageJson;
183
183
  }
@@ -59,6 +59,10 @@
59
59
  - 여러 파일을 훑거나 문서·README·저장소 구조를 조사할 때는 먼저 `skim`에 질문한다. 허용 텍스트 파일
60
60
  내용은 Google(Gemini), 그 호출이 실패해 한 번 대체되면 B.AI(DeepSeek)로도 전송된다.
61
61
  비밀 자료를 대상으로 삼지 말고, 수정할 정확한 줄은 `read`로 확인한다.
62
+ - 문서 초안(md·csv·Excel·Word·PDF 보고서·표)이 필요하면 Main·Maker 모두 먼저 `draft`로 쓴다. Gemini가
63
+ 작성해 cwd 안에 저장하고, 실패하면 DeepSeek로 한 번 대체한다(참고 텍스트는 Google 또는 B.AI로 전송).
64
+ 만들어진 파일은 직접 열어 검토한 뒤 쓴다. 코드·설정·규칙·`HANDOFF.md`·작업 기록·정확한 줄 수정은 초안이
65
+ 아니므로 `edit`/`write`를 쓴다. Gemini 할당량은 같은 Antigravity 계정의 vision·skim과 공유한다.
62
66
  - **브라우저는 relay가 기본값이다.** `browser.open`에 `app: { relay: false }`를 빼면 관리형 headless가
63
67
  아니라 **사용자의 실제 브라우저**에 탭이 열린다. 닫을 때는 `browser.close({ name })`로 자기 탭만
64
68
  닫고 `all`·`kill`·`app.path`는 쓰지 않는다 — 사용자 창까지 닫히거나 기존 프로세스에 붙는다.
@@ -3,10 +3,10 @@ name: maker
3
3
  description: Delegated end-to-end implementation agent that investigates, edits, repairs, validates its own slice, and verifies the actual surface it changed.
4
4
  model: "@implSonnet"
5
5
  thinking-level: auto
6
- tools: [read, bash, edit, write, grep, glob, skim, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, checkpoint]
6
+ tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, checkpoint]
7
7
  ---
8
8
 
9
- <!-- omp-global-config:generated source-hash=e79273fcc4b3
9
+ <!-- omp-global-config:generated source-hash=29515cbfea01
10
10
  이 파일은 patches/build-agents.mjs 가 만든 빌드 산출물이다. 직접 수정하지 마라.
11
11
  고칠 곳: agent/sop/maker.md · agent/sop/_writer.md · agent/sop/_common.md
12
12
  재생성: node patches/build-agents.mjs 검사: node patches/build-agents.mjs --check
@@ -26,6 +26,12 @@ is yours end to end.
26
26
  outside the brief boundary, to production or external processes, or invoking `write_memory` is
27
27
  allowed only when the brief names the exact target and action and applicable user approval exists.
28
28
  - Use `web_search` to discover current external information, then use `read` on the primary source.
29
+ - When the slice needs a document draft (Markdown, CSV, Excel, Word, PDF report or table), write it
30
+ with `draft(instruction, output, paths?)` first: Gemini Flash writes it (DeepSeek once on
31
+ failure; reference text goes to Google or B.AI) and the tool saves the file inside the cwd.
32
+ Open and review the produced file before you use or hand it over. Code, configuration, rules,
33
+ `HANDOFF.md`, your own `doc/history` record, and exact-line edits are not drafts; keep using
34
+ `edit`/`write` for them.
29
35
  - Use `todo` only when the work has three or more steps. If you invoke `checkpoint`, invoke the
30
36
  runtime-paired `rewind` before yielding.
31
37
  - In that same first investigation, cross-check the brief itself: does changing the assigned
@@ -260,10 +266,10 @@ is yours end to end.
260
266
 
261
267
  ## Common Rules
262
268
 
263
- - **Use Korean for the task brief's prose and every user-visible progress or final prose, even when
264
- upstream or historical material is English.** Preserve required structural keys/headings,
265
- model IDs, code, commands, paths, filenames, API names, and original errors verbatim. Do not
266
- create an English or bilingual fallback contract. Hidden reasoning language is not governed.
269
+ - **The task brief and every follow-up instruction from Main are written in English (user decision
270
+ 2026-10-09). Every user-visible progress or final prose you write stays Korean, even though the
271
+ brief is English.** Preserve required structural keys/headings, model IDs, code, commands, paths,
272
+ filenames, API names, and original errors verbatim. Hidden reasoning language is not governed.
267
273
  - The brief's allowed boundary — the projects, paths, and ownership it names — is absolute. The
268
274
  files it points you at are where the investigation starts, not the limit of what you may read
269
275
  or change. Inside that boundary you own the direct cause of the assigned problem even when it
@@ -425,7 +425,7 @@ export function renderCharacterSummonRouting(aliases: CharacterAlias | readonly
425
425
  `[CharacterSummon] 사용자가 ${alias}을(를) 인라인으로 호출했다.`,
426
426
  `먼저 사용자에게 자연스러운 한국어와 ${alias}의 성격·표현으로 ${target.model}을(를) 호출한다고 짧고 자연스럽게 답한다.`,
427
427
  "그 다음 첫 관련 행동은 반드시 task child 호출이어야 한다. Main이 대신 답하거나 다른 도구로 우회하지 않는다.",
428
- "task brief의 산문과 child가 사용자 화면에 표시하는 진행·최종 산문은 모두 한국어다. 선택된 character voice를 그대로 따르며, `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
428
+ "task brief와 후속 지시의 산문은 영어로 쓰고, child가 사용자 화면에 표시하는 진행·최종 산문은 모두 한국어다. 선택된 character voice를 그대로 따르며, `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
429
429
  "command guard가 task brief에 선택된 모델과 아래 voice 계약을 넣는다. child는 첫 provider request 전에 그 모델로 세션 한정 전환하고, 이전 voice block을 제거한 뒤 이 block을 effective system prompt에 정확히 한 번 설치한다.",
430
430
  renderCharacterVoice(alias),
431
431
  "호출된 캐릭터 child의 terminal 발화는 이미 사용자 화면에 표시되는 정본이다. Main은 그 본문을 다시 인용·요약·재집계하지 않고, child가 실패하거나 미완료일 때만 자기 말로 그 상태를 알린다.",
@@ -455,7 +455,7 @@ function renderMultiCharacterSummonRouting(aliases: readonly CharacterAlias[]):
455
455
  : "그 다음 첫 관련 행동은 반드시 task child 호출이어야 한다. Main이 대신 답하거나 다른 도구로 우회하지 않는다.",
456
456
  "한 번의 task 호출의 tasks[]에 tool-capable 캐릭터마다 task 하나씩을 언급 순서대로 넣는다. 각 task 본문에는 아래 exact marker를 그대로 쓴다. marker가 없거나 pending과 다른 alias이면 command guard가 차단한다. 일반 task를 같은 호출에 섞지 않는다.",
457
457
  ...capable.map((alias) => `- ${alias}: \`${summonMarkerFor(alias)}\``),
458
- "task brief의 산문과 child가 사용자 화면에 표시하는 진행·최종 산문은 모두 한국어다. `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
458
+ "task brief와 후속 지시의 산문은 영어로 쓰고, child가 사용자 화면에 표시하는 진행·최종 산문은 모두 한국어다. `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
459
459
  );
460
460
  }
461
461
  for (const alias of webOnly) {
@@ -565,7 +565,7 @@ export function buildCharacterSummonBrief(brief: string, alias: CharacterAlias):
565
565
  ? ""
566
566
  : ` Anthropic OAuth 계정은 stable storage position ${target.oauthPosition}을 먼저 pin한다.`
567
567
  }`,
568
- "- 이 task brief의 산문과 사용자 화면에 표시하는 모든 진행·최종 산문은 한국어다. 선택된 character voice를 그대로 따르며, `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
568
+ "- 이 task brief의 산문은 영어지만, 사용자 화면에 표시하는 모든 진행·최종 산문은 한국어다. 선택된 character voice를 그대로 따르며, `TASK_GUARD`와 필드명, 필요한 heading, 모델 ID, 코드, 명령, 경로, API, 원본 오류는 원문 그대로 둔다.",
569
569
  "- 아래 character voice block을 effective system prompt와 최종 provider payload에 정확히 한 번 설치하고, 이전 character voice block은 제거한다. 고정 catchphrase는 만들지 않는다.",
570
570
  renderCharacterVoice(alias),
571
571
  "- 이 child의 terminal 발화 자체가 사용자 화면에 표시되는 정본이다. Main에게 다시 전달할 요약을 쓰거나 본문 재인용을 요청하지 말고, 원래 과제에 대한 완전한 응답을 직접 끝낸다.",
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { resolve } from "node:path";
3
3
 
4
4
  import { isToolCallEventType, type ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
5
+ import { resolveToCwd } from "@oh-my-pi/pi-coding-agent/tools/path-utils";
5
6
  import {
6
7
  CHARACTER_TARGETS,
7
8
  malformedSummonMarkerReason,
@@ -55,6 +56,20 @@ function eventSessionCwd(ctx: { cwd?: string } | undefined): string {
55
56
  return typeof ctx?.cwd === "string" && ctx.cwd ? resolve(ctx.cwd) : process.cwd();
56
57
  }
57
58
 
59
+ /**
60
+ * bash 도구의 `cwd` 인자를 코어 bash와 같은 규칙으로 푼다. WSL에서 `E:/x`·`E:\x`는 코어가
61
+ * `/mnt/e/x`에서 실행하므로, `path.resolve`로 `<세션 cwd>/E:/x`를 만들면 없는 경로의 저장소를
62
+ * 증명하지 못해 다른 저장소의 git까지 막는다. 첫 판정 전에 명령을 동기적으로 읽어야 하므로 sync다.
63
+ */
64
+ function bashToolCwd(sessionCwd: string, inputCwd: string): string {
65
+ try {
66
+ return resolve(resolveToCwd(inputCwd, sessionCwd));
67
+ } catch {
68
+ // 내부 URL 등 코어가 거절한 cwd는 기존 판정으로 넘긴다.
69
+ return resolve(sessionCwd, inputCwd);
70
+ }
71
+ }
72
+
58
73
  /**
59
74
  * terminal id가 가리키는 현재 미보고 entry key를 await 전에 고정한다. 같은 child 이름을 다시 써도
60
75
  * reportOwnedChildren에 exact key를 넘기므로 먼저 끝난 capture가 새 항목을 소비하지 않는다.
@@ -823,7 +838,7 @@ export default function commandGuard(pi: ExtensionAPI): void {
823
838
 
824
839
  const sessionCwd = eventSessionCwd(ctx);
825
840
  const cwd =
826
- typeof event.input.cwd === "string" ? resolve(sessionCwd, event.input.cwd) : sessionCwd;
841
+ typeof event.input.cwd === "string" ? bashToolCwd(sessionCwd, event.input.cwd) : sessionCwd;
827
842
  const targetDirectories: string[] = [];
828
843
  const collectTarget = (directory: string): boolean => {
829
844
  targetDirectories.push(directory);
@@ -18,13 +18,14 @@ const CMDLETS = new Set(
18
18
  );
19
19
 
20
20
  const GUIDANCE_WINDOWS =
21
- "PowerShell 로직은 `write`로 .ps1 파일을 만든 뒤 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:/절대/슬래시/경로.ps1`로 실행하세요.";
21
+ "PowerShell 로직은 `write`로 .ps1 파일을 만든 뒤 PowerShell 7 `pwsh -NoProfile -ExecutionPolicy Bypass -File C:/절대/슬래시/경로.ps1`로 실행하세요(7이 없을 때만 `powershell.exe`).";
22
22
 
23
23
  // WSL의 bash는 진짜 bash라 따옴표·`\$`가 정상 처리되고, Windows 프로그램에는 Windows 경로를 넘겨야 한다.
24
- // 한글은 BOM 없는 .ps1을 PowerShell 5.1이 CP949로 읽고 stdout도 CP949로 내보내 깨진다(2026-10-07 관측).
24
+ // Linux `pwsh`가 아니라 Windows PowerShell 7(`pwsh.exe`)을 부른다. 5.1은 BOM 없는 .ps1을 CP949로 읽는다.
25
+ // 7도 WSL로 넘기는 stdout은 시스템 코드페이지(CP949)라 한글이 깨진다(2026-10-09 7.6.6에서 관측).
25
26
  const GUIDANCE_LINUX =
26
- "PowerShell 로직은 `write`로 .ps1 파일을 만든 뒤 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"$(wslpath -w /절대/경로.ps1)\"`로 실행하세요. " +
27
- "한글이 있으면 .ps1을 UTF-8 BOM으로 저장하고 첫 줄에 `[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)`를 두세요. " +
27
+ "PowerShell 로직은 `write`로 .ps1 파일을 만든 뒤 Windows PowerShell 7 `pwsh.exe -NoProfile -ExecutionPolicy Bypass -File \"$(wslpath -w /절대/경로.ps1)\"`로 실행하세요(7이 없을 때만 `powershell.exe`). " +
28
+ "한글을 출력하면 .ps1 첫 줄(짧은 `-Command`면 본문 맨 앞)에 `[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)`를 두세요. `powershell.exe`(5.1)로 실행하면 .ps1도 UTF-8 BOM으로 저장해야 합니다. " +
28
29
  "짧은 `-Command` 본문은 작은따옴표로 감싸거나 `\\$`로 이스케이프하면 통과합니다.";
29
30
 
30
31
  function reason(subject: string, platform: NodeJS.Platform): string {
@@ -0,0 +1,226 @@
1
+ // draft의 docx·pdf 변환이 함께 쓰는 Markdown 부분집합 파서.
2
+ // 지원: 제목(#~######), 문단, **굵게**·*기울임*·`코드`·[링크](url), 글머리·번호 목록(들여쓰기 중첩),
3
+ // GFM 표, 펜스 코드 블록, 인용(>), 가로줄. 그 밖의 문법은 글자 그대로 문단에 남는다.
4
+
5
+ export interface Run { text: string; bold?: boolean; italic?: boolean; code?: boolean; href?: string }
6
+ export interface ListItem { level: number; ordered: boolean; runs: Run[] }
7
+ export type Block =
8
+ | { kind: "heading"; level: number; runs: Run[] }
9
+ | { kind: "paragraph"; runs: Run[] }
10
+ | { kind: "list"; items: ListItem[] }
11
+ | { kind: "table"; header: Run[][]; rows: Run[][][] }
12
+ | { kind: "code"; lines: string[] }
13
+ | { kind: "quote"; runs: Run[] }
14
+ | { kind: "rule" };
15
+
16
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
17
+ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
18
+ const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
19
+ const ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
20
+ const TABLE_SEPARATOR = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/;
21
+ const QUOTE = /^ {0,3}>\s?(.*)$/;
22
+ const ESCAPABLE = /[\\`*_[\]#|()~>+\-.!{}]/;
23
+
24
+ function startsBlock(line: string, next: string | undefined): boolean {
25
+ return HEADING.test(line) || FENCE.test(line) || RULE.test(line) || ITEM.test(line) || QUOTE.test(line)
26
+ || (line.includes("|") && next !== undefined && TABLE_SEPARATOR.test(next) && next.includes("-"));
27
+ }
28
+
29
+ /** 인라인 서식을 run 목록으로 바꾼다. 닫는 표시가 없는 `*`·`**`는 글자로 남긴다. */
30
+ export function parseInline(source: string): Run[] {
31
+ const runs: Run[] = [];
32
+ let bold = false;
33
+ let italic = false;
34
+ let buffer = "";
35
+ const flush = () => {
36
+ if (buffer) runs.push({ text: buffer, ...(bold ? { bold } : {}), ...(italic ? { italic } : {}) });
37
+ buffer = "";
38
+ };
39
+ const src = source.replace(/<br\s*\/?>/gi, "\n");
40
+ for (let i = 0; i < src.length;) {
41
+ const ch = src[i]!;
42
+ if (ch === "\\" && i + 1 < src.length && ESCAPABLE.test(src[i + 1]!)) { buffer += src[i + 1]; i += 2; continue; }
43
+ if (ch === "`") {
44
+ const ticks = /^`+/.exec(src.slice(i))![0];
45
+ const end = src.indexOf(ticks, i + ticks.length);
46
+ if (end > 0) {
47
+ flush();
48
+ runs.push({ text: src.slice(i + ticks.length, end).replace(/^ (.+) $/, "$1"), code: true, ...(bold ? { bold } : {}), ...(italic ? { italic } : {}) });
49
+ i = end + ticks.length;
50
+ continue;
51
+ }
52
+ buffer += ticks; i += ticks.length; continue;
53
+ }
54
+ if (ch === "[") {
55
+ const link = /^\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/.exec(src.slice(i));
56
+ if (link) {
57
+ flush();
58
+ for (const run of parseInline(link[1]!)) runs.push({ ...run, ...(bold ? { bold } : {}), ...(italic ? { italic } : {}), href: link[2]! });
59
+ i += link[0].length;
60
+ continue;
61
+ }
62
+ }
63
+ if (src.startsWith("***", i) && ((bold && italic) || src.indexOf("***", i + 3) > 0)) { flush(); bold = !bold; italic = !italic; i += 3; continue; }
64
+ const pair = src.slice(i, i + 2);
65
+ if ((pair === "**" || pair === "__") && (bold || src.indexOf(pair, i + 2) > 0)) { flush(); bold = !bold; i += 2; continue; }
66
+ if (ch === "*" && (italic ? true : src[i + 1] !== " " && src.indexOf("*", i + 1) > 0)) { flush(); italic = !italic; i += 1; continue; }
67
+ buffer += ch;
68
+ i += 1;
69
+ }
70
+ flush();
71
+ return runs;
72
+ }
73
+
74
+ function splitRow(line: string): string[] {
75
+ let body = line.trim();
76
+ if (body.startsWith("|")) body = body.slice(1);
77
+ if (body.endsWith("|") && !body.endsWith("\\|")) body = body.slice(0, -1);
78
+ const cells: string[] = [];
79
+ let current = "";
80
+ let inCode = false;
81
+ for (let i = 0; i < body.length; i++) {
82
+ const ch = body[i]!;
83
+ if (ch === "\\" && body[i + 1] === "|") { current += "|"; i++; continue; }
84
+ if (ch === "`") inCode = !inCode;
85
+ if (ch === "|" && !inCode) { cells.push(current.trim()); current = ""; continue; }
86
+ current += ch;
87
+ }
88
+ cells.push(current.trim());
89
+ return cells;
90
+ }
91
+
92
+ export function parseMarkdown(source: string): Block[] {
93
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
94
+ const blocks: Block[] = [];
95
+ for (let i = 0; i < lines.length;) {
96
+ const line = lines[i]!;
97
+ if (!line.trim()) { i++; continue; }
98
+ const fence = FENCE.exec(line);
99
+ if (fence) {
100
+ const marker = fence[1]!;
101
+ const body: string[] = [];
102
+ for (i++; i < lines.length && !lines[i]!.trimStart().startsWith(marker); i++) body.push(lines[i]!);
103
+ i++; // 닫는 펜스(없으면 문서 끝)
104
+ blocks.push({ kind: "code", lines: body });
105
+ continue;
106
+ }
107
+ const heading = HEADING.exec(line);
108
+ if (heading) { blocks.push({ kind: "heading", level: heading[1]!.length, runs: parseInline(heading[2]!) }); i++; continue; }
109
+ if (RULE.test(line)) { blocks.push({ kind: "rule" }); i++; continue; }
110
+ if (line.includes("|") && i + 1 < lines.length && TABLE_SEPARATOR.test(lines[i + 1]!) && lines[i + 1]!.includes("-")) {
111
+ const header = splitRow(line);
112
+ const rows: Run[][][] = [];
113
+ for (i += 2; i < lines.length && lines[i]!.trim() && lines[i]!.includes("|"); i++) {
114
+ const cells = splitRow(lines[i]!);
115
+ rows.push(header.map((_, index) => parseInline(cells[index] ?? "")));
116
+ }
117
+ blocks.push({ kind: "table", header: header.map(parseInline), rows });
118
+ continue;
119
+ }
120
+ if (QUOTE.test(line)) {
121
+ const body: string[] = [];
122
+ for (; i < lines.length && QUOTE.test(lines[i]!); i++) body.push(QUOTE.exec(lines[i]!)![1]!);
123
+ blocks.push({ kind: "quote", runs: parseInline(body.join("\n").trim()) });
124
+ continue;
125
+ }
126
+ if (ITEM.test(line)) {
127
+ const items: ListItem[] = [];
128
+ const indents: number[] = [];
129
+ for (; i < lines.length;) {
130
+ const current = lines[i]!;
131
+ const item = ITEM.exec(current);
132
+ if (item) {
133
+ const indent = item[1]!.replace(/\t/g, " ").length;
134
+ while (indents.length && indents[indents.length - 1]! > indent) indents.pop();
135
+ if (!indents.length || indents[indents.length - 1]! < indent) indents.push(indent);
136
+ items.push({ level: Math.min(indents.length - 1, 2), ordered: /\d/.test(item[2]!), runs: parseInline(item[3]!.replace(/^\[[ xX]\]\s+/, (box) => (/[xX]/.test(box) ? "☑ " : "☐ "))) });
137
+ i++;
138
+ continue;
139
+ }
140
+ // 들여 쓴 이어지는 줄은 직전 항목에 붙인다. 빈 줄 뒤에 항목이 오면 같은 목록이 이어진다.
141
+ if (current.trim() && /^\s+/.test(current) && items.length && !startsBlock(current.trim(), lines[i + 1])) {
142
+ const last = items[items.length - 1]!;
143
+ last.runs = [...last.runs, { text: " " }, ...parseInline(current.trim())];
144
+ i++;
145
+ continue;
146
+ }
147
+ if (!current.trim() && i + 1 < lines.length && ITEM.test(lines[i + 1]!)) { i++; continue; }
148
+ break;
149
+ }
150
+ blocks.push({ kind: "list", items });
151
+ continue;
152
+ }
153
+ const body: string[] = [line.trim()];
154
+ for (i++; i < lines.length && lines[i]!.trim() && !startsBlock(lines[i]!, lines[i + 1]); i++) body.push(lines[i]!.trim());
155
+ blocks.push({ kind: "paragraph", runs: parseInline(body.join(" ")) });
156
+ }
157
+ return blocks;
158
+ }
159
+
160
+ export function plainText(runs: readonly Run[]): string { return runs.map((run) => run.text).join(""); }
161
+
162
+ /** 결과 보고용 개요: 제목 목록, 제목이 없으면 앞 문단 몇 개. */
163
+ export function outline(blocks: readonly Block[], limit = 12): string[] {
164
+ const headings = blocks.filter((block): block is Extract<Block, { kind: "heading" }> => block.kind === "heading")
165
+ .map((block) => `${"#".repeat(block.level)} ${plainText(block.runs)}`);
166
+ if (headings.length) return headings.length > limit ? [...headings.slice(0, limit), `… 제목 ${headings.length - limit}개 더`] : headings;
167
+ return blocks.filter((block): block is Extract<Block, { kind: "paragraph" }> => block.kind === "paragraph")
168
+ .slice(0, 3).map((block) => plainText(block.runs).slice(0, 120));
169
+ }
170
+
171
+ function escapeHtml(text: string): string {
172
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
173
+ }
174
+
175
+ function htmlRuns(runs: readonly Run[]): string {
176
+ return runs.map((run) => {
177
+ let html = escapeHtml(run.text).replace(/\n/g, "<br>");
178
+ if (run.code) html = `<code>${html}</code>`;
179
+ if (run.italic) html = `<em>${html}</em>`;
180
+ if (run.bold) html = `<strong>${html}</strong>`;
181
+ if (run.href) html = `<a href="${escapeHtml(run.href)}">${html}</a>`;
182
+ return html;
183
+ }).join("");
184
+ }
185
+
186
+ function htmlList(items: readonly ListItem[]): string {
187
+ let html = "";
188
+ const open: string[] = [];
189
+ let depth = -1;
190
+ for (const item of items) {
191
+ const tag = item.ordered ? "ol" : "ul";
192
+ while (depth > item.level) { html += `</li></${open.pop()}>`; depth--; }
193
+ if (depth === item.level && open[open.length - 1] !== tag) { html += `</li></${open.pop()}>`; depth--; }
194
+ if (depth === item.level) html += "</li>";
195
+ while (depth < item.level) { html += `<${tag}>`; open.push(tag); depth++; if (depth < item.level) html += "<li>"; }
196
+ html += `<li>${htmlRuns(item.runs)}`;
197
+ }
198
+ while (open.length) html += `</li></${open.pop()}>`;
199
+ return html;
200
+ }
201
+
202
+ const CSS = `@page { size: A4; margin: 18mm 16mm; }
203
+ body { font-family: "Malgun Gothic", "맑은 고딕", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif; font-size: 10.5pt; line-height: 1.6; color: #111; }
204
+ h1 { font-size: 20pt; border-bottom: 1px solid #999; padding-bottom: 4px; } h2 { font-size: 15pt; } h3 { font-size: 12.5pt; } h4, h5, h6 { font-size: 11pt; }
205
+ h1, h2, h3, h4, h5, h6 { margin: 1.1em 0 0.4em; page-break-after: avoid; }
206
+ table { border-collapse: collapse; width: 100%; margin: 0.6em 0; page-break-inside: auto; } tr { page-break-inside: avoid; }
207
+ th, td { border: 1px solid #888; padding: 4px 6px; text-align: left; vertical-align: top; } th { background: #eee; }
208
+ pre { background: #f4f4f4; padding: 8px; white-space: pre-wrap; word-break: break-all; font-size: 9pt; }
209
+ code { font-family: Consolas, "D2Coding", monospace; } blockquote { margin: 0.6em 0; padding-left: 10px; border-left: 3px solid #bbb; color: #444; }
210
+ hr { border: 0; border-top: 1px solid #aaa; }`;
211
+
212
+ export function toHtml(blocks: readonly Block[], title: string): string {
213
+ const body = blocks.map((block) => {
214
+ switch (block.kind) {
215
+ case "heading": return `<h${block.level}>${htmlRuns(block.runs)}</h${block.level}>`;
216
+ case "paragraph": return `<p>${htmlRuns(block.runs)}</p>`;
217
+ case "list": return htmlList(block.items);
218
+ case "table": return `<table><thead><tr>${block.header.map((cell) => `<th>${htmlRuns(cell)}</th>`).join("")}</tr></thead><tbody>${
219
+ block.rows.map((row) => `<tr>${row.map((cell) => `<td>${htmlRuns(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
220
+ case "code": return `<pre><code>${escapeHtml(block.lines.join("\n"))}</code></pre>`;
221
+ case "quote": return `<blockquote>${htmlRuns(block.runs)}</blockquote>`;
222
+ case "rule": return "<hr>";
223
+ }
224
+ }).join("\n");
225
+ return `<!doctype html>\n<html lang="ko"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>${CSS}</style></head><body>\n${body}\n</body></html>\n`;
226
+ }