Skip to content

feat: manifest-driven installer, lite mode, tests, and CI#3

Merged
WPHILLIPMACLAYNE merged 1 commit intomainfrom
agent/claude-opus-cortex-improvements
Apr 14, 2026
Merged

feat: manifest-driven installer, lite mode, tests, and CI#3
WPHILLIPMACLAYNE merged 1 commit intomainfrom
agent/claude-opus-cortex-improvements

Conversation

@WPHILLIPMACLAYNE
Copy link
Copy Markdown
Owner

Summary

  • Installer refactored to read file lists from MANIFEST.yaml instead of hardcoded Python list — single source of truth, no more divergence risk
  • Lite mode (--lite) installs only 3 essential files (PROJECT_CONTEXT, CURRENT_STATUS, AGENT_HANDOFF) — reduces adoption barrier for small projects
  • 12 smoke tests covering: manifest↔template consistency, full/lite install, dry-run, force, skip, upgrade lite→full, error cases
  • GitHub Actions CI running tests on Python 3.10/3.11/3.12
  • Filled example in examples/filled-cortex/ showing realistic .cortex/ files for onboarding

Motivation

Independent analysis identified a gap between CORTEX's "operating system" promise and its technical execution. This PR addresses the top priorities:

  1. Eliminates the hardcoded file list (dual source of truth)
  2. Adds progressive adoption path (Lite → Full)
  3. Introduces automated validation (the project that preaches "verify before changing" now verifies itself)
  4. Provides concrete examples instead of only documentation

Test plan

  • python3 tests/test_installer.py — 12/12 passing
  • python3 scripts/install_cortex.py --list-files — reads from MANIFEST.yaml
  • python3 scripts/install_cortex.py --list-files --lite — shows 3 files
  • python3 scripts/install_cortex.py <target> --dry-run — no files written
  • python3 scripts/install_cortex.py <target> --lite then without --lite — upgrade works
  • CI workflow validates on push

🤖 Generated with Claude Code

Refactor installer to read file lists from MANIFEST.yaml instead of
hardcoded list, eliminating the dual source of truth. Add --lite flag
for progressive adoption (3 essential files). Add 12 smoke tests,
GitHub Actions CI, and a filled .cortex/ example for onboarding.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copilot AI review requested due to automatic review settings April 14, 2026 23:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the CORTEX installer to be manifest-driven, adds a Lite installation mode, and introduces smoke tests + CI to validate installer behavior and manifest/template consistency.

Changes:

  • Refactor installer to source install file lists from MANIFEST.yaml and add --lite support.
  • Add installer smoke tests covering list-files, full/lite install, dry-run, force/skip behavior, and lite→full upgrade.
  • Add CI workflow to run the smoke tests across Python 3.10/3.11/3.12 and add onboarding examples/docs updates.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/install_cortex.py Loads file lists from MANIFEST.yaml, adds --lite, updates output formatting, and supports optional PyYAML with fallback parsing.
MANIFEST.yaml Adds lite_files and remains the source of truth for required_project_files.
tests/test_installer.py Adds smoke tests validating manifest/template consistency and core installer behaviors.
.github/workflows/ci.yml Adds CI job to run smoke tests and basic installer verification across Python versions.
docs/installing.md Documents Lite install flow and manifest-driven file selection.
docs/templates.md Documents Lite vs Full template sets and links to filled example.
README.md Updates onboarding/install expectations, mentions Lite mode, tests, examples, and CI.
examples/filled-cortex/README.md Adds guidance for the filled .cortex/ example.
examples/filled-cortex/PROJECT_CONTEXT.md Adds a realistic filled example of PROJECT_CONTEXT.
examples/filled-cortex/CURRENT_STATUS.md Adds a realistic filled example of CURRENT_STATUS.
examples/filled-cortex/AGENT_HANDOFF.md Adds a realistic filled example of AGENT_HANDOFF.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/install_cortex.py
Comment on lines +19 to +36
with open(MANIFEST) as f:
return yaml.safe_load(f)
# Fallback: minimal YAML parser for simple list fields.
# Avoids forcing a PyYAML dependency on end users.
data = {}
current_key = None
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
current_key = stripped[:-1].strip()
data[current_key] = []
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped.strip("- ").strip())
elif not stripped.startswith(" ") and ":" in stripped:
key, _, value = stripped.partition(":")
current_key = None
data[key.strip()] = value.strip()
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

