cuelo 0.10.95 → 0.10.97

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 (85) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +32 -32
  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/apply/route.js +2 -2
  26. package/.next/server/app/api/updates/route.js +1 -1
  27. package/.next/server/app/index.html +1 -1
  28. package/.next/server/app/index.rsc +4 -4
  29. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  30. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  31. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  32. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  33. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/.next/server/app/page.js +2 -2
  35. package/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
  37. package/.next/server/app/recover.html +1 -1
  38. package/.next/server/app/recover.rsc +2 -2
  39. package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
  40. package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
  41. package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
  42. package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
  43. package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
  44. package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
  45. package/.next/server/app-paths-manifest.json +32 -32
  46. package/.next/server/middleware-build-manifest.js +1 -1
  47. package/.next/server/pages/404.html +1 -1
  48. package/.next/server/pages/500.html +1 -1
  49. package/.next/server/server-reference-manifest.json +1 -1
  50. package/.next/static/chunks/app/{layout-cf209ec1ffc50146.js → layout-e946c4e8f6b1554d.js} +1 -1
  51. package/.next/static/chunks/app/{page-248f0f6ee3f5b6ee.js → page-0b878506cec78452.js} +2 -2
  52. package/.next/trace +108 -108
  53. package/.next/trace-build +1 -1
  54. package/README.md +1 -9
  55. package/Tools/CUELO_Setup/files/native-runtime-patch.js +3 -3
  56. package/Tools/OMP_Global_Config/agent/agents/maker.md +3 -3
  57. package/Tools/OMP_Global_Config/agent/extensions/command-guard/index.ts +174 -31
  58. package/Tools/OMP_Global_Config/agent/extensions/command-guard/matcher.ts +417 -54
  59. package/Tools/OMP_Global_Config/agent/extensions/command-guard/task-guard.ts +206 -48
  60. package/Tools/OMP_Global_Config/agent/extensions/cross-review.ts +1 -1
  61. package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +39 -59
  62. package/Tools/OMP_Global_Config/agent/extensions/lib/cross-review.ts +214 -43
  63. package/Tools/OMP_Global_Config/agent/extensions/lib/draft-coverage.ts +7 -2
  64. package/Tools/OMP_Global_Config/agent/extensions/lib/job-observation.ts +6 -5
  65. package/Tools/OMP_Global_Config/agent/extensions/lib/maker-routing.ts +35 -7
  66. package/Tools/OMP_Global_Config/agent/extensions/lib/prepared-task.ts +7 -5
  67. package/Tools/OMP_Global_Config/agent/extensions/lib/slop-gate.ts +31 -6
  68. package/Tools/OMP_Global_Config/agent/extensions/report-chart-guard.ts +21 -4
  69. package/Tools/OMP_Global_Config/agent/extensions/slop-gate/review.ts +67 -16
  70. package/Tools/OMP_Global_Config/agent/extensions/slop-gate.ts +2 -2
  71. package/Tools/OMP_Global_Config/agent/extensions/todo-nudge.ts +14 -4
  72. package/Tools/OMP_Global_Config/agent/rules/deploy-ops.md +1 -0
  73. package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +14 -10
  74. package/Tools/OMP_Global_Config/agent/rules/subagent.md +17 -12
  75. package/Tools/OMP_Global_Config/agent/rules/task-guard.md +5 -3
  76. package/Tools/OMP_Global_Config/agent/sop/_writer.md +1 -1
  77. package/Tools/OMP_Global_Config/agent/sop/maker.md +1 -1
  78. package/Tools/OMP_Global_Config/agent/tools/ci-chain/index.ts +166 -4
  79. package/Tools/OMP_Global_Config/agent/tools/ci-chain/paths-filter.ts +130 -0
  80. package/Tools/OMP_Global_Config/agent/tools/git-finalizer/index.ts +20 -1
  81. package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +176 -0
  82. package/bin/self-update.js +2 -1
  83. package/package.json +7 -7
  84. /package/.next/static/{njFeZPW-f3RTy2-iTWbzY → VSLxmLHaJ0rBK71SMfdbr}/_buildManifest.js +0 -0
  85. /package/.next/static/{njFeZPW-f3RTy2-iTWbzY → VSLxmLHaJ0rBK71SMfdbr}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-webpack","duration":55665409,"timestamp":290849,"id":14,"parentId":1,"tags":{},"startTime":1793124353003,"traceId":"3a5092aa09192926"},{"name":"run-typescript","duration":10635118,"timestamp":56273025,"id":11054,"parentId":1,"tags":{},"startTime":1793124408985,"traceId":"3a5092aa09192926"},{"name":"static-check","duration":2015854,"timestamp":66965888,"id":11057,"parentId":1,"tags":{},"startTime":1793124419678,"traceId":"3a5092aa09192926"},{"name":"static-generation","duration":4211742,"timestamp":69049018,"id":11231,"parentId":1,"tags":{},"startTime":1793124421761,"traceId":"3a5092aa09192926"},{"name":"collect-build-traces","duration":11429045,"timestamp":69035937,"id":11228,"parentId":1,"tags":{},"startTime":1793124421748,"traceId":"3a5092aa09192926"},{"name":"telemetry-flush","duration":40,"timestamp":80467627,"id":11240,"parentId":1,"tags":{},"startTime":1793124433180,"traceId":"3a5092aa09192926"},{"name":"next-build","duration":80226568,"timestamp":241114,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1793124352953,"traceId":"3a5092aa09192926"}]
