diff --git a/.specify/feature.json b/.specify/feature.json index d11bc89..f3600ea 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "docs/specs/082-syft-sharded-sbom-plan" + "feature_directory": "docs/specs/083-tool-acquisition-guidance" } diff --git a/AGENTS.md b/AGENTS.md index c77bf9e..79553db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,5 +202,5 @@ go run ./cmd/portolan scan --help For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan: -`docs/specs/082-syft-sharded-sbom-plan/plan.md` +`docs/specs/083-tool-acquisition-guidance/plan.md` diff --git a/docs/product-backlog.md b/docs/product-backlog.md index c30b380..06753ed 100644 --- a/docs/product-backlog.md +++ b/docs/product-backlog.md @@ -164,6 +164,7 @@ fixtures are preflight evidence only. | P6-080 | `docs/specs/080-clean-start-artifact-guard/` | Context packs and acceptance guidance make the current artifact boundary explicit so Cursor/agent stress lanes do not mix stale `.portolan/stress`, root-level `run`, or unrelated generated outputs into clean-start evidence. | Ready-for-review PR #58; post-Cursor local baseline, fresh Bigtop context smoke, final Cursor Composer 2.5 clean-start stress, and three final assessed non-GPT review lanes verified; merge approval `not_assessed` | | P6-081 | `docs/specs/081-maven-sharded-producer-plan/` | Context packs emit repository-sharded Maven/CycloneDX next actions for multi-repo JVM landscapes so agents do not treat one sample `pom.xml` as a landscape rollout plan. | Ready-for-review PR #59; local baseline, fresh Bigtop context smoke, Cursor Composer 2.5 stress, three assessed non-GPT review lanes, and GitHub checks verified. Maven execution, dependency evidence, JVM relationship claims, GitHub review approval, and merge approval remain `not_assessed` | | P6-082 | `docs/specs/082-syft-sharded-sbom-plan/` | Context packs emit repository-sharded Syft/CycloneDX SBOM next actions for multi-repo landscapes so component/dependency evidence can be acquired incrementally without full-root SBOM scans. | Ready-for-review PR #60; local baseline, fresh Bigtop context smoke, Cursor Composer 2.5 stress, three assessed non-GPT review lanes, and GitHub checks verified. Syft execution, component inventory, dependency evidence, GitHub review approval, and merge approval remain `not_assessed` | +| P6-083 | `docs/specs/083-tool-acquisition-guidance/` | Context packs make tool acquisition guidance explicit and stack-agnostic: agents can pull in the right local producer tools without treating Portolan as a PHP/JVM/Gradle adapter stack. | Ready-for-review PR #61; local baseline, fresh Bigtop context smoke, Cursor Composer 2.5 stress, integrated PR #57-#61 stack-agnostic stress, three assessed non-GPT review lanes, and GitHub checks verified. Native producer execution, tool install/acquisition, component inventory, dependency relationships, duplication metrics, runtime topology, GitHub review approval, and merge approval remain `not_assessed` | ## Backlog Rules diff --git a/docs/specs/083-tool-acquisition-guidance/plan.md b/docs/specs/083-tool-acquisition-guidance/plan.md new file mode 100644 index 0000000..aca0a9b --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/plan.md @@ -0,0 +1,54 @@ +# Implementation Plan: Tool Acquisition Guidance + +**Branch**: `codex/083-tool-acquisition-guidance` + +**Date**: 2026-06-02 + +**Spec**: `docs/specs/083-tool-acquisition-guidance/spec.md` + +## Summary + +Clarify the producer-planning surface so Portolan stays stack-agnostic while +still telling agents which local tools to pull in for missing evidence families. +The slice adds generic acquisition/risk guidance around existing OSS tool plans; +it does not add a new scanner, install tools, run native producers, or create a +language-specific adapter. + +## Decision Gate + +- **Simpler/Faster**: Leave the existing `oss-plan.json` text as-is. Rejected + because Cursor and operator discussion showed the guidance can be misread as + incremental stack-specific adapter work. +- **Blocking Edge Cases**: External tools can install packages, hit networks, + write caches, mutate targets, or expose private dependency coordinates. + Therefore acquisition guidance is descriptive and approval-gated; no native + command is run by Portolan. +- **Existing Open Source**: Continue composing mature OSS/native producer tools + such as Syft, CycloneDX plugins, jscpd, and Semgrep. Portolan owns the + evidence contract and normalization boundary, not their scanner logic. + +## Technical Context + +**Language/Version**: Go. + +**Primary Dependencies**: Standard library only. No new dependency. + +**Storage**: Local context artifacts and SpecKit docs. + +**Testing**: Focused `internal/contextprep` test, then full baseline. + +**Constraints**: Local-first/read-only defaults. No installs, network access, +producer execution, daemon behavior, credentials, or target mutation. + +## Verification + +```bash +go test ./internal/contextprep +go test ./... +go vet ./... +jq empty schema/*.json +git diff --check +``` + +Fresh Bigtop smoke and Cursor Composer 2.5 stress must use a clean context path +and must not run native producers. diff --git a/docs/specs/083-tool-acquisition-guidance/research.md b/docs/specs/083-tool-acquisition-guidance/research.md new file mode 100644 index 0000000..5e21bde --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/research.md @@ -0,0 +1,25 @@ +# Research: Tool Acquisition Guidance + +## Decision: Generic acquisition guidance over per-stack adapters + +- **Decision**: Keep producer planning organized by evidence family and local + tool candidate, not by programming-language adapter. +- **Rationale**: The operator needs practical next actions such as "pull in a + local SBOM producer" or "run a duplication tool", but Portolan must not become + a JVM/PHP/Gradle scanner. +- **Rejected alternative**: Add a Gradle-specific slice after integrated stress. + Rejected because it turns a residual tool gap into stack ownership. + +## OSS posture + +- Mature OSS/native tools remain the right acquisition targets when they + produce local files that Portolan can normalize. +- Tool recommendations are options, not evidence. +- Missing or unrun candidate tools remain `not_assessed`. + +## Risk posture + +- Tool acquisition can involve network access, cache writes, project mutation, + dependency coordinate exposure, or runtime side effects. +- Portolan must surface those risks before suggesting any command as a next + action. diff --git a/docs/specs/083-tool-acquisition-guidance/reviews/bigtop-context-and-cursor-stress-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/reviews/bigtop-context-and-cursor-stress-2026-06-02.md new file mode 100644 index 0000000..6f0abc8 --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/reviews/bigtop-context-and-cursor-stress-2026-06-02.md @@ -0,0 +1,69 @@ +# Bigtop Context Smoke And Cursor Stress + +Date: 2026-06-02 + +Spec: `docs/specs/083-tool-acquisition-guidance/` + +## Fresh Context Smoke + +Command: + +```bash +go run ./cmd/portolan context prepare \ + --root /home/fall_out_bug/projects/bigtop-landscape \ + --out /home/fall_out_bug/projects/bigtop-landscape/.portolan/stress/20260602-083-tool-acquisition-guidance/context \ + --profile cursor \ + --force +``` + +verified: + +- Context pack was written under: + `/home/fall_out_bug/projects/bigtop-landscape/.portolan/stress/20260602-083-tool-acquisition-guidance/context` +- `context/tool-outputs` is absent; no native producer was executed. +- JSON validation passed for `repos.json`, `tool-registry.json`, + `oss-plan.json`, and `gaps.jsonl`. +- Selected tool acquisition states: + - `cyclonedx`: installed / `not_assessed` + - `jscpd`: installed / `not_assessed` + - `maven-cyclonedx`: installed / `not_assessed` + - `gradle-cyclonedx`: installed but requires local evaluation / + `not_assessed` + +not_assessed: + +- Actual Syft, jscpd, Maven, Gradle, Semgrep, Docker, or producer execution. +- Actual producer output validity. +- Component inventory, dependency relationships, duplication metrics, and + runtime topology. + +## Cursor Composer 2.5 Stress + +Command: + +```bash +cursor-agent --print --mode ask --model composer-2.5 --trust "$(cat docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-prompt-2026-06-02.md)" +``` + +verified: + +- `forbidden_read: false` +- `artifacts_read_count: 8` +- `acquisition_guidance_present: true` +- `acquisition_tool_count: 5` +- `stack_specific_adapter_requested: false` +- `candidate_tools_as_evidence: false` +- `installed_tools_claimed_as_supported_evidence: false` +- `approval_boundary_present: true` +- `risks_named: true` +- `evidence_until_output: not_assessed` +- `component_dependency_claimable: false` +- `runtime_topology_claimable: false` +- `verdict: pass` + +disposition: + +- Accepted as a passing stress lane for the tool acquisition guidance + correction. +- Residual clean-start producer-run handling is owned by pending PR #58 and was + already verified in the integrated PR #57-#60 scratch stress. diff --git a/docs/specs/083-tool-acquisition-guidance/reviews/pr-readiness-closeout-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/reviews/pr-readiness-closeout-2026-06-02.md new file mode 100644 index 0000000..7d98e31 --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/reviews/pr-readiness-closeout-2026-06-02.md @@ -0,0 +1,91 @@ +# PR Readiness Closeout + +Date: 2026-06-02 + +Spec: `docs/specs/083-tool-acquisition-guidance/` + +PR: https://github.com/fcon-tech/portolan/pull/61 + +Branch: `codex/083-tool-acquisition-guidance` + +Head at PR creation: `da2b2072b1c7decdc7954327d8f0a29b3bc4fbe6` + +## Implementation State + +verified: + +- `oss-plan.json` tool plans include stack-agnostic acquisition guidance. +- Candidate tools are represented as native producer options, not + Portolan-owned stack adapters. +- Tool availability remains separate from evidence. +- `evidence_until_output` remains `not_assessed` until local output is produced + and re-ingested. +- Answer and query guidance reject defaulting to PHP/JVM/Scala/Gradle adapter + requests. + +not_assessed: + +- Actual native producer execution. +- Actual tool install/acquisition. +- Component inventory, dependency relationships, duplication metrics, and + runtime topology. + +## Local Verification + +verified: + +- `go test ./internal/contextprep` +- `go test ./...` +- `go vet ./...` +- `jq empty schema/*.json` +- `git diff --check` +- Fresh Bigtop context smoke. +- Cursor Agent `composer-2.5` bounded tool-acquisition stress. + +## Review Evidence + +verified: + +- Requirements/product-vision drift review recorded. +- Cursor Composer 2.5 stress recorded. +- Three assessed non-GPT review lanes recorded: + - `openrouter/moonshotai/kimi-k2.6` + - `openrouter/deepseek/deepseek-v4-pro` + - `openrouter/qwen/qwen3-coder` +- Degraded MiMo/MiniMax attempts recorded as non-counting evidence. + +## PR State + +verified at PR creation: + +- PR #61 exists. +- PR is open. +- PR is not draft. +- PR head branch is `codex/083-tool-acquisition-guidance`. + +not_assessed at PR creation: + +- GitHub checks were queued. +- GitHub review approval absent/not_assessed. +- Ready-to-merge approval absent/not_assessed. + +verified after current-head refresh: + +- Current PR head: `bd6e15fad6981b15c14975e59a828c1f364da5f3` +- PR is open and not draft. +- `mergeStateStatus=CLEAN`. +- Current GitHub checks: all reported checks completed successfully. +- Integrated PR #57-#61 stack-agnostic stress is recorded under + `docs/specs/083-tool-acquisition-guidance/reviews/integrated-stack-agnostic-navigation-stress-2026-06-02.md` + on the scratch integration branch. + +## Readiness + +- Ready-for-review PR: yes. +- Ready-to-merge PR: no. + +Stop reason: + +- PR is ready for review. +- Do not merge without explicit user approval and current merge-state/check + verification. diff --git a/docs/specs/083-tool-acquisition-guidance/reviews/requirements-product-vision-drift-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/reviews/requirements-product-vision-drift-2026-06-02.md new file mode 100644 index 0000000..c3a57c8 --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/reviews/requirements-product-vision-drift-2026-06-02.md @@ -0,0 +1,42 @@ +# Requirements And Product-Vision Drift Review + +Date: 2026-06-02 + +Spec: `docs/specs/083-tool-acquisition-guidance/` + +## Inputs + +- Integrated Cursor Composer 2.5 stress for PR #57-#60. +- User correction: Portolan must be stack-agnostic, but should help pull in the + right tools. +- Portolan product boundary in `AGENTS.md`. +- Constitution local-first, evidence-state, and OSS-composition principles. + +## Drift Assessment + +Requirements: + +- Aligned. The slice improves the navigation surface for missing evidence + families without adding a stack-specific scanner or adapter. + +Product boundary: + +- Aligned. Portolan remains a read-only local discovery substrate and + normalizer for local producer evidence. +- The slice explicitly rejects defaulting to PHP/JVM/Gradle adapters. +- Tool candidates remain options for local acquisition, not observed evidence. + +Evidence semantics: + +- Aligned. Candidate tools, install suggestions, and approval-gated commands + remain `not_assessed` until local output exists and is re-ingested. + +Open-source posture: + +- Aligned. Mature OSS/native producer tools stay outside Portolan ownership; + Portolan documents how to acquire and normalize their outputs safely. + +Decision: + +- Proceed with a focused context guidance correction. +- Do not create a Gradle-specific implementation slice. diff --git a/docs/specs/083-tool-acquisition-guidance/reviews/slice-review-disposition-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/reviews/slice-review-disposition-2026-06-02.md new file mode 100644 index 0000000..49eafeb --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/reviews/slice-review-disposition-2026-06-02.md @@ -0,0 +1,108 @@ +# Slice Review Disposition + +Date: 2026-06-02 + +Spec: `docs/specs/083-tool-acquisition-guidance/` + +## Independent Lanes + +| Lane | Model | State | Verdict | +| --- | --- | --- | --- | +| Kimi | `openrouter/moonshotai/kimi-k2.6` | assessed with degraded process text | Found no stack-boundary or evidence-state blockers. Raised schema-contract concern that was locally checked. | +| DeepSeek | `openrouter/deepseek/deepseek-v4-pro` | assessed | Stack-agnostic boundary, evidence honesty, and local-first behavior pass. Raised backward-compat concern for old `oss-plan.json`. | +| Qwen | `openrouter/qwen/qwen3-coder` | assessed | No actionable findings. | + +Degraded lanes: + +- `openrouter/xiaomi/mimo-v2.5-pro`: returned a tool-call request instead of a + review verdict; `not_assessed`. +- `minimax/MiniMax-M2.7`: provider id returned 404; `failed`. +- `openrouter/minimax/minimax-m2.7`: endpoint rejected no-reasoning mode; + `failed`. + +## Findings + +### F1: `oss-plan.json` schema-contract drift + +Sources: Kimi, DeepSeek + +Classification: major from reviewers; rejected as current blocker after local +verification. + +Local verification: + +- `schema/` exists, but there is no committed `schema/oss-plan.json` contract in + current `origin/main`. +- Current verification command `jq empty schema/*.json` checks committed schema + syntax only; it does not validate `oss-plan.json` output shape. +- Current code writes `oss-plan.json` but does not consume older context packs + as a public input contract. + +Disposition: + +- Rejected as a blocker for this slice. +- Recorded as a future contract-hardening option if `oss-plan.json` becomes a + formal schema-validated artifact. +- The field is additive and does not remove existing JSON fields. + +### F2: Older `oss-plan.json` without `acquisition` may unmarshal with empty fields + +Source: DeepSeek + +Classification: major from reviewer; rejected as current blocker. + +Rationale: + +- No current Portolan code path reads old `oss-plan.json` as an input contract. +- The new field is generated for all current tool plans. +- Downstream consumers that ignore unknown fields remain compatible; consumers + requiring a formal schema need a future `oss-plan` schema/contract slice. + +### F3: Availability-string coverage could be stricter + +Source: DeepSeek + +Classification: minor + +Disposition: non-blocking. + +Rationale: + +- Focused test already verifies Gradle's local-evaluation next action and + acquisition presence. +- Availability exact strings are less important than evidence-state and + approval boundaries. + +### F4: Gradle/Semgrep remain partial next actions + +Sources: Cursor, DeepSeek + +Classification: residual, not a 083 defect. + +Disposition: accepted as residual. + +Rationale: + +- 083 makes acquisition guidance explicit; it does not add new bounded native + commands. +- `not_assessed` plus local-evaluation guidance is the desired boundary for + tools without safe output-bounded commands. + +## Verification + +verified: + +- `go test ./internal/contextprep` +- `go test ./...` +- `go vet ./...` +- `jq empty schema/*.json` +- `git diff --check` +- Fresh Bigtop context smoke. +- Cursor Agent `composer-2.5` bounded tool-acquisition stress. + +not_assessed: + +- Actual native producer execution. +- Actual tool install/acquisition. +- Formal JSON Schema validation for `oss-plan.json`, because no such committed + schema exists in the current repository. diff --git a/docs/specs/083-tool-acquisition-guidance/spec.md b/docs/specs/083-tool-acquisition-guidance/spec.md new file mode 100644 index 0000000..c470867 --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/spec.md @@ -0,0 +1,60 @@ +# Feature Specification: Tool Acquisition Guidance + +**Feature Branch**: `codex/083-tool-acquisition-guidance` + +**Created**: 2026-06-02 + +**Status**: Ready-for-review PR #61; local baseline, fresh Bigtop context +smoke, Cursor Composer 2.5 stress, integrated PR #57-#61 stack-agnostic stress, +three assessed non-GPT review lanes, and GitHub checks verified; merge approval +`not_assessed` + +**Input**: Integrated Cursor Composer 2.5 stress for PR #57-#60 judged the +context adequate as a pre-UX navigation harness, but the follow-up discussion +clarified the product boundary: Portolan must stay stack-agnostic while still +helping the operator pull in the right local producer tools. + +## User Scenarios & Testing + +### User Story 1 - Stack-Agnostic Tool Acquisition (Priority: P1) + +An agent reading a context pack sees missing evidence families, candidate local +producer tools, availability, approval/risk boundaries, and next actions +without interpreting them as Portolan-owned language adapters. + +**Independent Test**: A fixture with Maven/Gradle manifests and local tools +shows `oss-plan.json` acquisition guidance for native producer candidates while +the answer contract says candidate tools are not Portolan adapters. + +### User Story 2 - Evidence Honesty (Priority: P1) + +An agent can recommend installing or running a local tool only as a way to +produce future local evidence; it must keep the evidence family `not_assessed` +until output exists and is re-ingested. + +**Independent Test**: Context guidance includes `not_assessed` boundaries for +candidate tools and explicitly requires context refresh after approved output. + +## Requirements + +- **FR-001**: `oss-plan.json` MUST describe producer candidates as tool + acquisition guidance, not stack-specific Portolan adapters. +- **FR-002**: Candidate tools MUST expose availability and a next acquisition + action such as run approved local command, install/evaluate locally, or supply + local output. +- **FR-003**: Candidate tools MUST expose risk boundaries for network, target + mutation, cache writes, and approval where relevant. +- **FR-004**: Guidance MUST preserve `not_assessed` evidence state until local + output exists and `context prepare --force` re-ingests it. +- **FR-005**: Answer guidance MUST explicitly reject defaulting to + Portolan-owned PHP/JVM/Scala/Gradle adapters for language coverage gaps. + +## Success Criteria + +- **SC-001**: Focused contextprep tests verify acquisition guidance in + `oss-plan.json`, `answer-contract.md`, and `query-plan.md`. +- **SC-002**: Fresh Bigtop context shows tool acquisition guidance without + running native producers. +- **SC-003**: Cursor Composer 2.5 confirms the context remains stack-agnostic + while still surfacing useful local tools to pull in. +- **SC-004**: Baseline checks pass. diff --git a/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-output-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-output-2026-06-02.md new file mode 100644 index 0000000..186aae9 --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-output-2026-06-02.md @@ -0,0 +1,61 @@ +# Portolan 083 Stress Evaluation: Tool Acquisition Guidance + +Assessment from allowed context only: + +```text +/home/fall_out_bug/projects/bigtop-landscape/.portolan/stress/20260602-083-tool-acquisition-guidance/context +``` + +No forbidden paths were read and no producers were run. + +| Field | Value | +| --- | --- | +| `lane_state` | `completed` | +| `artifacts_read_count` | 8 | +| `forbidden_read` | false | +| `acquisition_guidance_present` | true | +| `acquisition_tool_count` | 5 | +| `stack_specific_adapter_requested` | false | +| `candidate_tools_as_evidence` | false | +| `installed_tools_claimed_as_supported_evidence` | false | +| `approval_boundary_present` | true | +| `risks_named` | true | +| `evidence_until_output` | `not_assessed` | +| `producer_outputs_present` | partial | +| `component_dependency_claimable` | false | +| `runtime_topology_claimable` | false | +| `next_actions_specific_enough` | true for Syft, jscpd, and Maven; partial for Gradle/Semgrep | +| `verdict` | pass | + +Cursor verdict: + +> Portolan behaves as a stack-agnostic local-first navigation harness: it routes +> agents to approval-gated native producers without posing them as +> Portolan-owned language adapters, and keeps producer/candidate evidence +> `not_assessed` until local output is produced and re-ingested. + +## Key Evidence + +- `oss-plan.json` includes `acquisition` guidance on all 5 tool plans. +- Candidate tools are local producer options, not PHP/JVM/Scala/Gradle + adapters. +- `producer-recommendation` records remain `candidate_only` / + `not_assessed`. +- Installed Syft, jscpd, and Maven availability does not become evidence: + `evidence_state` and `evidence_until_output` remain `not_assessed`. +- Approval, network, mutation, and output-not-evidence-until-reingested risks + are named. +- Component/dependency and runtime-topology claims remain blocked. + +## Residual Gaps + +- Empty `tool-registry.json`. +- No map bundle in context. +- Native map remains limited to Go. +- Gradle/Semgrep have no bounded command in this isolated branch. +- This branch is based on `origin/main` and does not include pending clean-start + guard PR #58, so older verified producer-run records can still appear as + metadata-visible prior proof paths. That is not a 083 regression, but the + integrated PR #57-#60 scratch stress already showed the clean-start guard + scrubs sibling stress outputs when #58 is included. +- External completeness remains `unknown`. diff --git a/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-prompt-2026-06-02.md b/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-prompt-2026-06-02.md new file mode 100644 index 0000000..ea2801e --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/stress/cursor-tool-acquisition-prompt-2026-06-02.md @@ -0,0 +1,42 @@ +# Cursor Composer 2.5 Stress Prompt: Tool Acquisition Guidance + +Evaluate Portolan as a stack-agnostic local-first navigation harness after a +tool acquisition guidance correction. + +Allowed context only: +`/home/fall_out_bug/projects/bigtop-landscape/.portolan/stress/20260602-083-tool-acquisition-guidance/context` + +Forbidden reads/actions: + +- do not read sibling `.portolan/stress/*` roots +- do not read root `run/` or map bundles outside this context +- do not read `/home/fall_out_bug/projects/bigtop-landscape/repos` source files +- do not run Syft, Maven, Gradle, jscpd, Docker, or any producer +- do not install tools or mutate the target + +Task: + +Judge whether Portolan stays stack-agnostic while still helping an agent pull +in the right local producer tools. Verify that candidate tools are not treated +as Portolan-owned PHP/JVM/Gradle adapters and that all producer evidence remains +`not_assessed` until local output exists. + +Required output fields: + +- lane_state +- artifacts_read_count +- forbidden_read +- acquisition_guidance_present +- acquisition_tool_count +- stack_specific_adapter_requested +- candidate_tools_as_evidence +- installed_tools_claimed_as_supported_evidence +- approval_boundary_present +- risks_named +- evidence_until_output +- producer_outputs_present +- component_dependency_claimable +- runtime_topology_claimable +- next_actions_specific_enough +- residual_gaps +- verdict diff --git a/docs/specs/083-tool-acquisition-guidance/tasks.md b/docs/specs/083-tool-acquisition-guidance/tasks.md new file mode 100644 index 0000000..165586c --- /dev/null +++ b/docs/specs/083-tool-acquisition-guidance/tasks.md @@ -0,0 +1,27 @@ +# Tasks: Tool Acquisition Guidance + +**Input**: `docs/specs/083-tool-acquisition-guidance/` + +## Phase 1: Setup And Review + +- [x] T001 Create dedicated branch/worktree for spec 083. +- [x] T002 Create concrete `spec.md`, `plan.md`, `research.md`, and `tasks.md`. +- [x] T003 Update `.specify/feature.json`, `AGENTS.md`, and + `docs/product-backlog.md` for spec 083. +- [x] T004 Record requirements/product-vision drift review. + +## Phase 2: Generic Acquisition Guidance + +- [x] T005 Add focused failing test for tool acquisition guidance. +- [x] T006 Add generic acquisition/risk fields to `oss-plan.json` tool plans. +- [x] T007 Update answer/query guidance to reject stack-owned adapters while + still recommending local producer tools. + +## Phase 3: Stress And Verification + +- [x] T008 Run focused `go test ./internal/contextprep`. +- [x] T009 Run full baseline. +- [x] T010 Run fresh Bigtop context smoke. +- [x] T011 Run Cursor Composer 2.5 bounded stress. +- [x] T012 Run independent review lanes and record disposition. +- [x] T013 Commit, push, create PR, and refresh check state. diff --git a/internal/contextprep/contextprep.go b/internal/contextprep/contextprep.go index 6dc2ddd..5999dff 100644 --- a/internal/contextprep/contextprep.go +++ b/internal/contextprep/contextprep.go @@ -188,16 +188,26 @@ type ossPlanFile struct { } type OSSToolPlan struct { - ID string `json:"id"` - Family string `json:"family"` - Producer string `json:"producer"` - Purpose string `json:"purpose"` - Executable string `json:"executable"` - AgentCapabilities []string `json:"agent_capabilities,omitempty"` - Status string `json:"status"` - EvidenceState string `json:"evidence_state"` - Reason string `json:"reason"` - Commands []OSSCommand `json:"commands,omitempty"` + ID string `json:"id"` + Family string `json:"family"` + Producer string `json:"producer"` + Purpose string `json:"purpose"` + Executable string `json:"executable"` + AgentCapabilities []string `json:"agent_capabilities,omitempty"` + Status string `json:"status"` + EvidenceState string `json:"evidence_state"` + Reason string `json:"reason"` + Acquisition ToolAcquisition `json:"acquisition"` + Commands []OSSCommand `json:"commands,omitempty"` +} + +type ToolAcquisition struct { + Kind string `json:"kind"` + Availability string `json:"availability"` + NextAction string `json:"next_action"` + EvidenceUntilOutput string `json:"evidence_until_output"` + Risks []string `json:"risks"` + Boundary string `json:"boundary"` } type OSSCommand struct { @@ -1778,6 +1788,9 @@ func buildOSSPlan(root, out, profile string, tools []ToolEntry, buildTools build sort.Slice(plan.Tools, func(i, j int) bool { return plan.Tools[i].ID < plan.Tools[j].ID }) + for i := range plan.Tools { + plan.Tools[i].Acquisition = toolAcquisition(plan.Tools[i]) + } return plan } @@ -1789,6 +1802,88 @@ func toolFamiliesPresent(tools []ToolEntry) map[string]bool { return present } +func toolAcquisition(tool OSSToolPlan) ToolAcquisition { + acquisition := ToolAcquisition{ + Kind: "native-producer-tool", + Availability: acquisitionAvailability(tool), + NextAction: acquisitionNextAction(tool), + EvidenceUntilOutput: "not_assessed", + Risks: acquisitionRisks(tool), + Boundary: "candidate tool guidance is stack-agnostic and does not create a Portolan-owned language adapter; local output must be produced and re-ingested before claims are upgraded", + } + if tool.Status == "input_present" { + acquisition.EvidenceUntilOutput = tool.EvidenceState + acquisition.NextAction = "inspect existing local output before acquiring or running another producer" + } + return acquisition +} + +func acquisitionAvailability(tool OSSToolPlan) string { + switch tool.Status { + case "available_not_run": + return "installed" + case "not_assessed": + if tool.Executable != "" { + return "installed_but_requires_local_evaluation" + } + return "requires_local_evaluation" + case "not_available": + return "missing" + case "input_present": + return "local_output_present" + default: + return "unknown" + } +} + +func acquisitionNextAction(tool OSSToolPlan) string { + switch tool.Status { + case "available_not_run": + if len(tool.Commands) > 0 { + return "request approval, run the bounded native producer command, then rerun context prepare --force" + } + return "request approval and evaluate the local producer before running it" + case "not_available": + return "install or expose the local producer tool only after approval, then rerun context prepare --force" + case "not_assessed": + return "evaluate the local producer configuration and output path boundary before suggesting a command" + case "input_present": + return "inspect existing local output before acquiring or running another producer" + default: + return "preserve not_assessed and ask for local producer output" + } +} + +func acquisitionRisks(tool OSSToolPlan) []string { + risks := []string{ + "candidate tool output is not evidence until local output exists and is re-ingested", + "do not install, fetch, or update tools without explicit approval", + } + if tool.Executable == "" && tool.Status == "not_available" { + risks = append(risks, "tool is missing from PATH") + } + if len(tool.Commands) == 0 { + risks = append(risks, "no bounded command is currently declared") + } + network := "" + mutatesTarget := false + for _, command := range tool.Commands { + if command.Network != "" && command.Network != "not_expected" && command.Network != "not_expected_for_local_filesystem_source" { + network = command.Network + } + if command.MutatesTarget { + mutatesTarget = true + } + } + if network != "" { + risks = append(risks, "network may be used: "+network) + } + if mutatesTarget { + risks = append(risks, "native command may mutate target files or tool caches") + } + return risks +} + func jscpdPlan(root, out, toolOutputDir string, inputPresent bool, repos []Repository) OSSToolPlan { plan := baseOSSPlan("jscpd", "jscpd", "jscpd", "Detect duplicated source and text fragments as metadata-visible duplication evidence.") plan.AgentCapabilities = []string{ @@ -2336,7 +2431,8 @@ func renderAnswerContract(root, out string) string { fmt.Fprintf(&b, "Relationship answers must name both relationship kind and evidence type. For relationship claims, including \"what talks to what?\", look first at `evidence-index.jsonl`, `tool-registry.json`, `gaps.jsonl`, then map-bundle `summary.json`, `graph-index.json`, and `findings.jsonl`. `evidence-index.jsonl` may include build/deploy relationship candidates such as build manifests, distribution manifests, RPM specs, and deployment manifests; those are source-visible places to inspect, not parsed service topology. Native map relationship extraction is limited to Go imports and go.mod manifests; JVM, PHP, Scala, and other non-Go coupling stays `not_assessed` unless supplied through local producer output. `source-visible` and `metadata-visible` records do not prove runtime communication; runtime topology is `not_assessed` unless runtime-visible local observations were supplied and inspected. Dependency and symbol records from local producer outputs do not mean Portolan has native PHP, JVM, Scala, or other language semantics; they are producer evidence. Missing relationship surfaces remain `unknown`, `cannot_verify`, or `not_assessed`; `claim-only` remains a claim, not observed evidence.\n\n") fmt.Fprintf(&b, "## Producer Family Recommendations\n\n") fmt.Fprintf(&b, "Producer recommendations are options, not observed evidence. Treat `producer-recommendation` records in `evidence-index.jsonl` as a safe next-action surface for missing local producer families; they do not prove the candidate tool is installed, supported, or appropriate for this landscape. Candidate tools marked `candidate_only` remain `not_assessed` until local output or a local evaluation record exists. Portolan does not synthesize producer evaluations from recommendation records; if no `producer-evaluation` record is present, candidate evaluation remains `not_assessed`. Check both `verification_state` and `support_state`: `verification_state` describes local evidence for the candidate, while `support_state` describes whether Portolan can present it as supported. Do not propose a Portolan-owned PHP/JVM/Scala adapter as the default answer to language coverage gaps; ask for or evaluate local dependency, symbol-index, API/catalog, deployment/model, static finding, duplication, config, or runtime-observation producer evidence instead.\n\n") - fmt.Fprintf(&b, "Native Maven/Gradle build-tool producer output is the preferred first step for visible JVM build manifests. Java/Scala/Maven dependency relationships remain `not_assessed` until local producer output exists and is inspected. `oss-plan.json` may list Maven or Gradle CycloneDX producer options when `pom.xml`, `build.gradle`, or `build.gradle.kts` files are visible, but those options are approval-required and do not imply Portolan executed Maven, Gradle, or a `portolan produce` command. Do not turn a visible `pom.xml` or `build.gradle` into a Portolan-owned JVM adapter request; keep it as a native producer-evidence acquisition question.\n\n") + fmt.Fprintf(&b, "Tool acquisition guidance is stack-agnostic. Candidate tools are local producer options, not Portolan-owned language adapters. Use `oss-plan.json` acquisition fields to decide whether a tool is installed, missing, or requires local evaluation, and to name approval, network, cache, mutation, and output-path risks before asking the operator to pull it in. Do not propose a Portolan-owned PHP/JVM/Scala/Gradle adapter as the default answer to language coverage gaps; ask for local producer output from an appropriate existing tool family and keep evidence `not_assessed` until that output is present and re-ingested.\n\n") + fmt.Fprintf(&b, "Build-system and SBOM producer output can be the right first evidence path when dependency/build manifests are visible, but the choice is landscape-specific and approval-gated. Java, Scala, Maven, Gradle, PHP, Composer, and other ecosystem relationships remain `not_assessed` until local producer output exists and is inspected. `oss-plan.json` may list concrete options such as Maven, Gradle, Composer, Syft, or CycloneDX-compatible producers when matching local surfaces are visible, but those options do not imply Portolan executed the tool or owns that stack. Keep visible manifests as native producer-evidence acquisition questions, not requests for Portolan-owned language or build-system adapters.\n\n") fmt.Fprintf(&b, "## Producer Run Records\n\n") fmt.Fprintf(&b, "`producer-run` records in `evidence-index.jsonl` describe externally generated local outputs selected by the operator. They are not Portolan execution receipts and they do not imply a `portolan produce` command exists. A `verified` producer-run record proves only that the referenced local output file existed during context preparation and that the record passed Portolan's metadata validation. Use `producer_family`, `producer_tool`, `output_path`, `scope`, `covered_units`, `freshness`, and `limitations` before making a claim. Static `deployment-model` and `api-catalog` records stay `metadata-visible`; they must not be promoted to `runtime-visible` or to whole-landscape coverage. Runtime topology stays `not_assessed` unless a runtime producer family supplies `runtime-visible` local observations.\n\n") fmt.Fprintf(&b, "## Hard Boundaries\n\n") @@ -2373,17 +2469,21 @@ func renderQueryPlan() string { records with kind ` + "`relationship-candidate`" + ` before opening raw source. They point at build manifests, distribution manifests, RPM specs, and deployment manifests; they are candidate evidence, not parsed topology. -- Maven/Gradle dependency evidence: if build manifests are visible but +- Dependency producer evidence: if dependency/build manifests are visible but CycloneDX/build-tool outputs are missing, inspect ` + "`oss-plan.json`" + ` for - ` + "`maven-cyclonedx`" + ` and ` + "`gradle-cyclonedx`" + ` plans. Treat them as - approval-gated native producer options, not Portolan-owned JVM adapters, and - keep dependency relationships ` + "`not_assessed`" + ` until local output exists. + build-system, SBOM, or CycloneDX-compatible plans. Treat them as + approval-gated native producer options, not Portolan-owned stack adapters, + and keep dependency relationships ` + "`not_assessed`" + ` until local output exists. - Producer family gaps: inspect ` + "`producer-coverage`" + ` and ` + "`producer-recommendation`" + ` records in ` + "`evidence-index.jsonl`" + ` before making mixed-language coverage claims. Recommendations are options, not verified support. If no ` + "`producer-evaluation`" + ` records are present, candidate evaluation remains not_assessed; Portolan does not synthesize evaluations from recommendations. +- Tool acquisition: inspect ` + "`oss-plan.json`" + ` acquisition fields before + asking the operator to pull in a local tool. Treat the tool as a candidate + producer, not as a Portolan-owned stack adapter. Name availability, approval, + network/cache/mutation risks, and the required post-run context refresh. - Implicit knowledge: inspect repository manifests, local catalogs, contracts, and index handles. Do not turn naming conventions into facts without evidence. - Service relationships: start with Backstage, OpenAPI, AsyncAPI, Structurizr, diff --git a/internal/contextprep/contextprep_test.go b/internal/contextprep/contextprep_test.go index 84dfd24..91497cf 100644 --- a/internal/contextprep/contextprep_test.go +++ b/internal/contextprep/contextprep_test.go @@ -104,17 +104,89 @@ func TestRunAddsBuildToolDependencyProducerPlans(t *testing.T) { contract := mustReadContextprep(t, filepath.Join(out, "answer-contract.md")) for _, want := range []string{ - "Native Maven/Gradle build-tool producer output", - "Java/Scala/Maven dependency relationships remain `not_assessed`", - "Do not turn a visible `pom.xml` or `build.gradle` into a Portolan-owned JVM adapter request", + "Build-system and SBOM producer output can be the right first evidence path", + "Java, Scala, Maven, Gradle, PHP, Composer, and other ecosystem relationships remain `not_assessed`", + "not requests for Portolan-owned language or build-system adapters", } { if !strings.Contains(contract, want) { t.Fatalf("answer-contract.md missing %q:\n%s", want, contract) } } queryPlan := mustReadContextprep(t, filepath.Join(out, "query-plan.md")) - if !strings.Contains(queryPlan, "Maven/Gradle dependency evidence") { - t.Fatalf("query-plan.md missing Maven/Gradle dependency guidance:\n%s", queryPlan) + if !strings.Contains(queryPlan, "Dependency producer evidence") { + t.Fatalf("query-plan.md missing dependency producer guidance:\n%s", queryPlan) + } +} + +func TestRunWritesStackAgnosticToolAcquisitionGuidance(t *testing.T) { + root := t.TempDir() + mavenRepo := filepath.Join(root, "repos", "maven-service") + gradleRepo := filepath.Join(root, "repos", "gradle-service") + mustMkdirContextprep(t, filepath.Join(mavenRepo, ".git")) + mustMkdirContextprep(t, filepath.Join(gradleRepo, ".git")) + mustWriteContextprep(t, filepath.Join(mavenRepo, "pom.xml"), `maven-service`) + mustWriteContextprep(t, filepath.Join(gradleRepo, "build.gradle.kts"), `plugins { java }`) + + bin := filepath.Join(t.TempDir(), "bin") + mustMkdirContextprep(t, bin) + for _, name := range []string{"mvn", "gradle", "syft", "jscpd"} { + path := filepath.Join(bin, name) + mustWriteContextprep(t, path, "#!/bin/sh\nexit 0\n") + if err := os.Chmod(path, 0o755); err != nil { + t.Fatal(err) + } + } + t.Setenv("PATH", bin) + + out := filepath.Join(root, ".portolan", "context") + if _, err := Run(Options{RootPath: root, OutputPath: out, Profile: "agent"}); err != nil { + t.Fatalf("Run returned error: %v", err) + } + + plan := readOSSPlanContextprep(t, filepath.Join(out, "oss-plan.json")) + if len(plan.Tools) == 0 { + t.Fatal("oss-plan has no tool candidates") + } + for _, tool := range plan.Tools { + if tool.Acquisition.Kind == "" { + t.Fatalf("tool %s missing acquisition guidance: %#v", tool.ID, tool) + } + if tool.Acquisition.EvidenceUntilOutput != "not_assessed" { + t.Fatalf("tool %s acquisition evidence = %q, want not_assessed", tool.ID, tool.Acquisition.EvidenceUntilOutput) + } + if len(tool.Acquisition.Risks) == 0 { + t.Fatalf("tool %s missing acquisition risks: %#v", tool.ID, tool.Acquisition) + } + } + + maven := toolPlanByIDContextprep(plan, "maven-cyclonedx") + if maven.Acquisition.Kind != "native-producer-tool" { + t.Fatalf("maven acquisition = %#v, want native producer tool", maven.Acquisition) + } + if !strings.Contains(maven.Acquisition.NextAction, "approval") { + t.Fatalf("maven next action = %q, want approval boundary", maven.Acquisition.NextAction) + } + gradle := toolPlanByIDContextprep(plan, "gradle-cyclonedx") + if gradle.Acquisition.Kind != "native-producer-tool" { + t.Fatalf("gradle acquisition = %#v, want native producer tool", gradle.Acquisition) + } + if !strings.Contains(gradle.Acquisition.NextAction, "evaluate") { + t.Fatalf("gradle next action = %q, want local evaluation boundary", gradle.Acquisition.NextAction) + } + + contract := mustReadContextprep(t, filepath.Join(out, "answer-contract.md")) + for _, want := range []string{ + "Tool acquisition guidance is stack-agnostic", + "Candidate tools are local producer options, not Portolan-owned language adapters", + "Do not propose a Portolan-owned PHP/JVM/Scala/Gradle adapter", + } { + if !strings.Contains(contract, want) { + t.Fatalf("answer-contract.md missing %q:\n%s", want, contract) + } + } + queryPlan := mustReadContextprep(t, filepath.Join(out, "query-plan.md")) + if !strings.Contains(queryPlan, "Tool acquisition") { + t.Fatalf("query-plan.md missing tool acquisition guidance:\n%s", queryPlan) } } @@ -141,6 +213,15 @@ func TestResolveBuildToolExecutablePrefersExecutableWrapper(t *testing.T) { } } +func toolPlanByIDContextprep(plan ossPlanFile, id string) OSSToolPlan { + for _, tool := range plan.Tools { + if tool.ID == id { + return tool + } + } + return OSSToolPlan{} +} + func TestMavenCycloneDXPlanCapsRepositoryCommands(t *testing.T) { root := t.TempDir() bin := filepath.Join(t.TempDir(), "bin")