agentbox-app 0.1.13 → 0.1.16
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.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -1,60 +1,32 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The untracked local files a new task folder is given: dependencies, a `.env`,
|
|
2
|
+
// a machine's own config. A worktree holds only the commit, so without this
|
|
3
|
+
// they are missing.
|
|
2
4
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 2026-10-07 (w-5952e6de3e): of the seven task folders then live, seven had
|
|
8
|
-
// node_modules because exactly one ignored path was ever carried, and five had
|
|
9
|
-
// no `zero.config.json`, so the app ran in them on fallback defaults.
|
|
5
|
+
// `.worktreeinclude` is the file Conductor and Claude Code already read: a file
|
|
6
|
+
// at the repository root in .gitignore syntax naming the ignored files to carry.
|
|
7
|
+
// Git matches the patterns (`git ls-files --others --ignored --exclude-from`),
|
|
8
|
+
// so negation, anchoring and globs behave exactly as git defines them.
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
// the repository
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// That command does not consult the repository's own .gitignore. A path it
|
|
11
|
+
// selects that the repository does not ignore would stay untracked in the
|
|
12
|
+
// folder, and `parkTaskFolder` runs `git add -A`, so closing the task would
|
|
13
|
+
// commit it, credentials included. Every selected path is therefore checked
|
|
14
|
+
// with `git check-ignore` in both checkouts.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// comments, blank lines, anchoring, nested globs, directory patterns and `!`
|
|
19
|
-
// negation right by construction rather than by a matcher of ours that would
|
|
20
|
-
// drift. `--others` never lists tracked content, so a pattern naming a file the
|
|
21
|
-
// commit carries selects nothing.
|
|
22
|
-
//
|
|
23
|
-
// AND SELECTING IS NOT THE SAME AS BEING IGNORED, which cost a review to notice.
|
|
24
|
-
// That command means "untracked files matching THESE rules"; it does not consult
|
|
25
|
-
// the repository's own .gitignore at all. So a `.worktreeinclude` naming an
|
|
26
|
-
// untracked file that the repository does NOT ignore would have carried it in,
|
|
27
|
-
// where it stays untracked -- and `parkTaskFolder` runs `git add -A`, so closing
|
|
28
|
-
// the task would commit it. A credential named in this file would have been
|
|
29
|
-
// committed onto a branch. Every selected path is therefore put through
|
|
30
|
-
// `git check-ignore` IN BOTH CHECKOUTS, and anything the repository does not
|
|
31
|
-
// actually ignore is refused rather than carried (Codex's review, 2026-10-07).
|
|
32
|
-
//
|
|
33
|
-
// NOTHING IS DROPPED QUIETLY. Every path this module will not carry, and every
|
|
34
|
-
// link it cannot vouch for, refuses the folder and names itself. A folder
|
|
35
|
-
// quietly missing a file somebody asked for by name, or quietly sharing its
|
|
36
|
-
// dependencies with the checkout, is the exact fault this module exists to end,
|
|
37
|
-
// so it may not be the way this module fails.
|
|
16
|
+
// Every path that is not carried, and every link that cannot be vouched for,
|
|
17
|
+
// refuses the whole folder by name. Nothing is dropped quietly.
|
|
38
18
|
import { execFileSync } from 'node:child_process';
|
|
39
19
|
import fs from 'node:fs';
|
|
40
20
|
import os from 'node:os';
|
|
41
21
|
import path from 'node:path';
|
|
42
22
|
|
|
43
|
-
/** The file a repository says what it needs in, at its root. */
|
|
44
23
|
export const INCLUDE_FILE = '.worktreeinclude';
|
|
45
24
|
|
|
46
25
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* filesystem can do it and copies where it cannot, which is the same bargain.
|
|
52
|
-
* The inbox runs on Linux too (origin/main, bec6d92), so a hardcoded `-c` is a
|
|
53
|
-
* copy that cannot work there.
|
|
54
|
-
*
|
|
55
|
-
* It lives in this file rather than in `main/task-folders.mjs`, which is the
|
|
56
|
-
* other caller, because that file imports this one and the reverse would be a
|
|
57
|
-
* circle.
|
|
26
|
+
* macOS `cp -c` asks APFS for a block-sharing clone. GNU `cp` has no `-c`;
|
|
27
|
+
* `-a --reflink=auto` shares blocks where the filesystem can and copies where
|
|
28
|
+
* it cannot. Lives here rather than in `main/task-folders.mjs`, the other
|
|
29
|
+
* caller, because that file imports this one.
|
|
58
30
|
*/
|
|
59
31
|
export function copyTree(from, to) {
|
|
60
32
|
const args = process.platform === 'darwin'
|
|
@@ -64,46 +36,29 @@ export function copyTree(from, to) {
|
|
|
64
36
|
}
|
|
65
37
|
|
|
66
38
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `node_modules` anywhere in the tree. And WITHOUT the trailing slash, which is
|
|
72
|
-
* not cosmetic: measured 2026-10-07, `/node_modules/` matches a directory and
|
|
73
|
-
* nothing else, so a `node_modules` that is really a SYMLINK to a shared one is
|
|
74
|
-
* not selected, not carried, and -- the part that matters -- not reported either.
|
|
75
|
-
* Without the slash it is selected, and then refused by name.
|
|
39
|
+
* For a repository with no `.worktreeinclude`: the root's dependencies. No
|
|
40
|
+
* trailing slash on purpose. `/node_modules/` matches directories only, so a
|
|
41
|
+
* `node_modules` that is a symlink to a shared one would be skipped without a
|
|
42
|
+
* word. Without the slash it is selected and then refused by name.
|
|
76
43
|
*/
|
|
77
44
|
export const DEFAULT_PATTERNS = ['/node_modules'];
|
|
78
45
|
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
81
|
-
* It is ignored (the app ignores it itself), so a pattern as broad as `**`
|
|
82
|
-
* selects it, and carrying it would copy every other task's folder into this
|
|
83
|
-
* one. Said as a constant because it is the single unconditional exclusion.
|
|
84
|
-
*/
|
|
46
|
+
/** The app's own task folders. They are ignored, so a pattern like `**` selects
|
|
47
|
+
* them, and carrying them would copy every other task into this one. */
|
|
85
48
|
const MANAGED = '.claude/worktrees';
|
|
86
49
|
|
|
87
50
|
const git = (cwd, args, input) => execFileSync('git', args, {
|
|
88
51
|
cwd, input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
89
52
|
});
|
|
90
53
|
|
|
91
|
-
/** Lines that are a pattern: not blank, not a comment. */
|
|
92
54
|
const patternsIn = (text) => text.split('\n')
|
|
93
55
|
.map((line) => line.replace(/\r$/, ''))
|
|
94
56
|
.filter((line) => line.trim() !== '' && !line.trimStart().startsWith('#'));
|
|
95
57
|
|
|
96
58
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* decision, not an absence: without that there is no way to say "give me a bare
|
|
101
|
-
* folder", and a repository that deliberately wants its dependencies installed
|
|
102
|
-
* fresh would be overruled by our default.
|
|
103
|
-
*
|
|
104
|
-
* A file that exists and cannot be READ is an error and not a default. Falling
|
|
105
|
-
* back there would answer "we could not find out what you need" with "here is
|
|
106
|
-
* what we guessed", which is the shape of every bug in this module's history.
|
|
59
|
+
* An empty `.worktreeinclude` carries nothing rather than the default, so a
|
|
60
|
+
* repository can ask for a bare folder. One that exists and cannot be read
|
|
61
|
+
* throws rather than falling back to a guess.
|
|
107
62
|
*/
|
|
108
63
|
export function askedFor(root) {
|
|
109
64
|
const file = path.join(root, INCLUDE_FILE);
|
|
@@ -113,12 +68,8 @@ export function askedFor(root) {
|
|
|
113
68
|
}
|
|
114
69
|
|
|
115
70
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* `--directory` collapses a wholly untracked directory to one entry, which turns
|
|
119
|
-
* a node_modules of sixty thousand files into a single `cp`. `-z` because a
|
|
120
|
-
* newline is a legal character in a filename and this list decides what is
|
|
121
|
-
* copied.
|
|
71
|
+
* `--directory` collapses a wholly untracked directory to one entry, so
|
|
72
|
+
* node_modules is one `cp`. `-z` because a filename may contain a newline.
|
|
122
73
|
*/
|
|
123
74
|
export function localFilesIn(root) {
|
|
124
75
|
const { patterns, source } = askedFor(root);
|
|
@@ -141,14 +92,10 @@ export function localFilesIn(root) {
|
|
|
141
92
|
}
|
|
142
93
|
|
|
143
94
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* than picked apart: `**` collapses to `.claude`, and narrowing that by hand
|
|
149
|
-
* would be a second matcher to get wrong. A repository that needs one file from
|
|
150
|
-
* in there names it exactly, and an exact name is not an ancestor of the managed
|
|
151
|
-
* folder, so it still comes through.
|
|
95
|
+
* Exclusions no pattern can override: `.git` and the task folders. A path that
|
|
96
|
+
* contains the task folders (`**` collapses to `.claude`) is dropped whole
|
|
97
|
+
* rather than narrowed by a matcher of ours. A repository that needs one file
|
|
98
|
+
* from `.claude` names it exactly, and that still comes through.
|
|
152
99
|
*/
|
|
153
100
|
function keepable(rel) {
|
|
154
101
|
if (rel.split('/')[0] === '.git') return false;
|
|
@@ -158,10 +105,9 @@ function keepable(rel) {
|
|
|
158
105
|
}
|
|
159
106
|
|
|
160
107
|
/**
|
|
161
|
-
* Which of these paths the
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* git call as "not ignored" silently stops carrying everything.
|
|
108
|
+
* Which of these paths the checkout's own rules ignore. `git check-ignore`
|
|
109
|
+
* exits 1 for "none of them"; any other failure throws rather than reading as
|
|
110
|
+
* "not ignored".
|
|
165
111
|
*/
|
|
166
112
|
function ignoredIn(cwd, paths) {
|
|
167
113
|
if (!paths.length) return new Set();
|
|
@@ -175,16 +121,10 @@ function ignoredIn(cwd, paths) {
|
|
|
175
121
|
}
|
|
176
122
|
|
|
177
123
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* nothing at it. Both of the questions below are asked to decide whether
|
|
183
|
-
* something may be written, and answering them from an error nobody saw is how
|
|
184
|
-
* a folder gets built through a link that was never inspected. ENOENT and
|
|
185
|
-
* ENOTDIR are real absences: the name is not there, or a name on the way to it
|
|
186
|
-
* is a file, and either way nothing can be. Anything else is this folder
|
|
187
|
-
* failing to be made, which `carryLocalFiles` reports on the row.
|
|
124
|
+
* A path that cannot be looked at is not a path with nothing at it. ENOENT and
|
|
125
|
+
* ENOTDIR are real absences; any other error throws, because these answers
|
|
126
|
+
* decide whether something may be written through a link nobody inspected.
|
|
127
|
+
* Pinned by tests/a-file-the-app-cannot-look-at-is-not-a-file-that-is-missing.test.mjs.
|
|
188
128
|
*/
|
|
189
129
|
function lookAt(p) {
|
|
190
130
|
try { return fs.lstatSync(p); }
|
|
@@ -199,10 +139,8 @@ const isLink = (p) => lookAt(p)?.isSymbolicLink() === true;
|
|
|
199
139
|
const stillThere = (p) => lookAt(p) !== null;
|
|
200
140
|
|
|
201
141
|
/**
|
|
202
|
-
* Where a link points,
|
|
203
|
-
*
|
|
204
|
-
* ordinary; a link we are not allowed to read is a link we cannot judge, and
|
|
205
|
-
* carrying one of those in unexamined is the whole risk this module manages.
|
|
142
|
+
* Where a link points, or null if it went away (ENOENT) or is no longer a link
|
|
143
|
+
* (EINVAL). A link that cannot be read cannot be judged, so that throws.
|
|
206
144
|
*/
|
|
207
145
|
function linkTarget(p) {
|
|
208
146
|
try { return fs.readlinkSync(p); }
|
|
@@ -212,7 +150,6 @@ function linkTarget(p) {
|
|
|
212
150
|
}
|
|
213
151
|
}
|
|
214
152
|
|
|
215
|
-
/** Is any directory on the way to this path a link? Then nothing may be written through it. */
|
|
216
153
|
function reachedThroughALink(folder, rel) {
|
|
217
154
|
let at = folder;
|
|
218
155
|
for (const part of rel.split('/').slice(0, -1)) {
|
|
@@ -223,11 +160,8 @@ function reachedThroughALink(folder, rel) {
|
|
|
223
160
|
}
|
|
224
161
|
|
|
225
162
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* `..` IS A COMPONENT AND NOT A PREFIX. `startsWith('..')` reads a directory
|
|
229
|
-
* honestly named `..deps` as a climb out of the folder, and refused the whole
|
|
230
|
-
* task over it (Codex's review, 2026-10-07).
|
|
163
|
+
* Whether a path is under a folder, by its text only. `..` is checked as a
|
|
164
|
+
* whole component so a directory named `..deps` is not read as a climb out.
|
|
231
165
|
*/
|
|
232
166
|
const climbsOut = (rel) => rel === '..' || rel.startsWith(`..${path.sep}`);
|
|
233
167
|
const within = (folder, abs) => {
|
|
@@ -240,16 +174,9 @@ const withinOrIs = (folder, abs) => {
|
|
|
240
174
|
};
|
|
241
175
|
|
|
242
176
|
/**
|
|
243
|
-
* The
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* THIS IS WHAT MAKES A DANGLING LINK SAFE TO JUDGE. A target that does not exist
|
|
247
|
-
* cannot be resolved, and skipping the check for those left a hole: with a
|
|
248
|
-
* TRACKED `bridge -> /somewhere/shared` in the commit, a carried
|
|
249
|
-
* `node_modules/pkg/cache -> ../../bridge/newfile` looks like a path inside the
|
|
250
|
-
* folder, dangles, and the first write through it creates a file in the shared
|
|
251
|
-
* directory. The last component being absent says nothing about the ones before
|
|
252
|
-
* it, so the ones before it are the ones resolved.
|
|
177
|
+
* The real path of the nearest ancestor on disk. This lets a dangling link be
|
|
178
|
+
* judged: with a tracked `bridge -> /shared`, a carried link to
|
|
179
|
+
* `../../bridge/newfile` dangles but would write into `/shared`.
|
|
253
180
|
*/
|
|
254
181
|
function nearestReal(abs) {
|
|
255
182
|
let at = abs;
|
|
@@ -257,10 +184,8 @@ function nearestReal(abs) {
|
|
|
257
184
|
if (stillThere(at)) {
|
|
258
185
|
try { return fs.realpathSync(at); }
|
|
259
186
|
catch (error) {
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// process cannot follow, and a containment check skipped because the
|
|
263
|
-
// answer was unreadable is a containment check that did not happen.
|
|
187
|
+
// ENOENT means it went away between the two calls. Any other error
|
|
188
|
+
// would skip the containment check, so it throws.
|
|
264
189
|
if (error?.code === 'ENOENT') return null;
|
|
265
190
|
throw Error(`${at} could not be followed: ${error.message}`);
|
|
266
191
|
}
|
|
@@ -272,34 +197,22 @@ function nearestReal(abs) {
|
|
|
272
197
|
}
|
|
273
198
|
|
|
274
199
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* `node_modules -> ../agentbox-team/node_modules`, so an install in it rewrote
|
|
279
|
-
* the dependencies of every other folder pointing at that target. Carrying such
|
|
280
|
-
* a link forward recreates exactly that, in a folder whose whole purpose is that
|
|
281
|
-
* nothing it does reaches anybody else.
|
|
200
|
+
* Links in a carried entry that would let the folder write somewhere else, such
|
|
201
|
+
* as `node_modules -> ../other-checkout/node_modules`, where an install rewrites
|
|
202
|
+
* another folder's dependencies.
|
|
282
203
|
*
|
|
283
|
-
* An
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* EVERY LINK IN THE SUBTREE IS LOOKED AT, not just the top ones. One level of
|
|
290
|
-
* children was tried first and is not enough: `node_modules/@scope/package` is
|
|
291
|
-
* an ordinary layout, and a scope directory hides every link under it. `find`
|
|
292
|
-
* does not follow links while walking, so a link cannot be used to escape the
|
|
293
|
-
* walk either.
|
|
204
|
+
* An absolute link is never carried, even into this checkout, because after the
|
|
205
|
+
* copy it still points at the original. A relative one is carried when it lands
|
|
206
|
+
* inside the folder, as workspace links do. Every link in the subtree is checked,
|
|
207
|
+
* because `node_modules/@scope/` hides links a level down. `find` does not follow
|
|
208
|
+
* links while walking, so a link cannot lead the walk out.
|
|
294
209
|
*/
|
|
295
210
|
function linksThatDoNotHold(root, folder, rel) {
|
|
296
211
|
const to = path.join(folder, rel);
|
|
297
212
|
let found = [];
|
|
298
213
|
try {
|
|
299
214
|
// `find <path>` on a path that is itself a link reports that link, so an
|
|
300
|
-
// entry
|
|
301
|
-
// directory. That is not incidental: an entry that was a link got no
|
|
302
|
-
// destination check at all while this gated on the entry being a directory.
|
|
215
|
+
// entry that is a link gets the same check as the links inside a directory.
|
|
303
216
|
found = execFileSync('find', [to, '-type', 'l', '-print0'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
304
217
|
.split('\0').filter(Boolean);
|
|
305
218
|
} catch (error) {
|
|
@@ -312,16 +225,11 @@ function linksThatDoNotHold(root, folder, rel) {
|
|
|
312
225
|
if (target === null) continue; // gone from under us, or no longer a link
|
|
313
226
|
if (path.isAbsolute(target)) { bad.push({ path: name, why: 'it is a link to somewhere outside this folder' }); continue; }
|
|
314
227
|
const resolved = path.resolve(path.dirname(link), target);
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// `node_modules/pkg/out -> ../../../w-neighbour/newfile` points into the
|
|
318
|
-
// folder the NEXT TASK is working in, and the first thing written through it
|
|
319
|
-
// lands in that task's work. Checking only when the target exists let that
|
|
320
|
-
// through (Codex's review, 2026-10-07).
|
|
228
|
+
// Checked even when the link dangles: the first write through a dangling
|
|
229
|
+
// link to `../../../w-neighbour/newfile` lands in another task's folder.
|
|
321
230
|
if (!within(folder, resolved)) { bad.push({ path: name, why: 'it is a link that leads out of this folder' }); continue; }
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// and the links on the way exist even when the last name does not.
|
|
231
|
+
// A path inside the folder can still arrive outside it through a link on
|
|
232
|
+
// the way, and those links exist even when the last name does not.
|
|
325
233
|
const home = nearestReal(folder);
|
|
326
234
|
const reached = nearestReal(resolved);
|
|
327
235
|
if (home && reached && !withinOrIs(home, reached)) {
|
|
@@ -329,30 +237,20 @@ function linksThatDoNotHold(root, folder, rel) {
|
|
|
329
237
|
continue;
|
|
330
238
|
}
|
|
331
239
|
if (fs.existsSync(resolved)) continue;
|
|
332
|
-
// Dangling
|
|
333
|
-
//
|
|
334
|
-
//
|
|
240
|
+
// Dangling but contained. It is refused only if the copy broke it, meaning
|
|
241
|
+
// the target exists in the checkout and was not carried. A link already
|
|
242
|
+
// dangling in the checkout is left as it was.
|
|
335
243
|
const sourceSide = path.resolve(path.dirname(path.join(root, name)), target);
|
|
336
244
|
if (fs.existsSync(sourceSide)) bad.push({ path: name, why: 'it points at something that was not carried in with it' });
|
|
337
|
-
// A link that was already dangling where it came from is left exactly as it
|
|
338
|
-
// was found. It is the checkout's own state, not something this made.
|
|
339
245
|
}
|
|
340
246
|
return bad;
|
|
341
247
|
}
|
|
342
248
|
|
|
343
249
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* free space by nothing at all, because the blocks are shared until something
|
|
349
|
-
* writes.
|
|
350
|
-
*
|
|
351
|
-
* A PATTERN THAT MATCHES NOTHING IS SILENT, and it is the only silence here: a
|
|
352
|
-
* repository may name a `.env` that this machine does not have. Everything else
|
|
353
|
-
* that will not or cannot be carried THROWS, with every path and reason in the
|
|
354
|
-
* message, because the caller's only honest options are a complete folder or no
|
|
355
|
-
* folder, and a list of reasons is what makes the second one actionable.
|
|
250
|
+
* Carries the selected local files into a folder that has just been made.
|
|
251
|
+
* A pattern that matches nothing is fine, since a repository may name a `.env`
|
|
252
|
+
* this machine lacks. Anything else that cannot be carried throws, with every
|
|
253
|
+
* path and reason, so the caller gets a complete folder or none.
|
|
356
254
|
*/
|
|
357
255
|
export function carryLocalFiles(root, folder) {
|
|
358
256
|
const { paths, source } = localFilesIn(root);
|
|
@@ -365,13 +263,9 @@ export function carryLocalFiles(root, folder) {
|
|
|
365
263
|
const from = path.join(root, rel);
|
|
366
264
|
const to = path.join(folder, rel);
|
|
367
265
|
if (!fs.existsSync(from) && !isLink(from)) continue; // it went away; silent
|
|
368
|
-
// IGNORED BY THE REPOSITORY ITSELF, on both sides. Being named in
|
|
369
|
-
// `.worktreeinclude` is not enough: an untracked file the repository does not
|
|
370
|
-
// ignore stays untracked in the folder, and closing the task commits it.
|
|
371
266
|
if (!theirs.has(rel)) { no(rel, 'the repository does not ignore it, so carrying it in could commit it'); continue; }
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
// path this branch tracks is content rather than a local file.
|
|
267
|
+
// The folder may be on a different commit from the checkout, where this
|
|
268
|
+
// path is tracked.
|
|
375
269
|
if (tracksIt(folder, rel)) { no(rel, 'this branch tracks it'); continue; }
|
|
376
270
|
if (reachedThroughALink(folder, rel)) { no(rel, 'a link stands where it would have to be written'); continue; }
|
|
377
271
|
if (fs.existsSync(to)) continue; // already there; silent
|
|
@@ -383,13 +277,10 @@ export function carryLocalFiles(root, folder) {
|
|
|
383
277
|
no(rel, `it could not be copied: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
|
|
384
278
|
continue;
|
|
385
279
|
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
// for every one of them. Asked after, the answer is the real one. A file the
|
|
391
|
-
// folder's own branch would not ignore is taken straight back out, because
|
|
392
|
-
// closing the task runs `git add -A` and would commit it.
|
|
280
|
+
// Asked only after the copy: a trailing-slash pattern like `node_modules/`
|
|
281
|
+
// matches directories only, so git answers "not ignored" for a path not on
|
|
282
|
+
// disk yet. What the folder's branch does not ignore is removed again,
|
|
283
|
+
// because closing the task runs `git add -A`.
|
|
393
284
|
if (!ignoredIn(folder, [rel]).has(rel)) {
|
|
394
285
|
try { fs.rmSync(to, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
395
286
|
no(rel, 'the branch in this folder does not ignore it, so closing the task would commit it');
|
|
@@ -398,14 +289,8 @@ export function carryLocalFiles(root, folder) {
|
|
|
398
289
|
carried.push(rel);
|
|
399
290
|
}
|
|
400
291
|
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
// perfectly good repository: with `node_modules` and `packages` both asked for
|
|
404
|
-
// and `node_modules/thing -> ../packages/thing`, git lists `node_modules`
|
|
405
|
-
// first, so the link was judged against a folder that did not have `packages`
|
|
406
|
-
// in it yet, found dangling, and refused -- and nothing ever cleared that when
|
|
407
|
-
// `packages` arrived a moment later. No task in such a repository could have
|
|
408
|
-
// started (Codex's review, 2026-10-07).
|
|
292
|
+
// Links are judged only after every copy. `node_modules/thing ->
|
|
293
|
+
// ../packages/thing` dangles until `packages`, listed later, is copied.
|
|
409
294
|
for (const rel of carried) refused.push(...linksThatDoNotHold(root, folder, rel));
|
|
410
295
|
|
|
411
296
|
if (refused.length) {
|
|
@@ -416,10 +301,9 @@ export function carryLocalFiles(root, folder) {
|
|
|
416
301
|
}
|
|
417
302
|
|
|
418
303
|
/**
|
|
419
|
-
* An entry that is itself a link, judged
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* this one keeps an obviously shared one from being copied at all.
|
|
304
|
+
* An entry that is itself a link, judged in the checkout before copying. The
|
|
305
|
+
* check that matters runs in the folder after every copy; this one keeps an
|
|
306
|
+
* obviously shared link from being copied at all.
|
|
423
307
|
*/
|
|
424
308
|
function carriableLink(root, abs) {
|
|
425
309
|
const target = linkTarget(abs);
|
|
@@ -438,18 +322,9 @@ const tracksIt = (folder, rel) => {
|
|
|
438
322
|
};
|
|
439
323
|
|
|
440
324
|
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* living inside a tracked one (`renderer/dist`) rides along. That used to be the
|
|
445
|
-
* difference between the two ways of making a folder: the clone carried every
|
|
446
|
-
* untracked file in the checkout and the ordinary checkout carried one. Two
|
|
447
|
-
* folders that are not the same thing is the bug underneath "worktrees don't
|
|
448
|
-
* work", so the extras are cleared and the asked-for files are put back by name.
|
|
449
|
-
*
|
|
450
|
-
* Run BEFORE anything is carried in, so it can never remove what was asked for.
|
|
451
|
-
* A failure throws: leaving an extra behind would quietly hand one folder
|
|
452
|
-
* something another did not get, which is the difference being removed.
|
|
325
|
+
* Clears untracked files a clone brought along, such as `renderer/dist` inside
|
|
326
|
+
* a tracked top-level directory, so a cloned folder matches a checked-out one.
|
|
327
|
+
* Runs before anything is carried in, so it never removes what was asked for.
|
|
453
328
|
*/
|
|
454
329
|
export function onlyTrackedContent(folder) {
|
|
455
330
|
let found;
|
|
@@ -460,9 +335,8 @@ export function onlyTrackedContent(folder) {
|
|
|
460
335
|
if (!keepable(rel)) continue;
|
|
461
336
|
const at = path.join(folder, rel);
|
|
462
337
|
try { fs.rmSync(at, { recursive: true, force: true }); }
|
|
463
|
-
// `lstat
|
|
464
|
-
// link
|
|
465
|
-
// would be reported as a success.
|
|
338
|
+
// `lstat`, because existsSync follows links and would report a dangling
|
|
339
|
+
// link that failed to be removed as gone.
|
|
466
340
|
catch (error) { if (stillThere(at)) throw Error(`${rel} came with the clone and could not be cleared: ${error.message}`); }
|
|
467
341
|
gone.push(rel);
|
|
468
342
|
}
|
|
@@ -470,15 +344,12 @@ export function onlyTrackedContent(folder) {
|
|
|
470
344
|
}
|
|
471
345
|
|
|
472
346
|
/**
|
|
473
|
-
* The top-level entries
|
|
474
|
-
*
|
|
475
|
-
* whichever of it is wanted arrives by name through `.worktreeinclude`.
|
|
347
|
+
* The top-level entries holding tracked content, which the clone copies.
|
|
348
|
+
* Anything else wanted arrives by name through `.worktreeinclude`.
|
|
476
349
|
*
|
|
477
|
-
* A
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* `git worktree move` also refuses a worktree holding one. The ordinary checkout
|
|
481
|
-
* knows what a submodule is, so a repository with any is left to it.
|
|
350
|
+
* A submodule (mode 160000) stops the clone: copying it would hand the folder
|
|
351
|
+
* another repository's git data, and `git worktree move` refuses a worktree
|
|
352
|
+
* holding one. A plain `git worktree add` handles submodules.
|
|
482
353
|
*/
|
|
483
354
|
export function trackedTopLevel(root) {
|
|
484
355
|
let listed;
|
package/mcp/core/documents.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import { readIndex, readCreationContent, saveCreation, setCreationStatus, CREATION_STATUSES } from '../../main/store/project.mjs';
|
|
14
14
|
import { runForProject } from '../../main/store/queues.mjs';
|
|
15
|
-
import { assertInside, deniesGitOrEnvPath } from '../../main/store/paths.mjs';
|
|
15
|
+
import { assertInside, deniesGitOrEnvPath, deniesMachineryPath } from '../../main/store/paths.mjs';
|
|
16
16
|
import { AGENT_CREATION_KINDS, SCHEMA_VERSION } from '../../shared/contracts.mjs';
|
|
17
17
|
import { libraryFolderOf } from '../../shared/library-folder.mjs';
|
|
18
18
|
import { resolveProduct } from './account.mjs';
|
|
@@ -142,6 +142,7 @@ export function readDocument(ref, { id = null, path: relPath = null } = {}) {
|
|
|
142
142
|
// are what make an agent-supplied path safe to open at all.
|
|
143
143
|
if (id) throw new Error(`no document with id ${id} in ${project.name}`);
|
|
144
144
|
if (deniesGitOrEnvPath(relPath)) throw new Error(`refusing to read a .git/.env path: ${relPath}`);
|
|
145
|
+
if (deniesMachineryPath(relPath)) throw new Error(`refusing to read a reserved store path: ${relPath}`);
|
|
145
146
|
let content;
|
|
146
147
|
try {
|
|
147
148
|
content = fs.readFileSync(assertInside(project.dir, relPath), 'utf8');
|
|
@@ -181,6 +182,7 @@ export async function writeDocument(ref, { id = null, path: relPath = null, titl
|
|
|
181
182
|
const nextPath = relPath ?? existing?.path;
|
|
182
183
|
if (!nextPath) throw new Error('a new document needs a path, e.g. "reports/first-run.md" or "personas/marcus.md"');
|
|
183
184
|
if (deniesGitOrEnvPath(nextPath)) throw new Error(`refusing to write a .git/.env path: ${nextPath}`);
|
|
185
|
+
if (deniesMachineryPath(nextPath)) throw new Error(`refusing to write a reserved store path: ${nextPath}`);
|
|
184
186
|
|
|
185
187
|
// THE PATH ALREADY SAYS WHAT THIS IS, so a caller that did not name a kind
|
|
186
188
|
// gets the one its extension implies rather than a blanket 'markdown'. Two
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The standing instruction behind a repeating task, for the agent on one of its
|
|
2
|
+
// runs.
|
|
3
|
+
//
|
|
4
|
+
// WHICH RUN THE SESSION IS ON COMES FROM THE APP, NOT FROM THE MODEL.
|
|
5
|
+
// `ZERO_ITEM` and `ZERO_PRODUCT` are written into a worker's environment when
|
|
6
|
+
// the app spawns it (main/supervisor.mjs), which is why no argument below names
|
|
7
|
+
// the task: a session reaches the rule it is standing on or no rule at all.
|
|
8
|
+
//
|
|
9
|
+
// AND ONLY THE WORDS, THOUGH `patchRule` TAKES ANYTHING. The app gates `engine`
|
|
10
|
+
// and `model` at the door (`engineOffered`, `modelOffered` in main/ipc.mjs) so a
|
|
11
|
+
// rule cannot be marked for a coding agent the machine never offered; this
|
|
12
|
+
// server cannot ask that question, so it does not accept the answer. The hour
|
|
13
|
+
// and the day stay on the person's own screen.
|
|
14
|
+
|
|
15
|
+
import { Repeats } from '../../main/repeats.mjs';
|
|
16
|
+
import { readWorkItem } from '../../main/store/work-items.mjs';
|
|
17
|
+
import { listProjects } from '../../main/store/project.mjs';
|
|
18
|
+
import { nextRunAt, ruleIdOf, ruleLabel, standingInstruction } from '../../shared/repeats.mjs';
|
|
19
|
+
import { resolveProduct } from './account.mjs';
|
|
20
|
+
|
|
21
|
+
const NO_THREAD = 'this session is not on a thread, so there is no repeating task behind it';
|
|
22
|
+
|
|
23
|
+
// Read and patch only, so the store modules `serve` needs are never built here:
|
|
24
|
+
// nothing below creates a work item or takes the project lock.
|
|
25
|
+
const rules = new Repeats(null);
|
|
26
|
+
|
|
27
|
+
function projectOf(itemId, product) {
|
|
28
|
+
if (product) return resolveProduct(product);
|
|
29
|
+
for (const p of listProjects()) {
|
|
30
|
+
try { if (readWorkItem(p.dir, itemId)) return p; } catch { /* a product with no ledger */ }
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`no thread ${itemId} in any product`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The rule this session's run belongs to, or a sentence saying why there is none. */
|
|
36
|
+
function ruleBehindThisRun({ env = process.env, now = Date.now() } = {}) {
|
|
37
|
+
const itemId = String(env.ZERO_ITEM ?? '').trim();
|
|
38
|
+
if (!itemId) throw new Error(NO_THREAD);
|
|
39
|
+
|
|
40
|
+
const project = projectOf(itemId, String(env.ZERO_PRODUCT ?? '').trim() || null);
|
|
41
|
+
const item = readWorkItem(project.dir, itemId, now);
|
|
42
|
+
if (!item) throw new Error(`no thread ${itemId} in ${project.name}`);
|
|
43
|
+
|
|
44
|
+
const ruleId = ruleIdOf(item);
|
|
45
|
+
if (!ruleId) {
|
|
46
|
+
throw new Error(`${itemId} is not a run of a repeating task, so it has no standing instruction to change`);
|
|
47
|
+
}
|
|
48
|
+
const rule = rules.get(project.dir, ruleId, now);
|
|
49
|
+
if (!rule) throw new Error(`${itemId} names the repeating task ${ruleId}, which is no longer on file`);
|
|
50
|
+
return { project, item, ruleId, rule };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function asAnswer({ project, item, ruleId, rule }, now) {
|
|
54
|
+
return {
|
|
55
|
+
repeat: ruleId,
|
|
56
|
+
product: project.id,
|
|
57
|
+
run: item.id,
|
|
58
|
+
label: rule.title,
|
|
59
|
+
instruction: standingInstruction(rule),
|
|
60
|
+
schedule: ruleLabel(rule),
|
|
61
|
+
nextRun: nextRunAt(rule, now),
|
|
62
|
+
ended: !!rule.endedAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What every run after this one is told, as it stands. */
|
|
67
|
+
export function readStandingInstruction({ env = process.env, now = Date.now() } = {}) {
|
|
68
|
+
return asAnswer(ruleBehindThisRun({ env, now }), now);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Change it. The whole instruction, replaced, because that is the operation the
|
|
73
|
+
* person's own screen performs and the only one that can also take a sentence
|
|
74
|
+
* back out.
|
|
75
|
+
*/
|
|
76
|
+
export function setStandingInstruction({ instruction }, { env = process.env, now = Date.now() } = {}) {
|
|
77
|
+
const words = typeof instruction === 'string' ? instruction.trim() : '';
|
|
78
|
+
if (!words) throw new Error('a repeating task needs an instruction; send the whole of it, not an empty one');
|
|
79
|
+
|
|
80
|
+
const found = ruleBehindThisRun({ env, now });
|
|
81
|
+
if (found.rule.endedAt) throw new Error(`the repeating task ${found.ruleId} was ended, so it has no next run to change`);
|
|
82
|
+
|
|
83
|
+
// Only the body. The title is a label the app derived from what the person
|
|
84
|
+
// typed, and a line added to a standing instruction is not a new name for the
|
|
85
|
+
// task.
|
|
86
|
+
const rule = rules.patchRule(found.project.dir, found.ruleId, { body: words }, now);
|
|
87
|
+
return asAnswer({ ...found, rule }, now);
|
|
88
|
+
}
|