flowviant 0.103.0 → 0.104.0

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 (140) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +93 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactPolicy.mjs +44 -0
  7. package/bin/lib/artifactPreview.mjs +52 -0
  8. package/bin/lib/artifactReporter.mjs +163 -0
  9. package/bin/lib/artifacts.mjs +47 -227
  10. package/bin/lib/authReject.mjs +16 -4
  11. package/bin/lib/authproxy.mjs +47 -304
  12. package/bin/lib/boxIdentity.mjs +245 -0
  13. package/bin/lib/childEnv.mjs +11 -9
  14. package/bin/lib/claude.mjs +14 -851
  15. package/bin/lib/claudeAuth.mjs +3 -3
  16. package/bin/lib/claudePosture.mjs +367 -0
  17. package/bin/lib/claudeSessions.mjs +411 -0
  18. package/bin/lib/claudeStream.mjs +207 -0
  19. package/bin/lib/cliLimit.mjs +34 -0
  20. package/bin/lib/commandSpecs.mjs +173 -0
  21. package/bin/lib/config.mjs +11 -42
  22. package/bin/lib/credentialDate.mjs +22 -0
  23. package/bin/lib/credentials.mjs +21 -26
  24. package/bin/lib/daemonLogging.mjs +55 -0
  25. package/bin/lib/daemonState.mjs +64 -0
  26. package/bin/lib/deploy.mjs +148 -647
  27. package/bin/lib/deployCheckout.mjs +265 -0
  28. package/bin/lib/deployConfig.mjs +142 -0
  29. package/bin/lib/deployRunner.mjs +311 -0
  30. package/bin/lib/deployWire.mjs +41 -0
  31. package/bin/lib/desktopContract.mjs +56 -105
  32. package/bin/lib/env.mjs +49 -484
  33. package/bin/lib/fleet.mjs +183 -2383
  34. package/bin/lib/fleetJobs.mjs +120 -0
  35. package/bin/lib/fleetPost.mjs +40 -0
  36. package/bin/lib/fleetReports.mjs +392 -0
  37. package/bin/lib/fleetRoster.mjs +470 -0
  38. package/bin/lib/fleetStartup.mjs +196 -0
  39. package/bin/lib/fleetWake.mjs +45 -0
  40. package/bin/lib/fleetWire.mjs +46 -0
  41. package/bin/lib/git.mjs +2 -2
  42. package/bin/lib/gitMergeIdentity.mjs +51 -0
  43. package/bin/lib/grant.mjs +5 -3
  44. package/bin/lib/help.mjs +3 -125
  45. package/bin/lib/holder.mjs +482 -0
  46. package/bin/lib/instance.mjs +124 -46
  47. package/bin/lib/knowledge.mjs +16 -799
  48. package/bin/lib/knowledgeFetch.mjs +38 -0
  49. package/bin/lib/knowledgeFileSync.mjs +112 -0
  50. package/bin/lib/knowledgeFiles.mjs +487 -0
  51. package/bin/lib/knowledgeLibrary.mjs +249 -0
  52. package/bin/lib/landed.mjs +2 -1
  53. package/bin/lib/listeners.mjs +99 -141
  54. package/bin/lib/localSessions.mjs +30 -555
  55. package/bin/lib/login.mjs +3 -2
  56. package/bin/lib/loginPath.mjs +3 -5
  57. package/bin/lib/machineDisconnect.mjs +203 -0
  58. package/bin/lib/machineEnv.mjs +22 -0
  59. package/bin/lib/machineLimits.mjs +61 -0
  60. package/bin/lib/machineListing.mjs +338 -0
  61. package/bin/lib/machines.mjs +78 -455
  62. package/bin/lib/machinesCommand.mjs +214 -0
  63. package/bin/lib/mcp-cli.mjs +2 -1
  64. package/bin/lib/patchRevert.mjs +60 -0
  65. package/bin/lib/prWorkflow.mjs +157 -0
  66. package/bin/lib/preflight.mjs +2 -1
  67. package/bin/lib/pressurePolicy.mjs +104 -0
  68. package/bin/lib/preview.mjs +2 -272
  69. package/bin/lib/previewFramePolicy.mjs +112 -0
  70. package/bin/lib/previewGatePolicy.mjs +236 -0
  71. package/bin/lib/previewRegistry.mjs +235 -0
  72. package/bin/lib/printable.mjs +2 -2
  73. package/bin/lib/procRegistry.mjs +28 -18
  74. package/bin/lib/procScan.mjs +146 -0
  75. package/bin/lib/processes.mjs +29 -37
  76. package/bin/lib/projectToolRuntimes.mjs +138 -0
  77. package/bin/lib/projectTools.mjs +13 -88
  78. package/bin/lib/prompts.mjs +28 -42
  79. package/bin/lib/repoPath.mjs +26 -0
  80. package/bin/lib/resources.mjs +7 -69
  81. package/bin/lib/resumeLost.mjs +107 -0
  82. package/bin/lib/runTurn.mjs +324 -0
  83. package/bin/lib/runtimeAntigravity.mjs +230 -0
  84. package/bin/lib/runtimeCapabilities.mjs +334 -0
  85. package/bin/lib/runtimeClaude.mjs +126 -0
  86. package/bin/lib/runtimeCodex.mjs +217 -0
  87. package/bin/lib/runtimeDetection.mjs +121 -0
  88. package/bin/lib/runtimeEvents.mjs +482 -0
  89. package/bin/lib/runtimeOfferings.mjs +4 -1
  90. package/bin/lib/runtimes.mjs +42 -1402
  91. package/bin/lib/safeFileName.mjs +74 -0
  92. package/bin/lib/standDownExit.mjs +181 -0
  93. package/bin/lib/standDownGate.mjs +48 -0
  94. package/bin/lib/startProject.mjs +279 -0
  95. package/bin/lib/trace.mjs +3 -3
  96. package/bin/lib/turnLock.mjs +68 -0
  97. package/bin/lib/turnProfile.mjs +77 -0
  98. package/bin/lib/uninstall.mjs +55 -45
  99. package/bin/lib/uninstallCopies.mjs +112 -0
  100. package/bin/lib/update.mjs +30 -17
  101. package/bin/lib/uplinkScrub.mjs +265 -0
  102. package/bin/lib/vault.mjs +22 -151
  103. package/bin/lib/vaultDiff.mjs +183 -0
  104. package/bin/lib/versionOrder.mjs +42 -0
  105. package/bin/lib/views.mjs +17 -17
  106. package/bin/lib/wikiRunner.mjs +630 -0
  107. package/bin/lib/work.mjs +188 -3034
  108. package/bin/lib/workAdoptCarry.mjs +94 -0
  109. package/bin/lib/workAgentCheck.mjs +278 -0
  110. package/bin/lib/workAgentMerges.mjs +54 -142
  111. package/bin/lib/workAgentPlans.mjs +14 -12
  112. package/bin/lib/workAgentPrecheck.mjs +393 -0
  113. package/bin/lib/workAgentPublish.mjs +177 -0
  114. package/bin/lib/workAgentReview.mjs +8 -666
  115. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  116. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  117. package/bin/lib/workAgentTurnOutcome.mjs +207 -0
  118. package/bin/lib/workAgentTurnReports.mjs +189 -0
  119. package/bin/lib/workAgentTurns.mjs +91 -898
  120. package/bin/lib/workAttachments.mjs +93 -0
  121. package/bin/lib/workBrain.mjs +53 -0
  122. package/bin/lib/workChildren.mjs +110 -0
  123. package/bin/lib/workCommandAudit.mjs +62 -0
  124. package/bin/lib/workDiffs.mjs +2 -1
  125. package/bin/lib/workNarration.mjs +152 -0
  126. package/bin/lib/workPlaceReports.mjs +426 -0
  127. package/bin/lib/workPlaces.mjs +228 -0
  128. package/bin/lib/workPreviews.mjs +31 -9
  129. package/bin/lib/workProcesses.mjs +4 -5
  130. package/bin/lib/workPullRequests.mjs +52 -126
  131. package/bin/lib/workReportQueue.mjs +254 -0
  132. package/bin/lib/workRetire.mjs +185 -0
  133. package/bin/lib/workSessionGroups.mjs +166 -0
  134. package/bin/lib/workSessionRuntime.mjs +164 -0
  135. package/bin/lib/workSessionTokens.mjs +68 -0
  136. package/bin/lib/workSessionTurns.mjs +892 -0
  137. package/bin/lib/workShip.mjs +9 -29
  138. package/bin/lib/workToolLog.mjs +76 -0
  139. package/package.json +3 -2
  140. package/bin/lib/patch.mjs +0 -363
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Two ways they diverge, and this module reports both:
18
18
  *
19
- * · THE ENVIRONMENT. `claude.mjs` spawns the CLI with `{...process.env}` on
19
+ * · THE ENVIRONMENT. `runTurn.mjs` spawns the CLI with `{...process.env}` on
20
20
  * purpose — its header says so: "the CLI's own credentials live in this
21
21
  * environment, and handing it a curated one signs it out". It also stopped
22
22
  * deleting ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN deliberately, because on
@@ -33,7 +33,7 @@
33
33
  * ── WHAT THIS IS NOT ──
34
34
  *
35
35
  * NOT A POLICY. Flowviant does not pick a credential, does not prefer the
36
- * subscription over a key, and does not call either one wrong — `claude.mjs`
36
+ * subscription over a key, and does not call either one wrong — `runTurn.mjs`
37
37
  * settled that: "Which credential is correct, and whether an account may be
38
38
  * shared, is between the operator and the vendor." This reports what is
39
39
  * PRESENT so a person can see the divergence; it never resolves it.
@@ -62,7 +62,7 @@ import { join } from 'node:path';
62
62
  /**
63
63
  * The variables that can carry auth into the CLI from the environment.
64
64
  *
65
- * The first two are the pair `claude.mjs` documents itself as deliberately NOT
65
+ * The first two are the pair `runTurn.mjs` documents itself as deliberately NOT
66
66
  * deleting, and both appear in childEnv.mjs's allowlist. The third is Claude
67
67
  * Code's own headless/CI token. Names only ever leave this module — a NAME is
68
68
  * what a person greps for, and it is not a secret.
@@ -0,0 +1,367 @@
1
+ /**
2
+ * CLAUDE'S PERMISSION POSTURES — the `--allowedTools` / `--disallowedTools`
3
+ * lists (and the read guard's `--settings`) that ARE each turn profile on
4
+ * Claude Code, looked up by the profile's name (`claudePermFor`).
5
+ *
6
+ * Split out of claude.mjs (SOLID 2026-09-26) because a posture changes for one
7
+ * reason — a measured fact about what the CLI admits under a list — and that
8
+ * reason has nothing to do with how a turn is spawned or how its event stream
9
+ * is read. Every list here was probed on a real CLI; the probes are recorded
10
+ * beside the list they justify, and they move with it.
11
+ */
12
+
13
+ import { fileURLToPath } from 'node:url';
14
+ import { SAFE } from './config.mjs';
15
+
16
+ /**
17
+ * THE READ GUARD, ON EVERY CURATED POSTURE (2026-09-23) — see
18
+ * hooks/readGuard.mjs for what it refuses and the probe that proved it fires.
19
+ *
20
+ * A `Bash(git log:*)` allow is a PREFIX: it names the program and says nothing
21
+ * about the arguments, and `git log --output=<path>` writes any file while
22
+ * `-c core.fsmonitor=…` / `--ext-diff` run a program. A reviewer landed
23
+ * `git log -1 --format='tformat:x' --output=pwned.txt` under RESEARCH_PERM with
24
+ * no denial, and it re-measured the same way here. So every list below that
25
+ * promises "reads, and nothing else" carries a `PreToolUse` hook through
26
+ * `--settings`, measured on Claude Code 2.1.281: it fires under
27
+ * `--allowedTools`, exit 2 blocks the call, and its sentence reaches the model.
28
+ *
29
+ * NEVER ON THE BUILD POSTURE: `--dangerously-skip-permissions` is the
30
+ * operator's own choice for a turn that is meant to write, and a guard there
31
+ * would be refusing the job. Nor on PLAN_MODE_PERM, which is the CLI's own
32
+ * posture on the owner's own tab, not a list this file promises anything about.
33
+ *
34
+ * The hook runs as `<this node> <readGuard.mjs>` — the daemon's own
35
+ * `process.execPath`, never a `node` looked up on a PATH the operator may not
36
+ * have, because a hook that fails to START is a non-blocking error and the
37
+ * command runs anyway.
38
+ */
39
+ export const READ_GUARD_PATH = fileURLToPath(new URL('./hooks/readGuard.mjs', import.meta.url));
40
+ const hookQuote = (s) =>
41
+ process.platform === 'win32' ? `"${s}"` : `'${String(s).replace(/'/g, `'\\''`)}'`;
42
+ export const READ_GUARD_SETTINGS = JSON.stringify({
43
+ hooks: {
44
+ PreToolUse: [
45
+ {
46
+ matcher: 'Bash',
47
+ hooks: [{ type: 'command', command: `${hookQuote(process.execPath)} ${hookQuote(READ_GUARD_PATH)}` }],
48
+ },
49
+ ],
50
+ },
51
+ });
52
+ /** Spread FIRST into a curated list: `--allowedTools` is variadic, so the
53
+ * `--settings` pair must stand before it or be swallowed as a tool name. */
54
+ const READ_GUARD = ['--settings', READ_GUARD_SETTINGS];
55
+
56
+ /** A path a permission rule can name: absolute POSIX, and free of the glob and
57
+ * rule-syntax characters that would change what the rule means. Anything else
58
+ * gets no rule — `--add-dir` still admits the directory (measured). */
59
+ const ruleSafeAbsolute = (p) =>
60
+ typeof p === 'string' && p.startsWith('/') && !/[*?[\]{}()\\\n]/.test(p) ? p.replace(/\/+$/, '') : null;
61
+
62
+ /**
63
+ * THE READ FENCE — the worktree, plus the knowledge library by its absolute
64
+ * path, and nothing else on the box (2026-09-24, the audit).
65
+ *
66
+ * Every curated posture used to allow bare `Read`, `Grep` and `Glob`, and a
67
+ * bare allow is not "read the repo": measured on Claude Code 2.1.281, bare
68
+ * `Read` read a file outside the working directory and bare `Grep` searched
69
+ * one. So a design, consult or capture turn — each steered by words anyone who
70
+ * can file a card can write — could read `~/.flowviant/credentials.json`, which
71
+ * holds the MACHINE CREDENTIAL of every project connected on this box, and put
72
+ * it in an artifact, a plan note or a transcript that leaves the machine. That
73
+ * is a cross-project leak from a posture this file calls read-only.
74
+ *
75
+ * The fence is research's, measured the same day in the design shape:
76
+ * `Read(./**)` and `Glob(./**)` read the worktree; the knowledge library reads
77
+ * through `--add-dir` plus its own `//` rule; a file outside both was refused
78
+ * ("Claude requested permissions to read from …, but you haven't granted it
79
+ * yet"). GREP CARRIES NO ALLOW RULE AT ALL, on purpose: with none, Grep in the
80
+ * working directory (and in the added knowledge directory) ran, and Grep of a
81
+ * path outside them was refused. The Bash readers these postures keep are
82
+ * path-validated by the CLI on its own (see CONSULT_BASH).
83
+ */
84
+ function fencedReads(knowledgeDir) {
85
+ const kd = ruleSafeAbsolute(knowledgeDir);
86
+ return [
87
+ 'Read(./**)',
88
+ ...(kd ? [`Read(/${kd}/**)`] : []),
89
+ 'Glob(./**)',
90
+ ...(kd ? [`Glob(/${kd}/**)`] : []),
91
+ ];
92
+ }
93
+
94
+ /** The shell readers CONSULT and PLAN keep. Each one is PATH-VALIDATED by the
95
+ * CLI itself — measured on 2.1.281 (2026-09-24): with `Bash(cat:*)`,
96
+ * `Bash(head:*)` and `Bash(ls:*)` allowed, `cat`/`ls`/`head` on a file outside
97
+ * the working directories were each refused ("Claude Code may only
98
+ * concatenate files from the allowed working directories") — so it is the
99
+ * Read/Grep/Glob TOOLS, not these, that needed the fence above. */
100
+ const CONSULT_BASH = [
101
+ 'Bash(ls:*)',
102
+ 'Bash(wc:*)',
103
+ 'Bash(head:*)',
104
+ 'Bash(cat:*)',
105
+ 'Bash(git log:*)',
106
+ 'Bash(git show:*)',
107
+ 'Bash(git diff:*)',
108
+ 'Bash(git rev-parse:*)',
109
+ ];
110
+
111
+ /**
112
+ * PLAN — read the repo, write the plan, never the code.
113
+ *
114
+ * The read half is `consultPermFor`'s verbatim (the read fence and the
115
+ * path-validated shell readers): this turn's prompt is steered by
116
+ * anything a project editor can type, so the same threat applies and the same
117
+ * allowlist answers it. What is added is the control plane and NOTHING else —
118
+ * `mcp__flowviant` is the plan principal's token, whose entire tool set is the
119
+ * five plan tools (the server refuses anything else on it). So even a fully
120
+ * hijacked turn's most destructive reachable act is dropping a slice from the
121
+ * plan it is already in, which a human can see and undo in the thread.
122
+ *
123
+ * Note what is absent versus WIKI_PERM: Write, Edit, mkdir and rm. The
124
+ * cartographer needs those because it authors files; a planner authors records
125
+ * through an API, and there is no file on this machine it has any business
126
+ * touching.
127
+ */
128
+ export function planPermFor(knowledgeDir) {
129
+ return [
130
+ ...READ_GUARD,
131
+ '--allowedTools',
132
+ 'mcp__flowviant',
133
+ ...fencedReads(knowledgeDir),
134
+ ...CONSULT_BASH,
135
+ ];
136
+ }
137
+
138
+ // Unattended (default) skips prompts so the agent never stalls with no terminal;
139
+ // FLOWVIANT_SAFE=1 restricts to a curated toolset instead.
140
+ const PERM = SAFE
141
+ ? [
142
+ '--allowedTools',
143
+ 'mcp__flowviant',
144
+ 'Edit',
145
+ 'Write',
146
+ 'Read',
147
+ 'Grep',
148
+ 'Glob',
149
+ 'Bash(git:*)',
150
+ 'Bash(gh:*)',
151
+ 'Bash(npm:*)',
152
+ 'Bash(bun:*)',
153
+ ]
154
+ : ['--dangerously-skip-permissions'];
155
+
156
+ // Wiki turns are read-the-repo + write-the-vault ONLY — always curated, never
157
+ // --dangerously-skip-permissions: no gh, no push-capable git, no package
158
+ // managers, and nothing that can EXECUTE arbitrary commands — no `find`
159
+ // (-exec/-delete) and no `git grep` (-O<pager> runs a shell; the Grep tool
160
+ // covers search). Command execution is the line: it enables network exfil,
161
+ // which plain file writes never do. `rm` IS allowed: pruning a stale vault
162
+ // page requires a real file deletion (that's how the sync protocol learns of
163
+ // it), and the blast radius is bounded — the daemon resets the repo worktree
164
+ // after every wiki turn, and the vault has its own git history.
165
+ // (Write/Edit can't be path-scoped here; the worktree reset is the backstop.)
166
+ const WIKI_PERM = [
167
+ ...READ_GUARD,
168
+ '--allowedTools',
169
+ 'Read',
170
+ 'Grep',
171
+ 'Glob',
172
+ 'Edit',
173
+ 'Write',
174
+ 'Bash(ls:*)',
175
+ 'Bash(wc:*)',
176
+ 'Bash(head:*)',
177
+ 'Bash(cat:*)',
178
+ 'Bash(mkdir:*)',
179
+ 'Bash(rm:*)',
180
+ 'Bash(git status:*)',
181
+ 'Bash(git log:*)',
182
+ 'Bash(git show:*)',
183
+ 'Bash(git diff:*)',
184
+ 'Bash(git rev-parse:*)',
185
+ ];
186
+
187
+ // A CONSULT reads and answers. Nothing else.
188
+ //
189
+ // It used to run on WIKI_PERM, whose comment two blocks up says the quiet part:
190
+ // Write/Edit "can't be path-scoped here; the worktree reset is the backstop".
191
+ // That is a fine trade for the cartographer, which exists to author files and
192
+ // gets reset after every turn. It is the wrong trade for a consult, whose prompt
193
+ // is steered by a question ANY project editor can write and which had no reset
194
+ // behind it — so a sentence in a chat box could reach Write, rm and mkdir on
195
+ // someone else's machine. The permission list is the enforcement; the prompt's
196
+ // "do not change anything" is only an instruction, and instructions are exactly
197
+ // what an injected question competes with.
198
+ export function consultPermFor(knowledgeDir) {
199
+ return [...READ_GUARD, '--allowedTools', ...fencedReads(knowledgeDir), ...CONSULT_BASH];
200
+ }
201
+
202
+ /**
203
+ * THE TWO NON-CODE POSTURES (0.97.0) — a design card and a research card may
204
+ * WRITE, and only under `.flowviant/artifacts/`.
205
+ *
206
+ * The card's words are steered by anyone who can file a card, so "change
207
+ * nothing" as prose is only an instruction, and the permission list is the
208
+ * enforcement. What each carries is the ONE write its contract needs:
209
+ *
210
+ * · `Edit(.flowviant/artifacts/**)` — PROBED on Claude Code 2.1.281 before
211
+ * relying on it (2026-09-23): with `--allowedTools 'Read'
212
+ * 'Edit(.flowviant/artifacts/**)'`, a `-p` turn told to write
213
+ * `.flowviant/artifacts/a.html` AND `b.txt` at the root wrote the first and
214
+ * was DENIED the second. The Edit rule governs every file-writing tool,
215
+ * Write included. Spelled `Write(.flowviant/artifacts/**)` — or `./`- or
216
+ * `//abs`-anchored — the same probe was denied BOTH files, so that spelling
217
+ * is not a scoped write at all, it is no write. Relative to the turn's cwd,
218
+ * which is the agent's own worktree, which is where the artifact scan looks.
219
+ * `**` AND NOT `*`, measured the same day: `Edit(.flowviant/artifacts/*)`
220
+ * let a Write to `.flowviant/artifacts/sub/b.html` through exactly as `**`
221
+ * did, so the single-level spelling is not narrower on this CLI and would
222
+ * only read as if it were. The scan keeps top-level files, so the
223
+ * contracts say "no subfolders" — the prompt is the only thing that can.
224
+ *
225
+ * THEY ARE NOT THE SAME READER, and the difference is the network:
226
+ *
227
+ * · DESIGN draws THIS product, so it reads the repo — the worktree and the
228
+ * knowledge library through the read fence (`fencedReads`, 2026-09-24:
229
+ * it was bare Read/Grep/Glob, which reach the whole box), Grep with no
230
+ * allow rule, `ls`, and `.env*` denied by name — and has no web at all (a mockup that went looking on the
231
+ * web would be a mockup of somebody else's product). No git readers and no
232
+ * cat/head/wc: Read covers every one of them, and each git reader was an
233
+ * `--output` away from a write (see READ_GUARD, which it still carries for
234
+ * the `ls` it keeps).
235
+ * · RESEARCH has the web — WebSearch and WebFetch — and a turn that can both
236
+ * read a secret and fetch a URL can send one. So its reading is FENCED to
237
+ * what the card is about: `Read(./**)` (the worktree) plus the project's
238
+ * knowledge library by its absolute path, `Glob(./**)` for names, NO Grep
239
+ * (an unscoped search of the home directory prints matching lines), NO
240
+ * Bash at all, and `.env*` DENIED by name. PROBED on 2.1.281, the whole
241
+ * list with `--add-dir <knowledge>`: a file in the cwd read; `/etc/hostname`,
242
+ * a sibling directory's file and `~/.flowviant/credentials.json` were each
243
+ * refused ("Claude requested permissions to read from …, but you haven't
244
+ * granted it yet"); `.env` was refused ("File is in a directory that is
245
+ * denied by your permission settings"); the knowledge file read. Two
246
+ * controls: WITHOUT the `--disallowedTools` pair, `Read(./**)` read `.env`
247
+ * — the deny is load-bearing; and a bare `Glob` listed `~/.flowviant`
248
+ * while `Glob(./**)` refused it and still listed the cwd and the added
249
+ * directory. The knowledge `Read(//…)` rule is belt: `--add-dir` alone
250
+ * admitted the file, and the rule keeps the read allowed should the
251
+ * adapter ever stop passing that flag.
252
+ * AND `Bash` IS DENIED BY NAME, because leaving it off the allow list is
253
+ * not "no Bash": measured the same day, with Bash absent from
254
+ * `--allowedTools` the CLI still ran `git log -1 --format=%s` and `ls` on
255
+ * its own read-only classifier (it did refuse `git log … --output=…`,
256
+ * "This command requires approval" — the reviewer's write needed the
257
+ * explicit `Bash(git log:*)` prefix the old list carried). With `Bash` in
258
+ * `--disallowedTools` the tool was not there at all.
259
+ *
260
+ * NO MCP, and no shell that could commit: the agent turn has no MCP anyway
261
+ * (see SYSTEM_AGENT's header), and a posture that could run `git commit` could
262
+ * commit the mockup it was told to keep out of git.
263
+ *
264
+ * STATED, not closed: research may still read any file in the worktree and
265
+ * name it in a WebFetch URL — the repository is what a research card is about,
266
+ * and a turn that could not read it could not answer. Design may read any
267
+ * file in its worktree and put it in an artifact the server stores; text
268
+ * artifacts are scrubbed of the machine's known secret values — and of any
269
+ * Flowviant credential, by shape — on the way out (artifacts.mjs, uplinkScrub.mjs), and
270
+ * binary ones are withheld when they carry one.
271
+ * Design still runs a plain `git log` on the CLI's own read-only classifier
272
+ * though no git reader is allowed (measured) — the guard is what refuses its
273
+ * `--output`. And the Agent tool is offered with no allow rule on this CLI:
274
+ * a research turn with Bash denied launched a subagent to run one, and the
275
+ * subagent had no Bash either (measured) — the rules are inherited. Whether
276
+ * the guard's hook sees a subagent's own calls was not measured here.
277
+ */
278
+ const ARTIFACT_WRITE = 'Edit(.flowviant/artifacts/**)';
279
+ /**
280
+ * PLAN MODE (0.97.0) — a Workbench tab's `planMode` switch, and Claude Code's
281
+ * own `--permission-mode plan` INSTEAD OF every list above: the CLI reads,
282
+ * decides and answers with a plan, and refuses each write itself.
283
+ *
284
+ * PROBED on Claude Code 2.1.281 before relying on it (2026-09-23), in a scratch
285
+ * git repo, `-p … --output-format stream-json --verbose`:
286
+ *
287
+ * · ALONE, asked to "add a subtract(a, b) function to app.js": the init event
288
+ * reads `permissionMode: "plan"`, the turn Read the file, ran a read-only
289
+ * `find`, wrote its plan to `~/.claude/plans/<slug>.md` (the CLI's own plan
290
+ * file, outside the repo), tried `ExitPlanMode` and was told "Error: No such
291
+ * tool available: ExitPlanMode. ExitPlanMode is disabled for this session",
292
+ * and ended with the plan as its result text. app.js was untouched.
293
+ * · BESIDE `--dangerously-skip-permissions`, same ask: the init event reads
294
+ * `permissionMode: "bypassPermissions"` — no error, no warning — and the
295
+ * edit LANDED. The bypass silently wins. So this list is a REPLACEMENT for
296
+ * `perm`, never an addition, and a plan turn can never carry both.
297
+ * · WITH `--mcp-config` and `--allowedTools mcp__flowviant`: the server
298
+ * connected and its tools were listed, but a call was refused — "Cannot
299
+ * call mcp__flowviant__log_work while in plan mode." — and without the
300
+ * `--allowedTools` entry, "Claude requested permissions to use
301
+ * mcp__flowviant__log_work, but you haven't granted it yet." Plan mode
302
+ * admitted the call ONLY when the server annotated the tool
303
+ * `readOnlyHint: true`, and none of the session tools (stream_session_turn,
304
+ * log_work, file_card …) are read-only or annotated so. So a plan turn runs
305
+ * PLAIN — no MCP, the plain tab's contract — rather than mounting a control
306
+ * plane whose every call the CLI refuses. See workSessionTurns.mjs.
307
+ */
308
+ export const PLAN_MODE_PERM = ['--permission-mode', 'plan'];
309
+ export function designPermFor(knowledgeDir) {
310
+ return [
311
+ ...READ_GUARD,
312
+ '--allowedTools',
313
+ ...fencedReads(knowledgeDir),
314
+ 'Bash(ls:*)',
315
+ ARTIFACT_WRITE,
316
+ // A mockup is uploaded and runs scripts in a frame that may navigate
317
+ // itself, so the checkout's own secrets are denied by name too — the same
318
+ // pair research carries, measured load-bearing there.
319
+ '--disallowedTools',
320
+ 'Read(./.env*)',
321
+ 'Read(./**/.env*)',
322
+ ];
323
+ }
324
+ export const DESIGN_PERM = designPermFor(null);
325
+
326
+ /**
327
+ * RESEARCH, built at spawn — the knowledge library is the one directory
328
+ * outside the worktree it may read, and only the spawn knows where that is.
329
+ * `//` is the CLI's own spelling for an absolute path in a rule.
330
+ */
331
+ export function researchPerm(knowledgeDir) {
332
+ return [
333
+ ...READ_GUARD,
334
+ '--allowedTools',
335
+ ...fencedReads(knowledgeDir),
336
+ 'WebSearch',
337
+ 'WebFetch',
338
+ ARTIFACT_WRITE,
339
+ '--disallowedTools',
340
+ 'Read(./.env*)',
341
+ 'Read(./**/.env*)',
342
+ 'Bash',
343
+ ];
344
+ }
345
+ /** The list with no library — what a project that keeps none runs under. */
346
+ export const RESEARCH_PERM = researchPerm(null);
347
+
348
+ /**
349
+ * CLAUDE'S PERMISSION LIST FOR A TURN PROFILE, by the profile's name — one
350
+ * entry per name in turnProfile.mjs (a test holds the two tables equal). The
351
+ * builders that take the knowledge directory get it; the fixed lists ignore it.
352
+ */
353
+ const CLAUDE_PERM = {
354
+ build: () => PERM,
355
+ wiki: () => WIKI_PERM,
356
+ consult: (kd) => consultPermFor(kd),
357
+ plan: (kd) => planPermFor(kd),
358
+ 'plan-mode': () => PLAN_MODE_PERM,
359
+ design: (kd) => designPermFor(kd),
360
+ research: (kd) => researchPerm(kd),
361
+ };
362
+ export const CLAUDE_PERM_PROFILES = Object.freeze(Object.keys(CLAUDE_PERM));
363
+ export function claudePermFor(profileName, knowledgeDir) {
364
+ const build = Object.hasOwn(CLAUDE_PERM, profileName) ? CLAUDE_PERM[profileName] : null;
365
+ if (!build) throw new Error(`no Claude permission list for profile '${profileName}'`);
366
+ return build(knowledgeDir);
367
+ }