From 7e936c87ab2ffa723986ce38619df3ed0e10036f Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 13:38:40 -0400 Subject: [PATCH 1/6] Add prose write run option --- README.md | 2 +- packages/std/ops/README.md | 2 +- packages/std/ops/prose-author.prose.md | 62 +++++++- skills/open-prose/SKILL.md | 2 +- skills/open-prose/help.md | 2 +- skills/open-prose/prose.md | 2 +- tools/cli/README.md | 10 ++ tools/cli/src/commands/base.ts | 53 +++++-- tools/cli/src/commands/index.ts | 3 +- tools/cli/src/prose/command-model.ts | 167 ++++++++++++++++---- tools/cli/src/prose/index.ts | 3 + tools/cli/tests/cli/cli.test.ts | 60 ++++++- tools/cli/tests/prose/command-model.test.ts | 23 ++- 13 files changed, 326 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 819c6a9f..712862c2 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ be checked, maintained, and restored across bounded runs: | `prose compile` | Run the bundled OpenProse compiler program, emit `/dist/manifest.next.json`, and validate it before success | | `prose serve` | Load active IR and run local cron and HTTP trigger adapters | | `prose run` | Execute one bounded OpenProse VM activation | -| `prose write` | Author a validated OpenProse program package from rough English/pseudo-Prose, asking targeted shape/root questions in interactive hosts and returning `unresolved-intent` only for non-interactive missing decisions | +| `prose write` | Author a validated OpenProse program package from rough English/pseudo-Prose; shell `--out --apply` writes the package after lint passes, and `--out --run` writes then runs the generated root file | | `prose status` | Inspect active IR, diagnostics, trigger plan, recent runs, and responsibility status/pressure | The compiled Responsibility Runtime manifest preserves responsibilities as diff --git a/packages/std/ops/README.md b/packages/std/ops/README.md index 181adc07..4549f2de 100644 --- a/packages/std/ops/README.md +++ b/packages/std/ops/README.md @@ -19,7 +19,7 @@ come back as `unresolved-intent` instead of terminal prompts. | `preflight.prose.md` | `prose preflight ` | Check that dependencies are installed and environment variables are set | | `wire.prose.md` | `prose run std/ops/wire` | Run Forme wiring to produce an execution manifest | | `status.prose.md` | `prose status` | Show recent runs with system name, duration, and pass/fail status | -| `prose-author.prose.md` | `prose write [request...]` | Interactive-by-default authoring of a validated OpenProse package from rough English or pseudo-Prose | +| `prose-author.prose.md` | `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring of a validated OpenProse package from rough English or pseudo-Prose; shell `--out --apply` may write it, and `--out --run` writes then runs it | | `diagnose.prose.md` | `prose run std/ops/diagnose` | Diagnose why a run failed -- root cause analysis with fix recommendations | | `profiler.prose.md` | `prose run std/ops/profiler` | Profile a run for cost, tokens, and time using actual API session data | diff --git a/packages/std/ops/prose-author.prose.md b/packages/std/ops/prose-author.prose.md index 8d47ac4b..4999d199 100644 --- a/packages/std/ops/prose-author.prose.md +++ b/packages/std/ops/prose-author.prose.md @@ -41,13 +41,24 @@ decisions instead of guessing. ### Requires - `output_mode`: output mode requested by the caller. `prose write` passes - `source-package-only`; in that mode the system must return source content and - apply notes only. + `source-package-only` by default; shell `--out --apply` and `--out --run` + pass `source-package-and-files`, where the system must write the generated + source after validation and still return a reviewable summary. - `apply`: whether this run is allowed to write generated source into the - caller's repository. `prose write` passes `false`. + caller's repository. `prose write` passes `false` by default; shell + `--apply` and `--run` pass `true`. +- `target_path`: optional root-relative file or folder path selected by the + caller. When present with `apply: true`, generated files must stay under this + path. Folder targets use `index.prose.md` as the root file; file targets must + end in `.prose.md`. +- `run_after_write`: follow-up run mode. `false` means no follow-up run; + `host-managed` means the shell wrapper will run the generated root file after + successful apply. The authoring system must not execute the generated root + itself. - `run_state`: preferred run-state mode. `prose write` passes `in-context` so package-only authoring avoids creating run artifacts in the caller's - workspace when the host can honor that mode. + workspace when the host can honor that mode. Apply-enabled shell runs may + pass `filesystem`. - `terminal_summary`: whether a concise final terminal status block is required. `prose write` passes `required`. - `interactive`: whether the host may ask targeted follow-up questions before @@ -106,6 +117,9 @@ decisions instead of guessing. - Landscape inspection is read-only: it may list and read nearby source, configuration, and OpenProse layout markers, but it must not create, modify, delete, format, install, compile, or migrate files. +- Apply-enabled source writing may happen only after planning, linting, and + repair pass. It must be limited to the generated files under `target_path` + and must not modify unrelated repository files. - When `output_mode` is `source-package-only` or `apply` is `false`, generated files are returned as source package content and optional apply notes only. The authoring run must not write generated files to the caller's repository, @@ -177,7 +191,9 @@ decisions instead of guessing. `co/...`, or `github.com/owner/repo/path`. Do not invent bare `owner/repo` registry references. - Do not write directly to the caller's repository. Return a package and patch - notes; a separate file-writing step can apply it after human review. + notes unless `apply: true` and `target_path` are explicitly present. In + apply-enabled mode, write only the generated source package under + `target_path` after lint passes. - Treat any request phrase such as "under src/foo" or "add it to this repo" as a desired package path when `apply: false`, not as permission to create files. - Use CLI-facing language for `prose write`: describe the result as an @@ -194,6 +210,8 @@ let authoring_intent = call intent-normalizer request: request output_mode: output_mode apply: apply + target_path: target_path + run_after_write: run_after_write run_state: run_state terminal_summary: terminal_summary interactive: interactive @@ -239,6 +257,7 @@ let draft_source_package = call source-author guidance_report: guidance_report output_mode: output_mode apply: apply + target_path: target_path let lint_report = call source-linter draft_source_package: draft_source_package @@ -270,6 +289,9 @@ let assembled = call package-assembler landscape: landscape shape_decision: triage_result.shape_decision guidance_report: guidance_report + apply: apply + target_path: target_path + run_after_write: run_after_write return { source_package: assembled.source_package, @@ -291,6 +313,9 @@ Normalize the caller's rough request into an explicit authoring intent. brief from the caller - `output_mode`: caller output mode, usually `source-package-only` - `apply`: whether this authoring run may write files, usually `false` +- `target_path`: optional root-relative destination for generated source +- `run_after_write`: follow-up run mode, usually `false`; `host-managed` means + the shell wrapper will perform the run after a successful apply - `run_state`: preferred run-state mode, usually `in-context` - `terminal_summary`: whether the final status block is required - `interactive`: whether targeted follow-up questions are allowed before source @@ -309,6 +334,9 @@ Normalize the caller's rough request into an explicit authoring intent. persistence, memory, and safety boundaries implied by the request - output_mode: caller output mode, preserved for downstream authoring - apply: caller apply flag, preserved for downstream authoring + - target_path: caller destination path, preserved for downstream authoring + - run_after_write: caller follow-up run mode, preserved for downstream + authoring - run_state: caller run-state preference, preserved for downstream authoring - terminal_summary: caller terminal-summary requirement, preserved for @@ -458,6 +486,10 @@ planning begins. or a graph that should be edited service-by-service later. - Choose a native OpenProse root only when the landscape already looks native or the user is clearly creating an OpenProse repository. +- When `target_path` is present, treat it as the binding destination for the + package. If it ends in `.prose.md`, choose single-file output at that file. + Otherwise choose a folder-shaped package rooted at `target_path` with + `index.prose.md` as the runnable root unless the request makes that invalid. - Choose an attached sidecar root for ordinary application repositories where generated Prose should live alongside, not inside, the app source. - Choose user-global only when the user asks for a personal agent, cross-repo @@ -573,6 +605,10 @@ Plan the generated file tree and contracts before drafting source. `interactive-triage` - `guidance_report`: baseline and shape-specific guidance from `guidance-loader` +- `apply`: whether this run may write generated files +- `target_path`: optional root-relative destination for generated files +- `run_after_write`: follow-up run mode; `host-managed` means the shell wrapper + will run the generated root file after a successful apply ### Ensures @@ -647,6 +683,8 @@ Draft the planned source package. `guidance-loader` - `output_mode`: caller output mode, usually `source-package-only` - `apply`: whether this authoring run may write files, usually `false` +- `target_path`: optional root-relative destination for apply-enabled source + writing ### Ensures @@ -669,6 +707,9 @@ Draft the planned source package. - In `source-package-only` or `apply: false` mode, include the chosen paths and full file contents in `draft_source_package`; do not write those paths to the filesystem. +- In `source-package-and-files` with `apply: true`, still draft the package as + explicit file paths plus contents. The later `package-assembler` performs the + filesystem write only after lint passes. - Use current Contract Markdown headings exactly. Unknown `###` sections may be preserved only as documentation, never as hidden runtime behavior. - Make every `### Ensures` item named, evaluable, and specific enough for a @@ -862,10 +903,12 @@ Publish the validated source package and concise next-step notes. - `source_package`: `returned` - `shape`: chosen package shape - `root`: selected root file or root folder - - `apply`: `false` for `prose write` - - `files_written`: `none` when no generated source files were applied + - `apply`: whether generated source files were applied + - `files_written`: `none` when no generated source files were applied, or + a compact list/count of files written under `target_path` - `lint`: `pass` - - `next`: recommended command or manual apply step + - `next`: recommended command, the run command just triggered by the shell + wrapper, or a manual apply step ### Errors @@ -876,6 +919,9 @@ Publish the validated source package and concise next-step notes. - Do not publish a package when `lint_report` has blocking findings. - Keep final output reviewable: include paths first, then file contents in the same order a human would open them. +- When `apply: true`, write the generated source files only after + `lint_report.status` is `pass`, only under `target_path`, and report the + concrete files in `files_written`. - Do not claim files were written to the caller's repository unless this run actually wrote them through an explicitly requested file-writing step. - End every successful package-only response with `final_status_summary` so a diff --git a/skills/open-prose/SKILL.md b/skills/open-prose/SKILL.md index 29da8615..82dc052b 100644 --- a/skills/open-prose/SKILL.md +++ b/skills/open-prose/SKILL.md @@ -128,7 +128,7 @@ executing the system. The shell executable is the agent runner, e.g. | `prose run runtime/judge-responsibility.prose.md` | Resolve from the OpenProse skill root; judge one responsibility from activation context | | `prose run //[/path]` | Resolve installed dependency service or system, detect format, then route as above | | `prose run std/...` / `co/...` | Expand OpenProse package shorthand, resolve installed dependency service or system, then route as above | -| `prose write [request...]` | Interactive-by-default authoring: load `contract-markdown.md`, `guidance/tenets.md`, and `guidance/authoring.md`; run `std/ops/prose-author`; scan the local landscape read-only, decide shape/root/path, load shape-specific guidance, ask a small number of targeted `ask_user` questions when the host can support them, then return a fully validated source package. If the caller or host marks the run non-interactive, return `unresolved-intent` with the missing decisions instead of guessing. Do not apply files unless the caller explicitly asks for that follow-up | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring: load `contract-markdown.md`, `guidance/tenets.md`, and `guidance/authoring.md`; run `std/ops/prose-author`; scan the local landscape read-only, decide shape/root/path, load shape-specific guidance, ask a small number of targeted `ask_user` questions when the host can support them, then return a fully validated source package. If the caller or host marks the run non-interactive, return `unresolved-intent` with the missing decisions instead of guessing. Apply generated files only when the caller explicitly passes apply permission, normally through `--out --apply`; `--run` implies apply permission and then runs the generated root file | | `prose lint ` | Validate Contract Markdown structure, headers, frontmatter, contracts, shapes, and wiring | | `prose preflight ` | Check dependencies and `### Environment` declarations without executing | | `prose test ` | Load `contract-markdown.md`, `state/README.md` plus the selected backend, and `prose.md`; run `kind: test` file(s) | diff --git a/skills/open-prose/help.md b/skills/open-prose/help.md index 6623a654..0e8a7b2b 100644 --- a/skills/open-prose/help.md +++ b/skills/open-prose/help.md @@ -48,7 +48,7 @@ Options: | `prose compile [path] [--out ]` | Compile source into `/dist/manifest.next.json` | | `prose serve` | Serve the active IR as local cron and HTTP trigger adapters | | `prose run ` | Run a service or system | -| `prose write [request...]` | Interactive-by-default authoring from rough English/pseudo-Prose into a validated source package; non-interactive runs return `unresolved-intent` when more detail is required | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring from rough English/pseudo-Prose into a validated source package; `--out --apply` writes it after lint passes, and `--out --run` writes then runs the generated root file | | `prose lint ` | Validate structure, schema, and contracts | | `prose preflight ` | Check dependencies and environment | | `prose test ` | Run tests with assertions | diff --git a/skills/open-prose/prose.md b/skills/open-prose/prose.md index 4968998e..e2232c04 100644 --- a/skills/open-prose/prose.md +++ b/skills/open-prose/prose.md @@ -45,7 +45,7 @@ codex exec "prose run system.prose.md" | `prose run /` | Reserved for the OpenProse registry (future home at `p.prose.md`) | | `prose run ...@` | Pin to a SHA or tag; require that version in `/deps/` | | `prose run ... --offline` | Require disk-only resolution; error if not in `/deps/` | -| `prose write [request...]` | Interactive-by-default authoring through `std/ops/prose-author`, asking targeted shape/root questions when supported and returning a validated source package | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring through `std/ops/prose-author`, asking targeted shape/root questions when supported and returning a validated source package; shell `--out --apply` writes it, and `--out --run` writes then runs the generated root file | | `prose lint ` | Validate structure, schema, shapes, and contracts | | `prose preflight ` | Check dependencies, declared tools, and environment variables | | `prose test ` | Run test(s) and report results | diff --git a/tools/cli/README.md b/tools/cli/README.md index 04e2e58a..109b25ff 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -76,6 +76,7 @@ Examples: prose run std/evals/inspector prose run std/evals/prose-contributor -- subjects: 20260406-201439-1a3369 prose write "draft a release readiness responsibility" +prose write --out src/release-readiness --run "draft a release readiness responsibility" cat brief.txt | prose write --harness codex-sdk prose run std/evals/inspector --harness codex-sdk prose run co/systems/company-repo-checker --harness claude-sdk @@ -160,6 +161,13 @@ missing blocking decisions are reported as `unresolved-intent` instead of a mid-run terminal prompt. CLI flags that would imply mid-run interaction are rejected; put literal text after `--` or pipe it on stdin. +By default, `prose write` returns a source package for review and does not +modify the repository. Use `--out ` with `--apply` to let the authoring +run write the generated package after lint passes. Use `--out --run` to +write the package and then immediately invoke `prose run` on the generated +root file. Directory targets run `/index.prose.md`; file targets must end +in `.prose.md`. + ```bash prose compile prose compile src/responsibilities --out dist @@ -167,6 +175,8 @@ cp dist/manifest.next.json dist/manifest.active.json prose serve prose run src/systems/reviewer.prose.md prose write "draft a release readiness responsibility" +prose write --out src/reviewer --apply "draft a reviewer system" +prose write --out src/reviewer --run "draft a reviewer system" prose write < brief.txt prose run co/systems/company-repo-checker --harness claude-sdk prose upgrade diff --git a/tools/cli/src/commands/base.ts b/tools/cli/src/commands/base.ts index d805dfd6..81bbc097 100644 --- a/tools/cli/src/commands/base.ts +++ b/tools/cli/src/commands/base.ts @@ -1,9 +1,9 @@ import { Command } from "@oclif/core"; import type { CommandName } from "../prose/index.js"; -import { canonicalPrompt, CommandModelError, usageFor } from "../prose/index.js"; +import { canonicalPrompt, CommandModelError, resolveWriteRunTarget, usageFor } from "../prose/index.js"; import { recordForwardedFulfillmentArtifact } from "../prose/fulfillment-artifact.js"; import { createHarness, type HarnessName } from "../harnesses/index.js"; -import type { Harness, WritableStreamLike } from "../harnesses/types.js"; +import type { Harness, HarnessRunOptions, WritableStreamLike } from "../harnesses/types.js"; import { ensureOpenProseSkill, loadOpenProseSkillBootstrap, type OpenProseSkillBootstrap } from "../skills/open-prose.js"; export interface SkillPreflightOptions { @@ -95,6 +95,7 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom const { harness, args } = splitHarnessArgs(options.argv, options.env, options.command); const promptArgs = await hydrateForwardedArgs(options.command, args, options.stdin); const prompt = canonicalPrompt(options.command, promptArgs); + const writeRunTarget = options.command === "write" ? resolveWriteRunTarget(promptArgs) : undefined; if (shouldRunSkillPreflight(options)) { await runSkillPreflight(harness, options); } @@ -103,7 +104,41 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom : undefined; const selectedHarness = (options.harnessFactory ?? createHarness)(harness); - const exitCode = await selectedHarness.run(prompt, { + const harnessRunOptions = buildHarnessRunOptions(options, skillBootstrap); + const exitCode = await selectedHarness.run(prompt, harnessRunOptions); + await recordForwardedFulfillmentArtifact({ + command: options.command, + argv: args, + cwd: options.cwd, + env: options.env, + exitCode, + harness, + prompt, + }); + + if (exitCode !== 0 || writeRunTarget === undefined) { + return exitCode; + } + + const runPrompt = canonicalPrompt("run", [writeRunTarget]); + const runExitCode = await selectedHarness.run(runPrompt, harnessRunOptions); + await recordForwardedFulfillmentArtifact({ + command: "run", + argv: [writeRunTarget], + cwd: options.cwd, + env: options.env, + exitCode: runExitCode, + harness, + prompt: runPrompt, + }); + return runExitCode; +} + +function buildHarnessRunOptions( + options: ForwardRunOptions, + skillBootstrap: OpenProseSkillBootstrap | undefined, +): HarnessRunOptions { + return { ...(skillBootstrap === undefined ? {} : { @@ -115,17 +150,7 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom stdout: options.stdout, stderr: options.stderr, ...(options.signal === undefined ? {} : { signal: options.signal }), - }); - await recordForwardedFulfillmentArtifact({ - command: options.command, - argv: args, - cwd: options.cwd, - env: options.env, - exitCode, - harness, - prompt, - }); - return exitCode; + }; } async function hydrateForwardedArgs( diff --git a/tools/cli/src/commands/index.ts b/tools/cli/src/commands/index.ts index 73fa55ee..1fa2420d 100644 --- a/tools/cli/src/commands/index.ts +++ b/tools/cli/src/commands/index.ts @@ -65,10 +65,11 @@ const forwardCommandDefinitions = { command: "write", examples: [ '<%= config.bin %> write "draft a release readiness responsibility" --harness codex-sdk', + '<%= config.bin %> write --out src/release-readiness --run "draft a release readiness responsibility"', "cat brief.txt | <%= config.bin %> write --harness codex-sdk", ], summary: "Write linted OpenProse source from rough intent.", - usage: "write [request...] [--harness ]", + usage: "write [--out ] [--apply] [--run] [request...] [--harness ]", }, } satisfies Record; diff --git a/tools/cli/src/prose/command-model.ts b/tools/cli/src/prose/command-model.ts index f1b384e5..bcb02d3f 100644 --- a/tools/cli/src/prose/command-model.ts +++ b/tools/cli/src/prose/command-model.ts @@ -1,3 +1,5 @@ +import { isAbsolute } from "node:path"; + export type CommandName = | "compile" | "run" @@ -40,7 +42,7 @@ export const supportedCommands = [ const usageByCommand: Record = { compile: "prose compile [path] [--out ]", run: "prose run [inputs...]", - write: "prose write [request...]", + write: "prose write [--out ] [--apply] [--run] [request...]", lint: "prose lint ", preflight: "prose preflight ", test: "prose test ", @@ -55,21 +57,25 @@ const usageByCommand: Record = { export function canonicalPrompt(command: CommandName, args: readonly string[]): string { validate(command, args); if (command === "write") { + const write = parseWriteCommand(args); return shellJoin([ "prose", "write", "output_mode:", - "source-package-only", + write.apply ? "source-package-and-files" : "source-package-only", "apply:", - "false", + String(write.apply), + ...(write.out === undefined ? [] : ["target_path:", write.out]), + "run_after_write:", + write.run ? "host-managed" : "false", "run_state:", - "in-context", + write.apply ? "filesystem" : "in-context", "terminal_summary:", "required", "interactive:", "false", "request:", - parseWriteRequest(args), + write.request, ]); } return shellJoin(["prose", command, ...args]); @@ -79,6 +85,102 @@ export function usageFor(command: CommandName): string { return usageByCommand[command]; } +export interface WriteCommandOptions { + apply: boolean; + out?: string; + request: string; + run: boolean; +} + +export function parseWriteCommand(args: readonly string[]): WriteCommandOptions { + const requestParts: string[] = []; + let literalRequest = false; + let out: string | undefined; + let apply = false; + let run = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === undefined) { + continue; + } + + if (!literalRequest && arg === "--") { + literalRequest = true; + continue; + } + + if (!literalRequest) { + if (arg === "--interactive" || arg === "--no-interactive") { + fail( + "write", + "'prose write' does not support interactive flags. Pass all authoring context in argv/stdin, or run prose-author from a host that supports ask_user.", + ); + } + + if (arg === "--apply") { + apply = true; + continue; + } + + if (arg === "--run") { + apply = true; + run = true; + continue; + } + + if (arg === "--out") { + if (out !== undefined) { + fail("write", "Duplicate option for 'prose write'."); + } + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + fail("write", "Missing value for --out."); + } + out = normalizeWriteTargetPath(value); + index += 1; + continue; + } + + if (arg.startsWith("--out=")) { + if (out !== undefined) { + fail("write", "Duplicate option for 'prose write'."); + } + out = normalizeWriteTargetPath(arg.slice("--out=".length)); + continue; + } + } + + requestParts.push(arg); + } + + const request = requestParts.join(" "); + if (request.trim() === "") { + fail("write", "Missing request text for 'prose write'. Pass text arguments or pipe stdin."); + } + if (apply && out === undefined) { + fail("write", "'prose write --apply' and 'prose write --run' require --out ."); + } + + return { + apply, + ...(out === undefined ? {} : { out }), + request, + run, + }; +} + +export function resolveWriteRunTarget(args: readonly string[]): string | undefined { + const write = parseWriteCommand(args); + if (!write.run) { + return undefined; + } + if (write.out === undefined) { + return undefined; + } + return rootFileForWriteTarget(write.out); +} + function validate(command: CommandName, args: readonly string[]): void { switch (command) { case "compile": @@ -135,34 +237,11 @@ function requireAtLeastOne(command: CommandName, args: readonly string[], label: } function requireNonBlankWriteRequest(command: "write", args: readonly string[]): void { - if (parseWriteRequest(args).trim() === "") { + if (parseWriteCommand(args).request.trim() === "") { fail(command, "Missing request text for 'prose write'. Pass text arguments or pipe stdin."); } } -function parseWriteRequest(args: readonly string[]): string { - const requestParts: string[] = []; - let literalRequest = false; - - for (const arg of args) { - if (!literalRequest && arg === "--") { - literalRequest = true; - continue; - } - - if (!literalRequest && (arg === "--interactive" || arg === "--no-interactive")) { - fail( - "write", - "'prose write' does not support interactive flags. Pass all authoring context in argv/stdin, or run prose-author from a host that supports ask_user.", - ); - } - - requestParts.push(arg); - } - - return requestParts.join(" "); -} - function requireExactlyOne(command: CommandName, args: readonly string[], label: string): void { requireAtLeastOne(command, args, label); if (args.length > 1) { @@ -231,6 +310,36 @@ function fail(command: CommandName, message: string): never { throw new CommandModelError(message, usageByCommand[command]); } +function normalizeWriteTargetPath(value: string): string { + const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, ""); + if (normalized === "") { + fail("write", "Missing value for --out."); + } + if (value.includes("\0")) { + fail("write", "--out must be a root-relative path."); + } + if (isAbsolute(value) || normalized.startsWith("/")) { + fail("write", "--out must be a root-relative path."); + } + if (normalized.split("/").includes("..")) { + fail("write", "--out must stay inside the OpenProse root."); + } + if (normalized.endsWith(".md") && !normalized.endsWith(".prose.md")) { + fail("write", "--out file paths must end in .prose.md."); + } + return normalized === "." ? "." : normalized; +} + +function rootFileForWriteTarget(out: string): string { + if (out.endsWith(".prose.md")) { + return out; + } + if (out === ".") { + return "index.prose.md"; + } + return `${out}/index.prose.md`; +} + function shellJoin(tokens: readonly string[]): string { return tokens.map(shellQuote).join(" "); } diff --git a/tools/cli/src/prose/index.ts b/tools/cli/src/prose/index.ts index bd610c7f..ef3593ea 100644 --- a/tools/cli/src/prose/index.ts +++ b/tools/cli/src/prose/index.ts @@ -1,9 +1,12 @@ export { CommandModelError, canonicalPrompt, + parseWriteCommand, + resolveWriteRunTarget, supportedCommands, usageFor, type CommandName, + type WriteCommandOptions, } from "./command-model.js"; export { ATTACHED_OPENPROSE_ROOT_PATH, diff --git a/tools/cli/tests/cli/cli.test.ts b/tools/cli/tests/cli/cli.test.ts index b617d37b..31e7f599 100644 --- a/tools/cli/tests/cli/cli.test.ts +++ b/tools/cli/tests/cli/cli.test.ts @@ -1,6 +1,7 @@ import { chmodSync, copyFileSync, + existsSync, mkdirSync, mkdtempSync, readFileSync, @@ -376,7 +377,7 @@ describe("runForwardedProseCommand", () => { expect(exitCode).toBe(0); expect(seen).toEqual([ - "prose write output_mode: source-package-only apply: false run_state: in-context terminal_summary: required interactive: false request: 'draft release readiness'", + "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft release readiness'", ]); }); @@ -404,10 +405,65 @@ describe("runForwardedProseCommand", () => { expect(exitCode).toBe(0); expect(seen).toEqual([ - "prose write output_mode: source-package-only apply: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'", + "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'", ]); }); + it("applies and runs the exact vulnerability detection write example", async () => { + const cwd = mkdtempSync(join(tmpdir(), "prose-write-run-")); + const io = memoryStreams(); + const seen: string[] = []; + const rootFile = join(cwd, "src", "vulnerability-detection", "index.prose.md"); + const exactRequest = + "a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/"; + + try { + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/vulnerability-detection", "--run", exactRequest, "--harness", "mock"], + cwd, + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + harnessFactory: () => ({ + name: "mock", + async run(prompt) { + seen.push(prompt); + if (prompt.startsWith("prose write ")) { + mkdirSync(join(cwd, "src", "vulnerability-detection"), { recursive: true }); + writeFileSync( + rootFile, + `--- +name: vulnerability-detection +kind: system +--- + +# Vulnerability Detection + +### Services + +- \`reporter\` +`, + ); + } + if (prompt.startsWith("prose run ")) { + expect(existsSync(rootFile)).toBe(true); + } + return 0; + }, + }), + }); + + expect(exitCode).toBe(0); + expect(seen).toEqual([ + "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'", + "prose run src/vulnerability-detection/index.prose.md", + ]); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } + }); + it("rejects write flags that would imply unsupported CLI interaction", async () => { const io = memoryStreams(); let harnessCalled = false; diff --git a/tools/cli/tests/prose/command-model.test.ts b/tools/cli/tests/prose/command-model.test.ts index 703ad34e..18957a02 100644 --- a/tools/cli/tests/prose/command-model.test.ts +++ b/tools/cli/tests/prose/command-model.test.ts @@ -16,8 +16,11 @@ describe("command model", () => { ["run", ["system.prose.md", "--activation-context", "{\"kind\":\"openprose.activation\"}"]], "prose run system.prose.md --activation-context '{\"kind\":\"openprose.activation\"}'", ], - [["write", ["draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], - [["write", ["--", "--no-interactive is literal request text"]], "prose write output_mode: source-package-only apply: false run_state: in-context terminal_summary: required interactive: false request: '--no-interactive is literal request text'"], + [["write", ["draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out", "src/release-readiness", "--apply", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: false run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out=src/release-readiness", "--run", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out", "src/vulnerability-detection", "--run", "a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/"]], "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'"], + [["write", ["--", "--no-interactive is literal request text"]], "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: '--no-interactive is literal request text'"], [["lint", ["system.prose.md"]], "prose lint system.prose.md"], [["preflight", ["system.prose.md"]], "prose preflight system.prose.md"], [["test", ["tests/systems"]], "prose test tests/systems"], @@ -53,10 +56,18 @@ describe("command model", () => { [["run", []], "Missing required argument ", "prose run [inputs...]"], [["run", ["system.md"]], "Expected ", "prose run [inputs...]"], [["run", ["script.prose"]], "Expected ", "prose run [inputs...]"], - [["write", []], "Pass text arguments or pipe stdin", "prose write [request...]"], - [["write", [" "]], "Pass text arguments or pipe stdin", "prose write [request...]"], - [["write", ["--interactive", "draft"]], "does not support interactive flags", "prose write [request...]"], - [["write", ["--no-interactive", "draft"]], "does not support interactive flags", "prose write [request...]"], + [["write", []], "Pass text arguments or pipe stdin", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", [" "]], "Pass text arguments or pipe stdin", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--interactive", "draft"]], "does not support interactive flags", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--no-interactive", "draft"]], "does not support interactive flags", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--apply", "draft"]], "require --out ", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--run", "draft"]], "require --out ", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out"]], "Missing value for --out", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out=", "draft"]], "Missing value for --out", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out", "/tmp/source", "draft"]], "root-relative path", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out", "../source", "draft"]], "inside the OpenProse root", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out", "source.md", "draft"]], "end in .prose.md", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out", "one", "--out", "two", "draft"]], "Duplicate option", "prose write [--out ] [--apply] [--run] [request...]"], [["inspect", []], "Missing required argument ", "prose inspect "], [["lint", ["system.md"]], "Expected ", "prose lint "], [["preflight", ["system.md"]], "Expected ", "prose preflight "], From 1f3bb197ee73f41fc2d3db0f1ad6c598ef5d4aa8 Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 14:06:27 -0400 Subject: [PATCH 2/6] Fix write-run audit findings --- packages/std/ops/prose-author.prose.md | 7 ++++ tools/cli/src/commands/base.ts | 34 +++++++++++++++-- tools/cli/tests/cli/cli.test.ts | 52 ++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/packages/std/ops/prose-author.prose.md b/packages/std/ops/prose-author.prose.md index 4999d199..36569d12 100644 --- a/packages/std/ops/prose-author.prose.md +++ b/packages/std/ops/prose-author.prose.md @@ -248,6 +248,9 @@ let source_plan = call source-planner landscape: landscape shape_decision: triage_result.shape_decision guidance_report: guidance_report + apply: apply + target_path: target_path + run_after_write: run_after_write let draft_source_package = call source-author source_plan: source_plan @@ -841,6 +844,10 @@ Repair blocking lint findings without changing the caller's intent. `interactive-triage` - `guidance_report`: baseline and shape-specific guidance from `guidance-loader` +- `apply`: whether this authoring run may write files +- `target_path`: optional root-relative destination for generated source +- `run_after_write`: follow-up run mode; `host-managed` means the shell wrapper + will run the generated root file after a successful apply ### Ensures diff --git a/tools/cli/src/commands/base.ts b/tools/cli/src/commands/base.ts index 81bbc097..d1db055f 100644 --- a/tools/cli/src/commands/base.ts +++ b/tools/cli/src/commands/base.ts @@ -1,6 +1,6 @@ import { Command } from "@oclif/core"; import type { CommandName } from "../prose/index.js"; -import { canonicalPrompt, CommandModelError, resolveWriteRunTarget, usageFor } from "../prose/index.js"; +import { canonicalPrompt, CommandModelError, parseWriteCommand, resolveWriteRunTarget, usageFor } from "../prose/index.js"; import { recordForwardedFulfillmentArtifact } from "../prose/fulfillment-artifact.js"; import { createHarness, type HarnessName } from "../harnesses/index.js"; import type { Harness, HarnessRunOptions, WritableStreamLike } from "../harnesses/types.js"; @@ -95,6 +95,7 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom const { harness, args } = splitHarnessArgs(options.argv, options.env, options.command); const promptArgs = await hydrateForwardedArgs(options.command, args, options.stdin); const prompt = canonicalPrompt(options.command, promptArgs); + const writeRequiresFilesystem = options.command === "write" ? parseWriteCommand(promptArgs).apply : false; const writeRunTarget = options.command === "write" ? resolveWriteRunTarget(promptArgs) : undefined; if (shouldRunSkillPreflight(options)) { await runSkillPreflight(harness, options); @@ -104,7 +105,10 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom : undefined; const selectedHarness = (options.harnessFactory ?? createHarness)(harness); - const harnessRunOptions = buildHarnessRunOptions(options, skillBootstrap); + const harnessRunOptions = buildHarnessRunOptions(options, skillBootstrap, { + harness, + requiresFilesystemWrites: writeRequiresFilesystem, + }); const exitCode = await selectedHarness.run(prompt, harnessRunOptions); await recordForwardedFulfillmentArtifact({ command: options.command, @@ -137,7 +141,12 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom function buildHarnessRunOptions( options: ForwardRunOptions, skillBootstrap: OpenProseSkillBootstrap | undefined, + behavior: { + harness: string; + requiresFilesystemWrites: boolean; + }, ): HarnessRunOptions { + const env = writeEnabledEnv(options.env, behavior); return { ...(skillBootstrap === undefined ? {} @@ -146,13 +155,32 @@ function buildHarnessRunOptions( systemPromptAppend: skillBootstrap.systemPromptAppend, }), cwd: options.cwd, - env: { ...options.env }, + env: { ...env }, stdout: options.stdout, stderr: options.stderr, ...(options.signal === undefined ? {} : { signal: options.signal }), }; } +function writeEnabledEnv( + env: Readonly>, + behavior: { harness: string; requiresFilesystemWrites: boolean }, +): Readonly> { + if ( + behavior.harness !== "codex-sdk" || + !behavior.requiresFilesystemWrites || + env.PROSE_CODEX_SANDBOX_MODE !== undefined || + process.env.PROSE_CODEX_SANDBOX_MODE !== undefined + ) { + return env; + } + + return { + ...env, + PROSE_CODEX_SANDBOX_MODE: "workspace-write", + }; +} + async function hydrateForwardedArgs( command: CommandName, args: readonly string[], diff --git a/tools/cli/tests/cli/cli.test.ts b/tools/cli/tests/cli/cli.test.ts index 31e7f599..6947291d 100644 --- a/tools/cli/tests/cli/cli.test.ts +++ b/tools/cli/tests/cli/cli.test.ts @@ -464,6 +464,58 @@ kind: system } }); + it("defaults codex write apply runs to workspace-write when no sandbox is configured", async () => { + const io = memoryStreams(); + const seenSandboxModes: Array = []; + + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--apply", "draft release readiness", "--harness", "codex-sdk"], + cwd: "/repo", + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + skillBootstrap: false, + skillPreflight: false, + harnessFactory: () => ({ + name: "codex-sdk", + async run(_prompt, options) { + seenSandboxModes.push(options.env?.PROSE_CODEX_SANDBOX_MODE); + return 0; + }, + }), + }); + + expect(exitCode).toBe(0); + expect(seenSandboxModes).toEqual(["workspace-write"]); + }); + + it("does not override an explicit codex sandbox for write apply runs", async () => { + const io = memoryStreams(); + const seenSandboxModes: Array = []; + + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--apply", "draft release readiness", "--harness", "codex-sdk"], + cwd: "/repo", + env: { PROSE_CODEX_SANDBOX_MODE: "danger-full-access" }, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + skillBootstrap: false, + skillPreflight: false, + harnessFactory: () => ({ + name: "codex-sdk", + async run(_prompt, options) { + seenSandboxModes.push(options.env?.PROSE_CODEX_SANDBOX_MODE); + return 0; + }, + }), + }); + + expect(exitCode).toBe(0); + expect(seenSandboxModes).toEqual(["danger-full-access"]); + }); + it("rejects write flags that would imply unsupported CLI interaction", async () => { const io = memoryStreams(); let harnessCalled = false; From 00b5a1ae32e63f0680d8cbe686beb2db6ff9e882 Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 14:16:27 -0400 Subject: [PATCH 3/6] Address write-run review followups --- skills/open-prose/SKILL.md | 5 ++++ tools/cli/README.md | 6 +++++ tools/cli/src/commands/base.ts | 3 +++ tools/cli/src/prose/command-model.ts | 3 ++- tools/cli/tests/cli/cli.test.ts | 29 +++++++++++++++++++++ tools/cli/tests/prose/command-model.test.ts | 1 + 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/skills/open-prose/SKILL.md b/skills/open-prose/SKILL.md index 82dc052b..4e8e590d 100644 --- a/skills/open-prose/SKILL.md +++ b/skills/open-prose/SKILL.md @@ -142,6 +142,11 @@ executing the system. The shell executable is the agent runner, e.g. | `prose examples` | List or run bundled examples from `examples/` | | Other | Interpret intent and load the smallest relevant spec set | +Shell wrappers may chain forwarded commands only when a single flag names a +deterministic pipeline, such as `prose write --run` forwarding authoring and +then the generated root run. The wrapper still must not inspect or execute the +generated source itself; VM semantics remain in this skill and `prose.md`. + There is one skill: `open-prose`. Do not look for separate `prose-run`, `prose-lint`, `prose-compile`, or `prose-boot` skills. diff --git a/tools/cli/README.md b/tools/cli/README.md index 109b25ff..a781cc50 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -168,6 +168,12 @@ write the package and then immediately invoke `prose run` on the generated root file. Directory targets run `/index.prose.md`; file targets must end in `.prose.md`. +`--out` is a validated, root-relative target contract passed to the authoring +harness. The CLI rejects absolute paths, parent traversal, and file targets +that do not end in `.prose.md`; the actual write is still performed by the +selected harness under the `prose-author` contract, not by a separate CLI +filesystem sandbox. + ```bash prose compile prose compile src/responsibilities --out dist diff --git a/tools/cli/src/commands/base.ts b/tools/cli/src/commands/base.ts index d1db055f..614b6cee 100644 --- a/tools/cli/src/commands/base.ts +++ b/tools/cli/src/commands/base.ts @@ -123,6 +123,9 @@ export async function runForwardedProseCommand(options: ForwardRunOptions): Prom if (exitCode !== 0 || writeRunTarget === undefined) { return exitCode; } + if (options.signal?.aborted) { + return 143; + } const runPrompt = canonicalPrompt("run", [writeRunTarget]); const runExitCode = await selectedHarness.run(runPrompt, harnessRunOptions); diff --git a/tools/cli/src/prose/command-model.ts b/tools/cli/src/prose/command-model.ts index bcb02d3f..7af0cc7a 100644 --- a/tools/cli/src/prose/command-model.ts +++ b/tools/cli/src/prose/command-model.ts @@ -324,7 +324,8 @@ function normalizeWriteTargetPath(value: string): string { if (normalized.split("/").includes("..")) { fail("write", "--out must stay inside the OpenProse root."); } - if (normalized.endsWith(".md") && !normalized.endsWith(".prose.md")) { + const lastSegment = normalized.split("/").at(-1) ?? normalized; + if (normalized !== "." && lastSegment.includes(".") && !normalized.endsWith(".prose.md")) { fail("write", "--out file paths must end in .prose.md."); } return normalized === "." ? "." : normalized; diff --git a/tools/cli/tests/cli/cli.test.ts b/tools/cli/tests/cli/cli.test.ts index 6947291d..7d1d614a 100644 --- a/tools/cli/tests/cli/cli.test.ts +++ b/tools/cli/tests/cli/cli.test.ts @@ -464,6 +464,35 @@ kind: system } }); + it("does not start the write follow-up run when aborted after apply", async () => { + const io = memoryStreams(); + const seen: string[] = []; + const controller = new AbortController(); + + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--run", "draft release readiness", "--harness", "mock"], + cwd: "/repo", + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + signal: controller.signal, + harnessFactory: () => ({ + name: "mock", + async run(prompt) { + seen.push(prompt); + controller.abort(); + return 0; + }, + }), + }); + + expect(exitCode).toBe(143); + expect(seen).toEqual([ + "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'draft release readiness'", + ]); + }); + it("defaults codex write apply runs to workspace-write when no sandbox is configured", async () => { const io = memoryStreams(); const seenSandboxModes: Array = []; diff --git a/tools/cli/tests/prose/command-model.test.ts b/tools/cli/tests/prose/command-model.test.ts index 18957a02..594c5f7b 100644 --- a/tools/cli/tests/prose/command-model.test.ts +++ b/tools/cli/tests/prose/command-model.test.ts @@ -67,6 +67,7 @@ describe("command model", () => { [["write", ["--out", "/tmp/source", "draft"]], "root-relative path", "prose write [--out ] [--apply] [--run] [request...]"], [["write", ["--out", "../source", "draft"]], "inside the OpenProse root", "prose write [--out ] [--apply] [--run] [request...]"], [["write", ["--out", "source.md", "draft"]], "end in .prose.md", "prose write [--out ] [--apply] [--run] [request...]"], + [["write", ["--out", "src/source.txt", "draft"]], "end in .prose.md", "prose write [--out ] [--apply] [--run] [request...]"], [["write", ["--out", "one", "--out", "two", "draft"]], "Duplicate option", "prose write [--out ] [--apply] [--run] [request...]"], [["inspect", []], "Missing required argument ", "prose inspect "], [["lint", ["system.md"]], "Expected ", "prose lint "], From f041cb63600a67ab8ec5a6160d3d33c6d803283a Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 16:26:02 -0400 Subject: [PATCH 4/6] Clarify write-run host adapter boundary --- README.md | 2 +- packages/std/ops/README.md | 2 +- packages/std/ops/prose-author.prose.md | 43 +++++++------ skills/open-prose/SKILL.md | 12 ++-- skills/open-prose/help.md | 2 +- skills/open-prose/prose.md | 2 +- tools/cli/README.md | 9 +-- tools/cli/src/prose/command-model.ts | 4 +- tools/cli/tests/cli/cli.test.ts | 61 +++++++++++++++++-- tools/cli/tests/prose/command-model.test.ts | 10 +-- .../tests/prose/prose-author-contract.test.ts | 12 ++++ tools/cli/tests/skills/open-prose.test.ts | 15 ++++- 12 files changed, 132 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 712862c2..5a3ee2c8 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ be checked, maintained, and restored across bounded runs: | `prose compile` | Run the bundled OpenProse compiler program, emit `/dist/manifest.next.json`, and validate it before success | | `prose serve` | Load active IR and run local cron and HTTP trigger adapters | | `prose run` | Execute one bounded OpenProse VM activation | -| `prose write` | Author a validated OpenProse program package from rough English/pseudo-Prose; shell `--out --apply` writes the package after lint passes, and `--out --run` writes then runs the generated root file | +| `prose write` | Author a validated OpenProse program package from rough English/pseudo-Prose; `--out --apply` writes the package after lint passes, and host adapters that support `--run` expand it to write/apply followed by ordinary `prose run ` | | `prose status` | Inspect active IR, diagnostics, trigger plan, recent runs, and responsibility status/pressure | The compiled Responsibility Runtime manifest preserves responsibilities as diff --git a/packages/std/ops/README.md b/packages/std/ops/README.md index 4549f2de..e0aa9816 100644 --- a/packages/std/ops/README.md +++ b/packages/std/ops/README.md @@ -19,7 +19,7 @@ come back as `unresolved-intent` instead of terminal prompts. | `preflight.prose.md` | `prose preflight ` | Check that dependencies are installed and environment variables are set | | `wire.prose.md` | `prose run std/ops/wire` | Run Forme wiring to produce an execution manifest | | `status.prose.md` | `prose status` | Show recent runs with system name, duration, and pass/fail status | -| `prose-author.prose.md` | `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring of a validated OpenProse package from rough English or pseudo-Prose; shell `--out --apply` may write it, and `--out --run` writes then runs it | +| `prose-author.prose.md` | `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring of a validated OpenProse package from rough English or pseudo-Prose; `--out --apply` may write it, and host adapters that support `--run` expand it to write/apply followed by ordinary `prose run ` | | `diagnose.prose.md` | `prose run std/ops/diagnose` | Diagnose why a run failed -- root cause analysis with fix recommendations | | `profiler.prose.md` | `prose run std/ops/profiler` | Profile a run for cost, tokens, and time using actual API session data | diff --git a/packages/std/ops/prose-author.prose.md b/packages/std/ops/prose-author.prose.md index 36569d12..f97c5d78 100644 --- a/packages/std/ops/prose-author.prose.md +++ b/packages/std/ops/prose-author.prose.md @@ -51,10 +51,11 @@ decisions instead of guessing. caller. When present with `apply: true`, generated files must stay under this path. Folder targets use `index.prose.md` as the root file; file targets must end in `.prose.md`. -- `run_after_write`: follow-up run mode. `false` means no follow-up run; - `host-managed` means the shell wrapper will run the generated root file after - successful apply. The authoring system must not execute the generated root - itself. +- `post_apply_action`: optional non-operational host follow-up marker. `none` + means no known host follow-up; `host-will-run-root` means the invoking host + adapter intends to run the generated root as a separate top-level + `prose run` after successful apply. This marker does not authorize + `prose-author` to run, simulate, or publish receipts for the generated root. - `run_state`: preferred run-state mode. `prose write` passes `in-context` so package-only authoring avoids creating run artifacts in the caller's workspace when the host can honor that mode. Apply-enabled shell runs may @@ -162,6 +163,9 @@ decisions instead of guessing. issue trackers, status pages, deploy systems, feature flags, and similar integrations are declarations or future runtime tools in the returned package, never actions performed by `prose-author`. +- `post_apply_action` is advisory only. `prose-author` must not execute, + simulate, or publish run receipts for the generated root; follow-up execution + belongs to the host adapter and ordinary `prose run` semantics. ### Tools @@ -211,7 +215,7 @@ let authoring_intent = call intent-normalizer output_mode: output_mode apply: apply target_path: target_path - run_after_write: run_after_write + post_apply_action: post_apply_action run_state: run_state terminal_summary: terminal_summary interactive: interactive @@ -250,7 +254,7 @@ let source_plan = call source-planner guidance_report: guidance_report apply: apply target_path: target_path - run_after_write: run_after_write + post_apply_action: post_apply_action let draft_source_package = call source-author source_plan: source_plan @@ -294,7 +298,7 @@ let assembled = call package-assembler guidance_report: guidance_report apply: apply target_path: target_path - run_after_write: run_after_write + post_apply_action: post_apply_action return { source_package: assembled.source_package, @@ -317,8 +321,9 @@ Normalize the caller's rough request into an explicit authoring intent. - `output_mode`: caller output mode, usually `source-package-only` - `apply`: whether this authoring run may write files, usually `false` - `target_path`: optional root-relative destination for generated source -- `run_after_write`: follow-up run mode, usually `false`; `host-managed` means - the shell wrapper will perform the run after a successful apply +- `post_apply_action`: non-operational host follow-up marker, usually `none`; + `host-will-run-root` means the host adapter intends to invoke ordinary + `prose run` after successful apply - `run_state`: preferred run-state mode, usually `in-context` - `terminal_summary`: whether the final status block is required - `interactive`: whether targeted follow-up questions are allowed before source @@ -338,8 +343,8 @@ Normalize the caller's rough request into an explicit authoring intent. - output_mode: caller output mode, preserved for downstream authoring - apply: caller apply flag, preserved for downstream authoring - target_path: caller destination path, preserved for downstream authoring - - run_after_write: caller follow-up run mode, preserved for downstream - authoring + - post_apply_action: caller follow-up marker, preserved for downstream + authoring but never executed by `prose-author` - run_state: caller run-state preference, preserved for downstream authoring - terminal_summary: caller terminal-summary requirement, preserved for @@ -610,8 +615,9 @@ Plan the generated file tree and contracts before drafting source. `guidance-loader` - `apply`: whether this run may write generated files - `target_path`: optional root-relative destination for generated files -- `run_after_write`: follow-up run mode; `host-managed` means the shell wrapper - will run the generated root file after a successful apply +- `post_apply_action`: non-operational host follow-up marker; + `host-will-run-root` means the host adapter intends to run the generated root + after successful apply ### Ensures @@ -846,8 +852,9 @@ Repair blocking lint findings without changing the caller's intent. `guidance-loader` - `apply`: whether this authoring run may write files - `target_path`: optional root-relative destination for generated source -- `run_after_write`: follow-up run mode; `host-managed` means the shell wrapper - will run the generated root file after a successful apply +- `post_apply_action`: non-operational host follow-up marker; + `host-will-run-root` means the host adapter intends to run the generated root + after successful apply ### Ensures @@ -914,8 +921,7 @@ Publish the validated source package and concise next-step notes. - `files_written`: `none` when no generated source files were applied, or a compact list/count of files written under `target_path` - `lint`: `pass` - - `next`: recommended command, the run command just triggered by the shell - wrapper, or a manual apply step + - `next`: recommended command, host follow-up note, or a manual apply step ### Errors @@ -931,6 +937,9 @@ Publish the validated source package and concise next-step notes. concrete files in `files_written`. - Do not claim files were written to the caller's repository unless this run actually wrote them through an explicitly requested file-writing step. +- Do not claim a follow-up run happened during authoring. When + `post_apply_action` is `host-will-run-root`, report only that the host adapter + is expected to invoke ordinary `prose run` after this authoring run succeeds. - End every successful package-only response with `final_status_summary` so a terminal user can distinguish success from validation failure at a glance. - When `terminal_summary` is `required`, treat the final status block as part of diff --git a/skills/open-prose/SKILL.md b/skills/open-prose/SKILL.md index 4e8e590d..cae8d19f 100644 --- a/skills/open-prose/SKILL.md +++ b/skills/open-prose/SKILL.md @@ -128,7 +128,7 @@ executing the system. The shell executable is the agent runner, e.g. | `prose run runtime/judge-responsibility.prose.md` | Resolve from the OpenProse skill root; judge one responsibility from activation context | | `prose run //[/path]` | Resolve installed dependency service or system, detect format, then route as above | | `prose run std/...` / `co/...` | Expand OpenProse package shorthand, resolve installed dependency service or system, then route as above | -| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring: load `contract-markdown.md`, `guidance/tenets.md`, and `guidance/authoring.md`; run `std/ops/prose-author`; scan the local landscape read-only, decide shape/root/path, load shape-specific guidance, ask a small number of targeted `ask_user` questions when the host can support them, then return a fully validated source package. If the caller or host marks the run non-interactive, return `unresolved-intent` with the missing decisions instead of guessing. Apply generated files only when the caller explicitly passes apply permission, normally through `--out --apply`; `--run` implies apply permission and then runs the generated root file | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring: load `contract-markdown.md`, `guidance/tenets.md`, and `guidance/authoring.md`; run `std/ops/prose-author`; scan the local landscape read-only, decide shape/root/path, load shape-specific guidance, ask a small number of targeted `ask_user` questions when the host can support them, then return a fully validated source package. If the caller or host marks the run non-interactive, return `unresolved-intent` with the missing decisions instead of guessing. Apply generated files only when the caller explicitly passes apply permission, normally through `--out --apply`; `--run` is a host-adapter macro that implies apply permission and expands to an ordinary `prose run ` only after authoring succeeds | | `prose lint ` | Validate Contract Markdown structure, headers, frontmatter, contracts, shapes, and wiring | | `prose preflight ` | Check dependencies and `### Environment` declarations without executing | | `prose test ` | Load `contract-markdown.md`, `state/README.md` plus the selected backend, and `prose.md`; run `kind: test` file(s) | @@ -142,10 +142,12 @@ executing the system. The shell executable is the agent runner, e.g. | `prose examples` | List or run bundled examples from `examples/` | | Other | Interpret intent and load the smallest relevant spec set | -Shell wrappers may chain forwarded commands only when a single flag names a -deterministic pipeline, such as `prose write --run` forwarding authoring and -then the generated root run. The wrapper still must not inspect or execute the -generated source itself; VM semantics remain in this skill and `prose.md`. +`prose write --run` is a host-adapter macro. A host that supports it must run +it as two top-level operations: invoke `prose-author` with apply permission, +then invoke ordinary `prose run` semantics for the generated root only if +authoring succeeds. A host that cannot perform this macro must reject +`prose write --run` before authoring and must not pass the macro into +`prose-author`. There is one skill: `open-prose`. Do not look for separate `prose-run`, `prose-lint`, `prose-compile`, or `prose-boot` skills. diff --git a/skills/open-prose/help.md b/skills/open-prose/help.md index 0e8a7b2b..34f484bd 100644 --- a/skills/open-prose/help.md +++ b/skills/open-prose/help.md @@ -48,7 +48,7 @@ Options: | `prose compile [path] [--out ]` | Compile source into `/dist/manifest.next.json` | | `prose serve` | Serve the active IR as local cron and HTTP trigger adapters | | `prose run ` | Run a service or system | -| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring from rough English/pseudo-Prose into a validated source package; `--out --apply` writes it after lint passes, and `--out --run` writes then runs the generated root file | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring from rough English/pseudo-Prose into a validated source package; `--out --apply` writes it after lint passes, and host adapters that support `--run` expand it to write/apply followed by ordinary `prose run ` | | `prose lint ` | Validate structure, schema, and contracts | | `prose preflight ` | Check dependencies and environment | | `prose test ` | Run tests with assertions | diff --git a/skills/open-prose/prose.md b/skills/open-prose/prose.md index e2232c04..1bc5b304 100644 --- a/skills/open-prose/prose.md +++ b/skills/open-prose/prose.md @@ -45,7 +45,7 @@ codex exec "prose run system.prose.md" | `prose run /` | Reserved for the OpenProse registry (future home at `p.prose.md`) | | `prose run ...@` | Pin to a SHA or tag; require that version in `/deps/` | | `prose run ... --offline` | Require disk-only resolution; error if not in `/deps/` | -| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring through `std/ops/prose-author`, asking targeted shape/root questions when supported and returning a validated source package; shell `--out --apply` writes it, and `--out --run` writes then runs the generated root file | +| `prose write [--out ] [--apply] [--run] [request...]` | Interactive-by-default authoring through `std/ops/prose-author`, asking targeted shape/root questions when supported and returning a validated source package; `--out --apply` writes it when permitted, and host adapters that support `--run` expand it to write/apply followed by ordinary `prose run ` | | `prose lint ` | Validate structure, schema, shapes, and contracts | | `prose preflight ` | Check dependencies, declared tools, and environment variables | | `prose test ` | Run test(s) and report results | diff --git a/tools/cli/README.md b/tools/cli/README.md index a781cc50..728e05ea 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -163,10 +163,11 @@ rejected; put literal text after `--` or pipe it on stdin. By default, `prose write` returns a source package for review and does not modify the repository. Use `--out ` with `--apply` to let the authoring -run write the generated package after lint passes. Use `--out --run` to -write the package and then immediately invoke `prose run` on the generated -root file. Directory targets run `/index.prose.md`; file targets must end -in `.prose.md`. +run write the generated package after lint passes. This CLI is a host adapter +that supports the `--run` macro: `--out --run` writes the package and +then starts a separate ordinary `prose run` for the generated root file only +after authoring succeeds. Directory targets run `/index.prose.md`; file +targets must end in `.prose.md`. `--out` is a validated, root-relative target contract passed to the authoring harness. The CLI rejects absolute paths, parent traversal, and file targets diff --git a/tools/cli/src/prose/command-model.ts b/tools/cli/src/prose/command-model.ts index 7af0cc7a..2e9bdb6b 100644 --- a/tools/cli/src/prose/command-model.ts +++ b/tools/cli/src/prose/command-model.ts @@ -66,8 +66,8 @@ export function canonicalPrompt(command: CommandName, args: readonly string[]): "apply:", String(write.apply), ...(write.out === undefined ? [] : ["target_path:", write.out]), - "run_after_write:", - write.run ? "host-managed" : "false", + "post_apply_action:", + write.run ? "host-will-run-root" : "none", "run_state:", write.apply ? "filesystem" : "in-context", "terminal_summary:", diff --git a/tools/cli/tests/cli/cli.test.ts b/tools/cli/tests/cli/cli.test.ts index 7d1d614a..b115430c 100644 --- a/tools/cli/tests/cli/cli.test.ts +++ b/tools/cli/tests/cli/cli.test.ts @@ -377,7 +377,7 @@ describe("runForwardedProseCommand", () => { expect(exitCode).toBe(0); expect(seen).toEqual([ - "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft release readiness'", + "prose write output_mode: source-package-only apply: false post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: 'draft release readiness'", ]); }); @@ -405,7 +405,7 @@ describe("runForwardedProseCommand", () => { expect(exitCode).toBe(0); expect(seen).toEqual([ - "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'", + "prose write output_mode: source-package-only apply: false post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'", ]); }); @@ -456,7 +456,7 @@ kind: system expect(exitCode).toBe(0); expect(seen).toEqual([ - "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'", + "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'", "prose run src/vulnerability-detection/index.prose.md", ]); } finally { @@ -464,6 +464,59 @@ kind: system } }); + it("does not start the write-run macro when authoring fails", async () => { + const io = memoryStreams(); + const seen: string[] = []; + + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--run", "draft release readiness", "--harness", "mock"], + cwd: "/repo", + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + harnessFactory: () => ({ + name: "mock", + async run(prompt) { + seen.push(prompt); + return 7; + }, + }), + }); + + expect(exitCode).toBe(7); + expect(seen).toEqual([ + "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'draft release readiness'", + ]); + }); + + it("returns the follow-up run exit code from the write-run macro", async () => { + const io = memoryStreams(); + const seen: string[] = []; + + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--run", "draft release readiness", "--harness", "mock"], + cwd: "/repo", + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + harnessFactory: () => ({ + name: "mock", + async run(prompt) { + seen.push(prompt); + return prompt.startsWith("prose run ") ? 9 : 0; + }, + }), + }); + + expect(exitCode).toBe(9); + expect(seen).toEqual([ + "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'draft release readiness'", + "prose run src/release-readiness/index.prose.md", + ]); + }); + it("does not start the write follow-up run when aborted after apply", async () => { const io = memoryStreams(); const seen: string[] = []; @@ -489,7 +542,7 @@ kind: system expect(exitCode).toBe(143); expect(seen).toEqual([ - "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'draft release readiness'", + "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'draft release readiness'", ]); }); diff --git a/tools/cli/tests/prose/command-model.test.ts b/tools/cli/tests/prose/command-model.test.ts index 594c5f7b..d35c7eeb 100644 --- a/tools/cli/tests/prose/command-model.test.ts +++ b/tools/cli/tests/prose/command-model.test.ts @@ -16,11 +16,11 @@ describe("command model", () => { ["run", ["system.prose.md", "--activation-context", "{\"kind\":\"openprose.activation\"}"]], "prose run system.prose.md --activation-context '{\"kind\":\"openprose.activation\"}'", ], - [["write", ["draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], - [["write", ["--out", "src/release-readiness", "--apply", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: false run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], - [["write", ["--out=src/release-readiness", "--run", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], - [["write", ["--out", "src/vulnerability-detection", "--run", "a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/"]], "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection run_after_write: host-managed run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'"], - [["write", ["--", "--no-interactive is literal request text"]], "prose write output_mode: source-package-only apply: false run_after_write: false run_state: in-context terminal_summary: required interactive: false request: '--no-interactive is literal request text'"], + [["write", ["draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out", "src/release-readiness", "--apply", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: none run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out=src/release-readiness", "--run", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out", "src/vulnerability-detection", "--run", "a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/"]], "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'"], + [["write", ["--", "--no-interactive is literal request text"]], "prose write output_mode: source-package-only apply: false post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: '--no-interactive is literal request text'"], [["lint", ["system.prose.md"]], "prose lint system.prose.md"], [["preflight", ["system.prose.md"]], "prose preflight system.prose.md"], [["test", ["tests/systems"]], "prose test tests/systems"], diff --git a/tools/cli/tests/prose/prose-author-contract.test.ts b/tools/cli/tests/prose/prose-author-contract.test.ts index c5c0a939..5a66c1e7 100644 --- a/tools/cli/tests/prose/prose-author-contract.test.ts +++ b/tools/cli/tests/prose/prose-author-contract.test.ts @@ -66,6 +66,18 @@ describe("prose-author contract", () => { expect(source).toContain("never actions performed by `prose-author`"); }); + it("keeps write-run as a non-operational host follow-up", () => { + const source = proseAuthorSource(); + const normalized = source.replace(/\s+/g, " "); + + expect(source).toContain("post_apply_action"); + expect(source).toContain("host-will-run-root"); + expect(normalized).toContain("does not authorize `prose-author` to run"); + expect(normalized).toContain("must not execute, simulate, or publish run receipts"); + expect(source).not.toContain("run_after_write"); + expect(source).not.toContain("host-managed"); + }); + it("requires a terminal-friendly success summary for prose write", () => { const source = proseAuthorSource(); diff --git a/tools/cli/tests/skills/open-prose.test.ts b/tools/cli/tests/skills/open-prose.test.ts index b27ad539..265ce815 100644 --- a/tools/cli/tests/skills/open-prose.test.ts +++ b/tools/cli/tests/skills/open-prose.test.ts @@ -1,6 +1,7 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import type { ProcessRunner } from "../../src/harnesses/index.js"; import { @@ -13,6 +14,8 @@ import { resolveOpenProseSkill, } from "../../src/skills/open-prose.js"; +const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url)); + function tempDir(): string { return mkdtempSync(join(tmpdir(), "prose-skill-")); } @@ -58,6 +61,16 @@ function memoryStream() { } describe("OpenProse skill checks", () => { + it("documents write-run as a host-adapter macro that unsupported session routers must reject", () => { + const source = readFileSync(join(repoRoot, "skills/open-prose/SKILL.md"), "utf8"); + const normalized = source.replace(/\s+/g, " "); + + expect(source).toContain("host-adapter macro"); + expect(normalized).toContain("reject `prose write --run` before authoring"); + expect(normalized).toContain("must not pass the macro into `prose-author`"); + expect(normalized).toContain("ordinary `prose run` semantics"); + }); + it("builds a bootstrap prompt with the full skill text and skill root instructions", () => { const bootstrap = buildOpenProseSkillBootstrapPrompt({ skillPath: "/home/prose/.agents/skills/open-prose/SKILL.md", From 757379a755bee70775c2ab89a7b5241725b59eb8 Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 16:38:47 -0400 Subject: [PATCH 5/6] Prevent write authoring note side effects --- packages/std/ops/prose-author.prose.md | 5 +++++ skills/open-prose/SKILL.md | 6 ++++++ tools/cli/tests/prose/prose-author-contract.test.ts | 2 ++ tools/cli/tests/skills/open-prose.test.ts | 10 ++++++++++ 4 files changed, 23 insertions(+) diff --git a/packages/std/ops/prose-author.prose.md b/packages/std/ops/prose-author.prose.md index f97c5d78..957b1286 100644 --- a/packages/std/ops/prose-author.prose.md +++ b/packages/std/ops/prose-author.prose.md @@ -163,6 +163,11 @@ decisions instead of guessing. issue trackers, status pages, deploy systems, feature flags, and similar integrations are declarations or future runtime tools in the returned package, never actions performed by `prose-author`. +- Authoring must not create optional memory notes, mycelium entries, + recognition memories, issue drafts, run evidence, or other non-source + artifacts while applying a generated package. Successful authoring depends + only on the generated source package, lint/repair checks, and explicitly + requested writes under `target_path`. - `post_apply_action` is advisory only. `prose-author` must not execute, simulate, or publish run receipts for the generated root; follow-up execution belongs to the host adapter and ordinary `prose run` semantics. diff --git a/skills/open-prose/SKILL.md b/skills/open-prose/SKILL.md index cae8d19f..b2aa751a 100644 --- a/skills/open-prose/SKILL.md +++ b/skills/open-prose/SKILL.md @@ -358,6 +358,12 @@ When writing a `### Execution` block or pattern delegation, also load You do not owe OpenProse anything. But if a run went well and you want to help the next agent succeed, here are the ways — and the gates each one needs. +Forwarded CLI commands, non-interactive runs, and `prose write` authoring runs +must not perform giving-back actions, save recognition memories, create +mycelium notes, file issues, or otherwise write optional non-source artifacts +unless the outer user explicitly asked for that action in the current run. +Authoring success must not depend on optional memory or note writes. + You are the user's proxy. Actions attach to identities, and the identity determines what you can do on your own vs. what needs approval: | Identity attached | Bucket | Approval gate | diff --git a/tools/cli/tests/prose/prose-author-contract.test.ts b/tools/cli/tests/prose/prose-author-contract.test.ts index 5a66c1e7..e4a5848d 100644 --- a/tools/cli/tests/prose/prose-author-contract.test.ts +++ b/tools/cli/tests/prose/prose-author-contract.test.ts @@ -64,6 +64,8 @@ describe("prose-author contract", () => { expect(source).toContain("not invoke external operational systems while authoring"); expect(source).toContain("never actions performed by `prose-author`"); + expect(source).toContain("must not create optional memory notes"); + expect(source).toContain("recognition memories"); }); it("keeps write-run as a non-operational host follow-up", () => { diff --git a/tools/cli/tests/skills/open-prose.test.ts b/tools/cli/tests/skills/open-prose.test.ts index 265ce815..7bf8d66a 100644 --- a/tools/cli/tests/skills/open-prose.test.ts +++ b/tools/cli/tests/skills/open-prose.test.ts @@ -71,6 +71,16 @@ describe("OpenProse skill checks", () => { expect(normalized).toContain("ordinary `prose run` semantics"); }); + it("keeps optional giving-back side effects out of forwarded write runs", () => { + const source = readFileSync(join(repoRoot, "skills/open-prose/SKILL.md"), "utf8"); + const normalized = source.replace(/\s+/g, " "); + + expect(normalized).toContain("Forwarded CLI commands, non-interactive runs, and `prose write` authoring runs"); + expect(normalized).toContain("must not perform giving-back actions"); + expect(normalized).toContain("create mycelium notes"); + expect(normalized).toContain("Authoring success must not depend on optional memory or note writes"); + }); + it("builds a bootstrap prompt with the full skill text and skill root instructions", () => { const bootstrap = buildOpenProseSkillBootstrapPrompt({ skillPath: "/home/prose/.agents/skills/open-prose/SKILL.md", From 5ff240199868264a7b6fc14a5847fb790393a1fc Mon Sep 17 00:00:00 2001 From: Raymond Weitekamp Date: Thu, 21 May 2026 18:35:29 -0400 Subject: [PATCH 6/6] Tighten write env defaults --- tools/cli/src/commands/base.ts | 3 +- tools/cli/tests/cli/cli.test.ts | 36 +++++++++++++++++++++ tools/cli/tests/prose/command-model.test.ts | 1 + 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/tools/cli/src/commands/base.ts b/tools/cli/src/commands/base.ts index 614b6cee..a4144f90 100644 --- a/tools/cli/src/commands/base.ts +++ b/tools/cli/src/commands/base.ts @@ -172,8 +172,7 @@ function writeEnabledEnv( if ( behavior.harness !== "codex-sdk" || !behavior.requiresFilesystemWrites || - env.PROSE_CODEX_SANDBOX_MODE !== undefined || - process.env.PROSE_CODEX_SANDBOX_MODE !== undefined + env.PROSE_CODEX_SANDBOX_MODE !== undefined ) { return env; } diff --git a/tools/cli/tests/cli/cli.test.ts b/tools/cli/tests/cli/cli.test.ts index b115430c..e0c7ead6 100644 --- a/tools/cli/tests/cli/cli.test.ts +++ b/tools/cli/tests/cli/cli.test.ts @@ -572,6 +572,42 @@ kind: system expect(seenSandboxModes).toEqual(["workspace-write"]); }); + it("derives codex sandbox defaults from the forwarded env only", async () => { + const io = memoryStreams(); + const seenSandboxModes: Array = []; + const previous = process.env.PROSE_CODEX_SANDBOX_MODE; + process.env.PROSE_CODEX_SANDBOX_MODE = "danger-full-access"; + + try { + const exitCode = await runForwardedProseCommand({ + command: "write", + argv: ["--out", "src/release-readiness", "--apply", "draft release readiness", "--harness", "codex-sdk"], + cwd: "/repo", + env: {}, + stdout: io.streams.stdout, + stderr: io.streams.stderr, + skillBootstrap: false, + skillPreflight: false, + harnessFactory: () => ({ + name: "codex-sdk", + async run(_prompt, options) { + seenSandboxModes.push(options.env?.PROSE_CODEX_SANDBOX_MODE); + return 0; + }, + }), + }); + + expect(exitCode).toBe(0); + expect(seenSandboxModes).toEqual(["workspace-write"]); + } finally { + if (previous === undefined) { + delete process.env.PROSE_CODEX_SANDBOX_MODE; + } else { + process.env.PROSE_CODEX_SANDBOX_MODE = previous; + } + } + }); + it("does not override an explicit codex sandbox for write apply runs", async () => { const io = memoryStreams(); const seenSandboxModes: Array = []; diff --git a/tools/cli/tests/prose/command-model.test.ts b/tools/cli/tests/prose/command-model.test.ts index d35c7eeb..4e4a684f 100644 --- a/tools/cli/tests/prose/command-model.test.ts +++ b/tools/cli/tests/prose/command-model.test.ts @@ -17,6 +17,7 @@ describe("command model", () => { "prose run system.prose.md --activation-context '{\"kind\":\"openprose.activation\"}'", ], [["write", ["draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], + [["write", ["--out", "src/release-readiness", "draft a release readiness responsibility"]], "prose write output_mode: source-package-only apply: false target_path: src/release-readiness post_apply_action: none run_state: in-context terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], [["write", ["--out", "src/release-readiness", "--apply", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: none run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], [["write", ["--out=src/release-readiness", "--run", "draft a release readiness responsibility"]], "prose write output_mode: source-package-and-files apply: true target_path: src/release-readiness post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'draft a release readiness responsibility'"], [["write", ["--out", "src/vulnerability-detection", "--run", "a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/"]], "prose write output_mode: source-package-and-files apply: true target_path: src/vulnerability-detection post_apply_action: host-will-run-root run_state: filesystem terminal_summary: required interactive: false request: 'a vulnerability detection system that uses lessons from https://blog.cloudflare.com/cyber-frontier-models/'"],