marketplace/bundles/plan-marshall/skills/manage-files/SKILL.md
Generic file I/O operations for plan directories
npx skillsauth add cuioss/plan-marshall manage-filesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generic file operations for plan directories. Provides basic CRUD operations for any file within a plan directory.
Base contract: See manage-contract.md for shared enforcement rules, TOON output format, and error response patterns.
Skill-specific constraints:
.. traversals in --file arguments# MUST be passed via --content-file or --stdin, never via inline --content. The Bash permission heuristic ("Newline followed by # inside a quoted argument can hide arguments from path validation") fires before manage-files.py runs, so a script-level rejector cannot prevent the originating prompt — caller compliance is the only enforcement surface. Canonical pattern: stage the payload via the Write tool to .plan/temp/{name}.{ext} first, then invoke manage-files write --plan-id {plan_id} --file work/{name}.{ext} --content-file .plan/temp/{name}.{ext}.Files are stored in plan directories:
.plan/plans/{plan_id}/
For domain-specific files within the plan directory, use the dedicated manage-* skills (see Relationship to Domain Skills below).
Script: plan-marshall:manage-files:manage-files
Read file content from a plan directory.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files read \
--plan-id {plan_id} \
--file notes.md
Output: Raw file content (no wrapping)
Write content to a file in a plan directory.
Use only for single-line scalar values.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files write \
--plan-id {plan_id} \
--file notes.txt \
--content "single line value with no newlines and no leading hash"
Use for any multi-line content (markdown, TOON, JSON) OR any payload whose first line begins with #.
# Stage payload via the Write tool to .plan/temp/payload.md
# Write(.plan/temp/payload.md) with the multi-line content
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files write \
--plan-id {plan_id} \
--file request.md \
--content-file .plan/temp/payload.md
Parameters:
--plan-id (required): Plan identifier--file (required): Relative file path within plan directory--content: Content to write (mutually exclusive with --content-file)--content-file PATH: Path to a UTF-8 file whose contents fill the write payload. Mutually exclusive with --content. Resolution order when multiple inputs are provided: --content-file wins over --content, which wins over --stdin. Combining --content and --content-file returns mutually_exclusive. A missing or non-regular path returns content_file_not_found.--stdin: Read content from stdin instead of --contentNote: For any multi-line content (markdown, TOON, JSON) OR any payload whose first line begins with #, the inline --content form is FORBIDDEN by the ## Enforcement clause above — use --content-file (stage via Write to .plan/temp/payload.{ext} first, then pass --content-file .plan/temp/payload.{ext}) or --stdin for piped input from another script. Do NOT use --stdin with shell heredocs or cat commands — the executor handles content passing; stdin is only for piped input from other scripts.
Content requirement: Content must be non-empty. Empty content produces an error (missing_content).
Output: TOON with status: success, action: created/updated. Exit code 0 on success, 1 on error.
Side effect: Successful writes are logged via log_entry() to the plan's work log.
Remove a file from a plan directory.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files remove \
--plan-id {plan_id} \
--file old-file.md
Output: Confirmation message to stderr, exit code 0 on success
List files in a plan directory.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files list \
--plan-id {plan_id} \
[--dir subdir]
Output (TOON format): File listing with status: success and files array
Check if a file exists. Returns TOON output with exists: true/false.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files exists \
--plan-id {plan_id} \
--file references.json
Output (TOON format):
When file exists:
status: success
plan_id: my-feature
file: references.json
exists: true
path: .plan/plans/my-feature/references.json
When file does not exist:
status: success
plan_id: my-feature
file: missing.md
exists: false
path: .plan/plans/my-feature/missing.md
On validation error (invalid plan_id or path):
status: error
plan_id: Invalid_Plan
error: invalid_plan_id
message: Invalid plan_id format: Invalid_Plan
Note: Always exits 0 for both exists=true and exists=false (both are valid query results). Only exits 1 for actual errors (invalid plan_id, invalid path). Check status and exists fields to determine result.
Create a subdirectory in a plan directory.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files mkdir \
--plan-id {plan_id} \
--dir requirements
Output (TOON format):
status: success
plan_id: my-feature
action: created
dir: requirements
path: /path/to/.plan/plans/my-feature/requirements
The action field is created if the directory was newly created, or exists if it already existed.
Discover filesystem paths matching one or more glob patterns under an absolute root directory. Pure pathlib implementation — never spawns a subprocess and never invokes the shell.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files discover \
--root /abs/path/to/search \
--glob "**/*.py" \
--glob "**/*.md" \
--include-files
Parameters:
--root (required): Absolute root directory to search under. Must exist.--glob (required, repeatable): Glob pattern relative to the root. Pass --glob multiple times to combine patterns; results are deduplicated and sorted.--include-files: Include files in results. Default behaviour when neither --include-files nor --include-dirs is given is files-only.--include-dirs: Include directories in results. Combine with --include-files to include both.Output (TOON format):
status: success
root: /abs/path/to/search
paths[N]:
- /abs/path/to/search/foo.py
- /abs/path/to/search/bar/baz.py
Error responses:
status: error
error: invalid_root
message: Root does not exist or is not a directory: {root}
status: error
error: no_patterns
message: At least one --glob pattern is required
Consumer skills should call manage-files discover rather than instructing the LLM to use the Glob tool. Routing discovery through this subcommand makes path resolution deterministic (the script always returns the same set for a given root + patterns), auditable (the call is logged via the executor), and decoupled from a particular harness's tool surface. The Glob tool is appropriate for ad-hoc exploration during a conversation; discover is appropriate for skill workflows that depend on a stable, reproducible set of paths.
Open a file in the active IDE. Detection is environment-based — __CFBundleIdentifier and TERM_PROGRAM on macOS, TERM_PROGRAM plus PATH probing on Linux — and the launcher is selected from a named per-platform table. On unknown IDE the verb refuses to fall through to open <path> / xdg-open <path> and exits non-zero with reason: ide_not_detected.
Two input modes:
Mode A — direct path:
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files open-in-ide \
--path /abs/path/to/file.md
Mode B — plan-resolved document:
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files open-in-ide \
--plan-id {plan_id} --document {request|solution_outline}
--path and --plan-id are mutually exclusive (argparse enforces). --document is required only in Mode B and is constrained by choices=(request, solution_outline).
Supported IDE matrix:
| Platform | Signal | Launcher |
|----------|--------|----------|
| macOS | __CFBundleIdentifier=com.jetbrains.intellij[.ce|-EAP] | open -a "IntelliJ IDEA" |
| macOS | __CFBundleIdentifier=com.jetbrains.pycharm | open -a "PyCharm" |
| macOS | __CFBundleIdentifier=com.jetbrains.WebStorm | open -a "WebStorm" |
| macOS | __CFBundleIdentifier=com.jetbrains.goland | open -a "GoLand" |
| macOS | __CFBundleIdentifier=com.jetbrains.rider | open -a "Rider" |
| macOS | __CFBundleIdentifier=com.google.android.studio | open -a "Android Studio" |
| macOS | TERM_PROGRAM=vscode | open -a "Visual Studio Code" |
| macOS | TERM_PROGRAM=cursor | open -a "Cursor" |
| Linux | TERM_PROGRAM=vscode AND code on PATH | code |
| Linux | TERM_PROGRAM=cursor AND cursor on PATH | cursor |
| Linux | PATH probe: idea → pycharm → webstorm → goland → rider → studio | first match wins |
TOON return shapes:
Success (launch fired):
status: success
ide: "IntelliJ IDEA"
command: "open -a IntelliJ IDEA /abs/path"
path: /abs/path
Success but disabled by config (no detection, no launcher invocation):
status: success
action: skipped
reason: disabled_by_config
Error:
status: error
reason: ide_not_detected | launcher_missing | document_resolution_failed | invalid_arguments
detail: "<short context>"
No-tempfile invariant: The script call graph imports neither tempfile nor mkstemp / NamedTemporaryFile / mkdtemp. Absolute paths are passed verbatim to the launcher. A static AST guard in test_manage_files_open_in_ide.py enforces this.
The verb is gated by the boolean config key plan.open_in_ide in .plan/marshal.json (a flat boolean under the existing plan namespace). Resolution rules:
status: success, action: skipped, reason: disabled_by_config. Detection and launcher invocation are NEVER triggered.plan namespace absent, or marshal.json file absent → treated as true (the documented default).marshal.json raises (consistent with the rest of the config surface).manage-config init seeds fresh marshal.json files with plan.open_in_ide: true (marshall-steward seed contract).
Create a plan directory if it doesn't exist, or reference an existing one. This is an atomic operation that replaces the two-step pattern of listing plans and checking for conflicts.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files create-or-reference \
--plan-id {plan_id}
Output (TOON format):
When plan is newly created:
status: success
plan_id: my-feature
action: created
path: /path/to/.plan/plans/my-feature
When plan already exists:
status: success
plan_id: my-feature
action: exists
path: /path/to/.plan/plans/my-feature
current_phase: refine
domain: java
Use case: Called by plan-init to atomically check/create plan directories.
base_path()--file accepts relative paths within plan dir (e.g., requirements/REQ-001.toon)read returns raw content; mutations return minimal status| Check | Validation |
|-------|------------|
| plan_id format | kebab-case, no special chars |
| file path | No .., no absolute paths, no leading / |
| directory | Must exist (unless mkdir) |
| content | Non-empty for write |
See manage-contract.md for the standard error response format.
| Error Code | Cause |
|------------|-------|
| invalid_plan_id | plan_id contains invalid characters (must be kebab-case) |
| file_not_found | File does not exist (read, remove) |
| missing_content | Write called with empty or missing content |
| mutually_exclusive | Write called with both --content and --content-file |
| content_file_not_found | --content-file path is missing or not a regular file |
| invalid_path | Path contains .. or absolute path components |
| permission_error | File system permission denied |
| ide_not_detected | open-in-ide: no supported IDE matched env + platform |
| launcher_missing | open-in-ide: launcher binary missing or returned non-zero |
| document_resolution_failed | open-in-ide: Mode B resolver returned error |
| invalid_arguments | open-in-ide: Mode B invoked without --document |
The canonical argparse surface for manage-files.py. The D4 plugin-doctor analyzer
(_analyze_manage_invocation.py) reads this section as source-of-truth for markdown
notation occurrences across the marketplace. Consuming skills xref this section by
name (e.g., "see manage-files Canonical invocations → write") instead of
restating the command inline.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files read \
--plan-id PLAN_ID --file REL_PATH
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files write \
--plan-id PLAN_ID --file REL_PATH \
(--content TEXT | --content-file PATH | --stdin)
--content, --content-file, and --stdin are mutually exclusive; exactly one
must be supplied.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files remove \
--plan-id PLAN_ID --file REL_PATH
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files list \
--plan-id PLAN_ID [--dir SUBDIR]
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files exists \
--plan-id PLAN_ID --file REL_PATH
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files mkdir \
--plan-id PLAN_ID --dir REL_PATH
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files create-or-reference \
--plan-id PLAN_ID
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files discover \
--root ABS_ROOT --glob PATTERN [--glob PATTERN ...] \
[--include-files] [--include-dirs]
--glob is repeatable. Defaults to files-only when neither --include-files
nor --include-dirs is supplied.
python3 .plan/execute-script.py plan-marshall:manage-files:manage-files open-in-ide \
(--path ABS_PATH | --plan-id PLAN_ID --document {request|solution_outline})
--path and --plan-id are mutually exclusive (Mode A vs Mode B). --document is required in Mode B and constrained to the enum above.
| Client | Operation | Purpose |
|--------|-----------|---------|
| phase-1-init | create-or-reference, write | Create plan directory and initial files |
| phase-3-outline | write, read | Generic file I/O for plan artifacts |
| phase-5-execute | read, write, list | File operations during task execution |
| Skill | Manages | Use manage-files for | |-------|---------|---------------------| | manage-references | references.json | N/A (use manage-references) | | manage-status | status.json | N/A (use manage-status) | | manage-plan-documents | request.md | N/A (use manage-plan-documents) | | manage-solution-outline | solution_outline.md | N/A (use manage-solution-outline) | | manage-tasks | tasks/*.toon | N/A (use manage-tasks) | | manage-files | any other file | Generic read/write/list |
manage-files has no resolve-path verb of its own — it exposes only the generic file operations listed under Operations above. Callers needing a resolved absolute path to a plan document MUST use the owning typed skill, not manage-files:
plan-marshall:manage-solution-outline:manage-solution-outline resolve-pathplan-marshall:manage-plan-documents:manage-plan-documents request pathA manage-files resolve-path call does not exist in the argparse surface and will be rejected with exit_code: 2 (argparse_rejection). Route path resolution through the owning script above.
The broader class of caller-drift argparse_rejection failures (invented or paraphrased manage-* subcommands and flags) is remediated by the pm-plugin-development:recipe-fix-argparse-rejection corpus — no bespoke per-call guard is added here.
manage-plan-documents — Typed plan document operations (request.md)manage-references — Reference tracking for plans (references.json)manage-logging — Logging operations that complement file I/Odevelopment
The single append-only change-ledger — one worktree_sha-stamped substrate for kind=build and kind=change entries — plus the first-class worktree-sha freshness API
development
Authoring standards for ASCII box diagrams in skill and doc source — box-drawing conventions, right-border alignment, and a deterministic check/fix validator over fenced/literal code blocks in .md and .adoc files
testing
Recipe for verifying and fixing alignment of ASCII box diagrams across .md skill source and .adoc documentation, one deliverable per offending file
development
Pure platform-agnostic terminal-title composition consumed by platform-runtime via PYTHONPATH