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
18 changes: 10 additions & 8 deletions .ai-context/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ the v1.x compatibility window; new automation should use `--debug-wrapper`.
using a stable hash of the canonical project root to isolate same-named checkouts;
`--output` explicitly selects another destination, including project directories.
`--print` and `--list-files` do not write a bundle.
- `basectl trust status [project]` - inspect one project's manifest command
trust or all discovered command-bearing projects. Its terminal workspace
view is a compact, blocked-first summary; use
`basectl trust status <project>` for identity digests and review/allow
guidance.
- `basectl trust status [project]` - read-only inspection of local trust. With
no project, show a compact, blocked-first summary of discovered
command-bearing projects; with a project, show detailed identity and review
guidance. Workspace selection uses `--workspace`, then `workspace.root`, then
the parent of `BASE_HOME`.
- Generated onboarding trust commands include the canonical workspace and
manifest digest; they retain their target when copied to another directory.
- `basectl trust <allow|revoke> <project>` - add or remove local approval for
manifest-declared project commands. Revocation covers every approved contract
revision for that canonical checkout and manifest, preserving other checkouts.
- `basectl trust <allow|revoke> <project>` - `allow` records local approval for
the current manifest command contract on this machine; `revoke` removes local
approval for that canonical checkout and manifest, covering every approved
contract revision while preserving other checkouts. `--workspace` resolves
the project using the same default.
- `basectl prompt <list|name>` - list and render repo-owned Markdown prompts
for AI-assisted Base workflows. `product-self-review` prints the periodic
product assessment prompt with current Base metadata, and `--output <path>`
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ numeric next development line is tracked in `DEVELOPMENT_VERSION`.

### Changed

- Clarify `basectl trust` help for workspace scope and approval actions, and make
workspace status reasons identify stale approval.
- Make project-less terminal `basectl trust status` output a compact workspace
table with blocked projects first, short reasons, and an aggregate summary.

Expand Down
47 changes: 30 additions & 17 deletions cli/bash/commands/basectl/subcommands/trust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@ readonly _base_trust_subcommand_sourced

base_trust_subcommand_usage() {
cat <<'EOF'
Purpose:
Status is read-only: it shows detailed guidance for one project, or a compact
blocked-first summary of discovered command-bearing projects when no project
is supplied. Allow records local approval for manifest-declared commands;
revoke removes local approval.
See `basectl trust status <project>` for details on what approval covers.

Usage:
basectl trust status [project] [options]
basectl trust allow <project> [options]
basectl trust revoke <project> [options]

Options:
--project <name> Select a project explicitly, including one named help.
--workspace <path> Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.
--project <name> Select a project explicitly instead of the
positional <project> argument.
--workspace <path> Workspace for status discovery or project
resolution. Defaults to workspace.root, then
the parent of BASE_HOME.
--format <text|csv|tsv|yaml|json>
Output format for status. Defaults to text.
--manifest-sha256 <sha256> Expected manifest SHA-256 for allow.
--test-requirements-sha256 <sha256>
Expected SHA-256 for the declared test-requirements file.
-v Enable DEBUG logging for this subcommand.
-h, --help Show this help text.

Inspect trust for one project or all discovered projects, and manage local
approval for manifest-declared project commands.
EOF
}

Expand All @@ -36,12 +43,14 @@ Usage:
basectl trust status [project] [options]

Purpose:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Convention: Purpose: now comes before Usage: here and in the allow and revoke leaves (lines 62 and 83) and in the parent (line 8). The other 17 basectl subcommand files that have a Purpose section put Usage: first (for example ci.sh, doctor.sh, gh_issue.sh). #2647 only asked to move the parent summary ahead of Usage. I'd keep Usage first in the three leaves, and for the parent either keep the issue's requested order or accept it as a parent-only exception. Update the new help test accordingly if you move them (it does not depend on order today, since it flattens the text).

Show manifest command trust for one project, or for all discovered
command-bearing projects when no project is supplied.
Read-only trust inspection. Show detailed status for one project, or a
compact blocked-first summary of discovered command-bearing projects when no
project is supplied.

Options:
--project <name> Select a project explicitly, including one named help.
--workspace <path> Workspace directory to scan.
--project <name> Select a project explicitly instead of using [project].
--workspace <path> Workspace to scan for command-bearing projects.
Defaults to workspace.root, then the parent of BASE_HOME.
--format <text|csv|tsv|yaml|json>
Output format. Defaults to text.
-v Enable DEBUG logging for this subcommand.
Expand All @@ -54,12 +63,14 @@ Usage:
basectl trust allow <project> [options]

Purpose:
Approve the current manifest command contract for one project on this
machine.
Record local approval for the current manifest command contract on this
machine. See `basectl trust status <project>` for approval-scope details.

Options:
--project <name> Select a project explicitly, including one named help.
--workspace <path> Workspace directory to scan.
--project <name> Select a project explicitly instead of using

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nit: in the parent usage, instead of using its positional argument has no clear antecedent, because the block lists three commands. instead of the positional <project> argument would be unambiguous, and it matches what the leaves say.

<project>.
--workspace <path> Workspace used to resolve the project. Defaults
to workspace.root, then the parent of BASE_HOME.
--manifest-sha256 <sha256> Require the current manifest to match this digest.
--test-requirements-sha256 <sha256>
Require the declared test-requirements file to match this digest.
Expand All @@ -76,10 +87,12 @@ Purpose:
Remove local manifest command approval for one project.

Options:
--project <name> Select a project explicitly, including one named help.
--workspace <path> Workspace directory to scan.
-v Enable DEBUG logging for this subcommand.
-h, --help Show this help text.
--project <name> Select a project explicitly instead of using
<project>.
--workspace <path> Workspace used to resolve the project. Defaults
to workspace.root, then the parent of BASE_HOME.
-v Enable DEBUG logging for this subcommand.
-h, --help Show this help text.
EOF
;;
*)
Expand Down
69 changes: 64 additions & 5 deletions cli/bash/commands/basectl/tests/help.bats
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,48 @@ PY
done
}

@test "trust help describes status scope and approval effects" {
local help_text

run_basectl trust --help
[ "$status" -eq 0 ]
help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')"
[[ "$help_text" == *"Purpose:"*"Usage:"* ]]
[[ "$help_text" == *"instead of the positional <project> argument"* ]]
[[ "$help_text" == *"Status is read-only"* ]]
[[ "$help_text" == *"compact blocked-first summary of discovered command-bearing projects"* ]]
[[ "$help_text" == *"Allow records local approval"* ]]
[[ "$help_text" == *"revoke removes local approval"* ]]
[[ "$help_text" == *"workspace.root, then the parent of BASE_HOME"* ]]
[[ "$help_text" != *"all discovered projects"* ]]

run_basectl trust status --help
[ "$status" -eq 0 ]
help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')"
[[ "$help_text" == *"Usage:"*"Purpose:"* ]]
[[ "$help_text" == *"Read-only trust inspection"* ]]
[[ "$help_text" == *"command-bearing projects"* ]]
[[ "$help_text" == *"Select a project explicitly instead of using [project]"* ]]
[[ "$help_text" == *"Workspace to scan"* ]]
[[ "$help_text" == *"workspace.root, then the parent of BASE_HOME"* ]]

run_basectl trust allow demo --help
[ "$status" -eq 0 ]
help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')"
[[ "$help_text" == *"Usage:"*"Purpose:"* ]]
[[ "$help_text" == *"Record local approval for the current manifest command contract"* ]]
[[ "$help_text" == *"Workspace used to resolve the project"* ]]
[[ "$help_text" != *scan* ]]

run_basectl trust revoke demo --help
[ "$status" -eq 0 ]
help_text="$(tr '\n' ' ' <<<"$output" | tr -s ' ')"
[[ "$help_text" == *"Usage:"*"Purpose:"* ]]
[[ "$help_text" == *"Remove local manifest command approval"* ]]
[[ "$help_text" == *"Workspace used to resolve the project"* ]]
[[ "$help_text" != *scan* ]]
}

@test "command reference documents nested help and repo check release parity" {
local command_reference="$BASE_REPO_ROOT/docs/command-reference.md"
local repo_check_row
Expand Down Expand Up @@ -301,14 +343,21 @@ PY

@test "AI command context includes current clone and update surfaces" {
local commands_file="$BASE_REPO_ROOT/.ai-context/COMMANDS.md"
local commands_text

commands_text="$(tr '\n' ' ' <"$commands_file" | tr -s ' ')"

grep -Fqx -- "- \`basectl workspace <status|check|doctor|onboarding|agent-brief|clone|pull|update|init|configure|setup|test>\` -" "$commands_file"
grep -Fqx -- " - \`workspace clone\` mutates repository checkouts only when invoked directly;" "$commands_file"
grep -Fqx -- "- \`basectl repo <init|clone|check|configure|agent-guidance|installer-template>\` -" "$commands_file"
grep -Fqx -- "- \`basectl update [project]\` - update Base or a named project using the" "$commands_file"
grep -Fqx -- "- \`basectl docs\` - open the Base documentation home page on GitHub." "$commands_file"
grep -Fqx -- "- \`basectl trust status [project]\` - inspect one project's manifest command" "$commands_file"
grep -Fqx -- "- \`basectl trust <allow|revoke> <project>\` - add or remove local approval for" "$commands_file"
[[ "$commands_text" == *'- `basectl trust status [project]` - read-only inspection of local trust.'* ]]
[[ "$commands_text" == *'compact, blocked-first summary of discovered command-bearing projects'* ]]
[[ "$commands_text" == *'Workspace selection uses `--workspace`, then `workspace.root`, then the parent of `BASE_HOME`.'* ]]
[[ "$commands_text" == *'- `basectl trust <allow|revoke> <project>` - `allow` records local approval for the current manifest command contract on this machine; `revoke` removes local approval'* ]]
[[ "$commands_text" == *'covering every approved contract revision while preserving other checkouts.'* ]]
[[ "$commands_text" == *'`--workspace` resolves the project using the same default.'* ]]
}

@test "public command inventories include demo, run, and release" {
Expand Down Expand Up @@ -405,12 +454,22 @@ PY
grep -Fqx -- "| \`basectl docs\` | Open the Base documentation home page on GitHub. | \`--show-url\` |" "$command_reference"
}

@test "manifest trust docs define workspace stale-approval labels" {
local trust_doc="$BASE_REPO_ROOT/docs/manifest-command-trust.md"
local trust_text

trust_text="$(tr '\n' ' ' <"$trust_doc" | tr -s ' ')"
[[ "$trust_text" == *'`Approval stale: manifest`'* ]]
[[ "$trust_text" == *'`Approval stale: test reqs`'* ]]
[[ "$trust_text" == *'contract changed since approval'* ]]
}

@test "command reference documents trust commands" {
local command_reference="$BASE_REPO_ROOT/docs/command-reference.md"

grep -Fqx -- "| \`basectl trust status [project]\` | Show detailed trust guidance for one project, or a compact blocked-first table for discovered command-bearing projects. | \`--project <name>\`, \`--workspace <path>\`, \`--format <text\\|csv\\|tsv\\|yaml\\|json>\` |" "$command_reference"
grep -Fqx -- "| \`basectl trust allow <project>\` | Approve the current manifest command contract on this machine. | \`--project <name>\`, \`--workspace <path>\`, \`--manifest-sha256 <sha256>\`, \`--test-requirements-sha256 <sha256>\` |" "$command_reference"
grep -Fqx -- "| \`basectl trust revoke <project>\` | Remove local manifest command approval. | \`--project <name>\`, \`--workspace <path>\` |" "$command_reference"
grep -Fqx -- "| \`basectl trust status [project]\` | Read-only inspection: show detailed trust guidance for one project, or a compact blocked-first summary for discovered command-bearing projects. | \`--project <name>\`, \`--workspace <path>\`, \`--format <text\\|csv\\|tsv\\|yaml\\|json>\` |" "$command_reference"
grep -Fqx -- "| \`basectl trust allow <project>\` | Record local approval for the project's current manifest command contract on this machine. | \`--project <name>\`, \`--workspace <path>\`, \`--manifest-sha256 <sha256>\`, \`--test-requirements-sha256 <sha256>\` |" "$command_reference"
grep -Fqx -- "| \`basectl trust revoke <project>\` | Remove local manifest command approval for one project. | \`--project <name>\`, \`--workspace <path>\` |" "$command_reference"
}

@test "command reference documents repo and Project configuration options" {
Expand Down
2 changes: 1 addition & 1 deletion cli/bash/commands/basectl/tests/trust.bats
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ load ./basectl_helpers.bash
[[ "$output" == *"--manifest-sha256 <sha256>"* ]]
[[ "$output" == *"--test-requirements-sha256 <sha256>"* ]]
[[ "$output" != *"--format"* ]]
[[ "$output" != *"basectl trust status"* ]]
[[ "$output" == *"basectl trust status <project>"* ]]

run_basectl trust revoke --help

Expand Down
21 changes: 15 additions & 6 deletions cli/python/base_trust/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
help="Manage local approval for manifest-declared project commands.",
)

_STATUS_WORKSPACE_HELP = (
"Workspace directory to scan for command-bearing projects. "
"Defaults to workspace.root, then the parent of BASE_HOME."
)
_PROJECT_WORKSPACE_HELP = (
"Workspace directory used to resolve the project. "
"Defaults to workspace.root, then the parent of BASE_HOME."
)


def main(argv: list[str] | None = None) -> int:
return base_cli.run_app(app, argv)
Expand All @@ -58,7 +67,7 @@ def main(argv: list[str] | None = None) -> int:
@base_cli.argument("project", required=False)
@base_cli.option(
"--workspace",
help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.",
help=_STATUS_WORKSPACE_HELP,
)
@base_cli.option(
"--format",
Expand Down Expand Up @@ -104,7 +113,7 @@ def status_command(ctx: base_cli.Context, project: str | None, workspace: str |
@base_cli.argument("project")
@base_cli.option(
"--workspace",
help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.",
help=_PROJECT_WORKSPACE_HELP,
)
@base_cli.option("--manifest", "manifest_path", help="Resolved base_manifest.yaml path to verify.")
def require_command(ctx: base_cli.Context, project: str, workspace: str | None, manifest_path: str | None) -> int:
Expand All @@ -130,7 +139,7 @@ def require_command(ctx: base_cli.Context, project: str, workspace: str | None,
@base_cli.argument("project")
@base_cli.option(
"--workspace",
help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.",
help=_PROJECT_WORKSPACE_HELP,
)
@base_cli.option("--manifest-sha256", help="Expected SHA-256 digest of base_manifest.yaml.")
@base_cli.option(
Expand Down Expand Up @@ -177,7 +186,7 @@ def allow_command(
@base_cli.argument("project")
@base_cli.option(
"--workspace",
help="Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.",
help=_PROJECT_WORKSPACE_HELP,
)
def revoke_command(ctx: base_cli.Context, project: str, workspace: str | None) -> int:
try:
Expand Down Expand Up @@ -289,8 +298,8 @@ def _workspace_status_reason_label(reason: str) -> str:
labels = {
"allowed": "Approved",
"not_allowed": "Not approved",
"manifest_changed": "Manifest changed",
"test_requirements_changed": "Test requirements changed",
"manifest_changed": "Approval stale: manifest",
"test_requirements_changed": "Approval stale: test reqs",
}
if reason in labels:
return labels[reason]
Expand Down
79 changes: 77 additions & 2 deletions cli/python/base_trust/tests/test_command_error_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ def test_workspace_status_text_summarizes_blocked_reasons_before_allowed_project
)

assert result.exit_code == 0, result.output
assert "changed-manifest Blocked Manifest changed" in result.stdout
assert "changed-requirements Blocked Test requirements changed" in result.stdout
assert "changed-manifest Blocked Approval stale: manifest" in result.stdout
assert "changed-requirements Blocked Approval stale: test reqs" in result.stdout
assert "fresh Blocked Not approved" in result.stdout
assert "allowed Allowed Approved" in result.stdout
assert result.stdout.index("changed-manifest Blocked") < result.stdout.index("allowed Allowed")
Expand Down Expand Up @@ -229,6 +229,81 @@ def display_width(value: str) -> int:
assert len({display_width(line.split("Blocked", maxsplit=1)[0]) for line in rows}) == 1


def test_workspace_status_reason_labels_survive_narrow_terminal_fitting(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Worth a one-line comment here explaining the budget: at width 60 the 25-character label leaves zero slack (24 + 7 + 25 + 2 gaps of 2 = 60). That is a good thing, since it makes the test fail if the label grows, but a future editor will not know the width and the 40-character name are chosen on purpose.

_force_terminal_output(monkeypatch)
monkeypatch.setattr(base_cli_output, "_terminal_width", lambda _stream: 60)
# At width 60, the 24-character project, 7-character status, and 25-character
# reason use all columns with two 2-space gaps; the long name forces truncation
# while the longest reason label fits with zero slack.
long_project_name = "project-" + "x" * 32
assert len(long_project_name) == 40
statuses = (
engine.TrustStatus(
status="blocked",
reason="manifest_changed",
identity=engine.ManifestCommandTrustIdentity(
project_name=long_project_name,
project_root=tmp_path,
manifest_path=tmp_path / "base_manifest.yaml",
manifest_sha256="a" * 64,
identity_key=long_project_name,
),
),
engine.TrustStatus(
status="blocked",
reason="test_requirements_changed",
identity=engine.ManifestCommandTrustIdentity(
project_name="demo",
project_root=tmp_path,
manifest_path=tmp_path / "base_manifest.yaml",
manifest_sha256="b" * 64,
identity_key="demo",
),
),
)

engine._print_workspace_status_text(tmp_path, statuses) # pylint: disable=protected-access

output = capsys.readouterr().out
rows = [line for line in output.splitlines() if "Blocked" in line]
assert len(rows) == 2
assert any("Approval stale: manifest" in line for line in rows)
assert any("Approval stale: test reqs" in line for line in rows)


@pytest.mark.parametrize(
("command", "workspace_help", "expect_scan"),
[
("status", "Workspace directory to scan for command-bearing projects.", True),
("require", "Workspace directory used to resolve the project.", False),
("allow", "Workspace directory used to resolve the project.", False),
("revoke", "Workspace directory used to resolve the project.", False),
],
)
def test_trust_python_help_describes_workspace_resolution(
tmp_path: Path,
command: str,
workspace_help: str,
expect_scan: bool,
) -> None:
result = invoke(
engine.app,
[command, "--help"],
home=tmp_path / "home",
env={"BASE_HOME": str(tmp_path / "base")},
)

assert result.exit_code == 0, result.output
help_text = " ".join(result.stdout.split())
assert workspace_help in help_text
assert "Defaults to workspace.root, then the parent of BASE_HOME." in help_text
assert ("scan" in help_text) is expect_scan


def test_workspace_status_reason_fallback_preserves_acronyms() -> None:
assert engine._workspace_status_reason_label("remote_HTTP_status") == "Remote HTTP Status" # pylint: disable=protected-access

Expand Down
Loading
Loading