1
+ [{"name":"run-webpack","duration":89357769,"timestamp":485993,"id":14,"parentId":1,"tags":{},"startTime":1793151123067,"traceId":"059273d5d8eacd56"},{"name":"run-typescript","duration":18835781,"timestamp":90184364,"id":11054,"parentId":1,"tags":{},"startTime":1793151212765,"traceId":"059273d5d8eacd56"},{"name":"static-check","duration":3380120,"timestamp":109097788,"id":11057,"parentId":1,"tags":{},"startTime":1793151231678,"traceId":"059273d5d8eacd56"},{"name":"static-generation","duration":6490073,"timestamp":112569019,"id":11231,"parentId":1,"tags":{},"startTime":1793151235150,"traceId":"059273d5d8eacd56"},{"name":"collect-build-traces","duration":19257460,"timestamp":112546107,"id":11228,"parentId":1,"tags":{},"startTime":1793151235127,"traceId":"059273d5d8eacd56"},{"name":"telemetry-flush","duration":69,"timestamp":131808185,"id":11240,"parentId":1,"tags":{},"startTime":1793151254389,"traceId":"059273d5d8eacd56"},{"name":"next-build","duration":131402761,"timestamp":405526,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1793151122986,"traceId":"059273d5d8eacd56"}]
package/README.md CHANGED
@@ -161,7 +161,7 @@ Requires Node.js 22.19+ and Bun 1.4.2+. Open http://127.0.0.1:30141 and sign in
161
161
 
162
162
  ## 하네스 세부 동작
163
163
 
164
- 내장 OMP 코어는 **18.13.1**입니다. 별도로 설치한 `omp` CLI의 버전은 웹 업데이트와 독립적이며, 앱에 포함된 CLI를 `omp` 명령으로 사용하는 설치에서는 웹과 같은 코어를 사용합니다.
164
+ 내장 OMP 코어는 **18.13.2**입니다. 별도로 설치한 `omp` CLI의 버전은 웹 업데이트와 독립적이며, 앱에 포함된 CLI를 `omp` 명령으로 사용하는 설치에서는 웹과 같은 코어를 사용합니다.
165
165
 
166
166
  문서 하네스는 기존 Word·Excel·PowerPoint 문서를 열고 편집하거나 변환할 때 LibreOffice를 우선 사용합니다. 원본은 보존하고 출력 결과를 확인하며, 매크로·전용 기능·서식 호환성이 필요하거나 사용자가 지정하면 MS Office를 사용합니다.
167
167
 
@@ -277,14 +277,6 @@ Windows의 WSL Ubuntu에서 실행 중인 CUELO를 갱신하는 동안에는 WSL
277
277
  WSL 갱신에서는 시스템 시각이 보정되어도 실행 중인 서버를 같은 서버로 인식해, 진행 중인 대화의 중단·재개 절차를 건너뛰지 않습니다.
278
278
  WSL 갱신은 실행 중인 서버와 분리해 새 버전을 미리 빌드한 뒤 교체합니다. 빌드 캐시는 재사용하고 다시 만들 산출물은 복사하지 않아 준비 과정의 디스크 쓰기를 줄입니다.
279
279
 