load_manifest() doesn't handle missing/unreadable/invalid MANIFEST.yaml (e.g., open() can raise FileNotFoundError, and yaml.safe_load() can return None). This can lead to an unhelpful crash later (e.g., manifest.get(...) raising AttributeError). Consider catching I/O / parse errors and validating the loaded manifest is a mapping, then SystemExit with a clear message if not.

Suggested change
with open(MANIFEST) as f:
return yaml.safe_load(f)
# Fallback: minimal YAML parser for simple list fields.
# Avoids forcing a PyYAML dependency on end users.
data = {}
current_key = None
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
current_key = stripped[:-1].strip()
data[current_key] = []
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped.strip("- ").strip())
elif not stripped.startswith(" ") and ":" in stripped:
key, _, value = stripped.partition(":")
current_key = None
data[key.strip()] = value.strip()
try:
with open(MANIFEST) as f:
manifest = yaml.safe_load(f)
except OSError as exc:
raise SystemExit(f"Unable to read {MANIFEST}: {exc}") from exc
except yaml.YAMLError as exc:
raise SystemExit(f"Invalid YAML in {MANIFEST}: {exc}") from exc
if not isinstance(manifest, dict):
raise SystemExit(
f"{MANIFEST} must contain a YAML mapping at the top level."
)
return manifest
# Fallback: minimal YAML parser for simple list fields.
# Avoids forcing a PyYAML dependency on end users.
data = {}
current_key = None
try:
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
current_key = stripped[:-1].strip()
data[current_key] = []
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped.strip("- ").strip())
elif not stripped.startswith(" ") and ":" in stripped:
key, _, value = stripped.partition(":")
current_key = None
data[key.strip()] = value.strip()
except OSError as exc:
raise SystemExit(f"Unable to read {MANIFEST}: {exc}") from exc
if not isinstance(data, dict):
raise SystemExit(
f"{MANIFEST} must contain a mapping of manifest keys to values."
)

Copilot uses AI. Check for mistakes.
Comment thread scripts/install_cortex.py
Comment on lines +21 to +36
# Fallback: minimal YAML parser for simple list fields.
# Avoids forcing a PyYAML dependency on end users.
data = {}
current_key = None
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
current_key = stripped[:-1].strip()
data[current_key] = []
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped.strip("- ").strip())
elif not stripped.startswith(" ") and ":" in stripped:
key, _, value = stripped.partition(":")
current_key = None
data[key.strip()] = value.strip()
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The fallback YAML parser sets any top-level key: to an empty list and only supports list items with exactly two-space indentation (" - "). For this repo's MANIFEST.yaml, that means nested mappings like language:, documentation:, branch_policy:, scoring: etc. will be loaded with incorrect types/empty values when PyYAML isn't installed. If the intent is only to support required_project_files/lite_files without a dependency, consider narrowing the fallback to parse just those keys (and ignore other sections) or extending it to preserve top-level mapping types instead of turning them into lists.

Suggested change
# Fallback: minimal YAML parser for simple list fields.
# Avoids forcing a PyYAML dependency on end users.
data = {}
current_key = None
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
current_key = stripped[:-1].strip()
data[current_key] = []
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped.strip("- ").strip())
elif not stripped.startswith(" ") and ":" in stripped:
key, _, value = stripped.partition(":")
current_key = None
data[key.strip()] = value.strip()
# Fallback: parse only the simple top-level list fields this script uses.
# Avoids forcing a PyYAML dependency on end users without inventing
# incorrect values for unsupported nested YAML sections.
supported_list_keys = {"required_project_files", "lite_files"}
data = {key: [] for key in supported_list_keys}
current_key = None
with open(MANIFEST) as f:
for line in f:
stripped = line.rstrip()
if not stripped or stripped.lstrip().startswith("#"):
continue
if stripped.endswith(":") and not stripped.startswith(" ") and not stripped.startswith("-"):
key = stripped[:-1].strip()
current_key = key if key in supported_list_keys else None
elif stripped.startswith(" - ") and current_key is not None:
data[current_key].append(stripped[4:].strip())
elif not stripped.startswith(" "):
current_key = None

Copilot uses AI. Check for mistakes.
@WPHILLIPMACLAYNE WPHILLIPMACLAYNE merged commit e3456da into main Apr 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants