From cce059fc3ab9212d4ea0c6a517b4624f5123f1c9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sun, 27 Sep 2026 15:38:52 -0400 Subject: [PATCH] Begin portable TORCH fleet rewrite --- README.md | 470 +----- bin/torch.mjs | 4 + docs/PORTABLE_AGENT_FLEET_SPEC.md | 1488 +++++++++++++++++ package-lock.json | 139 +- package.json | 84 +- ...grity-note-2026-09-27-portable-kernel.yaml | 41 + src/cli.mjs | 68 + src/kernel/analyze.mjs | 87 + src/kernel/doctor.mjs | 55 + src/kernel/errors.mjs | 16 + src/kernel/files.mjs | 24 + src/kernel/git.mjs | 57 + src/kernel/install.mjs | 203 +++ src/kernel/paths.mjs | 11 + test/fleet/cli-scenarios.test.mjs | 45 + test/fleet/kernel-lifecycle.test.mjs | 79 + 16 files changed, 2258 insertions(+), 613 deletions(-) create mode 100644 bin/torch.mjs create mode 100644 docs/PORTABLE_AGENT_FLEET_SPEC.md create mode 100644 reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml create mode 100644 src/cli.mjs create mode 100644 src/kernel/analyze.mjs create mode 100644 src/kernel/doctor.mjs create mode 100644 src/kernel/errors.mjs create mode 100644 src/kernel/files.mjs create mode 100644 src/kernel/git.mjs create mode 100644 src/kernel/install.mjs create mode 100644 src/kernel/paths.mjs create mode 100644 test/fleet/cli-scenarios.test.mjs create mode 100644 test/fleet/kernel-lifecycle.test.mjs diff --git a/README.md b/README.md index 1800701..241f173 100644 --- a/README.md +++ b/README.md @@ -1,445 +1,81 @@ -# TORCH: Task Orchestration via Relay-Coordinated Handoff +# TORCH -TORCH is a portable Nostr-based task locking toolkit for multi-agent coordination. +TORCH turns a Git repository into a persistent, coordinated team of +domain-specialized AI development sessions. -## Why Run Torch? (The Cool Stuff) +The new TORCH is being extracted from the agent-fleet operating model proven +in COMBATRIG. It is local-first, provider-independent, worktree-isolated, and +designed to manage its own development. The former Nostr lock/scheduler product +is preserved on the `legacy/nostr-torch` branch and is not part of this +architecture. +## Current status -TORCH isn't just another script—it's an operating system for your AI workforce. Here’s why it’s awesome: +The rewrite is under active development on `rewrite/portable-agent-fleet`. +Stage 1 currently provides the first portable kernel: -- **Decentralized Coordination:** No central server required. Agents coordinate via Nostr relays like digital telepathy. -- **Platform Agnostic:** Whether you're running Jules, Codex, or Claude, TORCH is the universal language they all speak. -- **Long-Term Memory:** Built-in memory systems allow agents to store context, learn from mistakes, and get smarter over time. -- **Verifiable Handoffs:** Trust, but verify. Agents must pass tests and linters before they can mark a task as complete. -- **Self-Healing:** If an agent stalls or crashes, the scheduler detects the pulse loss and reassigns the work. The factory must grow. -- **Drop-in Divinity:** No complex SaaS integrations. Just drop the `torch` folder into your repo and you're live. +- read-only Git repository inspection; +- repository inventory and check discovery; +- explicit install approval; +- tracked `.torch/` project configuration; +- XDG-local runtime state; +- installation ownership manifest; +- health diagnostics; +- conservative, reversible purge. -## Installation +Session orchestration, worktree generation, messaging, checks, integration, +runtime adapters, self-hosting, and the Fleet Console remain staged work. -Prerequisites: - -- Node.js 22 or newer - -First 5 minutes: - -```bash -npm install https://github.com/PR0M3TH3AN/TORCH/archive/.tar.gz --force \ - && npx --no-install torch-lock init - -# sanity check (optional, but recommended) -npx --no-install torch-lock check --cadence daily - -# start dashboard -npx --no-install torch-lock dashboard --port 4173 --host 127.0.0.1 -``` - -`torch-lock init` and `torch-lock update` now auto-upsert the `## TORCH Memory Integration` block into root-level `AGENTS.md` and `CLAUDE.md` (and create `AGENTS.md` if neither file exists). - -> TORCH is distributed from GitHub tarballs and is not currently published to the npm registry. -> Use pinned commit tarballs (`archive/.tar.gz`) for repeatable installs. Avoid `refs/heads/main.tar.gz` for production rollouts. - -### Upgrade TORCH In An Existing Repository - -Use this when the repository already has a `torch/` directory: - -```bash -npm install https://github.com/PR0M3TH3AN/TORCH/archive/.tar.gz --force -npx --no-install torch-lock update --force -npm install --prefix torch -npm run --prefix torch lock:check:daily -- --json --quiet -``` - -Use this for first-time setup in a repository without `torch/`: - -```bash -npm install https://github.com/PR0M3TH3AN/TORCH/archive/.tar.gz --force -npx --no-install torch-lock init --force -npm install --prefix torch -npm run --prefix torch lock:check:daily -- --json --quiet -``` - -One-liner (auto-detect existing vs new install): - -```bash -npm install https://github.com/PR0M3TH3AN/TORCH/archive/.tar.gz --force && \ -if [ -d torch ]; then npx --no-install torch-lock update --force; else npx --no-install torch-lock init --force; fi && \ -npm install --prefix torch && \ -npm run --prefix torch lock:check:daily -- --json --quiet -``` - -Post-upgrade validation checklist: - -1. Confirm root config exists: `test -f torch-config.json`. -2. Confirm handoff defaults exist: `rg -n "handoffCommandByCadence" torch-config.json`. -3. Confirm host install dependencies exist: `test -d torch/node_modules`. -4. Run one scheduler cycle (expected to fail on Linux simulation due to artifact checks, but not due to missing config/handoff): - - `AGENT_PLATFORM=linux npm run --prefix torch scheduler:daily` -5. Verify installed package matches expected resolver behavior: - - `rg -n "cwdBaseName === 'torch' && fs.existsSync\\(parentPath\\)" node_modules/torch-lock/src/torch-config.mjs` - - `rg -n "cwdBaseName === 'torch' && fs.existsSync\\(parentPath\\)" torch/src/torch-config.mjs` - -### Reusable Agent Prompt: Upgrade TORCH - -Use this prompt in a project repository when you want an AI agent to perform a safe TORCH upgrade: - -```md -You are updating TORCH in this repository to the latest pinned version. - -Goal: -- Upgrade TORCH safely. -- Keep behavior stable. -- Validate the new memory event pipeline (`torch-lock memory add/build/verify`). - -Constraints: -- Do not weaken or edit tests to make CI pass. -- Make minimal, reviewable changes. -- Use pinned TORCH commit tarball, never a moving branch tarball. - -Steps: - -1) Create a branch -- `git checkout -b chore/update-torch-393ba35` - -2) Install new TORCH package (pinned) -- `npm install https://github.com/PR0M3TH3AN/TORCH/archive/393ba35.tar.gz --force` - -3) Apply TORCH files into repo -- If `torch/` already exists: - - `npx --no-install torch-lock update --force` -- Else (fresh setup): - - `npx --no-install torch-lock init --force` - -4) Install/update TORCH runtime deps -- If TORCH is installed under `torch/`: - - `npm install --prefix torch` -- If installed in root, run normal install: - - `npm install` - -5) Validate lock/scheduler basics -- `npm run --prefix torch lock:check:daily -- --json --quiet` (or root equivalent) -- `npx --no-install torch-lock doctor` - -6) Validate new memory contract (required) -- Ensure schema exists: `memory/schema/memory-event.schema.json` -- Run: - - `npx --no-install torch-lock memory build` - - `npx --no-install torch-lock memory verify` -- If verify fails, fix root cause (schema/path/duplicate/out-of-date), do not bypass checks. - -7) Run project validation -- Run the project’s standard checks (at minimum lint + tests). -- Record exact commands run and pass/fail outcomes. - -8) Commit -- Stage only relevant update files. -- Commit message: - - `chore(torch): update to 393ba35 and enable memory event verify pipeline` - -9) Report back -Provide: -- Files changed -- Commands run -- Validation results -- Any migration notes (especially memory-related changes) -- Risks/follow-ups -``` +The canonical architecture and delivery plan is +[docs/PORTABLE_AGENT_FLEET_SPEC.md](docs/PORTABLE_AGENT_FLEET_SPEC.md). ## Development -For instructions on how to contribute to TORCH, including building, testing, and linting, please see [CONTRIBUTING.md](CONTRIBUTING.md). +Requirements: -Quick start: +- Node.js 22 or later +- Git + +Run the current verification suite: ```bash -npm install -npm run build -npm run format npm test +npm run lint +npm run check ``` -## Usage - -### CLI - -The `torch-lock` CLI allows you to check, lock, and list tasks. +Inspect a repository without modifying it: ```bash -# Check locks for a specific cadence -npx --no-install torch-lock check --cadence daily - -# Lock a task for an agent -AGENT_PLATFORM=codex npx --no-install torch-lock lock --agent docs-agent --cadence daily - -# List all active locks -npx --no-install torch-lock list - -# Mark a task as completed (permanent) -AGENT_PLATFORM=codex npx --no-install torch-lock complete --agent docs-agent --cadence daily - -# Convenience script for the above -npm run lock:complete -- --agent docs-agent --cadence daily - -# Generic usage for lock completion -npm run lock:complete -- --agent --cadence - -# Initialize TORCH in a new repository -npx --no-install torch-lock init - -# Update TORCH configuration and scripts -npx --no-install torch-lock update - -# Validate setup and print actionable fixes -npx --no-install torch-lock doctor - -# Remove TORCH completely from this project -npx --no-install torch-lock remove - -# Run the Dashboard -npx --no-install torch-lock dashboard +node bin/torch.mjs init --json +node bin/torch.mjs analyze --json +node bin/torch.mjs install --dry-run --json ``` -### Scheduler - -The scheduler automates the selection and execution of agents in a round-robin rotation. +Install the current kernel only after reviewing the dry run: ```bash -# Run full daily scheduler cycle -npm run scheduler:daily - -# Run full weekly scheduler cycle -npm run scheduler:weekly +node bin/torch.mjs install --yes --json +node bin/torch.mjs doctor --json ``` -### Memory CLI +The alpha kernel is not yet a working multi-agent fleet. Its install format may +change before the first usable release. -The `torch-lock` CLI also supports memory operations: +## Product principles -```bash -# Append one schema-validated memory event (append-only) -npx --no-install torch-lock memory add \ - --agent codex \ - --topic memory \ - --message "Lock preflight failed on relay outage; rerun with fallback relays." +- Git is required; GitHub is optional. +- The repository is durable organizational memory; chat sessions are workers. +- Every persistent domain owns one branch and one worktree. +- Every domain has explicit scope and explicit exclusions. +- Peers coordinate directly; the Session Manager controls priority and + ownership. +- Monitoring may be automatic; active worktrees are changed intentionally. +- Verification belongs to an exact commit. +- The TORCH engine runs outside the repository it manages. +- Installation is reversible and refuses unsafe deletion. +- Stable TORCH must be able to build its successor. -# Build deterministic memory_update.md from memory/events/**/*.json -npx --no-install torch-lock memory build +## License -# Verify schema + duplicates + determinism + generated-file no-diff -npx --no-install torch-lock memory verify - -# List memories (optional filters: --agent, --type, --tags, --pinned) -npx --no-install torch-lock list-memories --limit 5 - -# Inspect a specific memory -npx --no-install torch-lock inspect-memory --id - -# Pin/Unpin a memory -npx --no-install torch-lock pin-memory --id -npx --no-install torch-lock unpin-memory --id - -# Check memory stats -npx --no-install torch-lock memory-stats -``` - -Memory event contract: -- Source: `memory/events/YYYY/MM/DD/_.json` (append-only). -- Schema: `memory/schema/memory-event.schema.json`. -- Generated file: `memory_update.md` with auto-generated header. -- Full contract: `docs/memory-events-contract.md`. - -### Prompt Governance & Versioning CLI - -Agent prompts are changed through a governance workflow that archives the previous version before overwriting, enabling full rollback. - -```bash -# Propose a change to an agent prompt -npx --no-install torch-lock proposal create \ - --agent my-agent \ - --target src/prompts/daily/my-agent.md \ - --content /path/to/new-content.md \ - --reason "Add missing EXIT CRITERIA section" - -# List pending proposals -npx --no-install torch-lock proposal list -npx --no-install torch-lock proposal list --status pending - -# Inspect or apply a proposal -npx --no-install torch-lock proposal show --id -npx --no-install torch-lock proposal apply --id -npx --no-install torch-lock proposal reject --id --reason "..." - -# List available archived versions of a prompt -npx --no-install torch-lock rollback --target src/prompts/daily/my-agent.md --list - -# Roll back to the most recent archived version -npx --no-install torch-lock rollback --target src/prompts/daily/my-agent.md - -# Roll back to a specific version by hash fragment -npx --no-install torch-lock rollback --target src/prompts/daily/my-agent.md --strategy -``` - -### State Backup CLI - -Snapshot agent runtime state (long-term memory and scheduler state) to a timestamped directory under `.torch/backups/`. - -```bash -# Create a snapshot -npx --no-install torch-lock backup - -# List existing snapshots -npx --no-install torch-lock backup --list - -# Write snapshot to a custom directory -npx --no-install torch-lock backup --output /path/to/backup-dir -``` - -See [docs/prompt-versioning.md](docs/prompt-versioning.md) for the full workflow, restore procedure, and storage layout. - -### Configuration (`torch-config.json`) - -Create a `torch-config.json` file in your project root to configure TORCH for your repository. You can use the included `torch-config.example.json` as a template. - -Configuration options: - -- `nostrLock.namespace` (d-tag/tag namespace) -- `nostrLock.relays` (array of relay URLs) -- `nostrLock.ttlSeconds` (lock duration in seconds) -- `nostrLock.queryTimeoutMs` (timeout for relay queries) -- `nostrLock.publishTimeoutMs` (per-relay publish timeout in ms) -- `nostrLock.minSuccessfulRelayPublishes` (minimum successful publishes before lock continues, default: `1`) -- `nostrLock.relayFallbacks` (optional fallback relay URLs used when primary relays fail quorum) -- `nostrLock.minActiveRelayPool` (minimum relays kept active even when others are quarantined, default: `1`) -- `nostrLock.dailyRoster` / `nostrLock.weeklyRoster` (explicit rosters) -- `dashboard.defaultCadenceView` (`daily`, `weekly`, or `all`) -- `dashboard.defaultStatusView` (`active` or `all`) -- `dashboard.hashtag` (custom hashtag for lock events) -- `scheduler.firstPromptByCadence.daily` / `.weekly` (first prompt to run) -- `scheduler.handoffCommandByCadence.daily` / `.weekly` (shell command run after lock acquisition; must use `SCHEDULER_AGENT`, `SCHEDULER_CADENCE`, `SCHEDULER_PROMPT_PATH` env vars) -- `scheduler.paused.daily` / `.weekly` (array of agent names to pause locally) -- `scheduler.strict_lock` (lock backend policy; when `false`, defers backend-unavailable failures before failing, default: `true`) -- `scheduler.degraded_lock_retry_window` (ms window for non-strict deferral of backend failures) -- `scheduler.max_deferrals` (max non-strict lock deferrals before recording a hard failure) - -### Dashboard - -The dashboard provides a live view of lock events. - -```bash -npx --no-install torch-lock dashboard --port 4173 --host 127.0.0.1 -# Open http://localhost:4173/dashboard/ -``` - -By default, the dashboard binds to `127.0.0.1`. To allow external access, you can set `--host 0.0.0.0`. It is highly recommended to enable authentication if the dashboard is exposed to a network. - -#### Authentication - -The dashboard supports Basic Authentication. You can enable it by setting the `TORCH_DASHBOARD_AUTH` environment variable or the `dashboard.auth` field in `torch-config.json` with the format `username:password`. - -## Removing TORCH - -To completely remove TORCH from a project: - -```bash -npx torch-lock remove -``` - -This removes the `torch/` directory, configuration files, runtime artifacts, host package.json scripts, and the npm package. Use `--force` to skip the confirmation prompt. - -For manual removal steps and a full list of what gets cleaned up, see [docs/removal.md](docs/removal.md). - -## Environment variables - -You can override configuration using environment variables: - -- `NOSTR_LOCK_NAMESPACE` -- `NOSTR_LOCK_HASHTAG` -- `NOSTR_LOCK_RELAYS` (comma-separated) -- `NOSTR_LOCK_TTL` -- `NOSTR_LOCK_QUERY_TIMEOUT_MS` -- `NOSTR_LOCK_PUBLISH_TIMEOUT_MS` -- `NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES` -- `NOSTR_LOCK_RELAY_FALLBACKS` (comma-separated fallback relay URLs) -- `NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL` -- `NOSTR_LOCK_DAILY_ROSTER` (comma-separated list of agents) -- `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated list of agents) -- `TORCH_CONFIG_PATH` (path to config file) -- `TORCH_DASHBOARD_AUTH` (Basic Auth `user:pass` for the dashboard) -- `AGENT_PLATFORM` (platform identifier, e.g. `codex`) -- `TORCH_MEMORY_ENABLED` (`true`/`false`; global memory kill switch, defaults to enabled) -- `TORCH_MEMORY_INGEST_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list) -- `TORCH_MEMORY_RETRIEVAL_ENABLED` (`true`/`false` or comma-separated canary `agent_id` allow list) -- `TORCH_MEMORY_PRUNE_ENABLED` (`true`, `false`, or `dry-run`) - -## Roster precedence - -The lock CLI resolves roster names in this order: - -1. `NOSTR_LOCK_DAILY_ROSTER` / `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated env overrides). -2. `torch-config.json` (`nostrLock.dailyRoster` / `nostrLock.weeklyRoster`). -3. `src/prompts/roster.json` (canonical scheduler roster included in the package). -4. Built-in fallback roster. - -## Included Resources - -- `src/lib.mjs` — Core library logic (can be imported in scripts) -- `TORCH.md` — Protocol summary and usage -- `src/prompts/` — Generic scheduler prompts and flow -- `skills/` — Repository-local skill guides for agent onboarding and repeatable workflows -- `dashboard/` — Static lock dashboard assets - -## NPM Scripts (for development) - -If you are developing `torch-lock` itself: - -- `npm run lock:check:daily` -- `npm run lock:check:weekly` -- `npm run lock:list` -- `npm run lock:lock` (manually acquire a lock) -- `npm run lock:health -- --cadence daily` (relay websocket + publish/read probe; writes history to `task-logs/relay-health/`) -- `npm run lock:complete` (manually complete a task) -- `npm run dashboard:serve` -- `npm test` (run unit tests) -- `npm run test:unit:lock-backend` (run lock backend unit tests) -- `npm run test:extended-main` (run extended integration tests) -- `npm run validate:scheduler` (validate scheduler roster and prompts) -- `npm run lint` (run linter) -- `npm run scheduler:daily` (run full daily scheduler cycle) -- `npm run scheduler:weekly` (run full weekly scheduler cycle) -- `npm run report:lock-reliability` (aggregate recent scheduler logs into markdown+JSON reliability reports) -- `npm run torch:memory:list` (list memories) -- `npm run torch:memory:inspect` (inspect a memory) -- `npm run torch:remove` (remove TORCH from the project) - - - - - -## Scheduler lock reliability reporting - -Use the reporting hook below to inspect lock backend reliability trends by platform/cadence/error category/relay: - -```bash -npm run report:lock-reliability -``` - -This writes: -- `artifacts/lock-reliability/lock-reliability-summary.md` -- `artifacts/lock-reliability/lock-reliability-summary.json` - -Recommended cadence: run this at least weekly (or wire into your CI/cron scheduler) and compare `codex` vs other `platform` values. - - -## Memory rollout plan - -1. Deploy memory schema changes with scheduler jobs disabled (`TORCH_MEMORY_ENABLED=false` or subsystem flags set to `false`). -2. Enable ingest for one canary `agent_id` via `TORCH_MEMORY_INGEST_ENABLED=`. -3. Validate retrieval quality and storage growth metrics before expanding scope. -4. Enable broader retrieval (`TORCH_MEMORY_RETRIEVAL_ENABLED=` then `true`). -5. Enable pruning in `dry-run` mode first, then switch to active pruning after revalidation. - -## Memory rollback plan - -1. Disable memory flags (`TORCH_MEMORY_ENABLED=false` and/or set ingest/retrieval/prune flags to `false`). -2. Stop memory maintenance scheduler processes. -3. Preserve database state for post-incident analysis; do not drop or rewrite memory tables during rollback. -4. Keep prune actions in `dry-run` (or disabled) until lifecycle policy and data integrity are revalidated. +MIT diff --git a/bin/torch.mjs b/bin/torch.mjs new file mode 100644 index 0000000..d0125ff --- /dev/null +++ b/bin/torch.mjs @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { runCli } from '../src/cli.mjs'; + +process.exitCode = await runCli(); diff --git a/docs/PORTABLE_AGENT_FLEET_SPEC.md b/docs/PORTABLE_AGENT_FLEET_SPEC.md new file mode 100644 index 0000000..c6e0c49 --- /dev/null +++ b/docs/PORTABLE_AGENT_FLEET_SPEC.md @@ -0,0 +1,1488 @@ +# TORCH Portable Agent Fleet Specification + +Status: Draft for implementation + +Version: 0.1 + +Date: 2026-09-27 + +Product: TORCH +Reference implementation: COMBATRIG agent fleet + +## 1. Document authority + +This document is the canonical product and architecture specification for the +new TORCH. + +It consolidates the requirements developed in the conversation exported as: + +`/home/user/Downloads/ChatGPT-Document Portable Fleet System-20260927-1435.md` + +The conversation is design provenance, not an additional specification. When +the conversation contains multiple versions of an idea, its later decisions +take precedence and this document records the consolidated result. + +The working COMBATRIG fleet is the behavioral reference. Its operational +lessons and proven invariants should be preserved, but its project-specific +paths, prompts, model assignments, resource rules, release commands, and +Claude-specific mechanisms are not portable requirements. + +This specification supersedes the existing TORCH Nostr lock/scheduler product +and the `v2/development-network` direction. Those systems are legacy products; +their protocols, funding model, relay architecture, scheduler, dashboard, and +implementation seams are not constraints on the new TORCH. + +Normative terms have their usual meanings: + +- **MUST** and **MUST NOT** are required for conformance. +- **SHOULD** and **SHOULD NOT** are recommended unless a documented project + constraint justifies a different choice. +- **MAY** is optional. + +## 2. Product definition + +TORCH turns a Git repository into a persistent, coordinated team of +domain-specialized AI development sessions. + +One human owner primarily works through one Session Manager. The Session +Manager routes work to persistent domain managers. Each domain manager has a +stable identity, explicit ownership, a dedicated Git branch and worktree, +known neighboring domains, required verification, and durable restart state. +TORCH supplies the control plane that lets these sessions coordinate across +different agent runtimes without making any one runtime the definition of the +fleet. + +The product promise is: + +> A developer with a Git-capable laptop and access to a coding model can run a +> persistent, coordinated, multi-agent software engineering organization +> without depending on GitHub or any other development platform. + +TORCH is not merely a prompt collection, terminal launcher, task lock, agent +catalog, or wrapper around one model vendor. It is a lightweight development +control plane with project analysis, organizational configuration, isolated +execution, durable coordination, verification, integration, and recovery. + +## 3. Goals + +TORCH MUST: + +1. install into an existing or new Git repository; +2. analyze that repository before proposing an organization; +3. obtain human approval before creating permanent domains or worktrees; +4. create stable domain identities with explicit `scope` and `notScope`; +5. give each persistent development domain its own branch and worktree; +6. provide one primary Session Manager conversation for the human owner; +7. support direct peer coordination without removing Session Manager + authority over priority and ownership; +8. externalize identity, ownership, backlog, decisions, unfinished work, and + restart instructions so conversation context is not the database; +9. expose the same organizational tools to Claude, Codex, OpenCode, and future + runtimes through adapters; +10. operate in a local-only configuration with no forge, hosted CI, hosted + database, or external message queue; +11. support optional forge, CI, issue, release, and deployment adapters; +12. verify changes against exact Git commits before integration; +13. coordinate scarce shared resources explicitly; +14. install, detach, uninstall, upgrade, and roll back safely; +15. manage development of its own successor from a stable installed release; +16. re-import COMBATRIG without special-case orchestration code. + +## 4. Non-goals + +The initial product is not required to: + +- recreate GitHub, GitLab, or a general-purpose forge; +- replace Git itself; +- provide social coding, public profiles, or human code-review workflows; +- provide its own foundation model; +- guarantee that every runtime exposes identical native session controls; +- automatically resolve merge conflicts; +- silently invent a domain organization without human review; +- automatically merge active worktrees on a timer; +- deploy project code without project-defined authority; +- preserve compatibility with the legacy Nostr TORCH APIs; +- launch the largest possible number of agents. + +The number of agents is an output of repository analysis and project need, not +a product success metric. Idle is a valid fleet state. + +## 5. Core invariants + +Every conforming implementation MUST preserve these invariants: + +1. One persistent development domain maps to one persistent worktree. +2. Every domain has explicit ownership and explicit exclusions. +3. Every domain has a stable Fleet identity independent of runtime session ID. +4. The human has one primary fleet-facing conversation. +5. The Session Manager controls routing, priority, and ownership rulings. +6. Peers may coordinate directly but may not grant themselves authority. +7. Repository state outranks model memory and chat history. +8. Live messages are transport, not the sole durable record. +9. Unfinished work is externalized before shutdown. +10. A session never edits another session's worktree. +11. A persistent Fleet branch is merged, never rebased by automation. +12. A running measurement is never changed underneath itself. +13. Shared resources have explicit capacity and fairness policies. +14. Verification results name the exact commit they measured. +15. Integration and deployment are separate states. +16. Scheduled work declares whether it survives AI-session shutdown. +17. Monitoring may be automatic; worktree mutation is intentional. +18. Secrets never enter tracked Fleet state. +19. The installed TORCH engine is never sourced from the repository it is + currently managing. +20. Every installation is exactly reversible within declared safety limits. +21. Stable TORCH can manage development and release of its successor. +22. Provider integrations reinforce TORCH policy; they do not define it. + +## 6. Terminology + +**Owner** + +The human authority for the project. The owner approves the organization, +high-impact policy, deployments, destructive actions, and other decisions +reserved by project configuration. + +**Fleet** + +The complete TORCH-managed organization for one project. + +**Session Manager** + +The persistent dispatcher session that receives owner requests, routes work, +establishes ownership and ordering, monitors fleet health, and reports useful +decisions or risks to the owner. + +**Domain manager** + +A persistent specialist assigned to an architectural concern or operational +responsibility. A domain manager is staff; temporary subagents are helpers. + +**Runtime** + +The agent harness used to host a session, such as Claude Code, Codex, or +OpenCode. + +**Runtime adapter** + +The implementation that maps TORCH session operations to one runtime. + +**Control plane** + +The provider-independent service for identity, roster queries, presence, +messaging, acknowledgements, handoffs, status, backlog coordination, and +resource leases. + +**Canonical branch** + +The shared integration branch, normally `main`. + +**Canonical Git store** + +The Git repository considered authoritative for shared refs. It may be a local +bare repository or a network remote. + +**Forge** + +An optional service such as GitHub, GitLab, Forgejo, Gitea, or Bitbucket. + +**Convergence** + +The intentional process of bringing current canonical-branch state into a +domain branch and re-verifying the combined result. + +**Check receipt** + +A durable record of a check command, inputs, result, and exact tested commit. + +**Integration request** + +A request to land a tested domain commit onto the canonical branch. + +## 7. System architecture + +TORCH has four logical layers: + +```text +┌─────────────────────────────────────────────────────────┐ +│ Project │ +│ Source, tests, assets, docs and project configuration │ +└───────────────────────────▲─────────────────────────────┘ + │ +┌───────────────────────────┴─────────────────────────────┐ +│ Fleet engine │ +│ Analysis, roster, worktrees, backlog, checks, doctor, │ +│ resources, convergence, integration and lifecycle │ +└───────────────────────────▲─────────────────────────────┘ + │ +┌───────────────────────────┴─────────────────────────────┐ +│ Communication control plane │ +│ Identity, presence, ownership, messages, acknowledgments│ +│ handoffs, status and session registry │ +└───────────────┬────────────────┬────────────────────────┘ + │ │ + ┌───────▼──────┐ ┌──────▼───────┐ ... + │Claude adapter│ │ Codex adapter │ + └──────────────┘ └──────────────┘ +``` + +The installed engine MUST expose one internal service layer. The CLI, MCP +server, local console, and automation hooks MUST call that service layer rather +than implement separate business rules. + +MCP is a primary agent-facing interface, but it is not TORCH's internal data +model. A CLI fallback MUST exist for runtimes or environments without MCP. + +## 8. Installation and filesystem model + +### 8.1 Installed engine + +Most TORCH implementation code MUST live outside managed repositories. A +typical user installation is: + +```text +~/.local/bin/torch +~/.local/share/torch/ + versions/ + adapters/ + projects/ +``` + +The first implementation MAY require Node.js 22 or later. Packaging as a +standalone executable is a later distribution goal and MUST NOT change project +configuration semantics. + +### 8.2 Tracked project state + +The project owns its durable organizational memory under `.torch/`: + +```text +.torch/ + torch.yaml + roster.yaml + prompts/ + COMMON.md + session-manager.md + .md + backlog/ + decisions.md + RESUME-BRIEF.md + install-manifest.json +``` + +Tracked state MUST be sufficient to reconstruct the approved organization. It +MUST NOT contain credentials, PIDs, sockets, transient runtime IDs, or active +leases. + +The original conversation used `.agent-fleet/` and a `fleet` executable in +generic examples. This specification normalizes those names to the TORCH +product: `.torch/`, `torch`, and `torch_*` MCP tools. + +### 8.3 Machine-local state + +Runtime state belongs outside Git: + +```text +~/.local/share/torch/projects// + state.db + sessions/ + messages/ + locks/ + leases/ + checks/ + integration/ + logs/ + generated-prompts/ + remote.git +``` + +The reference implementation SHOULD use SQLite for structured local state and +filesystem locks or sockets where operating-system coordination requires them. +Storage is an implementation detail; observable behavior and migration safety +are normative. + +### 8.4 Worktrees + +Worktrees MUST live outside the main checkout by default: + +```text +~/TORCHWorktrees// + session-manager/ + backend/ + frontend/ + qa/ +``` + +Their parent path is configurable. TORCH MUST never place managed worktrees +inside the tracked source tree. + +## 9. Installation lifecycle + +### 9.1 Read-only initialization + +`torch init` MUST begin read-only. It identifies the repository, validates Git, +detects existing TORCH state, identifies the canonical branch and remotes, and +reports what analysis would inspect. + +It MUST NOT create worktrees, branches, sessions, hooks, remotes, or tracked +files before approval. + +### 9.2 Installation + +After analysis and human approval, `torch install` creates the tracked +configuration, machine-local state, approved worktrees, runtime integrations, +and ownership manifest. + +Required modes include: + +```bash +torch install --dry-run +torch install --runtime claude +torch install --runtime codex +torch install --runtime claude,codex +``` + +The installer MUST be language-agnostic. A target project may use JavaScript, +Python, Rust, Go, C++, a game engine, or a mixed toolchain. + +### 9.3 Ownership manifest + +Every modification made outside the newly created `.torch/` directory MUST be +recorded in `.torch/install-manifest.json`. Each record MUST identify: + +- created files; +- patched files and the owned fragment or structured path; +- runtime MCP registrations; +- installed hooks; +- generated shims; +- created worktrees and branches; +- local services or schedules; +- canonical-remote changes. + +Uninstall MUST reverse only entries owned by that installation and MUST detect +user changes before reversing a patch. + +### 9.4 Detach and uninstall + +`torch detach` stops runtime integration while preserving the organization, +tracked state, and worktrees. + +`torch uninstall` stops managing the repository and removes owned runtime +integrations. It SHOULD preserve `.torch/` for easy restoration. + +`torch uninstall --purge` additionally removes tracked TORCH configuration +after explicit confirmation and safety checks. + +No uninstall mode may delete or remove a worktree containing: + +- uncommitted changes; +- an in-progress Git operation; +- commits absent from the configured recoverability boundary; +- an active check or measurement; +- an unresolved resource lease. + +Uninstall MUST stop before destructive cleanup and report exact remediation. + +## 10. Project analysis and bootstrap + +### 10.1 Repository reconnaissance + +`torch analyze` MUST inventory: + +- languages and frameworks; +- packages, services, modules, plugins, and architectural layers; +- major source, test, documentation, data, infrastructure, and tooling paths; +- manifests, build systems, and dependency boundaries; +- existing ownership or contributor guidance; +- unit, integration, browser, visual, performance, security, and deployment + verification; +- CI, cron, systemd, release, deployment, backup, mirroring, and artifact jobs; +- secret references and credential boundaries without reading secret values; +- shared configuration and generated artifacts; +- scarce resources such as GPUs, browsers, ports, databases, devices, API + quotas, and deployment accounts. + +The analysis phase MUST be read-only. + +### 10.2 Architecture graph + +Analysis produces a graph containing: + +```text +component +owned paths +dependencies +consumers +shared surfaces +verification surfaces +operational responsibilities +resource requirements +``` + +The graph MUST retain evidence paths so a human can inspect why a domain was +proposed. + +### 10.3 Domain proposal + +A proposed domain SHOULD exhibit: + +1. cohesion: its files and responsibilities change together; +2. separation: ordinary work rarely requires another domain's files; +3. independent verification: meaningful evidence can be produced locally; +4. persistent expertise: the area benefits from durable specialist context. + +A directory alone is not sufficient justification. Domains such as +"programming" are too broad; domains such as "button colors" are normally too +narrow. Small projects may need four domains. Large projects may need twenty +or more. TORCH MUST not target a predetermined fleet size. + +Cross-cutting roles such as QA, performance, security, release, architecture, +documentation, design direction, migration, or operations are created only +when repository evidence and project goals justify them. + +### 10.4 Collision analysis + +For every proposed pair of domains, TORCH MUST identify likely collisions: + +- files both might edit; +- APIs joining the domains; +- shared schemas and configuration; +- tests spanning both; +- shared generated outputs; +- shared external resources. + +Each significant collision requires one declared resolution: + +1. redraw the boundary; +2. designate one owner; +3. define a shared-file protocol; +4. create an integration responsibility; +5. require Session Manager coordination. + +Collision analysis generates `notScope`, `neighbours`, shared-surface rules, +and coordination requirements. + +### 10.5 Human review + +TORCH presents the proposed organization before mutation. The owner may accept, +merge, split, rename, add, or remove domains; change ownership; change +authority; or reject the analysis. + +Permanent identities, branches, worktrees, and sessions are created only from +an approved proposal. + +### 10.6 Generated configuration and prompts + +For each approved domain TORCH generates: + +- stable ID and display name; +- purpose and scope; +- explicit `notScope`; +- owned and shared paths; +- neighbors; +- worktree and branch mapping; +- required checks; +- resource needs; +- authority and landing permissions; +- runtime and model policy; +- project invariants; +- initial backlog and first-move instructions. + +Prompts MUST be derived from configuration and recorded decisions. Generated +prose MUST NOT become a competing source of truth. + +### 10.7 Worktree creation + +For every persistent development domain, TORCH creates or verifies a branch +and worktree. A default mapping is: + +```text +domain ID: backend +branch: torch/backend +worktree: //backend +``` + +The branch naming convention is configurable. TORCH MUST verify that each +worktree maps to the intended branch, that its task marker is ignored, and +that the briefing system resolves the correct identity. + +### 10.8 Bootstrap acceptance test + +Installation is incomplete until TORCH proves: + +- every domain has configuration, prompt, branch, and worktree; +- stable identities are unique; +- every live runtime session has a unique runtime ID; +- each worktree receives the correct brief; +- presence appears in the registry; +- doctor sees every worktree; +- the Session Manager can message every live domain; +- neighboring domains can exchange and acknowledge a message; +- ownership lookup returns the approved owner; +- backlog items can be created, assigned, updated, and closed; +- check recording binds results to a commit; +- resource capacity and queue behavior work; +- capture, shutdown, and restart preserve Fleet identity; +- uninstall dry-run accurately describes reversal. + +## 11. Configuration model + +`.torch/torch.yaml` is the primary machine-readable project configuration. +Roster and prompt files may be separate for reviewability, but they MUST be +generated or validated against this configuration and approved decisions. + +Illustrative configuration: + +```yaml +schema: torch.dev/v1alpha1 + +project: + id: acme + name: Acme + main_branch: main + +paths: + tracked_state: .torch + worktree_parent: ~/TORCHWorktrees + +repository: + canonical: + type: local + path: ~/.local/share/torch/projects/acme/remote.git + +forge: + provider: none + +session_manager: + id: session-manager + runtime: claude + start_last: true + landing_authority: true + +git: + branch_prefix: torch/ + convergence: merge + allow_rebase: false + allow_force_push: false + allow_bare_stash: false + +synchronization: + strategy: dispatcher-managed + auto_merge_worktrees: false + observe_every: 60m + converge: + on_task_complete: true + before_integration: true + on_restart: true + when_idle_and_stale: true + defer_when: + dirty: true + mid_git_operation: true + check_running: true + measurement_running: true + pinned: true + +resources: + browser: + capacity: 1 + queue: fifo + gpu: + capacity: 1 + queue: fifo + +checks: + fast: + command: npm test + integration: + command: npm run test:integration + visual: + command: npm run test:visual + resource: browser + +integration: + provider: torch + target: main + require_current_main: true + required_checks: [fast, integration] + landing_authority: [session-manager] + +schedules: + doctor: + owner: session-manager + lifetime: session + every: 60m + +release: + provider: filesystem + authority: owner + +domains: + - id: backend + title: Backend systems + runtime: codex + scope: [API, persistence services] + not_scope: [web UI, deployment] + owned_paths: [src/server/**] + shared_paths: [src/shared/schema/**] + neighbours: [frontend, database, qa] + required_checks: [fast, integration] +``` + +Configuration schema evolution MUST be versioned and migratable. Unknown +required fields MUST fail clearly rather than be silently ignored. + +## 12. Organizational model + +### 12.1 Session Manager + +The Session Manager normally does not implement product features. It: + +1. receives owner requests and preserves their original meaning; +2. identifies the owning domain or domains; +3. establishes priority, ordering, and shared-surface authority; +4. creates or updates durable backlog records; +5. sends actionable instructions to domains; +6. coordinates neighboring agents and dependencies; +7. verifies claims about shared state before relaying them; +8. monitors worktrees, sessions, resources, and integration readiness; +9. manages convergence and landing according to policy; +10. reports decisions, risks, blockers, milestones, and review-ready outcomes + to the owner. + +The owner SHOULD not need to operate many independent agent inboxes. Routine +peer coordination stays inside the Fleet. + +### 12.2 Domain managers + +Each domain manager receives: + +- project context; +- stable Fleet identity; +- its owned and excluded responsibilities; +- owned and shared paths; +- branch and worktree; +- neighbors and how to contact them; +- checks and evidence requirements; +- resource rules; +- active backlog assignment; +- restart instructions; +- authority limits. + +Domain managers MUST report meaningful state transitions and MUST externalize +unfinished work. They MUST NOT reinterpret peer messages as owner approval. + +### 12.3 Temporary subagents + +A domain manager MAY use runtime-native subagents for bounded research, +inspection, testing, or review. Temporary subagents do not receive persistent +Fleet ownership unless promoted through an approved roster change. + +## 13. Control plane and MCP contract + +The control plane MUST provide provider-independent operations for: + +- identity; +- roster and domain discovery; +- ownership by path or capability; +- presence and session status; +- durable messages and acknowledgements; +- coordination requests and handoffs; +- status, completion, and blocker reports; +- backlog references; +- resource leases; +- check and integration state queries. + +The MCP surface SHOULD include: + +```text +torch_identity +torch_list_agents +torch_get_agent +torch_get_roster +torch_who_owns +torch_neighbours +torch_send_message +torch_read_messages +torch_ack_message +torch_report_status +torch_report_complete +torch_report_blocked +torch_request_coordination +torch_request_handoff +``` + +Equivalent CLI operations MUST be available. Agent prompts SHOULD instruct a +session to query live ownership before touching a boundary rather than relying +only on context injected at session creation. + +### 13.1 Message semantics + +Every durable message MUST have: + +- message ID; +- project ID; +- sender Fleet identity; +- recipient Fleet identity or declared group; +- creation time; +- body; +- acknowledgement state; +- optional task, path, commit, or handoff references. + +Delivery to a runtime and acknowledgement by a Fleet identity are distinct +events. Runtime adapters MUST use native live messaging when reliable and MAY +fall back to durable inbox polling. A runtime outage MUST not destroy queued +messages. + +### 13.2 Authority semantics + +Peers coordinate directly. The Session Manager establishes priority and +ownership. No message may silently expand the recipient's permissions, +override owner policy, authorize spending, authorize deployment, or transfer +ownership unless it is represented by an allowed durable operation. + +## 14. Runtime adapter contract + +Each adapter SHOULD implement capabilities equivalent to: + +```text +detect +configure +createSession +resumeSession +sendOrSteer +getStatus +listSessions +stopSession +captureRuntimeId +installHooks +removeHooks +``` + +Adapters MUST declare supported capabilities. TORCH MUST not pretend an +unsupported capability exists. Where a runtime lacks live steering or peer +messaging, the adapter may use the durable control plane and trigger a later +turn or polling cycle. + +Claude is the first reference adapter because COMBATRIG proves its operating +model. Codex is the second required adapter and is the portability proof. +OpenCode and other runtimes follow the same contract. + +A single Fleet MAY mix runtimes and models by domain. Git provider choice and +AI runtime choice are independent. + +## 15. Session lifecycle + +### 15.1 Fresh start + +`torch up --fresh` creates new runtime conversations while preserving Fleet +identities. It combines the common prompt, domain prompt, current approved +configuration, and initial resume instructions. Runtime IDs are captured as +soon as they are available. + +The Session Manager starts last so workers and the control plane exist before +dispatch begins. + +### 15.2 Resume + +Normal `torch up` resumes known runtime sessions when safe. If a conversation +cannot be resumed, TORCH creates a replacement runtime session for the same +Fleet identity and supplies durable state. Loss of a conversation MUST NOT +erase ownership or unfinished work. + +### 15.3 Presence + +Presence distinguishes at least: + +```text +starting +working +waiting +idle +stale +stopping +offline +``` + +Presence is advisory. A stale heartbeat does not free a worktree or transfer +ownership. + +### 15.4 Wind-down + +Before shutdown, every domain MUST: + +- stop or safely leave active checks; +- release or transfer resource leases; +- record dirty, committed, pushed, and integration state; +- update its backlog and unfinished-work record; +- push work required by the configured recoverability boundary; +- send one final status to the Session Manager. + +The Session Manager writes or validates `RESUME-BRIEF.md`, captures session +mapping, records fleet-wide risks, and stops last. + +## 16. Git and worktree policy + +### 16.1 Isolation + +The main checkout remains on the canonical branch and SHOULD not be used for +domain implementation. A domain session works only in its assigned worktree. + +TORCH MUST inspect process ownership and working directory before stopping a +process. Parentage alone does not establish ownership. + +### 16.2 Safe convergence + +TORCH MUST distinguish scheduled observation from scheduled mutation: + +> Monitor automatically; mutate worktrees intentionally. + +No timer may blindly pull, merge, rebase, or reset domain worktrees. Doctor may +observe drift and the Session Manager may request convergence at a safe point. +The owning domain performs the mutation. + +The normal sequence is: + +```text +implement +→ verify locally +→ commit +→ fetch canonical Git store +→ merge canonical main +→ resolve conflicts +→ rerun required checks on the combined tree +→ push domain branch +→ request integration +``` + +Persistent branches MUST NOT be automatically rebased. Bare `git stash` SHOULD +be prohibited where worktrees share one stash stack. + +### 16.3 Measurement protection + +TORCH MUST defer convergence when a worktree is: + +- running a check or measurement; +- deliberately pinned; +- dirty at an unsafe checkpoint; +- resolving a merge or other Git operation; +- holding state whose inputs would change under merge. + +A worktree legitimately measuring an older commit is not stale merely because +main advanced. + +## 17. Backlog, decisions, and durable memory + +The tracked backlog is the durable work queue. It MUST support at least: + +```text +proposed +ready +assigned +in_progress +blocked +verification +ready_to_integrate +completed +cancelled +``` + +Items SHOULD identify owner, dependencies, affected domains, acceptance +criteria, evidence, and current commit when applicable. + +Owner decisions affecting future work MUST be recorded durably. Chat messages +alone are insufficient. Project source, tests, approved configuration, and +accepted decision records outrank summaries and generated prompts. + +## 18. Checks and evidence + +Projects declare checks as commands with optional resources and applicability. +TORCH executes them locally or through a configured provider and records a +receipt containing: + +- check ID and definition version; +- exact Git commit; +- worktree and domain; +- command and relevant environment identity; +- start and finish time; +- exit status; +- coverage or subject description; +- artifact references; +- resource lease used; +- pass, fail, blocked, or incomplete result. + +A passing receipt becomes stale when its tested commit changes or when policy +declares a relevant input changed. TORCH MUST never apply a receipt from one +commit to another merely because the branch name is unchanged. + +Projects SHOULD retain scenario-level checks at meaningful system boundaries. +Fleet infrastructure itself MUST have acceptance tests that demonstrate both +success and failure behavior. + +## 19. Shared-resource coordination + +Resources such as browsers, GPUs, ports, databases, devices, API quotas, and +deployment accounts are declared with capacity and policy. + +TORCH MUST support: + +- FIFO queuing; +- lease ownership; +- acquisition and release; +- stale-owner detection; +- maximum hold policy; +- cancellation without leaking the resource; +- doctor visibility; +- project-specific bypass policy requiring explicit authority. + +Fairness matters more than raw agent count. The Fleet SHOULD be sized around +the scarce machine and the cost of meaningful verification. + +## 20. Integration queue and main protection + +Pull requests are optional. TORCH provides a native integration queue with +records containing: + +- source domain, branch, and commit; +- target branch; +- convergence state; +- required and observed check receipts; +- affected ownership boundaries; +- dependencies; +- authorization; +- current state and reason. + +Example states include: + +```text +needs_convergence +testing +blocked +ready +landing +landed +superseded +``` + +Before landing, TORCH MUST verify: + +1. the source commit still exists and is recoverable; +2. the branch includes the required canonical state; +3. required checks passed for the exact source commit; +4. no required check was invalidated; +5. ownership and dependency conditions are satisfied; +6. the actor has landing authority; +7. the target has not advanced beyond policy tolerance. + +Fleet policy MUST be enforceable without provider branch protection. A forge +adapter MAY mirror the same policy as defense in depth. + +## 21. Doctor and observability + +`torch doctor` is an observer and diagnostician, not an automatic repair tool. +It SHOULD report: + +- domain-to-worktree and branch consistency; +- dirty files and in-progress Git operations; +- commits absent from the recoverability boundary; +- convergence drift measured by both count and age; +- pinned or actively measuring worktrees; +- session and presence state; +- prompt, roster, schedule, and generated-doc drift; +- missing dependencies or broken runtime integrations; +- message backlog and missing acknowledgements; +- resource holders and queue waiters; +- exact-SHA check validity; +- integration readiness; +- canonical Git and forge availability; +- local-only versus off-machine recoverability; +- installation-manifest inconsistencies. + +Doctor output MUST distinguish observation from recommended action. Repair +commands require explicit invocation and normal authority checks. + +## 22. Scheduling + +TORCH recognizes two schedule classes. + +**System schedules** survive AI sessions and may perform approved release, +backup, CI, mirroring, archival, dependency scan, or data-refresh work. + +**Session schedules** exist while the Session Manager is active and may run +doctor, inspect drift, dispatch backlog, schedule QA, monitor resources, +monitor budget, or prepare wind-down. + +Every schedule MUST declare: + +- owner; +- lifetime; +- trigger; +- read-only or mutating behavior; +- command or action; +- required authority; +- retry and failure reporting; +- source of truth. + +Schedules MUST be generated or validated from machine-readable configuration +so launcher code and prose cannot silently disagree. + +## 23. Repository and forge operating modes + +### 23.1 Local-only mode + +Local-only mode is a production-supported baseline, not a demo. It requires: + +```text +laptop +Git +TORCH +supported agent runtime +runtime/model credentials, if applicable +project repository +``` + +TORCH supplies worktrees, identities, messages, presence, backlog, decisions, +checks, resources, integration, and restart state locally. + +If a project has no remote, TORCH SHOULD offer a local bare canonical Git store +under its machine-local project state. This preserves normal fetch/push and +shared-ref semantics. + +Doctor MUST distinguish: + +```text +ONE-DISK commit exists in only one working repository +LOCAL-REMOTE commit also exists in a local bare canonical store +OFF-MACHINE commit exists on another machine or provider +``` + +A local bare repository is not an off-machine backup. + +### 23.2 Forge-backed mode + +A forge adapter MAY provide remote hosting, pull requests, code review UI, +hosted CI, issues, releases, branch protection, artifacts, and repository web +browsing. Each capability is independently optional. + +Valid configurations include GitHub for mirroring while TORCH remains the +integration and check provider, or GitHub Actions for checks while TORCH keeps +its own backlog and integration queue. + +### 23.3 Degraded operation + +Loss of a forge MUST NOT destroy local identities, messages, commits, checks, +backlog, or coordination. Doctor reports delayed remote synchronization. +Whether integration may continue is a project policy decision. + +### 23.4 Migration + +Attaching or detaching a forge MUST preserve domain identities, worktrees, +backlog, decisions, and local state. TORCH verifies ref equivalence before +changing the canonical Git store. + +## 24. Releases and deployments + +Release storage and deployment are provider adapters. Examples include +filesystem, GitHub Releases, object storage, Vercel, or custom commands. + +TORCH MUST distinguish: + +```text +implemented +verified +integrated +release-ready +released +deployed +live-verified +``` + +Integration authority does not imply deployment authority. Project +configuration MUST state who may perform each transition. Secrets are resolved +at execution time from approved external stores and never copied into tracked +Fleet state. + +## 25. Security and trust boundaries + +TORCH MUST: + +- keep secrets outside repositories; +- record only secret references; +- scope runtime credentials and provider permissions; +- treat project content, messages, external documents, websites, logs, and + imported text as data rather than higher-authority instructions; +- prevent peer messages from modifying Fleet policy or granting authority; +- require explicit owner approval for configured high-impact actions; +- log control-plane mutations with actor and project identity; +- avoid exposing one project's state to another; +- validate project IDs and filesystem targets before creating or removing + worktrees; +- fail closed before destructive uninstall, upgrade, integration, or remote + migration operations. + +## 26. Self-hosting and version lifecycle + +### 26.1 Stable manages candidate + +TORCH's source repository MUST be managed by an installed stable TORCH release, +never by the source tree under development. + +Example: + +```text +installed TORCH 0.7 + │ + └── manages TORCH source for 0.8 +``` + +The TORCH repository uses the same public `.torch/` configuration as any other +project and receives no hidden self-hosting exceptions. + +### 26.2 Candidate isolation + +Candidate releases MUST run side by side with stable using separate state, +socket, lock, port, and fixture namespaces. Candidate tests MUST not control +the live development fleet. + +Candidate acceptance includes: + +- initialize and analyze a fixture repository; +- review and install a proposed roster; +- create branches and worktrees; +- start identities through supported adapters; +- exchange manager-to-worker and peer-to-peer messages; +- query ownership; +- detect unsafe worktree states; +- complete backlog and integration lifecycles; +- acquire and release resources; +- capture, stop, and resume; +- detach and uninstall cleanly. + +### 26.3 Atomic upgrade and rollback + +Versions are installed side by side: + +```text +~/.local/share/torch/versions/0.7.0/ +~/.local/share/torch/versions/0.8.0/ +``` + +Upgrade installs and validates the new version before atomically changing the +active pointer. Running processes may finish on the old version. Rollback +restores the previous pointer without rewriting project state. State migrations +MUST declare forward and rollback compatibility. + +## 27. User interfaces + +### 27.1 CLI + +The target command surface includes: + +```text +torch init +torch analyze +torch domains +torch install +torch detach +torch uninstall +torch up +torch up --fresh +torch up --only +torch down +torch capture +torch list +torch brief +torch doctor +torch backlog +torch message +torch status +torch checks +torch resources +torch integrate +torch schedules +torch candidate build +torch candidate test +torch upgrade +torch rollback +``` + +Commands MUST offer structured output where automation needs it. + +### 27.2 Local Fleet Console + +The operational console SHOULD expose: + +- agents and presence; +- ownership and domain graph; +- worktrees, branches, drift, and recoverability; +- messages and acknowledgements; +- backlog and blockers; +- checks and exact tested commits; +- integration queue; +- resource holders and waiters; +- decisions, schedules, releases, and provider health. + +The console is an operational view over the same core service, not a separate +source of truth. + +### 27.3 Public website + +The public website MUST be redesigned around the new product. It MUST NOT +describe Nostr task locks, relay coordination, the old scheduler, or a catalog +of generic agents. + +Its primary story is: + +```text +owner request +→ Session Manager routing +→ domain-owned worktrees +→ direct coordination +→ exact-commit verification +→ controlled convergence +→ canonical main +``` + +The site SHOULD separate product explanation and documentation from the local +Fleet Console. A distinctive routing or dispatch-board visualization should +demonstrate the system more clearly than a wall of feature cards. + +Recommended product language: + +> One repository. A coordinated AI engineering team. + +TORCH SHOULD be treated as the product name rather than retaining the legacy +"Task Orchestration via Relay-Coordinated Handoff" expansion. + +## 28. Delivery plan + +### Stage 0: Preserve and specify + +- archive the legacy Nostr main line; +- preserve the development-network experiment separately; +- establish a clean rewrite branch; +- accept this specification and record any amendments; +- define scenario IDs and fixture repositories. + +Exit gate: the owner approves the product boundary and legacy preservation +plan. + +### Stage 1: Portable kernel + +- configuration schema and loader; +- project identity and XDG state layout; +- read-only `init` and `analyze` scaffolding; +- install manifest; +- detach, uninstall dry-run, and doctor foundation; +- Git repository and worktree inspection. + +Exit gate: installation and complete reversal succeed on a fixture repository +without modifying user code. + +### Stage 2: Project decomposition + +- reconnaissance and architecture graph; +- domain proposal and collision analysis; +- human review artifact; +- roster, prompt, backlog, and worktree generation; +- bootstrap acceptance test. + +Exit gate: two structurally different fixture repositories receive defensible, +reviewable organizations. + +### Stage 3: Claude reference fleet + +- control-plane service and MCP server; +- Claude adapter; +- identity, presence, messages, acknowledgement, and lifecycle; +- Session Manager and domain startup; +- capture, wind-down, and resume. + +Exit gate: a local Claude fleet completes a bounded multi-domain change and +resumes after shutdown. + +### Stage 4: Verification and integration + +- checks and exact-SHA receipts; +- resource queues and leases; +- doctor completeness; +- convergence workflow; +- integration queue and main policy; +- local bare canonical Git store. + +Exit gate: a local-only fleet can take a change from assignment through tested +integration without GitHub or hosted CI. + +### Stage 5: Runtime portability + +- Codex adapter; +- mixed-runtime fleet; +- CLI fallback for MCP-limited environments; +- capability reporting and degraded behavior. + +Exit gate: Claude and Codex occupy persistent domains in one Fleet and +coordinate through TORCH rather than native vendor messaging. + +### Stage 6: Self-hosting + +- stable/candidate isolation; +- candidate fixture suite; +- atomic upgrade and rollback; +- TORCH's own approved roster and worktrees. + +Exit gate: stable TORCH manages development, testing, and promotion of its +successor in local-only mode. + +### Stage 7: COMBATRIG re-import + +- express COMBATRIG domains, prompts, resources, checks, schedules, and release + boundaries through portable configuration; +- replace project-owned generic fleet scripts with installed TORCH operations; +- compare behavior against the current working fleet; +- retain COMBATRIG-specific gates and deployment as project adapters. + +Exit gate: COMBATRIG operates without special-case orchestration in TORCH core. + +### Stage 8: Product surface + +- new public website and documentation; +- local Fleet Console; +- additional runtime and forge adapters; +- standalone distribution packaging. + +Exit gate: the public surface accurately demonstrates behavior proven by the +released product. + +## 29. Required acceptance configurations + +### A. Laptop-only + +```text +existing or new Git repository +no forge +Fleet-created local bare canonical repository +one laptop +Claude or Codex +``` + +The complete lifecycle from analysis through integration, restart, and detach +MUST pass. + +### B. Existing local repository + +Installation after project history exists MUST preserve all source history, +branches, untracked user files, and preexisting configuration. Uninstall MUST +restore the pre-install integration state. + +### C. Forge-backed + +An existing GitHub or equivalent remote MAY supply selected capabilities while +TORCH semantics remain authoritative. + +### D. Local to forge-backed + +Attaching a forge later MUST preserve worktrees, identities, backlog, +decisions, and messages. + +### E. Forge-backed to local + +Detaching the forge MUST leave local development operational. + +### F. Forge outage + +Local sessions, messages, commits, checks, backlog, and integration permitted +by policy MUST continue. Pending remote synchronization is visible. + +### G. Mixed runtimes + +At least two runtime adapters MUST exchange durable messages, query the same +roster, and coordinate one shared-boundary change. + +### H. TORCH builds TORCH + +An installed stable release MUST manage the next candidate using local-only +Git and TORCH's own control plane. + +### I. COMBATRIG compatibility + +The portable system MUST reproduce the essential COMBATRIG behavior without +hard-coded COMBATRIG paths or names. + +## 30. COMBATRIG extraction map + +The following files are behavioral references, not copy mandates: + +| COMBATRIG surface | Portable TORCH responsibility | +| --- | --- | +| `tools/dev/fleet.sh` | lifecycle orchestration and first Claude adapter | +| `tools/dev/fleet-capture.mjs` | runtime-ID capture | +| `tools/dev/brief.mjs` | configuration-derived identity brief | +| `tools/dev/sessions.mjs` | presence and session registry | +| `tools/dev/doctor.mjs` | Git, worktree, session, resource, and risk diagnostics | +| `tools/dev/backlog.mjs` | durable work queue | +| `docs/agents/roster.json` | roster schema, scope, exclusions, and neighbors | +| `docs/agents/prompts/` | common plus domain prompt composition | +| `docs/DISPATCH.md` | Session Manager routing and reporting policy | +| `docs/WORKTREES.md` | isolation, process ownership, measurement, and resource rules | +| `docs/agents/OPERATIONS.md` | lifecycle, schedules, restart, and release separation | +| `docs/agents/WIND-DOWN.md` | coordinated shutdown | +| `docs/agents/RESUME-BRIEF.md` | durable restart handoff | +| `.claude/settings.json` | Claude-specific hook adapter | +| `tools/test/serve.mjs` | project-specific resource lease lessons | +| `tools/dev/release.sh` | project-specific release adapter example | + +Extraction classification is mandatory: + +**Portable core** includes roster semantics, briefing, worktree lifecycle, +backlog, doctor architecture, messaging, checks, integration, and resource +coordination. + +**Runtime adapter** includes session creation, naming, resume, steering, +listing, stopping, ID capture, and lifecycle hooks. + +**Project configuration or adapter** includes COMBATRIG domains, tests, port +allocation, GPU/browser policy, model assignments, secrets, deployment, asset +pipelines, and release schedule. + +**Do not carry forward** includes duplicated prose configuration, hard-coded +home paths, COMBATRIG prefixes, implicit external cron state, or assumptions +that native Claude messaging is the Fleet API. + +## 31. Open implementation decisions + +These choices remain open but MUST be resolved through short architecture +decisions before their implementation stage: + +1. SQLite schema and event-retention policy for machine-local state. +2. Control-plane transport: Unix socket, localhost HTTP, or a combination. +3. Exact project identity derivation and repository-move behavior. +4. Runtime process supervision across Linux, macOS, and Windows. +5. Configuration generation versus checked-in derived files. +6. Adapter capability negotiation and minimum conformance levels. +7. Check-input invalidation beyond Git commit identity. +8. Local canonical-remote naming when a project already has `origin`. +9. Multi-machine authentication and encryption. +10. Distribution and update signing. + +None of these decisions may weaken the core invariants or make a forge +mandatory. + +## 32. Definition of the first usable release + +The first usable TORCH release is complete only when a user can run: + +```bash +cd an-existing-project +torch init +torch analyze +torch install --runtime claude +torch up +``` + +review and approve a proposed organization, assign a real task through one +Session Manager, observe at least two domain managers coordinate, record +verification against exact commits, integrate through TORCH policy, stop the +fleet, resume it without losing identity or unfinished work, and detach it +without damaging the repository. + +That lifecycle MUST pass with no Nostr relay, no GitHub account, no hosted CI, +no hosted database, and no project-local copy of the TORCH engine. diff --git a/package-lock.json b/package-lock.json index 7779419..9855c33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,15 @@ { - "name": "torch-lock", - "version": "0.1.0", + "name": "torch-agent-fleet", + "version": "0.1.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "torch-lock", - "version": "0.1.0", + "name": "torch-agent-fleet", + "version": "0.1.0-alpha.0", "license": "MIT", - "dependencies": { - "nostr-tools": "2.23.1", - "ws": "8.19.0" - }, "bin": { - "torch-lock": "bin/torch-lock.mjs" + "torch": "bin/torch.mjs" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -254,45 +250,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@noble/ciphers": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", - "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -309,42 +266,6 @@ "node": ">=18" } }, - "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", - "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", - "license": "MIT", - "dependencies": { - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", - "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1207,35 +1128,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nostr-tools": { - "version": "2.23.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.1.tgz", - "integrity": "sha512-Q5SJ1omrseBFXtLwqDhufpFLA6vX3rS/IuBCc974qaYX6YKGwEPxa/ZsyxruUOr+b+5EpWL2hFmCB5AueYrfBw==", - "license": "Unlicense", - "dependencies": { - "@noble/ciphers": "2.1.1", - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0", - "@scure/bip32": "2.0.1", - "@scure/bip39": "2.0.1", - "nostr-wasm": "0.1.0" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/nostr-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "license": "MIT" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1526,27 +1418,6 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index a09fd49..5b4a6ef 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { - "name": "torch-lock", - "version": "0.1.0", + "name": "torch-agent-fleet", + "version": "0.1.0-alpha.0", "type": "module", - "description": "TORCH — portable Nostr-based task locking toolkit for multi-agent coordination", - "main": "src/lib.mjs", + "description": "TORCH — a portable, provider-independent AI development fleet for Git repositories", + "main": "src/cli.mjs", "exports": { - ".": "./src/lib.mjs", - "./constants": "./src/constants.mjs", - "./lock-ops": "./src/lock-ops.mjs" + ".": "./src/cli.mjs", + "./kernel/analyze": "./src/kernel/analyze.mjs", + "./kernel/git": "./src/kernel/git.mjs", + "./kernel/install": "./src/kernel/install.mjs" }, "bin": { - "torch-lock": "./bin/torch-lock.mjs" + "torch": "./bin/torch.mjs" }, "engines": { "node": ">=22.0.0" @@ -20,71 +21,30 @@ "url": "https://github.com/PR0M3TH3AN/TORCH.git" }, "keywords": [ - "nostr", - "task-locking", + "agent-fleet", + "worktrees", "multi-agent", "coordination", - "scheduler", + "orchestration", "cli" ], "license": "MIT", "files": [ - "bin/", - "src/*.mjs", - "src/services/", - "src/prompts/", - "src/docs/", - "scripts/check-file-size.mjs", - "scripts/check-innerhtml.mjs", - "scripts/test-audit/", - "scripts/memory/", - "memory/schema/", - "migrations/", - "skills/", - "dashboard/", - "assets/", - "eslint.config.mjs", - "torch-config.example.json", + "bin/torch.mjs", + "src/cli.mjs", + "src/kernel/", + "docs/PORTABLE_AGENT_FLEET_SPEC.md", "README.md", - "TORCH.md", "LICENSE" ], "scripts": { - "test": "npm run validate:scheduler && npm run test:integration:e2e && node --test --test-timeout=30000 test/*.test.mjs test/*.test.js", - "test:unit:lock-backend": "node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs", - "lock:check:daily": "node bin/torch-lock.mjs check --cadence daily", - "lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly", - "lock:list": "node bin/torch-lock.mjs list", - "lock:lock": "node bin/torch-lock.mjs lock", - "lock:health": "node bin/torch-lock.mjs health", - "dashboard:serve": "node bin/torch-lock.mjs dashboard", - "build": "node build.mjs", - "format": "eslint . --fix", - "lint": "eslint .", - "lint:inline-styles": "! grep -r \"style=\" src/ dashboard/ --exclude-dir=test_logs --exclude-dir=task-logs", - "lock:complete": "node bin/torch-lock.mjs complete", - "validate:scheduler": "node scripts/validate-scheduler-roster.mjs && node scripts/validate-prompt-contract.mjs && node scripts/validate-scheduler-flow-parity.mjs && npm run validate:scheduler-failure-schema", - "scheduler:daily": "node scripts/agent/run-scheduler-cycle.mjs daily", - "scheduler:weekly": "node scripts/agent/run-scheduler-cycle.mjs weekly", - "report:lock-reliability": "node scripts/agent/summarize-lock-reliability.mjs --since-days 30", - "test:integration:e2e": "node --test test/scheduler-preflight-lock.e2e.test.mjs", - "validate:scheduler-failure-schema": "node --test test/scheduler-lock-failure-schema.contract.test.mjs", - "test:ci-resilience": "node --test test/lock-acquisition-resilience.integration.test.mjs", - "test:extended-main": "npm run test:integration:e2e && npm run test:ci-resilience", - "test:playwright": "playwright test", - "test:playwright:coverage": "rm -rf coverage/playwright/tmp && mkdir -p coverage/playwright/tmp && CI=true playwright test && npx c8 report --temp-directory coverage/playwright/tmp --report-dir coverage/playwright --reporter text --reporter json --all --src src", - "test:playwright:ui": "playwright test --ui", - "torch:memory:list": "node bin/torch-lock.mjs list-memories", - "torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory", - "torch:memory:add": "node bin/torch-lock.mjs memory add", - "torch:memory:build": "node bin/torch-lock.mjs memory build", - "torch:memory:verify": "node bin/torch-lock.mjs memory verify", - "torch:remove": "node bin/torch-lock.mjs remove" - }, - "dependencies": { - "nostr-tools": "2.23.1", - "ws": "8.19.0" + "test": "node --test --test-timeout=30000 test/fleet/*.test.mjs", + "torch": "node bin/torch.mjs", + "check": "node --check bin/torch.mjs && node --check src/cli.mjs", + "format": "eslint src/kernel src/cli.mjs bin/torch.mjs test/fleet --fix", + "lint": "eslint src/kernel src/cli.mjs bin/torch.mjs test/fleet" }, + "dependencies": {}, "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.58.2", diff --git a/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml b/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml new file mode 100644 index 0000000..52c27a3 --- /dev/null +++ b/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml @@ -0,0 +1,41 @@ +test_integrity_note: + change_type: + - new_tests + scenarios: + - id: SCN-init-read-only + given: a normal Git repository with committed project files + when: TORCH inspects and analyzes it or produces an installation plan + then: no project or machine-local TORCH state is created and Git status is unchanged + - id: SCN-install-doctor-purge + given: a reviewed fresh installation into a Git repository + when: TORCH installs, diagnoses, and purges its unchanged state + then: doctor reports healthy, all owned TORCH state is removed, and project source remains unchanged + - id: SCN-purge-protects-user-change + given: an installed TORCH project whose managed configuration was edited by its owner + when: a purge is requested + then: TORCH refuses destructive cleanup and preserves the modified file + - id: SCN-cli-install-approval + given: a Git repository without TORCH + when: install is invoked without a reviewed approval flag + then: the command refuses and creates no TORCH state + observable_outcomes: + - Git status before and after read-only analysis + - presence or absence of .torch and XDG-local project state + - doctor health and finding codes + - CLI exit status and structured error code + - preservation of project source and modified owner configuration + determinism_controls: + - isolated temporary Git repositories + - fixed test Git identity + - fixed installation timestamp where content is asserted + - per-fixture XDG_DATA_HOME + anti_cheat_rationale: + prevents: + - init that silently writes files + - installer that overwrites existing state + - doctor that reports healthy without required state + - uninstall that deletes owner-modified configuration + - CLI that mutates without explicit approval + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" diff --git a/src/cli.mjs b/src/cli.mjs new file mode 100644 index 0000000..cf9e327 --- /dev/null +++ b/src/cli.mjs @@ -0,0 +1,68 @@ +import { analyzeRepository } from './kernel/analyze.mjs'; +import { diagnoseProject } from './kernel/doctor.mjs'; +import { asErrorRecord, TorchError } from './kernel/errors.mjs'; +import { inspectRepository } from './kernel/git.mjs'; +import { installProject, planInstall, uninstallProject } from './kernel/install.mjs'; + +const HELP = `TORCH — portable agent fleet + +Usage: + torch init [--json] + torch analyze [--json] + torch install [--dry-run] [--yes] [--json] + torch doctor [--json] + torch uninstall [--dry-run] [--purge] [--json] +`; + +function print(value, { json = false } = {}) { + if (json || typeof value !== 'string') process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); + else process.stdout.write(`${value}\n`); +} + +export async function runCli(argv = process.argv.slice(2), { cwd = process.cwd(), env = process.env } = {}) { + const command = argv.find((arg) => !arg.startsWith('-')) ?? 'help'; + const json = argv.includes('--json'); + try { + if (command === 'help' || argv.includes('--help') || argv.includes('-h')) { + process.stdout.write(HELP); + return 0; + } + const repository = inspectRepository(cwd); + if (command === 'init' || command === 'analyze') { + const analysis = analyzeRepository(repository); + print({ command, repository, analysis, next: 'Review analysis, then run torch install --dry-run.' }, { json }); + return 0; + } + if (command === 'install') { + if (argv.includes('--dry-run')) { + print(planInstall({ repository, env }), { json }); + return 0; + } + if (!argv.includes('--yes')) { + throw new TorchError('Installation requires reviewed approval. Re-run with --dry-run, then --yes.', { + code: 'APPROVAL_REQUIRED', + }); + } + print(installProject({ repository, env }), { json }); + return 0; + } + if (command === 'doctor') { + const diagnosis = diagnoseProject({ repository, env }); + print(diagnosis, { json }); + return diagnosis.healthy ? 0 : 1; + } + if (command === 'uninstall') { + const result = uninstallProject({ + repository, + purge: argv.includes('--purge'), + dryRun: argv.includes('--dry-run'), + }); + print(result, { json }); + return result.canProceed === false ? 1 : 0; + } + throw new TorchError(`Unknown command: ${command}`, { code: 'UNKNOWN_COMMAND' }); + } catch (error) { + print(asErrorRecord(error), { json: true }); + return error instanceof TorchError ? 2 : 1; + } +} diff --git a/src/kernel/analyze.mjs b/src/kernel/analyze.mjs new file mode 100644 index 0000000..5ff7752 --- /dev/null +++ b/src/kernel/analyze.mjs @@ -0,0 +1,87 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { extname } from 'node:path'; +import { trackedFiles } from './git.mjs'; + +const LANGUAGE_BY_EXTENSION = new Map([ + ['.js', 'JavaScript'], ['.mjs', 'JavaScript'], ['.cjs', 'JavaScript'], + ['.ts', 'TypeScript'], ['.tsx', 'TypeScript'], ['.jsx', 'JavaScript'], + ['.py', 'Python'], ['.rs', 'Rust'], ['.go', 'Go'], ['.java', 'Java'], + ['.c', 'C'], ['.h', 'C/C++'], ['.cc', 'C++'], ['.cpp', 'C++'], + ['.cs', 'C#'], ['.rb', 'Ruby'], ['.php', 'PHP'], ['.swift', 'Swift'], + ['.kt', 'Kotlin'], ['.sh', 'Shell'], ['.bash', 'Shell'], ['.ps1', 'PowerShell'], + ['.html', 'HTML'], ['.css', 'CSS'], ['.scss', 'CSS'], ['.vue', 'Vue'], + ['.svelte', 'Svelte'], ['.sol', 'Solidity'], +]); + +const MANIFESTS = [ + 'package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', + 'pom.xml', 'build.gradle', 'Makefile', 'CMakeLists.txt', 'Dockerfile', +]; + +export function analyzeRepository(repository) { + const files = trackedFiles(repository.root); + const languageCounts = new Map(); + const topLevel = new Set(); + + for (const file of files) { + const language = LANGUAGE_BY_EXTENSION.get(extname(file).toLowerCase()); + if (language) languageCounts.set(language, (languageCounts.get(language) ?? 0) + 1); + const first = file.split('/')[0]; + if (file.includes('/')) topLevel.add(first); + } + + const languages = [...languageCounts.entries()] + .map(([name, files]) => ({ name, files })) + .sort((a, b) => b.files - a.files || a.name.localeCompare(b.name)); + const manifests = MANIFESTS.filter((file) => files.includes(file)); + const operations = files.filter((file) => + /(^|\/)(\.github\/workflows|Dockerfile|docker-compose|vercel\.json|systemd|deploy|release|cron)/i.test(file)); + const testFiles = files.filter((file) => /(^|\/)(test|tests|spec|e2e)(\/|\.|$)/i.test(file)); + const docs = files.filter((file) => /(^|\/)(docs?|README)(\/|\.|$)/i.test(file)); + const sharedCandidates = files.filter((file) => + /(^|\/)(package(-lock)?\.json|[^/]*config[^/]*|schema|schemas|types|shared)(\/|\.|$)/i.test(file)); + + const checks = []; + const packagePath = `${repository.root}/package.json`; + if (existsSync(packagePath)) { + try { + const pkg = JSON.parse(readFileSync(packagePath, 'utf8')); + for (const [name, command] of Object.entries(pkg.scripts ?? {})) { + if (/test|check|lint|type|build|verify|audit/i.test(name)) checks.push({ name, command }); + } + } catch { + // The analyzer reports observable repository shape. Invalid manifests are + // surfaced later by doctor and should not make read-only analysis mutate. + } + } + + const scarceResources = []; + const joined = files.join('\n').toLowerCase(); + if (/playwright|puppeteer|cypress|browser/.test(joined)) scarceResources.push('browser'); + if (/cuda|gpu|webgl|three\.js|threejs/.test(joined)) scarceResources.push('gpu'); + if (/postgres|mysql|sqlite|database/.test(joined)) scarceResources.push('database'); + + return { + generatedAt: new Date().toISOString(), + repository: { + root: repository.root, + name: repository.name, + branch: repository.branch, + head: repository.head, + hasRemote: repository.remotes.length > 0, + }, + inventory: { + trackedFileCount: files.length, + languages, + manifests, + topLevelDirectories: [...topLevel].sort(), + testFileCount: testFiles.length, + documentationFileCount: docs.length, + operations: operations.slice(0, 100), + sharedSurfaceCandidates: sharedCandidates.slice(0, 100), + checks, + scarceResources, + }, + mutationPerformed: false, + }; +} diff --git a/src/kernel/doctor.mjs b/src/kernel/doctor.mjs new file mode 100644 index 0000000..fcca436 --- /dev/null +++ b/src/kernel/doctor.mjs @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileHash } from './files.mjs'; +import { projectStatePath } from './paths.mjs'; + +function parseJsonYaml(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +export function diagnoseProject({ repository, env = process.env }) { + const trackedRoot = join(repository.root, '.torch'); + const configPath = join(trackedRoot, 'torch.yaml'); + const manifestPath = join(trackedRoot, 'install-manifest.json'); + const findings = []; + + if (!existsSync(configPath)) { + return { status: 'not-installed', healthy: false, findings: [{ severity: 'error', code: 'CONFIG_MISSING', path: configPath }] }; + } + if (!existsSync(manifestPath)) { + return { status: 'broken', healthy: false, findings: [{ severity: 'error', code: 'MANIFEST_MISSING', path: manifestPath }] }; + } + + let config; + let manifest; + try { config = parseJsonYaml(configPath); } catch (error) { + findings.push({ severity: 'error', code: 'CONFIG_INVALID', message: error.message }); + } + try { manifest = parseJsonYaml(manifestPath); } catch (error) { + findings.push({ severity: 'error', code: 'MANIFEST_INVALID', message: error.message }); + } + + if (manifest) { + for (const record of manifest.created ?? []) { + const path = join(repository.root, record.path); + if (!existsSync(path)) findings.push({ severity: 'error', code: 'OWNED_FILE_MISSING', path: record.path }); + else if (fileHash(path) !== record.sha256) findings.push({ severity: 'info', code: 'OWNED_FILE_MODIFIED', path: record.path }); + } + const stateRoot = projectStatePath(manifest.projectId, env); + if (!existsSync(stateRoot)) findings.push({ severity: 'error', code: 'LOCAL_STATE_MISSING', path: stateRoot }); + } + + if (config?.project?.id && manifest?.projectId && config.project.id !== manifest.projectId) { + findings.push({ severity: 'error', code: 'PROJECT_ID_MISMATCH' }); + } + if (repository.dirtyEntries.length) { + findings.push({ severity: 'info', code: 'WORKTREE_DIRTY', count: repository.dirtyEntries.length }); + } + + return { + status: findings.some((finding) => finding.severity === 'error') ? 'broken' : 'ok', + healthy: !findings.some((finding) => finding.severity === 'error'), + projectId: config?.project?.id ?? null, + findings, + }; +} diff --git a/src/kernel/errors.mjs b/src/kernel/errors.mjs new file mode 100644 index 0000000..47a381e --- /dev/null +++ b/src/kernel/errors.mjs @@ -0,0 +1,16 @@ +export class TorchError extends Error { + constructor(message, { code = 'TORCH_ERROR', details = undefined } = {}) { + super(message); + this.name = 'TorchError'; + this.code = code; + this.details = details; + } +} + +export function asErrorRecord(error) { + return { + error: error?.code ?? 'UNEXPECTED_ERROR', + message: error instanceof Error ? error.message : String(error), + ...(error?.details === undefined ? {} : { details: error.details }), + }; +} diff --git a/src/kernel/files.mjs b/src/kernel/files.mjs new file mode 100644 index 0000000..b3844fc --- /dev/null +++ b/src/kernel/files.mjs @@ -0,0 +1,24 @@ +import { createHash } from 'node:crypto'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { TorchError } from './errors.mjs'; + +export function sha256(content) { + return createHash('sha256').update(content).digest('hex'); +} + +export function writeNewFile(path, content) { + if (existsSync(path)) { + throw new TorchError(`Refusing to overwrite existing path: ${path}`, { + code: 'PATH_ALREADY_EXISTS', + details: { path }, + }); + } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + return { path, sha256: sha256(content) }; +} + +export function fileHash(path) { + return sha256(readFileSync(path)); +} diff --git a/src/kernel/git.mjs b/src/kernel/git.mjs new file mode 100644 index 0000000..78022ff --- /dev/null +++ b/src/kernel/git.mjs @@ -0,0 +1,57 @@ +import { execFileSync } from 'node:child_process'; +import { realpathSync } from 'node:fs'; +import { basename } from 'node:path'; +import { TorchError } from './errors.mjs'; + +function git(cwd, args, { optional = false } = {}) { + try { + return execFileSync('git', ['-C', cwd, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } catch (error) { + if (optional) return ''; + throw new TorchError(`Git command failed: git ${args.join(' ')}`, { + code: 'GIT_COMMAND_FAILED', + details: error.stderr?.toString().trim() || undefined, + }); + } +} + +export function inspectRepository(cwd = process.cwd()) { + const rootText = git(cwd, ['rev-parse', '--show-toplevel'], { optional: true }); + if (!rootText) { + throw new TorchError(`Not a Git repository: ${cwd}`, { code: 'NOT_GIT_REPOSITORY' }); + } + + const root = realpathSync(rootText); + const branch = git(root, ['branch', '--show-current'], { optional: true }) || null; + const head = git(root, ['rev-parse', 'HEAD'], { optional: true }) || null; + const initialCommit = head + ? git(root, ['rev-list', '--max-parents=0', 'HEAD'], { optional: true }).split('\n')[0] || null + : null; + const status = git(root, ['status', '--porcelain=v1', '--untracked-files=all'], { optional: true }); + const remotesText = git(root, ['remote', '-v'], { optional: true }); + const remotes = remotesText + ? remotesText.split('\n').map((line) => { + const [name, url, kindText = ''] = line.split(/\s+/); + return { name, url, kind: kindText.replace(/[()]/g, '') || 'unknown' }; + }) + : []; + + return { + root, + name: basename(root), + branch, + head, + initialCommit, + dirtyEntries: status ? status.split('\n') : [], + remotes, + canonicalRemote: remotes.find((remote) => remote.name === 'origin' && remote.kind === 'fetch') ?? null, + }; +} + +export function trackedFiles(root) { + const output = git(root, ['ls-files', '-z'], { optional: true }); + return output ? output.split('\0').filter(Boolean) : []; +} diff --git a/src/kernel/install.mjs b/src/kernel/install.mjs new file mode 100644 index 0000000..1c800fb --- /dev/null +++ b/src/kernel/install.mjs @@ -0,0 +1,203 @@ +import { randomUUID } from 'node:crypto'; +import { + existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, +} from 'node:fs'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { TorchError } from './errors.mjs'; +import { fileHash, writeNewFile } from './files.mjs'; +import { projectStatePath } from './paths.mjs'; + +const TRACKED_DIR = '.torch'; + +function jsonYaml(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function initialFiles({ repository, projectId, createdAt }) { + const config = { + schema: 'torch.dev/v1alpha1', + project: { + id: projectId, + name: repository.name, + main_branch: repository.branch || 'main', + }, + paths: { tracked_state: TRACKED_DIR, worktree_parent: '~/TORCHWorktrees' }, + repository: repository.canonicalRemote + ? { canonical: { type: 'remote', remote: repository.canonicalRemote.name } } + : { canonical: { type: 'unconfigured' } }, + forge: { provider: 'none' }, + git: { + branch_prefix: 'torch/', convergence: 'merge', allow_rebase: false, + allow_force_push: false, allow_bare_stash: false, + }, + synchronization: { strategy: 'dispatcher-managed', auto_merge_worktrees: false }, + session_manager: { id: 'session-manager', start_last: true }, + domains: [], + }; + const roster = { + schema: 'torch.dev/roster/v1alpha1', + areas: [{ + id: 'session-manager', title: 'TORCH Session Manager', + scope: ['routing', 'priority', 'ownership rulings', 'fleet health'], + not_scope: ['project feature implementation by default'], + neighbours: ['all'], + }], + }; + + return new Map([ + ['torch.yaml', jsonYaml(config)], + ['roster.yaml', jsonYaml(roster)], + ['decisions.md', '# TORCH decisions\n\nNo project decisions recorded yet.\n'], + ['RESUME-BRIEF.md', '# TORCH resume brief\n\nFleet not started yet.\n'], + ['prompts/COMMON.md', '# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n'], + ['prompts/session-manager.md', '# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n'], + ['backlog/.gitkeep', ''], + ['INSTALLATION.md', `# TORCH installation\n\nInstalled ${createdAt}. Run \`torch doctor\` before starting the fleet.\n`], + ]); +} + +function manifestPath(root) { + return join(root, TRACKED_DIR, 'install-manifest.json'); +} + +function readManifest(root) { + const path = manifestPath(root); + if (!existsSync(path)) { + throw new TorchError(`No TORCH ownership manifest at ${path}`, { code: 'NOT_INSTALLED' }); + } + try { + return JSON.parse(readFileSync(path, 'utf8')); + } catch (error) { + throw new TorchError(`Invalid TORCH ownership manifest: ${path}`, { + code: 'INVALID_INSTALL_MANIFEST', details: error.message, + }); + } +} + +function safeProjectPath(root, relativePath) { + const target = resolve(root, relativePath); + const prefix = `${resolve(root)}${sep}`; + if (!target.startsWith(prefix)) { + throw new TorchError(`Manifest path escapes project root: ${relativePath}`, { + code: 'UNSAFE_MANIFEST_PATH', details: { relativePath }, + }); + } + return target; +} + +export function planInstall({ repository, env = process.env }) { + const trackedRoot = join(repository.root, TRACKED_DIR); + return { + action: 'install', + projectRoot: repository.root, + trackedRoot, + stateParent: join(projectStatePath('', env), '..'), + wouldCreateTrackedState: !existsSync(trackedRoot), + mutationPerformed: false, + }; +} + +export function installProject({ + repository, + env = process.env, + projectId = randomUUID(), + now = () => new Date(), +} = {}) { + const trackedRoot = join(repository.root, TRACKED_DIR); + if (existsSync(trackedRoot)) { + throw new TorchError(`Refusing to replace existing ${trackedRoot}`, { + code: 'ALREADY_INSTALLED', details: { trackedRoot }, + }); + } + + const createdAt = now().toISOString(); + const stateRoot = projectStatePath(projectId, env); + if (existsSync(stateRoot)) { + throw new TorchError(`Refusing to replace existing TORCH state: ${stateRoot}`, { + code: 'STATE_ALREADY_EXISTS', details: { stateRoot }, + }); + } + + const created = []; + try { + for (const [path, content] of initialFiles({ repository, projectId, createdAt })) { + const absolute = join(trackedRoot, path); + const record = writeNewFile(absolute, content); + created.push({ path: relative(repository.root, record.path), sha256: record.sha256 }); + } + + mkdirSync(dirname(stateRoot), { recursive: true }); + mkdirSync(stateRoot, { recursive: false }); + for (const dir of ['sessions', 'messages', 'locks', 'leases', 'checks', 'integration', 'logs']) { + mkdirSync(join(stateRoot, dir)); + } + writeFileSync(join(stateRoot, 'project.json'), jsonYaml({ + schema: 'torch.dev/local-project/v1alpha1', projectId, + root: repository.root, createdAt, detachedAt: null, + }), { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + + const manifest = { + schema: 'torch.dev/install-manifest/v1alpha1', + installationId: randomUUID(), + projectId, + createdAt, + created, + patched: [], + external: [{ type: 'local-state', path: stateRoot }], + }; + writeNewFile(manifestPath(repository.root), jsonYaml(manifest)); + return { projectId, trackedRoot, stateRoot, manifest, mutationPerformed: true }; + } catch (error) { + if (existsSync(trackedRoot)) rmSync(trackedRoot, { recursive: true, force: true }); + if (existsSync(stateRoot)) rmSync(stateRoot, { recursive: true, force: true }); + throw error; + } +} + +function nonEmptyEntries(path) { + if (!existsSync(path)) return []; + return readdirSync(path, { recursive: true, withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name !== 'project.json') + .map((entry) => entry.name); +} + +export function planUninstall({ repository, purge = false }) { + const manifest = readManifest(repository.root); + const problems = []; + for (const record of manifest.created) { + const path = safeProjectPath(repository.root, record.path); + if (!existsSync(path)) problems.push({ type: 'missing', path: record.path }); + else if (fileHash(path) !== record.sha256) problems.push({ type: 'modified', path: record.path }); + } + for (const entry of manifest.external ?? []) { + if (entry.type === 'local-state') { + const extras = nonEmptyEntries(entry.path); + if (extras.length) problems.push({ type: 'local-state-not-empty', path: entry.path, entries: extras }); + } + } + return { + action: purge ? 'purge' : 'detach', manifest, problems, + canProceed: !purge || problems.length === 0, + mutationPerformed: false, + }; +} + +export function uninstallProject({ repository, purge = false, dryRun = false }) { + const plan = planUninstall({ repository, purge }); + if (dryRun || !purge) return plan; + if (!plan.canProceed) { + throw new TorchError('TORCH purge stopped because managed state changed', { + code: 'UNSAFE_TO_PURGE', details: plan.problems, + }); + } + + for (const record of [...plan.manifest.created].reverse()) { + rmSync(safeProjectPath(repository.root, record.path)); + } + rmSync(manifestPath(repository.root)); + rmSync(join(repository.root, TRACKED_DIR), { recursive: true }); + for (const entry of plan.manifest.external ?? []) { + if (entry.type === 'local-state' && existsSync(entry.path)) rmSync(entry.path, { recursive: true }); + } + return { ...plan, mutationPerformed: true }; +} diff --git a/src/kernel/paths.mjs b/src/kernel/paths.mjs new file mode 100644 index 0000000..e97eee3 --- /dev/null +++ b/src/kernel/paths.mjs @@ -0,0 +1,11 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +export function torchDataHome(env = process.env) { + const dataHome = env.XDG_DATA_HOME || join(homedir(), '.local', 'share'); + return join(dataHome, 'torch'); +} + +export function projectStatePath(projectId, env = process.env) { + return join(torchDataHome(env), 'projects', projectId); +} diff --git a/test/fleet/cli-scenarios.test.mjs b/test/fleet/cli-scenarios.test.mjs new file mode 100644 index 0000000..a1857db --- /dev/null +++ b/test/fleet/cli-scenarios.test.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { existsSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname; + +function repo() { + const root = mkdtempSync(join(tmpdir(), 'torch-cli-')); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'README.md'), '# Fixture\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']); + return root; +} + +function run(root, args) { + return spawnSync(process.execPath, [CLI, ...args], { + cwd: root, + env: { ...process.env, XDG_DATA_HOME: join(root, '.data') }, + encoding: 'utf8', + }); +} + +test('SCN-cli-init: init reports analysis without creating project state', () => { + const root = repo(); + const result = run(root, ['init', '--json']); + assert.equal(result.status, 0, result.stderr || result.stdout); + const output = JSON.parse(result.stdout); + assert.equal(output.command, 'init'); + assert.equal(output.analysis.mutationPerformed, false); + assert.equal(existsSync(join(root, '.torch')), false); +}); + +test('SCN-cli-install-approval: install requires an explicit reviewed approval flag', () => { + const root = repo(); + const result = run(root, ['install', '--json']); + assert.equal(result.status, 2); + assert.equal(JSON.parse(result.stdout).error, 'APPROVAL_REQUIRED'); + assert.equal(existsSync(join(root, '.torch')), false); +}); diff --git a/test/fleet/kernel-lifecycle.test.mjs b/test/fleet/kernel-lifecycle.test.mjs new file mode 100644 index 0000000..82ff45b --- /dev/null +++ b/test/fleet/kernel-lifecycle.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, writeFileSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { analyzeRepository } from '../../src/kernel/analyze.mjs'; +import { diagnoseProject } from '../../src/kernel/doctor.mjs'; +import { inspectRepository } from '../../src/kernel/git.mjs'; +import { installProject, planInstall, uninstallProject } from '../../src/kernel/install.mjs'; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-kernel-')); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'package.json'), JSON.stringify({ + name: 'fixture-app', scripts: { test: 'node --test', build: 'node build.mjs' }, + })); + writeFileSync(join(root, 'index.js'), 'export const ready = true;\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']); + const stateHome = join(root, '.xdg-data'); + return { root, env: { ...process.env, XDG_DATA_HOME: stateHome } }; +} + +test('SCN-init-read-only: inspection and analysis do not mutate the project', () => { + const { root, env } = fixture(); + const before = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }); + const repository = inspectRepository(root); + const analysis = analyzeRepository(repository); + const plan = planInstall({ repository, env }); + const after = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }); + + assert.equal(analysis.mutationPerformed, false); + assert.equal(plan.mutationPerformed, false); + assert.equal(existsSync(join(root, '.torch')), false); + assert.equal(after, before); + assert.deepEqual(analysis.inventory.languages[0], { name: 'JavaScript', files: 1 }); + assert.equal(analysis.inventory.checks.some((check) => check.name === 'test'), true); +}); + +test('SCN-install-doctor-purge: a fresh install is healthy and exactly reversible', () => { + const { root, env } = fixture(); + const repository = inspectRepository(root); + const installed = installProject({ + repository, env, projectId: 'fixture-project', now: () => new Date('2026-09-27T00:00:00Z'), + }); + + assert.equal(existsSync(join(root, '.torch', 'torch.yaml')), true); + assert.equal(existsSync(installed.stateRoot), true); + const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml'), 'utf8')); + assert.equal(config.project.id, 'fixture-project'); + + const diagnosis = diagnoseProject({ repository: inspectRepository(root), env }); + assert.equal(diagnosis.healthy, true); + assert.equal(diagnosis.status, 'ok'); + + const result = uninstallProject({ repository: inspectRepository(root), purge: true }); + assert.equal(result.mutationPerformed, true); + assert.equal(existsSync(join(root, '.torch')), false); + assert.equal(existsSync(installed.stateRoot), false); + assert.equal(readFileSync(join(root, 'index.js'), 'utf8'), 'export const ready = true;\n'); +}); + +test('SCN-purge-protects-user-change: uninstall refuses to delete modified managed files', () => { + const { root, env } = fixture(); + const repository = inspectRepository(root); + installProject({ repository, env, projectId: 'modified-project' }); + const configPath = join(root, '.torch', 'torch.yaml'); + writeFileSync(configPath, `${readFileSync(configPath, 'utf8')}\n# owner change\n`); + + assert.throws( + () => uninstallProject({ repository: inspectRepository(root), purge: true }), + (error) => error.code === 'UNSAFE_TO_PURGE' + && error.details.some((problem) => problem.type === 'modified' && problem.path === '.torch/torch.yaml'), + ); + assert.equal(existsSync(configPath), true); +});