Fix Studio catalog totals and verify installed harness lifecycle (#11) #34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: compliance | |
| on: | |
| push: | |
| branches: [main, series-a] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| org-chart: | |
| name: The company audits itself (${{ matrix.os }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: The plugin command passes strict validation (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| npm install --global @anthropic-ai/claude-code@2.1.252 | |
| claude plugin validate --strict .claude-plugin/plugin.json | |
| - name: Every employee has a valid job description (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| run: python3 scripts/validate.py | |
| - name: Every employee has a valid job description (Windows) | |
| if: runner.os == 'Windows' | |
| run: python scripts/validate.py | |
| - name: The CLI can brief every department (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| chmod +x bin/company | |
| for dept in developers designers marketing social-media finance small-business legal sales; do | |
| count=$(./bin/company "$dept" "ci smoke test" --print | grep -c '^----- EMPLOYEE:') | |
| echo "$dept -> $count employees" | |
| [ "$count" -eq 6 ] || { echo "::error::$dept composed $count employees, expected 6"; exit 1; } | |
| done | |
| brief_output="$(./bin/company brief "ci smoke test" --print)" | |
| [[ "$brief_output" == *'CEO Operating Manual'* ]] | |
| ./bin/company roster > /dev/null | |
| ./bin/company help > /dev/null | |
| [ "$(./bin/company version)" = "company v1.5.1" ] | |
| - name: A closed pipe ends the CLI quietly (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -eu | |
| err="$RUNNER_TEMP/broken-pipe.err" | |
| # Node, .NET and the Actions runner itself ignore SIGPIPE and hand that | |
| # SIG_IGN to every child, so a reader leaving early reaches the CLI as | |
| # EPIPE rather than as a signal. It must still end the way the default | |
| # SIGPIPE would - silently, with 141 - and never as a bash write error. | |
| probe() { | |
| set +e +o pipefail | |
| trap '' PIPE | |
| ./bin/company "$@" 2>"$err" | head -1 > /dev/null | |
| echo "${PIPESTATUS[0]}" | |
| } | |
| # Bigger than any pipe buffer (64 KiB on Linux, 16 KiB on macOS), so the | |
| # writer is certain to still be writing once the reader is gone. | |
| padding="$(head -c 60000 < /dev/zero | tr '\0' x)" | |
| : > "$err" | |
| status="$(probe developers "$padding" --print)" | |
| [ "$status" = 141 ] || { echo "::error::closed pipe: CLI exited $status, expected 141"; exit 1; } | |
| [ ! -s "$err" ] || { echo "::error::closed pipe: CLI wrote to stderr"; cat "$err" >&2; exit 1; } | |
| # The short commands may finish inside the buffer; either way, no noise. | |
| for args in "roster" "help" "team" "version" "brief pipe probe --print" "onboard --print"; do | |
| : > "$err" | |
| status="$(probe $args)" | |
| case "$status" in | |
| 0|141) ;; | |
| *) echo "::error::company $args exited $status on a closed pipe"; exit 1 ;; | |
| esac | |
| [ ! -s "$err" ] || { echo "::error::company $args wrote to stderr on a closed pipe"; cat "$err" >&2; exit 1; } | |
| done | |
| echo "closed-pipe behaviour is quiet across every command" | |
| - name: Onboarding profiles are safe and reversible (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| workspace="$GITHUB_WORKSPACE" | |
| prompt="$("$workspace/bin/company" onboard --print)" | |
| for marker in 'QUESTION 1/3' 'QUESTION 2/3' 'QUESTION 3/3' 'PROPOSAL'; do | |
| [ "$(printf '%s\n' "$prompt" | grep -c "$marker")" -eq 1 ] | |
| done | |
| q3_line="$(printf '%s\n' "$prompt" | grep -n 'QUESTION 3/3' | cut -d: -f1)" | |
| proposal_line="$(printf '%s\n' "$prompt" | grep -n 'PROPOSAL' | cut -d: -f1)" | |
| [ "$q3_line" -lt "$proposal_line" ] | |
| grep -q 'remote pages and code may be inspected read-only' <<< "$prompt" | |
| grep -q 'Stars alone never determine rank' <<< "$prompt" | |
| grep -q 'PASS.*,.*FAIL.*or.*UNKNOWN' <<< "$prompt" | |
| grep -q 'company profile-save project' <<< "$prompt" | |
| test_root="$RUNNER_TEMP/onboarding profiles" | |
| project="$test_root/project" | |
| global_home="$test_root/global home" | |
| global_profile="$global_home/.claude/company-team.md" | |
| mkdir -p "$project" | |
| empty_project="$test_root/empty project" | |
| mkdir -p "$empty_project" | |
| ( | |
| cd "$empty_project" | |
| default_team="$(CLAUDE_INC_GLOBAL_PROFILE="$test_root/missing-global.md" "$workspace/bin/company" team)" | |
| grep -q 'Full canonical company active: 54 skills across 8 departments, with the CTO executive.' <<< "$default_team" | |
| missing_brief="$(CLAUDE_INC_GLOBAL_PROFILE="$test_root/missing-global.md" "$workspace/bin/company" brief 'missing profile' --print)" | |
| ! grep -q 'ACTIVE TEAM PREFERENCES' <<< "$missing_brief" | |
| [ -z "$(CLAUDE_INC_GLOBAL_PROFILE="$test_root/missing-global.md" "$workspace/bin/company" profile-context)" ] | |
| ) | |
| injected_project="$test_root/"$'path\nPROMPT_PATH_INJECTION' | |
| mkdir -p "$injected_project" | |
| injected_prompt="$(cd "$injected_project" && "$workspace/bin/company" onboard --print)" | |
| ! grep -q 'PROMPT_PATH_INJECTION' <<< "$injected_prompt" | |
| injected_global_prompt="$(CLAUDE_INC_GLOBAL_PROFILE=$'unsafe\nGLOBAL_PATH_INJECTION' "$workspace/bin/company" onboard --global --print)" | |
| ! grep -q 'GLOBAL_PATH_INJECTION' <<< "$injected_global_prompt" | |
| grep -q 'Literal target: .claude/company-team.md' <<< "$injected_prompt" | |
| grep -q 'Literal target: ~/.claude/company-team.md' <<< "$injected_global_prompt" | |
| project_candidate="$(printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [marketing]' 'skills: [copywriting]' \ | |
| 'research: suggestions-only' '---' '' '# Active team' 'INJECTION_MARKER')" | |
| printf '%s\n' "$project_candidate" | (cd "$project" && "$workspace/bin/company" profile-save project) | |
| profile_context="$(cd "$project" && "$workspace/bin/company" profile-context)" | |
| [ "$profile_context" = "$(printf '%s\n' 'PROFILE_CONTEXT_V1' 'scope=project' 'departments=[marketing]' 'skills=[copywriting]')" ] | |
| ! grep -qE 'INJECTION_MARKER|suggestions-only|company-team|onboarding profiles' <<< "$profile_context" | |
| global_candidate="$(printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: global' \ | |
| 'departments: [developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' '' '# Active team' 'GLOBAL_PROFILE_MARKER')" | |
| mkdir -p "$global_home" | |
| printf '%s\n' "$global_candidate" | (cd "$test_root" && HOME="$global_home" CLAUDE_INC_GLOBAL_PROFILE=$'ignored\nSAVE_PATH_INJECTION' "$workspace/bin/company" profile-save global) | |
| [ -f "$global_profile" ] | |
| [ ! -e "$test_root/ignored" ] | |
| global_brief="$(cd "$project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" "$workspace/bin/company" brief 'global fallback' --print)" | |
| grep -q '^Scope: project$' <<< "$global_brief" | |
| project_brief="$(cd "$project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" "$workspace/bin/company" brief 'project override' --print)" | |
| grep -q '^Scope: project$' <<< "$project_brief" | |
| grep -q '^Departments: \[marketing\]$' <<< "$project_brief" | |
| grep -q '^Skills: \[copywriting\]$' <<< "$project_brief" | |
| grep -q 'explicit consent in the current session' <<< "$project_brief" | |
| ! grep -q 'suggestions-only' <<< "$project_brief" | |
| ! grep -q 'INJECTION_MARKER' <<< "$project_brief" | |
| ! grep -q 'GLOBAL_PROFILE_MARKER' <<< "$project_brief" | |
| team_output="$(cd "$project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" "$workspace/bin/company" team)" | |
| grep -q '^Skills: \[copywriting\]$' <<< "$team_output" | |
| grep -q '^Research metadata (does not grant consent): suggestions-only$' <<< "$team_output" | |
| ! grep -q 'INJECTION_MARKER' <<< "$team_output" | |
| ! grep -qE 'company-team|onboarding profiles' <<< "$team_output" | |
| global_project="$test_root/global project" | |
| mkdir -p "$global_project" | |
| global_brief="$(cd "$global_project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" "$workspace/bin/company" brief 'global fallback' --print)" | |
| grep -q '^Scope: global$' <<< "$global_brief" | |
| grep -q '^Departments: \[developers\]$' <<< "$global_brief" | |
| ! grep -q 'GLOBAL_PROFILE_MARKER' <<< "$global_brief" | |
| symlink_project="$test_root/symlink project" | |
| symlink_target="$test_root/symlink target.md" | |
| mkdir -p "$symlink_project/.claude" | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'SYMLINK_BODY_MUST_NOT_LEAK' > "$symlink_target" | |
| ln -s "$symlink_target" "$symlink_project/.claude/company-team.md" | |
| if (cd "$symlink_project" && CLAUDE_INC_GLOBAL_PROFILE="$test_root/missing-global.md" \ | |
| "$workspace/bin/company" brief 'symlink profile' --print > "$test_root/symlink.out" 2> "$test_root/symlink.err"); then | |
| echo 'symlink profile unexpectedly accepted' >&2 | |
| exit 1 | |
| fi | |
| grep -q 'symbolic links are not allowed' "$test_root/symlink.err" | |
| ! grep -q 'SYMLINK_BODY_MUST_NOT_LEAK' "$test_root/symlink.out" | |
| ! grep -Fq "$symlink_project" "$test_root/symlink.err" | |
| check_invalid() { | |
| expected="$1" | |
| output_file="$test_root/$expected.out" | |
| error_file="$test_root/$expected.err" | |
| if (cd "$project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" \ | |
| "$workspace/bin/company" brief 'invalid profile' --print > "$output_file" 2> "$error_file"); then | |
| echo "$expected unexpectedly accepted" >&2 | |
| return 1 | |
| fi | |
| grep -q 'company: invalid project team profile:' "$error_file" | |
| ! grep -Fq "$project" "$error_file" | |
| ! grep -q 'INVALID_PROFILE_BODY\|GLOBAL_PROFILE_MARKER' "$output_file" | |
| ! grep -q 'ACTIVE TEAM PREFERENCES' "$output_file" | |
| } | |
| mkdir -p "$workspace/skills/evil" | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [evil]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid evil-local-directory | |
| rmdir "$workspace/skills/evil" | |
| printf '%s\n' 'not frontmatter' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid malformed | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: global' \ | |
| 'departments: [developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid wrong-scope | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [unknown-team]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid unknown-department | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [unknown-skill]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid unknown-skill | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| head -c 33000 /dev/zero | tr '\0' x >> "$project/.claude/company-team.md" | |
| check_invalid oversized | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| printf '\0' >> "$project/.claude/company-team.md" | |
| check_invalid nul-under-limit | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers, developers]' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid duplicate-department | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'departments: [developers]' \ | |
| 'scope: project' 'skills: [superpowers]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid reordered-frontmatter | |
| printf '%s\n' \ | |
| '---' 'schema: 1' 'scope: project' \ | |
| 'departments: [developers]' 'skills: [copywriting]' \ | |
| 'research: disabled' '---' 'INVALID_PROFILE_BODY' > "$project/.claude/company-team.md" | |
| check_invalid mismatched-skill-department | |
| invalid_project_error="$test_root/invalid-project-no-fallback.err" | |
| if (cd "$project" && CLAUDE_INC_GLOBAL_PROFILE="$global_profile" "$workspace/bin/company" profile-context > /dev/null 2> "$invalid_project_error"); then | |
| echo 'invalid-project-no-fallback unexpectedly succeeded' >&2 | |
| exit 1 | |
| fi | |
| grep -q 'invalid project team profile' "$invalid_project_error" | |
| ! grep -Fq "$project" "$invalid_project_error" | |
| exact_project="$test_root/exact-32768" | |
| mkdir -p "$exact_project" | |
| exact_prefix="$(printf '%s\n' '---' 'schema: 1' 'scope: project' 'departments: [developers]' 'skills: [superpowers]' 'research: disabled' '---')" | |
| exact_prefix="$exact_prefix"$'\n' | |
| exact_padding=$((32768 - ${#exact_prefix})) | |
| { printf '%s' "$exact_prefix"; head -c "$exact_padding" /dev/zero | tr '\0' x; } | (cd "$exact_project" && "$workspace/bin/company" profile-save project) | |
| [ "$(wc -c < "$exact_project/.claude/company-team.md" | tr -d '[:space:]')" -eq 32768 ] | |
| nul_project="$test_root/nul-under-limit" | |
| mkdir -p "$nul_project" | |
| if { printf '%s\n' '---' 'schema: 1' 'scope: project' 'departments: [developers]' 'skills: [superpowers]' 'research: disabled' '---'; printf '\0'; } | (cd "$nul_project" && "$workspace/bin/company" profile-save project); then | |
| echo 'nul-under-limit unexpectedly saved' >&2 | |
| exit 1 | |
| fi | |
| [ ! -e "$nul_project/.claude/company-team.md" ] | |
| save_rejects() { | |
| name="$1" | |
| candidate="$2" | |
| save_project="$test_root/$name" | |
| mkdir -p "$save_project" | |
| if printf '%s\n' "$candidate" | (cd "$save_project" && "$workspace/bin/company" profile-save project); then | |
| echo "$name unexpectedly saved" >&2 | |
| return 1 | |
| fi | |
| [ ! -e "$save_project/.claude/company-team.md" ] | |
| } | |
| save_rejects duplicate-skill "$(printf '%s\n' '---' 'schema: 1' 'scope: project' 'departments: [developers]' 'skills: [superpowers, superpowers]' 'research: disabled' '---')" | |
| save_rejects reordered-save "$(printf '%s\n' '---' 'scope: project' 'schema: 1' 'departments: [developers]' 'skills: [superpowers]' 'research: disabled' '---')" | |
| symlink_save="$test_root/symlink-target" | |
| mkdir -p "$symlink_save/.claude" | |
| ln -s "$test_root/symlink target.md" "$symlink_save/.claude/company-team.md" | |
| if printf '%s\n' "$project_candidate" | (cd "$symlink_save" && "$workspace/bin/company" profile-save project); then exit 1; fi | |
| symlink_parent="$test_root/symlink-parent" | |
| mkdir -p "$symlink_parent" "$test_root/real profile parent" | |
| ln -s "$test_root/real profile parent" "$symlink_parent/.claude" | |
| if printf '%s\n' "$project_candidate" | (cd "$symlink_parent" && "$workspace/bin/company" profile-save project); then exit 1; fi | |
| special_project="$test_root/special-target" | |
| mkdir -p "$special_project/.claude" | |
| mkfifo "$special_project/.claude/company-team.md" | |
| if printf '%s\n' "$project_candidate" | (cd "$special_project" && "$workspace/bin/company" profile-save project); then exit 1; fi | |
| replace_project="$test_root/replace-confirmed" | |
| mkdir -p "$replace_project" | |
| printf '%s\n' "$project_candidate" | (cd "$replace_project" && "$workspace/bin/company" profile-save project) | |
| if printf '%s\n' "$global_candidate" | (cd "$replace_project" && "$workspace/bin/company" profile-save project); then exit 1; fi | |
| grep -q 'scope: project' "$replace_project/.claude/company-team.md" | |
| replacement="$(printf '%s\n' '---' 'schema: 1' 'scope: project' 'departments: [developers]' 'skills: [superpowers]' 'research: disabled' '---' 'REPLACED')" | |
| printf '%s\n' "$replacement" | (cd "$replace_project" && "$workspace/bin/company" profile-save project --replace) | |
| grep -q 'REPLACED' "$replace_project/.claude/company-team.md" | |
| deferred="$(cd "$project" && CLAUDE_INC_ENGINE=missing-company-engine "$workspace/bin/company" onboard < /dev/null)" | |
| [ "$deferred" = 'Onboarding deferred. Run: company onboard' ] | |
| - name: The CLI can brief every department (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $departments = @('developers', 'designers', 'marketing', 'social-media', 'finance', 'small-business', 'legal', 'sales') | |
| foreach ($department in $departments) { | |
| $output = & bash ./bin/company $department 'ci smoke test' --print | |
| if ($LASTEXITCODE -ne 0) { throw "CLI failed for $department" } | |
| $count = @($output | Select-String '^----- EMPLOYEE:').Count | |
| Write-Host "$department -> $count employees" | |
| if ($count -ne 6) { throw "$department composed $count employees, expected 6" } | |
| } | |
| $brief = & bash ./bin/company brief 'ci smoke test' --print | |
| if ($LASTEXITCODE -ne 0) { throw 'CLI brief failed' } | |
| if (-not ($brief | Select-String 'CEO Operating Manual')) { throw 'CEO Operating Manual missing from CLI brief' } | |
| & bash ./bin/company roster | Out-Null | |
| if ($LASTEXITCODE -ne 0) { throw 'CLI roster failed' } | |
| $version = & bash ./bin/company version | |
| if ($LASTEXITCODE -ne 0 -or $version -ne 'company v1.5.1') { throw "Unexpected CLI version: $version" } | |
| $prompt = (& bash ./bin/company onboard --print) -join "`n" | |
| foreach ($marker in @('QUESTION 1/3', 'QUESTION 2/3', 'QUESTION 3/3', 'PROPOSAL')) { | |
| if ([regex]::Matches($prompt, [regex]::Escape($marker)).Count -ne 1) { throw "Unexpected onboarding marker count: $marker" } | |
| } | |
| if (-not $prompt.Contains('remote pages and code may be inspected read-only')) { throw 'Research inspection boundary missing' } | |
| if (-not $prompt.Contains('Stars alone never determine rank')) { throw 'Research quality gate missing' } | |
| if (-not $prompt.Contains('company profile-save project')) { throw 'Profile save helper missing from onboarding prompt' } | |
| $company = (Resolve-Path './bin/company').Path | |
| $testRoot = Join-Path $env:RUNNER_TEMP 'profile smoke with spaces' | |
| $project = Join-Path $testRoot 'project with spaces' | |
| $globalProject = Join-Path $testRoot 'global project with spaces' | |
| $globalHome = Join-Path $testRoot 'global home' | |
| $globalProfile = Join-Path $globalHome '.claude/company-team.md' | |
| New-Item -ItemType Directory -Force -Path (Join-Path $project '.claude'), $globalProject, $globalHome | Out-Null | |
| function Write-TeamProfile([string]$Path, [string]$Scope, [string]$Departments, [string]$Skills, [string]$Body, [string]$Research = 'disabled') { | |
| $content = @('---', 'schema: 1', "scope: $Scope", "departments: $Departments", "skills: $Skills", "research: $Research", '---', '', $Body) -join "`n" | |
| [IO.File]::WriteAllText($Path, $content, [Text.UTF8Encoding]::new($false)) | |
| } | |
| $globalCandidate = Join-Path $testRoot 'global candidate.md' | |
| Write-TeamProfile $globalCandidate 'global' '[developers]' '[superpowers]' 'WINDOWS_GLOBAL_BODY' | |
| $env:HOME = $globalHome | |
| $env:CLAUDE_INC_GLOBAL_PROFILE = $globalProfile | |
| & bash -l -c '"$1" profile-save global < "$2"' bash $company $globalCandidate | |
| if ($LASTEXITCODE -ne 0) { throw 'Windows global profile-save failed' } | |
| Push-Location $globalProject | |
| try { $globalBrief = (& bash -l $company brief 'global fallback' --print) -join "`n" } finally { Pop-Location } | |
| if (-not $globalBrief.Contains('Scope: global') -or -not $globalBrief.Contains('Skills: [superpowers]')) { throw 'Windows global fallback failed' } | |
| if ($globalBrief.Contains('WINDOWS_GLOBAL_BODY')) { throw 'Windows global body leaked' } | |
| $projectProfile = Join-Path $project '.claude/company-team.md' | |
| $projectCandidate = Join-Path $testRoot 'project candidate.md' | |
| Write-TeamProfile $projectCandidate 'project' '[marketing]' '[copywriting]' 'INJECTION_MARKER' 'suggestions-only' | |
| Push-Location $project | |
| try { | |
| & bash -l -c '"$1" profile-save project < "$2"' bash $company $projectCandidate | |
| if ($LASTEXITCODE -ne 0) { throw 'Windows project profile-save failed' } | |
| $projectBrief = (& bash -l $company brief 'project override' --print) -join "`n" | |
| $teamOutput = (& bash -l $company team) -join "`n" | |
| $profileContext = (& bash -l $company profile-context) -join "`n" | |
| } finally { Pop-Location } | |
| if (-not $projectBrief.Contains('Scope: project') -or -not $projectBrief.Contains('Skills: [copywriting]')) { throw 'Windows project profile failed' } | |
| if (-not $projectBrief.Contains('explicit consent in the current session') -or $projectBrief.Contains('suggestions-only')) { throw 'Stored research metadata granted or implied consent' } | |
| if (-not $teamOutput.Contains('Research metadata (does not grant consent): suggestions-only')) { throw 'Windows team metadata display failed' } | |
| if ($projectBrief.Contains('INJECTION_MARKER') -or $teamOutput.Contains('INJECTION_MARKER')) { throw 'Windows profile body leaked' } | |
| if ($projectBrief.Contains('WINDOWS_GLOBAL_BODY')) { throw 'Windows project did not override global' } | |
| if ($profileContext -ne "PROFILE_CONTEXT_V1`nscope=project`ndepartments=[marketing]`nskills=[copywriting]") { throw "Windows profile-context mismatch: $profileContext" } | |
| if ($profileContext.Contains('INJECTION_MARKER') -or $profileContext.Contains('suggestions-only') -or $profileContext.Contains('company-team')) { throw 'Windows profile-context leaked profile data' } | |
| if ($teamOutput.Contains($project) -or $teamOutput.Contains('company-team')) { throw 'Windows team output leaked a profile path' } | |
| $crlfContent = @('---','schema: 1','scope: project','departments: [developers]','skills: [superpowers]','research: disabled','---','CRLF_BODY') -join "`r`n" | |
| [IO.File]::WriteAllText($projectProfile, $crlfContent, [Text.UTF8Encoding]::new($false)) | |
| Push-Location $project | |
| try { $crlfBrief = (& bash -l $company brief 'crlf profile' --print) -join "`n" } finally { Pop-Location } | |
| if (-not $crlfBrief.Contains('Scope: project') -or -not $crlfBrief.Contains('Skills: [superpowers]')) { throw 'Windows CRLF profile failed' } | |
| if ($crlfBrief.Contains('CRLF_BODY')) { throw 'Windows CRLF body leaked' } | |
| $env:CLAUDE_INC_GLOBAL_PROFILE = Join-Path $testRoot 'missing global profile.md' | |
| $invalidCases = @( | |
| [pscustomobject]@{ Name = 'malformed'; Content = "not frontmatter`nINVALID_BODY" }, | |
| [pscustomobject]@{ Name = 'unknown-department'; Content = (@('---','schema: 1','scope: project','departments: [evil]','skills: [superpowers]','research: disabled','---','INVALID_BODY') -join "`n") }, | |
| [pscustomobject]@{ Name = 'unknown-skill'; Content = (@('---','schema: 1','scope: project','departments: [developers]','skills: [evil]','research: disabled','---','INVALID_BODY') -join "`n") }, | |
| [pscustomobject]@{ Name = 'duplicate-department'; Content = (@('---','schema: 1','scope: project','departments: [developers, developers]','skills: [superpowers]','research: disabled','---','INVALID_BODY') -join "`n") }, | |
| [pscustomobject]@{ Name = 'reordered-frontmatter'; Content = (@('---','scope: project','schema: 1','departments: [developers]','skills: [superpowers]','research: disabled','---','INVALID_BODY') -join "`n") }, | |
| [pscustomobject]@{ Name = 'oversized'; Content = ((@('---','schema: 1','scope: project','departments: [developers]','skills: [superpowers]','research: disabled','---','INVALID_BODY') -join "`n") + ('x' * 33000)) } | |
| ) | |
| foreach ($case in $invalidCases) { | |
| [IO.File]::WriteAllText($projectProfile, $case.Content, [Text.UTF8Encoding]::new($false)) | |
| Push-Location $project | |
| try { | |
| $invalidOutput = (& bash -l $company brief $case.Name --print 2>&1) -join "`n" | |
| $invalidExit = $LASTEXITCODE | |
| } finally { Pop-Location } | |
| if ($invalidExit -eq 0) { throw "Windows invalid profile was accepted: $($case.Name)" } | |
| if (-not $invalidOutput.Contains('invalid project team profile')) { throw "Windows invalid profile was not warned: $($case.Name)" } | |
| if ($invalidOutput.Contains('INVALID_BODY') -or $invalidOutput.Contains('ACTIVE TEAM PREFERENCES')) { throw "Windows invalid profile leaked: $($case.Name)" } | |
| if ($invalidOutput.Contains($project)) { throw "Windows invalid profile leaked its path: $($case.Name)" } | |
| } | |
| # invalid-project-no-fallback | |
| Write-TeamProfile $projectProfile 'project' '[developers]' '[copywriting]' 'INVALID_PROFILE_BODY' | |
| Push-Location $project | |
| try { | |
| $invalidFallback = (& bash -l $company profile-context 2>&1) -join "`n" | |
| $invalidFallbackExit = $LASTEXITCODE | |
| } finally { Pop-Location } | |
| if ($invalidFallbackExit -eq 0 -or -not $invalidFallback.Contains('invalid project team profile')) { throw 'Windows invalid project profile did not fail closed' } | |
| if ($invalidFallback.Contains('WINDOWS_GLOBAL_BODY') -or $invalidFallback.Contains($project)) { throw 'Windows invalid project profile leaked or fell back' } | |
| # exact-32768 | |
| $exactProject = Join-Path $testRoot 'exact-32768' | |
| $exactCandidate = Join-Path $testRoot 'exact-32768.md' | |
| New-Item -ItemType Directory -Force -Path $exactProject | Out-Null | |
| $exactPrefix = (@('---','schema: 1','scope: project','departments: [developers]','skills: [superpowers]','research: disabled','---') -join "`n") + "`n" | |
| $exactBytes = [Text.Encoding]::UTF8.GetBytes($exactPrefix) | |
| $exactContent = New-Object byte[] 32768 | |
| [Array]::Copy($exactBytes, $exactContent, $exactBytes.Length) | |
| for ($index = $exactBytes.Length; $index -lt 32768; $index++) { $exactContent[$index] = [byte][char]'x' } | |
| [IO.File]::WriteAllBytes($exactCandidate, $exactContent) | |
| Push-Location $exactProject | |
| try { & bash -l -c '"$1" profile-save project < "$2"' bash $company $exactCandidate; $exactExit = $LASTEXITCODE } finally { Pop-Location } | |
| if ($exactExit -ne 0 -or (Get-Item (Join-Path $exactProject '.claude/company-team.md')).Length -ne 32768) { throw 'Windows exact-32768 profile failed' } | |
| # nul-under-limit | |
| $nulProject = Join-Path $testRoot 'nul-under-limit' | |
| $nulCandidate = Join-Path $testRoot 'nul-under-limit.md' | |
| New-Item -ItemType Directory -Force -Path $nulProject | Out-Null | |
| [byte[]]$nulBytes = [Text.Encoding]::UTF8.GetBytes($exactPrefix + 'body') + [byte]0 | |
| [IO.File]::WriteAllBytes($nulCandidate, $nulBytes) | |
| Push-Location $nulProject | |
| try { & bash -l -c '"$1" profile-save project < "$2"' bash $company $nulCandidate; $nulExit = $LASTEXITCODE } finally { Pop-Location } | |
| if ($nulExit -eq 0 -or (Test-Path -LiteralPath (Join-Path $nulProject '.claude/company-team.md'))) { throw 'Windows nul-under-limit profile was accepted' } | |
| # replace-confirmed | |
| $replaceCandidate = Join-Path $testRoot 'replace candidate.md' | |
| Write-TeamProfile $replaceCandidate 'project' '[developers]' '[superpowers]' 'REPLACED' | |
| Push-Location $project | |
| try { | |
| & bash -l -c '"$1" profile-save project < "$2"' bash $company $replaceCandidate | |
| $replaceRefused = $LASTEXITCODE | |
| & bash -l -c '"$1" profile-save project --replace < "$2"' bash $company $replaceCandidate | |
| $replaceAccepted = $LASTEXITCODE | |
| } finally { Pop-Location } | |
| if ($replaceRefused -eq 0 -or $replaceAccepted -ne 0) { throw 'Windows replacement confirmation contract failed' } | |
| if (-not ([IO.File]::ReadAllText($projectProfile).Contains('REPLACED'))) { throw 'Windows confirmed replacement did not persist' } | |
| - name: Global, project and no-CLI installs work (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| workspace="$GITHUB_WORKSPACE" | |
| test_root="$RUNNER_TEMP/install smoke" | |
| export HOME="$test_root/global home" | |
| mkdir -p "$HOME" | |
| bash "$workspace/install.sh" | |
| bash "$workspace/install.sh" | |
| [ "$(ls -1 "$HOME/.claude/skills" | wc -l | tr -d ' ')" -eq 54 ] | |
| [ "$(ls -1 "$HOME/.claude/agents" | wc -l | tr -d ' ')" -eq 9 ] | |
| [ -L "$HOME/.local/bin/company" ] | |
| [ "$("$HOME/.local/bin/company" version)" = "company v1.5.1" ] | |
| export HOME="$test_root/project home" | |
| project="$test_root/project with spaces" | |
| mkdir -p "$HOME" "$project" | |
| (cd "$project" && bash "$workspace/install.sh" --project) | |
| [ "$(ls -1 "$project/.claude/skills" | wc -l | tr -d ' ')" -eq 54 ] | |
| [ "$(ls -1 "$project/.claude/agents" | wc -l | tr -d ' ')" -eq 9 ] | |
| export HOME="$test_root/no cli home" | |
| mkdir -p "$HOME" | |
| bash "$workspace/install.sh" --no-bin | |
| [ ! -e "$HOME/.local/bin/company" ] | |
| export HOME="$test_root/onboard home" | |
| onboard_project="$test_root/onboard project" | |
| mkdir -p "$HOME" "$onboard_project" | |
| onboard_output="$(cd "$onboard_project" && CLAUDE_INC_ENGINE=missing-company-engine bash "$workspace/install.sh" --project --no-bin --onboard)" | |
| grep -q '^Onboarding deferred. Install the Claude Code plugin, then run: /claude-inc:onboard$' <<< "$onboard_output" | |
| ! grep -q 'Run: company onboard' <<< "$onboard_output" | |
| [ ! -e "$onboard_project/.claude/company-team.md" ] | |
| - name: Piped project install clones the checked-out commit (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| workspace="$GITHUB_WORKSPACE" | |
| test_root="$(mktemp -d "$RUNNER_TEMP/claude-inc-piped.XXXXXX")" | |
| project="$test_root/project" | |
| mkdir -p "$project" | |
| ( | |
| cd "$project" | |
| export CLAUDE_INC_HOME="$test_root/cache" | |
| export CLAUDE_INC_REPO_URL="file://$workspace" | |
| cat "$workspace/install.sh" | bash -s -- --project --no-bin > "$test_root/install.out" | |
| ) | |
| ! grep -q 'QUESTION [123]/3' "$test_root/install.out" | |
| [ ! -e "$project/.claude/company-team.md" ] | |
| expected_commit="$(git -C "$workspace" rev-parse HEAD)" | |
| checkout_count=0 | |
| promoted_checkout="" | |
| for checkout in "$test_root/cache.checkouts"/*; do | |
| [ -d "$checkout" ] || continue | |
| checkout_count=$((checkout_count + 1)) | |
| promoted_checkout="$checkout" | |
| done | |
| [ "$checkout_count" -eq 1 ] | |
| [ "${promoted_checkout##*/}" = "$expected_commit" ] | |
| [ "$(git -C "$promoted_checkout" rev-parse HEAD)" = "$expected_commit" ] | |
| [ -z "$(git -C "$promoted_checkout" status --porcelain --untracked-files=all)" ] | |
| ! find "$test_root" -maxdepth 1 -name 'cache.candidate.*' -print -quit | grep -q . | |
| [ -f "$project/.claude/commands/company.md" ] | |
| cmp -s "$workspace/commands/company.md" "$project/.claude/commands/company.md" | |
| skill_count=0 | |
| for skill in "$project/.claude/skills"/*/; do | |
| [ -d "$skill" ] || continue | |
| skill_count=$((skill_count + 1)) | |
| done | |
| agent_count=0 | |
| for agent in "$project/.claude/agents"/*.md; do | |
| [ -f "$agent" ] || continue | |
| agent_count=$((agent_count + 1)) | |
| done | |
| [ "$skill_count" -eq 54 ] | |
| [ "$agent_count" -eq 9 ] | |
| - name: Global, project and no-CLI installs work (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $workspace = $env:GITHUB_WORKSPACE | |
| $testRoot = Join-Path $env:RUNNER_TEMP 'install smoke' | |
| $env:HOME = Join-Path $testRoot 'global home' | |
| New-Item -ItemType Directory -Force -Path $env:HOME | Out-Null | |
| & "$workspace/install.ps1" | |
| & "$workspace/install.ps1" | |
| if ((Get-ChildItem "$env:HOME/.claude/skills" -Directory).Count -ne 54) { throw 'Global skill count is not 54' } | |
| if ((Get-ChildItem "$env:HOME/.claude/agents" -File).Count -ne 9) { throw 'Global agent count is not 9' } | |
| $wrapper = Join-Path $env:HOME '.local/bin/company.exe' | |
| if (-not (Test-Path -LiteralPath $wrapper)) { throw 'CLI wrapper was not installed' } | |
| $version = & $wrapper version | |
| if ($LASTEXITCODE -ne 0 -or $version -ne 'company v1.5.1') { throw "Unexpected installed CLI version: $version" } | |
| $wrapperProject = Join-Path $testRoot 'wrapper profile project with spaces' | |
| $wrapperProfileDir = Join-Path $wrapperProject '.claude' | |
| New-Item -ItemType Directory -Force -Path $wrapperProfileDir | Out-Null | |
| $wrapperProfile = Join-Path $wrapperProfileDir 'company-team.md' | |
| $wrapperContent = @('---','schema: 1','scope: project','departments: [developers]','skills: [superpowers]','research: disabled','---','WRAPPER_INJECTION_MARKER') -join "`n" | |
| [IO.File]::WriteAllText($wrapperProfile, $wrapperContent, [Text.UTF8Encoding]::new($false)) | |
| Push-Location $wrapperProject | |
| try { $wrapperBrief = (& $wrapper brief 'wrapper profile' --print) -join "`n" } finally { Pop-Location } | |
| if (-not $wrapperBrief.Contains('Scope: project') -or -not $wrapperBrief.Contains('Skills: [superpowers]')) { throw 'Windows wrapper profile failed' } | |
| if ($wrapperBrief.Contains('WRAPPER_INJECTION_MARKER')) { throw 'Windows wrapper leaked profile body' } | |
| $env:HOME = Join-Path $testRoot 'project home' | |
| $project = Join-Path $testRoot 'project with spaces' | |
| New-Item -ItemType Directory -Force -Path $env:HOME, $project | Out-Null | |
| Push-Location $project | |
| try { & "$workspace/install.ps1" -Project } finally { Pop-Location } | |
| if ((Get-ChildItem "$project/.claude/skills" -Directory).Count -ne 54) { throw 'Project skill count is not 54' } | |
| if ((Get-ChildItem "$project/.claude/agents" -File).Count -ne 9) { throw 'Project agent count is not 9' } | |
| $env:HOME = Join-Path $testRoot 'no cli home' | |
| New-Item -ItemType Directory -Force -Path $env:HOME | Out-Null | |
| & "$workspace/install.ps1" -NoBin | |
| if (Test-Path -LiteralPath "$env:HOME/.local/bin/company.exe") { throw 'NoBin installed a CLI wrapper' } | |
| $env:HOME = Join-Path $testRoot 'onboard home' | |
| $env:CLAUDE_INC_ENGINE = 'missing-company-engine' | |
| $onboardProject = Join-Path $testRoot 'onboard project' | |
| New-Item -ItemType Directory -Force -Path $env:HOME, $onboardProject | Out-Null | |
| Push-Location $onboardProject | |
| try { $onboardOutput = & "$workspace/install.ps1" -Project -NoBin -Onboard | Out-String } finally { Pop-Location } | |
| if (-not $onboardOutput.Contains('Onboarding deferred.')) { throw 'Project onboarding did not defer safely' } | |
| if ($onboardOutput.Contains('Run: company onboard')) { throw 'NoBin advised an unavailable company command' } | |
| if (-not $onboardOutput.Contains('Install the Claude Code plugin, then run: /claude-inc:onboard')) { throw 'NoBin deferred guidance is not actionable' } | |
| if (Test-Path -LiteralPath (Join-Path $onboardProject '.claude/company-team.md')) { throw 'Deferred onboarding created a profile' } | |
| - name: Real installed company project lifecycle (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| run: python3 tests/project_install_smoke.py | |
| - name: Real installed company.exe project lifecycle (Windows) | |
| if: runner.os == 'Windows' | |
| run: python tests/project_install_smoke.py --quote-probe | |
| - name: Native launcher literal arguments and process contract (Windows) | |
| if: runner.os == 'Windows' | |
| run: python -m unittest discover -s tests -p test_windows_launcher.py | |
| - name: Canonical company examples preserve fenced Markdown | |
| run: python -m unittest discover -s tests -p test_company_builder.py | |
| - name: Installer collisions fail without mutation (macOS and Linux) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: bash tests/installers_safety.sh | |
| - name: Installer collisions fail without mutation (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: ./tests/installers_safety.ps1 |