diff --git a/.azure/azure-pipeline.template.yaml b/.azure/azure-pipeline.template.yaml deleted file mode 100644 index eeadb38..0000000 --- a/.azure/azure-pipeline.template.yaml +++ /dev/null @@ -1,94 +0,0 @@ -parameters: - name: '' - displayName: '' - vmImage: '' - outputDirectory: '' - artifactName: '' - timeoutInMinutes: 120 - -jobs: - - job: ${{ parameters.name }} - displayName: ${{ parameters.displayName }} - timeoutInMinutes: ${{ parameters.timeoutInMinutes }} - - pool: - vmImage: ${{ parameters.vmImage }} - - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: false # whether to fetch clean each time - submodules: recursive # set to 'true' for a single level of submodules or 'recursive' to get submodules of submodules - persistCredentials: true - - - task: UseDotNet@2 - displayName: 'Use .NET' - inputs: - packageType: 'sdk' - useGlobalJson: true - - - script: dotnet tool restore - displayName: 'Restore dotnet tools' - - - pwsh: | - .\build.ps1 - displayName: 'Update Release Notes' - continueOnError: false - - - script: dotnet build -c Release - displayName: 'dotnet build' - continueOnError: false - - - script: dotnet test -c Release --no-build --logger:trx --collect:"XPlat Code Coverage" --results-directory TestResults --settings coverlet.runsettings - displayName: 'Run tests' - continueOnError: true # Allow continuation even if tests fail - - - task: PublishTestResults@2 - inputs: - testResultsFormat: VSTest - testResultsFiles: '**/*.trx' #TestResults folder usually - testRunTitle: ${{ parameters.name }} - mergeTestResults: true - failTaskOnFailedTests: false - publishRunAttachments: true - - - pwsh: | - $coverageFiles = Get-ChildItem -Path "$(Build.SourcesDirectory)/TestResults" -Filter "*.cobertura.xml" -Recurse - $hasCoverageFiles = $coverageFiles.Count -gt 0 - Write-Host "##vso[task.setvariable variable=HasCoverageFiles]$hasCoverageFiles" - displayName: 'Check for Coverage Files' - condition: always() - continueOnError: true - - - task: reportgenerator@5 - displayName: ReportGenerator - # Only run if coverage files exist - condition: and(always(), eq(variables['HasCoverageFiles'], 'True')) - continueOnError: true - inputs: - reports: '$(Build.SourcesDirectory)/TestResults/**/*.cobertura.xml' - targetdir: '$(Build.SourcesDirectory)/coveragereport' - reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' - assemblyfilters: '-xunit*' - publishCodeCoverageResults: true - - - publish: $(Build.SourcesDirectory)/coveragereport - displayName: 'Publish Coverage Report' - # Only run if coverage files exist - condition: and(always(), eq(variables['HasCoverageFiles'], 'True')) - continueOnError: true - artifact: 'CoverageReports-$(Agent.OS)-$(Build.BuildId)' - - - script: dotnet pack -c Release --no-build -o $(Build.ArtifactStagingDirectory)/nuget - displayName: 'Create packages' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish artifacts' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/nuget' - ArtifactName: 'nuget' - publishLocation: 'Container' - - - script: 'echo 1>&2' - failOnStderr: true - displayName: 'If above is partially succeeded, then fail' - condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues') diff --git a/.azure/pr-validation.yaml b/.azure/pr-validation.yaml deleted file mode 100644 index 704d238..0000000 --- a/.azure/pr-validation.yaml +++ /dev/null @@ -1,29 +0,0 @@ -trigger: - branches: - include: - - dev - - master - -pr: - autoCancel: true # indicates whether additional pushes to a PR should cancel in-progress runs for the same PR. Defaults to true - branches: - include: [ dev, master ] # branch names which will trigger a build - -name: $(SourceBranchName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) - -jobs: - - template: azure-pipeline.template.yaml - parameters: - name: 'windows_pr' - displayName: 'Windows PR Validation' - vmImage: 'windows-latest' - outputDirectory: 'bin/nuget' - artifactName: 'nuget_pack-$(Build.BuildId)' - - - template: azure-pipeline.template.yaml - parameters: - name: 'linux_pr' - displayName: 'Linux PR Validation' - vmImage: 'ubuntu-latest' - outputDirectory: 'bin/nuget' - artifactName: 'nuget_pack-$(Build.BuildId)' diff --git a/.azure/windows-release.yaml b/.azure/windows-release.yaml deleted file mode 100644 index 7081f45..0000000 --- a/.azure/windows-release.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Release task for Akka.Streams.Kafka -# See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference - -trigger: - branches: - include: - - refs/tags/* -pr: none - -variables: - - group: nugetKeys - - name: githubConnectionName - value: AkkaDotNet_Releases - - name: projectName - value: Akka.Streams.Kafka - - name: githubRepositoryName - value: akkadotnet/Akka.Streams.Kafka - -jobs: - - job: Release - pool: - vmImage: windows-latest - demands: Cmd - steps: - - task: UseDotNet@2 - displayName: 'Use .NET SDK from global.json' - inputs: - useGlobalJson: true - - - powershell: ./build.ps1 - displayName: 'Update Release Notes' - - # Pack without version suffix for release - - script: dotnet pack -c Release -o $(Build.ArtifactStagingDirectory)/nuget - displayName: 'Create packages' - - - script: dotnet nuget push "$(Build.ArtifactStagingDirectory)\nuget\*.nupkg" --api-key $(nugetKey) --source https://api.nuget.org/v3/index.json --skip-duplicate - displayName: 'Publish to NuGet.org' - - - task: GitHubRelease@0 - displayName: 'GitHub release (create)' - inputs: - gitHubConnection: $(githubConnectionName) - repositoryName: $(githubRepositoryName) - title: '$(projectName) v$(Build.SourceBranchName)' - releaseNotesFile: 'RELEASE_NOTES.md' - assets: '$(Build.ArtifactStagingDirectory)/nuget/*.nupkg' diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 71aff30..b82ebe4 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -8,13 +8,6 @@ "incrementalist" ], "rollForward": false - }, - "docfx": { - "version": "2.78.3", - "commands": [ - "docfx" - ], - "rollForward": false } } -} \ No newline at end of file +} diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml index 5c4a6e4..9732270 100644 --- a/.github/workflows/pr_validation.yml +++ b/.github/workflows/pr_validation.yml @@ -36,19 +36,19 @@ jobs: - name: "Restore .NET tools" run: dotnet tool restore - - name: "Update release notes" + - name: "Verify copyright headers" shell: pwsh - run: | - ./build.ps1 + run: ./scripts/Add-FileHeaders.ps1 -Verify - - name: "dotnet build" - run: dotnet build -c Release + - name: "dotnet restore" + run: dotnet restore - # .NET Framework tests can't run reliably on Linux, so we only do .NET 8 + - name: "dotnet build" + run: dotnet build -c Release --no-restore - name: "dotnet test" shell: bash - run: dotnet test -c Release + run: dotnet test -c Release --no-build - name: "dotnet pack" - run: dotnet pack -c Release -o ./bin/nuget \ No newline at end of file + run: dotnet pack -c Release --no-build -o ./bin/nuget diff --git a/.github/workflows/publish_nuget.yml b/.github/workflows/publish_nuget.yml index 5fa311f..8d5d129 100644 --- a/.github/workflows/publish_nuget.yml +++ b/.github/workflows/publish_nuget.yml @@ -3,72 +3,69 @@ name: Publish NuGet on: push: tags: - - '*' + - 'v*.*.*' + - 'v*.*.*-*' permissions: - contents: write + contents: write jobs: publish-nuget: - name: publish-nuget - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] + runs-on: ubuntu-latest steps: - - name: "Checkout" - uses: actions/checkout@v6.0.2 - with: - lfs: true - fetch-depth: 0 - - - name: "Install .NET SDK" - uses: actions/setup-dotnet@v5.1.0 - with: - global-json-file: "./global.json" + - name: "Checkout" + uses: actions/checkout@v6.0.2 + with: + lfs: true + fetch-depth: 0 - - name: "Restore .NET tools" - run: dotnet tool restore + - name: "Install .NET SDK" + uses: actions/setup-dotnet@v5.1.0 + with: + global-json-file: "./global.json" - - name: "Update release notes" - shell: pwsh - run: | - ./build.ps1 + - name: "Restore .NET tools" + run: dotnet tool restore - - name: Create Packages - run: dotnet pack /p:PackageVersion=${{ github.ref_name }} -c Release -o ./output + - name: "Compute package version from tag" + shell: bash + run: | + # tag form is vX.Y.Z or vX.Y.Z-suffix + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#v}" + echo "PACKAGE_VERSION=${VERSION}" >> "$GITHUB_ENV" + echo "Publishing package version: ${VERSION}" - - name: Push Packages - run: dotnet nuget push "output/*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json + - name: "dotnet pack" + run: dotnet pack /p:PackageVersion=${{ env.PACKAGE_VERSION }} -c Release -o ./output - - name: "Extract latest release notes" - shell: pwsh - run: | - $content = Get-Content RELEASE_NOTES.md -Raw - # Match from the first #### heading to just before the second one - if ($content -match '(?s)(####.+?)(?=\n####|\z)') { - $Matches[1].Trim() | Set-Content RELEASE_NOTES_LATEST.md - } else { - $content | Set-Content RELEASE_NOTES_LATEST.md - } + - name: "dotnet nuget push" + run: dotnet nuget push "output/*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate - - name: release - uses: actions/create-release@v1 - id: create_release - with: - draft: false - prerelease: false - release_name: 'Akka.Hosting ${{ github.ref_name }}' - tag_name: ${{ github.ref }} - body_path: RELEASE_NOTES_LATEST.md - env: - GITHUB_TOKEN: ${{ github.token }} + - name: "Extract latest release notes" + shell: pwsh + run: | + if (-not (Test-Path RELEASE_NOTES.md)) { + "No release notes available." | Set-Content RELEASE_NOTES_LATEST.md + exit 0 + } + $content = Get-Content RELEASE_NOTES.md -Raw + if ($content -match '(?s)(####.+?)(?=\n####|\z)') { + $Matches[1].Trim() | Set-Content RELEASE_NOTES_LATEST.md + } else { + $content | Set-Content RELEASE_NOTES_LATEST.md + } - - name: Upload Release Asset - uses: AButler/upload-release-assets@v3.0 - with: - repo-token: ${{ github.token }} - release-tag: ${{ github.ref_name }} - files: 'output/*.nupkg' + - name: "Create GitHub release" + uses: softprops/action-gh-release@v2 + with: + name: "ShellSyntaxTree ${{ github.ref_name }}" + tag_name: ${{ github.ref_name }} + body_path: RELEASE_NOTES_LATEST.md + files: output/*.nupkg + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f8bfe18 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,207 @@ +# ShellSyntaxTree Agent Constitution + +This file is the repository's stable agent constitution. Keep it small. Keep +it durable. Keep it routing-focused. Volatile detail belongs in the +referenced repo-owned artifacts, not here. + +## Authority and Scope + +- You are authorized to plan, design, implement, test, document, and + release ShellSyntaxTree v0.x. +- Default to the smallest safe change that advances the v0.1 acceptance + criteria in `SPEC.md` §17. +- Prefer explicit trade-offs over hidden complexity. Bias toward marking + inputs `DynamicSkip` / `IsUnparseable` over guessing — this library + feeds security gates. + +## Read First (in this order) + +1. `SPEC.md` — the locked v0.1 contract (public API, AST, grammar, verb + tables, resolver, corpus rules). **The contract.** +2. `PROJECT_CONTEXT.md` — what this is, who consumes it, scope discipline. +3. `TOOLING.md` — what's available and how to access it. +4. `IMPLEMENTATION_PLAN.md` — current NOW / NEXT / LATER work. +5. The relevant `dotnet-skills:*` skill for the .NET concern at hand + (see `TOOLING.md` routing table). + +When `SPEC.md` and any other artifact disagree, **`SPEC.md` wins** — fix +the other artifact in the same change. + +## Required Task Routing (MODE) + +Pick a MODE before acting. Each MODE has different signals, expected +outputs, and definition-of-done. + +### MODE=build + +**Use when:** implementing parser logic, lexer, resolver, verb tables, +AST records, options, or any production code under `src/ShellSyntaxTree/`, +plus the unit tests that exercise it. + +**Signals:** changes under `src/`, non-corpus changes under `tests/`, +public-API surface changes (rare — requires bumping toward v0.2+). + +**Expected outputs:** + +- code under `src/ShellSyntaxTree/` +- unit tests under `tests/ShellSyntaxTree.Tests/` (NOT corpus JSON) +- `dotnet build` and `dotnet test` both pass locally +- Public API surface matches `SPEC.md` §2 exactly (any drift is a bug) + +**Definition of done:** see Universal DoD below, plus: every behavior +change is covered either by a unit test or a corpus entry. + +### MODE=corpus + +**Use when:** authoring, sanitizing, or curating JSON entries under +`tests/ShellSyntaxTree.Tests/Corpus/bash/*.json`. The corpus is the +acceptance contract — corpus work is materially different from code work +(JSON shape, sanitization rules, PII audit). + +**Signals:** changes under `tests/.../Corpus/`, references to dogfood +logs, requests like "add a case for `chmod`", "seed from logs". + +**Expected outputs:** + +- new or updated `*.json` corpus entries matching the SPEC §13 schema +- entries cover the SPEC §13 category coverage targets +- if seeded from logs: every sanitization rule in SPEC §14 is applied, + manually reviewed, and the entry is committed only after PII audit + would pass on it + +**Definition of done:** see Universal DoD, plus: + +- the PII audit scan (regex rules from SPEC §14) finds zero hits +- every new entry parses to the declared `expected` AST when run through + `BashParser` (i.e., implementation must already cover it, OR this entry + is intentionally a `[Theory]` failure that pins a NOW item in the + implementation plan — note the link in the entry's `notes` field) + +### MODE=release + +**Use when:** version bumps, RELEASE_NOTES.md updates, tagging, +NuGet publish flow changes. + +**Signals:** changes to `RELEASE_NOTES.md`, `Directory.Build.props` +`VersionPrefix`/`VersionSuffix`, `.github/workflows/publish_nuget.yml`, +or a request to "tag" / "ship" / "cut a release". + +**Expected outputs:** + +- semantic version that matches SPEC §15 progression rules + (`v0.1.x` additive, `v0.2.0` for first PowerShell parser, etc.) +- `RELEASE_NOTES.md` updated with the new section in the existing format +- on tag push, `publish_nuget.yml` produces a `.nupkg` matching + `VersionPrefix`/`VersionSuffix` and pushes to nuget.org + +**Definition of done:** see Universal DoD, plus: + +- the tag triggers the publish workflow and the package appears on + nuget.org +- for v0.1.0-alpha specifically: SPEC §17 acceptance #1-#8 all hold + +## Discovery Rules + +Before adding code or a test, discover in this order: + +1. Is there a SPEC clause that already specifies the behavior? Implement + to that. Do not invent shape that disagrees with the SPEC. +2. Is there an existing corpus entry that pins the expected AST for this + case? Make the implementation match the corpus. +3. Is there a `dotnet-skills:*` skill for the .NET concern? Open it; apply + the parts that fit; note conflicts. +4. Is the construct in SPEC §1 / §18 non-goals? If yes, mark + `IsUnparseable` and stop. + +If any of (1)-(4) conflict, **fix the conflict before implementing**. + +## Universal Quality Bar + +- **API surface is locked.** Public types in `SPEC.md` §2 do not change + without a deliberate version bump. Internal types are free. +- **No silent fallbacks.** If a token can't be safely classified, mark + `DynamicSkip`. If a construct can't be parsed, mark `IsUnparseable`. + Do not guess. Consumers can always relax — they can't un-execute a + command we falsely classified safe. +- **No native dependencies.** Multi-target `netstandard2.0` and `net8.0`; + AOT-trim friendly. +- **`TreatWarningsAsErrors=true`** is enforced repo-wide via + `Directory.Build.props`. Don't suppress warnings to make a build pass — + fix the cause. +- **No `Thread.Sleep`/`Task.Delay` in tests** to wait for conditions. + Tests should be deterministic; the parser is synchronous. +- **Comments: skip noise, keep signal.** Don't narrate code that + identifiers already explain. Do explain *why* a verb is in (or absent + from) `BashArity` / `FileVerbs`, *why* a token marks `DynamicSkip`, + *why* a fallback is or isn't safe in this context. +- **No stray sample apps, helper scripts, or "interesting" files.** This + repo ships one library and its tests. If a helper is genuinely needed, + put it under `tests/` or a clearly-scoped `tools/` folder and document + it in `TOOLING.md`. + +## Definition of Done (universal) + +A change is done when **all** of these hold: + +- behavior aligns with `SPEC.md` (or `SPEC.md` was updated in the same + change with explicit justification) +- `dotnet build -c Release` is clean +- `dotnet test -c Release` passes (unit + corpus) +- copyright headers present on every new/modified `.cs` file — + `pwsh ./scripts/Add-FileHeaders.ps1 -Verify` exits 0 (CI enforces this) +- public API surface still matches SPEC §2 exactly +- corpus entries that touch the changed code path still pass +- if MODE=corpus: PII audit regex finds zero hits +- if MODE=release: tag-driven publish workflow succeeds end-to-end +- `IMPLEMENTATION_PLAN.md` is updated (item moved, completed, or + parked) so the plan reflects reality + +## Validation Expectations + +Run before declaring done: + +```bash +dotnet tool restore +dotnet build -c Release +dotnet test -c Release +dotnet pack -c Release -o ./bin/nuget # only on release-shaped changes +``` + +For any code change that touches public API: + +- diff `src/ShellSyntaxTree/` headers against `SPEC.md` §2 — they must + match field-for-field. + +For corpus changes: + +- the PII audit test (a `[Fact]` that scans `tests/.../Corpus/bash/*.json` + for the SPEC §14 forbidden patterns) must pass. + +## Continuous Improvement Rule + +- If a workflow or correction repeats twice → extract or refine a skill or + add a row to `TOOLING.md`. +- If volatile project knowledge surfaces → put it in `PROJECT_CONTEXT.md` + or `IMPLEMENTATION_PLAN.md`, not here. +- This file should rarely change. Constitution edits are a deliberate act + — they ripple to every future agent run. +- Compressing a referenced authority (SPEC, dotnet-skills, etc.) into a + rule here is a *retrieval* operation, not a *memory* operation. If you + can't write the rule without losing a distinction the source draws, + drop the rule and link to the source instead. + +## Handy Commands + +```bash +# Restore and build everything +dotnet tool restore && dotnet restore && dotnet build -c Release + +# Run the full test+corpus suite +dotnet test -c Release + +# Pack locally to validate package metadata +dotnet pack -c Release -o ./bin/nuget + +# Cut a release (manual, then push the tag) +git tag v0.1.0-alpha && git push origin v0.1.0-alpha +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index bc17372..fdea59e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,48 +1,44 @@ - Copyright © 2015-$([System.DateTime]::Now.Year) Akka.NET Team - Akka.NET Team - akka, akka.streams, kafka, akkadotnet, akka streams, akka.streams.kafka + Copyright © 2026-$([System.DateTime]::Now.Year) Aaron Stannard + Aaron Stannard + Aaron Stannard $(NoWarn);CS1591 latest enable true - 1.0.0 - Example release notes + 0.1.0 + alpha - + netstandard2.0 - net6.0 - net9.0 + net8.0 + net10.0 - - - - - - - + + + + + + - + - 1.5.25 June 17 2024 - -* [Updated Akka.NET to 1.5.25](https://github.com/akkadotnet/akka.net/releases/tag/1.5.25) - akka;actors;actor model;Akka;concurrency;test - https://github.com/akkadotnet/Akka.MultiNodeTestRunner + bash;shell;parser;ast;security;agent;syntax-tree + https://github.com/Aaronontheweb/ShellSyntaxTree + https://github.com/Aaronontheweb/ShellSyntaxTree + git Apache-2.0 - akkalogo.png README.md + icon.png true - true - true snupkg - \ No newline at end of file + diff --git a/Directory.Packages.props b/Directory.Packages.props index ab3f264..7aedbe6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,17 +1,19 @@ true - - 1.5.63 + + 2.9.2 + 2.8.2 - + - - + + + + - - + - + - \ No newline at end of file + diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..f2d4c80 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,164 @@ +# Implementation Plan — ShellSyntaxTree + +Source of truth for active work. Translates `SPEC.md` §16 into NOW / NEXT / +LATER buckets. Park items aggressively — autonomous loops will otherwise +bulldoze priorities. + +> **Hard rule:** every PR ends with this file updated (item moved / +> completed / parked) so the plan reflects reality. + +--- + +## NOW (v0.1.0-alpha shipping path) + +### 1. Bootstrap projects + +- [ ] Create `src/ShellSyntaxTree/ShellSyntaxTree.csproj` (library, + multi-target `netstandard2.0;net8.0`) +- [ ] Create `tests/ShellSyntaxTree.Tests/ShellSyntaxTree.Tests.csproj` + (xunit, target `net10.0`) +- [ ] Add both to `ShellSyntaxTree.slnx` +- [ ] `dotnet build` clean, `dotnet test` clean (zero tests OK) + +### 2. Public API skeleton (lock surface first) + +- [ ] Implement public types from `SPEC.md` §2 verbatim: + `IShellParser`, `BashParser`, `BashParserOptions`, `ParsedCommand`, + `Clause`, `VerbChain`, `Arg`, `Redirect`, and the three enums +- [ ] `BashParser.Parse` throws `NotImplementedException` for now +- [ ] `Arg` includes `IsCwdAttribution` per SPEC §9 +- [ ] Public API snapshot test (a single test that asserts each public + member exists with the expected shape — fast feedback when the + surface drifts) + +### 3. BashLexer (SPEC §5) + +- [ ] Token kinds: WORD, QUOTED_STRING, OPERATOR, WHITESPACE, + CONTINUATION +- [ ] Quote handling (single literal, double with `\"`, `\\`, `\$`) +- [ ] Escape handling outside quotes +- [ ] Operator boundaries (no whitespace required) +- [ ] Heavy unit tests on tokenization + +### 4. Verb tables (SPEC §6, data only) + +- [ ] `BashArity` (multi-token verbs) +- [ ] `CwdVerbs` (`cd`, `chdir`, `popd`, `pushd`, ...) +- [ ] `FileVerbs` (cat, grep, find, ls, ...) +- [ ] `FlagsWithValue` (per-verb flags that consume the next token) +- [ ] Probe order: longest-match-first when joining 1, 2, 3 tokens + +### 5. BashParser core (SPEC §4) + +- [ ] Compound splitting on `&&`, `||`, `;`, `|` +- [ ] Verb chain extraction using `BashArity` +- [ ] Args + redirects per clause +- [ ] Subshell `( ... )` handling +- [ ] `bash -c "..."` recursion (capped at depth 5) +- [ ] Anomaly safe-fail (SPEC §11): never throw on well-formed input + +### 6. Resolver (SPEC §8) + +- [ ] Tilde + `$HOME` expansion against `BashParserOptions.HomeDirectory` +- [ ] All other `$VAR` / `${VAR}` → `DynamicSkip` +- [ ] `filesystem::/path` prefix stripping +- [ ] Glob detection (don't expand) +- [ ] Relative path joining against `BashParserOptions.WorkingDirectory` +- [ ] `LooksLikePath` heuristic per §8 + +### 7. Per-verb path-arg rules (SPEC §7) + +- [ ] Default: every non-flag positional after the verb chain is a path +- [ ] Per-verb overrides: `chmod`, `chown`, `chgrp`, `ln`, `find`, + `grep`, `rg`, `sed`, `awk`, `tar`, `curl`/`wget`, `scp`/`rsync`, + `cd`-family +- [ ] Flag-with-value handling (`-o file`, `git -C /repo`, `--output=file`) + +### 8. cd-in-compound propagation (SPEC §9) + +- [ ] First-clause `cd` sets attributed cwd for the compound +- [ ] Subsequent clauses receive synthetic `Arg` with + `IsCwdAttribution=true` +- [ ] Subsequent `cd` in the same compound replaces attribution +- [ ] Subshell boundaries reset attribution + +### 9. Subshell + bash -c surfacing (SPEC §10) + +- [ ] Flatten subshell clauses into parent's `Clauses` with + `IsSubshell=true` +- [ ] Surface `bash -c` inner clauses inline with `IsBashCWrapped=true` +- [ ] Recursion-depth cap + +### 10. Hand-authored corpus (SPEC §13 — minimum 105 entries) + +- [ ] 10 simple-verb cases +- [ ] 10 multi-token-verb cases +- [ ] 15 compound cases +- [ ] 10 cd-in-compound cases +- [ ] 10 quote-handling cases +- [ ] 10 redirect cases +- [ ] 10 subshell cases +- [ ] 10 `bash -c` cases +- [ ] 10 dynamic-skip cases +- [ ] 10 per-verb path-rule cases +- [ ] 10 unparseable cases + +### 11. Corpus runner test + +- [ ] Single `[Theory] [MemberData]` enumerating + `tests/.../Corpus/bash/*.json` +- [ ] Per-entry test name so failures point at the specific case +- [ ] Structural equality helper `AstAssert.Equal` + +### 12. PII audit (SPEC §14) + +- [ ] Single `[Fact]` that scans `tests/.../Corpus/bash/*.json` for + SPEC §14 forbidden patterns +- [ ] Wired into `pr_validation.yml` via `dotnet test` (no separate job) + +### 13. Release v0.1.0-alpha + +- [ ] `RELEASE_NOTES.md` updated with v0.1.0-alpha section +- [ ] Tag `v0.1.0-alpha`; verify `publish_nuget.yml` produces and pushes + the package + +### 14. Netclaw integration smoke (SPEC §17 #7-#8) + +- [ ] Add `` to a Netclaw + project; verify `IShellParser` resolves in DI +- [ ] One Netclaw integration test exercises a real corpus entry through + the live matcher and gets the expected gate decision + +### 15. NuGet package icon + +- [ ] Generate a fitting icon via `/generate-image` (HCTI) +- [ ] Place at `assets/icon.png`; wire into `Directory.Build.props` + (`PackageIcon` + a packed `` item) +- [ ] Re-pack to validate icon embeds + +--- + +## NEXT (v0.1.x — additive, post-alpha) + +- Seed 50–100 corpus entries from sanitized real-world dogfood logs + (SPEC §14 workflow) +- Expand verb tables as corpus surfaces real commands +- Document the "consumer's algorithm" — given a `ParsedCommand`, here is + how a security gate walks it (likely a section in `SPEC.md` Appendix + or a separate `docs/CONSUMER_GUIDE.md`) +- Performance sanity check (~1 ms typical) with a tiny BenchmarkDotNet + harness — only if anything in the daemon hot path complains + +## LATER (v0.2+ — out of v0.1 scope) + +- PowerShell parser (`PwshParser : IShellParser`) — first time we exercise + the multi-shell seam +- Windows `cmd` parser +- Source-mapping (line/column on AST nodes) — only if an IDE consumer + asks +- Heredoc body extraction, process substitution, function definitions — + only if a real consumer need surfaces + +## Parked + +*(empty; move items here when scope changes rather than deleting them)* diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md new file mode 100644 index 0000000..0c05623 --- /dev/null +++ b/PROJECT_CONTEXT.md @@ -0,0 +1,160 @@ +# Project Context — ShellSyntaxTree + +**Repository:** `Aaronontheweb/ShellSyntaxTree` +**License:** Apache-2.0 +**Owner:** Aaron Stannard ([@Aaronontheweb](https://github.com/Aaronontheweb)) + +## What ShellSyntaxTree Is + +ShellSyntaxTree is a focused .NET library that **parses shell command strings +into a structured AST** for downstream policy / security gate evaluation. It +is a *parser*, not an interpreter — it never executes, expands, or evaluates +commands. + +The output is a `ParsedCommand` containing: + +- one or more `Clause` records (split on `&&`, `||`, `;`, `|`) +- per-clause verb chain (multi-token verbs like `git push`, `docker compose up`) +- per-clause args with `IsPath` classification, `Resolved` absolute path + when known, and explicit `DynamicSkip` marking for unresolved env vars + / unexpanded globs +- redirect operators (`>`, `>>`, `<`, `2>`, `2>>`) +- `cd && cmd` propagation — the cd target is attributed to subsequent + clauses in the same compound +- recursion into `bash -c ""` so wrapped commands surface as clauses +- subshell `( ... )` isolation for `cd` attribution +- safe-fail flag `IsUnparseable` for unsupported constructs (control flow, + function definitions, process substitution, deep `bash -c` nesting, + unbalanced quotes/parens) + +The complete contract lives in [`SPEC.md`](./SPEC.md). + +## Who It's For + +**Primary consumer: [Netclaw](https://github.com/netclaw-dev/netclaw)** — an +open-source autonomous operations agent. Netclaw's approval gate currently +hand-rolls equivalent functionality in +`src/Netclaw.Security/ShellApprovalSemantics.cs` and `ShellTokenizer.cs`. +ShellSyntaxTree v0.1 is intended to **replace that hand-rolled approximation** +with a richer, structured AST that Netclaw's gate can walk directly. + +**Acceptance is tied to Netclaw integration** (see SPEC §17 #7-#8): the +package must be consumable via `` and exercise at least +one corpus entry through Netclaw's live matcher. + +**Secondary consumers:** any tool that needs to reason about the shape of +agent-emitted shell commands without executing them — pre-commit hooks, +audit pipelines, sandbox policy engines, IDE security extensions. + +## Why It Exists + +LLM-driven agents emit shell commands. Naive substring matching is unsafe +(`rm /tmp/foo` vs `rm -rf $HOME/foo`); shelling out to `bash -n` is unsafe +and doesn't yield AST nodes; tree-sitter-bash is overkill and ships native +binaries. ShellSyntaxTree fills the middle: a hand-rolled, AOT-friendly, +zero-native-deps .NET parser sized to what security gates actually need. + +## Scope Discipline + +### v0.1 (current) + +- Bash only. PowerShell and Windows `cmd` are deferred — but the + `IShellParser` seam is in place so consumers don't have to refactor. +- Public API surface in SPEC §2 is **locked**. Internal changes are free. +- Acceptance is the corpus contract (SPEC §13): every JSON entry parses to + its expected AST. + +### Explicit non-goals (v0.1) + +- Command execution. +- Variable expansion of any kind (we **mark** dynamic tokens, never resolve + them; `$HOME` is the only exception). +- Heredoc body extraction. +- Process substitution `<(cmd)`, `>(cmd)`. +- Function definitions, `for`/`while`/`case` control flow, arithmetic + expansion `$((...))`. +- Performance optimization beyond "fast enough to invoke per shell call + without noticeable latency" (~1 ms typical). +- Source-mapping (line/column for AST nodes — useful for IDEs, irrelevant + for security gates). + +### Versioning + +- `v0.1.0-alpha` — first publishable cut, bash-only. +- `v0.1.x` — additive (more verb table entries, more corpus, bug fixes). +- `v0.2.0` — first PowerShell parser implementation. +- `v1.0.0` — at least one external consumer beyond Netclaw ships against it + without finding API gaps. + +## Architectural Constraints + +- **Public API in `SPEC.md` §2 is the contract.** Everything else is + `internal`. Renaming/removing public fields requires a major version bump. +- **`IShellParser` is the multi-shell seam.** PowerShell and `cmd` parsers + must be addable without touching consumer code. +- **No native dependencies.** AOT-trim friendly; ship a single managed + package. +- **Multi-targeting**: `netstandard2.0` for broad consumer reach, `net8.0` + for modern runtimes, tests on `net10.0`. +- **Security defaults bias**: when in doubt, mark `DynamicSkip` / + `IsUnparseable`. Consumers can always relax — they cannot retroactively + un-execute a command we falsely classified as safe. + +## Acceptance for v0.1.0-alpha + +Per SPEC §17, all of the following must be true: + +1. Public API matches SPEC §2 exactly. `dotnet pack` produces a + `ShellSyntaxTree.0.1.0-alpha.nupkg`. +2. Every corpus entry in `tests/Corpus/bash/*.json` parses to its expected + AST. `dotnet test` runs them all and passes. +3. Corpus has ≥ 105 entries spanning the SPEC §13 categories. +4. PII audit scan over `tests/Corpus/bash/*.json` finds zero hits. +5. PR validation runs on GitHub Actions and passes. +6. Tagging `v0.1.0-alpha` triggers `publish_nuget.yml` and the package + appears on nuget.org. +7. Netclaw consumes the package via `` and `IShellParser` + resolves at runtime in Netclaw's DI container. +8. At least one Netclaw integration test exercises a real corpus entry + through Netclaw's matcher and produces the expected gate decision. + +## Key Pain Points and Risks + +- **Corpus PII**. Real-world bash corpus entries seed from agent dogfood + logs (`~/.netclaw/logs/daemon-*.log`) that contain usernames, repo paths, + channel/thread IDs. Sanitization is mandatory and gated by a CI scan + (SPEC §14). Shipping unsanitized PII is a release-blocker. +- **Verb table drift**. `BashArity`, `FileVerbs`, `CwdVerbs`, and + `FlagsWithValue` are static data. Adding entries is cheap; *missing* + entries means the parser silently mis-classifies. The corpus is the + early-warning system. +- **`bash -c` recursion depth**. Capped at 5 (SPEC §10). Going deeper + marks the deepest clause `IsUnparseable` so we don't blow the stack on + hostile inputs. +- **Resolver assumptions**. `WorkingDirectory` defaults to the daemon's + cwd. If consumers pass the wrong cwd, relative-path attribution will + silently disagree with what bash would do at runtime. Document loudly. +- **API surface lock**. v0.1 commits to the AST shape. Mistakes here cost + a major-version bump to fix. + +## Where Things Live + +| Concern | Location | +|---|---| +| Library source | `src/ShellSyntaxTree/` *(to be created)* | +| Tests + corpus | `tests/ShellSyntaxTree.Tests/` *(to be created)* | +| Corpus entries | `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` | +| The contract | `SPEC.md` | +| Active work plan | `IMPLEMENTATION_PLAN.md` | +| Tooling inventory | `TOOLING.md` | +| Agent constitution | `AGENTS.md` (and `CLAUDE.md`) | +| CI | `.github/workflows/{pr_validation,publish_nuget}.yml` | +| Build settings | `Directory.Build.props`, `Directory.Packages.props`, `global.json` | +| Solution | `ShellSyntaxTree.slnx` | + +## Update Discipline + +This file is **mutable** but should change only when the project's purpose, +audience, scope, or constraints actually shift. Day-to-day work tracking +belongs in `IMPLEMENTATION_PLAN.md`. Tooling changes belong in `TOOLING.md`. +The agent constitution (`AGENTS.md`) should rarely change. diff --git a/README.md b/README.md index 73b4795..b093a0c 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,84 @@ -# build-system-template -Akka.NET project build system template that provides standardized build and CI/CD configuration for all Akka.NET projects. +# ShellSyntaxTree -## Build System Overview -This repository contains our standardized build system setup that can be used across all Akka.NET projects. Here are the key components and practices we follow: +A focused .NET library that parses shell command strings into a structured AST. +Purpose-built for **security gate evaluators** — tools that inspect agent-emitted +shell commands and decide whether to allow, prompt for, or deny execution. -### CI/CD Configuration -We primarily use GitHub Actions for our CI/CD pipelines, but also maintain Azure DevOps pipeline examples. You can find the configuration examples in: -- `.github/workflows/` - GitHub Actions pipeline examples -- `.azuredevops/` - Azure DevOps pipeline examples +ShellSyntaxTree is **not** a shell interpreter. It does not execute, expand, +or evaluate commands. It returns an AST (verb chain, args with path +classification, redirects, compound operators, `cd`-in-compound propagation, +`bash -c` recursion) that consumers walk to make policy decisions. -### SDK Version Management -We use `global.json` to pin the .NET SDK version for both CI/CD environments and local development. This ensures consistent builds across all environments and developers. +The original consumer is [Netclaw](https://github.com/netclaw-dev/netclaw)'s +approval policy. Any tool that needs to reason about the shape of an +agent-emitted shell command — without running it — is welcome to consume it. -### .NET Tools -We use local .NET tools to enhance our build and documentation process. The tools are configured in `.config/dotnet-tools.json` and include: +## Status -- [Incrementalist](https://github.com/petabridge/Incrementalist) (v1.0.0-beta4) - Used for determining which projects need to be rebuilt based on Git changes -- [DocFx](https://dotnet.github.io/docfx/) (v2.78.3) - Used for generating documentation +**v0.1 — pre-alpha.** Public API surface and behavior are specified in +[`SPEC.md`](./SPEC.md). Implementation is in progress. -To restore these tools in your local environment, run: -```powershell -dotnet tool restore -``` +## Why not `tree-sitter-bash`? + +Native dependencies, AOT trim concerns, and IDE-grade fidelity we don't need. +We want unsupported constructs to mark `IsUnparseable = true` so consumers +route to safe-fail. See [`SPEC.md` Appendix B](./SPEC.md) for the full +trade-off analysis. -This command is automatically executed in our CI/CD pipelines (both GitHub Actions and Azure DevOps) to ensure tools are available during builds. +## Public API surface (locked for v0.1) -### Centralized Package and Build Management -We utilize two key MSBuild files for centralized configuration: +```csharp +namespace ShellSyntaxTree; -1. `Directory.Packages.props` - Implements [Central Package Version Management](https://learn.microsoft.com/nuget/consume-packages/Central-Package-Management) for consistent NuGet package versions across all projects in the solution. +public interface IShellParser { ParsedCommand Parse(string command); } +public sealed class BashParser : IShellParser { /* ... */ } +public sealed record BashParserOptions { /* HomeDirectory, WorkingDirectory */ } -2. `Directory.Build.props` - Defines common build properties, including: - - Copyright and author information - - Source linking configuration - - NuGet package metadata - - Common compiler settings - - Target framework definitions +public sealed record ParsedCommand { /* Source, Clauses, IsUnparseable, ... */ } +public sealed record Clause { /* Operator, Verb, Args, Redirects, ... */ } +public sealed record VerbChain { /* Tokens */ } +public sealed record Arg { /* Raw, Resolved, Kind, IsPath, ... */ } +public sealed record Redirect { /* Direction, Target */ } -### Code Coverage Configuration -The `coverlet.runsettings` file configures code coverage collection using Coverlet, with settings for: -- Multiple coverage report formats (JSON, Cobertura, LCOV, TeamCity, OpenCover) -- Test assembly exclusions -- Source linking integration -- Performance optimizations +public enum ArgKind { Literal, EnvVar, Glob, Tilde, DynamicSkip } +public enum RedirectDirection { In, Out, Append, ErrOut, ErrAppend } +public enum CompoundOperator { None, AndIf, OrIf, Sequence, Pipe } +``` + +PowerShell and Windows `cmd` parsers are deferred to later versions; the +`IShellParser` seam is already in place so consumers don't have to refactor +when they ship. -### Release Management -Our release process is streamlined through: -- `RELEASE_NOTES.md` - Contains version history and release notes -- `build.ps1` - PowerShell script that processes release notes and updates version information -- Supporting scripts in `/scripts`: - - `bumpVersion.ps1` - Updates version numbers - - `getReleaseNotes.ps1` - Parses release notes +## Multi-targeting -The build system primarily relies on standard `dotnet` CLI commands, with the PowerShell scripts mainly handling release note processing and version management. +`netstandard2.0` for broad consumer compatibility, `net8.0` for modern +runtimes. Tests target `net10.0`. -### Solution Format -We prefer the new `.slnx` XML-based solution format over the traditional `.sln` format. This requires .NET 9 SDK or later. The new format is more concise and easier to work with. You can migrate existing solutions using: +## Repository layout -```powershell -dotnet sln migrate ``` +src/ShellSyntaxTree/ # library (TBD) +tests/ShellSyntaxTree.Tests/ # xunit tests + corpus runner (TBD) +tests/ShellSyntaxTree.Tests/Corpus/ # JSON test corpus (acceptance contract) +SPEC.md # the implementation specification +PROJECT_CONTEXT.md # what this is, who it serves +TOOLING.md # available tooling and how to access it +AGENTS.md / CLAUDE.md # agent operating constitution +IMPLEMENTATION_PLAN.md # NOW / NEXT / LATER work tracker +``` + +## Building + +```bash +dotnet tool restore +dotnet build -c Release +dotnet test -c Release +dotnet pack -c Release -o ./bin/nuget +``` + +`global.json` pins the SDK version. Targeting requires .NET 10 SDK or later +(`.slnx` solution format). -For more information about the new `.slnx` format, see the [official announcement](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/). +## License -## Getting Started -1. Ensure you have the correct .NET SDK version installed (check `global.json`) -2. Clone this repository -3. Run `dotnet build` to verify the build system -4. Customize the configuration files for your specific project needs +[Apache-2.0](./LICENSE). Copyright © 2026 Aaron Stannard. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..0fd2e36 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,1208 @@ +# ShellSyntaxTree — v0.1 Specification + +**Status:** Draft for v0.1. Approved decisions; implementation pending. +**Audience:** Whoever (human or agent) implements ShellSyntaxTree v0.1. +**Read this end-to-end before writing any code.** + +This document specifies the public API, AST, grammar, verb tables, resolver +semantics, and corpus contract for ShellSyntaxTree v0.1. The library is a +focused bash command parser designed for **security gate evaluators** — +tools that inspect agent-emitted shell commands to decide whether to allow, +prompt for, or deny execution. + +It is **not** a general-purpose shell interpreter. It does not execute, +expand, or evaluate commands. It returns a structured AST that consumers +walk to make decisions. + +The original consumer is [Netclaw](https://github.com/Aaronontheweb/netclaw)'s +approval policy. The library is designed to be reusable beyond Netclaw — +any tool that needs to reason about the shape of an agent-emitted bash +command can consume it. + +--- + +## 1. Goals & Non-Goals + +### Goals (v0.1) + +1. **Parse bash commands into a structured AST** with per-clause verbs, + args, redirects, and compound operators. +2. **Extract paths a command operates on** with per-verb knowledge of which + positional args are paths vs flags vs literal values (`chmod 755 file` + knows `755` is a mode). +3. **Honor `cd && cmd` propagation** within a compound — `` + counts as a path each subsequent command operates on. +4. **Recurse into `bash -c ""`** so the inner command is parsed and + its clauses surface to the consumer. +5. **Mark dynamic-content tokens** (unresolved `$VAR`, unexpanded globs) + so consumers don't misextract literal `$VAR/foo` as a path. +6. **Multi-shell-ready** via `IShellParser` interface — bash is the only + v0.1 implementation; PowerShell and cmd are deferred to later versions + without breaking the seam. + +### Non-Goals (v0.1) + +- PowerShell parsing (deferred; interface seam is present). +- Windows cmd parsing (deferred). +- Command execution. The library never runs anything. +- Variable expansion. We mark dynamic tokens, never resolve them. +- Function definitions, here-docs body extraction, complex parameter + expansion (`${var//pattern/replacement}`), arithmetic expansion. +- Performance tuning beyond "fast enough to invoke per shell call without + noticeable latency" (~1ms per typical input). + +--- + +## 2. Public API Surface + +The package exposes a small surface from a single namespace +`ShellSyntaxTree`. **Public types only**: + +```csharp +namespace ShellSyntaxTree; + +/// +/// Parses shell command strings into structured ASTs. +/// +public interface IShellParser +{ + /// + /// Parse the command. Always returns a ParsedCommand; sets + /// when the input cannot + /// be tokenized (unbalanced quotes, etc.). Never throws on + /// well-formed strings; throws ArgumentNullException on null input. + /// + ParsedCommand Parse(string command); +} + +/// Bash implementation of IShellParser. +public sealed class BashParser : IShellParser +{ + public BashParser(); + public BashParser(BashParserOptions options); + public ParsedCommand Parse(string command); +} + +/// Configuration knobs for BashParser. +public sealed record BashParserOptions +{ + /// + /// User home directory used to expand `~` and `$HOME` tokens during + /// resolution. Defaults to . + /// + public string? HomeDirectory { get; init; } + + /// + /// Working directory used to resolve relative path tokens during + /// resolution. Defaults to the daemon-process cwd. + /// + public string? WorkingDirectory { get; init; } +} + +// AST records — see §3. +public sealed record ParsedCommand { ... } +public sealed record Clause { ... } +public sealed record VerbChain { ... } +public sealed record Arg { ... } +public sealed record Redirect { ... } +public enum ArgKind { Literal, EnvVar, Glob, Tilde, DynamicSkip } +public enum RedirectDirection { In, Out, Append, ErrOut, ErrAppend } +public enum CompoundOperator { None, AndIf, OrIf, Sequence, Pipe } +``` + +That's the entire public API. **Everything else is internal.** The lexer, +parser internals, verb tables, resolver — all implementation detail. + +--- + +## 3. AST Reference + +### `ParsedCommand` + +The top-level result of parsing. Always returned (never null). + +```csharp +public sealed record ParsedCommand +{ + /// The original input string, verbatim. + public string Source { get; init; } = ""; + + /// + /// Top-level clauses, split on compound operators (&&, ||, ;, |). + /// For a simple command, exactly one clause with Operator=None. + /// + public IReadOnlyList Clauses { get; init; } = []; + + /// + /// True when the parser could not produce a clean AST (unbalanced + /// quotes, unparseable construct). When true, Clauses MAY be partial + /// or empty. Consumers should route to safe-fail. + /// + public bool IsUnparseable { get; init; } + + /// + /// Human-readable diagnostic when IsUnparseable=true; null otherwise. + /// + public string? UnparseableReason { get; init; } +} +``` + +### `Clause` + +One logical command within a compound. Each clause has its own verb chain, +args, redirects, and the operator that joined it to the previous clause. + +```csharp +public sealed record Clause +{ + /// + /// The operator joining this clause to the previous one. The first + /// clause in a ParsedCommand has Operator=None. Subsequent clauses + /// carry the operator that preceded them in the source + /// (e.g. `a && b` produces clauses [{None,a}, {AndIf,b}]). + /// + public CompoundOperator Operator { get; init; } + + /// The verb chain (see §3.3 and §6). + public VerbChain Verb { get; init; } = new(); + + /// + /// All argument tokens after the verb chain, in source order. Includes + /// flags and positional args. See for token kind. + /// + public IReadOnlyList Args { get; init; } = []; + + /// + /// Redirect operators on this clause (>, >>, <, 2>, 2>>). Each entry + /// includes direction and target path. + /// + public IReadOnlyList Redirects { get; init; } = []; + + /// + /// True when this clause is wrapped in a subshell (parens). Subshells + /// isolate cd state — see §9. + /// + public bool IsSubshell { get; init; } + + /// + /// True when this clause is the result of recursing into a `bash -c` + /// or `sh -c` wrapper. Useful for consumers that want to surface + /// "this came from a wrapped invocation" in UI. + /// + public bool IsBashCWrapped { get; init; } +} +``` + +### `VerbChain` + +The verb of a clause. Multi-token to handle commands like `git push`, +`docker compose up`, `bun run`, `dotnet test`. Length determined by the +`BashArity` table (see §6). + +```csharp +public sealed record VerbChain +{ + /// + /// Verb tokens in source order. Empty when the clause has no verb + /// (e.g. clause is just a redirect or an empty fragment). + /// + public IReadOnlyList Tokens { get; init; } = []; + + /// Convenience: tokens joined with spaces. + public string Joined => string.Join(' ', Tokens); +} +``` + +### `Arg` + +One argument token after the verb chain. Includes resolution state. + +```csharp +public sealed record Arg +{ + /// Verbatim token from the source. + public string Raw { get; init; } = ""; + + /// + /// Resolved value for path tokens — tilde expanded, env vars + /// substituted, normalized to absolute path against + /// BashParserOptions.WorkingDirectory. Null when Kind is not a path + /// (Literal non-path / Glob / DynamicSkip). + /// + public string? Resolved { get; init; } + + /// Token kind. See . + public ArgKind Kind { get; init; } + + /// + /// True when this token starts with '-' or '--' (a flag, not a + /// positional arg). + /// + public bool IsFlag => Raw.StartsWith('-'); + + /// + /// True when this token is a path the clause operates on (per the + /// per-verb pathArgs table; see §7). Set during parsing so consumers + /// don't reapply per-verb rules. + /// + public bool IsPath { get; init; } +} + +public enum ArgKind +{ + /// Literal value (string, number, flag). + Literal, + /// Token containing an unresolved env var reference. + EnvVar, + /// Token containing glob metachars (* ? [). + Glob, + /// Token starting with ~ (tilde). + Tilde, + /// + /// Token whose value cannot be safely resolved (unresolved env var, + /// unexpandable glob). Consumers SHALL treat as "no value extracted" + /// rather than using Raw as a literal path. + /// + DynamicSkip +} +``` + +### `Redirect` + +```csharp +public sealed record Redirect +{ + public RedirectDirection Direction { get; init; } + /// Target path (resolved per Arg conventions). + public string Target { get; init; } = ""; + /// True when target is a dynamic token (skip). + public bool IsDynamicSkip { get; init; } +} + +public enum RedirectDirection +{ + In, // < + Out, // > + Append, // >> + ErrOut, // 2> + ErrAppend // 2>> +} +``` + +### `CompoundOperator` + +```csharp +public enum CompoundOperator +{ + None, // first clause; no prior operator + AndIf, // && + OrIf, // || + Sequence, // ; + Pipe // | +} +``` + +--- + +## 4. Grammar + +Approximate BNF for what the parser accepts. Anything outside this grammar +is unparseable (`ParsedCommand.IsUnparseable = true`). + +``` +command := clause (compound_op clause)* +compound_op := "&&" | "||" | ";" | "|" +clause := subshell | bash_c_wrapper | simple_clause +subshell := "(" command ")" +bash_c_wrapper := ("bash" | "sh") "-c" QUOTED_STRING +simple_clause := verb_chain arg* redirect* +verb_chain := word{1..N} // N = BashArity[word_0] +arg := word | flag | quoted_string +flag := "-" letter+ | "--" word +redirect := redirect_op target +redirect_op := ">" | ">>" | "<" | "2>" | "2>>" +target := word | quoted_string +word := non-whitespace, non-operator characters +quoted_string := single-quoted | double-quoted +``` + +**Notes:** + +- Whitespace between tokens is one or more spaces or tabs. +- `\` followed by a newline is a line continuation (treat as whitespace). +- `\` before a metachar inside a double-quoted string escapes the metachar. +- Single-quoted strings preserve all bytes literally — no escape processing. +- Heredocs (`<`, `>>`, `<`, `2>`, `2>>`, + `(`, `)`, `<<`. +- **WHITESPACE** — one or more spaces or tabs. Discarded after splitting. +- **CONTINUATION** — `\` + `\n`. Treated as whitespace. + +### Quote handling + +- **Single quotes** `'...'` preserve bytes literally. No escape processing, + no variable expansion. Anything inside is one token. +- **Double quotes** `"..."` preserve whitespace but allow: + - `\"` escapes the closing quote. + - `\\` escapes a backslash. + - `\$` escapes a dollar sign. + - `$VAR` and `${VAR}` are recognized as env var references but **not + expanded** — the token is marked `ArgKind.EnvVar` (or `DynamicSkip` + if resolution would be required for path classification). +- Unbalanced quotes → `IsUnparseable = true` with reason + `"unbalanced quote at position N"`. + +### Escape handling + +- `\X` outside quotes: removes the backslash, takes X literally. Example: + `echo \$HOME` produces token `$HOME` with `ArgKind.Literal`. +- `\X` inside double quotes: only `\"`, `\\`, `\$`, `\\`, and `\\`+newline + are recognized escape sequences. Other backslashes preserved literally. + +### Operator boundaries + +Operators terminate the current token. `cd /tmp&&ls` lexes as +`[cd, /tmp, &&, ls]` — no whitespace required around operators. The lexer +must handle this. + +--- + +## 6. Verb Tables + +These are **data**, not logic. Implement as `static readonly` collections. + +### 6.1 BashArity + +How many tokens form the verb chain for known commands. Defaults to 1 +when not in the table. + +```csharp +internal static readonly IReadOnlyDictionary BashArity = + new Dictionary(StringComparer.OrdinalIgnoreCase) +{ + // Two-token verbs + ["git"] = 2, // git push, git log, git checkout + ["dotnet"] = 2, // dotnet test, dotnet build + ["npm"] = 2, // npm install, npm run + ["yarn"] = 2, // yarn add, yarn install + ["pnpm"] = 2, // pnpm add, pnpm install + ["cargo"] = 2, // cargo build, cargo test + ["go"] = 2, // go build, go test, go run + ["kubectl"] = 2, // kubectl apply, kubectl get + ["helm"] = 2, // helm install, helm upgrade + ["systemctl"] = 2, // systemctl start, systemctl status + ["service"] = 2, // service nginx + ["pip"] = 2, // pip install, pip uninstall + ["pip3"] = 2, // pip3 install + ["brew"] = 2, // brew install, brew upgrade + ["apt"] = 2, // apt install, apt update + ["apt-get"] = 2, // apt-get install + ["yum"] = 2, // yum install + ["dnf"] = 2, // dnf install + ["pacman"] = 2, // pacman -S, pacman -Syu (the -S is the verb) + ["aws"] = 2, // aws s3, aws ec2 (top-level) + ["gcloud"] = 2, // gcloud compute, gcloud auth + ["az"] = 2, // az vm, az storage + + // Three-token verbs + ["docker"] = 2, // docker run, docker ps; "docker compose up" handled below + ["docker compose"] = 3, // docker compose up, docker compose down + ["docker-compose"] = 2, // legacy single-token form + ["bun"] = 2, // bun run, bun install + ["bun run"] = 3, // bun run my-script (treat as 3-tuple verb) + ["nuget"] = 2, // nuget push, nuget pack +}; +``` + +The table is not exhaustive. Missing verbs default to 1 token. Add entries +as the corpus surfaces real commands. + +**Implementation note:** The parser must look up multi-token verbs by +joining the first 1, 2, then 3 tokens and probing the table from longest +to shortest. So `docker compose up nginx` first probes `docker compose up` +(not in table; arity defaults wouldn't fire), then probes `docker compose` +(in table → arity 3) → verb chain is the first 3 tokens. + +### 6.2 CWD verbs + +Verbs whose first non-flag positional arg becomes the cwd for subsequent +clauses in the same compound (see §9). + +```csharp +internal static readonly HashSet CwdVerbs = + new(StringComparer.OrdinalIgnoreCase) +{ + "cd", "chdir", "popd", "pushd", + "push-location", "set-location" // PowerShell idioms (forward-compat) +}; +``` + +### 6.3 FILE verbs + +Verbs whose positional args are paths. The default extraction rule is "all +non-flag positional args after the verb chain are paths." Per-verb overrides +in §7. + +```csharp +internal static readonly HashSet FileVerbs = + new(StringComparer.OrdinalIgnoreCase) +{ + // CWD verbs are also FILE verbs (their target is a path) + "cd", "chdir", "popd", "pushd", "push-location", "set-location", + // File mutation + "rm", "cp", "mv", "mkdir", "rmdir", "touch", "ln", + "chmod", "chown", "chgrp", "stat", "test", + // Read + "cat", "less", "more", "head", "tail", "grep", "rg", + "find", "fd", "locate", "wc", "file", + // Editors / text tools + "sed", "awk", "vi", "vim", "nano", "emacs", "ed", + // Compression + "tar", "zip", "unzip", "gzip", "gunzip", "bzip2", "xz", + // Network with file targets + "curl", "wget", "scp", "rsync", "sftp", + // Shell / interpreter loaders + "bash", "sh", "zsh", "fish", + "python", "python3", "node", "ruby", "perl", "php", + // Diff / patch + "diff", "patch", "cmp", + // Listing + "ls", "dir", "tree", +}; +``` + +### 6.4 CMD_FILE verbs (Windows cmd; deferred for v0.1 implementation) + +Reserved for future PowerShell/cmd parser support. Document the table +shape now so the seam is clear. + +```csharp +internal static readonly HashSet CmdFileVerbs = + new(StringComparer.OrdinalIgnoreCase) +{ + "type", "copy", "move", "del", "erase", "ren", "ren", + "xcopy", "robocopy", "findstr", + // PowerShell cmdlets + "get-content", "set-content", "remove-item", "copy-item", + "move-item", "new-item", +}; +``` + +--- + +## 7. Per-Verb Path-Arg Extraction Rules + +The default rule for FILE verbs: every non-flag positional arg after the +verb chain is a path. Per-verb overrides: + +| Verb | Rule | +|---|---| +| `chmod` | First non-flag positional is **mode** (e.g. `755`, `+x`); rest are paths. | +| `chown` | First non-flag positional is **user[:group]**; rest are paths. | +| `chgrp` | First non-flag positional is **group**; rest are paths. | +| `ln` | All positionals are paths (source then target). | +| `find` | First positional is a path; rest are predicate args (skip). | +| `grep` | First positional is **pattern**; rest are paths. | +| `rg` | First positional is **pattern**; rest are paths. | +| `sed` | First positional is **script**; rest are paths. | +| `awk` | First positional is **program**; rest are paths. | +| `tar` | Action flag determines path roles; default to extracting all non-flag positionals as paths. | +| `curl`, `wget` | First positional is **URL**, not a path. `-o file` flag arg is a path. | +| `scp`, `rsync`, `sftp` | All positionals are paths (some remote). | +| `cd`, `chdir`, `pushd`, `popd` | First non-flag positional is the cwd target (a path). | +| Others (in FileVerbs, no override) | All non-flag positionals are paths. | + +### Flag-with-value handling + +Some flags take values (`-o file`, `-C /repo`, `--output=file`). The parser +must know which flags consume the next token as a value. Curated table: + +```csharp +internal static readonly IReadOnlyDictionary> + FlagsWithValue = new Dictionary>( + StringComparer.OrdinalIgnoreCase) +{ + ["git"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "-C", "--git-dir", "--work-tree" }, + ["curl"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "-o", "--output", "-d", "--data" }, + ["wget"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "-O", "--output-document" }, + ["docker"]= new HashSet(StringComparer.OrdinalIgnoreCase) { "-v", "--volume", "-f", "--file" }, + ["tar"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "-f", "--file", "-C", "--directory" }, + // Add as corpus surfaces real cases. +}; +``` + +When a flag-with-value consumes the next token, the consumed token's +`IsPath` flag is set if the value is path-shaped (per the resolver in §8). +For `git -C /repo log`: the `-C` flag consumes `/repo`, marks it as a +path, then the verb chain continues with `log`. + +`--output=file` (equals form) is parsed as one token; the path value after +`=` is extracted into a synthetic Arg with `IsPath=true`. + +--- + +## 8. Resolver + +For each Arg with potential path content, the resolver attempts to produce +a normalized absolute path. Resolution order: + +1. **Tilde expansion.** `~` → `BashParserOptions.HomeDirectory`. + `~/foo` → `/foo`. `~user` not supported → `DynamicSkip`. + +2. **Env-var substitution.** `$VAR` and `${VAR}` are **not expanded** + even if the value is in `Environment`. We treat any env var reference + as `DynamicSkip` because the env var available at parse time may + differ from what's available when the agent's command actually runs. + `$HOME` is the **only** exception — we treat it as equivalent to `~` + and expand it from `BashParserOptions.HomeDirectory`. + +3. **`filesystem::/path` prefix stripping.** Some tools emit + `filesystem::/path/to/file`; strip the prefix. Become `/path/to/file`. + +4. **Glob detection.** Tokens containing `*`, `?`, or `[` are marked + `ArgKind.Glob`. The resolver does **not** expand globs. The token + stays as-is in `Raw`; `Resolved` is null. Consumers that want the + glob's "covering directory" can use `Path.GetDirectoryName(Raw)` to + approximate. + +5. **Relative path resolution.** Tokens not starting with `/` (or `\\` on + Windows) are joined to `BashParserOptions.WorkingDirectory`. If + `WorkingDirectory` is null, the token stays relative and `Resolved` + is null with `Kind = DynamicSkip`. + +6. **Dynamic-skip predicates.** A token is `DynamicSkip` when: + - It contains an unresolved env var reference (other than `$HOME`). + - It contains glob metachars AND the resolver was asked for an + absolute resolution. + - Resolution throws an `IOException` or path-format exception. + + `DynamicSkip` tokens have `Resolved = null`. Consumers must not use + `Raw` as a literal path. + +### Path-shape heuristic + +When deciding whether a token "looks like a path" (used to decide whether +to apply the resolver): + +``` +LooksLikePath(token) = + token starts with '/' (Unix absolute) +|| token starts with '\\' or ':' (Windows absolute) +|| token starts with './' or '../' (Unix relative) +|| token starts with '~' (Tilde) +|| token contains '/' or '\\' anywhere +|| token ends with a known file extension (.json, .md, .txt, .conf, ...) +|| token is in the args of a FileVerb at a position the per-verb rule + marks as a path +``` + +The per-verb rule wins when present; the heuristic is the fallback. + +--- + +## 9. cd-in-Compound Propagation + +The agent's natural idiom is `cd /target && cmd1 && cmd2`. Bash semantics: +`cmd1` and `cmd2` execute with cwd `/target`. The parser honors this for +**path attribution within the same compound**. + +### Rules + +1. **First clause is a `cd`-family verb** (per §6.2): the cd target becomes + the **attributed cwd** for subsequent clauses in the same compound. +2. **Subsequent clauses inherit the attributed cwd** as if it were + prepended with `-C` semantics. Specifically: a synthetic `Arg` with + `IsPath=true`, `Resolved=`, and `Kind=Literal` is added to + each subsequent clause's `Args` list at the **end**, marked with a flag + `IsCwdAttribution=true` so consumers can distinguish it from + user-emitted args. + + *(Add `IsCwdAttribution: bool` to the `Arg` record. Default false.)* + +3. **A subsequent `cd`** in the same compound **replaces** the attributed + cwd for clauses after it. (`cd /a && cmd1 && cd /b && cmd2` → cmd1 + inherits `/a`, cmd2 inherits `/b`.) + +4. **Subshell boundaries reset attribution.** `cd /a && (cd /b && cmd1) && cmd2`: + cmd1 (inside subshell) inherits `/b`; cmd2 (outside subshell) inherits + `/a` (the subshell's `cd /b` does not leak out). + +5. **Attribution does not change the clause's verb or original args.** + The attribution is purely additive — the `cd` clause itself is still + parsed normally, and subsequent clauses retain everything the user + typed, plus the synthetic Arg. + +### Example + +Input: `cd /target && git -C /other log && cat file.txt` + +Parsed clauses: + +``` +Clause 0: Operator=None, Verb=[cd], Args=[/target] +Clause 1: Operator=AndIf, Verb=[git, log], + Args=[ + Arg{Raw="-C",IsFlag=true}, + Arg{Raw="/other",IsPath=true,Resolved="/other"}, + Arg{Raw="/target",IsPath=true,Resolved="/target",IsCwdAttribution=true} + ] +Clause 2: Operator=AndIf, Verb=[cat], + Args=[ + Arg{Raw="file.txt",IsPath=true,Resolved="/target/file.txt"}, + Arg{Raw="/target",IsPath=true,Resolved="/target",IsCwdAttribution=true} + ] +``` + +Note: `file.txt` in clause 2 resolves against the attributed cwd +`/target` to produce `/target/file.txt`. The attributed-cwd Arg is also +appended for completeness, even though the resolver already used it. + +Consumers can choose to ignore `IsCwdAttribution=true` args if they +already see the resolved path in another arg. + +--- + +## 10. Subshell & `bash -c` Recursion + +### Subshells + +Subshells are clauses wrapped in parens: `(cd /a && cmd)`. The parser +recognizes the parens, parses the inner command, and emits a single +clause with: + +- `Verb` = empty (a subshell has no verb of its own) +- `Args` = empty +- `IsSubshell = true` +- A nested `ParsedCommand` field — **but** since the AST should be flat + for consumer convenience, instead we **flatten** the subshell into the + parent's `Clauses` list with each inner clause's `IsSubshell=true`. + +Specifically: `(cd /b && cmd) && cmd2` produces three clauses: + +``` +Clause 0: Op=None, Verb=cd, Args=[/b], IsSubshell=true +Clause 1: Op=AndIf, Verb=cmd, Args=[/b attribution], IsSubshell=true +Clause 2: Op=AndIf, Verb=cmd2, Args=[] // no /b attribution — subshell isolated +``` + +### `bash -c` Recursion + +`bash -c "inner command"` and `sh -c "inner command"` are common wrappers +the agent emits. The parser: + +1. Recognizes the `bash -c` or `sh -c` prefix. +2. Parses the quoted argument as a fresh `ParsedCommand`. +3. Surfaces the inner command's clauses inline in the outer's `Clauses` + list, each with `IsBashCWrapped=true`. + +Example: `bash -c "cd /a && cmd"` produces: + +``` +Clause 0: Op=None, Verb=cd, Args=[/a], IsBashCWrapped=true +Clause 1: Op=AndIf, Verb=cmd, Args=[/a attribution], IsBashCWrapped=true +``` + +The outer `bash -c` itself does not appear as a clause — it's "consumed" +by the recursion. Consumers that care that this came from a wrapper can +inspect `IsBashCWrapped` on the surfaced clauses. + +**Recursion limit:** parse `bash -c "bash -c ..."` chains up to depth 5. +Deeper nesting → mark the deepest clause as `IsUnparseable = true`. + +--- + +## 11. Parser Anomaly Behavior + +When the parser cannot produce a clean AST: + +1. **Set `ParsedCommand.IsUnparseable = true`.** +2. **Set `UnparseableReason`** to a human-readable diagnostic. +3. **Return whatever clauses were successfully parsed** in `Clauses`. May + be empty. +4. **Never throw** on well-formed input strings (only throw on null). + +Conditions that produce `IsUnparseable = true`: + +- Unbalanced quotes (`"foo` with no closing `"`). +- Unbalanced parens (`(cmd && cmd2`). +- Unrecognized control-flow keywords (`for`, `while`, `do`, `done`, + `then`, `fi`, `case`, `esac`). +- Function definitions (`name() { ... }`). +- Process substitution (`<(cmd)`, `>(cmd)`). +- Recursion depth exceeded on `bash -c` chains. + +Consumers (e.g. Netclaw's gate evaluator) route unparseable commands to a +safe-fail path (prompt the user; offer only Once and Deny — no persistent +grants on shapes the parser can't model). + +--- + +## 12. Public Examples + +A handful of input/expected-AST pairs to anchor understanding. These belong +in the corpus (§13) verbatim. + +### Simple verb + +Input: `ls -la /tmp` + +``` +ParsedCommand { + Source = "ls -la /tmp", + IsUnparseable = false, + Clauses = [ + Clause { + Operator = None, + Verb = VerbChain { Tokens = ["ls"] }, + Args = [ + Arg { Raw = "-la", IsFlag = true, Kind = Literal }, + Arg { Raw = "/tmp", IsPath = true, Resolved = "/tmp", Kind = Literal } + ], + Redirects = [], + IsSubshell = false, + IsBashCWrapped = false + } + ] +} +``` + +### Multi-token verb + +Input: `git push origin main` + +``` +Clauses = [ + Clause { + Verb = VerbChain { Tokens = ["git", "push"] }, + Args = [ + Arg { Raw = "origin", Kind = Literal, IsPath = false }, + Arg { Raw = "main", Kind = Literal, IsPath = false } + ] + } +] +``` + +### Compound with cd attribution + +Input: `cd /target && cmd1 && cmd2 file.txt` + +``` +Clauses = [ + Clause { Verb = [cd], Args = [/target attributed-as-path], Op = None }, + Clause { + Verb = [cmd1], Op = AndIf, + Args = [Arg { Raw = "/target", Resolved = "/target", + IsPath = true, IsCwdAttribution = true }] + }, + Clause { + Verb = [cmd2], Op = AndIf, + Args = [ + Arg { Raw = "file.txt", Resolved = "/target/file.txt", IsPath = true }, + Arg { Raw = "/target", Resolved = "/target", + IsPath = true, IsCwdAttribution = true } + ] + } +] +``` + +### `git -C` flag-with-value + +Input: `git -C /repo log` + +``` +Clauses = [ + Clause { + Verb = VerbChain { Tokens = ["git", "log"] }, + Args = [ + Arg { Raw = "-C", IsFlag = true }, + Arg { Raw = "/repo", IsPath = true, Resolved = "/repo" } + ] + } +] +``` + +### Redirect + +Input: `cmd > /tmp/out.txt` + +``` +Clauses = [ + Clause { + Verb = [cmd], + Args = [], + Redirects = [Redirect { Direction = Out, Target = "/tmp/out.txt" }] + } +] +``` + +### Subshell isolation + +Input: `cd /a && (cd /b && cmd1) && cmd2` + +``` +Clauses = [ + Clause { Verb = [cd], Args = [/a], Op = None }, + Clause { Verb = [cd], Args = [/b], Op = AndIf, IsSubshell = true, + Args = [/a attribution from outer compound] }, + Clause { Verb = [cmd1], Op = AndIf, IsSubshell = true, + Args = [/b attribution — local to subshell] }, + Clause { Verb = [cmd2], Op = AndIf, + Args = [/a attribution — inherited from outer cd, NOT /b] } +] +``` + +### Dynamic skip + +Input: `rm $UNRESOLVED/foo` + +``` +Clauses = [ + Clause { + Verb = [rm], + Args = [ + Arg { Raw = "$UNRESOLVED/foo", Kind = DynamicSkip, IsPath = false, + Resolved = null } + ] + } +] +``` + +Consumer impact: zone-gate sees zero paths to evaluate; routes to the +fallback "treat as one untrusted path = the raw token" prompt. + +### Unparseable + +Input: `for i in 1 2; do echo $i; done` + +``` +ParsedCommand { + Source = "for i in 1 2; do echo $i; done", + IsUnparseable = true, + UnparseableReason = "control-flow keyword 'for' is not supported in v0.1", + Clauses = [] // or partial; consumer should not rely on contents +} +``` + +--- + +## 13. Test Corpus Contract + +The corpus is the **acceptance contract** for the parser. Implementation is +"done" when every corpus entry parses to its expected AST. + +### Location + +`tests/Corpus/bash/*.json` — one file per corpus entry. File name pattern: +`NN_descriptive_slug.json` where NN is a zero-padded sequence number. + +### Format + +Each file: + +```json +{ + "name": "Multi-token verb: git push", + "input": "git push origin main", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": ["git", "push"], + "args": [ + { "raw": "origin", "kind": "Literal", "isPath": false }, + { "raw": "main", "kind": "Literal", "isPath": false } + ], + "redirects": [], + "isSubshell": false, + "isBashCWrapped": false + } + ] + }, + "notes": "Optional explanation of edge case being captured." +} +``` + +### Coverage targets for v0.1 + +Author at least: + +- 10 simple-verb cases (ls, pwd, echo, cat, grep, etc.) +- 10 multi-token-verb cases (git push, dotnet test, docker compose up, etc.) +- 15 compound cases (`&&`, `||`, `;`, `|` combinations) +- 10 `cd`-in-compound propagation cases (single, sequential, with subshell) +- 10 quote-handling cases (single, double, escaped, mixed) +- 10 redirect cases (`>`, `>>`, `<`, `2>`, `2>>`, multiple redirects) +- 10 subshell cases (with and without isolation effects) +- 10 `bash -c` recursion cases (depth 1, 2, with inner compounds) +- 10 dynamic-skip cases (`$VAR`, `${VAR}`, `~user`, glob args) +- 10 per-verb path-rule cases (chmod, chown, find, grep, curl, git -C, etc.) +- 10 unparseable cases (unbalanced quotes, control-flow keywords, function + definitions) + +**Total minimum: 105 entries.** Strive for 150+ once seeded from sanitized +real-world commands (see §14). + +### Test runner + +A single xunit test method enumerates `tests/Corpus/bash/*.json`, parses +each `input`, and asserts the result matches `expected` field-by-field. +The runner emits a per-corpus-entry test name so failures point at the +specific case. + +```csharp +[Theory] +[MemberData(nameof(CorpusEntries))] +public void Corpus_entry_parses_to_expected_ast(CorpusEntry entry) +{ + var parser = new BashParser(); + var actual = parser.Parse(entry.Input); + AstAssert.Equal(entry.Expected, actual); // structural equality +} + +public static IEnumerable CorpusEntries() +{ + var dir = Path.Combine(AppContext.BaseDirectory, "Corpus", "bash"); + foreach (var file in Directory.GetFiles(dir, "*.json")) + { + var entry = JsonSerializer.Deserialize(File.ReadAllText(file)); + yield return [entry]; + } +} +``` + +`AstAssert.Equal` is a helper that does structural equality with helpful +diff messages on mismatch. Implement to taste. + +--- + +## 14. Sanitization Process + +A portion of the corpus seeds from real shell commands captured from +agent dogfood logs. The seed source is a daemon log file at +`~/.netclaw/logs/daemon-2026-05-09.log` (and similar). These logs contain +PII (usernames, repo paths, channel/thread IDs) that **must not** appear +in the public corpus. + +### Sanitization rules + +Apply these transformations to every seeded entry **before** committing: + +| Pattern | Replacement | +|---|---| +| `/home//` (any specific username) | `/home/user/` | +| `/Users//` (macOS) | `/Users/user/` | +| `~//` | `~/` | +| Specific repo paths like `/home/user/repositories/stannardlabs/` | `/home/user/repos/sample-repo` | +| Specific repo names (not in the org's public list) | `sample-repo` or `project` | +| Slack channel IDs (`D[A-Z0-9]{10}`) | `` (only if appears in command) | +| Slack thread IDs (`\d{10}\.\d{6}`) | `` | +| Internal hostnames | `internal-host.example` | +| Email addresses | `user@example.com` | +| API keys, tokens, secrets (any `[A-Za-z0-9]{20,}` that looks key-shaped) | `` (but prefer to drop the entry entirely) | + +### Workflow + +1. Pull candidate commands from logs: + ```bash + grep -oP "command \K\{[^}]+\}" ~/.netclaw/logs/daemon-*.log \ + | jq -r .Command | sort -u > /tmp/raw-corpus.txt + ``` +2. Apply sanitization (script TBD) — for each line, walk the table above. +3. **Manual review** of each sanitized entry before committing. The script + can miss patterns; a human (or careful agent) reviews for residual PII. +4. Drop any entry that can't be cleanly sanitized (too many specific + identifiers; rewrite as a fully-synthetic entry instead). +5. Commit with a clear message: `chore(corpus): seed from sanitized agent + logs (NN entries)`. + +### Audit gate + +Before any corpus PR merges, CI runs a regex check against the corpus +files for residual PII patterns. The check fails the build if any +sanitization-rule pattern appears in any committed corpus file. Implement +as a small `dotnet test` that scans `tests/Corpus/bash/*.json` for the +forbidden patterns. + +--- + +## 15. CI & Release Flow + +The repo template already has: + +- `.github/workflows/pr_validation.yml` — runs `dotnet test` on PR. +- `.github/workflows/publish_nuget.yml` — publishes to NuGet on release tag. + +Adapt for ShellSyntaxTree: + +- **Trigger NuGet publish on tag pattern `v*.*.*`** (e.g. `v0.1.0-alpha`). +- **Test job** runs the corpus runner plus all unit tests. +- **PII audit job** runs the sanitization-pattern scan over `tests/Corpus/`. + +### Versioning + +- **v0.1.0-alpha** — first publishable cut. Bash-only. Public API surface + per §2 is locked; internal changes are free. +- **v0.1.x** — additive changes (more verb table entries, more corpus, + bug fixes). +- **v0.2.0** — first PowerShell parser implementation. +- **v1.0.0** — ready when at least one external consumer beyond Netclaw + ships against it without finding API gaps. + +### Release notes + +Update `RELEASE_NOTES.md` for each tagged release. Format: + +``` +0.1.0-alpha YYYY-MM-DD + +* First publishable cut. +* Bash parser per SPEC.md v0.1. +* Corpus: N entries. +* Public API: IShellParser, BashParser, ParsedCommand, Clause, VerbChain, + Arg, Redirect, ArgKind, RedirectDirection, CompoundOperator. +``` + +--- + +## 16. Implementation Sequencing + +A natural order for the implementer: + +1. **Bootstrap projects.** Create `src/ShellSyntaxTree/ShellSyntaxTree.csproj` + (library) and `tests/ShellSyntaxTree.Tests/ShellSyntaxTree.Tests.csproj` + (xunit). Update `SampleSln.slnx` (rename to `ShellSyntaxTree.slnx`) and + delete the `Akka.Console` sample. +2. **Update template defaults.** `Directory.Build.props`: replace Akka + metadata with ShellSyntaxTree. `README.md`: real intro. `LICENSE`: keep + Apache-2.0 (already correct). `Directory.Packages.props`: add xunit, drop + Akka.Hosting. `Tags`: bash, shell, parser, ast. +3. **Write public API skeleton** (§2): interface + record stubs that compile + but throw `NotImplementedException` on `Parse()`. Lock the surface + first. +4. **Implement BashLexer** (§5). Heavy unit tests on tokenization. +5. **Implement BashArity / FILE / CWD verb tables** (§6) as static data. +6. **Implement BashParser** (§4). One production at a time; unit-test + each. +7. **Implement Resolver** (§8). Unit-test each resolution rule. +8. **Implement per-verb path-arg rules** (§7). Unit-test per verb. +9. **Implement cd-in-compound propagation** (§9). Unit-test. +10. **Implement subshell + bash -c recursion** (§10). Unit-test. +11. **Implement parser anomaly safe-fail** (§11). Unit-test. +12. **Author corpus** (§13) — start with 105 hand-authored entries + covering each section. Iterate parser to make all pass. +13. **Sanitize and seed from real logs** (§14) — script + manual review. + Add 50-100 more corpus entries. +14. **Wire CI** (§15). Tag v0.1.0-alpha when corpus is green and PII audit + passes. + +Estimated implementation effort: 600-800 LOC of source + 400-600 LOC of +test infrastructure + 100-150 corpus entries (~50 KB JSON). + +--- + +## 17. Acceptance Criteria + +v0.1.0-alpha ships when **all** of the following hold: + +1. ✅ Public API matches §2 exactly. `dotnet pack` produces a + ShellSyntaxTree.0.1.0-alpha.nupkg. +2. ✅ Every corpus entry in `tests/Corpus/bash/*.json` parses to its + expected AST. `dotnet test` runs them all and passes. +3. ✅ Corpus has at least 105 entries spanning the categories in §13. +4. ✅ PII audit scan over `tests/Corpus/bash/*.json` finds zero hits. +5. ✅ `dotnet test` runs on PR via GitHub Actions and passes. +6. ✅ Tagging `v0.1.0-alpha` triggers `publish_nuget.yml` and the package + appears on nuget.org. +7. ✅ Netclaw can consume the package via `` and the + `IShellParser` resolves at runtime in Netclaw's DI container. +8. ✅ At least one Netclaw integration test exercises a real corpus entry + through the live Netclaw matcher and gets the expected gate decision. + +--- + +## 18. Out of Scope (deferred from v0.1) + +- PowerShell and cmd parsers. +- Variable expansion (any kind). +- Heredoc body extraction. +- Process substitution `<(cmd)`, `>(cmd)`. +- Function definitions. +- Arithmetic expansion `$((...))`. +- `for`/`while`/`case` control flow. +- Performance optimization beyond "fast enough" (~1ms typical). +- Source-mapping (line/column for AST nodes — useful for IDEs, irrelevant + for security gates). +- Extensible verb table loading from config (v0.1 ships static tables; + consumers can layer their own knowledge on top via `BashParserOptions` + in a future version). + +--- + +## Appendix A: Consumer Contract (Netclaw) + +What Netclaw expects from this library: + +1. `IShellParser` is registered in DI and `Parse(string)` returns a + `ParsedCommand` that Netclaw walks. +2. For each `Clause`, Netclaw extracts: + - `Verb.Tokens` for the verb-pattern gate evaluation. + - All `Args` where `IsPath = true` (excluding `IsCwdAttribution = true` + when the resolved path already appears in another arg) for the zone + gate evaluation. + - All `Redirects` where the target is path-shaped — the target is a + path the clause "operates on" for zone-gate purposes. +3. When `IsUnparseable = true`, Netclaw routes to safe-fail (prompt user; + offer Once / Deny only). +4. When any `Arg.Kind = DynamicSkip`, Netclaw treats that token as + "path unknown" — falls back to prompting on the raw command for the + zone gate. +5. Hard-deny rules in Netclaw evaluate against parsed `Clause` records, + not raw text (except the `rawText` escape-hatch rules — those operate + on the **rendered clause string**, recoverable via + `Clause.ToCommandString()` if we add it, or `string.Join(" ", verb + + args + redirects)` if we don't). + +The contract is stable — additive changes to AST records (new fields with +default values) are compatible; renaming or removing fields is breaking +and requires a major version bump. + +--- + +## Appendix B: Why not tree-sitter-bash? + +OpenCode (Node) uses tree-sitter-bash. We considered porting that approach +to .NET. The packaging cost is real: + +- No first-class .NET tree-sitter binding. Community bindings exist but + vary in maintenance. +- Native dependency: ship `libtree-sitter` + `libtree-sitter-bash` per + platform (Linux x64, Linux arm64, macOS x64, macOS arm64, Windows x64). + Five binaries to ship and maintain, plus PowerShell would need a + separate native lib. +- AOT-trimming compatibility is uncertain. +- We don't need IDE-grade fidelity. Fork bombs and function definitions + legitimately confuse our parser; we want them to mark `IsUnparseable` + so the consumer routes to safe-fail. tree-sitter would parse them and + we'd have to teach the consumer to ignore the result anyway. + +The hand-rolled approach trades a higher ceiling for control over scope, +zero native deps, and a clean upgrade path to PowerShell via the same +`IShellParser` seam. For our use case, that trade is correct. diff --git a/SampleSln.slnx b/SampleSln.slnx deleted file mode 100644 index d9693ae..0000000 --- a/SampleSln.slnx +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/ShellSyntaxTree.slnx b/ShellSyntaxTree.slnx new file mode 100644 index 0000000..5ba88fc --- /dev/null +++ b/ShellSyntaxTree.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/TOOLING.md b/TOOLING.md new file mode 100644 index 0000000..bc470e6 --- /dev/null +++ b/TOOLING.md @@ -0,0 +1,136 @@ +# Tooling Inventory — ShellSyntaxTree + +What's available to agents working in this repo, how to access it, and what +it enables. Update this file when tooling actually changes; keep it concise. + +## Runtime and Build + +| Tool | Access | Purpose | +|---|---|---| +| .NET SDK | pinned by `global.json` (10.x line, `rollForward: major`) | build, test, pack, restore | +| `dotnet` CLI | shell | every build action | +| Local .NET tools | `.config/dotnet-tools.json` (run `dotnet tool restore`) | currently `incrementalist` (selective rebuilds) | + +DocFX and `build.ps1` (Akka template release-notes injector) were intentionally +removed — this repo does not need them. + +## Test Stack + +| Tool | Access | Purpose | +|---|---|---| +| xUnit 2.9.x | NuGet via Central Package Management | unit + corpus tests | +| `Microsoft.NET.Test.Sdk` 17.11+ | NuGet | test discovery | +| `coverlet.collector` | NuGet | code coverage | + +The corpus runner (SPEC §13) is a single `[Theory] [MemberData]` test that +enumerates `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` and asserts each +parses to its declared `expected` AST. + +## Source Control and CI + +| Tool | Access | Purpose | +|---|---|---| +| `git` | shell | everything | +| `gh` CLI | shell | issues, PRs, releases, tag pushes | +| GitHub Actions | `.github/workflows/` | `pr_validation.yml` (build + test + pack on PR/push), `publish_nuget.yml` (pack + push + release on `v*.*.*` tags) | +| GitHub Dependabot | `.github/dependabot.yml` | NuGet bumps | + +## NuGet + +| Tool | Access | Purpose | +|---|---|---| +| `NuGet.Config` | repo root | feed configuration (currently `nuget.org`) | +| `dotnet pack` | shell | produces `.nupkg` and `.snupkg` (symbol package) | +| `dotnet nuget push` | CI only (`NUGET_KEY` secret) | publishes to `nuget.org` on tag | +| Central Package Management | `Directory.Packages.props` | single source of truth for package versions | + +## Source-Level Conventions + +- `Directory.Build.props` enforces: `Nullable=enable`, `LangVersion=latest`, + `TreatWarningsAsErrors=true`. Any new project inherits these. +- Solution format is `.slnx` (`ShellSyntaxTree.slnx`). +- Every `.cs` file under `src/` and `tests/` carries a copyright header. + Use `scripts/Add-FileHeaders.ps1` to add them; CI runs the same script + with `-Verify` and fails the build on missing headers. + +### Copyright header script + +| Command | Purpose | +|---|---| +| `pwsh ./scripts/Add-FileHeaders.ps1` | Add Aaron Stannard copyright headers to all `.cs` files missing them under `src/` and `tests/` | +| `pwsh ./scripts/Add-FileHeaders.ps1 -WhatIf` | Preview which files would be modified | +| `pwsh ./scripts/Add-FileHeaders.ps1 -Verify` | CI mode: exit 1 if any file is missing a header | + +The script auto-detects file creation year via `git log --diff-filter=A` +and falls back to the current year for new files. + +## External Resources + +| Resource | Access | Purpose | +|---|---|---| +| Netclaw repo | local at `/home/petabridge/repositories/stannardlabs/netclaw` (also `github.com/netclaw-dev/netclaw`) | reference for the consumer contract; read `src/Netclaw.Security/ShellApprovalSemantics.cs` and `ShellTokenizer.cs` to see the surface ShellSyntaxTree replaces | +| Daemon dogfood logs (Netclaw) | `~/.netclaw/logs/daemon-*.log` (operator's machine; not in CI) | corpus seed source — strict sanitization required (SPEC §14) before any entry lands in the public corpus | + +## dotnet-skills Marketplace + +Install-once skill bundle that surfaces curated .NET guidance. Prefer +retrieval-led reasoning (open the skill) over pretraining for any non-trivial +.NET work. Routing for this repo: + +| Topic | Skill | +|---|---| +| Project layout / `.slnx` / `Directory.Build.props` / SourceLink | `dotnet-skills:project-structure` | +| Central Package Management, `dotnet add/remove` | `dotnet-skills:package-management` | +| Modern C# (records, pattern matching, value objects, Span/Memory) | `dotnet-skills:csharp-coding-standards` | +| Public API stability and extend-only design | `dotnet-skills:csharp-api-design` | +| Type/perf design (sealed classes, readonly structs, etc.) | `dotnet-skills:csharp-type-design-performance` | +| xUnit + corpus discipline | (no dedicated skill; corpus contract is in SPEC §13) | +| Snapshot/golden testing | `dotnet-skills:snapshot-testing` | +| Reward-hacking detector after code changes | `dotnet-skills:slopwatch` | +| Coverage / risk hot-spots | `dotnet-skills:crap-analysis` | +| Decompile a NuGet to confirm behavior | `dotnet-skills:ilspy-decompile` | +| Marketplace publishing workflow (skills, not this lib) | `dotnet-skills:marketplace-publishing` | + +Skills are advisory. Open the skill, apply the parts that fit ShellSyntaxTree, +note conflicts back here. + +## Specialist Agents + +Available via the `Agent` tool: + +- `dotnet-skills:dotnet-concurrency-specialist` — only relevant if the + parser ever grows mutable state (currently it shouldn't). +- `dotnet-skills:dotnet-performance-analyst` — for the "fast enough" + budget (~1 ms typical) if it's ever in doubt. +- `dotnet-skills:dotnet-benchmark-designer` — if benchmarks become + needed (SPEC explicitly defers performance tuning). +- `pr-review-specialist` — coordinated PR review, especially for the + initial public-API-locking PR. + +## Helper Skills (Claude Code) + +| Skill | When to use | +|---|---| +| `/init` | (already done — generated CLAUDE.md companion) | +| `/commit` | Branch-and-commit hygiene (creates feature branch off `dev` if needed) | +| `/pr` | Open PR against `dev` | +| `/review-pr` | Pre-flight review before requesting human review | +| `/security-review` | Targeted security review — relevant for this library | +| `/generate-image` | HCTI-backed image generation; used to mint the NuGet package icon | + +## Image Generation (HCTI) + +Used to mint the package icon. Output goes to `assets/icon.png` (or similar) +and is wired into `Directory.Build.props` via `PackageIcon`. The icon is the +only graphical artifact this repo needs. + +## Working Assumptions + +- Single maintainer (Aaron Stannard). No org-wide review SLA. +- CI is the only place that publishes packages; local `dotnet nuget push` is + not used. +- Dogfood log sanitization happens on the operator's machine before any + corpus entry is committed. CI enforces a regex audit; it does not + sanitize. +- No external API/secret credentials are required to build or test. + Publishing to nuget.org requires the `NUGET_KEY` repo secret. diff --git a/akkalogo.png b/akkalogo.png deleted file mode 100644 index 0f9cf4e..0000000 Binary files a/akkalogo.png and /dev/null differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..e0862aa Binary files /dev/null and b/assets/icon.png differ diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 221898a..0000000 --- a/build.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -. "$PSScriptRoot\scripts\getReleaseNotes.ps1" -. "$PSScriptRoot\scripts\bumpVersion.ps1" - -###################################################################### -# Step 1: Grab release notes and update solution metadata -###################################################################### -$releaseNotes = Get-ReleaseNotes -MarkdownFile (Join-Path -Path $PSScriptRoot -ChildPath "RELEASE_NOTES.md") - -# inject release notes into Directory.Buil -UpdateVersionAndReleaseNotes -ReleaseNotesResult $releaseNotes -XmlFilePath (Join-Path -Path $PSScriptRoot -ChildPath "Directory.Build.props") - -Write-Output "Added release notes $releaseNotes" \ No newline at end of file diff --git a/scripts/Add-FileHeaders.ps1 b/scripts/Add-FileHeaders.ps1 new file mode 100644 index 0000000..261a16d --- /dev/null +++ b/scripts/Add-FileHeaders.ps1 @@ -0,0 +1,139 @@ +<# +.SYNOPSIS + Adds Aaron Stannard copyright headers to C# files that don't have them. + +.DESCRIPTION + Scans all .cs files in src/ and tests/ directories and adds the standard + copyright header to files that are missing it. + +.PARAMETER WhatIf + Shows what files would be modified without making changes. + +.PARAMETER Verify + Returns exit code 1 if any files are missing headers (for CI validation). + +.EXAMPLE + ./scripts/Add-FileHeaders.ps1 + Adds headers to all files missing them. + +.EXAMPLE + ./scripts/Add-FileHeaders.ps1 -WhatIf + Shows which files would be modified. + +.EXAMPLE + ./scripts/Add-FileHeaders.ps1 -Verify + Checks if all files have headers (for CI). +#> + +param( + [switch]$WhatIf, + [switch]$Verify +) + +$ErrorActionPreference = "Stop" + +$rootDir = Split-Path -Parent $PSScriptRoot +$currentYear = (Get-Date).Year + +function Get-FileHeader { + param([string]$FileName, [int]$CreatedYear) + + return @" +// ----------------------------------------------------------------------- +// +// Copyright (C) $CreatedYear - $currentYear Aaron Stannard +// +// ----------------------------------------------------------------------- + +"@ +} + +function Test-HasHeader { + param([string]$Content) + return $Content -match "^\s*(//|/\*)\s*-{5,}" -or $Content -match "$null | Select-Object -First 1 + if ($gitYear) { + return [int]($gitYear.Substring(0, 4)) + } + + # Fall back to current year for new files + return $currentYear +} + +# Find all C# files in src/ and tests/ +$searchPaths = @( + (Join-Path $rootDir "src"), + (Join-Path $rootDir "tests") +) + +$csFiles = @() +foreach ($path in $searchPaths) { + if (Test-Path $path) { + $csFiles += Get-ChildItem -Path $path -Filter "*.cs" -Recurse | + Where-Object { $_.FullName -notmatch "[\\/](obj|bin)[\\/]" } + } +} + +$missingHeaders = @() +$processedCount = 0 + +foreach ($file in $csFiles) { + $content = Get-Content -Path $file.FullName -Raw -ErrorAction SilentlyContinue + + if (-not $content) { + continue + } + + if (-not (Test-HasHeader -Content $content)) { + $missingHeaders += $file + + if (-not $WhatIf -and -not $Verify) { + $createdYear = Get-FileCreatedYear -FilePath $file.FullName + $header = Get-FileHeader -FileName $file.Name -CreatedYear $createdYear + + # Handle BOM if present + $encoding = [System.Text.UTF8Encoding]::new($true) + $newContent = $header + $content.TrimStart() + + [System.IO.File]::WriteAllText($file.FullName, $newContent, $encoding) + $processedCount++ + Write-Host "Added header to: $($file.FullName -replace [regex]::Escape($rootDir), '')" + } + } +} + +if ($WhatIf) { + if ($missingHeaders.Count -eq 0) { + Write-Host "All files have headers." -ForegroundColor Green + } else { + Write-Host "Files missing headers:" -ForegroundColor Yellow + foreach ($file in $missingHeaders) { + Write-Host " $($file.FullName -replace [regex]::Escape($rootDir), '')" + } + Write-Host "`n$($missingHeaders.Count) file(s) would be modified." -ForegroundColor Yellow + } +} elseif ($Verify) { + if ($missingHeaders.Count -eq 0) { + Write-Host "All files have headers." -ForegroundColor Green + exit 0 + } else { + Write-Host "Files missing headers:" -ForegroundColor Red + foreach ($file in $missingHeaders) { + Write-Host " $($file.FullName -replace [regex]::Escape($rootDir), '')" + } + Write-Host "`n$($missingHeaders.Count) file(s) missing headers. Run './scripts/Add-FileHeaders.ps1' to fix." -ForegroundColor Red + exit 1 + } +} else { + if ($processedCount -eq 0) { + Write-Host "All files already have headers." -ForegroundColor Green + } else { + Write-Host "`nAdded headers to $processedCount file(s)." -ForegroundColor Green + } +} diff --git a/scripts/bumpVersion.ps1 b/scripts/bumpVersion.ps1 deleted file mode 100644 index cdc46ed..0000000 --- a/scripts/bumpVersion.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -function UpdateVersionAndReleaseNotes { - param ( - [Parameter(Mandatory=$true)] - [PSCustomObject]$ReleaseNotesResult, - - [Parameter(Mandatory=$true)] - [string]$XmlFilePath - ) - - # Load XML - $xmlContent = New-Object XML - $xmlContent.Load($XmlFilePath) - - # Update VersionPrefix and PackageReleaseNotes - $versionPrefixElement = $xmlContent.SelectSingleNode("//VersionPrefix") - $versionPrefixElement.InnerText = $ReleaseNotesResult.Version - - $packageReleaseNotesElement = $xmlContent.SelectSingleNode("//PackageReleaseNotes") - $packageReleaseNotesElement.InnerText = $ReleaseNotesResult.ReleaseNotes - - # Save the updated XML - $xmlContent.Save($XmlFilePath) -} - -# Usage example: -# $notes = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" -# $propsPath = Join-Path -Path (Get-Item $PSScriptRoot).Parent.FullName -ChildPath "Directory.Build.props" -# UpdateVersionAndReleaseNotes -ReleaseNotesResult $notes -XmlFilePath $propsPath diff --git a/scripts/getReleaseNotes.ps1 b/scripts/getReleaseNotes.ps1 deleted file mode 100644 index 419d85f..0000000 --- a/scripts/getReleaseNotes.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -function Get-ReleaseNotes { - param ( - [Parameter(Mandatory=$true)] - [string]$MarkdownFile - ) - - # Read markdown file content - $content = Get-Content -Path $MarkdownFile -Raw - - # Split content based on headers - $sections = $content -split "####" - - # Output object to store result - $outputObject = [PSCustomObject]@{ - Version = $null - Date = $null - ReleaseNotes = $null - } - - # Check if we have at least 3 sections (1. Before the header, 2. Header, 3. Release notes) - if ($sections.Count -ge 3) { - $header = $sections[1].Trim() - $releaseNotes = $sections[2].Trim() - - # Extract version and date from the header - $headerParts = $header -split " ", 2 - if ($headerParts.Count -eq 2) { - $outputObject.Version = $headerParts[0] - $outputObject.Date = $headerParts[1] - } - - $outputObject.ReleaseNotes = $releaseNotes - } - - # Return the output object - return $outputObject -} - -# Call function example: -#$result = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" -#Write-Output "Version: $($result.Version)" -#Write-Output "Date: $($result.Date)" -#Write-Output "Release Notes:" -#Write-Output $result.ReleaseNotes diff --git a/src/Akka.Console/Akka.Console.csproj b/src/Akka.Console/Akka.Console.csproj deleted file mode 100644 index dd9b804..0000000 --- a/src/Akka.Console/Akka.Console.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - - - - - - diff --git a/src/Akka.Console/HelloActor.cs b/src/Akka.Console/HelloActor.cs deleted file mode 100644 index 880c08b..0000000 --- a/src/Akka.Console/HelloActor.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Akka.Console; - -public class HelloActor : ReceiveActor -{ - private readonly ILoggingAdapter _log = Context.GetLogger(); - private int _helloCounter = 0; - - public HelloActor() - { - Receive(message => - { - _log.Info("{0} {1}", message, _helloCounter++); - }); - } -} \ No newline at end of file diff --git a/src/Akka.Console/Program.cs b/src/Akka.Console/Program.cs deleted file mode 100644 index 4d8962c..0000000 --- a/src/Akka.Console/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Akka.Hosting; -using Akka.Console; -using Microsoft.Extensions.Hosting; - -var hostBuilder = new HostBuilder(); - -hostBuilder.ConfigureServices((context, services) => -{ - services.AddAkka("MyActorSystem", (builder, sp) => - { - builder - .WithActors((system, registry, resolver) => - { - var helloActor = system.ActorOf(Props.Create(() => new HelloActor()), "hello-actor"); - registry.Register(helloActor); - }) - .WithActors((system, registry, resolver) => - { - var timerActorProps = - resolver.Props(); // uses Msft.Ext.DI to inject reference to helloActor - var timerActor = system.ActorOf(timerActorProps, "timer-actor"); - registry.Register(timerActor); - }); - }); -}); - -var host = hostBuilder.Build(); - -await host.RunAsync(); \ No newline at end of file diff --git a/src/Akka.Console/README.md b/src/Akka.Console/README.md deleted file mode 100644 index 0c15d99..0000000 --- a/src/Akka.Console/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Akka.NET Console Template - -This is a simple template designed to incorporate local [Akka.NET](https://getakka.net/) into a console application. - -See https://github.com/akkadotnet/akkadotnet-templates/blob/dev/docs/ConsoleTemplate.md for complete and current documentation on this template. \ No newline at end of file diff --git a/src/Akka.Console/TimerActor.cs b/src/Akka.Console/TimerActor.cs deleted file mode 100644 index 823960d..0000000 --- a/src/Akka.Console/TimerActor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Akka.Hosting; - -namespace Akka.Console; - -public class TimerActor : ReceiveActor, IWithTimers -{ - private readonly IActorRef _helloActor; - - public TimerActor(IRequiredActor helloActor) - { - _helloActor = helloActor.ActorRef; - Receive(message => - { - _helloActor.Tell(message); - }); - } - - protected override void PreStart() - { - Timers.StartPeriodicTimer("hello-key", "hello", TimeSpan.FromSeconds(1)); - } - - public ITimerScheduler Timers { get; set; } = null!; // gets set by Akka.NET -} \ No newline at end of file diff --git a/src/Akka.Console/Usings.cs b/src/Akka.Console/Usings.cs deleted file mode 100644 index 15066e8..0000000 --- a/src/Akka.Console/Usings.cs +++ /dev/null @@ -1,2 +0,0 @@ -global using Akka.Actor; -global using Akka.Event; \ No newline at end of file