Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Pre-commit hook: fast gate (build + unit tests + race detection).
# Full CI (integration, e2e, coverage thresholds) runs in CI pipeline.
# Per DORA research: pre-commit should be <10s for fast feedback loops.

set -euo pipefail

echo "==> Pre-commit: build check"
go build -o /dev/null ./cmd/

echo "==> Pre-commit: unit tests with race detection"
go test -race ./pkg/chatarchive/... ./internal/chatarchivecmd/...

echo "Pre-commit checks passed."
Empty file modified .github/hooks/pre-commit
100644 → 100755
Empty file.
78 changes: 78 additions & 0 deletions .github/workflows/chatarchive-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Chat Archive Quality

on:
pull_request:
paths:
- 'pkg/chatarchive/**'
- 'internal/chatarchivecmd/**'
- 'cmd/create/chat_archive.go'
- 'cmd/backup/chats.go'
- 'test/e2e/**'
- 'scripts/chatarchive-ci.sh'
- 'package.json'
- '.github/hooks/pre-commit'
- '.github/hooks/setup-hooks.sh'
- 'scripts/install-git-hooks.sh'
- '.github/workflows/chatarchive-quality.yml'
push:
branches:
- main
- develop
paths:
- 'pkg/chatarchive/**'
- 'internal/chatarchivecmd/**'
- 'cmd/create/chat_archive.go'
- 'cmd/backup/chats.go'
- 'test/e2e/**'
- 'scripts/chatarchive-ci.sh'
- 'package.json'
- '.github/hooks/pre-commit'
- '.github/hooks/setup-hooks.sh'
- 'scripts/install-git-hooks.sh'
- '.github/workflows/chatarchive-quality.yml'

