finitemonkey 0.1.0-next.4 → 0.1.0-next.5

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 (2) hide show
  1. package/dist/finitemonkey.mjs +1325 -146
  2. package/package.json +1 -1
@@ -583,10 +583,10 @@ var needsPersonProblem = (messageKey, fix, params2 = {}) => ({
583
583
  });
584
584
  var FmError = class extends Error {
585
585
  problem;
586
- constructor(problem3) {
587
- super(problem3.code);
586
+ constructor(problem4) {
587
+ super(problem4.code);
588
588
  this.name = "FmError";
589
- this.problem = problem3;
589
+ this.problem = problem4;
590
590
  }
591
591
  };
592
592
 
@@ -657,11 +657,11 @@ var success = (data, human2 = []) => ({
657
657
  data,
658
658
  human: human2
659
659
  });
660
- var failureFrom = (t, problem3, human2) => ({
660
+ var failureFrom = (t, problem4, human2) => ({
661
661
  ok: false,
662
- exitCode: problem3.exitCode,
663
- error: { code: problem3.code, message: t(problem3.messageKey, problem3.params), fix: problem3.fix },
664
- ...problem3.data === void 0 ? {} : { data: problem3.data },
662
+ exitCode: problem4.exitCode,
663
+ error: { code: problem4.code, message: t(problem4.messageKey, problem4.params), fix: problem4.fix },
664
+ ...problem4.data === void 0 ? {} : { data: problem4.data },
665
665
  ...human2 === void 0 ? {} : { human: human2 }
666
666
  });
667
667
  var toEnvelope = (command, result) => ({
@@ -1883,6 +1883,7 @@ var implicitContract = (files) => {
1883
1883
  ...hasTests ? [] : [{ id: "tests", kind: "tests" }],
1884
1884
  { id: "placeholders", kind: "placeholders" },
1885
1885
  { id: "secrets", kind: "secrets" },
1886
+ { id: "prose", kind: "prose" },
1886
1887
  { id: "pr", kind: "pr", requireOpen: false }
1887
1888
  ],
1888
1889
  decisionsNeeded: [],
@@ -1905,7 +1906,8 @@ var CHECK_KINDS = [
1905
1906
  "secrets",
1906
1907
  "worktree",
1907
1908
  "merge",
1908
- "prod"
1909
+ "prod",
1910
+ "prose"
1909
1911
  ];
1910
1912
  var GIT_REQUIREMENTS = ["committed", "pushed", "pr_open"];
1911
1913
  var SIZES = ["S", "M", "L"];
@@ -2153,8 +2155,8 @@ var fetchView = (runtime, git, branch) => {
2153
2155
  var prStatus = (runtime, git, paths, ttlMs = PR_CACHE_TTL_MS) => {
2154
2156
  const branch = currentBranch(git);
2155
2157
  if (branch === void 0) return { kind: "no_branch" };
2156
- const hit = cached(runtime, paths, branch, ttlMs);
2157
- if (hit !== void 0) return { kind: "found", branch, pr: hit, localHead: headSha(git) };
2158
+ const hit2 = cached(runtime, paths, branch, ttlMs);
2159
+ if (hit2 !== void 0) return { kind: "found", branch, pr: hit2, localHead: headSha(git) };
2158
2160
  const status = fetchView(runtime, git, branch);
2159
2161
  if (status.kind !== "fetched") return status;
2160
2162
  writeJsonFile(runtime, paths.cache("pr.json"), { fetchedAt: nowMs(runtime), branch, view: status.view });
@@ -2172,21 +2174,21 @@ var conflictFiles = (git, base) => {
2172
2174
 
2173
2175
  // src/gate/prepare.ts
2174
2176
  var diskFiles = (runtime, root) => ({
2175
- read: (relative6) => {
2176
- const text6 = runtime.readText(toAbsolutePath(join3(root, relative6)));
2177
+ read: (relative8) => {
2178
+ const text6 = runtime.readText(toAbsolutePath(join3(root, relative8)));
2177
2179
  return text6.ok ? text6.value : void 0;
2178
2180
  },
2179
- exists: (relative6) => runtime.stat(toAbsolutePath(join3(root, relative6))) !== void 0
2181
+ exists: (relative8) => runtime.stat(toAbsolutePath(join3(root, relative8))) !== void 0
2180
2182
  });
2181
2183
  var baseFiles = (git, base, disk) => ({
2182
- read: (relative6) => base === EMPTY_TREE ? disk.read(relative6) : fileAt(git, base, relative6),
2184
+ read: (relative8) => base === EMPTY_TREE ? disk.read(relative8) : fileAt(git, base, relative8),
2183
2185
  exists: disk.exists
2184
2186
  });
2185
2187
  var loadContract = (options, paths, git, base) => {
2186
2188
  const disk = diskFiles(options.runtime, paths.root);
2187
2189
  const id = activeTaskId(options.runtime, paths, options.taskId);
2188
2190
  if (id === void 0) return accept(loadImplicit(options.runtime, paths, baseFiles(git, base, disk)));
2189
- return loadTask(options.runtime, paths, { disk: disk.read, base: (relative6) => fileAt(git, base, relative6) }, id);
2191
+ return loadTask(options.runtime, paths, { disk: disk.read, base: (relative8) => fileAt(git, base, relative8) }, id);
2190
2192
  };
2191
2193
  var memo = (make) => {
2192
2194
  let value;
@@ -2667,7 +2669,47 @@ var en_default = {
2667
2669
  "gate.secrets.finding": "secret {rule} at {file}:{line}",
2668
2670
  "gate.secrets.incident": "secret incident {id} is {state}",
2669
2671
  "gate.secrets.clean": "no new secret in the changes and no open incident",
2670
- "gate.require.secrets": "no new secret and no secret incident left open"
2672
+ "gate.require.secrets": "no new secret and no secret incident left open",
2673
+ "prose.command.check": "Check text for patterns readers recognize as machine-written (local only)",
2674
+ "prose.rule.opener-routine": "open with what happened to someone: a command, a time, an error message",
2675
+ "prose.rule.reassurance": "cut the consolation line; describe the problem and let the reader judge",
2676
+ "prose.rule.slogan-closer": "end on a fact or the next step instead of a slogan",
2677
+ "prose.rule.rhythmic-triplet": "join the short sentences into one that says what each part does",
2678
+ "prose.rule.imperative-fragment": "replace the loose commands with one sentence about what the product does",
2679
+ "prose.rule.antithesis": "say what it is; drop the contrast with what it is not",
2680
+ "prose.rule.abstract-noun": "swap the abstract noun for the scene: who did what, where",
2681
+ "prose.rule.crutch-word": "use the plain word, or a number that proves the claim",
2682
+ "prose.rule.colon-heading": "make the heading the question or the task the reader has",
2683
+ "prose.rule.decorative-emoji": "remove the emoji; the heading carries the meaning",
2684
+ "prose.rule.em-dash-series": "use a period, a comma or parentheses instead of dashes",
2685
+ "prose.target.commit": "The commit message",
2686
+ "prose.target.pr": "The PR text",
2687
+ "prose.target.issue": "The issue text",
2688
+ "prose.span": "lines {start}-{end}: {rules}",
2689
+ "prose.fix_item": "{rule}: {fix}",
2690
+ "prose.warn.command": "{target} reads as machine-written ({spans}). Rewrite it before sending. {fixes}. If it is brand text written on purpose, show the person two alternatives.",
2691
+ "prose.block.command": "blocked: {target} reads as machine-written ({spans}), and this project's preset does not send it. {fixes}. Rewrite it and run the command again.",
2692
+ "prose.block.unreadable": "blocked: could not read {file} to check its text ({detail}). Check the path and run the command again.",
2693
+ "prose.block.marker": "blocked: this would add the marker {marker} to {file}, and only the person can add it. Rewrite the passage instead, or ask the person to add the marker in their own editor.",
2694
+ "prose.check.file": "{file}: {count} passage(s) that read as machine-written",
2695
+ "prose.check.span": " {file}:{start}-{end} score {score}: {rules}",
2696
+ "prose.check.fix": " do instead: {fix}",
2697
+ "prose.check.allowed": "{file}: skipped, the person marked it with {marker}",
2698
+ "prose.check.clean": "{count} file(s) checked, nothing reads as machine-written. The monkey wrote this one itself.",
2699
+ "prose.error.found": "{count} passage(s) read as machine-written",
2700
+ "prose.error.no_input": "nothing to check: pass files, --stdin, or run it inside a git repository to check the changed Markdown files",
2701
+ "prose.error.unreadable": "could not read {file} ({detail})",
2702
+ "prose.block.marker_shell": "blocked: this command writes the marker {marker}, and only the person can add it. Rewrite the passage instead, or ask the person to add the marker in their own editor.",
2703
+ "prose.rule.requester-reference": "state the technical reason for the change; leave out who asked for it",
2704
+ "prose.rule.person-as-reason": "name the property of the change instead of a person",
2705
+ "prose.rule.origin-project": "describe the technical property you copied instead of naming where it came from",
2706
+ "prose.rule.first-person": "write what the change does, in the present tense, without I or you",
2707
+ "prose.rule.padding": "cut the preamble and start with what changed",
2708
+ "gate.require.prose": "changed Markdown does not read as machine-written",
2709
+ "gate.prose.unavailable": "could not read the changed Markdown files (git unavailable)",
2710
+ "gate.prose.finding": "{file}:{line} reads as machine-written ({rules})",
2711
+ "gate.prose.warning": "warning only in this preset: {file}:{line} reads as machine-written ({rules})",
2712
+ "gate.prose.clean": "{count} changed Markdown file(s), none reads as machine-written"
2671
2713
  };
2672
2714
 
2673
2715
  // src/i18n/es.json
@@ -2979,7 +3021,47 @@ var es_default = {
2979
3021
  "gate.secrets.finding": "secreto {rule} en {file}:{line}",
2980
3022
  "gate.secrets.incident": "incidente de secreto {id} está {state}",
2981
3023
  "gate.secrets.clean": "ningún secreto nuevo en los cambios y ningún incidente abierto",
2982
- "gate.require.secrets": "ningún secreto nuevo y ningún incidente de secreto abierto"
3024
+ "gate.require.secrets": "ningún secreto nuevo y ningún incidente de secreto abierto",
3025
+ "prose.command.check": "Revisa si el texto tiene patrones que el lector reconoce como escritura de IA (solo local)",
3026
+ "prose.rule.opener-routine": "empieza con lo que le pasó a alguien: un comando, una hora, un mensaje de error",
3027
+ "prose.rule.reassurance": "quita la frase de consuelo; describe el problema y deja que el lector juzgue",
3028
+ "prose.rule.slogan-closer": "termina con un hecho o con el siguiente paso, sin eslogan",
3029
+ "prose.rule.rhythmic-triplet": "une las frases cortas en una que diga qué hace cada parte",
3030
+ "prose.rule.imperative-fragment": "cambia las órdenes sueltas por una frase sobre lo que hace el producto",
3031
+ "prose.rule.antithesis": "di lo que es y quita el contraste con lo que no es",
3032
+ "prose.rule.abstract-noun": "cambia el sustantivo abstracto por la escena: quién hizo qué, dónde",
3033
+ "prose.rule.crutch-word": "usa la palabra simple, o un número que pruebe lo que dices",
3034
+ "prose.rule.colon-heading": "haz del título la pregunta o la tarea de quien lee",
3035
+ "prose.rule.decorative-emoji": "quita el emoji; el título ya lleva el sentido",
3036
+ "prose.rule.em-dash-series": "usa punto, coma o paréntesis en lugar de las rayas",
3037
+ "prose.target.commit": "El mensaje de commit",
3038
+ "prose.target.pr": "El texto del PR",
3039
+ "prose.target.issue": "El texto de la issue",
3040
+ "prose.span": "líneas {start}-{end}: {rules}",
3041
+ "prose.fix_item": "{rule}: {fix}",
3042
+ "prose.warn.command": "{target} parece escrito por IA ({spans}). Reescríbelo antes de enviarlo. {fixes}. Si es texto de marca escrito a propósito, muéstrale dos alternativas a la persona.",
3043
+ "prose.block.command": "bloqueado: {target} parece escrito por IA ({spans}), y el preset de este proyecto no deja enviarlo. {fixes}. Reescríbelo y vuelve a ejecutar el comando.",
3044
+ "prose.block.unreadable": "bloqueado: no se pudo leer {file} para revisar el texto ({detail}). Revisa la ruta y vuelve a ejecutar el comando.",
3045
+ "prose.block.marker": "bloqueado: esto añadiría el marcador {marker} en {file}, y solo la persona puede ponerlo. Reescribe el pasaje, o pide a la persona que ponga el marcador en su propio editor.",
3046
+ "prose.check.file": "{file}: {count} pasaje(s) que parecen escritos por IA",
3047
+ "prose.check.span": " {file}:{start}-{end} puntos {score}: {rules}",
3048
+ "prose.check.fix": " haz en su lugar: {fix}",
3049
+ "prose.check.allowed": "{file}: omitido, la persona lo marcó con {marker}",
3050
+ "prose.check.clean": "{count} archivo(s) revisado(s), nada parece escrito por IA. Este lo escribió el mono solo.",
3051
+ "prose.error.found": "{count} pasaje(s) parecen escritos por IA",
3052
+ "prose.error.no_input": "nada que revisar: pasa archivos, --stdin, o ejecútalo dentro de un repositorio git para revisar los Markdown modificados",
3053
+ "prose.error.unreadable": "no se pudo leer {file} ({detail})",
3054
+ "prose.block.marker_shell": "bloqueado: este comando escribe el marcador {marker}, y solo la persona puede ponerlo. Reescribe el pasaje, o pide a la persona que ponga el marcador en su propio editor.",
3055
+ "prose.rule.requester-reference": "di el motivo técnico del cambio, sin decir quién lo pidió",
3056
+ "prose.rule.person-as-reason": "nombra la propiedad del cambio en lugar de la persona",
3057
+ "prose.rule.origin-project": "describe la propiedad técnica, sin citar de dónde salió",
3058
+ "prose.rule.first-person": "escribe lo que hace el cambio, en presente, sin yo ni tú",
3059
+ "prose.rule.padding": "quita el preámbulo y empieza por lo que cambió",
3060
+ "gate.require.prose": "el Markdown cambiado no parece escrito por una máquina",
3061
+ "gate.prose.unavailable": "no se pudieron leer los archivos Markdown cambiados (git no disponible)",
3062
+ "gate.prose.finding": "{file}:{line} parece escrito por una máquina ({rules})",
3063
+ "gate.prose.warning": "solo aviso en este preset: {file}:{line} parece escrito por una máquina ({rules})",
3064
+ "gate.prose.clean": "{count} archivo(s) Markdown cambiado(s), ninguno parece escrito por una máquina"
2983
3065
  };
2984
3066
 
2985
3067
  // src/i18n/pt-BR.json
@@ -3291,7 +3373,47 @@ var pt_BR_default = {
3291
3373
  "gate.secrets.finding": "segredo {rule} em {file}:{line}",
3292
3374
  "gate.secrets.incident": "incidente de segredo {id} está {state}",
3293
3375
  "gate.secrets.clean": "nenhum segredo novo nas mudanças e nenhum incidente aberto",
3294
- "gate.require.secrets": "nenhum segredo novo e nenhum incidente de segredo aberto"
3376
+ "gate.require.secrets": "nenhum segredo novo e nenhum incidente de segredo aberto",
3377
+ "prose.command.check": "Confere se o texto tem padrões que o leitor reconhece como escrita de IA (só local)",
3378
+ "prose.rule.opener-routine": "abra com o que aconteceu com alguém: um comando, um horário, uma mensagem de erro",
3379
+ "prose.rule.reassurance": "corte a frase de consolo; descreva o problema e deixe o leitor julgar",
3380
+ "prose.rule.slogan-closer": "termine num fato ou no próximo passo, sem slogan",
3381
+ "prose.rule.rhythmic-triplet": "junte as frases curtas numa só que diga o que cada parte faz",
3382
+ "prose.rule.imperative-fragment": "troque as ordens soltas por uma frase sobre o que o produto faz",
3383
+ "prose.rule.antithesis": "diga o que é e tire o contraste com o que não é",
3384
+ "prose.rule.abstract-noun": "troque o substantivo abstrato pela cena: quem fez o quê, onde",
3385
+ "prose.rule.crutch-word": "use a palavra simples, ou um número que prove o que diz",
3386
+ "prose.rule.colon-heading": "faça do título a pergunta ou a tarefa de quem lê",
3387
+ "prose.rule.decorative-emoji": "tire o emoji; o título já carrega o sentido",
3388
+ "prose.rule.em-dash-series": "use ponto, vírgula ou parênteses no lugar dos travessões",
3389
+ "prose.target.commit": "A mensagem de commit",
3390
+ "prose.target.pr": "O texto do PR",
3391
+ "prose.target.issue": "O texto da issue",
3392
+ "prose.span": "linhas {start}-{end}: {rules}",
3393
+ "prose.fix_item": "{rule}: {fix}",
3394
+ "prose.warn.command": "{target} tem cara de texto de IA ({spans}). Reescreva antes de enviar. {fixes}. Se for texto de marca escrito de propósito, mostre duas alternativas à pessoa.",
3395
+ "prose.block.command": "bloqueado: {target} tem cara de texto de IA ({spans}), e o preset deste projeto não deixa enviar. {fixes}. Reescreva e rode o comando de novo.",
3396
+ "prose.block.unreadable": "bloqueado: não deu para ler {file} para conferir o texto ({detail}). Confira o caminho e rode o comando de novo.",
3397
+ "prose.block.marker": "bloqueado: isto acrescentaria o marcador {marker} em {file}, e só a pessoa pode pôr esse marcador. Reescreva o trecho, ou peça à pessoa que ponha o marcador no editor dela.",
3398
+ "prose.check.file": "{file}: {count} trecho(s) com cara de texto de IA",
3399
+ "prose.check.span": " {file}:{start}-{end} pontos {score}: {rules}",
3400
+ "prose.check.fix": " faça no lugar: {fix}",
3401
+ "prose.check.allowed": "{file}: pulado, a pessoa marcou com {marker}",
3402
+ "prose.check.clean": "{count} arquivo(s) conferido(s), nada com cara de IA. Esse o macaco escreveu sozinho.",
3403
+ "prose.error.found": "{count} trecho(s) com cara de texto de IA",
3404
+ "prose.error.no_input": "nada para conferir: passe arquivos, --stdin, ou rode dentro de um repositório git para conferir os Markdown alterados",
3405
+ "prose.error.unreadable": "não deu para ler {file} ({detail})",
3406
+ "prose.block.marker_shell": "bloqueado: este comando escreve o marcador {marker}, e só a pessoa pode pôr esse marcador. Reescreva o trecho, ou peça à pessoa que ponha o marcador no editor dela.",
3407
+ "prose.rule.requester-reference": "diga o motivo técnico da mudança, sem dizer quem pediu",
3408
+ "prose.rule.person-as-reason": "nomeie a propriedade da mudança no lugar da pessoa",
3409
+ "prose.rule.origin-project": "descreva a propriedade técnica, sem citar de onde veio",
3410
+ "prose.rule.first-person": "escreva o que a mudança faz, no presente, sem eu nem você",
3411
+ "prose.rule.padding": "corte o preâmbulo e comece pelo que mudou",
3412
+ "gate.require.prose": "Markdown alterado sem cara de texto de máquina",
3413
+ "gate.prose.unavailable": "não deu para ler os arquivos Markdown alterados (git indisponível)",
3414
+ "gate.prose.finding": "{file}:{line} tem cara de texto de máquina ({rules})",
3415
+ "gate.prose.warning": "só aviso neste preset: {file}:{line} tem cara de texto de máquina ({rules})",
3416
+ "gate.prose.clean": "{count} arquivo(s) Markdown alterado(s), nenhum com cara de texto de máquina"
3295
3417
  };
3296
3418
 
3297
3419
  // src/i18n/catalogs.ts
@@ -3518,8 +3640,8 @@ var requirement2 = (want, ctx) => {
3518
3640
  var gitCheck = (check2, ctx) => {
3519
3641
  const wants = check2.require ?? ["committed"];
3520
3642
  const missing = wants.flatMap((want) => {
3521
- const problem3 = requirement2(want, ctx);
3522
- return problem3 === void 0 ? [] : [problem3];
3643
+ const problem4 = requirement2(want, ctx);
3644
+ return problem4 === void 0 ? [] : [problem4];
3523
3645
  });
3524
3646
  return missing.length > 0 ? failed(missing) : passed([note("gate.git.ok", { require: wants.join(", ") })]);
3525
3647
  };
@@ -3678,6 +3800,668 @@ var prOutcome = (check2, ctx) => {
3678
3800
  }
3679
3801
  };
3680
3802
 
3803
+ // src/install/layout.ts
3804
+ var STATE_DIR = ".finitemonkey";
3805
+ var SETTINGS_FILE = ".claude/settings.json";
3806
+ var GITIGNORE_FILE = ".gitignore";
3807
+ var CONFIG_FILE = ".finitemonkey/config.yml";
3808
+ var LOCK_FILE = ".finitemonkey/lock.json";
3809
+ var BACKUPS_DIR = ".finitemonkey/backups";
3810
+ var LOCKS_DIR = ".finitemonkey/locks";
3811
+ var INSTALL_LOCK = ".finitemonkey/locks/install.lock";
3812
+ var LOCAL_BIN = "node_modules/finitemonkey/dist/finitemonkey.mjs";
3813
+ var VENDOR_BIN = ".finitemonkey/runtime/dist/finitemonkey.mjs";
3814
+ var VENDOR_PACKAGE = ".finitemonkey/runtime/package.json";
3815
+ var LOCK_SCHEMA = "finitemonkey/lock@1";
3816
+ var PLAN_SCHEMA = "finitemonkey/install-plan@1";
3817
+ var GENERATED_MARKER = "finitemonkey:generated v=1";
3818
+ var BLOCK_BEGIN = "# finitemonkey:begin v=1";
3819
+ var BLOCK_END = "# finitemonkey:end";
3820
+ var INSTALL_LOCK_STALE_MS = 5 * 60 * 1e3;
3821
+ var PRESETS = ["neutral", "gabriel"];
3822
+ var parsePreset = (raw) => raw === void 0 ? "neutral" : PRESETS.find((preset) => preset === raw);
3823
+ var COMMITTED_STATE = ["config.yml", "runtime/", "tasks/", "rules.yml", "policy.yml", "findings/"];
3824
+
3825
+ // src/prose/preset.ts
3826
+ var PRESET_LINE = /^preset:\s*([A-Za-z0-9_-]+)\s*$/m;
3827
+ var presetFromConfig = (text6) => parsePreset(PRESET_LINE.exec(text6)?.[1]) ?? "neutral";
3828
+ var projectPreset = (runtime, root) => {
3829
+ const config = runtime.readText(toAbsolutePath(CONFIG_FILE, root));
3830
+ return config.ok ? presetFromConfig(config.value) : "neutral";
3831
+ };
3832
+
3833
+ // src/prose/catalog.ts
3834
+ var lazy = (build2) => {
3835
+ let value;
3836
+ return () => value ??= build2();
3837
+ };
3838
+ var bounded = (alternatives, flags) => new RegExp(`(?<![\\p{L}\\p{N}])(?:${alternatives.join("|")})(?![\\p{L}\\p{N}])`, flags);
3839
+ var phrases = (...alternatives) => bounded(alternatives, "iu");
3840
+ var casedPhrases = (...alternatives) => bounded(alternatives, "u");
3841
+ var OPENER = () => [
3842
+ phrases("you know the (?:routine|drill)", "we've all been there", "we all know the feeling", "sound familiar\\?"),
3843
+ phrases(
3844
+ "(?:você )?(?:já )?conhece (?:a rotina|o roteiro|o esquema)",
3845
+ "todo mundo já passou por isso",
3846
+ "soa familiar\\?"
3847
+ ),
3848
+ phrases(
3849
+ "(?:ya )?conoces (?:la rutina|el guion|el procedimiento)",
3850
+ "todos hemos pasado por (?:eso|esto)",
3851
+ "¿te suena\\?"
3852
+ )
3853
+ ];
3854
+ var REASSURANCE = () => [
3855
+ phrases(
3856
+ "(?:is|it's|that's|that is) not a character flaw",
3857
+ "nobody(?:'s| is)(?: here)? (?:lazy|to blame)",
3858
+ "no one(?:'s| is)(?: here)? (?:lazy|to blame)",
3859
+ "it(?:'s| is) not your fault",
3860
+ "you(?:'re| are) not alone"
3861
+ ),
3862
+ phrases(
3863
+ "não é (?:um )?defeito de caráter",
3864
+ "ninguém (?:aqui )?é preguiçoso",
3865
+ "a culpa não é sua",
3866
+ "não é culpa sua",
3867
+ "você não está sozinh[oa]"
3868
+ ),
3869
+ phrases("no es un defecto de carácter", "(?:aquí )?nadie es vago", "no es (?:tu|su) culpa", "no estás sol[oa]")
3870
+ ];
3871
+ var SLOGAN = () => [
3872
+ phrases("and that changes everything", "that's the whole point", "simple as that", "it's that simple"),
3873
+ phrases("e isso muda tudo", "simples assim", "é simples assim", "e ponto final"),
3874
+ phrases("y eso lo cambia todo", "así de simple", "así de fácil", "y punto")
3875
+ ];
3876
+ var ABSTRACT = () => [
3877
+ phrases(
3878
+ "the (?:pace|speed|rhythm|nature|reality) of (?:the )?(?:work|modern work|software|development)",
3879
+ "the reality is",
3880
+ "in today's (?:fast-paced|digital|modern) world"
3881
+ ),
3882
+ phrases(
3883
+ "(?:o|a) (?:ritmo|velocidade|natureza|realidade) do (?:trabalho|desenvolvimento)",
3884
+ "a realidade é que",
3885
+ "no mundo (?:atual|de hoje)"
3886
+ ),
3887
+ phrases(
3888
+ "(?:el|la) (?:ritmo|velocidad|naturaleza|realidad) del (?:trabajo|desarrollo)",
3889
+ "la realidad es que",
3890
+ "en el mundo (?:actual|de hoy)"
3891
+ )
3892
+ ];
3893
+ var CRUTCH = () => [
3894
+ phrases(
3895
+ "delv(?:e|es|ed|ing)",
3896
+ "seamless(?:ly)?",
3897
+ "robust(?:ness)?",
3898
+ "leverag(?:e|es|ed|ing)",
3899
+ "game[- ]chang(?:er|ers|ing)",
3900
+ "it(?:'s| is) worth noting",
3901
+ "cutting[- ]edge",
3902
+ "unlock the (?:full )?(?:power|potential)",
3903
+ "supercharg(?:e|ed|es)",
3904
+ "effortless(?:ly)?",
3905
+ "in conclusion",
3906
+ "at the end of the day",
3907
+ "navigate the (?:complexities|landscape)"
3908
+ ),
3909
+ phrases(
3910
+ "vale (?:ressaltar|destacar|lembrar que)",
3911
+ "cabe (?:destacar|ressaltar)",
3912
+ "é importante (?:ressaltar|destacar|notar)",
3913
+ "em (?:resumo|suma)",
3914
+ "robust(?:o|a|os|as|ez)",
3915
+ "alavanc(?:ar|a|ando|ado)",
3916
+ "divisor de águas",
3917
+ "sem esforço",
3918
+ "potencializ(?:ar|a|ando)",
3919
+ "de forma fluida"
3920
+ ),
3921
+ phrases(
3922
+ "en (?:resumen|conclusión)",
3923
+ "cabe (?:destacar|señalar|mencionar)",
3924
+ "vale la pena (?:destacar|mencionar|señalar)",
3925
+ "es importante (?:destacar|señalar)",
3926
+ "robust(?:o|a|os|as|ez)",
3927
+ "apalanc(?:ar|a|ando)",
3928
+ "sin fisuras",
3929
+ "sin esfuerzo",
3930
+ "cambia las reglas del juego"
3931
+ )
3932
+ ];
3933
+ var phraseRules = lazy(() => [
3934
+ { rule: "opener-routine", weight: 3, patterns: OPENER() },
3935
+ { rule: "reassurance", weight: 3, patterns: REASSURANCE() },
3936
+ { rule: "slogan-closer", weight: 2, patterns: SLOGAN() },
3937
+ { rule: "abstract-noun", weight: 2, patterns: ABSTRACT() },
3938
+ { rule: "crutch-word", weight: 3, patterns: CRUTCH() }
3939
+ ]);
3940
+ var IMPERATIVE_VERBS = /* @__PURE__ */ new Set([
3941
+ "type",
3942
+ "ship",
3943
+ "work",
3944
+ "write",
3945
+ "let",
3946
+ "build",
3947
+ "go",
3948
+ "stop",
3949
+ "start",
3950
+ "keep",
3951
+ "make",
3952
+ "focus",
3953
+ "forget",
3954
+ "trust",
3955
+ "code",
3956
+ "digite",
3957
+ "trabalhe",
3958
+ "produza",
3959
+ "deixe",
3960
+ "escreva",
3961
+ "faça",
3962
+ "pare",
3963
+ "comece",
3964
+ "entregue",
3965
+ "esqueça",
3966
+ "confie",
3967
+ "foque",
3968
+ "programe",
3969
+ "escribe",
3970
+ "teclea",
3971
+ "trabaja",
3972
+ "produce",
3973
+ "deja",
3974
+ "haz",
3975
+ "empieza",
3976
+ "olvida",
3977
+ "confía",
3978
+ "programa",
3979
+ "entrega"
3980
+ ]);
3981
+ var STRUCTURE_WEIGHTS = {
3982
+ triplet: 1,
3983
+ imperative: 1,
3984
+ closer: 1,
3985
+ antithesis: 1,
3986
+ antithesisPair: 2,
3987
+ heading: 1,
3988
+ emoji: 1,
3989
+ dash: 1,
3990
+ dashSeries: 2
3991
+ };
3992
+
3993
+ // src/prose/marker.ts
3994
+ var ALLOW_MARKER = "finitemonkey:prose-allow";
3995
+ var ALLOW_FILE_MARKER = "finitemonkey:prose-allow-file";
3996
+ var ALLOW_COMMENT = `<!-- ${ALLOW_MARKER} -->`;
3997
+ var ALLOW_FILE_COMMENT = `<!-- ${ALLOW_FILE_MARKER} -->`;
3998
+ var LINE_MARKER = /<!--\s*finitemonkey:prose-allow\s*-->/;
3999
+ var FILE_MARKER = /<!--\s*finitemonkey:prose-allow-file\s*-->/;
4000
+ var ANY_MARKER = /<!--\s*finitemonkey:prose-allow(?:-file)?\s*-->/g;
4001
+ var INLINE_CODE = /`[^`]*`/g;
4002
+ var FENCE = /^\s*(```|~~~)/;
4003
+ var outsideCode = (line) => line.replace(INLINE_CODE, "");
4004
+ var unfencedLines = (text6) => {
4005
+ let fenced = false;
4006
+ return text6.split("\n").filter((line) => {
4007
+ if (FENCE.test(line)) fenced = !fenced;
4008
+ return !fenced && !FENCE.test(line);
4009
+ });
4010
+ };
4011
+ var allowsWholeFile = (text6) => unfencedLines(text6).some((line) => FILE_MARKER.test(outsideCode(line)));
4012
+ var allowsLine = (line) => LINE_MARKER.test(outsideCode(line));
4013
+ var markerCount = (text6) => text6.match(ANY_MARKER)?.length ?? 0;
4014
+ var isMarkerOnlyLine = (line) => allowsLine(line) && outsideCode(line).replace(LINE_MARKER, "").trim() === "";
4015
+
4016
+ // src/prose/extract.ts
4017
+ var FENCE2 = /^\s*(```|~~~)/;
4018
+ var HEADING = /^#{1,6}\s+/;
4019
+ var ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
4020
+ var SKIPPED_LINE = /^\s*[|>]|^\s*<(?!!--)/;
4021
+ var INDENTED_CODE = /^(?: {4}|\t)/;
4022
+ var SENTENCE_END = /(?<=[.!?…])\s+/;
4023
+ var MASKS = [
4024
+ [/!\[[^\]]*\]\([^)]*\)/g, ""],
4025
+ [/\[[^\]]*\]\([^)]*\)/g, "x"],
4026
+ [/\[[^\]]+\]\[[^\]]*\]/g, "x"],
4027
+ [/`[^`]*`/g, "x"],
4028
+ [/https?:\/\/\S+/g, "x"],
4029
+ [/"[^"]*"|“[^”]*”|«[^»]*»/g, "x"],
4030
+ [/<[^>]+>/g, ""],
4031
+ [/\*\*|__/g, ""]
4032
+ ];
4033
+ var maskInline = (line) => MASKS.reduce((text6, [pattern, replacement]) => text6.replace(pattern, replacement), line);
4034
+ var BlockReader = class {
4035
+ blocks = [];
4036
+ current;
4037
+ skipNext = false;
4038
+ flush() {
4039
+ const block3 = this.current;
4040
+ this.current = void 0;
4041
+ if (block3 === void 0 || block3.pieces.length === 0) return;
4042
+ if (this.skipNext) {
4043
+ this.skipNext = false;
4044
+ return;
4045
+ }
4046
+ this.blocks.push(block3);
4047
+ }
4048
+ skipNextBlock() {
4049
+ this.flush();
4050
+ this.skipNext = true;
4051
+ }
4052
+ start(kind, piece) {
4053
+ this.flush();
4054
+ this.current = { kind, pieces: [piece] };
4055
+ if (kind === "heading") this.flush();
4056
+ }
4057
+ append(piece) {
4058
+ this.current ??= { kind: "sentence", pieces: [] };
4059
+ this.current.pieces.push(piece);
4060
+ }
4061
+ get open() {
4062
+ return this.current !== void 0;
4063
+ }
4064
+ };
4065
+ var readLine = (reader, raw, line) => {
4066
+ if (raw.trim() === "") {
4067
+ reader.flush();
4068
+ } else if (isMarkerOnlyLine(raw)) {
4069
+ reader.skipNextBlock();
4070
+ } else if (allowsLine(raw)) {
4071
+ return;
4072
+ } else if (HEADING.test(raw)) {
4073
+ reader.start("heading", { text: raw.replace(HEADING, ""), line });
4074
+ } else if (SKIPPED_LINE.test(raw) || raw.trim().startsWith("<!--")) {
4075
+ reader.flush();
4076
+ } else if (ITEM.test(raw)) {
4077
+ reader.start("item", { text: raw.replace(ITEM, ""), line });
4078
+ } else if (INDENTED_CODE.test(raw) && !reader.open) {
4079
+ return;
4080
+ } else {
4081
+ reader.append({ text: raw.trim(), line });
4082
+ }
4083
+ };
4084
+ var skipFrontmatter = (lines2) => {
4085
+ if (lines2[0]?.trim() !== "---") return 0;
4086
+ const end = lines2.findIndex((line, index) => index > 0 && line.trim() === "---");
4087
+ return end < 0 ? 0 : end + 1;
4088
+ };
4089
+ var readBlocks = (lines2) => {
4090
+ const reader = new BlockReader();
4091
+ let fenced = false;
4092
+ let commented = false;
4093
+ for (let index = skipFrontmatter(lines2); index < lines2.length; index += 1) {
4094
+ const raw = lines2[index] ?? "";
4095
+ if (FENCE2.test(raw)) {
4096
+ fenced = !fenced;
4097
+ reader.flush();
4098
+ } else if (commented) {
4099
+ commented = !raw.includes("-->");
4100
+ } else if (!fenced) {
4101
+ if (raw.trim().startsWith("<!--") && !raw.includes("-->")) commented = true;
4102
+ readLine(reader, raw, index + 1);
4103
+ }
4104
+ }
4105
+ reader.flush();
4106
+ return reader.blocks;
4107
+ };
4108
+ var splitBlock = (block3) => {
4109
+ if (block3.kind === "heading") return block3.pieces.map((piece) => ({ ...piece, text: maskInline(piece.text) }));
4110
+ const starts = [];
4111
+ let joined = "";
4112
+ for (const piece of block3.pieces) {
4113
+ starts.push(joined.length);
4114
+ joined += `${maskInline(piece.text)} `;
4115
+ }
4116
+ const lineAt = (offset2) => {
4117
+ const index = starts.findLastIndex((start) => start <= offset2);
4118
+ return block3.pieces[Math.max(index, 0)]?.line ?? 0;
4119
+ };
4120
+ let offset = 0;
4121
+ return joined.split(SENTENCE_END).flatMap((sentence) => {
4122
+ const at = joined.indexOf(sentence, offset);
4123
+ offset = at + sentence.length;
4124
+ const text6 = sentence.trim();
4125
+ return text6 === "" ? [] : [{ text: text6, line: lineAt(at) }];
4126
+ });
4127
+ };
4128
+ var unitsOf = (blocks) => {
4129
+ let section = 0;
4130
+ return blocks.flatMap((block3, paragraph) => {
4131
+ if (block3.kind === "heading") section += 1;
4132
+ const pieces = splitBlock(block3);
4133
+ const next = blocks[paragraph + 1];
4134
+ const closesSection = block3.kind === "sentence" && (next === void 0 || next.kind === "heading");
4135
+ return pieces.map((piece, index) => ({
4136
+ text: piece.text,
4137
+ line: piece.line,
4138
+ paragraph,
4139
+ section,
4140
+ kind: block3.kind,
4141
+ lastInParagraph: index === pieces.length - 1,
4142
+ closesSection
4143
+ }));
4144
+ });
4145
+ };
4146
+ var extractProse = (text6) => {
4147
+ if (allowsWholeFile(text6)) return { units: [], allowed: true };
4148
+ const lines2 = text6.split("\n").map(withoutCarriageReturn);
4149
+ return { units: unitsOf(readBlocks(lines2)), allowed: false };
4150
+ };
4151
+
4152
+ // src/prose/types.ts
4153
+ var DENSITY_WINDOW = 6;
4154
+ var DENSITY_THRESHOLD = 4;
4155
+
4156
+ // src/prose/record-catalog.ts
4157
+ var TECH_NOUNS = [
4158
+ "readme",
4159
+ "commit",
4160
+ "pr",
4161
+ "issue",
4162
+ "botão",
4163
+ "erro",
4164
+ "aviso",
4165
+ "bloqueio",
4166
+ "hook",
4167
+ "portão",
4168
+ "prompt",
4169
+ "modal",
4170
+ "rodapé",
4171
+ "cabeçalho",
4172
+ "título",
4173
+ "menu",
4174
+ "banner",
4175
+ "changelog",
4176
+ "site",
4177
+ "app",
4178
+ "formulário",
4179
+ "plugin",
4180
+ "comando",
4181
+ "contrato",
4182
+ "relatório",
4183
+ "projeto",
4184
+ "arquivo",
4185
+ "código",
4186
+ "pacote",
4187
+ "repositório",
4188
+ "produto",
4189
+ "módulo",
4190
+ "sistema"
4191
+ ].join("|");
4192
+ var REQUESTER = () => [
4193
+ phrases(
4194
+ "as requested",
4195
+ "per (?:your|the user's|their) request",
4196
+ "as you (?:asked|requested|wanted)",
4197
+ "the user (?:asked|wants|wanted|requested)",
4198
+ "as discussed",
4199
+ "as agreed",
4200
+ "per (?:the|our) (?:conversation|discussion|chat)"
4201
+ ),
4202
+ phrases(
4203
+ "conforme (?:solicitado|pedido|combinado)",
4204
+ "a pedido d[oa]",
4205
+ "como (?:pedido|solicitado|combinado|conversado)",
4206
+ "como você (?:pediu|quer|queria)",
4207
+ "o usuário (?:pediu|quer|queria)"
4208
+ ),
4209
+ phrases(
4210
+ "según lo (?:pedido|solicitado|acordado)",
4211
+ "a (?:pedido|petición) de",
4212
+ "como (?:pediste|se pidió|acordamos)",
4213
+ "el usuario (?:pidió|quiere|quería)"
4214
+ )
4215
+ ];
4216
+ var PERSON = () => [
4217
+ casedPhrases("\\p{Lu}\\p{Ll}+'s (?:text|request|idea|suggestion|wording|preference|feedback)"),
4218
+ phrases(`(?:texto|pedido|ideia|sugestão|preferência|gosto) d[oa] (?!(?:${TECH_NOUNS})(?![\\p{L}\\p{N}]))\\p{L}+`),
4219
+ casedPhrases("(?:texto|pedido|idea|sugerencia|preferencia) de \\p{Lu}\\p{Ll}+")
4220
+ ];
4221
+ var ORIGIN = () => [
4222
+ phrases("in the style of", "(?:just )?like \\S+ does", "inspired by", "copied from"),
4223
+ phrases("no estilo d[oa]", "igual a[oà]s?", "inspirad[oa] n[oa]", "como o \\S+ faz"),
4224
+ phrases("al estilo de", "igual que", "como hace \\S+", "inspirad[oa] en")
4225
+ ];
4226
+ var FIRST_PERSON = () => [
4227
+ phrases("I (?:changed|added|fixed|made|updated|removed|did|think|decided)", "you (?:asked|wanted|requested)"),
4228
+ phrases(
4229
+ "fiz",
4230
+ "eu (?:mudei|troquei|adicionei|corrigi|fiz|acho)",
4231
+ "mudei",
4232
+ "troquei",
4233
+ "adicionei",
4234
+ "corrigi",
4235
+ "você (?:pediu|quis)"
4236
+ ),
4237
+ phrases("hice", "cambié", "añadí", "arreglé", "corregí", "pediste", "(?:tú|usted) (?:pediste|pidió)")
4238
+ ];
4239
+ var PADDING = () => [
4240
+ phrases(
4241
+ "this commit",
4242
+ "this (?:PR|pull request) (?:aims|is meant|is intended|seeks|tries) to",
4243
+ "the (?:purpose|goal) of this (?:PR|pull request|commit)"
4244
+ ),
4245
+ phrases("este commit", "este PR (?:tem como objetivo|visa|busca|pretende)", "o objetivo deste (?:PR|commit)"),
4246
+ phrases("este commit", "este PR (?:tiene como objetivo|busca|pretende)", "el objetivo de este (?:PR|commit)")
4247
+ ];
4248
+ var recordRules = lazy(() => [
4249
+ { rule: "requester-reference", weight: DENSITY_THRESHOLD, patterns: REQUESTER() },
4250
+ { rule: "person-as-reason", weight: DENSITY_THRESHOLD, patterns: PERSON() },
4251
+ { rule: "origin-project", weight: DENSITY_THRESHOLD, patterns: ORIGIN() },
4252
+ { rule: "first-person", weight: DENSITY_THRESHOLD, patterns: FIRST_PERSON() },
4253
+ { rule: "padding", weight: DENSITY_THRESHOLD, patterns: PADDING() }
4254
+ ]);
4255
+
4256
+ // src/prose/structure.ts
4257
+ var SHORT_SENTENCE = 6;
4258
+ var SHORT_IMPERATIVE = 8;
4259
+ var SHORT_CLOSER = 8;
4260
+ var TRIAD_PART = 5;
4261
+ var WORD = /[\p{L}\p{N}]/u;
4262
+ var EMOJI = new RegExp("\\p{Extended_Pictographic}", "u");
4263
+ var COLON_HEADING = /^[^:]{1,40}:\s+\S/;
4264
+ var ANTITHESIS_TAIL = /,\s+(?:not|never|não|nunca|no)\s+([^,;:]+)$/iu;
4265
+ var NOT_BUT = /(?<![\p{L}])(?:not\s[^,.;]{1,40},\s*but|não\s[^,.;]{1,40},\s*mas|no\s[^,.;]{1,40},\s*sino)(?![\p{L}])/iu;
4266
+ var NEGATION = /(?<![\p{L}])(?:is not|isn't|it's not|that's not|that is not|não é|no es)(?![\p{L}])/iu;
4267
+ var AFFIRMATION = /^(?:it is|it's|this is|é|es|isso é|eso es)(?![\p{L}])/iu;
4268
+ var CONNECTIVES = new Set(
4269
+ "with in on at by for of to from if is are was never not x por em en com con de do da del se si é es são son nunca não no para que como".split(
4270
+ " "
4271
+ )
4272
+ );
4273
+ var wordsOf = (text6) => text6.split(/\s+/).filter((word) => WORD.test(word));
4274
+ var hit = (rule, units, index, weight) => {
4275
+ const unit = units[index];
4276
+ return { rule, unit: index, line: unit?.line ?? 0, weight, excerpt: unit?.text ?? "" };
4277
+ };
4278
+ var isProse = (unit) => unit?.kind === "sentence";
4279
+ var samePara = (units, from, count3) => {
4280
+ const first = units[from];
4281
+ if (!isProse(first)) return false;
4282
+ return units.slice(from, from + count3).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
4283
+ };
4284
+ var runsOf = (units, size, matches) => {
4285
+ const starts = [];
4286
+ for (let index = 0; index + size <= units.length; index += 1) {
4287
+ const window = units.slice(index, index + size);
4288
+ if (!samePara(units, index, size) || !window.every(matches)) continue;
4289
+ if (starts.at(-1) !== void 0 && index - (starts.at(-1) ?? 0) < size) continue;
4290
+ starts.push(index);
4291
+ }
4292
+ return starts;
4293
+ };
4294
+ var isShort = (limit) => (unit) => wordsOf(unit.text).length <= limit;
4295
+ var isImperative = (unit) => {
4296
+ const [first] = wordsOf(unit.text);
4297
+ return isShort(SHORT_IMPERATIVE)(unit) && IMPERATIVE_VERBS.has((first ?? "").toLowerCase());
4298
+ };
4299
+ var isPhrase = (part) => {
4300
+ const words = wordsOf(part);
4301
+ return words.length >= 2 && words.length <= TRIAD_PART && !CONNECTIVES.has((words[0] ?? "").toLowerCase());
4302
+ };
4303
+ var isCommaTriad = (unit) => {
4304
+ const parts = unit.text.replace(/[.!?…]+$/, "").split(/,\s*|\s+(?:and|e|y)\s+/);
4305
+ return parts.length === 3 && parts.every(isPhrase);
4306
+ };
4307
+ var triplets = (units) => [
4308
+ ...runsOf(units, 3, isShort(SHORT_SENTENCE)).map(
4309
+ (index) => hit("rhythmic-triplet", units, index, STRUCTURE_WEIGHTS.triplet)
4310
+ ),
4311
+ ...units.flatMap(
4312
+ (unit, index) => isProse(unit) && unit.lastInParagraph && isCommaTriad(unit) ? [hit("rhythmic-triplet", units, index, STRUCTURE_WEIGHTS.triplet)] : []
4313
+ )
4314
+ ];
4315
+ var imperatives = (units) => runsOf(units, 2, isImperative).map((index) => hit("imperative-fragment", units, index, STRUCTURE_WEIGHTS.imperative));
4316
+ var closers = (units) => units.flatMap((unit, index) => {
4317
+ const isCloser = isProse(unit) && unit.lastInParagraph && unit.closesSection && index > 0 && isShort(SHORT_CLOSER)(unit) && !/\d/.test(unit.text);
4318
+ return isCloser ? [hit("slogan-closer", units, index, STRUCTURE_WEIGHTS.closer)] : [];
4319
+ });
4320
+ var antitheses = (units) => units.flatMap((unit, index) => {
4321
+ if (unit.kind === "heading") return [];
4322
+ const tail2 = ANTITHESIS_TAIL.exec(unit.text)?.[1];
4323
+ if (tail2 !== void 0 && wordsOf(tail2).length <= SHORT_SENTENCE || NOT_BUT.test(unit.text)) {
4324
+ return [hit("antithesis", units, index, STRUCTURE_WEIGHTS.antithesis)];
4325
+ }
4326
+ const next = units[index + 1];
4327
+ const paired = NEGATION.test(unit.text) && next?.paragraph === unit.paragraph && AFFIRMATION.test(next.text);
4328
+ return paired ? [hit("antithesis", units, index, STRUCTURE_WEIGHTS.antithesisPair)] : [];
4329
+ });
4330
+ var headings = (units) => units.flatMap(
4331
+ (unit, index) => unit.kind === "heading" && COLON_HEADING.test(unit.text) ? [hit("colon-heading", units, index, STRUCTURE_WEIGHTS.heading)] : []
4332
+ );
4333
+ var emoji = (units) => units.flatMap(
4334
+ (unit, index) => EMOJI.test(unit.text) ? [hit("decorative-emoji", units, index, STRUCTURE_WEIGHTS.emoji)] : []
4335
+ );
4336
+ var dashes = (units) => {
4337
+ const perParagraph = /* @__PURE__ */ new Map();
4338
+ for (const unit of units) {
4339
+ const count3 = unit.text.split("—").length - 1;
4340
+ perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count3);
4341
+ }
4342
+ return [...perParagraph].flatMap(([paragraph, count3]) => {
4343
+ if (count3 === 0) return [];
4344
+ const index = units.findIndex((unit) => unit.paragraph === paragraph && unit.text.includes("—"));
4345
+ const weight = count3 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
4346
+ return [hit("em-dash-series", units, index, weight)];
4347
+ });
4348
+ };
4349
+ var structureHits = (units) => [
4350
+ ...triplets(units),
4351
+ ...imperatives(units),
4352
+ ...closers(units),
4353
+ ...antitheses(units),
4354
+ ...headings(units),
4355
+ ...emoji(units),
4356
+ ...dashes(units)
4357
+ ];
4358
+
4359
+ // src/prose/score.ts
4360
+ var phraseHits = (units, catalog) => units.flatMap(
4361
+ (unit, index) => catalog.flatMap(({ rule, weight, patterns }) => {
4362
+ const match = patterns.map((pattern) => pattern.exec(unit.text)).find((found) => found !== null);
4363
+ return match === void 0 ? [] : [{ rule, unit: index, line: unit.line, weight, excerpt: match[0] }];
4364
+ })
4365
+ );
4366
+ var windowEnd = (units, from, size) => {
4367
+ const section = units[from]?.section;
4368
+ let to = from;
4369
+ while (to + 1 < Math.min(units.length, from + size) && units[to + 1]?.section === section) to += 1;
4370
+ return to;
4371
+ };
4372
+ var windowScore = (hits, window) => hits.filter((item) => item.unit >= window.from && item.unit <= window.to).reduce((sum, item) => sum + item.weight, 0);
4373
+ var hotWindows = (hits, units, size, threshold) => {
4374
+ const windows = [];
4375
+ for (let from = 0; from < units.length; from += 1) {
4376
+ const window = { from, to: windowEnd(units, from, size) };
4377
+ if (windowScore(hits, window) >= threshold) windows.push(window);
4378
+ }
4379
+ return windows;
4380
+ };
4381
+ var spanOf2 = (hits, from, to) => {
4382
+ const inside = hits.filter((item) => item.unit >= from && item.unit <= to);
4383
+ const lines2 = inside.map((item) => item.line);
4384
+ return {
4385
+ startLine: Math.min(...lines2),
4386
+ endLine: Math.max(...lines2),
4387
+ score: inside.reduce((sum, item) => sum + item.weight, 0),
4388
+ hits: inside
4389
+ };
4390
+ };
4391
+ var mergeSpans = (hits, windows) => {
4392
+ const ranges = [];
4393
+ for (const { from, to } of windows) {
4394
+ const previous = ranges.at(-1);
4395
+ if (previous !== void 0 && from <= previous[1] + 1) previous[1] = Math.max(previous[1], to);
4396
+ else ranges.push([from, to]);
4397
+ }
4398
+ return ranges.map(([from, to]) => spanOf2(hits, from, to)).filter((span) => span.hits.length > 0);
4399
+ };
4400
+ var byPosition = (left, right) => left.unit - right.unit || left.line - right.line;
4401
+ var scoreProse = (text6, options = {}) => {
4402
+ const { units, allowed: allowed2 } = extractProse(text6);
4403
+ const size = options.window ?? DENSITY_WINDOW;
4404
+ const threshold = options.threshold ?? DENSITY_THRESHOLD;
4405
+ const catalog = options.context === "record" ? [...phraseRules(), ...recordRules()] : phraseRules();
4406
+ const hits = [...phraseHits(units, catalog), ...structureHits(units)].sort(byPosition);
4407
+ const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold));
4408
+ return { hits, spans, allowed: allowed2 };
4409
+ };
4410
+ var isFlagged = (report2) => report2.spans.length > 0;
4411
+
4412
+ // src/prose/gate.ts
4413
+ var MARKDOWN = /\.(?:md|markdown|mdx)$/i;
4414
+ var RECORD_FILE = /(?:^|\/)(?:CHANGELOG|RELEASE[-_]?NOTES|HISTORY)[^/]*$/i;
4415
+ var contextOf = (file) => RECORD_FILE.test(toPosixPath(file)) ? "record" : "docs";
4416
+ var listed = (runtime, root, args) => {
4417
+ const result = runtime.git(args, root);
4418
+ if (!result.ok) return reject(`git ${args.join(" ")} failed`);
4419
+ return accept(result.stdout.split("\0").map(toPosixPath));
4420
+ };
4421
+ var changedMarkdown = (runtime, root) => {
4422
+ const tracked = listed(runtime, root, ["diff", "--name-only", "-z", "--diff-filter=ACMR", "HEAD"]);
4423
+ if (!tracked.ok) return tracked;
4424
+ const untracked3 = listed(runtime, root, ["ls-files", "-z", "--others", "--exclude-standard"]);
4425
+ if (!untracked3.ok) return untracked3;
4426
+ const files = [...tracked.value, ...untracked3.value].filter(
4427
+ (file) => MARKDOWN.test(file) && !file.startsWith(".finitemonkey/")
4428
+ );
4429
+ return accept([...new Set(files)].sort());
4430
+ };
4431
+ var scoreFiles = (runtime, root, files, forced) => {
4432
+ const reports = [];
4433
+ for (const file of files) {
4434
+ const text6 = runtime.readText(toAbsolutePath(file, root));
4435
+ if (!text6.ok) return reject(`${file}: ${text6.error}`);
4436
+ reports.push({ file: toPosixPath(file), report: scoreProse(text6.value, { context: forced ?? contextOf(file) }) });
4437
+ }
4438
+ return accept(reports);
4439
+ };
4440
+ var proseGateStatus = (runtime, root) => {
4441
+ const changed = changedMarkdown(runtime, root);
4442
+ if (!changed.ok) return changed;
4443
+ const files = scoreFiles(runtime, root, changed.value);
4444
+ if (!files.ok) return files;
4445
+ return accept({
4446
+ files: files.value,
4447
+ flagged: files.value.filter((item) => isFlagged(item.report)),
4448
+ blocking: projectPreset(runtime, root) === "gabriel"
4449
+ });
4450
+ };
4451
+
4452
+ // src/gate/checks/prose.ts
4453
+ var MAX_LISTED3 = 10;
4454
+ var rulesOf = (span) => [...new Set(span.hits.map((hit2) => hit2.rule))].join(", ");
4455
+ var proseOutcome = (status) => {
4456
+ if (!status.ok) return unverified("tool_missing", [note("gate.prose.unavailable")]);
4457
+ const { files, flagged, blocking } = status.value;
4458
+ const key = blocking ? "gate.prose.finding" : "gate.prose.warning";
4459
+ const notes2 = flagged.flatMap((item) => item.report.spans.map((span) => ({ file: item.file, span }))).slice(0, MAX_LISTED3).map(({ file, span }) => note(key, { file, line: span.startLine, rules: rulesOf(span) }));
4460
+ if (notes2.length === 0) return passed([note("gate.prose.clean", { count: files.length })]);
4461
+ return blocking ? failed(notes2) : passed(notes2);
4462
+ };
4463
+ var proseCheck = (_check, ctx) => proseOutcome(proseGateStatus(ctx.runtime, ctx.paths.root));
4464
+
3681
4465
  // src/secrets/baseline.ts
3682
4466
  import { join as join7 } from "node:path";
3683
4467
  var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
@@ -3945,7 +4729,7 @@ var MAX_FILE_BYTES = 2 * 1024 * 1024;
3945
4729
  var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
3946
4730
  var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
3947
4731
  var binary = (text6) => text6.slice(0, 8e3).includes("\0");
3948
- var listed = (runtime, root, target) => {
4732
+ var listed2 = (runtime, root, target) => {
3949
4733
  const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
3950
4734
  if (git.ok)
3951
4735
  return accept(
@@ -3962,7 +4746,7 @@ var scanFile = (runtime, root, file) => {
3962
4746
  return scanText(text6.value, { path: file }).map((finding) => locate(finding, file, finding.line));
3963
4747
  };
3964
4748
  var scanPath = (runtime, root, target) => {
3965
- const files = listed(runtime, root, target);
4749
+ const files = listed2(runtime, root, target);
3966
4750
  if (!files.ok) return files;
3967
4751
  return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
3968
4752
  };
@@ -3987,9 +4771,9 @@ var scanHistory = async (runtime, root) => {
3987
4771
 
3988
4772
  // src/secrets/gate.ts
3989
4773
  var untracked2 = (runtime, root) => {
3990
- const listed2 = runtime.git(["ls-files", "-z", "--others", "--exclude-standard"], root);
3991
- if (!listed2.ok) return [];
3992
- return listed2.stdout.split("\0").map(toPosixPath).filter((file) => file !== "" && !file.startsWith(".finitemonkey/")).flatMap((file) => scanFile(runtime, root, file));
4774
+ const listed3 = runtime.git(["ls-files", "-z", "--others", "--exclude-standard"], root);
4775
+ if (!listed3.ok) return [];
4776
+ return listed3.stdout.split("\0").map(toPosixPath).filter((file) => file !== "" && !file.startsWith(".finitemonkey/")).flatMap((file) => scanFile(runtime, root, file));
3993
4777
  };
3994
4778
  var secretsGateStatus = (runtime, root, since) => {
3995
4779
  const changed = scanStaged(runtime, root, true);
@@ -4004,7 +4788,7 @@ var secretsGateStatus = (runtime, root, since) => {
4004
4788
  };
4005
4789
 
4006
4790
  // src/gate/checks/secrets.ts
4007
- var MAX_LISTED3 = 10;
4791
+ var MAX_LISTED4 = 10;
4008
4792
  var EPOCH = "1970-01-01T00:00:00.000Z";
4009
4793
  var sinceOf = (ctx) => {
4010
4794
  const session = readSession(ctx.runtime, ctx.paths);
@@ -4015,8 +4799,8 @@ var secretsCheck = (_check, ctx) => {
4015
4799
  if (!status.ok) return unverified("tool_missing", [note("gate.secrets.unavailable")]);
4016
4800
  const { findings, openIncidents } = status.value;
4017
4801
  const notes2 = [
4018
- ...findings.slice(0, MAX_LISTED3).map((finding) => note("gate.secrets.finding", { file: finding.file, line: finding.line, rule: finding.ruleId })),
4019
- ...openIncidents.slice(0, MAX_LISTED3).map((incident) => note("gate.secrets.incident", { id: incident.id, state: incident.state }))
4802
+ ...findings.slice(0, MAX_LISTED4).map((finding) => note("gate.secrets.finding", { file: finding.file, line: finding.line, rule: finding.ruleId })),
4803
+ ...openIncidents.slice(0, MAX_LISTED4).map((incident) => note("gate.secrets.incident", { id: incident.id, state: incident.state }))
4020
4804
  ];
4021
4805
  return notes2.length > 0 ? failed(notes2) : passed([note("gate.secrets.clean")]);
4022
4806
  };
@@ -4169,7 +4953,7 @@ var forChange = (input, change) => {
4169
4953
  var analyzeTree = (input) => input.changes.flatMap((change) => forChange(input, change));
4170
4954
 
4171
4955
  // src/gate/checks/tests.ts
4172
- var MAX_LISTED4 = 10;
4956
+ var MAX_LISTED5 = 10;
4173
4957
  var violationNote = (violation) => note(`gate.tests.${violation.kind}`, {
4174
4958
  file: violation.path,
4175
4959
  before: violation.before ?? 0,
@@ -4194,7 +4978,7 @@ var treeViolations = (ctx) => {
4194
4978
  var testsCheck = (check2, ctx) => {
4195
4979
  const violations = treeViolations(ctx);
4196
4980
  const grant = violations.length === 0 ? void 0 : activeGrant(ctx.runtime, ctx.paths, "tests", nowMs(ctx.runtime), ctx.sessionId);
4197
- const notes2 = violations.slice(0, MAX_LISTED4).map(violationNote);
4981
+ const notes2 = violations.slice(0, MAX_LISTED5).map(violationNote);
4198
4982
  if (violations.length > 0 && grant === void 0) return failed([...notes2, note("gate.tests.fix")]);
4199
4983
  const released2 = grant === void 0 ? {} : { releasedBy: `person@${grant.grantedAt}` };
4200
4984
  const releaseNotes = grant === void 0 ? [] : [...notes2, note("gate.note.released", { guard: "tests", reason: grant.reason })];
@@ -4212,7 +4996,8 @@ var DEFAULT_CHECKS = {
4212
4996
  git: gitCheck,
4213
4997
  pr: prOutcome,
4214
4998
  manual: manualCheck,
4215
- secrets: secretsCheck
4999
+ secrets: secretsCheck,
5000
+ prose: proseCheck
4216
5001
  };
4217
5002
 
4218
5003
  // src/gate/report.ts
@@ -4483,10 +5268,10 @@ var helpFor = (ctx, spec) => {
4483
5268
  var topicHelp = (ctx, registry, topic) => {
4484
5269
  const resolved = resolveCommand(registry, topic);
4485
5270
  if (resolved !== void 0) return helpFor(ctx, resolved.spec);
4486
- const problem3 = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
5271
+ const problem4 = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
4487
5272
  command: topic.join(" ")
4488
5273
  });
4489
- return failureFrom(ctx.t, problem3);
5274
+ return failureFrom(ctx.t, problem4);
4490
5275
  };
4491
5276
  var helpCommand = {
4492
5277
  path: ["help"],
@@ -4821,7 +5606,7 @@ var testsGuard = (input) => {
4821
5606
 
4822
5607
  // src/gate/hooks/config.ts
4823
5608
  import { join as join12 } from "node:path";
4824
- var CONFIG_FILE = "config-hooks.json";
5609
+ var CONFIG_FILE2 = "config-hooks.json";
4825
5610
  var GUARDED_SOURCES = /* @__PURE__ */ new Set(["user_settings", "project_settings", "local_settings"]);
4826
5611
  var countIn = (value) => {
4827
5612
  if (typeof value === "string") return /\bfinitemonkey\b|\bfm hook\b/.test(value) ? 1 : 0;
@@ -4844,11 +5629,11 @@ var read2 = (runtime, path) => {
4844
5629
  return text6.ok ? text6.value : void 0;
4845
5630
  };
4846
5631
  var known = (runtime, paths) => {
4847
- const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE))?.["hooks"];
5632
+ const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE2))?.["hooks"];
4848
5633
  return isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter((entry2) => typeof entry2[1] === "number")) : {};
4849
5634
  };
4850
5635
  var save = (runtime, paths, counts) => {
4851
- writeJsonFile(runtime, paths.cache(CONFIG_FILE), { hooks: { ...counts } });
5636
+ writeJsonFile(runtime, paths.cache(CONFIG_FILE2), { hooks: { ...counts } });
4852
5637
  };
4853
5638
  var recordConfigHooks = (runtime, paths, env) => {
4854
5639
  const counts = Object.fromEntries(
@@ -5242,9 +6027,10 @@ var subagentStopGate = (input) => isOwnWorktree(input) ? stopGate(input) : { kin
5242
6027
  // src/gate/hooks/subagent.ts
5243
6028
  import { join as join13 } from "node:path";
5244
6029
  var NO_CONTRACT_AGENTS = /(^|:)(reviewer|verifier|explore|plan|claude-code-guide|statusline-setup)$/i;
6030
+ var KNOWN_KINDS = /* @__PURE__ */ new Set(["command", "tests", "diff", "placeholders", "git", "pr", "manual", "secrets", "prose"]);
5245
6031
  var requirementText = (t, check2) => {
5246
6032
  const key = `gate.require.${check2.kind}`;
5247
- const known2 = ["command", "tests", "diff", "placeholders", "git", "pr", "manual", "secrets"].includes(check2.kind);
6033
+ const known2 = KNOWN_KINDS.has(check2.kind);
5248
6034
  return `${check2.id} ${t(known2 ? key : "gate.require.other", { command: check2.run ?? check2.kind, kind: check2.kind })}`;
5249
6035
  };
5250
6036
  var contractFacts = (t, contract, briefing) => [
@@ -5260,8 +6046,8 @@ var subagentStart = (input) => {
5260
6046
  const { runtime, t } = input;
5261
6047
  const active = readState(runtime, paths).active;
5262
6048
  if (active === void 0) return { kind: "allow" };
5263
- const disk = (relative6) => {
5264
- const text6 = runtime.readText(toAbsolutePath(join13(paths.root, relative6)));
6049
+ const disk = (relative8) => {
6050
+ const text6 = runtime.readText(toAbsolutePath(join13(paths.root, relative8)));
5265
6051
  return text6.ok ? text6.value : void 0;
5266
6052
  };
5267
6053
  const loaded = loadTask(runtime, paths, { disk, base: () => void 0 }, active);
@@ -5282,14 +6068,8 @@ var PORTAO_HANDLERS = {
5282
6068
  ConfigChange: [configChange]
5283
6069
  };
5284
6070
 
5285
- // src/secrets/guards/describe.ts
5286
- var MAX_LISTED5 = 5;
5287
- var describeFindings = (t, items) => {
5288
- const listed2 = items.slice(0, MAX_LISTED5).map(({ ruleId, where }) => t("secrets.found_item", { rule: ruleId, where }));
5289
- const rest = items.length - listed2.length;
5290
- return rest > 0 ? `${listed2.join("; ")}; ${t("secrets.found_more", { count: rest })}` : listed2.join("; ");
5291
- };
5292
- var lineWhere = (t, finding) => t("secrets.where.line", { line: finding.line, column: finding.column });
6071
+ // src/prose/marker-guard.ts
6072
+ import { relative as relative5 } from "node:path";
5293
6073
 
5294
6074
  // src/secrets/guards/tool-input.ts
5295
6075
  var toolName2 = (payload) => optionalString(payload.raw, "tool_name") ?? "";
@@ -5332,6 +6112,305 @@ var stringLeaves = (value, out = []) => {
5332
6112
  return out;
5333
6113
  };
5334
6114
 
6115
+ // src/prose/shell-words.ts
6116
+ var HEREDOC = /<<-?[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[^\n]*\n([\s\S]*?)\n[ \t]*\2[ \t]*(?=\n|$)/g;
6117
+ var HEREDOC_REF = /<<__FM_HEREDOC_(\d+)__/;
6118
+ var SEPARATOR = /[;&|\n]/;
6119
+ var ASSIGNMENT2 = /^[A-Za-z_][A-Za-z0-9_]*=/;
6120
+ var WRAPPERS2 = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin", "env"]);
6121
+ var liftHeredocs = (command) => {
6122
+ const heredocs = [];
6123
+ const text6 = command.replace(HEREDOC, (_match, _quote, _tag, body) => {
6124
+ heredocs.push(body);
6125
+ return `<<__FM_HEREDOC_${String(heredocs.length - 1)}__`;
6126
+ });
6127
+ return { text: text6, heredocs };
6128
+ };
6129
+ var endWord = (scan) => {
6130
+ if (scan.word !== void 0) scan.words.push(scan.word);
6131
+ scan.word = void 0;
6132
+ };
6133
+ var endSegment = (scan) => {
6134
+ endWord(scan);
6135
+ if (scan.words.length > 0) scan.segments.push(scan.words);
6136
+ scan.words = [];
6137
+ };
6138
+ var quotedEnd2 = (text6, from, quote) => {
6139
+ let index = from;
6140
+ while (index < text6.length && text6[index] !== quote) index += quote === '"' && text6[index] === "\\" ? 2 : 1;
6141
+ return index;
6142
+ };
6143
+ var unescape = (value) => value.replace(/\\(["\\$`])/g, "$1");
6144
+ var splitWords = (text6) => {
6145
+ const scan = { words: [], segments: [], word: void 0 };
6146
+ for (let index = 0; index < text6.length; index += 1) {
6147
+ const char = text6[index] ?? "";
6148
+ if (char === '"' || char === "'") {
6149
+ const end = quotedEnd2(text6, index + 1, char);
6150
+ const inner = text6.slice(index + 1, end);
6151
+ scan.word = (scan.word ?? "") + (char === '"' ? unescape(inner) : inner);
6152
+ index = end;
6153
+ } else if (char === "\\" && index + 1 < text6.length) {
6154
+ scan.word = (scan.word ?? "") + (text6[index + 1] === "\n" ? "" : text6[index + 1] ?? "");
6155
+ index += 1;
6156
+ } else if (SEPARATOR.test(char)) {
6157
+ endSegment(scan);
6158
+ } else if (/\s/.test(char)) {
6159
+ endWord(scan);
6160
+ } else {
6161
+ scan.word = (scan.word ?? "") + char;
6162
+ }
6163
+ }
6164
+ endSegment(scan);
6165
+ return scan.segments;
6166
+ };
6167
+ var segmentOf = (words) => {
6168
+ let index = 0;
6169
+ while (index < words.length && (ASSIGNMENT2.test(words[index] ?? "") || WRAPPERS2.has(words[index] ?? ""))) index += 1;
6170
+ const program = words[index];
6171
+ if (program === void 0) return void 0;
6172
+ return { program: program.split(/[\\/]/).pop() ?? program, args: words.slice(index + 1) };
6173
+ };
6174
+ var shellWords = (command) => {
6175
+ const { text: text6, heredocs } = liftHeredocs(command);
6176
+ const segments = splitWords(text6).flatMap((words) => {
6177
+ const segment = segmentOf(words);
6178
+ return segment === void 0 ? [] : [segment];
6179
+ });
6180
+ return { segments, heredocs };
6181
+ };
6182
+ var heredocIn = (value, heredocs) => {
6183
+ const index = HEREDOC_REF.exec(value)?.[1];
6184
+ return index === void 0 ? void 0 : heredocs[Number(index)];
6185
+ };
6186
+
6187
+ // src/prose/marker-guard.ts
6188
+ var ALLOW = { kind: "allow" };
6189
+ var SHELLS2 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
6190
+ var READ_ONLY = /* @__PURE__ */ new Set(["grep", "rg", "cat", "less", "head", "tail", "ls", "wc", "finitemonkey", "fm"]);
6191
+ var GIT_READ_ONLY = /* @__PURE__ */ new Set(["grep", "log", "show", "diff", "status", "blame"]);
6192
+ var counted2 = (input, key) => markerCount(optionalString(input, key) ?? "");
6193
+ var existingCount = (input, file) => {
6194
+ if (file === "") return 0;
6195
+ const read3 = input.runtime.readText(toAbsolutePath(file, input.cwd));
6196
+ return read3.ok ? markerCount(read3.value) : 0;
6197
+ };
6198
+ var editPairs = (input) => {
6199
+ const edits = input["edits"];
6200
+ if (!Array.isArray(edits)) return [];
6201
+ return edits.flatMap((edit) => isRecord(edit) ? [[counted2(edit, "old_string"), counted2(edit, "new_string")]] : []);
6202
+ };
6203
+ var addsMarker = (input, tool, inputs, file) => {
6204
+ switch (tool) {
6205
+ case "Write":
6206
+ return counted2(inputs, "content") > existingCount(input, file);
6207
+ case "Edit":
6208
+ return counted2(inputs, "new_string") > counted2(inputs, "old_string");
6209
+ case "MultiEdit":
6210
+ return editPairs(inputs).reduce((sum, [before, after]) => sum + after - before, 0) > 0;
6211
+ case "NotebookEdit":
6212
+ return counted2(inputs, "new_source") > 0;
6213
+ default:
6214
+ return false;
6215
+ }
6216
+ };
6217
+ var isReadOnly = (program, args) => READ_ONLY.has(program) || program === "git" && GIT_READ_ONLY.has(args[0] ?? "");
6218
+ var shellAddsMarker = (command) => markerCount(command) > 0 && shellWords(command).segments.some((segment) => !isReadOnly(segment.program, segment.args));
6219
+ var shown = (cwd, file) => {
6220
+ const rel = toPosixPath(relative5(cwd, file));
6221
+ return file === "" || rel.startsWith("..") ? toPosixPath(file) : rel;
6222
+ };
6223
+ var blocked = (input, file) => ({
6224
+ kind: "block",
6225
+ reason: input.t("prose.block.marker", { marker: ALLOW_COMMENT, file })
6226
+ });
6227
+ var proseMarkerGuard = (input) => {
6228
+ const tool = toolName2(input.payload);
6229
+ const inputs = toolInput2(input.payload);
6230
+ if (SHELLS2.has(tool)) {
6231
+ if (!shellAddsMarker(optionalString(inputs, "command") ?? "")) return ALLOW;
6232
+ return { kind: "block", reason: input.t("prose.block.marker_shell", { marker: ALLOW_COMMENT }) };
6233
+ }
6234
+ const file = optionalString(inputs, "file_path") ?? optionalString(inputs, "notebook_path") ?? "";
6235
+ return addsMarker(input, tool, inputs, file) ? blocked(input, shown(input.cwd, file)) : ALLOW;
6236
+ };
6237
+
6238
+ // src/secrets/project.ts
6239
+ var projectRoot = (ctx) => {
6240
+ const top = ctx.runtime.git(["rev-parse", "--show-toplevel"], ctx.cwd);
6241
+ const path = top.stdout.trim();
6242
+ return top.ok && path !== "" ? toAbsolutePath(path) : ctx.cwd;
6243
+ };
6244
+ var isPerson = (ctx) => ctx.io.stdin.isTTY === true && ctx.env["CLAUDECODE"] === void 0 && ctx.env["CLAUDE_CODE_ENTRYPOINT"] === void 0;
6245
+ var personName = (ctx) => {
6246
+ const name = ctx.runtime.git(["config", "user.name"], ctx.cwd).stdout.trim();
6247
+ return name !== "" ? name : ctx.env["USER"] ?? ctx.env["USERNAME"] ?? "person";
6248
+ };
6249
+
6250
+ // src/prose/command-text.ts
6251
+ var GIT_FLAGS = { title: [], body: ["-m", "--message"], file: ["-F", "--file"] };
6252
+ var GH_FLAGS = { title: ["-t", "--title"], body: ["-b", "--body"], file: ["-F", "--body-file"] };
6253
+ var GIT_GLOBAL_WITH_VALUE = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace"]);
6254
+ var CLUSTER_WITH_MESSAGE = /^-[a-zA-Z]*m$/;
6255
+ var flagValues = (args, spec) => {
6256
+ const known2 = [...spec.title, ...spec.body, ...spec.file];
6257
+ const out = [];
6258
+ for (let index = 0; index < args.length; index += 1) {
6259
+ const arg = args[index] ?? "";
6260
+ const equals = arg.indexOf("=");
6261
+ const name = arg.startsWith("--") && equals > 0 ? arg.slice(0, equals) : arg;
6262
+ if (name !== arg && known2.includes(name)) {
6263
+ out.push({ flag: name, value: arg.slice(equals + 1) });
6264
+ } else if (known2.includes(arg) || spec === GIT_FLAGS && CLUSTER_WITH_MESSAGE.test(arg)) {
6265
+ out.push({ flag: known2.includes(arg) ? arg : "-m", value: args[index + 1] ?? "" });
6266
+ index += 1;
6267
+ }
6268
+ }
6269
+ return out;
6270
+ };
6271
+ var sourceOf = (value, isFile, segment, heredocs) => {
6272
+ const inline3 = heredocIn(value, heredocs);
6273
+ if (inline3 !== void 0) return { kind: "inline", text: inline3 };
6274
+ if (!isFile) return { kind: "inline", text: value };
6275
+ if (value !== "-") return { kind: "file", path: value };
6276
+ const piped = segment.args.map((arg) => heredocIn(arg, heredocs)).find((text6) => text6 !== void 0);
6277
+ return { kind: "inline", text: piped ?? "" };
6278
+ };
6279
+ var collect2 = (target, segment, args, spec, heredocs) => {
6280
+ const values = flagValues(args, spec);
6281
+ if (values.length === 0) return void 0;
6282
+ const title = values.find((item) => spec.title.includes(item.flag));
6283
+ const bodies = values.filter((item) => !spec.title.includes(item.flag)).map((item) => sourceOf(item.value, spec.file.includes(item.flag), segment, heredocs));
6284
+ return { target, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
6285
+ };
6286
+ var gitSubcommand = (args) => {
6287
+ let index = 0;
6288
+ while (index < args.length && (args[index] ?? "").startsWith("-")) {
6289
+ index += GIT_GLOBAL_WITH_VALUE.has(args[index] ?? "") ? 2 : 1;
6290
+ }
6291
+ return index;
6292
+ };
6293
+ var fromSegment = (segment, heredocs) => {
6294
+ if (segment.program === "git") {
6295
+ const at = gitSubcommand(segment.args);
6296
+ if (segment.args[at] !== "commit") return void 0;
6297
+ return collect2("commit", segment, segment.args.slice(at + 1), GIT_FLAGS, heredocs);
6298
+ }
6299
+ if (segment.program !== "gh") return void 0;
6300
+ const [noun, verb] = segment.args;
6301
+ const isPr = noun === "pr" && (verb === "create" || verb === "edit");
6302
+ const isIssue = noun === "issue" && (verb === "create" || verb === "edit");
6303
+ if (!isPr && !isIssue) return void 0;
6304
+ return collect2(isPr ? "pr" : "issue", segment, segment.args.slice(2), GH_FLAGS, heredocs);
6305
+ };
6306
+ var proseInCommand = (command) => {
6307
+ const { segments, heredocs } = shellWords(command);
6308
+ return segments.flatMap((segment) => {
6309
+ const found = fromSegment(segment, heredocs);
6310
+ return found === void 0 ? [] : [found];
6311
+ });
6312
+ };
6313
+
6314
+ // src/prose/describe.ts
6315
+ var RULE_FIX_KEYS = {
6316
+ "opener-routine": "prose.rule.opener-routine",
6317
+ reassurance: "prose.rule.reassurance",
6318
+ "slogan-closer": "prose.rule.slogan-closer",
6319
+ "rhythmic-triplet": "prose.rule.rhythmic-triplet",
6320
+ "imperative-fragment": "prose.rule.imperative-fragment",
6321
+ antithesis: "prose.rule.antithesis",
6322
+ "abstract-noun": "prose.rule.abstract-noun",
6323
+ "crutch-word": "prose.rule.crutch-word",
6324
+ "colon-heading": "prose.rule.colon-heading",
6325
+ "decorative-emoji": "prose.rule.decorative-emoji",
6326
+ "em-dash-series": "prose.rule.em-dash-series",
6327
+ "requester-reference": "prose.rule.requester-reference",
6328
+ "person-as-reason": "prose.rule.person-as-reason",
6329
+ "origin-project": "prose.rule.origin-project",
6330
+ "first-person": "prose.rule.first-person",
6331
+ padding: "prose.rule.padding"
6332
+ };
6333
+ var MAX_SPANS = 3;
6334
+ var MAX_FIXES = 3;
6335
+ var rulesOf2 = (spans) => {
6336
+ const weights = /* @__PURE__ */ new Map();
6337
+ for (const hit2 of spans.flatMap((span) => span.hits))
6338
+ weights.set(hit2.rule, (weights.get(hit2.rule) ?? 0) + hit2.weight);
6339
+ return [...weights].sort((left, right) => right[1] - left[1]).map(([rule]) => rule);
6340
+ };
6341
+ var spanRules = (span) => {
6342
+ const seen = /* @__PURE__ */ new Set();
6343
+ return span.hits.filter((hit2) => !seen.has(hit2.rule) && seen.add(hit2.rule)).map(
6344
+ (hit2) => hit2.rule === "crutch-word" || hit2.rule === "opener-routine" ? `${hit2.rule} "${hit2.excerpt}"` : hit2.rule
6345
+ ).join(", ");
6346
+ };
6347
+ var describeSpans = (t, spans) => spans.slice(0, MAX_SPANS).map((span) => t("prose.span", { start: span.startLine, end: span.endLine, rules: spanRules(span) })).join("; ");
6348
+ var describeFixes = (t, spans) => rulesOf2(spans).slice(0, MAX_FIXES).map((rule) => t("prose.fix_item", { rule, fix: t(RULE_FIX_KEYS[rule]) })).join("; ");
6349
+
6350
+ // src/prose/text-guard.ts
6351
+ var ALLOW2 = { kind: "allow" };
6352
+ var SHELLS3 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
6353
+ var TARGET_KEYS = {
6354
+ commit: "prose.target.commit",
6355
+ pr: "prose.target.pr",
6356
+ issue: "prose.target.issue"
6357
+ };
6358
+ var readSource = (input, source) => {
6359
+ if (source.kind === "inline") return accept(source.text);
6360
+ const read3 = input.runtime.readText(toAbsolutePath(source.path, input.cwd));
6361
+ return read3.ok ? read3 : reject({ file: source.path, detail: read3.error });
6362
+ };
6363
+ var textOf = (input, found) => {
6364
+ const parts = [];
6365
+ for (const source of [...found.title === void 0 ? [] : [found.title], ...found.bodies]) {
6366
+ const read3 = readSource(input, source);
6367
+ if (!read3.ok) return read3;
6368
+ parts.push(read3.value);
6369
+ }
6370
+ return accept(parts.join("\n\n"));
6371
+ };
6372
+ var decisionFor = (input, found, blocking) => {
6373
+ const text6 = textOf(input, found);
6374
+ if (!text6.ok) {
6375
+ return blocking ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
6376
+ }
6377
+ const report2 = scoreProse(text6.value, { context: "record" });
6378
+ if (report2.allowed || report2.spans.length === 0) return void 0;
6379
+ const params2 = {
6380
+ target: input.t(TARGET_KEYS[found.target]),
6381
+ spans: describeSpans(input.t, report2.spans),
6382
+ fixes: describeFixes(input.t, report2.spans)
6383
+ };
6384
+ if (blocking) return { kind: "block", reason: input.t("prose.block.command", params2) };
6385
+ const additionalContext = `finitemonkey: ${input.t("prose.warn.command", params2)}`;
6386
+ return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext } } };
6387
+ };
6388
+ var proseCommandGuard = (input) => {
6389
+ if (!SHELLS3.has(toolName2(input.payload))) return ALLOW2;
6390
+ const found = proseInCommand(optionalString(toolInput2(input.payload), "command") ?? "");
6391
+ if (found.length === 0) return ALLOW2;
6392
+ const blocking = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
6393
+ for (const item of found) {
6394
+ const decision = decisionFor(input, item, blocking);
6395
+ if (decision !== void 0) return decision;
6396
+ }
6397
+ return ALLOW2;
6398
+ };
6399
+
6400
+ // src/prose/hooks.ts
6401
+ var PROSE_HANDLERS = {
6402
+ PreToolUse: [proseMarkerGuard, proseCommandGuard]
6403
+ };
6404
+
6405
+ // src/secrets/guards/describe.ts
6406
+ var MAX_LISTED6 = 5;
6407
+ var describeFindings = (t, items) => {
6408
+ const listed3 = items.slice(0, MAX_LISTED6).map(({ ruleId, where }) => t("secrets.found_item", { rule: ruleId, where }));
6409
+ const rest = items.length - listed3.length;
6410
+ return rest > 0 ? `${listed3.join("; ")}; ${t("secrets.found_more", { count: rest })}` : listed3.join("; ");
6411
+ };
6412
+ var lineWhere = (t, finding) => t("secrets.where.line", { line: finding.line, column: finding.column });
6413
+
5335
6414
  // src/secrets/guards/exposure.ts
5336
6415
  var HOOK_ACTOR = "finitemonkey";
5337
6416
  var liveFindings = (input, texts) => {
@@ -5376,7 +6455,7 @@ var secretsPostToolUse = (input) => {
5376
6455
  };
5377
6456
 
5378
6457
  // src/secrets/guards/pre-tool.ts
5379
- import { relative as relative5 } from "node:path";
6458
+ import { relative as relative6 } from "node:path";
5380
6459
 
5381
6460
  // src/secrets/guards/paths.ts
5382
6461
  var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
@@ -5484,10 +6563,10 @@ var commitIntent = (segments) => {
5484
6563
  // src/secrets/guards/shell.ts
5485
6564
  var SEPARATORS2 = /\|\||&&|[|;&\n]/;
5486
6565
  var TOKEN = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
5487
- var ASSIGNMENT2 = /^[A-Za-z_][A-Za-z0-9_]*=/;
6566
+ var ASSIGNMENT3 = /^[A-Za-z_][A-Za-z0-9_]*=/;
5488
6567
  var OUTPUT_REDIRECT = /^(?:\d|&)?>{1,2}\|?$/;
5489
6568
  var OUTPUT_ATTACHED = /^(?:\d|&)?>{1,2}\|?\S/;
5490
- var WRAPPERS2 = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin"]);
6569
+ var WRAPPERS3 = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin"]);
5491
6570
  var tokensOf = (text6) => Array.from(text6.matchAll(TOKEN), (match) => match[1] ?? match[2] ?? match[3] ?? "");
5492
6571
  var splitRedirects = (tokens) => {
5493
6572
  const words = [];
@@ -5509,24 +6588,24 @@ var splitRedirects = (tokens) => {
5509
6588
  }
5510
6589
  return { words, inputs };
5511
6590
  };
5512
- var segmentOf = (text6) => {
6591
+ var segmentOf2 = (text6) => {
5513
6592
  const { words, inputs } = splitRedirects(tokensOf(text6));
5514
6593
  let index = 0;
5515
- while (index < words.length && (ASSIGNMENT2.test(words[index] ?? "") || WRAPPERS2.has(words[index] ?? ""))) index += 1;
6594
+ while (index < words.length && (ASSIGNMENT3.test(words[index] ?? "") || WRAPPERS3.has(words[index] ?? ""))) index += 1;
5516
6595
  const program = words[index];
5517
6596
  if (program === void 0) return inputs.length > 0 ? { program: "", args: [], redirectedFrom: inputs } : void 0;
5518
6597
  return { program: program.split(/[\\/]/).pop() ?? program, args: words.slice(index + 1), redirectedFrom: inputs };
5519
6598
  };
5520
6599
  var segmentsOf = (command) => command.split(SEPARATORS2).flatMap((part) => {
5521
- const segment = segmentOf(part);
6600
+ const segment = segmentOf2(part);
5522
6601
  return segment === void 0 ? [] : [segment];
5523
6602
  });
5524
6603
 
5525
6604
  // src/secrets/guards/pre-tool.ts
5526
- var ALLOW = { kind: "allow" };
6605
+ var ALLOW3 = { kind: "allow" };
5527
6606
  var relativeFile = (cwd, file) => {
5528
6607
  if (file === "") return file;
5529
- const rel = relative5(cwd, file).split("\\").join("/");
6608
+ const rel = relative6(cwd, file).split("\\").join("/");
5530
6609
  return rel.startsWith("..") || rel === "" ? file : rel;
5531
6610
  };
5532
6611
  var writeDecision = (input, tool) => {
@@ -5541,7 +6620,7 @@ var writeDecision = (input, tool) => {
5541
6620
  }));
5542
6621
  return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
5543
6622
  }
5544
- return ALLOW;
6623
+ return ALLOW3;
5545
6624
  };
5546
6625
  var bashMessage = (t, finding) => {
5547
6626
  switch (finding.kind) {
@@ -5555,7 +6634,7 @@ var bashMessage = (t, finding) => {
5555
6634
  };
5556
6635
  var commitDecision = (input, includeUnstaged) => {
5557
6636
  const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
5558
- if (!staged.ok || staged.value.length === 0) return ALLOW;
6637
+ if (!staged.ok || staged.value.length === 0) return ALLOW3;
5559
6638
  const items = staged.value.map((finding) => ({
5560
6639
  ruleId: finding.ruleId,
5561
6640
  where: input.t("secrets.where.file", { file: finding.file, line: finding.line, column: finding.column })
@@ -5564,7 +6643,7 @@ var commitDecision = (input, includeUnstaged) => {
5564
6643
  };
5565
6644
  var bashDecision = (input) => {
5566
6645
  const command = optionalString(toolInput2(input.payload), "command") ?? "";
5567
- if (command === "") return ALLOW;
6646
+ if (command === "") return ALLOW3;
5568
6647
  const segments = segmentsOf(command);
5569
6648
  const [first] = bashFindings(command, segments);
5570
6649
  if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
@@ -5574,23 +6653,23 @@ var bashDecision = (input) => {
5574
6653
  return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
5575
6654
  }
5576
6655
  const commit2 = commitIntent(segments);
5577
- return commit2 === void 0 ? ALLOW : commitDecision(input, commit2.includeUnstaged);
6656
+ return commit2 === void 0 ? ALLOW3 : commitDecision(input, commit2.includeUnstaged);
5578
6657
  };
5579
6658
  var readDecision = (input) => {
5580
6659
  const inputs = toolInput2(input.payload);
5581
6660
  const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
5582
- if (target === "" || !isSensitivePath(target)) return ALLOW;
6661
+ if (target === "" || !isSensitivePath(target)) return ALLOW3;
5583
6662
  return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
5584
6663
  };
5585
6664
  var WRITERS2 = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
5586
- var SHELLS2 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
6665
+ var SHELLS4 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
5587
6666
  var READS = /* @__PURE__ */ new Set(["Read", "Grep"]);
5588
6667
  var secretsPreToolUse = (input) => {
5589
6668
  const tool = toolName2(input.payload);
5590
6669
  if (WRITERS2.has(tool)) return writeDecision(input, tool);
5591
- if (SHELLS2.has(tool)) return bashDecision(input);
6670
+ if (SHELLS4.has(tool)) return bashDecision(input);
5592
6671
  if (READS.has(tool)) return readDecision(input);
5593
- return ALLOW;
6672
+ return ALLOW3;
5594
6673
  };
5595
6674
 
5596
6675
  // src/hooks/handlers.ts
@@ -5648,7 +6727,7 @@ var SECRETS_HANDLERS = {
5648
6727
  UserPromptSubmit: [secretsUserPromptSubmit],
5649
6728
  PostToolUse: [secretsPostToolUse]
5650
6729
  };
5651
- var DEFAULT_HANDLERS = mergeHandlers(SECRETS_HANDLERS, PORTAO_HANDLERS);
6730
+ var DEFAULT_HANDLERS = mergeHandlers(SECRETS_HANDLERS, PORTAO_HANDLERS, PROSE_HANDLERS);
5652
6731
 
5653
6732
  // src/hooks/payload.ts
5654
6733
  var EMPTY_PAYLOAD = {
@@ -5795,28 +6874,6 @@ var runHook = async (event, options) => {
5795
6874
  // src/install/plugin-yield.ts
5796
6875
  import { join as join14 } from "node:path";
5797
6876
 
5798
- // src/install/layout.ts
5799
- var STATE_DIR = ".finitemonkey";
5800
- var SETTINGS_FILE = ".claude/settings.json";
5801
- var GITIGNORE_FILE = ".gitignore";
5802
- var CONFIG_FILE2 = ".finitemonkey/config.yml";
5803
- var LOCK_FILE = ".finitemonkey/lock.json";
5804
- var BACKUPS_DIR = ".finitemonkey/backups";
5805
- var LOCKS_DIR = ".finitemonkey/locks";
5806
- var INSTALL_LOCK = ".finitemonkey/locks/install.lock";
5807
- var LOCAL_BIN = "node_modules/finitemonkey/dist/finitemonkey.mjs";
5808
- var VENDOR_BIN = ".finitemonkey/runtime/dist/finitemonkey.mjs";
5809
- var VENDOR_PACKAGE = ".finitemonkey/runtime/package.json";
5810
- var LOCK_SCHEMA = "finitemonkey/lock@1";
5811
- var PLAN_SCHEMA = "finitemonkey/install-plan@1";
5812
- var GENERATED_MARKER = "finitemonkey:generated v=1";
5813
- var BLOCK_BEGIN = "# finitemonkey:begin v=1";
5814
- var BLOCK_END = "# finitemonkey:end";
5815
- var INSTALL_LOCK_STALE_MS = 5 * 60 * 1e3;
5816
- var PRESETS = ["neutral", "gabriel"];
5817
- var parsePreset = (raw) => raw === void 0 ? "neutral" : PRESETS.find((preset) => preset === raw);
5818
- var COMMITTED_STATE = ["config.yml", "runtime/", "tasks/", "rules.yml", "policy.yml", "findings/"];
5819
-
5820
6877
  // src/install/hook-entries.ts
5821
6878
  var INSTALLED_HOOKS = [
5822
6879
  { event: "SessionStart", timeout: 30, guard: false },
@@ -6101,12 +7158,12 @@ var loadCatalogs = (ctx, dir) => {
6101
7158
  if (!es.ok) return es;
6102
7159
  return accept({ en: en.value, "pt-BR": ptBR.value, es: es.value });
6103
7160
  };
6104
- var describeProblem = (ctx, problem3) => {
6105
- const kind = ctx.t(`i18n.problem.${problem3.kind}`, {
6106
- expected: (problem3.expected ?? []).join(", "),
6107
- actual: (problem3.actual ?? []).join(", ")
7161
+ var describeProblem = (ctx, problem4) => {
7162
+ const kind = ctx.t(`i18n.problem.${problem4.kind}`, {
7163
+ expected: (problem4.expected ?? []).join(", "),
7164
+ actual: (problem4.actual ?? []).join(", ")
6108
7165
  });
6109
- return ` ${problem3.lang} ${problem3.key} ${kind}`;
7166
+ return ` ${problem4.lang} ${problem4.key} ${kind}`;
6110
7167
  };
6111
7168
  var runI18nCheck = (ctx, dir) => {
6112
7169
  const input = dir === void 0 ? accept(catalogs) : loadCatalogs(ctx, dir);
@@ -6119,7 +7176,7 @@ var runI18nCheck = (ctx, dir) => {
6119
7176
  ok: false,
6120
7177
  exitCode: ExitCode.CheckFailed,
6121
7178
  data,
6122
- human: [summary, ...report2.problems.map((problem3) => describeProblem(ctx, problem3))],
7179
+ human: [summary, ...report2.problems.map((problem4) => describeProblem(ctx, problem4))],
6123
7180
  error: {
6124
7181
  code: "i18n_mismatch",
6125
7182
  message: ctx.t("error.i18n_mismatch", { count: report2.problems.length }),
@@ -6471,7 +7528,7 @@ var generatedItem = (project, file, previous) => {
6471
7528
  return { ...base, action: actionFor(before, template, "update"), after: template };
6472
7529
  };
6473
7530
  var generatedFiles = (input, vendor) => {
6474
- const config = { path: CONFIG_FILE2, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker };
7531
+ const config = { path: CONFIG_FILE, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker };
6475
7532
  if (!vendor) return accept([config]);
6476
7533
  if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
6477
7534
  return accept([config, ...vendoredFiles(input.bundle, input.version)]);
@@ -6496,11 +7553,11 @@ var needsWrite = (plan) => hasChanges(plan) || plan.previous === void 0;
6496
7553
 
6497
7554
  // src/install/apply.ts
6498
7555
  var stamp = (iso) => iso.replace(/[:.]/g, "-");
6499
- var ensureParent = (project, relative6) => project.runtime.mkdir(project.abs(dirname4(relative6)));
6500
- var writeText = (project, relative6, text6) => {
6501
- const parent = ensureParent(project, relative6);
6502
- const written = parent.ok ? project.runtime.writeAtomic(project.abs(relative6), text6) : parent;
6503
- return written.ok ? accept(void 0) : reject({ path: relative6, detail: written.error });
7556
+ var ensureParent = (project, relative8) => project.runtime.mkdir(project.abs(dirname4(relative8)));
7557
+ var writeText = (project, relative8, text6) => {
7558
+ const parent = ensureParent(project, relative8);
7559
+ const written = parent.ok ? project.runtime.writeAtomic(project.abs(relative8), text6) : parent;
7560
+ return written.ok ? accept(void 0) : reject({ path: relative8, detail: written.error });
6504
7561
  };
6505
7562
  var restore = (project, items) => {
6506
7563
  for (const item of items) {
@@ -6633,9 +7690,9 @@ var lineDiff = (path, before, after) => {
6633
7690
  const changes = walk(splitLines(before), splitLines(after)).filter(([op]) => op !== "=");
6634
7691
  if (changes.length === 0) return [];
6635
7692
  const lines2 = changes.map(([op, line]) => `${op} ${line}`);
6636
- const shown = lines2.slice(0, MAX_DIFF_LINES);
6637
- const hidden = lines2.length - shown.length;
6638
- return [`--- ${path}`, `+++ ${path}`, ...shown, ...hidden > 0 ? [`… ${String(hidden)} more`] : []];
7693
+ const shown2 = lines2.slice(0, MAX_DIFF_LINES);
7694
+ const hidden = lines2.length - shown2.length;
7695
+ return [`--- ${path}`, `+++ ${path}`, ...shown2, ...hidden > 0 ? [`… ${String(hidden)} more`] : []];
6639
7696
  };
6640
7697
 
6641
7698
  // src/install/report.ts
@@ -6695,17 +7752,17 @@ var planLines = (t, data, withDiff) => [
6695
7752
  // src/install/project.ts
6696
7753
  import { dirname as dirname5, join as join17 } from "node:path";
6697
7754
  var projectAt = (root, runtime) => {
6698
- const abs = (relative6) => toAbsolutePath(join17(root, ...relative6.split("/")));
7755
+ const abs = (relative8) => toAbsolutePath(join17(root, ...relative8.split("/")));
6699
7756
  return {
6700
7757
  root,
6701
7758
  runtime,
6702
7759
  abs,
6703
- read: (relative6) => {
6704
- const text6 = runtime.readText(abs(relative6));
7760
+ read: (relative8) => {
7761
+ const text6 = runtime.readText(abs(relative8));
6705
7762
  return text6.ok ? text6.value : void 0;
6706
7763
  },
6707
- exists: (relative6) => runtime.stat(abs(relative6)) !== void 0,
6708
- isDir: (relative6) => runtime.stat(abs(relative6))?.kind === "dir"
7764
+ exists: (relative8) => runtime.stat(abs(relative8)) !== void 0,
7765
+ isDir: (relative8) => runtime.stat(abs(relative8))?.kind === "dir"
6709
7766
  };
6710
7767
  };
6711
7768
  var findGitRoot = (start, runtime) => {
@@ -6804,8 +7861,8 @@ var presentPlan = (ctx, request, plan) => {
6804
7861
  if (ctx.dryRun) return success(data, [...human2, ctx.t("install.dry_run")]);
6805
7862
  if (!needsWrite(plan)) return success(data, [...human2, ctx.t("install.nothing_to_do")]);
6806
7863
  const next = flagsFor(request, plan.preset);
6807
- const problem3 = confirmProblem(request.mode, ctx.t("install.confirm_question"), next);
6808
- return { ...failureFrom(ctx.t, { ...problem3, data }, human2), next };
7864
+ const problem4 = confirmProblem(request.mode, ctx.t("install.confirm_question"), next);
7865
+ return { ...failureFrom(ctx.t, { ...problem4, data }, human2), next };
6809
7866
  };
6810
7867
  var applyAndReport = (ctx, request, plan) => {
6811
7868
  const project = openProject(ctx).project;
@@ -6848,16 +7905,136 @@ var initCommand = {
6848
7905
  run: (ctx, args) => Promise.resolve(runInstall(ctx, initRequest(args), "neutral"))
6849
7906
  };
6850
7907
 
6851
- // src/secrets/project.ts
6852
- var projectRoot = (ctx) => {
6853
- const top = ctx.runtime.git(["rev-parse", "--show-toplevel"], ctx.cwd);
6854
- const path = top.stdout.trim();
6855
- return top.ok && path !== "" ? toAbsolutePath(path) : ctx.cwd;
7908
+ // src/commands/prose-check.ts
7909
+ import { relative as relative7 } from "node:path";
7910
+
7911
+ // src/commands/prose-view.ts
7912
+ var PROSE_CHECK_SCHEMA = "finitemonkey/prose-check@1";
7913
+ var spanView = (span) => ({
7914
+ startLine: span.startLine,
7915
+ endLine: span.endLine,
7916
+ score: span.score,
7917
+ rules: rulesOf2([span]),
7918
+ hits: span.hits.map(({ rule, line, weight, excerpt }) => ({ rule, line, weight, excerpt }))
7919
+ });
7920
+ var proseCheckData = (reports) => {
7921
+ const files = reports.map(({ file, report: report2 }) => ({
7922
+ file,
7923
+ allowed: report2.allowed,
7924
+ spans: report2.spans.map(spanView)
7925
+ }));
7926
+ return {
7927
+ schema: PROSE_CHECK_SCHEMA,
7928
+ files,
7929
+ summary: {
7930
+ files: files.length,
7931
+ flagged: files.filter((file) => file.spans.length > 0).length,
7932
+ spans: files.reduce((sum, file) => sum + file.spans.length, 0)
7933
+ }
7934
+ };
6856
7935
  };
6857
- var isPerson = (ctx) => ctx.io.stdin.isTTY === true && ctx.env["CLAUDECODE"] === void 0 && ctx.env["CLAUDE_CODE_ENTRYPOINT"] === void 0;
6858
- var personName = (ctx) => {
6859
- const name = ctx.runtime.git(["config", "user.name"], ctx.cwd).stdout.trim();
6860
- return name !== "" ? name : ctx.env["USER"] ?? ctx.env["USERNAME"] ?? "person";
7936
+ var fileLines = (t, report2) => {
7937
+ if (report2.report.allowed) return [t("prose.check.allowed", { file: report2.file, marker: ALLOW_FILE_COMMENT })];
7938
+ const { spans } = report2.report;
7939
+ if (spans.length === 0) return [];
7940
+ return [
7941
+ t("prose.check.file", { file: report2.file, count: spans.length }),
7942
+ ...spans.flatMap((span) => [
7943
+ t("prose.check.span", {
7944
+ file: report2.file,
7945
+ start: span.startLine,
7946
+ end: span.endLine,
7947
+ score: span.score,
7948
+ rules: spanRules(span)
7949
+ }),
7950
+ ...rulesOf2([span]).map((rule) => t("prose.check.fix", { fix: `${rule}: ${t(RULE_FIX_KEYS[rule])}` }))
7951
+ ])
7952
+ ];
7953
+ };
7954
+ var proseCheckHuman = (t, reports, data) => [
7955
+ ...reports.flatMap((report2) => fileLines(t, report2)),
7956
+ ...data.summary.spans === 0 ? [t("prose.check.clean", { count: data.summary.files })] : []
7957
+ ];
7958
+
7959
+ // src/commands/prose-check.ts
7960
+ var TEXT_FILE = /\.(?:md|markdown|mdx|txt)$/i;
7961
+ var STDIN_LABEL = "<stdin>";
7962
+ var problem2 = (code, messageKey, params2) => ({
7963
+ code,
7964
+ messageKey,
7965
+ params: params2,
7966
+ fix: "finitemonkey help prose check",
7967
+ exitCode: ExitCode.Usage
7968
+ });
7969
+ var underRoot = (root, path) => toPosixPath(relative7(root, path));
7970
+ var expand = (ctx, root, target) => {
7971
+ const path = toAbsolutePath(target, ctx.cwd);
7972
+ const stat = ctx.runtime.stat(path);
7973
+ if (stat === void 0)
7974
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: "not found" }));
7975
+ if (stat.kind === "file") return accept([underRoot(root, path)]);
7976
+ const walked = ctx.runtime.walkFiles(path);
7977
+ if (!walked.ok)
7978
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: walked.error }));
7979
+ return accept(
7980
+ walked.value.map((name) => underRoot(root, toAbsolutePath(toPosixPath(name), path))).filter((name) => TEXT_FILE.test(name) && !/(?:^|\/)(?:node_modules|\.git)\//.test(name)).sort()
7981
+ );
7982
+ };
7983
+ var targetsOf = (ctx, root, paths) => {
7984
+ if (paths.length === 0) {
7985
+ const changed = changedMarkdown(ctx.runtime, root);
7986
+ return changed.ok ? changed : reject(problem2("prose_no_input", "prose.error.no_input", {}));
7987
+ }
7988
+ const files = [];
7989
+ for (const path of paths) {
7990
+ const expanded = expand(ctx, root, path);
7991
+ if (!expanded.ok) return expanded;
7992
+ files.push(...expanded.value);
7993
+ }
7994
+ return accept([...new Set(files)]);
7995
+ };
7996
+ var reportsOf = async (ctx, root, paths, mode) => {
7997
+ const forced = mode.record ? "record" : void 0;
7998
+ if (mode.stdin) {
7999
+ const report2 = scoreProse(await readAll(ctx.io.stdin), { context: forced ?? "docs" });
8000
+ return accept([{ file: STDIN_LABEL, report: report2 }]);
8001
+ }
8002
+ const targets2 = targetsOf(ctx, root, paths);
8003
+ if (!targets2.ok) return targets2;
8004
+ const scored = scoreFiles(ctx.runtime, root, targets2.value, forced);
8005
+ return scored.ok ? scored : reject(problem2("prose_unreadable", "prose.error.unreadable", { file: "", detail: scored.error }));
8006
+ };
8007
+ var resultOf = (ctx, reports) => {
8008
+ const data = proseCheckData(reports);
8009
+ const human2 = proseCheckHuman(ctx.t, reports, data);
8010
+ if (data.summary.spans === 0) return success(data, human2);
8011
+ return failureFrom(
8012
+ ctx.t,
8013
+ {
8014
+ code: "prose_found",
8015
+ messageKey: "prose.error.found",
8016
+ params: { count: data.summary.spans },
8017
+ fix: `rewrite the listed passages; for deliberate brand text, the person adds ${ALLOW_COMMENT}`,
8018
+ exitCode: ExitCode.CheckFailed,
8019
+ data
8020
+ },
8021
+ human2
8022
+ );
8023
+ };
8024
+ var proseCheckCommand = {
8025
+ path: ["prose", "check"],
8026
+ summaryKey: "prose.command.check",
8027
+ usage: "finitemonkey prose check [paths...] [--stdin] [--record] [--json]",
8028
+ status: "ready",
8029
+ options: { stdin: { type: "boolean" }, record: { type: "boolean" } },
8030
+ maxPositionals: 1e3,
8031
+ run: async (ctx, args) => {
8032
+ const root = projectRoot(ctx);
8033
+ const mode = { stdin: args.values["stdin"] === true, record: args.values["record"] === true };
8034
+ const reports = await reportsOf(ctx, root, args.positionals, mode);
8035
+ if (!reports.ok) return failureFrom(ctx.t, reports.error);
8036
+ return resultOf(ctx, reports.value);
8037
+ }
6861
8038
  };
6862
8039
 
6863
8040
  // src/commands/secrets-problems.ts
@@ -6892,7 +8069,7 @@ var daysOf = (values) => {
6892
8069
  return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
6893
8070
  };
6894
8071
  var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS).toISOString();
6895
- var failure = (ctx, problem3) => failureFrom(ctx.t, problem3);
8072
+ var failure = (ctx, problem4) => failureFrom(ctx.t, problem4);
6896
8073
  var inputProblem = (ctx, id, reason, days) => {
6897
8074
  const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
6898
8075
  if (!isPerson(ctx)) {
@@ -6967,12 +8144,12 @@ var secretsAcceptCommand = {
6967
8144
  const id = args.positionals[0] ?? "";
6968
8145
  const reason = stringOption(args.values, "reason");
6969
8146
  const days = daysOf(args.values);
6970
- const problem3 = inputProblem(ctx, id, reason, days);
6971
- if (problem3 !== void 0 || reason === void 0 || days === void 0) {
8147
+ const problem4 = inputProblem(ctx, id, reason, days);
8148
+ if (problem4 !== void 0 || reason === void 0 || days === void 0) {
6972
8149
  return Promise.resolve(
6973
8150
  failure(
6974
8151
  ctx,
6975
- problem3 ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
8152
+ problem4 ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
6976
8153
  )
6977
8154
  );
6978
8155
  }
@@ -6993,7 +8170,7 @@ var PURGE_STEPS = [
6993
8170
  "secrets.purge.step4",
6994
8171
  "secrets.purge.step5"
6995
8172
  ];
6996
- var fail2 = (ctx, problem3) => failureFrom(ctx.t, problem3);
8173
+ var fail2 = (ctx, problem4) => failureFrom(ctx.t, problem4);
6997
8174
  var notFound2 = (ctx, id) => fail2(
6998
8175
  ctx,
6999
8176
  usageProblem("secrets_incident_not_found", "secrets.error.incident_not_found", `finitemonkey secrets incident`, {
@@ -7154,7 +8331,7 @@ var humanOf = (ctx, data) => [
7154
8331
  ),
7155
8332
  ...data.summary.total === 0 ? [ctx.t("secrets.scan.clean")] : []
7156
8333
  ];
7157
- var resultOf = (ctx, data) => {
8334
+ var resultOf2 = (ctx, data) => {
7158
8335
  const human2 = humanOf(ctx, data);
7159
8336
  const failing = data.summary.new + data.summary.expired;
7160
8337
  if (failing === 0) return success(data, human2);
@@ -7198,7 +8375,7 @@ var secretsScanCommand = {
7198
8375
  );
7199
8376
  if (mode === "path") prune(ctx, root, baseline.value, outcome);
7200
8377
  const incidents = mode === "history" ? historyIncidents(ctx, root, outcome.fresh) : [];
7201
- return resultOf(ctx, dataOf(mode, outcome, incidents));
8378
+ return resultOf2(ctx, dataOf(mode, outcome, incidents));
7202
8379
  }
7203
8380
  };
7204
8381
 
@@ -7286,11 +8463,11 @@ var explicitChecks = (draft) => [
7286
8463
  ];
7287
8464
  var draftChecks = (runtime, paths, draft) => {
7288
8465
  const disk = {
7289
- read: (relative6) => {
7290
- const text6 = runtime.readText(toAbsolutePath(join18(paths.root, relative6)));
8466
+ read: (relative8) => {
8467
+ const text6 = runtime.readText(toAbsolutePath(join18(paths.root, relative8)));
7291
8468
  return text6.ok ? text6.value : void 0;
7292
8469
  },
7293
- exists: (relative6) => runtime.stat(toAbsolutePath(join18(paths.root, relative6))) !== void 0
8470
+ exists: (relative8) => runtime.stat(toAbsolutePath(join18(paths.root, relative8))) !== void 0
7294
8471
  };
7295
8472
  const detected = detectChecks(disk);
7296
8473
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -7300,6 +8477,7 @@ var draftChecks = (runtime, paths, draft) => {
7300
8477
  ...hasTests ? [] : [{ id: "tests", kind: "tests" }],
7301
8478
  { id: "placeholders", kind: "placeholders" },
7302
8479
  { id: "secrets", kind: "secrets" },
8480
+ { id: "prose", kind: "prose" },
7303
8481
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
7304
8482
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
7305
8483
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
@@ -7342,7 +8520,7 @@ var createTask = (runtime, paths, draft) => {
7342
8520
  };
7343
8521
 
7344
8522
  // src/commands/task.ts
7345
- var problem2 = (code, messageKey, fix, params2 = {}, exitCode = ExitCode.Usage) => ({
8523
+ var problem3 = (code, messageKey, fix, params2 = {}, exitCode = ExitCode.Usage) => ({
7346
8524
  code,
7347
8525
  messageKey,
7348
8526
  params: params2,
@@ -7353,7 +8531,7 @@ var projectOf = (ctx) => {
7353
8531
  const root = gitRoot(gitAt(ctx.runtime, ctx.cwd));
7354
8532
  return root === void 0 ? void 0 : fmPaths(root);
7355
8533
  };
7356
- var noRepo = (ctx) => failureFrom(ctx.t, problem2("not_a_git_repo", "error.not_a_git_repo", "git init", {}, ExitCode.NotConfigured));
8534
+ var noRepo = (ctx) => failureFrom(ctx.t, problem3("not_a_git_repo", "error.not_a_git_repo", "git init", {}, ExitCode.NotConfigured));
7357
8535
  var list3 = (values, name) => {
7358
8536
  const value = values[name];
7359
8537
  const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
@@ -7385,17 +8563,17 @@ var createFailure = (ctx, failure2) => {
7385
8563
  if (failure2.kind === "exists") {
7386
8564
  return failureFrom(
7387
8565
  ctx.t,
7388
- problem2("task_exists", "error.task_exists", `finitemonkey task show ${failure2.id}`, { task: failure2.id })
8566
+ problem3("task_exists", "error.task_exists", `finitemonkey task show ${failure2.id}`, { task: failure2.id })
7389
8567
  );
7390
8568
  }
7391
8569
  const detail = failure2.kind === "invalid" ? failure2.issues.join("; ") : failure2.detail;
7392
- return failureFrom(ctx.t, problem2("task_invalid", "error.task_invalid", "finitemonkey help task new", { detail }));
8570
+ return failureFrom(ctx.t, problem3("task_invalid", "error.task_invalid", "finitemonkey help task new", { detail }));
7393
8571
  };
7394
8572
  var newTask = (ctx, id, values) => {
7395
8573
  if (id === void 0)
7396
8574
  return failureFrom(
7397
8575
  ctx.t,
7398
- problem2("invalid_arguments", "error.invalid_arguments", "finitemonkey help task new", { detail: "missing <id>" })
8576
+ problem3("invalid_arguments", "error.invalid_arguments", "finitemonkey help task new", { detail: "missing <id>" })
7399
8577
  );
7400
8578
  const paths = projectOf(ctx);
7401
8579
  if (paths === void 0) return noRepo(ctx);
@@ -7421,7 +8599,7 @@ var showTask = (ctx, id) => {
7421
8599
  if (report2 === void 0) {
7422
8600
  return failureFrom(
7423
8601
  ctx.t,
7424
- problem2(
8602
+ problem3(
7425
8603
  "task_not_found",
7426
8604
  "error.task_not_found",
7427
8605
  "finitemonkey task new <id> --goal <text>",
@@ -7531,9 +8709,9 @@ var applyItem = (project, item) => {
7531
8709
  if (item.after === void 0 || item.action === "gone" || item.action.startsWith("keep")) return accept(void 0);
7532
8710
  return writeText(project, item.path, item.after);
7533
8711
  };
7534
- var isEmptyDir = (project, relative6) => {
7535
- const listed2 = project.runtime.listDir(project.abs(relative6));
7536
- return listed2.ok && listed2.value.length === 0;
8712
+ var isEmptyDir = (project, relative8) => {
8713
+ const listed3 = project.runtime.listDir(project.abs(relative8));
8714
+ return listed3.ok && listed3.value.length === 0;
7537
8715
  };
7538
8716
  var finish = (project, plan) => {
7539
8717
  const target = plan.purge ? STATE_DIR : LOCK_FILE;
@@ -7588,8 +8766,8 @@ var runUninstall = (ctx, args) => {
7588
8766
  if (ctx.dryRun) return success(preview, [...linesOf(ctx, preview), ctx.t("install.dry_run")]);
7589
8767
  if (!ctx.yes) {
7590
8768
  const next = fixFor(args);
7591
- const problem3 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next);
7592
- return { ...failureFrom(ctx.t, { ...problem3, data: preview }, linesOf(ctx, preview)), next };
8769
+ const problem4 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next);
8770
+ return { ...failureFrom(ctx.t, { ...problem4, data: preview }, linesOf(ctx, preview)), next };
7593
8771
  }
7594
8772
  const removed = applyRemoval(project, plan);
7595
8773
  if (!removed.ok) return failureFrom(ctx.t, applyProblem(removed.error));
@@ -7672,6 +8850,7 @@ var REGISTRY = [
7672
8850
  secretsScanCommand,
7673
8851
  secretsAcceptCommand,
7674
8852
  secretsIncidentCommand,
8853
+ proseCheckCommand,
7675
8854
  versionCommand,
7676
8855
  helpCommand
7677
8856
  ];
@@ -7711,9 +8890,9 @@ var unknownCommand = (registry, attempted) => {
7711
8890
  suggestion
7712
8891
  });
7713
8892
  };
7714
- var failWith = (ctx, name, problem3) => ({
8893
+ var failWith = (ctx, name, problem4) => ({
7715
8894
  name,
7716
- execute: () => Promise.resolve(failureFrom(ctx.t, problem3))
8895
+ execute: () => Promise.resolve(failureFrom(ctx.t, problem4))
7717
8896
  });
7718
8897
  var invoke = (ctx, argv, spec, registry) => {
7719
8898
  const name = commandName(spec);
@@ -7798,11 +8977,11 @@ var run = async (options) => {
7798
8977
  let result;
7799
8978
  try {
7800
8979
  if (loose.lang !== void 0 && normalizeLang(loose.lang) === void 0) {
7801
- const problem3 = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
8980
+ const problem4 = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
7802
8981
  lang: loose.lang,
7803
8982
  languages: LANGUAGES.join(", ")
7804
8983
  });
7805
- result = failureFrom(t, problem3);
8984
+ result = failureFrom(t, problem4);
7806
8985
  } else {
7807
8986
  const dispatched = dispatch(ctx, options.argv, loose, options.registry ?? REGISTRY);
7808
8987
  name = dispatched.name;
@@ -7819,8 +8998,8 @@ var run = async (options) => {
7819
8998
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7820
8999
  var UNKNOWN_VERSION = "0.0.0-unknown";
7821
9000
  var VERSION_SOURCES = ["../package.json", "../.claude-plugin/plugin.json"];
7822
- var versionAt = (readText, moduleUrl, relative6) => {
7823
- const text6 = readText(toAbsolutePath(fileURLToPath3(new URL(relative6, moduleUrl))));
9001
+ var versionAt = (readText, moduleUrl, relative8) => {
9002
+ const text6 = readText(toAbsolutePath(fileURLToPath3(new URL(relative8, moduleUrl))));
7824
9003
  if (!text6.ok) return void 0;
7825
9004
  const parsed = parseJson(text6.value);
7826
9005
  if (!parsed.ok || !isRecord(parsed.value)) return void 0;
@@ -7828,8 +9007,8 @@ var versionAt = (readText, moduleUrl, relative6) => {
7828
9007
  return typeof version === "string" ? version : void 0;
7829
9008
  };
7830
9009
  var readPackageVersion = (readText, moduleUrl) => {
7831
- for (const relative6 of VERSION_SOURCES) {
7832
- const version = versionAt(readText, moduleUrl, relative6);
9010
+ for (const relative8 of VERSION_SOURCES) {
9011
+ const version = versionAt(readText, moduleUrl, relative8);
7833
9012
  if (version !== void 0) return version;
7834
9013
  }
7835
9014
  return UNKNOWN_VERSION;