280
- 기존 프로필의 하네스·역할 설정은 앱 업데이트만으로 자동 덮어쓰지 않습니다. 새 버전의 하네스는 `node install.mjs harness-diff`로 차이를 보고 `node install.mjs harness-update`로 반영합니다. 사용자가 고치지 않은 파일만 백업 후 바꾸고, 고친 파일과 `config.yml`은 그대로 둡니다([기존 프로필의 하네스 갱신](./docs/installation.md#기존-프로필의-하네스-갱신)). 반영 후 CUELO와 `omp`를 다시 시작하세요.
281
-
282
- ```bash
283
- git pull --ff-only
284
- node install.mjs setup
285
- node install.mjs start
286
- ```
287
-
288
280
  공개 웹 소스 릴리스와 변경 내용은 [GitHub Releases](https://github.com/gim47656-ship-it/CUELO/releases)에서 확인하세요. 새 `package.json` 버전이 공개 `main`에 올라가 CI가 성공하면 GitHub 릴리스와 npm `cuelo` 게시가 자동으로 실행됩니다([릴리스 절차](./docs/release.md)).
289
281
 
290
282
  ### 사이드카 서비스
@@ -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.95',
12
+ version: '0.10.97',
13
13
  distribution: 'cuelo',
14
- coreVersion: '18.13.1',
14
+ coreVersion: '18.13.2',
15
15
  };
16
16
 
17
17
  // OMP core `src/sdk.ts`: session-scoped AsyncJobManager for a process that hosts
@@ -88,7 +88,7 @@ function readNativePackage(target) {
88
88
  throw new Error(`unexpected native package ${packageJson.name}@${packageJson.version}`);
89
89
  }
90
90
  if (!build || build.distribution !== EXPECTED.distribution || build.coreVersion !== EXPECTED.coreVersion) {
91
- throw new Error('native cueloBuild metadata does not match cuelo / core 18.13.1');
91
+ throw new Error('native cueloBuild metadata does not match cuelo / core 18.13.2');
92
92
  }
93
93
  return packageJson;
94
94
  }
@@ -6,7 +6,7 @@ thinking-level: auto
6
6
  tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, computer_task, ego_browser, checkpoint, cross_review, slop_check]
7
7
  ---
8
8
 
9
- <!-- omp-global-config:generated source-hash=27baced8df24
9
+ <!-- omp-global-config:generated source-hash=9b754355fbb0
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
@@ -19,7 +19,7 @@ Every outward-facing artifact you write (README/CHANGELOG/docs and other markdow
19
19
 
20
20
  Draft coverage is per authored block, not per tool invocation. A draft for introductory paragraphs does not cover new explanations, table copy or slide prose you write yourself. Draft every new or paragraph-rewritten block before applying it; splitting edits or minifying HTML is not an exemption. Direct edits remain allowed for exact factual/typo/slop corrections and layout/code changes preserving the copy. New explanations, examples and conclusions are not corrections. Compare the prose being applied with the draft and draft the missing blocks only; a slop pass does not prove draft coverage.
21
21
 
22
- Run a `cross_review` `review` of your changed code files before you report when the review is required: the brief marks the slice high-risk or carries a plain cross-family review line, a trigger marker the `git_finalize` gate uses hits your changed lines or paths (process termination, deletion, destructive SQL, deploy/release/migration, credentials/auth, encoding, device I/O), or Main forwards a finalizer sample hit for your files. Other code changes are reviewed only when Main's brief asks for it; Main decides from the `maker_route` review-necessity advice. Targets are changed executable code and config; pass relevant unchanged callers or contracts as `inputs`/`references`, not as targets. Docs, tests, generated files, git-detected binary media/office assets and TS/JS edits the gate proves comment-only are not targets. Run it after your focused checks pass, with your own model selector as `authors` (never `"main"`), fix or refute each finding with evidence, use the probe round when a reviewer proposes one, and run a delta round on the same chain after fixes. Report the reviewId chain and a proposed status (`fixed`/`refuted`/`limit`) with an evidence locator for every finding; `resolve` is Main-only. Main does not start a duplicate review of your code: when Main forwards findings or later rounds, you run them.
22
+ Run a `cross_review` `review` of your changed code files before you report when the review is required: the brief marks the slice high-risk or carries a plain cross-family review line, a trigger marker the `git_finalize` gate uses hits your changed lines (a path name alone never triggers; process termination, deletion, destructive SQL, deploy/release/migration, credentials/auth, encoding, device I/O), or Main forwards a finalizer sample hit for your files. Other code changes are reviewed only when Main's brief asks for it; Main decides from the `maker_route` review-necessity advice. Targets are changed executable code and config; pass relevant unchanged callers or contracts as `inputs`/`references`, not as targets. Docs, tests, generated files, git-detected binary media/office assets and TS/JS edits the gate proves comment-only are not targets. Run it after your focused checks pass, with your own model selector as `authors` (never `"main"`), fix or refute each finding with evidence, use the probe round when a reviewer proposes one, and run a delta round on the same chain after fixes. Report the reviewId chain and a proposed status (`fixed`/`refuted`/`limit`) with an evidence locator for every finding; `resolve` is Main-only. Main does not start a duplicate review of your code: when Main forwards findings or later rounds, you run them.
23
23
 
24
24
  ## Maker Contract
25
25
 
@@ -159,7 +159,7 @@ Run a `cross_review` `review` of your changed code files before you report when
159
159
  - Run `cross_review` `action:"review"` yourself after your own focused checks pass and before your
160
160
  completion report when the review is required for your slice: the brief's `# Acceptance` asks
161
161
  for a cross-family review (Main asks for declared high-risk slices: code-nature triggers or
162
- project material risk), a `git_finalize` trigger marker hits your changed lines or paths, or
162
+ project material risk), a `git_finalize` trigger marker hits your changed lines (a path name alone never triggers), or
163
163
  Main forwards a finalizer sample hit for your files. Any other code change is reviewed only when