jobs:
chatarchive-ci:
name: Chat Archive CI (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Download Go modules
run: go mod download
shell: bash

- name: Run chat archive CI
run: npm run ci
Comment on lines +61 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Invoke the chatarchive pipeline from the new workflow

This job never runs the new chatarchive checks. I checked package.json, and npm run ci still maps to npm run ci:debug --silent, so scripts/chatarchive-ci.sh is never executed here. As a result the dedicated unit/integration/e2e/coverage gates for pkg/chatarchive/** are skipped, and the workflow usually will not produce the outputs/chatarchive-ci/ artifacts it tries to upload.

Useful? React with 👍 / 👎.

shell: bash

- name: Upload verification summary
if: always()
uses: actions/upload-artifact@v4
with:
name: chatarchive-summary-${{ matrix.os }}
path: outputs/chatarchive-ci/

- name: Publish job summary
if: always()
shell: bash
run: |
if [[ -f outputs/chatarchive-ci/summary.txt ]]; then
cat outputs/chatarchive-ci/summary.txt >> "$GITHUB_STEP_SUMMARY"
fi
Comment on lines +36 to +78

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 25 days ago

In general, to fix this class of issue you explicitly declare a permissions block at the workflow level (applies to all jobs) or at the specific job level (overrides workflow defaults) that grants only the minimum scopes required. For a pure CI workflow that only checks out code, runs tests, and uploads artifacts, contents: read is typically sufficient; actions/upload-artifact does not require elevated GitHub API permissions beyond what contents: read provides for the checkout step.

For this specific workflow, the simplest and safest fix without changing existing functionality is to add a workflow-level permissions block right after the name declaration and before the on: trigger. This block should set contents: read, which is sufficient for actions/checkout@v4 to read the repository and does not grant write access to any resources. No job-specific overrides are necessary because the single chatarchive-ci job only needs read access. No additional imports or methods are required, as this is purely a YAML configuration change.

Concretely:

  • Edit .github/workflows/chatarchive-quality.yml.

  • Insert:

    permissions:
      contents: read

    between the existing line name: Chat Archive Quality and the on: block.

  • Leave the rest of the workflow unchanged.

Suggested changeset 1
.github/workflows/chatarchive-quality.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/chatarchive-quality.yml b/.github/workflows/chatarchive-quality.yml
--- a/.github/workflows/chatarchive-quality.yml
+++ b/.github/workflows/chatarchive-quality.yml
@@ -1,5 +1,8 @@
 name: Chat Archive Quality
 
+permissions:
+  contents: read
+
 on:
   pull_request:
     paths:
EOF
@@ -1,5 +1,8 @@
name: Chat Archive Quality

permissions:
contents: read

on:
pull_request:
paths:
Copilot is powered by AI and may make mistakes. Always verify output.
26 changes: 25 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ PY
run: scripts/ci/preflight.sh

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.0
run: bash scripts/ci/install-golangci-lint.sh

- name: Run lint lane
env:
Expand Down Expand Up @@ -245,6 +245,10 @@ PY
run: |
test -f outputs/ci/unit/report.json && cat outputs/ci/unit/report.json || true

- name: Alert on unit lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-unit outputs/ci/unit/report.json

ci-deps-unit:
name: ci-deps-unit
runs-on: ubuntu-latest
Expand Down Expand Up @@ -295,6 +299,10 @@ PY
run: |
test -f outputs/ci/deps-unit/report.json && cat outputs/ci/deps-unit/report.json || true

- name: Alert on dependency-focused unit lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-deps-unit outputs/ci/deps-unit/report.json

ci-integration:
name: ci-integration
runs-on: ubuntu-latest
Expand Down Expand Up @@ -344,6 +352,10 @@ PY
run: |
test -f outputs/ci/integration/report.json && cat outputs/ci/integration/report.json || true

- name: Alert on integration lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-integration outputs/ci/integration/report.json

ci-e2e-smoke:
name: ci-e2e-smoke
runs-on: ubuntu-latest
Expand Down Expand Up @@ -391,6 +403,10 @@ PY
run: |
test -f outputs/ci/e2e-smoke/report.json && cat outputs/ci/e2e-smoke/report.json || true

- name: Alert on e2e smoke lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-e2e-smoke outputs/ci/e2e-smoke/report.json

ci-fuzz:
name: ci-fuzz
runs-on: ubuntu-latest
Expand Down Expand Up @@ -438,6 +454,10 @@ PY
run: |
test -f outputs/ci/fuzz/report.json && cat outputs/ci/fuzz/report.json || true

- name: Alert on fuzz lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-fuzz outputs/ci/fuzz/report.json

ci-e2e-full:
name: ci-e2e-full
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
Expand Down Expand Up @@ -485,3 +505,7 @@ PY
if: always()
run: |
test -f outputs/ci/e2e-full/report.json && cat outputs/ci/e2e-full/report.json || true

- name: Alert on e2e full lane report
if: always()
run: python3 scripts/ci/report-alert.py ci-e2e-full outputs/ci/e2e-full/report.json
16 changes: 8 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ repos:
# Local hooks for custom checks
- repo: local
hooks:
# Enforce local/CI parity lane via mage-compatible entrypoint
# Enforce local/CI parity lane via npm wrapper
- id: ci-debug-parity
name: CI debug parity gate (magew ci:debug)
entry: ./magew ci:debug
name: CI debug parity gate (npm run ci:debug)
entry: npm run ci:debug --silent
language: system
pass_filenames: false
require_serial: true
Expand Down Expand Up @@ -80,13 +80,13 @@ repos:
pass_filenames: false
description: Ensures code compiles successfully

# Verify E2E tests have build tags
- id: verify-e2e-build-tags
name: Verify E2E build tags
entry: bash -c 'for f in test/e2e/*_test.go; do head -1 "$f" | grep -q "//go:build e2e" || { echo "ERROR: $f missing //go:build e2e tag"; exit 1; }; done'
# Verify environment-dependent tests are build-tagged
- id: verify-test-build-tags
name: Verify test build tags
entry: bash scripts/ci/check-test-tags.sh
language: system
pass_filenames: false
description: Ensures all E2E tests have proper build tags
description: Ensures environment-dependent tests keep their explicit build tags

# Check for deprecated benchmark pattern
- id: check-benchmark-pattern
Expand Down
37 changes: 37 additions & 0 deletions cmd/create/chat_archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// cmd/create/chat_archive.go
//
// Thin orchestration layer for chat-archive. Business logic lives in
// pkg/chatarchive/ per the cmd/ vs pkg/ enforcement rule.

package create

import (
"github.com/CodeMonkeyCybersecurity/eos/internal/chatarchivecmd"
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
"github.com/spf13/cobra"
)

// CreateChatArchiveCmd copies and deduplicates chat transcripts.
var CreateChatArchiveCmd = &cobra.Command{
Use: "chat-archive",
Short: "Copy and deduplicate chat transcripts into a local archive",
Long: `Find transcript-like files (jsonl/json/html), copy unique files into one archive,
and write an index manifest with duplicate mappings.

Examples:
eos create chat-archive
eos create chat-archive --source ~/.claude --source ~/dev
eos create chat-archive --exclude conversation-api --exclude .cache
eos create chat-archive --dry-run`,
RunE: eos.Wrap(runCreateChatArchive),
}

func init() {
CreateCmd.AddCommand(CreateChatArchiveCmd)
chatarchivecmd.BindFlags(CreateChatArchiveCmd)
}

func runCreateChatArchive(rc *eos_io.RuntimeContext, cmd *cobra.Command, _ []string) error {
return chatarchivecmd.Run(rc, cmd)
}
50 changes: 50 additions & 0 deletions docs/inspect-follow-up-issues-2026-03-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
*Last Updated: 2026-03-21*

# pkg/inspect Follow-Up Issues

Issues discovered during adversarial review of `pkg/inspect/docker.go`.

## Issue 1: output.go / terraform_modular.go — 37 staticcheck warnings (P2)

**Problem**: `WriteString(fmt.Sprintf(...))` should be `fmt.Fprintf(...)` throughout output.go and terraform_modular.go.
**Impact**: Performance (unnecessary string allocation) and lint noise.
**Fix**: Replace all `tf.WriteString(fmt.Sprintf(...))` with `fmt.Fprintf(tf, ...)`.
**Files**: `pkg/inspect/output.go`, `pkg/inspect/terraform_modular.go`
**Effort**: ~30 min mechanical refactor

## Issue 2: services.go — unchecked filepath.Glob error (P2)

**Problem**: `pkg/inspect/services.go:381` ignores `filepath.Glob` error.
**Impact**: Silent failure when glob patterns are invalid.
**Fix**: Check and log the error.
**Effort**: 5 min

## Issue 3: kvm.go — goconst violations (P3)

**Problem**: String constants `"active"`, `"UUID"` repeated without named constants.
**Impact**: Violates P0 Rule #12 (no hardcoded values).
**Fix**: Extract to constants in `kvm.go` or a `constants.go` file.
**Effort**: 15 min

## Issue 4: Pre-existing lint issues across 30+ files on this branch (P1)

**Problem**: `npm run ci` fails due to 165 lint issues across the branch.
**Impact**: Cannot merge until resolved.
**Root cause**: Accumulated tech debt from many feature PRs merged without lint cleanup.
**Fix**: Dedicated lint cleanup pass before PR merge.
**Effort**: 2-4 hours

## Issue 5: Inspector lacks Docker SDK integration (P3)

**Problem**: All Docker operations use shell commands instead of the Docker SDK.
**Impact**: Fragile parsing, no type safety, extra process spawns.
**Fix**: Migrate to `github.com/docker/docker/client` SDK for container/image/network/volume operations.
**Rationale**: CLAUDE.md P1 states "ALWAYS use Docker SDK" for container operations.
**Effort**: 1-2 days

## Issue 6: Compose file search does not guard against TOCTOU (P3)

**Problem**: Between `os.Stat` size check and `os.ReadFile`, the file could be replaced.
**Impact**: Theoretical DoS via race condition on symlink swap.
**Fix**: Read file first, then check size of bytes read (simpler and race-free).
**Effort**: 15 min
105 changes: 105 additions & 0 deletions internal/chatarchivecmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package chatarchivecmd

import (
"fmt"
"io"
"strings"
"time"

"github.com/CodeMonkeyCybersecurity/eos/pkg/chatarchive"
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
"github.com/spf13/cobra"
"github.com/uptrace/opentelemetry-go-extra/otelzap"
"go.uber.org/zap"
)

func BindFlags(cmd *cobra.Command) {
cmd.Flags().StringSlice("source", chatarchive.DefaultSources(), "Source directories to scan")
cmd.Flags().String("dest", chatarchive.DefaultDest(), "Destination archive directory")
cmd.Flags().StringSlice("exclude", nil, "Path substrings to exclude from discovery (e.g. --exclude conversation-api)")
cmd.Flags().Bool("dry-run", false, "Show what would be archived without copying files")
}

func Run(rc *eos_io.RuntimeContext, cmd *cobra.Command) error {
sources, _ := cmd.Flags().GetStringSlice("source")

Check failure on line 24 in internal/chatarchivecmd/run.go

View workflow job for this annotation

GitHub Actions / Chat Archive CI (macos-latest)

Error return value of `(*github.com/spf13/pflag.FlagSet).GetStringSlice` is not checked (errcheck)
dest, _ := cmd.Flags().GetString("dest")

Check failure on line 25 in internal/chatarchivecmd/run.go

View workflow job for this annotation

GitHub Actions / Chat Archive CI (macos-latest)

Error return value of `(*github.com/spf13/pflag.FlagSet).GetString` is not checked (errcheck)
excludes, _ := cmd.Flags().GetStringSlice("exclude")

Check failure on line 26 in internal/chatarchivecmd/run.go

View workflow job for this annotation

GitHub Actions / Chat Archive CI (macos-latest)

Error return value of `(*github.com/spf13/pflag.FlagSet).GetStringSlice` is not checked (errcheck)
dryRun, _ := cmd.Flags().GetBool("dry-run")

Check failure on line 27 in internal/chatarchivecmd/run.go

View workflow job for this annotation

GitHub Actions / Chat Archive CI (macos-latest)

Error return value of `(*github.com/spf13/pflag.FlagSet).GetBool` is not checked (errcheck)

result, err := chatarchive.Archive(rc, chatarchive.Options{
Sources: sources,
Dest: dest,
Excludes: excludes,
DryRun: dryRun,
})
if err != nil {
return err
}

logger := otelzap.Ctx(rc.Ctx)
writeSummary(cmd.OutOrStdout(), result, dryRun, logger)
logger.Info("Chat archive summary",
zap.Int("sources_requested", result.SourcesRequested),
zap.Int("sources_scanned", result.SourcesScanned),
zap.Int("sources_missing", len(result.MissingSources)),
zap.Int("skipped_symlinks", result.SkippedSymlinks),
zap.Int("unreadable_entries", result.UnreadableEntries),
zap.Int("unique_files", result.UniqueFiles),
zap.Int("duplicates", result.Duplicates),
zap.Int("already_archived", result.Skipped),
zap.Int("empty_files", result.EmptyFiles),
zap.Int("failures", result.FailureCount),
zap.Duration("duration", result.Duration),
zap.Bool("dry_run", dryRun))
for _, failure := range result.Failures {
logger.Warn("Chat archive file failure",
zap.String("path", failure.Path),
zap.String("stage", failure.Stage),
zap.String("reason", failure.Reason))
}

return nil
}

func formatSummary(result *chatarchive.Result, dryRun bool) string {
lines := []string{
statusLine(dryRun),
fmt.Sprintf("Sources scanned: %d/%d", result.SourcesScanned, result.SourcesRequested),
fmt.Sprintf("Unique files: %d", result.UniqueFiles),
fmt.Sprintf("Duplicates in this run: %d", result.Duplicates),
fmt.Sprintf("Already archived: %d", result.Skipped),
fmt.Sprintf("Empty files ignored: %d", result.EmptyFiles),
fmt.Sprintf("File failures: %d", result.FailureCount),
fmt.Sprintf("Unreadable entries skipped: %d", result.UnreadableEntries),
fmt.Sprintf("Symlinks skipped: %d", result.SkippedSymlinks),
fmt.Sprintf("Duration: %s", result.Duration.Round(10*time.Millisecond)),
}

if result.ManifestPath != "" {
lines = append(lines, fmt.Sprintf("Manifest: %s", result.ManifestPath))
}
if result.RecoveredManifestPath != "" {
lines = append(lines, fmt.Sprintf("Recovered corrupt manifest: %s", result.RecoveredManifestPath))
}
if len(result.MissingSources) > 0 {
lines = append(lines, fmt.Sprintf("Unavailable sources: %s", strings.Join(result.MissingSources, ", ")))
}
if result.FailureCount > len(result.Failures) {
lines = append(lines, fmt.Sprintf("Additional failures not shown: %d", result.FailureCount-len(result.Failures)))
}

return strings.Join(lines, "\n")
}

func statusLine(dryRun bool) string {
if dryRun {
return "Dry run complete."
}
return "Archive complete."
}

func writeSummary(w io.Writer, result *chatarchive.Result, dryRun bool, logger otelzap.LoggerWithCtx) {
if _, err := fmt.Fprintln(w, formatSummary(result, dryRun)); err != nil {
logger.Warn("Failed to write chat archive summary", zap.Error(err))
}
}
Loading
Loading