SKILL.md
Manage Logging Skill
Unified logging infrastructure providing script execution logging, semantic work progress tracking, and decision logging.
Scope: hybrid means this skill operates at both plan-scoped and global levels. When a plan_id is provided and the plan directory exists, logs go to .plan/plans/{plan-id}/logs/. Otherwise, logs fall back to global daily files at .plan/logs/.
A third, opt-in store exists for orchestrator epics: --store orchestrator on the work / decision / read verbs routes to the main-anchored orchestrator tree at .plan/local/orchestrator/{slug}/logs/ (resolved via fileops.getstore_dir, stable across sessions and worktree cwds). The default store is plans — every existing call path is unchanged.
Enforcement
Base contract: See [manage-contract.md](../ref-workflow-architecture/standards/manage-contract.md) for shared enforcement rules, TOON output format, and error response patterns.
Skill-specific constraints:
- Log entries are fire-and-forget (no output parsing needed)
- Only valid log levels:
INFO,WARNING,ERROR - Log type determines the output file (script, work, decision)
- Work log messages must include
[CATEGORY] (caller)prefix format
Overview
This skill provides a single unified API for three logging concerns:
- Script Execution Logging: Tracking of script executor invocations (type:
script) - Work Logging: Semantic tracking of work progress (type:
work) - Decision Logging: Tracking of decisions made during execution (type:
decision)
Log Files
All plan-scoped logs are stored in the logs/ subdirectory of the plan.
Script Execution Log
File: .plan/plans/{plan-id}/logs/script-execution.log (plan-scoped) Fallback: .plan/logs/script-execution-YYYY-MM-DD.log (global)
Work Log
File: .plan/plans/{plan-id}/logs/work.log Fallback: .plan/logs/work-YYYY-MM-DD.log (global)
Decision Log
File: .plan/plans/{plan-id}/logs/decision.log Fallback: .plan/logs/decision-YYYY-MM-DD.log (global)
Orchestrator Store (--store orchestrator)
Files: .plan/local/orchestrator/{slug}/logs/work.log and .plan/local/orchestrator/{slug}/logs/decision.log (main-anchored — no global fallback; --plan-id carries the epic slug). The orchestrator logged-event vocabulary (decision, interaction, plan-status-change, reconciliation) is defined in [standards/log-format.md](standards/log-format.md) § Orchestrator Logged Events.
CLI Script Usage
Script: plan-marshall:manage-logging:manage-logging
Write API
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
{type} [--plan-id {plan_id}] --level {level} --message "{message}"
Arguments:
| Argument | Required | Values | Description |
|---|---|---|---|
type |
Yes | script, work, decision |
Log type (determines output file) |
--plan-id |
No | kebab-case | Plan identifier. Optional on write subcommands — when omitted, the entry is written to the dated global log under .plan/logs/ (the first-class global/no-plan path); when supplied and resolving to an initialized plan, the entry is plan-scoped. Required with --store orchestrator (the epic slug — the orchestrator store has no global fallback). |
--level |
Yes | INFO, WARNING, ERROR |
Log level |
--message |
Yes | string | Log message |
--store |
No | plans (default), orchestrator |
Store selection — work and decision verbs only (script has no store flag). orchestrator writes main-anchored to .plan/local/orchestrator/{slug}/logs/{work,decision}.log. |
Output: None (exit code only)
Global / no-plan logging path
Omitting --plan-id is a first-class call, not an error — the entry lands in the dated global log under .plan/logs/ (work-{date}.log, decision-{date}.log, or script-execution-{date}.log). This is the supported path for plan-less callers — components that run BEFORE any plan exists, such as marshall-steward (the project-configuration wizard). It replaces the previous workaround of passing a non-existent plan id to force a global fallback.
STEWARD audit namespace
marshall-steward writes its audit trail through the global/no-plan path with a stable message prefix:
[STEWARD] (plan-marshall:marshall-steward) {what happened}
One entry per AskUserQuestion answer and per auto-decision (e.g. [STEWARD] (plan-marshall:marshall-steward) Selected balanced effort preset). Decision-class entries use the decision subcommand (the file is the category, so no [DECISION] prefix); informational/status entries use the work subcommand and carry a [STEWARD] category bracket. Example:
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
decision --level INFO --message "[STEWARD] (plan-marshall:marshall-steward) Selected balanced effort preset"
Separator API
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
separator --plan-id {plan_id} [--type {work|script|decision}]
Arguments:
| Argument | Required | Default | Description |
|---|---|---|---|
--plan-id |
Yes | - | Plan identifier |
--type |
No | work |
Log type: work, script, or decision |
Output: None (appends a blank line to the log file for visual separation between phases)
Read API
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
read --plan-id {plan_id} --type {work|script|decision} [--limit N] [--phase PHASE] [--store {plans|orchestrator}]
Arguments:
| Argument | Required | Description |
|---|---|---|
--plan-id |
Yes | Plan identifier (or epic slug with --store orchestrator) |
--type |
Yes | Log type: work, script, or decision |
--limit |
No | Max entries to return (most recent) |
--phase |
No | Filter by phase (work/decision logs only) |
--store |
No | Store selection: plans (default) or orchestrator |
Output (TOON):
status: success
plan_id: EXAMPLE-PLAN
log_type: work
total_entries: 5
showing: 3
entries:
- timestamp: 2025-12-11T11:14:30Z
level: INFO
hash_id: c8d3e2
message: [STATUS] (plan-marshall:phase-1-init) Starting init phase
phase: 1-init
- timestamp: 2025-12-11T11:15:20Z
level: INFO
hash_id: f1a9b3
message: [ARTIFACT] (plan-marshall:phase-1-init) Created deliverable: auth module
Examples
# Write: Script execution logging
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
script --plan-id EXAMPLE-PLAN --level INFO --message "plan-marshall:manage-tasks:manage-tasks add (0.15s)"
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
script --plan-id EXAMPLE-PLAN --level ERROR --message "plan-marshall:manage-tasks:manage-tasks add failed (exit 1)"
# Write: Work logging (include [CATEGORY] (caller) prefix)
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
work --plan-id EXAMPLE-PLAN --level INFO --message "[ARTIFACT] (plan-marshall:phase-1-init) Created deliverable: auth module"
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
work --plan-id EXAMPLE-PLAN --level WARNING --message "[STATUS] (plan-marshall:phase-5-execute) Skipped validation step"
# Write: Decision logging (NO [DECISION] prefix - file is the category)
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
decision --plan-id EXAMPLE-PLAN --level INFO --message "(plan-marshall:phase-1-init) Detected domain: java - pom.xml found"
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
decision --plan-id EXAMPLE-PLAN --level INFO --message "(pm-plugin-development:ext-outline-workflow) Scope: bundles=all"
# Read: All work log entries
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
read --plan-id EXAMPLE-PLAN --type work
# Read: All decision log entries
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
read --plan-id EXAMPLE-PLAN --type decision
# Read: Last 5 work log entries
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
read --plan-id EXAMPLE-PLAN --type work --limit 5
# Read: Work log entries for 1-init phase only
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
read --plan-id EXAMPLE-PLAN --type work --phase 1-init
Canonical invocations
The canonical argparse surface for manage-logging.py. The plugin-doctor missing-canonical-block rule checks that this section is PRESENT, matching its heading only — the body is never read; manage-invocation-invalid derives its accept-set from a live --help walk rather than from this section. Consuming skills xref this section by name (e.g., "see manage-logging Canonical invocations → work") instead of restating the command inline.
--plan-id is OPTIONAL on the three write subcommands (work / decision / script); omitting it writes to the dated global log under .plan/logs/. --store is available on work, decision, and read only (default plans); --store orchestrator requires --plan-id (the epic slug) and routes to .plan/local/orchestrator/{slug}/logs/.
work
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging work \
[--plan-id PLAN_ID] --level {INFO|WARNING|ERROR} --message TEXT \
[--store {plans|orchestrator}]
decision
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging decision \
[--plan-id PLAN_ID] --level {INFO|WARNING|ERROR} --message TEXT \
[--store {plans|orchestrator}]
script
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging script \
[--plan-id PLAN_ID] --level {INFO|WARNING|ERROR} --message TEXT
separator
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging separator \
--plan-id PLAN_ID [--type {work|decision|script}]
Default --type is work.
read
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging read \
--plan-id PLAN_ID --type {work|decision|script} \
[--limit N] [--phase PHASE] [--store {plans|orchestrator}]
--phase filters only work and decision log reads.
Log Format
Standard Entry Structure
[{timestamp}] [{level}] [{hash}] {message}
Every log entry automatically includes a 6-character hash computed from the message content. This provides:
- Transparent ID generation: Callers don't need to compute or track hashes
- Deterministic: Same message always produces the same hash
- Traceability: Findings can be linked across stages (analysis → resolution → Q-gate)
Example Output
script-execution.log:
[2025-12-11T12:14:26Z] [INFO] [a3f2c1] plan-marshall:manage-files:manage-files create (0.19s)
[2025-12-11T12:17:50Z] [ERROR] [b7e4d9] plan-marshall:manage-tasks:manage-tasks add failed (exit 1)
work.log:
[2025-12-11T11:14:30Z] [INFO] [c8d3e2] [STATUS] (plan-marshall:phase-1-init) Starting init phase
[2025-12-11T11:15:20Z] [INFO] [f1a9b3] [ARTIFACT] (plan-marshall:phase-1-init) Created deliverable: auth module
[2025-12-11T11:17:30Z] [INFO] [e5c7d4] [PROGRESS] (plan-marshall:phase-5-execute) Task 1 completed
decision.log:
[2025-12-11T11:14:48Z] [INFO] [d2e8f1] (plan-marshall:phase-1-init) Detected domain: java - pom.xml found
[2025-12-11T11:20:15Z] [INFO] [a4b6c8] (pm-plugin-development:ext-outline-workflow) Scope: bundles=all
Note: Decision entries do NOT include a [DECISION] prefix - the file itself indicates the entry type. The hash ID (e.g., [d2e8f1]) is automatically generated from the message content.
Log Levels
| Level | Description |
|---|---|
INFO |
Progress, informational, or successful completion message |
WARNING |
Warning (non-fatal issue) |
ERROR |
Error with details |
Python Import (from scripts run via executor)
Scripts run via the executor have PYTHONPATH set up for cross-skill imports:
from plan_logging import log_entry
# Function signature:
# log_entry(log_type: str, plan_id: str | None, level: str, message: str) -> None
# log_type: 'script', 'work', or 'decision'
# plan_id: plan identifier, or None for the global log
# level: 'INFO', 'WARNING', or 'ERROR'
# message: log message text
# Log to global script log
log_entry('script', None, 'INFO', '[MY-COMPONENT] Processing started')
# Log to plan-specific log
log_entry('work', 'example-plan', 'INFO', '[ARTIFACT] Created deliverable')
Note: IDE warnings about unresolved imports are expected - PYTHONPATH is set at runtime by the executor.
Error behavior: Logging calls are fire-and-forget. If the target directory doesn't exist or a write fails, the error is silently swallowed to avoid disrupting the calling script. This is intentional — logging should never cause a script to fail.
planid validity is enforced below this API as well — see the invalidplan_id note under [Error Responses](#error-responses).
Storage Locations
Plan-Scoped Logs
.plan/plans/{plan-id}/
└── logs/
├── script-execution.log # Script execution tracking
├── work.log # Work progress tracking
└── decision.log # Decision tracking
Global Logs
.plan/logs/
├── script-execution-YYYY-MM-DD.log # Daily global script logs
├── work-YYYY-MM-DD.log # Daily global work logs (when no plan)
└── decision-YYYY-MM-DD.log # Daily global decision logs
Orchestrator Logs
.plan/local/orchestrator/{slug}/
└── logs/
├── work.log # Interaction / plan-status-change / reconciliation events
└── decision.log # Orchestrator decision events
Scope Selection:
- If
--store orchestrator: main-anchored orchestrator log (requiresplan_id= epic slug; no fallback) - If
plan_idis provided and plan directory exists: plan-scoped log - Otherwise: global log (both script and work types supported)
Environment Variables
| Variable | Description | Default |
|---|---|---|
PLANBASEDIR |
Base directory for .plan structure | .plan |
LOGMAXOUTPUT |
Max chars to capture from stdout/stderr | 2000 |
LOGRETENTIONDAYS |
Days to keep global logs (used by cleanupoldscript_logs()) |
7 |
Error Responses
See [manage-contract.md](../ref-workflow-architecture/standards/manage-contract.md) for the standard error response format.
| Error Code | Cause |
|---|---|
invalidplanid |
plan_id format invalid |
missingplanid |
--store orchestrator supplied without --plan-id (the epic slug) |
invalidlogtype |
Log type not in: script, work, decision |
invalid_level |
Level not in: INFO, WARNING, ERROR |
write_failed |
File system permission denied or directory missing |
Note: Write operations are fire-and-forget — the Python log_entry() function silently swallows errors to avoid disrupting callers. The CLI script (manage-logging) returns exit code 1 on validation errors but silently succeeds on I/O failures.
Note: invalidplanid is also enforced below the CLI boundary. A malformed planid handed to the Python API is rejected at getlogpath — the shared path resolver — so the entry is dropped instead of falling back to the global log; an absent (None) planid remains the first-class global path. The read verbs never reach that resolver with a malformed identifier: their own isvalidplanid pre-check surfaces it as the invalidplan_id TOON error first. The fire-and-forget write verbs swallow it per the note above.
Integration
Producers
| Client | Operation | Purpose |
|---|---|---|
| Script executor | logscriptexecution() (auto) |
Log every script invocation with timing/exit code |
| Phase agents (1-6) | work |
Log artifacts, progress, outcomes during execution |
| Phase agents (2-6) | decision |
Log decision-making points during planning |
manage-files |
log_entry() (import) |
Log file write operations |
Consumers
| Client | Operation | Purpose |
|---|---|---|
phase-6-finalize |
read |
Query work/decision logs for PR summary |
| Debugging workflows | read |
Inspect execution history |
Scripts
| Script | Notation | Description |
|---|---|---|
manage-logging.py |
plan-marshall:manage-logging:manage-logging |
CLI for logging operations (write and read) |
plan_logging.py |
- | Python module (imported, not executed) |
Related
manage-status— Phase tracking that logging augmentsmanage-metrics— Metric data that complements work logsmanage-files— Generic file operations used alongside logging