164
164
  the brief asks; Main decides that from the `maker_route` review-necessity advice, and a Maker
165
165
  never starts a blanket review of every edit. `files` are your changed executable code and config
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
- import { appendFileSync, existsSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { appendFileSync, existsSync, realpathSync, statSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
 
6
6
  import { isToolCallEventType, type ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
7
7
  import { resolveToCwd } from "@oh-my-pi/pi-coding-agent/tools/path-utils";
@@ -18,7 +18,7 @@ import { resolvePreparedTaskInput } from "../lib/prepared-task";
18
18
  import { onSessionLifecycle } from "../lib/session-identity";
19
19
  import { selectGitExecutor, type GitExecutor } from "../../tools/git-finalizer/wsl-git";
20
20
 
21
- import { matchBlockedCommand } from "./matcher";
21
+ import { isNotRepositoryDiagnostic, matchBlockedCommand } from "./matcher";
22
22
  import { interopHint, windowsOutputCommand } from "./interop-hint";
23
23
  import { matchPowerShellSyntax } from "./powershell-syntax";
24
24
  import {
@@ -34,11 +34,14 @@ import {
34
34
  hasUnreportedChildren,
35
35
  matchBlockedEvalModelBridge,
36
36
  noteUserRedirect,
37
+ noteUserRequestText,
37
38
  readCancelledChildIds,
38
39
  readSettledTaskIds,
39
40
  readSpawnProgress,
40
41
  registerSpawnedMakers,
41
42
  releaseCancelledSpawns,
43
+ noteOwnershipWrites,
44
+ OWNERSHIP_MAIN_WRITER,
42
45
  reportOwnedChildren,
43
46
  reserveTaskCall,
44
47
  reservedMakers,
@@ -263,10 +266,14 @@ function cachedCommonDirectory(directory: string): Promise<string | undefined> {
263
266
  return pending;
264
267
  }
265
268
 
269
+ /**
270
+ * 대상 디렉터리가 세션 저장소인지 가르는 판정과, 그 판정이 증명하지 못해 차단으로 기운 대상(세션이나 대상의 common-dir을
271
+ * 얻지 못함). 후자만 outsideRepositoryMatcher가 "어느 저장소에도 속하지 않음"을 따로 확인한다.
272
+ */
266
273
  async function repositoryMatcher(
267
274
  sessionCwd: string,
268
275
  targetDirectories: readonly string[],
269
- ): Promise<(directory: string) => boolean> {
276
+ ): Promise<{ isSessionRepository: (directory: string) => boolean; unproven: string[] }> {
270
277
  const targets = new Map<string, string>();
271
278
  for (const directory of targetDirectories) {
272
279
  const normalized = normalizeRepositoryPath(directory);
@@ -282,7 +289,10 @@ async function repositoryMatcher(
282
289
  const resolvedTargets = new Map(
283
290
  targetEntries.map(([normalized], index) => [normalized, targetCommonDirectories[index]]),
284
291
  );
285
- return (directory: string): boolean => {
292
+ const unproven = targetEntries.flatMap(([, directory], index) =>
293
+ !sessionCommonDirectory || !targetCommonDirectories[index] ? [directory] : [],
294
+ );
295
+ const isSessionRepository = (directory: string): boolean => {
286
296
  if (!directory) return true;
287
297
  const normalized = normalizeRepositoryPath(directory);
288
298
  // 1차 target 수집 뒤 입력이 달라져 미리 확인하지 않은 directory가 나타나도 허용하지 않는다.
@@ -295,6 +305,7 @@ async function repositoryMatcher(
295
305
  commonDirectory === sessionCommonDirectory
296
306
  );
297
307
  };
308
+ return { isSessionRepository, unproven };
298
309
  }
299
310
 
300
311
  /**
@@ -311,6 +322,107 @@ async function windowsGitMatcher(targetDirectories: readonly string[]): Promise<
311
322
  return (directory) => verdicts.get(directory) === true;
312
323
  }
313
324
 
325
+ /** 아직 없을 수 있는 경로의 가장 가까운 기존 조상 폴더(자기 자신 포함). 없으면 undefined. */
326
+ function nearestExistingDirectory(path: string): string | undefined {
327
+ let current = resolve(path);
328
+ for (;;) {
329
+ try {
330
+ if (statSync(current).isDirectory()) return current;
331
+ } catch {
332
+ // 없는 경로는 조상으로 올라간다.
333
+ }
334
+ const parent = dirname(current);
335
+ if (parent === current) return undefined;
336
+ current = parent;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * 대상 디렉터리(아직 없을 수 있다)가 git이 찾는 어느 저장소에도 속하지 않는지. 가장 가까운 기존 조상에서 git 자신의 저장소
342
+ * 탐색을 돌려 "not a git repository"로 끝날 때만 참이다. git 실행 실패·시간 초과·다른 오류는 증명이 아니므로 거짓(차단)이다.
343
+ * 같은 탐색을 실제 명령의 git도 하므로, 참이면 그 위치의 git add·commit은 세션 저장소에 닿지 못한다.
344
+ */
345
+ async function outsideRepositoryMatcher(targetDirectories: readonly string[]): Promise<(directory: string) => boolean> {
346
+ const verdicts = new Map<string, boolean>();
347
+ await Promise.all(
348
+ [...new Set(targetDirectories)].map(async (directory) => {
349
+ const ancestor = nearestExistingDirectory(directory);
350
+ if (!ancestor) return;
351
+ const git = await selectGitExecutor(ancestor);
352
+ const { promise, resolve: settle } = Promise.withResolvers<boolean>();
353
+ execFile(
354
+ git.command,
355
+ ["rev-parse", "--git-dir"],
356
+ {
357
+ cwd: ancestor,
358
+ encoding: "utf8",
359
+ windowsHide: true,
360
+ timeout: GIT_SPAWN_TIMEOUT_MS,
361
+ env: { ...process.env, LC_ALL: "C", LANG: "C", LANGUAGE: "C" },
362
+ },
363
+ (error, _stdout, stderr) => settle(Boolean(error) && isNotRepositoryDiagnostic(String(stderr))),
364
+ );
365
+ verdicts.set(directory, await promise);
366
+ }),
367
+ );
368
+ return (directory) => verdicts.get(directory) === true;
369
+ }
370
+
371
+ /**
372
+ * 대상 디렉터리(아직 없을 수 있다)의 실제 경로가 임시 루트(os.tmpdir()·/tmp) 아래이고 세션 cwd·세션 작업 트리와 겹치지 않는지.
373
+ * 기존 조상의 realpath로 보므로 /tmp 아래 심볼릭 링크가 세션 저장소를 가리키면 임시로 보지 않는다. 세션 cwd가 저장소의
374
+ * 하위 폴더일 때 같은 작업 트리의 다른 폴더를 임시로 보지 않도록 작업 트리 root(`sessionRoot`)도 함께 지킨다. root를
375
+ * 알아내지 못했으면(`undefined`) 무엇도 임시로 보지 않는다. `null`은 세션이 어느 저장소에도 속하지 않음이 증명된 경우다.
376
+ */
377
+ function temporaryDirectoryMatcher(sessionCwd: string, sessionRoot: string | null | undefined): (directory: string) => boolean {
378
+ if (sessionRoot === undefined) return () => false;
379
+ const real = (path: string): string | undefined => {
380
+ const ancestor = nearestExistingDirectory(path);
381
+ if (!ancestor) return undefined;
382
+ try {
383
+ return join(realpathSync(ancestor), relative(ancestor, resolve(path)));
384
+ } catch {
385
+ return undefined;
386
+ }
387
+ };
388
+ const roots = [...new Set([tmpdir(), "/tmp"].flatMap((root) => (existsSync(root) ? [real(root)] : [])))].filter(
389
+ (root): root is string => root !== undefined,
390
+ );
391
+ const protectedPaths = [sessionCwd, ...(sessionRoot ? [sessionRoot] : [])].map((path) => real(path) ?? resolve(path));
392
+ const within = (path: string, base: string) => path === base || path.startsWith(`${base}${sep}`);
393
+ return (directory) => {
394
+ const path = real(directory);
395
+ if (!path || protectedPaths.some((session) => within(path, session) || within(session, path))) return false;
396
+ return roots.some((root) => path.startsWith(`${root}${sep}`));
397
+ };
398
+ }
399
+
400
+ /**
401
+ * 세션 cwd가 속한 작업 트리의 root. git이 "not a git repository"로 끝나면 `null`(지킬 작업 트리 없음), 그 밖의 실패·
402
+ * 시간 초과·work tree 아님은 `undefined`(알 수 없음: 임시 면제를 쓰지 않는다)다.
403
+ */
404
+ async function sessionWorktreeRoot(sessionCwd: string): Promise<string | null | undefined> {
405
+ const git = await selectGitExecutor(sessionCwd);
406
+ const { promise, resolve: settle } = Promise.withResolvers<string | null | undefined>();
407
+ execFile(
408
+ git.command,
409
+ ["rev-parse", "--show-toplevel"],
410
+ {
411
+ cwd: sessionCwd,
412
+ encoding: "utf8",
413
+ windowsHide: true,
414
+ timeout: GIT_SPAWN_TIMEOUT_MS,
415
+ env: { ...process.env, LC_ALL: "C", LANG: "C", LANGUAGE: "C" },
416
+ },
417
+ (error, stdout, stderr) => {
418
+ if (error) return settle(isNotRepositoryDiagnostic(String(stderr)) ? null : undefined);
419
+ const root = String(stdout).trim();
420
+ settle(root && isAbsolute(root) ? root : undefined);
421
+ },
422
+ );
423
+ return promise;
424
+ }
425
+
314
426
  /**
315
427
  * git 호출 하나의 상한. 각 child callback을 기다리는 extension handler의 상한은 기본 30초이므로
316
428
  * 세 순차 호출의 최악값을 그 아래로 둔다. 비동기 child를 써 대기 중 JS event loop는 계속 돈다.
@@ -749,6 +861,7 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
749
861
  },
750
862
  state: writers,
751
863
  running: () => readRunningTaskJobs(ownerCtx),
864
+ noteWrite: (writer, paths) => noteOwnershipWrites(ownership, writer, paths),
752
865
  };
753
866
  const bindWriterView = (ctx: { cwd?: string; agent?: { id?: string } }): void => {
754
867
  ownerCtx = ctx;
@@ -775,10 +888,28 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
775
888
  }
776
889
  };
777
890
 
891
+ /** edit·write 대상 중 base 아래 작업 트리 파일을 base 기준 상대경로로. 내부 URL·base 밖 경로는 뺀다. */
892
+ const relativeWriteTargets = (input: unknown, cwd: string, base: string): string[] => {
893
+ const targets: string[] = [];
894
+ for (const path of writeTargets(input)) {
895
+ let absolute: string;
896
+ try {
897
+ absolute = resolveToCwd(path, cwd);
898
+ } catch {
899
+ continue; // agent://·proc:// 같은 내부 URL은 작업 트리 파일이 아니다.
900
+ }
901
+ const rel = relative(base, absolute);
902
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
903
+ targets.push(rel);
904
+ }
905
+ return targets;
906
+ };
907
+
778
908
  /**
779
909
  * 공유 작업공간 Maker child의 edit·write가 같은 부모의 다른 활성 Maker 소유 경로를 건드리면 막을 이유를 돌린다.
780
910
  * Main·Maker가 아닌 agent·격리 child(부모와 cwd가 다름)는 대상이 아니고, 부모 view나 job snapshot이 없어 소유자를
781
911
  * 확정하지 못하면 debug 기록만 남기고 막지 않는다. bash 쓰기는 대상 경로를 믿을 만하게 풀 수 없어 보지 않는다.
912
+ * 막지 않은 쓰기는 부모의 OwnershipGuard 귀속 기록에 남긴다(Main 자신의 쓰기는 이 세션의 기록에 남긴다).
782
913
  */
783
914
  const ownerWriteBlockReason = (
784
915
  toolName: string,
@@ -786,11 +917,13 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
786
917
  ctx: { cwd?: string; agent?: { kind?: string; id?: string; name?: string; parentId?: string } },
787
918
  ): string | undefined => {
788
919
  const agent = ctx.agent;
789
- if (!agent) {
790
- pi.logger?.debug("command-guard: OwnerWriteGuard 판정 생략(ctx.agent 없음)");
920
+ if (!agent) pi.logger?.debug("command-guard: OwnerWriteGuard 판정 생략(ctx.agent 없음)");
921
+ if (agent?.kind !== "sub") {
922
+ const cwd = eventSessionCwd(ctx);
923
+ noteOwnershipWrites(ownership, OWNERSHIP_MAIN_WRITER, relativeWriteTargets(input, cwd, cwd));
791
924
  return undefined;
792
925
  }
793
- if (agent.kind !== "sub" || agent.name !== "maker") return undefined;
926
+ if (agent.name !== "maker") return undefined;
794
927
  const skip = (why: string, detail?: Record<string, unknown>): undefined => {
795
928
  pi.logger?.debug(`command-guard: OwnerWriteGuard 판정 생략(${why})`, { agentId: agent.id, ...detail });
796
929
  return undefined;
@@ -804,29 +937,21 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
804
937
  if (!view.cwd || normalizeRepositoryPath(cwd) !== normalizeRepositoryPath(view.cwd)) {
805
938
  return skip("공유 checkout 아님", { cwd, parentCwd: view.cwd });
806
939
  }
807
- const targets: string[] = [];
808
- for (const path of writeTargets(input)) {
809
- let absolute: string;
810
- try {
811
- absolute = resolveToCwd(path, cwd);
812
- } catch {
813
- continue; // agent://·proc:// 같은 내부 URL은 작업 트리 파일이 아니다.
814
- }
815
- const rel = relative(view.cwd, absolute);
816
- if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
817
- targets.push(rel);
818
- }
940
+ const targets = relativeWriteTargets(input, cwd, view.cwd);
819
941
  if (targets.length === 0) return undefined;
820
942
  const decision = decideOwnerWrite(view, selfId, targets);
943
+ if (decision?.conflict) {
944
+ const { owner, path } = decision.conflict;
945
+ return (
946
+ `[OwnerWriteGuard] ${path}은(는) 지금 실행 중인 다른 Maker ${owner}의 OWNED_PATHS 안이라 ${toolName}을 막았습니다. ` +
947
+ `직접 고치지 말고 필요한 변경·이유·근거 위치를 write agent://${parentId}로 Main에 보고하세요. ` +
948
+ "자기 OWNED_PATHS 안이나 다른 실행 중 Maker가 소유하지 않은 경로는 그대로 쓸 수 있습니다."
949
+ );
950
+ }
951
+ view.noteWrite?.(selfId, targets);
821
952
  if (!decision) return skip("부모 job snapshot 없음", { parentId });
822
953
  if (decision.unknown.length > 0) skip("소유 주장 없는 실행 중 child", { unknown: decision.unknown });
823
- if (!decision.conflict) return undefined;
824
- const { owner, path } = decision.conflict;
825
- return (
826
- `[OwnerWriteGuard] ${path}은(는) 지금 실행 중인 다른 Maker ${owner}의 OWNED_PATHS 안이라 ${toolName}을 막았습니다. ` +
827
- `직접 고치지 말고 필요한 변경·이유·근거 위치를 write agent://${parentId}로 Main에 보고하세요. ` +
828
- "자기 OWNED_PATHS 안이나 다른 실행 중 Maker가 소유하지 않은 경로는 그대로 쓸 수 있습니다."
829
- );
954
+ return undefined;
830
955
  };
831
956
 
832
957
  /**
@@ -933,6 +1058,7 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
933
1058
  bindSession(ctx);
934
1059
  ownershipGeneration += 1;
935
1060
  resetTaskGuardState(taskGuard);
1061
+ noteUserRequestText(taskGuard, event.text ?? "");
936
1062
  resetOwnershipState(ownership);
937
1063
  summonCalls.clear();
938
1064
  const intent = parseCharacterIntent(event.text);
@@ -997,6 +1123,7 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
997
1123
  const intent = parseCharacterIntent(messageText);
998
1124
  pendingSummons = intent?.kind === "summon" ? intent.aliases : [];
999
1125
  noteUserRedirect(taskGuard);
1126
+ noteUserRequestText(taskGuard, messageText);
1000
1127
  });
1001
1128
 
1002
1129
  pi.on("tool_call", async (event, ctx) => {
@@ -1156,10 +1283,14 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
1156
1283
  typeof event.input.cwd === "string" ? bashToolCwd(sessionCwd, event.input.cwd) : sessionCwd;
1157
1284
  const targetDirectories: string[] = [];
1158
1285
  const windowsGitTargets: string[] = [];
1286
+ let temporaryTargets = false;
1287
+ const temporaryRoot = tmpdir();
1159
1288
  // 1차는 대상만 모은다. 저장소 콜백이 true(차단)를 돌려주면 그 세그먼트에서 판정이 끝나 같은 git의
1160
1289
  // Windows git 판정과 뒤 세그먼트의 대상을 놓친다. 그래서 둘 다 false로 끝까지 훑고 2차에서 실제 판정을 쓴다.
1290
+ // 임시 작업 트리 면제도 1차에서는 참(면제)으로 두고 끝까지 훑은 뒤 2차에서 실제 경로로 판정한다.
1161
1291
  const preliminaryReason = matchBlockedCommand(event.input.command, {
1162
1292
  cwd,
1293
+ temporaryRoot,
1163
1294
  isSessionRepository: (directory) => {
1164
1295
  targetDirectories.push(directory);
1165
1296
  return false;
@@ -1168,19 +1299,31 @@ function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
1168
1299
  windowsGitTargets.push(directory);
1169
1300
  return false;
1170
1301
  },
1302
+ isTemporaryDirectory: () => {
1303
+ temporaryTargets = true;
1304
+ return true;
1305
+ },
1171
1306
  });
1172
1307
  let reason = preliminaryReason;
1173
- if (targetDirectories.length > 0 || windowsGitTargets.length > 0) {
1174
- const [isSessionRepository, usesWindowsGit] = await Promise.all([
1308
+ if (targetDirectories.length > 0 || windowsGitTargets.length > 0 || temporaryTargets) {
1309
+ const [repository, usesWindowsGit, sessionRoot] = await Promise.all([
1175
1310
  targetDirectories.length > 0
1176
1311
  ? repositoryMatcher(sessionCwd, targetDirectories)
1177
- : (_directory: string) => true,
1312
+ : { isSessionRepository: (_directory: string) => true, unproven: [] },
1178
1313
  windowsGitMatcher(windowsGitTargets),
1314
+ temporaryTargets ? sessionWorktreeRoot(sessionCwd) : undefined,
1179
1315
  ]);
1316
+ const { isSessionRepository } = repository;
1317
+ // 세션 저장소임을 증명하지 못해 막힐 대상만 git이 "어느 저장소도 아님"을 증명하는지 따로 본다(보통은 git 호출이 늘지 않는다).
1318
+ const isOutsideRepository =
1319
+ repository.unproven.length > 0 ? await outsideRepositoryMatcher(repository.unproven) : () => false;
1180
1320
  reason = matchBlockedCommand(event.input.command, {
1181
1321
  cwd,
1322
+ temporaryRoot,
1182
1323
  isSessionRepository,
1183
1324
  usesWindowsGit,
1325
+ isOutsideRepository,
1326
+ isTemporaryDirectory: temporaryDirectoryMatcher(sessionCwd, sessionRoot),
1184
1327
  });
1185
1328
  }
1186
1329
  if (reason) return { block: true, reason };