Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions construct-cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`construct` is the Spacecraft Software **Construct** skills package manager (Rust
CLI + TUI) — the first executable in the Construct catalogue repository. It
conforms to the Spacecraft Software Dual-Mode Self-Documenting CLI Standard
(v1.0.0). This file and `CLAUDE.md` are peers; keep them identical.
(v1.1.0). This file and `CLAUDE.md` are peers; keep them identical.

## Build / test / lint

Expand Down Expand Up @@ -32,12 +32,19 @@ so run it locally before adding a dependency (Standard §3.3).
- `cli.rs` — the clap derive tree and the §3 global flags (`global = true`).
- `context.rs` — per-invocation resolved state (output mode, color, flags).
- `src/output/` is the **only** place that writes to stdout:
- `mode.rs` — the §5 detection cascade + §6 color precedence.
- `mode.rs` — the §5 detection cascade + §6 color precedence
(FORCE_COLOR overrides NO_COLOR).
- `envelope.rs` — the `{ metadata, data }` JSON envelope.
- `error.rs` — the structured `AppError` (machine: single-line `{"error":…}`).
- `error.rs` — the structured `AppError` (machine: single-line `{"error":…}`;
human: `[ERROR]`-tagged line + indented `hint:`). Never suppressible.
- `diagnostic.rs` — non-error diagnostics (`Severity` ladder `[OK]`/`[WARN]`/
`[INFO]`, machine: single-line `{"diagnostic":…}`, human: `[TAG]` line),
gated by `Context::severity_floor` (`--quiet` → errors only, agent env →
`warn`+, default → `ok`+, `--verbose` → `info`+). See the CLI Standard's
`references/diagnostics.md`.
- `render.rs` — json / jsonl / yaml / csv / human renderers; `--fields`.
- `theme.rs` — the Steelbore palette (v1.33 tokens, grandfathered per
Standard §11.1 until the next minor release; no inline hex).
- `theme.rs` — the `steelbore` theme: the eleven Steelbore 2 role tokens of
Standard §11.1 (no inline hex).
- `src/commands/` — one handler per command.
- `manifest.rs` — the single source of truth for `schema` and `describe`; the
`tests::manifest_in_sync_with_cli` test fails if it drifts from the clap tree.
Expand All @@ -50,6 +57,9 @@ so run it locally before adding a dependency (Standard §3.3).
- All timestamps go through `time::now_iso8601()` → ISO 8601 UTC with `Z`. Never
local time, never `chrono::Local` / `NaiveDateTime`.
- Errors are `AppError` whose `hint` is a RUNNABLE command, not prose.
- Every non-error stderr message goes through `output::diagnostic::Diagnostic`
(or `emit_passthrough` for raw subprocess output) so the severity floor and
`[TAG]` rendering apply — no bare `eprintln!` diagnostics.
- Exit codes follow the canonical map (0,1,2,3,4,5,127,…).
- Every `.rs` / `.toml` starts with the two-line SPDX header; license is
`GPL-3.0-or-later`.
Expand Down
2 changes: 1 addition & 1 deletion construct-cli/src/commands/ship.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub(crate) fn run(ctx: &Context, args: &ShipArgs) -> Result<CommandOutput, AppEr
ErrorCode::Conflict,
5,
format!("SKILL.md description exceeds the {DESCRIPTION_CAP}-character cap: {detail}"),
format!("$EDITOR {first}/SKILL.md # trim the `description` frontmatter field"),
format!("python3 .githooks/check-description-length.py {first}/SKILL.md"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the description-cap hint use the selected repo

When construct skill ship --repo /path/to/clone is invoked from outside that clone—a supported use case—this hint searches the caller's current directory for both .githooks/check-description-length.py and the skill, so pasting it fails instead of providing the promised runnable recovery command. Build both paths from repo (and quote them safely) so the hint works independently of the invocation directory.

AGENTS.md reference: construct-cli/AGENTS.md:L57-L60

Useful? React with 👍 / 👎.

)
.with_extension(
"oversized_skills",
Expand Down
10 changes: 3 additions & 7 deletions construct-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,9 @@ pub(crate) fn flake_update(ctx: &Context, flake_dir: &Path) -> Result<String, Ap
}
};

