From 804e2f0d7394fd5218a301513b2d385408988c8e Mon Sep 17 00:00:00 2001 From: Curry Date: Sat, 25 Jul 2026 02:56:46 +0800 Subject: [PATCH] feat: make compiled CLI the primary entry --- .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 1 + CONTEXT.md | 6 + Cargo.lock | 2 +- Find-CodeIntelProjects.ps1 | 2 +- README.md | 78 ++-- bootstrap-new-machine.ps1 | 2 +- code-intel.ps1 | 242 ++++++++++ crates/code-intel-cli/Cargo.toml | 2 +- crates/code-intel-cli/src/artifact_index.rs | 2 +- crates/code-intel-cli/src/artifacts.rs | 2 +- crates/code-intel-cli/src/capability.rs | 21 +- crates/code-intel-cli/src/dag_run.rs | 14 +- crates/code-intel-cli/src/doctor_adapter.rs | 4 +- crates/code-intel-cli/src/main.rs | 380 ++++++++++++++-- crates/code-intel-cli/src/orchestration.rs | 8 +- crates/code-intel-cli/tests/primary_entry.rs | 73 +++ .../0011-primary-cli-and-recovery-launcher.md | 34 ++ docs/artifact-data-contract.md | 6 +- docs/code-intel-architecture.md | 40 +- docs/follow-up-automation.md | 11 +- docs/project-management-support.md | 2 +- docs/public-beta.md | 28 +- docs/repository-layout.md | 15 +- install-code-intel-pipeline.ps1 | 3 +- invoke-code-intel.ps1 | 424 +++--------------- orchestration/integrations.json | 17 +- orchestration/internalization/linear.json | 2 +- orchestration/internalization/llm-wiki.json | 2 +- orchestration/internalization/obsidian.json | 2 +- scripts/tests/test-primary-launchers.ps1 | 99 ++++ scripts/tests/test-stable-wrapper-e2e.ps1 | 53 +-- skills/code-intel-pipeline/SKILL.md | 33 +- .../code-intel-pipeline/scripts/bootstrap.py | 84 ++-- tests/test_repository_layout.py | 1 + tests/test_skill_package.py | 41 +- tools/Test-BetaPackage.ps1 | 14 +- 37 files changed, 1123 insertions(+), 631 deletions(-) create mode 100644 code-intel.ps1 create mode 100644 crates/code-intel-cli/tests/primary_entry.rs create mode 100644 docs/adr/0011-primary-cli-and-recovery-launcher.md create mode 100644 scripts/tests/test-primary-launchers.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e0469f..053f45a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: shell: pwsh run: | $files = @( + "code-intel.ps1", "invoke-code-intel.ps1", "run-code-intel.ps1", "Invoke-ScopedRepowise.ps1", @@ -69,6 +70,7 @@ jobs: "Invoke-GitHubSolutionResearch.ps1", "scripts/tests/test-code-intel-pipeline.ps1", "scripts/tests/test-stable-wrapper-e2e.ps1", + "scripts/tests/test-primary-launchers.ps1", "scripts/tests/test-github-solution-research.ps1", "scripts/tests/test-hospital-trust-contract.ps1", "scripts/tests/test-atomic-capability-contract.ps1", @@ -274,6 +276,7 @@ jobs: shell: pwsh run: | $files = @( + "code-intel.ps1", "invoke-code-intel.ps1", "run-code-intel.ps1", "Invoke-ScopedRepowise.ps1", @@ -281,6 +284,7 @@ jobs: "Invoke-GitHubSolutionResearch.ps1", "scripts/tests/test-code-intel-pipeline.ps1", "scripts/tests/test-stable-wrapper-e2e.ps1", + "scripts/tests/test-primary-launchers.ps1", "scripts/tests/test-github-solution-research.ps1", "scripts/tests/test-hospital-trust-contract.ps1", "scripts/tests/test-atomic-capability-contract.ps1", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46ee16a..dad98e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,6 +132,7 @@ jobs: $payload = Join-Path $expanded "code-intel-pipeline" $required = @( (Join-Path $payload "install-code-intel-pipeline.ps1"), + (Join-Path $payload "code-intel.ps1"), (Join-Path $payload "skills\code-intel-pipeline\SKILL.md"), (Join-Path $payload "skills\code-intel-pipeline\agents\openai.yaml"), (Join-Path $payload "skills\code-intel-pipeline\scripts\bootstrap.py"), diff --git a/CONTEXT.md b/CONTEXT.md index 4da87d5..80689d3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,6 +7,12 @@ Code Intel Pipeline is an independent engineering-intelligence domain. It turns **Code Intel Pipeline**: The independent engineering-intelligence system that turns a Target Repository and its delivery evidence into deterministic engineering facts, derived views, diagnoses, and plans. OpenCLI Admin and other systems are consumers, not owners or internal modules of this domain. _Avoid_: OpenCLI Admin feature, analyzer, crawler, generic scanner +**Primary Operator Entry**: The canonical command surface through which humans and Agents request pipeline runs and related operations. It owns the user-visible command contract and is the only entry presented as the default. +_Avoid_: Preferred wrapper, one of several equivalent runners + +**Recovery Launcher**: A compatibility surface that restores access to the Primary Operator Entry by locating, validating, installing, repairing, or starting it. It never owns or independently implements Pipeline semantics. +_Avoid_: Redundant Pipeline, alternate scanner, second implementation + **Agent Goal Intake**: The pre-scan task-contract layer that turns vague work into a bounded goal, verification evidence, constraints, iteration policy, stop conditions, and pause conditions. _Avoid_: Scanner, prompt template, backlog item diff --git a/Cargo.lock b/Cargo.lock index 62f72ae..43234bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "code-intel" -version = "0.4.0" +version = "0.5.0" dependencies = [ "serde_json", ] diff --git a/Find-CodeIntelProjects.ps1 b/Find-CodeIntelProjects.ps1 index 317ef3e..e24c566 100644 --- a/Find-CodeIntelProjects.ps1 +++ b/Find-CodeIntelProjects.ps1 @@ -240,7 +240,7 @@ $result = @( sizeBytes = [long]$candidate.sizeBytes lastWriteTime = if ($null -ne $candidate.lastWriteTime) { ([datetime]$candidate.lastWriteTime).ToString("o") } else { "" } source = [string]$candidate.source - recommendedCommand = ".\invoke-code-intel.ps1 -RepoPath `"$($candidate.path)`" -Mode normal" + recommendedCommand = "code-intel `"$($candidate.path)`"" } } ) | Sort-Object score, sizeBytes, path -Descending diff --git a/README.md b/README.md index 8668cee..659801b 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,31 @@ Follow-up automation can proactively propose `/investigate` for actionable scan --- +## 30 秒开始 + +在要分析的仓库目录中运行稳定入口;不传参数时默认分析当前目录: + +```powershell +code-intel . +``` + +或显式指定仓库: + +```powershell +code-intel C:\path\to\your\repo +``` + +首次安装、Skill 安装和依赖说明见[完整上手](#安装与完整上手)。 + ## 仓库入口 -这个仓库的根目录暂时保留 PowerShell 入口,只为兼容已发布包和团队脚本;它们处于退休路径,不接受新产品逻辑。新实现优先进入 Rust core,MoonBit 仅用于隔离验证。真正的治理边界见 [Repository Layout](docs/repository-layout.md)。 +编译后的 `code-intel` 是唯一正式入口。PowerShell 只保留安装、恢复和旧命令转发,不实现 Pipeline 语义。真正的治理边界见 [Repository Layout](docs/repository-layout.md)。 公共入口: -- `invoke-code-intel.ps1`: 推荐人工入口,先 doctor 再运行 pipeline。 +- `code-intel`: 人工和 Agent 的正式主入口。 +- `code-intel.ps1`: PowerShell 7.2+ 恢复启动器;健康安装只转发,`-Update` 才显式更新。 +- `invoke-code-intel.ps1`: v0.x 兼容转发器,不再推荐新调用。 - `run-code-intel.ps1`: 兼容 facade;默认 normal 路径调用 Rust DAG、原子提交和 committed-only 索引,旧扫描器分支必须显式启用。 - `check-code-intel-tools.ps1`: 环境 doctor。 - `install-code-intel-pipeline.ps1`: 安装和修复入口。 @@ -45,7 +63,7 @@ PowerShell 合同测试已迁到 `scripts/tests/`。其余根目录兼容 facade 0.4.0 核心路径: -- `invoke-code-intel.ps1` / `run-code-intel.ps1` +- `code-intel` Primary Operator Entry - `code-intel.exe` 的 A01-A09 capability/DAG/policy/artifact core - `rg` inventory、native code evidence、内部 graph provider、真实 Sentrux `gate`/`check` 命令证据和 Hospital diagnosis - snapshot-bound staging、A07 原子提交、A08 completed-only 索引、query/impact/freshness @@ -106,7 +124,7 @@ GPT 娘坐在空白处,不紧不慢。她把文件列成星图,把依赖连 这套系统的边界很清楚:它负责看清楚、量出来、拦退化、给下一步方向。真正修改代码,还是人和 Agent 一起做。 -## 一分钟上手 +## 安装与完整上手 Codex 可以先安装官方结构的 Skill 包,再由 Skill 下载并校验稳定版 Release: @@ -119,20 +137,20 @@ https://github.com/2233admin/code-intel-pipeline/tree/main/skills/code-intel-pip Skill 默认只解析稳定版,校验 GitHub Release 提供的 SHA-256 后才解压。预发布版本和第三方依赖安装都需要显式选择。 -Windows PowerShell: +人工用户从 GitHub Release 下载并解压安装包;Agent 用户通过 Skill 安装。源码安装仍可使用: ```powershell git clone https://github.com/2233admin/code-intel-pipeline.git cd code-intel-pipeline .\install-code-intel-pipeline.ps1 -RepoPath C:\path\to\your\repo -RepairSkillLinks -InstallMissing -.\check-code-intel-tools.ps1 -RepoPath C:\path\to\your\repo -.\run-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal +code-intel C:\path\to\your\repo ``` -稳定入口: +PowerShell 恢复入口: ```powershell -.\invoke-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal +.\code-intel.ps1 C:\path\to\your\repo +.\code-intel.ps1 -Update ``` 先找候选项目: @@ -143,7 +161,7 @@ cd code-intel-pipeline .\Find-CodeIntelProjects.ps1 -WizTreeCsv C:\tmp\wiztree.csv -Json ``` -WizTree CLI/CSV 只是项目发现加速输入;真正选中项目后再运行 `invoke-code-intel.ps1`。 +WizTree CLI/CSV 只是项目发现加速输入;真正选中项目后再运行 `code-intel `。 完整 smoke test: @@ -175,16 +193,16 @@ Project management support contract 测试: .\scripts/tests/test-project-management-support.ps1 -RepoPath C:\path\to\your\repo ``` -从 GitHub Release ZIP 运行时,解压后直接使用稳定入口;不需要 Cargo,也不依赖仓库里的 `target/`: +从 GitHub Release ZIP 运行时,安装后直接使用编译入口;不需要 Cargo,也不依赖仓库里的 `target/`: ```powershell -.\invoke-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal +code-intel C:\path\to\your\repo ``` -如果不需要语义索引,或本机没有 Repowise: +只使用离线核心能力: ```powershell -.\invoke-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal -SkipRepowise +code-intel C:\path\to\your\repo --mode lite ``` Greenfield 行为规格适配器测试: @@ -193,10 +211,10 @@ Greenfield 行为规格适配器测试: .\scripts/tests/test-greenfield-integration.ps1 ``` -大仓库建议指定核心范围: +普通用户直接运行主入口;兼容 runner 只保留给维护测试: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal -SentruxPath backend +code-intel C:\path\to\your\repo --mode normal ``` ## 新机器部署 @@ -527,23 +545,19 @@ sentrux plugin list ## Repowise 语义记忆 -Repowise 是硬依赖语义记忆层。默认单步超时 `180` 秒;超时会作为 Repowise 失败写进报告,不再静默跳过: +Repowise 是可选语义记忆层。`normal` 在可用时使用,`lite` 不依赖它,`full` 才要求所有可选 provider 就绪: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -RepowiseTimeoutSeconds 60 +code-intel C:\path\to\repo --mode normal ``` -如果指定了 `-SentruxPath backend`,Repowise 会默认跟随同一 scope,避免把根目录里的外部轮子、临时文件、研究仓库一起吃进去。 +要缩小 scope,直接把目标路径指向相应子目录,避免把根目录里的外部轮子、临时文件、研究仓库一起吃进去。 ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -SentruxPath backend +code-intel C:\path\to\repo\backend --mode normal ``` -如果想生成 Repowise wiki 文档: - -```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -RepowiseDocs -``` +Repowise wiki 文档属于兼容适配器维护能力,不是正式主入口参数。 如果 provider 限流,报告会显示 `provider_quota`。这不是本地脚本坏了。 @@ -660,7 +674,7 @@ reason = "App 不应依赖 core 内部实现" 保存 baseline: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -SentruxPath backend -SaveSentruxBaseline +sentrux gate --save C:\path\to\repo\backend ``` 不要用新 baseline 掩盖真实退化。 @@ -685,13 +699,13 @@ tools 如果你要治理核心模块,直接指定 scope: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -SentruxPath backend -Mode normal +code-intel C:\path\to\repo\backend --mode normal ``` 如果你要分析 `tools/` 里的某个外部轮子,把 scope 指到那个轮子,而不是让它污染主项目: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo\tools\some-lib -Mode normal +code-intel C:\path\to\repo\tools\some-lib --mode normal ``` ## 真实跑通过的路径 @@ -766,14 +780,10 @@ CI 使用 Sentrux lite core 保底,所以 runner 没装真实 `sentrux` 时也 先 scoped: ```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -SentruxPath backend -Mode normal +code-intel C:\path\to\repo\backend --mode normal ``` -再缩短超时: - -```powershell -.\run-code-intel.ps1 -RepoPath C:\path\to\repo -SentruxPath backend -Mode normal -RepowiseTimeoutSeconds 60 -``` +仍然过慢时切到 `--mode lite`;provider 超时只在兼容适配器配置中维护。 ### 报告显示 `surgery_plan` diff --git a/bootstrap-new-machine.ps1 b/bootstrap-new-machine.ps1 index 8383011..3b42011 100644 --- a/bootstrap-new-machine.ps1 +++ b/bootstrap-new-machine.ps1 @@ -117,7 +117,7 @@ $result = [ordered]@{ markdown = $mdPath } nextAction = if ($ok) { - "Run invoke-code-intel.ps1 -RepoPath $repo -Mode $Mode for normal use." + "Run code-intel `"$repo`" --mode $Mode for normal use." } elseif (-not [bool]$installResult.ok) { "Fix install.missingRequired first." diff --git a/code-intel.ps1 b/code-intel.ps1 new file mode 100644 index 0000000..0816b84 --- /dev/null +++ b/code-intel.ps1 @@ -0,0 +1,242 @@ +#requires -Version 7.2 + +[CmdletBinding(PositionalBinding = $false)] +param( + [Parameter(Position = 0)] + [string]$RepoPath = "", + + [ValidateSet("lite", "normal", "full")] + [string]$Mode = "normal", + + [switch]$Update, + [switch]$Json, + + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Remaining = @() +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false + +function Get-CodeIntelDataRoot { + if ($IsWindows -and -not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { + return (Join-Path $env:LOCALAPPDATA "code-intel") + } + if ($IsMacOS) { + return (Join-Path ([Environment]::GetFolderPath("UserProfile")) "Library/Application Support/code-intel") + } + $base = if ($env:XDG_DATA_HOME) { + $env:XDG_DATA_HOME + } else { + Join-Path ([Environment]::GetFolderPath("UserProfile")) ".local/share" + } + return (Join-Path $base "code-intel") +} + +function Get-CanonicalManifestJson { + param([Parameter(Mandatory)]$Files) + + $names = [string[]]@($Files.PSObject.Properties.Name) + [Array]::Sort($names, [StringComparer]::Ordinal) + $parts = foreach ($name in $names) { + $entry = $Files.PSObject.Properties[$name].Value + $sha256 = [string]$entry.sha256 + $size = [long]$entry.size + if ($sha256 -cnotmatch "^[0-9a-f]{64}$" -or $size -lt 0) { + throw "invalid release manifest entry: $name" + } + $encodedName = [System.Text.Json.JsonEncodedText]::Encode([string]$name).ToString() + $encodedName = "`"$encodedName`"" + "$encodedName`:{`"sha256`":`"$sha256`",`"size`":$size}" + } + return "{$($parts -join ',')}" +} + +function Get-VerifiedReleaseBinary { + param([Parameter(Mandatory)][string]$Root) + + try { + $rootPath = [System.IO.Path]::GetFullPath($Root) + $markerPath = Join-Path $rootPath ".code-intel-release.json" + if (-not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { return $null } + $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json -Depth 100 + if ([string]$marker.schema -cne "code-intel-skill-release.v2" -or + [string]$marker.manifest_sha256 -cnotmatch "^[0-9a-f]{64}$" -or + $null -eq $marker.files) { + return $null + } + + $canonical = Get-CanonicalManifestJson -Files $marker.files + $digestBytes = [System.Security.Cryptography.SHA256]::HashData( + [System.Text.Encoding]::UTF8.GetBytes($canonical) + ) + $manifestDigest = [Convert]::ToHexString($digestBytes).ToLowerInvariant() + if ($manifestDigest -cne [string]$marker.manifest_sha256) { return $null } + + $expected = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + $rootPrefix = $rootPath.TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar + ) + [System.IO.Path]::DirectorySeparatorChar + foreach ($property in @($marker.files.PSObject.Properties)) { + $relative = [string]$property.Name + if ([string]::IsNullOrWhiteSpace($relative) -or + $relative.Contains("\") -or + [System.IO.Path]::IsPathRooted($relative) -or + @($relative.Split("/")) -contains "..") { + return $null + } + $target = [System.IO.Path]::GetFullPath( + (Join-Path $rootPath $relative.Replace("/", [System.IO.Path]::DirectorySeparatorChar)) + ) + if (-not $target.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase) -or + -not (Test-Path -LiteralPath $target -PathType Leaf)) { + return $null + } + $file = Get-Item -LiteralPath $target + if ($file.Length -ne [long]$property.Value.size) { return $null } + $actualHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -cne [string]$property.Value.sha256) { return $null } + $null = $expected.Add($relative) + } + + $actual = @(Get-ChildItem -LiteralPath $rootPath -File -Recurse | ForEach-Object { + [System.IO.Path]::GetRelativePath($rootPath, $_.FullName).Replace( + [System.IO.Path]::DirectorySeparatorChar, + "/" + ) + } | Where-Object { $_ -cne ".code-intel-release.json" }) + if ($actual.Count -ne $expected.Count -or + @($actual | Where-Object { -not $expected.Contains($_) }).Count -ne 0) { + return $null + } + + $binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } + $binaryRelative = "bin/$binaryName" + if (-not $expected.Contains($binaryRelative)) { return $null } + return (Join-Path $rootPath $binaryRelative) + } + catch { + if ($env:CODE_INTEL_DEBUG -eq "1") { + [Console]::Error.WriteLine("Code Intel release validation failed for ${Root}: $($_.Exception.Message)") + } + return $null + } +} + +function Test-CodeIntelBinary { + param([Parameter(Mandatory)][string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + try { + & $Path --help *> $null + return $LASTEXITCODE -eq 0 + } + catch { + return $false + } +} + +function Get-VerifiedReleaseRoots { + $roots = [System.Collections.Generic.List[string]]::new() + $roots.Add($PSScriptRoot) + $releases = Join-Path (Get-CodeIntelDataRoot) "releases" + if (Test-Path -LiteralPath $releases -PathType Container) { + $versioned = foreach ($directory in @(Get-ChildItem -LiteralPath $releases -Directory)) { + try { + [pscustomobject]@{ + Path = $directory.FullName + Version = [version]$directory.Name.TrimStart("v") + } + } + catch { + continue + } + } + foreach ($release in @($versioned | Sort-Object Version -Descending)) { + $roots.Add($release.Path) + } + } + return $roots | Select-Object -Unique +} + +function Get-DevelopmentCandidates { + # Explicit source-tree escape hatch; these are never recovery-trusted candidates. + if ($env:CODE_INTEL_ALLOW_UNVERIFIED_DEV -ne "1") { return @() } + $binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } + $candidates = [System.Collections.Generic.List[string]]::new() + foreach ($directory in @( + $env:CODE_INTEL_BIN, + (Join-Path $PSScriptRoot "bin"), + (Join-Path $PSScriptRoot "target/release"), + (Join-Path $PSScriptRoot "target/debug") + )) { + if (-not [string]::IsNullOrWhiteSpace($directory)) { + $candidates.Add((Join-Path $directory $binaryName)) + } + } + $command = Get-Command code-intel -CommandType Application -ErrorAction SilentlyContinue + if ($null -ne $command) { $candidates.Add($command.Source) } + return $candidates | Select-Object -Unique +} + +function Repair-CodeIntel { + param([Parameter(Mandatory)][string]$Repository) + + $python = Get-Command python, python3 -ErrorAction SilentlyContinue | Select-Object -First 1 + $bootstrap = Join-Path $PSScriptRoot "skills/code-intel-pipeline/scripts/bootstrap.py" + if ($null -eq $python -or -not (Test-Path -LiteralPath $bootstrap -PathType Leaf)) { + [Console]::Error.WriteLine("Code Intel recovery requires Python and the packaged Skill bootstrap.") + return $null + } + $output = @(& $python.Source $bootstrap --repo-path $Repository --json 2>&1) + if ($LASTEXITCODE -ne 0) { + [Console]::Error.WriteLine(($output -join [Environment]::NewLine)) + return $null + } + try { + $result = ($output -join [Environment]::NewLine) | ConvertFrom-Json + return [string]$result.release_root + } + catch { + [Console]::Error.WriteLine("Code Intel recovery returned invalid JSON.") + return $null + } +} + +$repo = if ([string]::IsNullOrWhiteSpace($RepoPath)) { (Get-Location).Path } else { $RepoPath } +$binary = $null +if (-not $Update) { + foreach ($root in @(Get-VerifiedReleaseRoots)) { + $candidate = Get-VerifiedReleaseBinary -Root $root + if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-CodeIntelBinary $candidate)) { + $binary = $candidate + break + } + } + if ($null -eq $binary) { + $binary = Get-DevelopmentCandidates | + Where-Object { Test-CodeIntelBinary $_ } | + Select-Object -First 1 + } +} + +if ($Update -or $null -eq $binary) { + $repairedRoot = Repair-CodeIntel -Repository $repo + if ([string]::IsNullOrWhiteSpace($repairedRoot)) { exit 69 } + $binary = Get-VerifiedReleaseBinary -Root $repairedRoot + if ([string]::IsNullOrWhiteSpace($binary) -or -not (Test-CodeIntelBinary $binary)) { exit 70 } +} + +$arguments = [System.Collections.Generic.List[string]]::new() +if (-not [string]::IsNullOrWhiteSpace($RepoPath)) { $arguments.Add($RepoPath) } +if ($PSBoundParameters.ContainsKey("Mode")) { + $arguments.Add("--mode") + $arguments.Add($Mode) +} +if ($Json) { $arguments.Add("--json") } +foreach ($argument in $Remaining) { $arguments.Add($argument) } + +& $binary @arguments +exit $LASTEXITCODE diff --git a/crates/code-intel-cli/Cargo.toml b/crates/code-intel-cli/Cargo.toml index 6b13a57..3e91cb5 100644 --- a/crates/code-intel-cli/Cargo.toml +++ b/crates/code-intel-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code-intel" -version = "0.4.0" +version = "0.5.0" edition = "2021" description = "Local Code Intel Pipeline CLI for artifact resume and contract checks" license = "MIT" diff --git a/crates/code-intel-cli/src/artifact_index.rs b/crates/code-intel-cli/src/artifact_index.rs index b061016..cb3b00e 100644 --- a/crates/code-intel-cli/src/artifact_index.rs +++ b/crates/code-intel-cli/src/artifact_index.rs @@ -478,7 +478,7 @@ fn is_lower_hex(value: &str) -> bool { .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) } -fn write_index(path: &Path, value: &Value) -> Result<(), IndexError> { +pub(crate) fn write_index(path: &Path, value: &Value) -> Result<(), IndexError> { let parent = path .parent() .filter(|parent| parent.is_dir()) diff --git a/crates/code-intel-cli/src/artifacts.rs b/crates/code-intel-cli/src/artifacts.rs index 066836e..53466c3 100644 --- a/crates/code-intel-cli/src/artifacts.rs +++ b/crates/code-intel-cli/src/artifacts.rs @@ -279,7 +279,7 @@ fn print_optional_path(label: &str, path: Option<&PathBuf>) { } } -fn resolve_artifact_root(explicit: Option<&Path>) -> Result { +pub(crate) fn resolve_artifact_root(explicit: Option<&Path>) -> Result { if let Some(path) = explicit { return Ok(path.to_path_buf()); } diff --git a/crates/code-intel-cli/src/capability.rs b/crates/code-intel-cli/src/capability.rs index 3f53c89..28b0d86 100644 --- a/crates/code-intel-cli/src/capability.rs +++ b/crates/code-intel-cli/src/capability.rs @@ -338,7 +338,7 @@ fn read_limited(reader: impl Read) -> io::Result> { Ok(bytes) } -fn discover_manifest(explicit: Option<&Path>) -> Option { +pub(crate) fn discover_manifest(explicit: Option<&Path>) -> Option { if let Some(path) = explicit { return path.is_file().then(|| path.to_path_buf()); } @@ -346,18 +346,23 @@ fn discover_manifest(explicit: Option<&Path>) -> Option { let path = PathBuf::from(path); return path.is_file().then_some(path); } - if let Some(home) = env::var_os("CODE_INTEL_HOME") { - let path = PathBuf::from(home) - .join("orchestration") - .join("integrations.json"); - return path.is_file().then_some(path); - } let mut candidates = vec![]; if let Ok(exe) = env::current_exe() { if let Some(parent) = exe.parent() { - candidates.push(parent.join("orchestration").join("integrations.json")); + candidates.extend( + parent + .ancestors() + .map(|root| root.join("orchestration").join("integrations.json")), + ); } } + if let Some(home) = env::var_os("CODE_INTEL_HOME") { + candidates.push( + PathBuf::from(home) + .join("orchestration") + .join("integrations.json"), + ); + } candidates.push( PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") diff --git a/crates/code-intel-cli/src/dag_run.rs b/crates/code-intel-cli/src/dag_run.rs index c4d9567..fc5ec00 100644 --- a/crates/code-intel-cli/src/dag_run.rs +++ b/crates/code-intel-cli/src/dag_run.rs @@ -6,7 +6,7 @@ use std::process::Command; use serde_json::{json, Value}; use crate::artifact_ref::{self, ArtifactError}; -use crate::capability::sha256_hex; +use crate::capability::{self, sha256_hex}; use crate::dag_coordinator::{ Coordinator, DagSpec, Dispatch, DomainVerdict, EdgeSpec, ExecutionFailure, NodeExecutor, NodeOutcome, NodeSpec, RunOutcome, VerifiedArtifactRef, @@ -888,9 +888,11 @@ fn declarations(registry: &Value) -> Result, RunError> { } fn default_registry() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("orchestration") - .join("integrations.json") + capability::discover_manifest(None).unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json") + }) } diff --git a/crates/code-intel-cli/src/doctor_adapter.rs b/crates/code-intel-cli/src/doctor_adapter.rs index 6549193..2c9b4bf 100644 --- a/crates/code-intel-cli/src/doctor_adapter.rs +++ b/crates/code-intel-cli/src/doctor_adapter.rs @@ -348,7 +348,9 @@ fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> } fn pipeline_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..") + crate::capability::discover_manifest(None) + .and_then(|manifest| manifest.parent()?.parent().map(Path::to_path_buf)) + .unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..")) } fn required_existing_directory(value: Option<&Value>, name: &str) -> Result { diff --git a/crates/code-intel-cli/src/main.rs b/crates/code-intel-cli/src/main.rs index 7fc5065..d300649 100644 --- a/crates/code-intel-cli/src/main.rs +++ b/crates/code-intel-cli/src/main.rs @@ -3,8 +3,9 @@ use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; use std::process; +use std::time::{SystemTime, UNIX_EPOCH}; -use serde_json::Value; +use serde_json::{json, Value}; mod adapter_contract; mod admissibility; @@ -207,6 +208,9 @@ struct ResumeSummary { fn main() { let raw: Vec = env::args().skip(1).collect(); + if is_primary_invocation(&raw) { + process::exit(run_primary(&raw)); + } if let Some(exit_code) = dispatch_raw_command(&raw) { process::exit(exit_code); } @@ -216,6 +220,278 @@ fn main() { } } +#[derive(Debug, PartialEq, Eq)] +struct PrimaryArgs { + repo: PathBuf, + mode: String, + artifact_root: Option, + json: bool, +} + +fn is_primary_invocation(raw: &[String]) -> bool { + raw.is_empty() + || raw.first().is_some_and(|first| { + !first.starts_with('-') && resolve_raw_route(raw).is_none() && !is_named_command(first) + }) + || matches!( + raw.first().map(String::as_str), + Some("--mode" | "--artifact-root" | "--json") + ) +} + +fn is_named_command(command: &str) -> bool { + matches!( + command, + "resume" + | "classify" + | "doctor" + | "sentrux-normalize" + | "sentrux-debt-register" + | "graph" + | "understand" + | "orchestrate" + | "orchestration" + | "provider" + | "providers" + | "route" + | "routes" + | "sentrux" + | "help" + ) +} + +fn parse_primary_args(raw: &[String]) -> std::result::Result { + let mut repo = None; + let mut mode = "normal".to_string(); + let mut artifact_root = None; + let mut json = false; + let mut index = 0; + while index < raw.len() { + match raw[index].as_str() { + "--mode" | "--artifact-root" => { + let flag = raw[index].as_str(); + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires one value"))?; + if flag == "--mode" { + if !matches!(value.as_str(), "lite" | "normal" | "full") { + return Err("--mode must be lite, normal, or full".into()); + } + mode = value.clone(); + } else if artifact_root.replace(PathBuf::from(value)).is_some() { + return Err("duplicate --artifact-root".into()); + } + index += 2; + } + "--json" => { + json = true; + index += 1; + } + token if token.starts_with('-') => { + return Err(format!("unknown primary entry argument: {token}")); + } + token => { + if repo.replace(PathBuf::from(token)).is_some() { + return Err("only one repository path may be supplied".into()); + } + index += 1; + } + } + } + let repo = repo.unwrap_or(env::current_dir().map_err(|error| error.to_string())?); + if !repo.is_dir() { + return Err(format!( + "repository path is not a directory: {}", + repo.display() + )); + } + Ok(PrimaryArgs { + repo: fs::canonicalize(repo).map_err(|error| error.to_string())?, + mode, + artifact_root, + json, + }) +} + +fn run_primary(raw: &[String]) -> i32 { + let json = raw.iter().any(|argument| argument == "--json"); + let args = match parse_primary_args(raw) { + Ok(args) => args, + Err(error) => { + print_primary_error(json, None, None, 64, &error); + return 64; + } + }; + match execute_primary(&args) { + Ok(code) => code, + Err(error) => { + print_primary_error( + args.json, + Some(&args.repo), + Some(&args.mode), + error.exit_code, + &error.message, + ); + error.exit_code + } + } +} + +fn execute_primary(args: &PrimaryArgs) -> std::result::Result { + let artifact_root = artifacts::resolve_artifact_root(args.artifact_root.as_deref()) + .map_err(|error| execution_kernel::RunError::io(error.to_string()))?; + fs::create_dir_all(&artifact_root).map_err(|error| { + execution_kernel::RunError::io(format!("create artifact root: {error}")) + })?; + let repo_name = args + .repo + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| !name.is_empty()) + .ok_or_else(|| execution_kernel::RunError::contract("repository has no usable name"))?; + let authority_root = artifact_root.join(repo_name); + fs::create_dir_all(&authority_root).map_err(|error| { + execution_kernel::RunError::io(format!("create repository authority root: {error}")) + })?; + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| execution_kernel::RunError::io(error.to_string()))? + .as_millis(); + let final_name = format!("{nonce}-{}-core", process::id()); + let staging_root = env::temp_dir().join(format!("code-intel-a09-{final_name}")); + let profile = match args.mode.as_str() { + "lite" => execution_policy::RunProfile::Offline, + "normal" => execution_policy::RunProfile::Default, + "full" => execution_policy::RunProfile::Strict, + _ => unreachable!("primary mode is validated"), + }; + let result = execution_kernel::execute(execution_kernel::RunRequest { + repo: args.repo.clone(), + staging_root: staging_root.clone(), + authority_root, + final_name, + manifest: None, + max_concurrency: 2, + policy: execution_policy::ExecutionPolicy::for_profile(profile), + session_evidence: None, + })?; + if staging_root.is_dir() { + fs::remove_dir_all(&staging_root).map_err(|error| { + execution_kernel::RunError::io(format!("remove committed staging directory: {error}")) + })?; + } + let index = artifact_index::rebuild(&artifact_root).map_err(|error| { + execution_kernel::RunError::io(format!("rebuild artifact index: {error}")) + })?; + artifact_index::write_index(&artifact_root.join("index.json"), &index).map_err(|error| { + execution_kernel::RunError::io(format!("publish artifact index: {error}")) + })?; + let output = primary_result(&args, &result); + if args.json { + println!( + "{}", + serde_json::to_string(&output).expect("primary result serializes") + ); + } else { + print_primary_summary(&output); + } + Ok(result.exit_code()) +} + +fn print_primary_error( + json_output: bool, + repo: Option<&Path>, + mode: Option<&str>, + exit_code: i32, + diagnostic: &str, +) { + if json_output { + println!( + "{}", + serde_json::to_string(&json!({ + "schema": "code-intel-primary-result.v1", + "repo": repo, + "mode": mode, + "outcome": "error", + "exitCode": exit_code, + "publication": Value::Null, + "failureNode": Value::Null, + "diagnostic": diagnostic, + })) + .expect("primary error result serializes") + ); + } else { + eprintln!("Code Intel error: {diagnostic}"); + } +} + +fn primary_result(args: &PrimaryArgs, result: &execution_kernel::ExecutionResult) -> Value { + let (failure_node, diagnostic) = first_failure(&result.manifest) + .map(|(node, diagnostic)| (Value::String(node), Value::String(diagnostic))) + .unwrap_or((Value::Null, Value::Null)); + json!({ + "schema": "code-intel-primary-result.v1", + "repo": args.repo, + "mode": args.mode, + "outcome": result.outcome.as_str(), + "exitCode": result.exit_code(), + "publication": { + "path": result.publication.path, + "marker": result.publication.path.join("run-complete.json"), + }, + "failureNode": failure_node, + "diagnostic": diagnostic, + }) +} + +fn first_failure(manifest: &Value) -> Option<(String, String)> { + manifest["nodes"] + .as_object()? + .iter() + .find_map(|(node, value)| { + matches!( + value["status"].as_str(), + Some("process_failed" | "domain_failed" | "domain_unknown") + ) + .then(|| { + ( + node.clone(), + value["diagnostic"] + .as_str() + .or_else(|| value["failure"].as_str()) + .unwrap_or("") + .to_string(), + ) + }) + }) +} + +fn print_primary_summary(output: &Value) { + let passed = output["exitCode"].as_i64() == Some(0); + println!( + "[{}] {}", + if passed { "PASS" } else { "FAIL" }, + output["repo"].as_str().unwrap_or("") + ); + println!(" Outcome: {}", output["outcome"].as_str().unwrap_or("")); + println!( + " Run evidence: {}", + output["publication"]["marker"].as_str().unwrap_or("") + ); + if let Some(node) = output["failureNode"].as_str() { + let diagnostic = output["diagnostic"].as_str().unwrap_or(""); + println!( + " Cause: {node}{}", + if diagnostic.is_empty() { + String::new() + } else { + format!(" - {diagnostic}") + } + ); + } +} + type RawRunner = fn(&[String]) -> i32; struct RawRoute { @@ -470,7 +746,7 @@ fn run() -> Result<()> { "route" | "routes" => cmd_route(&args), "sentrux" => cmd_sentrux(&args), "help" | "--help" | "-h" => { - print_help(); + print_help(args.full); Ok(()) } other => Err(format!("unknown command: {other}").into()), @@ -588,6 +864,7 @@ fn set_switch_arg(args: &mut Args, flag: &str) -> bool { match flag { "--write" => args.write = true, "--full" => args.full = true, + "--all" if matches!(args.command.as_str(), "help" | "--help" | "-h") => args.full = true, "--json" => args.json = true, "--help" | "-h" => args.command = "help".to_string(), _ => return false, @@ -1398,41 +1675,58 @@ fn cmd_sentrux(args: &Args) -> Result<()> { }) } -fn print_help() { - println!("code-intel [options]"); - println!(); - println!("Commands:"); - println!(" resume --repo [--artifact-root ] [--json]"); - println!(" classify --report [--json]"); - println!(" sentrux-normalize --steps [--out ]"); - println!(" sentrux-debt-register --failures [--repo ] [--out ]"); - println!(" doctor [--artifact-root ] [--json]"); - println!(" graph --repo [--language zh] [--full] [--write] [--json]"); - println!(" provider [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json]"); - println!(" provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds "); - println!(" provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds "); - println!(" provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds "); - println!(" provider session-adapt --repo --trace [--hotspots ] [--out ] [--working-tree-policy head_only|explicit_overlay]"); - println!(" provider file-boundary --request --out "); - println!(" provider runtime-ci-evidence --artifact-root --request --out "); - println!(" route [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json]"); - println!(" sentrux "); - println!(" capability exec --request --out [--artifact-root ] [--manifest ]"); - println!(" model inventory-validate --request [--out ]"); - println!(" model route --request [--out ]"); - println!(" snapshot identity --repo --working-tree-policy [--scope ]..."); - println!(" evidence validate --request --artifact-root "); - println!(" artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ]"); - println!(" artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>]"); - println!(" change impact --artifact-root --repo --repo-path --changed [--changed ]..."); - println!(" decision request-response --request [--response |--cancel ] --now --branch ..."); - println!(" decision record --resolution --store "); - println!(" decision replay --query --store "); - println!(" run execute --repo --out --authority-root --final-name [--profile default|strict|offline] [--manifest ] [--max-concurrency ] [--session-evidence ]"); - println!(" run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--session-evidence ]"); - println!(" run commit --source-root --authority-root --manifest-ref --final-name "); - println!(" governance ponytail-gate --request "); - println!(" orchestrate [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]"); +const HELP_TEXT: &str = r#"Code Intel Pipeline + +Quick start: + code-intel . + code-intel --mode lite|normal|full + +Common commands: + code-intel doctor --json + code-intel graph --repo --write + code-intel resume --repo --json + code-intel sentrux check + +Advanced commands: + code-intel --help --all"#; + +const FULL_HELP_TEXT: &str = r#"code-intel [options] + +Commands: + resume --repo [--artifact-root ] [--json] + classify --report [--json] + sentrux-normalize --steps [--out ] + sentrux-debt-register --failures [--repo ] [--out ] + doctor [--artifact-root ] [--json] + graph --repo [--language zh] [--full] [--write] [--json] + provider [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json] + provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds + provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds + provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds + provider session-adapt --repo --trace [--hotspots ] [--out ] [--working-tree-policy head_only|explicit_overlay] + provider file-boundary --request --out + provider runtime-ci-evidence --artifact-root --request --out + route [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json] + sentrux + capability exec --request --out [--artifact-root ] [--manifest ] + model inventory-validate --request [--out ] + model route --request [--out ] + snapshot identity --repo --working-tree-policy [--scope ]... + evidence validate --request --artifact-root + artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ] + artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>] + change impact --artifact-root --repo --repo-path --changed [--changed ]... + decision request-response --request [--response |--cancel ] --now --branch ... + decision record --resolution --store + decision replay --query --store + run execute --repo --out --authority-root --final-name [--profile default|strict|offline] [--manifest ] [--max-concurrency ] [--session-evidence ] + run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--session-evidence ] + run commit --source-root --authority-root --manifest-ref --final-name + governance ponytail-gate --request + orchestrate [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]"#; + +fn print_help(full: bool) { + println!("{}", if full { FULL_HELP_TEXT } else { HELP_TEXT }); } #[cfg(test)] @@ -1452,6 +1746,18 @@ mod tests { assert_eq!(args.command, "help"); } + #[test] + fn default_help_prioritizes_the_happy_path_and_hides_internal_commands() { + let args = parse_args(cli_args(&["--help", "--all"])).expect("full help should parse"); + + assert!(args.full); + assert!(HELP_TEXT.contains("Quick start")); + assert!(HELP_TEXT.contains("code-intel .")); + assert!(HELP_TEXT.contains("code-intel --help --all")); + assert!(!HELP_TEXT.contains("provider graph-adapt")); + assert!(FULL_HELP_TEXT.contains("provider graph-adapt")); + } + #[test] fn parse_args_preserves_graph_options() { let args = parse_args(cli_args(&[ diff --git a/crates/code-intel-cli/src/orchestration.rs b/crates/code-intel-cli/src/orchestration.rs index 39e3a11..c3c2c68 100644 --- a/crates/code-intel-cli/src/orchestration.rs +++ b/crates/code-intel-cli/src/orchestration.rs @@ -226,8 +226,8 @@ struct ProductionParticipant { const PRODUCTION_PARTICIPANTS: [ProductionParticipant; 12] = [ ProductionParticipant { capability_id: "doctor", - source: "invoke-code-intel.ps1", - marker: "$doctor = Join-Path $root \"check-code-intel-tools.ps1\"", + source: "crates/code-intel-cli/src/doctor_adapter.rs", + marker: "let bootstrap = run_bootstrap(&options)?;", }, ProductionParticipant { capability_id: "diagnosis.hospital", @@ -282,8 +282,8 @@ const PRODUCTION_PARTICIPANTS: [ProductionParticipant; 12] = [ }, ProductionParticipant { capability_id: "artifact.index-committed-only", - source: "invoke-code-intel.ps1", - marker: "$indexer = Join-Path $root \"update-code-intel-index.ps1\"", + source: "crates/code-intel-cli/src/main.rs", + marker: "artifact_index::write_index(&artifact_root.join(\"index.json\"), &index)", }, ]; diff --git a/crates/code-intel-cli/tests/primary_entry.rs b/crates/code-intel-cli/tests/primary_entry.rs new file mode 100644 index 0000000..c3fee23 --- /dev/null +++ b/crates/code-intel-cli/tests/primary_entry.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; +use std::process::Command; + +fn binary() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_code-intel")) +} + +#[test] +fn root_help_leads_with_the_compiled_primary_entry() { + let output = Command::new(binary()) + .arg("--help") + .output() + .expect("run code-intel --help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("help is UTF-8"); + assert!(stdout.contains("code-intel .")); + assert!(stdout.contains("code-intel --mode lite|normal|full")); + assert!(!stdout.contains("invoke-code-intel.ps1")); +} + +#[test] +fn root_entry_rejects_a_missing_repository_with_usage_exit_code() { + let missing = + std::env::temp_dir().join(format!("code-intel-missing-repo-{}", std::process::id())); + let output = Command::new(binary()) + .arg(&missing) + .arg("--mode") + .arg("lite") + .output() + .expect("run code-intel with a missing repository"); + + assert_eq!(output.status.code(), Some(64)); + let stderr = String::from_utf8(output.stderr).expect("error is UTF-8"); + assert!(stderr.contains("repository path is not a directory:")); + assert!(!stderr.contains("unknown command")); +} + +#[test] +fn root_entry_keeps_json_machine_readable_on_usage_errors() { + let missing = std::env::temp_dir().join(format!( + "code-intel-json-missing-repo-{}", + std::process::id() + )); + let output = Command::new(binary()) + .arg(&missing) + .args(["--mode", "lite", "--json"]) + .output() + .expect("run code-intel JSON error path"); + + assert_eq!(output.status.code(), Some(64)); + assert!(output.stderr.is_empty()); + let result: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("error output is JSON"); + assert_eq!(result["schema"], "code-intel-primary-result.v1"); + assert_eq!(result["outcome"], "error"); + assert_eq!(result["exitCode"], 64); + assert!(result["diagnostic"] + .as_str() + .is_some_and(|message| message.contains("repository path is not a directory:"))); +} + +#[test] +fn named_commands_are_not_misclassified_as_repository_paths() { + let output = Command::new(binary()) + .args(["orchestrate", "--action", "List", "--json"]) + .output() + .expect("run an existing named command"); + + assert!(output.status.success()); + let stderr = String::from_utf8(output.stderr).expect("error is UTF-8"); + assert!(!stderr.contains("unknown primary entry argument")); +} diff --git a/docs/adr/0011-primary-cli-and-recovery-launcher.md b/docs/adr/0011-primary-cli-and-recovery-launcher.md new file mode 100644 index 0000000..3f95a1c --- /dev/null +++ b/docs/adr/0011-primary-cli-and-recovery-launcher.md @@ -0,0 +1,34 @@ +--- +status: accepted +date: 2026-07-25 +--- + +# Make code-intel the primary entry and PowerShell the recovery launcher + +The compiled `code-intel` command is the only Primary Operator Entry and the +only implementation of Pipeline execution semantics. `code-intel.ps1` is a +cross-platform Recovery Launcher for PowerShell 7.2+: it may locate, validate, +install, repair, update, and start the compiled command, but it must not contain +an alternate Pipeline implementation. + +The launcher may automatically restore a missing or invalid installation from +the latest stable Release published by +`github.com/2233admin/code-intel-pipeline`. It must verify the published SHA-256, +stage and switch releases atomically, retain the current and previous verified +stable versions, and prefer the last verified local version when GitHub is +unavailable. A healthy installation is never upgraded during an ordinary run; +updates are explicit. + +`code-intel .` and `code-intel.ps1 .` analyze the given Target Repository, or +the current directory when omitted, using `normal` mode by default. The existing +`invoke-code-intel.ps1` remains a quiet forwarding compatibility surface +through v0.x. Agent installation is Skill-first; human installation is through +the GitHub Release package. + +## Consequences + +- Documentation and Skills lead with `code-intel`; they mention + `code-intel.ps1` only for installation and recovery. +- The official GitHub repository is the sole remote trust root for v0.5. +- The launcher exposes concise human output, a machine-readable JSON mode, and + stable failure exit codes without weakening digest verification. diff --git a/docs/artifact-data-contract.md b/docs/artifact-data-contract.md index 303a842..6e1ad80 100644 --- a/docs/artifact-data-contract.md +++ b/docs/artifact-data-contract.md @@ -4,7 +4,9 @@ Code Intel artifact files are the handoff surface between the scanner, CLI consu ## Authority -`run-code-intel.ps1` is the only producer of fresh artifact runs. `code-intel resume`, `code-intel classify`, `code-intel doctor`, and indexers consume existing runs; they do not replace scanner evidence. +The compiled `code-intel` execution kernel is the authority for fresh artifact +runs and publication. Compatibility adapters may collect evidence, but they do +not own Run Commit or index admission. Agent Goal Intake is an upstream product boundary. It may shape the operator's goal before a scan starts, but it must not produce, mutate, or reinterpret artifact-run files after scanner execution. @@ -14,7 +16,7 @@ An artifact run is one timestamped directory for one target repository: \\\ ``` -Do not hand-edit artifact runs. Regenerate them with `invoke-code-intel.ps1` or `run-code-intel.ps1`. +Do not hand-edit artifact runs. Regenerate them with `code-intel `. ## Files diff --git a/docs/code-intel-architecture.md b/docs/code-intel-architecture.md index 7c82542..c216bba 100644 --- a/docs/code-intel-architecture.md +++ b/docs/code-intel-architecture.md @@ -8,38 +8,43 @@ It is built around one rule: keep the entrypoint small, keep tool roles explicit Artifact ownership and reader/writer boundaries are defined in `docs/artifact-data-contract.md`. -1. `orchestration/integrations.json` and `code-intel.exe orchestrate` +1. Compiled `code-intel` + Primary Operator Entry and execution kernel. `code-intel .` runs the default + `normal` profile; `--mode lite|full` selects an explicit alternative. + +2. `orchestration/integrations.json` and `code-intel orchestrate` Integration registry and fusion layer. New scanners, memory systems, graph providers, governance strategies, and compatibility shims must be registered here before they are wired into runner scripts. `orchestration/capability-contract.v1.json` defines the Capability Atom declaration/request/result, Snapshot Identity, Artifact Ref, Effect Boundary, Domain Verdict, Run Commit, Materialized View, cache-key, and transactional publication vocabulary. `orchestration/schemas/code-intel-capability-envelope.v1.schema.json` rejects malformed envelopes and impossible outcome combinations. Existing integrations migrate behind that contract one atom at a time; the registry remains the graph authority. Runtime effect enforcement is not yet implemented. -2. Rust targets +3. Rust targets - `crates/code-intel-cli`: compiled `code-intel` CLI for integration orchestration, artifact resume, classify, and artifact doctor contracts. - `crates/code-nexus-lite`: incubated source, not a Cargo workspace member and not shipped as a beta binary. The supported beta surface is the optional CodeNexus compatibility adapter and artifact contract. -3. `invoke-code-intel.ps1` - Thin operator entrypoint. Runs doctor first, then the pipeline. Supports one direct repo path, one configured repo alias, a repo list, or all configured repos. +4. PowerShell compatibility + - `code-intel.ps1`: recovery/update launcher for official GitHub releases. + - `invoke-code-intel.ps1`: quiet v0.x compatibility forwarder. -4. `check-code-intel-tools.ps1` +5. `check-code-intel-tools.ps1` Environment doctor. Verifies local tools, Understand Anything presence, repo path, and Sentrux scope state. -5. `run-code-intel.ps1` - Main orchestrator. Produces artifacts, summary, report, hospital diagnosis, and failure classification. +6. `run-code-intel.ps1` + Compatibility adapter host used by capabilities not yet internalized. -6. Tool adapters +7. Tool adapters - `rg`: exact inventory - `repowise`: optional semantic index and docs; included in the default plan but non-blocking - `Understand Anything`: optional graph artifact - `sentrux`: structure gate - `sentruxInsight`: parsed structural deltas and follow-up hints for agents -7. Scoped helpers +8. Scoped helpers - `Invoke-ScopedRepowise.ps1` - `Invoke-RepowiseProviderProbe.ps1` - `Run-ScopedRepowiseDocs.py` - `Invoke-SentruxAgentTool.ps1` -8. Stable-ops helpers +9. Stable-ops helpers - `install-code-intel-pipeline.ps1` - `scripts/tests/test-code-intel-provider.ps1` (test wrapper only) - `scripts/tests/test-code-intel-pipeline.ps1` @@ -174,22 +179,17 @@ Install or repair a teammate machine: `-RepairSkillLinks` installs the bundled `skills/code-intel-pipeline/` package into the user profile when the shared `.agents` skill is absent, then links Codex and Claude to that shared copy. -Doctor and normal run: - -```powershell -& "$env:CODE_INTEL_HOME/invoke-code-intel.ps1" -RepoPath -Mode normal -``` - -Docs-enabled run: +Normal run: ```powershell -& "$env:CODE_INTEL_HOME/invoke-code-intel.ps1" -RepoPath -Mode normal -RepowiseDocs +code-intel ``` -Batch run: +Explicit profiles: ```powershell -& "$env:CODE_INTEL_HOME/invoke-code-intel.ps1" -Config "$env:CODE_INTEL_HOME/pipeline.config.json" -All -Mode lite +code-intel --mode lite +code-intel --mode full ``` Smoke test: diff --git a/docs/follow-up-automation.md b/docs/follow-up-automation.md index 04503d0..b2f11d6 100644 --- a/docs/follow-up-automation.md +++ b/docs/follow-up-automation.md @@ -19,11 +19,8 @@ Disable or change the suggestion in `pipeline.config.json`: } ``` -CLI values override configuration: - -```powershell -pwsh -File invoke-code-intel.ps1 -RepoPath C:\repo -ProactiveSkillSuggestions enabled -BugSkill /investigate -``` +The primary entry currently reads this policy from `pipeline.config.json`; it +does not expose a separate CLI override. ## Automatic pull requests @@ -36,10 +33,6 @@ The choices are: `enabled` means the operator requested the execution path; it is not sufficient authority by itself. The execution atom remains fail-closed until it receives scoped authorization artifacts and both runtime effect switches. `disabled` emits neither a consent request nor an external effect. -```powershell -pwsh -File invoke-code-intel.ps1 -RepoPath C:\repo -AutomaticPullRequests ask -``` - The core Pipeline never calls `gh pr create` from the advisory path. It does not listen to Codex, Claude, or OpenCode chat messages directly; a host that wants chat-triggered suggestions must submit normalized bug evidence to the Pipeline. The automatic-PR question is a feature-flow opt-in, not authority to publish a particular pull request. A concrete draft PR requires a separately hashed canonical proposal and replay-valid C07 diff --git a/docs/project-management-support.md b/docs/project-management-support.md index b94aede..dfd219a 100644 --- a/docs/project-management-support.md +++ b/docs/project-management-support.md @@ -2,7 +2,7 @@ Project Management Support is Code Intel Pipeline's agent-intake layer for turning repository evidence into trackable work and durable project knowledge. It internalizes the useful setup concepts from `mattpocock/skills`: issue tracker choice, triage label vocabulary, and domain documentation layout. -It is not scanner runtime. Do not install `mattpocock/skills`, Linear clients, Obsidian plugins, or wiki tooling to run Code Intel Pipeline. Scanner-owned artifact runs remain produced only by `run-code-intel.ps1` and `invoke-code-intel.ps1`. +It is not scanner runtime. Do not install `mattpocock/skills`, Linear clients, Obsidian plugins, or wiki tooling to run Code Intel Pipeline. Scanner-owned artifact runs are produced and published by the compiled `code-intel` execution kernel. ## Surfaces diff --git a/docs/public-beta.md b/docs/public-beta.md index e7f48bc..aedbbc4 100644 --- a/docs/public-beta.md +++ b/docs/public-beta.md @@ -2,9 +2,9 @@ ## Supported surface -The public beta ships as a Windows ZIP and uses PowerShell 7.2 or newer. The -stable entrypoint is `invoke-code-intel.ps1`; the packaged Rust core is -`bin/code-intel.exe`. A release package must not require Cargo, a source-tree +The public beta ships as a Windows ZIP. The stable entrypoint is the packaged +`bin/code-intel.exe`; `code-intel.ps1` is the PowerShell 7.2+ recovery launcher +and `invoke-code-intel.ps1` is a v0.x compatibility forwarder. A release package must not require Cargo, a source-tree `target/` directory, or a local Rust installation. The beta core covers repository inventory, Sentrux structural evidence, @@ -14,8 +14,8 @@ not redefine whether the core pipeline is usable. | Capability | Beta status | Missing-provider behavior | | --- | --- | --- | -| Stable PowerShell entrypoint and doctor | Core | Fail with an actionable local error | -| `code-intel.exe` policy/artifact core | Core | Fail; packaged binary is required | +| Compiled `code-intel` entrypoint and doctor | Core | Fail with an actionable local error | +| PowerShell recovery launcher | Recovery | Verify and repair from the official GitHub release | | `rg` inventory | Core | Fail with an actionable local error | | Sentrux structural evidence | Core | Report real gate/check failure | | Transactional run commit and reports | Core | Fail closed; incomplete runs are not indexed | @@ -38,13 +38,12 @@ surface is the optional compatibility adapter and its artifact contract. 4. Run: ```powershell -.\invoke-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -SkipRepowise +.\bin\code-intel.exe C:\path\to\repo ``` -Remove `-SkipRepowise` when Repowise is installed and semantic memory is -desired. Optional providers remain in the default orchestration plan so a -configured machine gets the richer result without using a different product -path. +Use `--mode lite` or `--mode full` only when the default `normal` profile is +not appropriate. Optional providers remain in the orchestration plan and are +used when available. ## Known limits @@ -58,8 +57,7 @@ path. ## Upgrade and rollback -Release ZIPs are self-contained. Extract a new beta beside the previous one, -run the package smoke test, and then switch the caller's path. Rollback means -switching the path back to the previous extracted directory; do not overwrite -the old directory in place. Generated artifacts live outside the package under -the platform Code Intel data root. +Release ZIPs are self-contained. `code-intel.ps1 -Update` installs a verified +official stable release while retaining a verified local fallback. Manual +rollback means switching back to the previous extracted directory. Generated +artifacts live outside the package under the platform Code Intel data root. diff --git a/docs/repository-layout.md b/docs/repository-layout.md index d51313e..41b2673 100644 --- a/docs/repository-layout.md +++ b/docs/repository-layout.md @@ -7,15 +7,16 @@ This repository is converging toward a smaller public surface. Keep these files at the repository root until a release explicitly changes the install and CI contract: -- `invoke-code-intel.ps1`: stable operator entry point. -- `run-code-intel.ps1`: current artifact-producing pipeline orchestrator. +- `code-intel.ps1`: recovery and update launcher for the compiled CLI. +- `invoke-code-intel.ps1`: v0.x compatibility forwarder. +- `run-code-intel.ps1`: compatibility adapter host for capabilities not yet internalized. - `check-code-intel-tools.ps1`: environment doctor. - `install-code-intel-pipeline.ps1`: installer and repair entry point. - `Find-CodeIntelProjects.ps1`: local project discovery entry point. - `bootstrap-new-machine.ps1`: new-machine bootstrap entry point. - `Invoke-SentruxAgentTool.ps1`: Sentrux compatibility entry point. - `README.md`, `CHANGELOG.md`, `CONTEXT.md`: operator-facing docs. -- `Cargo.toml`, `Cargo.lock`, `crates/code-intel-cli`: Rust CLI policy core. +- `Cargo.toml`, `Cargo.lock`, `crates/code-intel-cli`: primary compiled CLI and execution kernel. ## Internal Script Buckets @@ -26,7 +27,7 @@ Internal scripts use these buckets: - `scripts/adapters/`: tool-specific helper wrappers. - `scripts/incubator/`: experiments that are not in the shipped product path. -The seven public PowerShell entry points stay at the repository root. Test +The public PowerShell compatibility and recovery entry points stay at the repository root. Test scripts are internal and must remain under `scripts/tests/`. Do not move a root PowerShell file without one of these: @@ -37,7 +38,7 @@ Do not move a root PowerShell file without one of these: ## Rust Core Boundary -The Rust CLI owns pure policy and artifact-consumer logic first: +The Rust CLI owns the primary operator entry, execution kernel, policy, and artifact contracts: - artifact resume - failure classification @@ -48,8 +49,8 @@ The Rust CLI owns pure policy and artifact-consumer logic first: The current policy contract is documented in `docs/rust-policy-core.md`. -The PowerShell pipeline still owns local orchestration and tool invocation until -the Rust policy contract is stable enough to become the runner. +PowerShell remains for recovery, compatibility, and adapters that have not yet +been internalized. ## Incubator Boundary diff --git a/install-code-intel-pipeline.ps1 b/install-code-intel-pipeline.ps1 index 2d763ee..20b59f1 100644 --- a/install-code-intel-pipeline.ps1 +++ b/install-code-intel-pipeline.ps1 @@ -840,7 +840,7 @@ switch ($script:EffectivePlatform) { } } Add-InstallPlan $installPlan "repowise" "pip" "python/python3 -m pip install --user --upgrade repowise" "Semantic index and wiki/docs memory." "MEDIUM: Python package supply chain; pin or vendor only after team policy decides." "Skip repowise with -SkipRepowise for exact-search-only runs." "pip" $false -Add-InstallPlan $installPlan "code-intel" "repo-local release binary" "copy bin/code-intel or target/release/code-intel into CODE_INTEL_BIN; build with cargo when no binary is present" "Manifest-bound DAG, evidence query, impact analysis, and atomic publication." "LOW: Pipeline-owned binary; installed digest is reported and --help is executed before success." "Use invoke-code-intel.ps1 from the source tree; it can build a debug binary on demand." "repo-local" $false +Add-InstallPlan $installPlan "code-intel" "repo-local release binary" "copy bin/code-intel or target/release/code-intel into CODE_INTEL_BIN; build with cargo when no binary is present" "Manifest-bound DAG, evidence query, impact analysis, and atomic publication." "LOW: Pipeline-owned binary; installed digest is reported and --help is executed before success." "Use code-intel.ps1 only when the compiled command needs recovery." "repo-local" $false $sentruxBinaryName = if ($script:EffectivePlatform -eq "windows") { "sentrux.exe" } else { "sentrux" } Add-InstallPlan $installPlan "sentrux" "repo-local shim or preinstalled binary" "install tools/sentrux-shim first; optionally place a real $sentruxBinaryName on PATH" "Structural quality and regression gate." "LOW for repo-owned shim; MEDIUM for any separately supplied $sentruxBinaryName." "The repo-owned sentrux-lite core keeps scan/check/gate/plugin usable until the real binary is installed." "repo-local" $false Add-InstallPlan $installPlan "sentrux-shim" "repo-local" "copy tools/sentrux-shim launcher to CODE_INTEL_BIN and prepend PATH" "Open-source local Pro activation, stable forwarding to real sentrux, and deterministic lite-core fallback." "LOW: repo-owned PowerShell/CMD/sh shim; review tools/sentrux-shim before install." "Set SENTRUX_AUTO_PRO=0 to disable auto Pro activation." "repo-local" $false @@ -858,6 +858,7 @@ Install-SentruxVlangPluginOverlay $installActions $root $requiredFiles = @( "check-code-intel-tools.ps1", + "code-intel.ps1", "invoke-code-intel.ps1", "Install-SentruxVlangOverlay.ps1", "scripts/tests/Test-SentruxVlangOverlay.ps1", diff --git a/invoke-code-intel.ps1 b/invoke-code-intel.ps1 index 385eb5e..d812739 100644 --- a/invoke-code-intel.ps1 +++ b/invoke-code-intel.ps1 @@ -1,401 +1,95 @@ #requires -Version 7.2 +[CmdletBinding()] param( [string]$Repo = "", [string]$RepoPath = "", [string[]]$Repos = @(), [switch]$All, - [string]$Config = "", - - [ValidateSet("auto", "windows", "macos", "linux")] - [string]$Platform = "auto", - - [ValidateSet("lite", "normal", "full")] - [string]$Mode = "normal", - + [ValidateSet("auto", "windows", "macos", "linux")][string]$Platform = "auto", + [ValidateSet("lite", "normal", "full")][string]$Mode = "normal", [switch]$RepowiseDocs, [string]$RepowiseProvider = "", [string]$RepowiseModel = "", [string]$RepowiseReasoning = "", [switch]$SaveSentruxBaseline, -[switch]$AutoSaveMissingSentruxBaseline, -[switch]$RequireUnderstandGraph, -[switch]$SkipGitHubResearch, -[switch]$SkipRepowise, -[switch]$NoIndexUpdate, -[switch]$ValidateInstallation, -[switch]$LegacyCompatibility, -[ValidateSet("auto", "enabled", "disabled")] -[string]$ProactiveSkillSuggestions = "auto", -[ValidateSet("auto", "ask", "enabled", "disabled")] -[string]$AutomaticPullRequests = "auto", -[string]$BugSkill = "" + [switch]$AutoSaveMissingSentruxBaseline, + [switch]$RequireUnderstandGraph, + [switch]$SkipGitHubResearch, + [switch]$SkipRepowise, + [switch]$NoIndexUpdate, + [switch]$ValidateInstallation, + [switch]$LegacyCompatibility, + [ValidateSet("auto", "enabled", "disabled")][string]$ProactiveSkillSuggestions = "auto", + [ValidateSet("auto", "ask", "enabled", "disabled")][string]$AutomaticPullRequests = "auto", + [string]$BugSkill = "" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +$launcher = Join-Path $PSScriptRoot "code-intel.ps1" -$root = Split-Path -Parent $PSCommandPath -if ([string]::IsNullOrWhiteSpace($Config)) { - $Config = Join-Path $root "pipeline.config.json" +$supported = @("Repo", "RepoPath", "Config", "Mode", "ValidateInstallation") +$unsupported = @($PSBoundParameters.Keys | Where-Object { $_ -notin $supported }) +if ($unsupported.Count -gt 0) { + [Console]::Error.WriteLine("Code Intel error: unsupported compatibility option: -$($unsupported[0])") + exit 64 } -$doctor = Join-Path $root "check-code-intel-tools.ps1" -$runner = Join-Path $root "run-code-intel.ps1" -$indexer = Join-Path $root "update-code-intel-index.ps1" -$platformModule = Join-Path (Join-Path $root "tools") "code-intel-platform.psm1" -Import-Module $platformModule -Force -$binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } -$rustCliCandidates = @( - (Join-Path $root "bin/$binaryName"), - (Join-Path $root "target/release/$binaryName"), - (Join-Path $root "target/debug/$binaryName") -) -$rustCli = @( - $rustCliCandidates | - Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | - ForEach-Object { Get-Item -LiteralPath $_ } | - Sort-Object LastWriteTimeUtc -Descending | - Select-Object -First 1 -ExpandProperty FullName -) -if ($rustCli.Count -gt 0) { $rustCli = $rustCli[0] } else { $rustCli = $null } - -if ($SkipRepowise -and $RepowiseDocs) { - throw "-SkipRepowise cannot be combined with -RepowiseDocs." -} - -function Get-JsonProperty { - param( - [object]$Object, - [string]$Name - ) - - if ($null -eq $Object) { return $null } - $prop = $Object.PSObject.Properties[$Name] - if ($null -eq $prop) { return $null } - return $prop.Value -} - -function Get-RepoSelector { - param([string]$RepoName, [string]$DirectRepoPath) - - if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { - return @{ RepoPath = $DirectRepoPath } - } - return @{ Repo = $RepoName } -} - -function Get-RunnerParameters { - param([string]$RepoName, [string]$DirectRepoPath) - - $parameters = @{ - Config = $Config - Mode = $Mode - Platform = $Platform - RepowiseProvider = $RepowiseProvider - RepowiseModel = $RepowiseModel - RepowiseReasoning = $RepowiseReasoning - ProactiveSkillSuggestions = $ProactiveSkillSuggestions - AutomaticPullRequests = $AutomaticPullRequests - BugSkill = $BugSkill - } - foreach ($entry in (Get-RepoSelector -RepoName $RepoName -DirectRepoPath $DirectRepoPath).GetEnumerator()) { - $parameters[$entry.Key] = $entry.Value - } - foreach ($switchEntry in @( - @{ Name = "RepowiseDocs"; Enabled = $RepowiseDocs }, - @{ Name = "SaveSentruxBaseline"; Enabled = $SaveSentruxBaseline }, - @{ Name = "AutoSaveMissingSentruxBaseline"; Enabled = $AutoSaveMissingSentruxBaseline }, - @{ Name = "RequireUnderstandGraph"; Enabled = $RequireUnderstandGraph }, - @{ Name = "SkipGitHubResearch"; Enabled = $SkipGitHubResearch }, - @{ Name = "SkipRepowise"; Enabled = $SkipRepowise } - )) { - if ($switchEntry.Enabled) { $parameters[$switchEntry.Name] = $true } - } - return $parameters -} - -function Get-DoctorParameters { - param([string]$RepoName, [string]$DirectRepoPath) - - $parameters = @{ - Config = $Config - Platform = $Platform - RequireRepowise = [bool]$RepowiseDocs - RequireUnderstand = [bool]$RequireUnderstandGraph - } - foreach ($entry in (Get-RepoSelector -RepoName $RepoName -DirectRepoPath $DirectRepoPath).GetEnumerator()) { - $parameters[$entry.Key] = $entry.Value - } - return $parameters +if ($ValidateInstallation) { + & $launcher --help + exit $LASTEXITCODE } -function Resolve-InvocationRepoPath { - param([string]$RepoName, [string]$DirectRepoPath) - - if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { - return (Get-Item -LiteralPath $DirectRepoPath -ErrorAction Stop).FullName +$configData = $null +$configPath = $null +if ($PSBoundParameters.ContainsKey("Config") -or + ([string]::IsNullOrWhiteSpace($RepoPath) -and -not [string]::IsNullOrWhiteSpace($Repo))) { + $configPath = if ([string]::IsNullOrWhiteSpace($Config)) { + Join-Path $PSScriptRoot "pipeline.config.json" + } else { + $Config } - $configData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json - $reposConfig = Get-JsonProperty $configData "repos" - $repoConfig = Get-JsonProperty $reposConfig $RepoName - $configuredPath = Get-JsonProperty $repoConfig "path" - if ([string]::IsNullOrWhiteSpace([string]$configuredPath)) { - throw "Repository alias has no configured path: $RepoName" - } - return (Get-Item -LiteralPath ([string]$configuredPath) -ErrorAction Stop).FullName -} - -function Get-InvocationArtifactRoot { - $configData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json - $configured = Get-JsonProperty $configData "artifactRoot" - if (-not [string]::IsNullOrWhiteSpace([string]$configured)) { - return [System.IO.Path]::GetFullPath([string]$configured) + if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { + [Console]::Error.WriteLine("Code Intel error: config file does not exist: $configPath") + exit 64 } - return (Get-CodeIntelArtifactRoot -Platform $Platform) -} - -function Publish-AuthoritativeCoreRun { - param([string]$ResolvedRepoPath) - - $artifactRoot = Get-InvocationArtifactRoot - $repoName = Split-Path -Leaf $ResolvedRepoPath - $repoAuthority = Join-Path $artifactRoot $repoName - New-Item -ItemType Directory -Force -Path $repoAuthority | Out-Null - $temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) - $sourceRoot = Join-Path $temporaryRoot ("code-intel-a09-{0}-{1}" -f $PID, [guid]::NewGuid().ToString("N")) - $finalName = (Get-Date -Format "yyyyMMdd-HHmmss-fff") + "-core" - $committed = $false try { - Write-Host "Code intel invoke: authoritative DAG $ResolvedRepoPath" - $dagArguments = @( - "run", "execute", - "--repo", $ResolvedRepoPath, - "--out", $sourceRoot, - "--authority-root", $repoAuthority, - "--final-name", $finalName, - "--profile", "default", - "--doctor-require-repowise", ([bool]$RepowiseDocs).ToString().ToLowerInvariant(), - "--doctor-require-understand", ([bool]$RequireUnderstandGraph).ToString().ToLowerInvariant() - ) - $dagOutput = @(& $rustCli @dagArguments 2>&1) - $dagExitCode = $LASTEXITCODE - $executionResult = try { - ($dagOutput -join [Environment]::NewLine) | ConvertFrom-Json -ErrorAction Stop - } - catch { - $dagOutput | Out-Host - Write-Error "Authoritative execution kernel did not return a valid result: $($_.Exception.Message)" - return $(if ($dagExitCode -ne 0) { $dagExitCode } else { 3 }) - } - if ([string](Get-JsonProperty $executionResult "schema") -ne "code-intel-execution-result.v1") { - Write-Error "Authoritative execution kernel returned an unsupported result schema." - return 3 - } - $executionSchema = Join-Path $root "orchestration/schemas/code-intel-execution-result.v1.schema.json" - if (-not (($executionResult | ConvertTo-Json -Depth 100 -Compress) | - Test-Json -SchemaFile $executionSchema -ErrorAction Stop)) { - Write-Error "Authoritative execution kernel result violates its checked-in schema." - return 3 - } - $dagOutcome = [string](Get-JsonProperty $executionResult "outcome") - if ([string]::IsNullOrWhiteSpace($dagOutcome)) { - Write-Error "Authoritative execution result has no outcome." - return 3 - } - $manifest = Get-JsonProperty $executionResult "manifest" - if ([string](Get-JsonProperty $manifest "outcome") -ne $dagOutcome) { - Write-Error "Authoritative execution result outcome does not match its manifest." - return 3 - } - $reportedExitCode = 0 - if (-not [int]::TryParse( - [string](Get-JsonProperty $executionResult "exitCode"), - [ref]$reportedExitCode - ) -or $reportedExitCode -ne $dagExitCode) { - Write-Error "Authoritative execution result exitCode does not match the process exit code." - return 3 - } - $publication = Get-JsonProperty $executionResult "publication" - $publishedPath = [string](Get-JsonProperty $publication "path") - $expectedPublishedPath = [System.IO.Path]::GetFullPath((Join-Path $repoAuthority $finalName)) - if ([string](Get-JsonProperty $publication "status") -ne "committed" -or - [string](Get-JsonProperty $publication "name") -ne $finalName -or - [string]::IsNullOrWhiteSpace($publishedPath) -or - [System.IO.Path]::GetFullPath($publishedPath) -ne $expectedPublishedPath -or - -not (Test-Path -LiteralPath (Join-Path $publishedPath "run-complete.json") -PathType Leaf)) { - Write-Error "Authoritative execution kernel did not publish a committed run." - return 3 - } - $committed = $true - Write-Host "Code intel invoke: authoritative run committed $repoName/$finalName outcome=$dagOutcome" - if ($dagExitCode -ne 0) { - Write-Warning "Authoritative DAG outcome is $dagOutcome; the committed run is retained as failure evidence." - return $dagExitCode - } - if ($dagOutcome -ne "completed") { - Write-Warning "Authoritative execution kernel returned success for non-completed outcome $dagOutcome." - return 3 - } - return 0 + $configData = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json } - finally { - $resolvedSource = [System.IO.Path]::GetFullPath($sourceRoot) - if ($committed -and $resolvedSource.StartsWith($temporaryRoot, [System.StringComparison]::OrdinalIgnoreCase) -and - (Test-Path -LiteralPath $resolvedSource -PathType Container)) { - Remove-Item -LiteralPath $resolvedSource -Recurse -Force - } - elseif (-not $committed -and (Test-Path -LiteralPath $resolvedSource -PathType Container)) { - Write-Warning "Authoritative DAG staging retained for recovery: $resolvedSource" - } + catch { + [Console]::Error.WriteLine("Code Intel error: config file is not valid JSON: $configPath") + exit 64 } -} - -function Invoke-OneRepo { - param( - [string]$RepoName, - [string]$DirectRepoPath = "" - ) - - $label = if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { $DirectRepoPath } else { $RepoName } - $resolvedRepoPath = Resolve-InvocationRepoPath -RepoName $RepoName -DirectRepoPath $DirectRepoPath - $legacyCode = 0 - if ($LegacyCompatibility) { - Write-Warning "Legacy compatibility pipeline is enabled for $label; its artifacts are non-authoritative." - Write-Host "Code intel invoke: legacy doctor $label" - $global:LASTEXITCODE = 0 - $doctorParams = Get-DoctorParameters -RepoName $RepoName -DirectRepoPath $DirectRepoPath - & $doctor @doctorParams - if ($LASTEXITCODE -ne 0) { - return [pscustomobject][ordered]@{ - repo = $label - ok = $false - stage = "legacy_doctor" - exitCode = $LASTEXITCODE - } - } - - Write-Host "Code intel invoke: legacy compatibility pipeline $label" - $invokeParams = Get-RunnerParameters -RepoName $RepoName -DirectRepoPath $DirectRepoPath - & $runner @invokeParams - $legacyCode = $LASTEXITCODE - } - - $publicationCode = Publish-AuthoritativeCoreRun -ResolvedRepoPath $resolvedRepoPath - $code = if ($legacyCode -ne 0) { $legacyCode } else { $publicationCode } - return [pscustomobject][ordered]@{ - repo = $label - ok = $code -eq 0 - stage = if ($publicationCode -ne 0) { "authoritative_publication" } elseif ($legacyCode -ne 0) { "legacy_compatibility" } else { "authoritative_pipeline" } - exitCode = $code - legacyCompatibility = [bool]$LegacyCompatibility - legacyExitCode = $legacyCode - publicationExitCode = $publicationCode - } -} - -if ($LegacyCompatibility) { - if (-not (Test-Path -LiteralPath $doctor -PathType Leaf)) { - throw "Legacy doctor script missing: $doctor" - } - if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) { - throw "Legacy pipeline script missing: $runner" - } -} -if ($null -eq $rustCli) { - Push-Location $root - try { - & cargo build -p code-intel | Out-Host - } - finally { - Pop-Location + if ($null -eq $configData -or + $null -eq $configData.PSObject.Properties["artifactRoot"]) { + [Console]::Error.WriteLine("Code Intel error: config is missing artifactRoot: $configPath") + exit 64 } - $rustCli = Join-Path $root "target/debug/$binaryName" -} -if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { - throw "Rust integration orchestrator missing. Checked: $($rustCliCandidates -join ', ')" } -Write-Host "Code intel invoke: validate integration orchestration" -Push-Location $root -try { - & $rustCli orchestrate --action Validate | Out-Host - if ($LASTEXITCODE -ne 0) { - throw "Integration orchestration validation failed" - } -} -finally { - Pop-Location -} -if ($ValidateInstallation) { - if ($LegacyCompatibility) { - $doctorCommand = Get-Command -Name $doctor -ErrorAction Stop - $runnerCommand = Get-Command -Name $runner -ErrorAction Stop - if (-not $doctorCommand.Parameters.ContainsKey('RequireRepowise')) { - throw "Legacy doctor does not expose the optional Repowise contract." - } - if (-not $runnerCommand.Parameters.ContainsKey('SkipRepowise')) { - throw "Legacy pipeline runner does not expose the optional Repowise contract." - } +if ([string]::IsNullOrWhiteSpace($RepoPath) -and -not [string]::IsNullOrWhiteSpace($Repo)) { + if ($null -eq $configData.PSObject.Properties["repos"]) { + [Console]::Error.WriteLine("Code Intel error: config is missing repos: $configPath") + exit 64 } - Write-Host "Code intel invoke: installation validation passed; default route is the manifest-bound Rust DAG ($rustCli)" - exit 0 -} - -$targetRepos = @() -if ($All) { - $configData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json - $reposConfig = Get-JsonProperty $configData "repos" - if ($null -eq $reposConfig) { - throw "No repos configured in: $Config" + $entry = $configData.repos.PSObject.Properties[$Repo] + if ($null -eq $entry -or [string]::IsNullOrWhiteSpace([string]$entry.Value.path)) { + [Console]::Error.WriteLine("Code Intel error: repository alias has no configured path: $Repo") + exit 64 } - $targetRepos = @($reposConfig.PSObject.Properties.Name) -} -elseif ($Repos.Count -gt 0) { - $targetRepos = @($Repos) -} -elseif (-not [string]::IsNullOrWhiteSpace($RepoPath)) { - $targetRepos = @([pscustomobject]@{ repo = ""; path = $RepoPath }) -} -elseif (-not [string]::IsNullOrWhiteSpace($Repo)) { - $targetRepos = @($Repo) -} -else { - throw "Specify -Repo , -RepoPath , -Repos or -All." + $RepoPath = [string]$entry.Value.path } -$results = New-Object System.Collections.Generic.List[object] -foreach ($target in $targetRepos) { - if ($target -is [pscustomobject]) { - $results.Add((Invoke-OneRepo $target.repo $target.path)) - } - else { - $results.Add((Invoke-OneRepo $target)) - } +$arguments = @{ + RepoPath = $RepoPath + Mode = $Mode } - -if (-not $NoIndexUpdate -and (Test-Path -LiteralPath $indexer -PathType Leaf)) { - Write-Host "Code intel invoke: update artifact index" - $indexParams = @{} - if (Test-Path -LiteralPath $Config -PathType Leaf) { - $indexConfigData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json - $configuredArtifactRoot = Get-JsonProperty $indexConfigData "artifactRoot" - if (-not [string]::IsNullOrWhiteSpace([string]$configuredArtifactRoot)) { - $indexParams.ArtifactRoot = [string]$configuredArtifactRoot - } +if ($null -ne $configData) { + $configuredRoot = [string]$configData.artifactRoot + if (-not [string]::IsNullOrWhiteSpace($configuredRoot)) { + $arguments.Remaining = @("--artifact-root", $configuredRoot) } - $indexParams.Platform = $Platform - & $indexer @indexParams | Out-Host -} - -Write-Host "Code intel invoke: batch summary" -foreach ($result in $results) { - $mark = if ($result.ok) { "OK" } else { "FAILED" } - Write-Host "$mark $($result.repo) stage=$($result.stage) exit=$($result.exitCode)" -} - -$failed = @($results | Where-Object { -not $_.ok }) -if ($failed.Count -gt 0) { - exit 1 } -exit 0 +& $launcher @arguments +exit $LASTEXITCODE diff --git a/orchestration/integrations.json b/orchestration/integrations.json index f8926b8..bbacf09 100644 --- a/orchestration/integrations.json +++ b/orchestration/integrations.json @@ -10,7 +10,8 @@ "productionRegistry": { "mode": "enforce", "productionFiles": [ - "invoke-code-intel.ps1", + "crates/code-intel-cli/src/doctor_adapter.rs", + "crates/code-intel-cli/src/main.rs", "run-code-intel.ps1" ], "participants": [ @@ -18,10 +19,10 @@ "capabilityId": "doctor", "status": "declared", "callSite": { - "source": "invoke-code-intel.ps1", - "anchor": "$doctor = Join-Path $root \"check-code-intel-tools.ps1\"" + "source": "crates/code-intel-cli/src/doctor_adapter.rs", + "anchor": "let bootstrap = run_bootstrap(&options)?;" }, - "envelope": "legacy-powershell-script-result.v1", + "envelope": "rust-capability-result.v1", "owner": "code-intel-pipeline", "dependencies": [], "effects": ["repo_read", "process_execute"], @@ -161,10 +162,10 @@ "capabilityId": "artifact.index-committed-only", "status": "declared", "callSite": { - "source": "invoke-code-intel.ps1", - "anchor": "$indexer = Join-Path $root \"update-code-intel-index.ps1\"" + "source": "crates/code-intel-cli/src/main.rs", + "anchor": "artifact_index::write_index(&artifact_root.join(\"index.json\"), &index)" }, - "envelope": "legacy-powershell-script-result.v1", + "envelope": "rust-capability-result.v1", "owner": "code-intel-pipeline", "dependencies": ["run.commit"], "effects": ["local_write"], @@ -284,7 +285,7 @@ "id": "doctor.envelope.compat", "version": "1.0.0", "toolchainDigests": [ - "cc37c3df902ba62b699f63545e46b5e73aa517ca3f412ff62f25da6ccc8b1cb0", + "fedeeb8350ae252209f8d8de1a62fbde9148811e7bde29723ea26aacf7ccacbb", "979c365d82ba1a2c16232551d38feb8ebc8cf7a75c13b6c9dfcac2ef2f331266" ] }, diff --git a/orchestration/internalization/linear.json b/orchestration/internalization/linear.json index a035e25..fca3938 100644 --- a/orchestration/internalization/linear.json +++ b/orchestration/internalization/linear.json @@ -1,5 +1,5 @@ { - "schema":"code-intel-internalization-record.v1","id":"internalization.linear-record","projectId":"code-intel-pipeline","subject":{"name":"Linear optional project-state projection","kind":"design_reference","source":{"uri":"https://linear.app; local-policy=docs/project-management-support.md","revision":"service-api-revision-unverified; local-policy-sha256:b82000428768583dd72423763215d72a8d605f69d6a7fd65327d9406a5ebaa44; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c"},"license":{"id":"PROPRIETARY-SERVICE-TERMS-UNVERIFIED","obligations":["no connector install, credential storage, API call, issue creation, or status mutation is authorized by this record","review applicable service/API terms before any separately approved projection"]}}, + "schema":"code-intel-internalization-record.v1","id":"internalization.linear-record","projectId":"code-intel-pipeline","subject":{"name":"Linear optional project-state projection","kind":"design_reference","source":{"uri":"https://linear.app; local-policy=docs/project-management-support.md","revision":"service-api-revision-unverified; local-policy-sha256:e92c70f9357d3d636f2b1c6c33aeb8a7264ab5affe4c0cf2df3b1d241f8a3df8; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c"},"license":{"id":"PROPRIETARY-SERVICE-TERMS-UNVERIFIED","obligations":["no connector install, credential storage, API call, issue creation, or status mutation is authorized by this record","review applicable service/API terms before any separately approved projection"]}}, "authorityRequirements":{"repositoryGovernedAttestation":true}, "adoption":{"rung":"invoke","ownedBoundary":["reference-only optional projection outside scanner runtime","exactly one mutable task-state authority; scanner artifacts remain engineering evidence authority; credentials remain user-scoped"],"necessityEvidence":{"evidenceIds":["local:r23:no-current-use","gap:linear:scope-authority"],"checkedAt":1783900800,"expiresAt":1791676800},"compatibilityEvidence":{"evidenceIds":["local:r23:no-scanner-write","local:r23:single-authority-policy"],"checkedAt":1783900800,"expiresAt":1791676800},"conformanceEvidence":{"evidenceIds":["local:r23:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba","gap:linear:approved-projection-conformance"],"checkedAt":1783900800,"expiresAt":1791676800}}, "economics":{"benefit":{"metric":"current scanner operations requiring Linear","value":0,"unit":"operations"},"cost":{"metric":"unauthorized external mutable surfaces avoided","value":1,"unit":"services"},"benefitEvidence":{"evidenceIds":["local:r23:no-current-use"],"checkedAt":1783900800,"expiresAt":1791676800},"costEvidence":{"evidenceIds":["local:r23:no-credential-storage","gap:linear:service-cost-review"],"checkedAt":1783900800,"expiresAt":1791676800}}, diff --git a/orchestration/internalization/llm-wiki.json b/orchestration/internalization/llm-wiki.json index 3b485c5..d8d2c54 100644 --- a/orchestration/internalization/llm-wiki.json +++ b/orchestration/internalization/llm-wiki.json @@ -10,7 +10,7 @@ "kind": "design_reference", "source": { "uri": "unbound-provider:llm-wiki; local-policy=docs/project-management-support.md", - "revision": "model-and-provider-unbound; local-policy-sha256:b82000428768583dd72423763215d72a8d605f69d6a7fd65327d9406a5ebaa44; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c" + "revision": "model-and-provider-unbound; local-policy-sha256:e92c70f9357d3d636f2b1c6c33aeb8a7264ab5affe4c0cf2df3b1d241f8a3df8; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c" }, "license": { "id": "UNKNOWN-PROVIDER-AND-MODEL-TERMS", diff --git a/orchestration/internalization/obsidian.json b/orchestration/internalization/obsidian.json index cb9afad..97e8799 100644 --- a/orchestration/internalization/obsidian.json +++ b/orchestration/internalization/obsidian.json @@ -10,7 +10,7 @@ "kind": "design_reference", "source": { "uri": "https://obsidian.md; local-policy=docs/project-management-support.md", - "revision": "product-version-unbound; local-policy-sha256:b82000428768583dd72423763215d72a8d605f69d6a7fd65327d9406a5ebaa44; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c" + "revision": "product-version-unbound; local-policy-sha256:e92c70f9357d3d636f2b1c6c33aeb8a7264ab5affe4c0cf2df3b1d241f8a3df8; local-boundary-test-sha256:23fc91489fb33ad7c3092820ae860e7ecf674d55e405e14bc55889b6b9669e5c" }, "license": { "id": "PROPRIETARY-APPLICATION-TERMS-UNVERIFIED", diff --git a/scripts/tests/test-primary-launchers.ps1 b/scripts/tests/test-primary-launchers.ps1 new file mode 100644 index 0000000..a6bd634 --- /dev/null +++ b/scripts/tests/test-primary-launchers.ps1 @@ -0,0 +1,99 @@ +#requires -Version 7.2 + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$root = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "../..")) +$launcher = Join-Path $root "code-intel.ps1" +$legacy = Join-Path $root "invoke-code-intel.ps1" +$missingRepo = Join-Path ([System.IO.Path]::GetTempPath()) "code-intel-launcher-missing-repo" +$missingConfig = Join-Path ([System.IO.Path]::GetTempPath()) "code-intel-launcher-missing-config.json" +$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("code-intel-launcher-{0}-{1}" -f $PID, [guid]::NewGuid().ToString("N")) +$previousDevelopmentOverride = $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV +$previousLocalAppData = $env:LOCALAPPDATA +$previousPath = $env:PATH +$env:CODE_INTEL_ALLOW_UNVERIFIED_DEV = "1" + +try { + $help = @(& pwsh -NoLogo -NoProfile -File $launcher --help 2>&1) + if ($LASTEXITCODE -ne 0 -or ($help -join "`n") -notmatch "code-intel \.") { + throw "recovery launcher did not delegate to the compiled CLI" + } + + $legacyOutput = @(& pwsh -NoLogo -NoProfile -File $legacy -RepoPath $missingRepo -Mode lite 2>&1) + if ($LASTEXITCODE -ne 64 -or + ($legacyOutput -join "`n") -notmatch "repository path is not a directory:" -or + ($legacyOutput -join "`n") -match "validate integration orchestration") { + throw "legacy entry did not quietly forward to the compiled CLI" + } + + $retiredOutput = @(& pwsh -NoLogo -NoProfile -File $legacy -RepoPath $missingRepo -SkipRepowise 2>&1) + if ($LASTEXITCODE -ne 64 -or ($retiredOutput -join "`n") -notmatch "unsupported compatibility option: -SkipRepowise") { + throw "legacy entry silently ignored an unsupported option" + } + + $configOutput = @(& pwsh -NoLogo -NoProfile -File $legacy -RepoPath $missingRepo -Config $missingConfig 2>&1) + if ($LASTEXITCODE -ne 64 -or ($configOutput -join "`n") -notmatch "config file does not exist:") { + throw "legacy entry silently ignored a missing explicit config" + } + + function New-VerifiedReleaseFixture { + param([string]$Version) + + $releaseRoot = Join-Path $testRoot "code-intel/releases/$Version" + $binDir = Join-Path $releaseRoot "bin" + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + $binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } + $binaryPath = Join-Path $binDir $binaryName + Copy-Item -LiteralPath (Join-Path $root "target/debug/$binaryName") -Destination $binaryPath + $file = Get-Item -LiteralPath $binaryPath + $sha256 = (Get-FileHash -LiteralPath $binaryPath -Algorithm SHA256).Hash.ToLowerInvariant() + $relative = "bin/$binaryName" + $canonical = "{`"$relative`":{`"sha256`":`"$sha256`",`"size`":$($file.Length)}}" + $manifestDigest = [Convert]::ToHexString( + [System.Security.Cryptography.SHA256]::HashData([System.Text.Encoding]::UTF8.GetBytes($canonical)) + ).ToLowerInvariant() + [ordered]@{ + schema = "code-intel-skill-release.v2" + tag = $Version + asset = "fixture.zip" + url = "https://github.com/2233admin/code-intel-pipeline/releases/download/$Version/fixture.zip" + sha256 = ("0" * 64) + manifest_sha256 = $manifestDigest + files = [ordered]@{ + ($relative) = [ordered]@{ + sha256 = $sha256 + size = $file.Length + } + } + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $releaseRoot ".code-intel-release.json") -Encoding utf8NoBOM + return $binaryPath + } + + $env:LOCALAPPDATA = $testRoot + $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV = $null + $fallbackBinary = New-VerifiedReleaseFixture -Version "v0.9.0" + $newerBinary = New-VerifiedReleaseFixture -Version "v0.10.0" + [System.IO.File]::AppendAllText($newerBinary, "tampered") + $verifiedHelp = @(& pwsh -NoLogo -NoProfile -File $launcher --help 2>&1) + if ($LASTEXITCODE -ne 0 -or ($verifiedHelp -join "`n") -notmatch "code-intel \.") { + throw "recovery launcher did not reject the tampered newer release and use the verified fallback: $($verifiedHelp -join [Environment]::NewLine)" + } + + $pwshPath = (Get-Process -Id $PID).Path + $env:PATH = "" + $updateOutput = @(& $pwshPath -NoLogo -NoProfile -File $launcher -Update --help 2>&1) + if ($LASTEXITCODE -ne 69 -or ($updateOutput -join "`n") -notmatch "recovery requires Python") { + throw "explicit update failure silently fell back to an installed binary" + } + + Write-Host "Primary launchers: OK" +} +finally { + $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV = $previousDevelopmentOverride + $env:LOCALAPPDATA = $previousLocalAppData + $env:PATH = $previousPath + if (Test-Path -LiteralPath $testRoot -PathType Container) { + Remove-Item -LiteralPath $testRoot -Recurse -Force + } +} diff --git a/scripts/tests/test-stable-wrapper-e2e.ps1 b/scripts/tests/test-stable-wrapper-e2e.ps1 index 805ebb9..54556c5 100644 --- a/scripts/tests/test-stable-wrapper-e2e.ps1 +++ b/scripts/tests/test-stable-wrapper-e2e.ps1 @@ -10,18 +10,18 @@ $temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) $testRoot = Join-Path $temporaryRoot ("code-intel-wrapper-e2e-{0}-{1}" -f $PID, [guid]::NewGuid().ToString("N")) $repo = Join-Path $testRoot "fixture-repo" $artifactRoot = Join-Path $testRoot "artifacts" -$config = Join-Path $testRoot "pipeline.config.json" function Invoke-StableWrapper { - $output = @(& pwsh -NoLogo -NoProfile -File (Join-Path $root "invoke-code-intel.ps1") ` - -RepoPath $repo ` - -Config $config ` - -Mode lite ` - -SkipRepowise ` - -SkipGitHubResearch 2>&1) - return [pscustomobject]@{ - ExitCode = $LASTEXITCODE - Output = ($output -join [Environment]::NewLine) + Push-Location $repo + try { + $output = @(& $rustCli --artifact-root $artifactRoot 2>&1) + return [pscustomobject]@{ + ExitCode = $LASTEXITCODE + Output = ($output -join [Environment]::NewLine) + } + } + finally { + Pop-Location } } @@ -49,19 +49,6 @@ no_god_files = false & git -C $repo -c user.name=CodeIntelTest -c user.email=code-intel-test@example.invalid commit --quiet -m baseline if ($LASTEXITCODE -ne 0) { throw "fixture Git commit failed" } - $configuration = [ordered]@{ - artifactRoot = $artifactRoot - repowiseWorkspaceRoot = "" - codeEvidence = [ordered]@{ - enabled = $false - nativeMinimal = $false - adapters = [ordered]@{} - } - inventoryExclude = @() - repos = [ordered]@{} - } - $configuration | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $config -Encoding utf8NoBOM - $success = Invoke-StableWrapper if ($success.ExitCode -ne 0) { $failureDetails = "" @@ -86,6 +73,11 @@ no_god_files = false if ($success.Output -match "legacy compatibility pipeline") { throw "stable wrapper default route still executed the legacy pipeline" } + if ($success.Output -notmatch "\[PASS\]" -or + $success.Output -notmatch "Outcome:\s+completed" -or + $success.Output -notmatch "Run evidence:") { + throw "stable wrapper did not print the user-facing success summary:`n$($success.Output)" + } $authority = Join-Path $artifactRoot "fixture-repo" $completedRun = Get-ChildItem -LiteralPath $authority -Directory | Where-Object { $_.Name -like "*-core" } | @@ -164,8 +156,19 @@ no_god_files = false if ($failedDiagnostic.Count -ne 1 -or [string]$failedDiagnostic[0].reason -notmatch "process_failed") { throw "failed audit run was not classified outside the authoritative index" } - if ($failure.Output -notmatch "authoritative_publication" -or $failure.Output -notmatch "FAILED") { - throw "stable wrapper did not expose the authoritative publication failure in its batch summary" + if ($failure.Output -notmatch "\[FAIL\]" -or + $failure.Output -notmatch "Outcome:\s+process_failed" -or + $failure.Output -notmatch "Cause:\s+evidence\.native-code" -or + $failure.Output -notmatch "Run evidence:") { + throw "stable wrapper did not expose the authoritative failure in its user-facing summary:`n$($failure.Output)" + } + + $missingRepo = Join-Path $testRoot "missing-repo" + $invalidOutput = @(& $rustCli $missingRepo --artifact-root $artifactRoot 2>&1) + if ($LASTEXITCODE -eq 0 -or + ($invalidOutput -join [Environment]::NewLine) -notmatch "repository path is not a directory:" -or + ($invalidOutput -join [Environment]::NewLine) -match "main\.rs:\d+") { + throw "primary entry did not return a concise invalid-path error:`n$($invalidOutput -join [Environment]::NewLine)" } Write-Host "Stable wrapper E2E: OK" diff --git a/skills/code-intel-pipeline/SKILL.md b/skills/code-intel-pipeline/SKILL.md index 16ab9b1..de6f7e0 100644 --- a/skills/code-intel-pipeline/SKILL.md +++ b/skills/code-intel-pipeline/SKILL.md @@ -10,12 +10,12 @@ skill. ## Resolve the installation -1. Check whether `CODE_INTEL_HOME` points to a directory containing - `invoke-code-intel.ps1`. -2. Reuse that installation when it is valid. +1. Check whether `code-intel --help` succeeds. +2. Reuse that installation when it is valid. Use `code-intel.ps1` only to repair a missing or + invalid installation. 3. Bootstrap only when the user requested installation or the task explicitly requires the missing pipeline. -4. From this skill directory, inspect the fixed `v0.3.0` stable release plan: +4. From this skill directory, inspect the latest published stable release plan: ```powershell python scripts/bootstrap.py --repo-path "" --dry-run --json @@ -29,11 +29,11 @@ python scripts/bootstrap.py --repo-path "" --dry-run --json python scripts/bootstrap.py --repo-path "" --json ``` -The default is deliberately pinned to `v0.3.0`; it does not drift when another release becomes -latest. Add `--version ` only for a requested version. Add `--channel prerelease` only when the -user explicitly requests the latest published prerelease. Add `--install-missing` only when the -user authorizes installing third-party dependencies. Never put provider keys in commands, -repository files, artifacts, or Skill resources. +The default follows GitHub's latest published stable release. Add `--version ` only for a +requested version. Add `--channel prerelease` only when the user explicitly requests the latest +published prerelease. Add `--install-missing` only when the user authorizes installing third-party +dependencies. Never put provider keys in commands, repository files, artifacts, or Skill +resources. The bootstrap script supports the currently published Windows release package. Stop with the reported platform error on unsupported systems instead of substituting an unverified source @@ -41,21 +41,14 @@ archive. ## Run the pipeline -Run the doctor before analysis: +Run the compiled Primary Operator Entry: ```powershell -& "$env:CODE_INTEL_HOME/check-code-intel-tools.ps1" -RepoPath "" -Json +code-intel "" ``` -Run the stable wrapper: - -```powershell -& "$env:CODE_INTEL_HOME/invoke-code-intel.ps1" -RepoPath "" -Mode normal -``` - -Use `-Mode lite` for a cheap environment and inventory pass. Use `-Mode full` only when a fresh, -richer graph is required. Use the raw `run-code-intel.ps1` entry point only for a flag unavailable -on the stable wrapper. +Use `--mode lite` for local-only core evidence. Use `--mode full` only when every optional provider +must be present. Do not call the legacy PowerShell pipeline. Read generated artifacts in this order: diff --git a/skills/code-intel-pipeline/scripts/bootstrap.py b/skills/code-intel-pipeline/scripts/bootstrap.py index 593456e..63ed4c3 100644 --- a/skills/code-intel-pipeline/scripts/bootstrap.py +++ b/skills/code-intel-pipeline/scripts/bootstrap.py @@ -25,7 +25,6 @@ REPOSITORY = "2233admin/code-intel-pipeline" API_ROOT = f"https://api.github.com/repos/{REPOSITORY}" USER_AGENT = "code-intel-pipeline-skill-bootstrap/1" -DEFAULT_STABLE_VERSION = "v0.3.0" SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") TAG_PATTERN = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]*$") WINDOWS_RESERVED_NAMES = { @@ -93,8 +92,6 @@ def normalize_tag(version: str) -> str: def resolve_version(version: str | None, channel: str) -> str | None: if version: return normalize_tag(version) - if channel == "stable": - return DEFAULT_STABLE_VERSION return None @@ -335,6 +332,7 @@ def find_payload_root(extracted_root: Path) -> Path: required = ( "install-code-intel-pipeline.ps1", "check-code-intel-tools.ps1", + "code-intel.ps1", "invoke-code-intel.ps1", ) missing = [name for name in required if not (payload / name).is_file()] @@ -426,24 +424,9 @@ def install_release(asset: dict[str, str], install_root: Path) -> tuple[Path, st ) try: metadata = json.loads(marker.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - raise BootstrapError( - f"Existing release marker is unreadable: {marker}" - ) from error - if not isinstance(metadata, dict): - raise BootstrapError(f"Existing release marker is invalid: {marker}") - expected_marker = { - "tag": tag, - "asset": asset["name"], - "url": asset["url"], - "sha256": asset["sha256"], - } - if any(metadata.get(key) != value for key, value in expected_marker.items()): - raise BootstrapError( - f"Existing release marker does not match GitHub metadata: {destination}" - ) - find_payload_root(destination) - existing_metadata = metadata + existing_metadata = metadata if isinstance(metadata, dict) else {} + except (OSError, json.JSONDecodeError): + existing_metadata = {} install_root.mkdir(parents=True, exist_ok=True) staging_root = install_root / f".staging-{tag}-{uuid.uuid4().hex}" @@ -461,32 +444,42 @@ def install_release(asset: dict[str, str], install_root: Path) -> tuple[Path, st payload = find_payload_root(staging_root) verified_manifest = payload_manifest(payload) verified_manifest_digest = manifest_digest(verified_manifest) - if existing_metadata is not None: - installed_manifest = payload_manifest(destination) - if installed_manifest != verified_manifest: - raise BootstrapError( - f"Existing release files do not match the verified GitHub asset: {destination}" - ) - return destination, "already_installed" - + verified_metadata = { + "schema": "code-intel-skill-release.v2", + "tag": tag, + "asset": asset["name"], + "url": asset["url"], + "sha256": asset["sha256"], + "manifest_sha256": verified_manifest_digest, + "files": verified_manifest, + } payload_marker = payload / RELEASE_MARKER payload_marker.write_text( - json.dumps( - { - "schema": "code-intel-skill-release.v2", - "tag": tag, - "asset": asset["name"], - "url": asset["url"], - "sha256": asset["sha256"], - "manifest_sha256": verified_manifest_digest, - "files": verified_manifest, - }, - indent=2, - sort_keys=True, - ) + json.dumps(verified_metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) + if existing_metadata is not None: + try: + find_payload_root(destination) + installed_manifest = payload_manifest(destination) + except (BootstrapError, OSError): + installed_manifest = {} + if ( + existing_metadata == verified_metadata + and installed_manifest == verified_manifest + ): + return destination, "already_installed" + replaced = install_root / f".replaced-{tag}-{uuid.uuid4().hex}" + destination.replace(replaced) + try: + payload.replace(destination) + except BaseException: + replaced.replace(destination) + raise + shutil.rmtree(replaced) + return destination, "repaired" + payload.replace(destination) return destination, "installed" finally: @@ -509,10 +502,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: "--channel", choices=("stable", "prerelease"), default="stable", - help=( - "Release channel when --version is omitted. Stable uses the Skill's " - f"pinned {DEFAULT_STABLE_VERSION} release." - ), + help="Release channel when --version is omitted. Stable uses GitHub's latest release.", ) parser.add_argument( "--install-root", @@ -560,8 +550,6 @@ def run(args: argparse.Namespace) -> dict[str, Any]: "version_source": ( "explicit" if args.version - else "pinned_stable" - if args.channel == "stable" else "channel" ), "asset": asset["name"], diff --git a/tests/test_repository_layout.py b/tests/test_repository_layout.py index 51b1f29..6182894 100644 --- a/tests/test_repository_layout.py +++ b/tests/test_repository_layout.py @@ -11,6 +11,7 @@ "check-code-intel-tools.ps1", "Find-CodeIntelProjects.ps1", "install-code-intel-pipeline.ps1", + "code-intel.ps1", "invoke-code-intel.ps1", "Invoke-SentruxAgentTool.ps1", "run-code-intel.ps1", diff --git a/tests/test_skill_package.py b/tests/test_skill_package.py index 823ec0c..aa3459a 100644 --- a/tests/test_skill_package.py +++ b/tests/test_skill_package.py @@ -2,6 +2,7 @@ import hashlib import importlib.util +import json import shutil import tempfile import unittest @@ -33,16 +34,16 @@ def write_release_archive(path: Path, *, installer: str = "Write-Output 'ok'\n") for name, content in { "install-code-intel-pipeline.ps1": installer, "check-code-intel-tools.ps1": "Write-Output 'doctor'\n", + "code-intel.ps1": "Write-Output 'launch'\n", "invoke-code-intel.ps1": "Write-Output 'invoke'\n", }.items(): handle.writestr(f"code-intel-pipeline/{name}", content) class SkillPackageTests(unittest.TestCase): - def test_defaults_to_fixed_v030_stable_release(self) -> None: + def test_defaults_to_latest_published_stable_release(self) -> None: bootstrap = load_bootstrap_module() - self.assertEqual(bootstrap.DEFAULT_STABLE_VERSION, "v0.3.0") - self.assertEqual(bootstrap.resolve_version(None, "stable"), "v0.3.0") + self.assertIsNone(bootstrap.resolve_version(None, "stable")) self.assertEqual(bootstrap.resolve_version("0.2.0", "stable"), "v0.2.0") self.assertIsNone(bootstrap.resolve_version(None, "prerelease")) @@ -254,8 +255,38 @@ def copy_archive(_url: str, destination: Path) -> None: (destination / "install-code-intel-pipeline.ps1").write_text( "tampered\n", encoding="utf-8" ) - with self.assertRaises(bootstrap.BootstrapError): - bootstrap.install_release(asset, temp_path / "installs") + repaired_destination, repaired_status = bootstrap.install_release( + asset, temp_path / "installs" + ) + self.assertEqual(repaired_destination, destination) + self.assertEqual(repaired_status, "repaired") + self.assertNotEqual( + (destination / "install-code-intel-pipeline.ps1").read_text( + encoding="utf-8" + ), + "tampered\n", + ) + + marker_data = json.loads(marker.read_text(encoding="utf-8")) + marker_data["manifest_sha256"] = "0" * 64 + marker.write_text(json.dumps(marker_data), encoding="utf-8") + _, marker_status = bootstrap.install_release( + asset, temp_path / "installs" + ) + self.assertEqual(marker_status, "repaired") + self.assertNotEqual( + json.loads(marker.read_text(encoding="utf-8"))[ + "manifest_sha256" + ], + "0" * 64, + ) + + (destination / "code-intel.ps1").unlink() + _, missing_file_status = bootstrap.install_release( + asset, temp_path / "installs" + ) + self.assertEqual(missing_file_status, "repaired") + self.assertTrue((destination / "code-intel.ps1").is_file()) if __name__ == "__main__": diff --git a/tools/Test-BetaPackage.ps1 b/tools/Test-BetaPackage.ps1 index ec05d3f..f31667f 100644 --- a/tools/Test-BetaPackage.ps1 +++ b/tools/Test-BetaPackage.ps1 @@ -127,11 +127,13 @@ try { Assert-Condition ($LASTEXITCODE -eq 0) "Packaged code-intel.exe --help failed." Assert-Condition ($helpText -match 'code-intel') "Packaged CLI help output is unexpected." - $invokeWrapper = Join-Path $packageRoot "invoke-code-intel.ps1" - Assert-Condition (Test-Path -LiteralPath $invokeWrapper -PathType Leaf) "Package is missing invoke-code-intel.ps1." + $invokeWrapper = Join-Path $packageRoot "code-intel.ps1" + Assert-Condition (Test-Path -LiteralPath $invokeWrapper -PathType Leaf) "Package is missing code-intel.ps1." $pwshExecutable = (Get-Process -Id $PID).Path $originalPath = $env:PATH + $originalDevelopmentOverride = $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV try { + $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV = '1' $pathSeparator = [System.IO.Path]::PathSeparator $env:PATH = (@($originalPath.Split($pathSeparator) | Where-Object { $entry = $_ @@ -142,14 +144,14 @@ try { }) -join $pathSeparator) Assert-Condition ($null -eq (Get-Command cargo -ErrorAction SilentlyContinue)) "Package smoke must run without Cargo on PATH." Assert-Condition ($null -eq (Get-Command repowise -ErrorAction SilentlyContinue)) "Package smoke must run without Repowise on PATH." - $wrapperSmoke = (& $pwshExecutable -NoProfile -File $invokeWrapper -ValidateInstallation 2>&1) -join "`n" + $wrapperSmoke = (& $pwshExecutable -NoProfile -File $invokeWrapper --help 2>&1) -join "`n" } finally { $env:PATH = $originalPath + $env:CODE_INTEL_ALLOW_UNVERIFIED_DEV = $originalDevelopmentOverride } - Assert-Condition ($LASTEXITCODE -eq 0) "Packaged invoke-code-intel.ps1 -ValidateInstallation failed: $wrapperSmoke" - Assert-Condition ($wrapperSmoke -match 'installation validation passed') "Packaged wrapper smoke output is unexpected." - Assert-Condition ($wrapperSmoke -match 'default route is the manifest-bound Rust DAG') "Packaged wrapper did not validate the authoritative Rust DAG route." + Assert-Condition ($LASTEXITCODE -eq 0) "Packaged code-intel.ps1 --help failed: $wrapperSmoke" + Assert-Condition ($wrapperSmoke -match 'code-intel \.') "Packaged recovery launcher did not delegate to the compiled CLI." $result = [pscustomobject][ordered]@{ ok = $true