// nix logs progress to stderr; surface it only when the user asked (-v).
if ctx.verbose > 0 {
let progress = String::from_utf8_lossy(&output.stderr);
if !progress.trim().is_empty() {
eprint!("{progress}");
}
}
// nix logs progress to stderr; it is info-level passthrough, visible only
// when the severity floor admits it (`--verbose`, diagnostics.md §4).
crate::output::diagnostic::emit_passthrough(ctx, &String::from_utf8_lossy(&output.stderr));

if !output.status.success() {
return Err(AppError::general(
Expand Down
56 changes: 55 additions & 1 deletion construct-cli/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! stays consistent across the whole surface.

use crate::cli::Cli;
use crate::output::diagnostic::Severity;
use crate::output::mode::{self, OutputMode};

/// Resolved runtime settings for a single invocation.
Expand Down Expand Up @@ -37,14 +38,20 @@ pub(crate) struct Context {
pub(crate) yes: bool,
/// `--absolute-time`: render absolute timestamps in human mode.
pub(crate) absolute_time: bool,
/// The minimum severity emitted to stderr (diagnostics.md §4), resolved
/// once per invocation from `--quiet` / `--verbose` / the agent env.
pub(crate) severity_floor: Severity,
/// Why `--format explore` fell back to JSON, when it did. Emitted as a
/// `TUI_FALLBACK` warn diagnostic by `main` once the context exists.
pub(crate) tui_fallback: Option<&'static str>,
}

impl Context {
/// Build the context from parsed CLI arguments, applying the output-mode
/// detection cascade and color precedence chain.
pub(crate) fn from_cli(cli: &Cli) -> Self {
let g = &cli.global;
let mode = mode::resolve(g);
let (mode, tui_fallback) = mode::resolve(g);
Self {
command: invocation_string(),
mode,
Expand All @@ -56,8 +63,31 @@ impl Context {
print0: g.print0,
yes: g.yes,
absolute_time: g.absolute_time,
severity_floor: resolve_floor(g.quiet, g.verbose, mode::is_agent_env()),
tui_fallback,
}
}

/// Whether a diagnostic of `severity` clears the floor and is emitted.
/// Errors always do — `AppError` never consults the floor.
pub(crate) fn allows(&self, severity: Severity) -> bool {
severity >= self.severity_floor
}
}

/// Resolve the severity floor (diagnostics.md §4). Explicit flags beat the
/// environment: `--quiet` → errors only; `--verbose` → everything; a detected
/// agent env → failures and degradations (`warn`+); default → `ok`+.
fn resolve_floor(quiet: bool, verbose: u8, agent_env: bool) -> Severity {
if quiet {
Severity::Error
} else if verbose > 0 {
Severity::Info
} else if agent_env {
Severity::Warn
} else {
Severity::Ok
}
}

/// The full command line with `argv[0]` normalized to the canonical binary name
Expand All @@ -69,3 +99,27 @@ fn invocation_string() -> String {
}
args.join(" ")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn floor_table_matches_diagnostics_spec() {
// --quiet → errors only; beats the agent env.
assert_eq!(resolve_floor(true, 0, true), Severity::Error);
// --verbose → everything; beats the agent env.
assert_eq!(resolve_floor(false, 1, true), Severity::Info);
// agent env → failures and degradations.
assert_eq!(resolve_floor(false, 0, true), Severity::Warn);
// default → ok and up.
assert_eq!(resolve_floor(false, 0, false), Severity::Ok);
}

#[test]
fn severity_ordering_backs_the_floor_comparison() {
assert!(Severity::Info < Severity::Ok);
assert!(Severity::Ok < Severity::Warn);
assert!(Severity::Warn < Severity::Error);
}
}
6 changes: 6 additions & 0 deletions construct-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ fn real_main() -> i32 {

let ctx = Context::from_cli(&cli);

// `--format explore` fell back to JSON: surface why as a warn diagnostic,
// now that the context can render it per mode and severity floor.
if let Some(reason) = ctx.tui_fallback {
output::diagnostic::emit_tui_fallback(&ctx, reason);
}

let dispatched = std::panic::catch_unwind(AssertUnwindSafe(|| commands::dispatch(&cli, &ctx)));

match dispatched {
Expand Down
Loading
Loading