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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-22
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Why

- `bin/multiagent-safety.js` is roughly 7,864 lines long and currently mixes CLI parsing, template rendering, git/worktree plumbing, protected-base sandboxing, finish/merge logic, toolchain self-update, and output/report formatting.
- That shared module scope makes even small changes hard to review and easy to regress because unrelated helpers are tightly coupled and tests have to exercise one monolith.
- The requested outcome is a seam-based decomposition so future Guardex CLI changes can land in smaller diffs with clearer ownership and lower regression risk.

## What Changes

- Introduce a `src/` runtime layout that separates `cli`, `output`, `git`, `scaffold`, `hooks`, `toolchain`, `sandbox`, and `finish`, with only small shared helpers left in `src/context.js` and `src/core/runtime.js`.
- Reduce `bin/multiagent-safety.js` to a thin entrypoint that boots `src/cli/main.js`.
- Preserve the current command surface, aliases, and targeted behavior while moving the existing logic wholesale into the new modules.
- Update package shipping and regression coverage so installed CLIs still include `src/**` and the extracted runtime stays exercised by install/metadata tests.

## Impact

- Primary surfaces: `bin/multiagent-safety.js`, new `src/**` modules, `package.json`, and CLI regression tests.
- Main refactor risk is hidden cross-module coupling in `doctor`, protected-main sandbox flows, and finish/cleanup helpers, so extraction should move lower-risk seams first and verify after each pass.
- This is an internal architecture cleanup only; it must not intentionally change command names, output contracts, or the zero-copy install surface.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## ADDED Requirements

### Requirement: Thin CLI entrypoint
The CLI SHALL keep `bin/multiagent-safety.js` as a thin bootstrap surface that delegates command execution into `src/cli`.

#### Scenario: Entrypoint delegates into src/cli
- **WHEN** the published CLI binary is executed
- **THEN** `bin/multiagent-safety.js` loads the modular runtime from `src/cli/main.js`
- **AND** command dispatch logic no longer depends on the monolithic file body.

### Requirement: Module seams mirror operational responsibility
The CLI SHALL separate major operational seams into dedicated modules under `src/` instead of keeping them in one file.

#### Scenario: Responsibilities live under dedicated src modules
- **WHEN** a maintainer inspects the refactored CLI
- **THEN** argument parsing and dispatch live under `src/cli`
- **AND** output formatting lives under `src/output`
- **AND** git/worktree helpers live under `src/git`
- **AND** managed-file and template logic live under `src/scaffold` and `src/hooks`
- **AND** toolchain and self-update logic live under `src/toolchain`
- **AND** protected-base sandbox and finish flows live under `src/sandbox` and `src/finish`.

### Requirement: Refactor preserves targeted CLI behavior
The modularization SHALL preserve the current command surface for targeted verified flows.

#### Scenario: Targeted CLI regressions stay green after extraction
- **WHEN** the focused install/metadata/command regression suites and packaging checks are run after the extraction
- **THEN** they pass without command-name regressions
- **AND** the published package still contains the runtime files required by the extracted `src/**` modules.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## Definition of Done

This change is complete only when **all** of the following are true:

- Every checkbox below is checked.
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.

## Handoff

- Handoff: change=`agent-codex-decompose-cli-monolith-2026-04-22-11-06`; branch=`agent/codex/decompose-cli-monolith-2026-04-22-11-06`; scope=`bin/multiagent-safety.js`, new `src/**` runtime modules, packaging metadata, and targeted CLI regression tests; action=`decompose the monolithic CLI into seam-owned modules while preserving the command surface`.
- Copy prompt: Continue `agent-codex-decompose-cli-monolith-2026-04-22-11-06` on branch `agent/codex/decompose-cli-monolith-2026-04-22-11-06`. Work inside the existing sandbox, review `openspec/changes/agent-codex-decompose-cli-monolith-2026-04-22-11-06/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/decompose-cli-monolith-2026-04-22-11-06 --base dev --via-pr --wait-for-merge --cleanup`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-decompose-cli-monolith-2026-04-22-11-06`.
- [x] 1.2 Define normative requirements in `specs/cli-modularization/spec.md`.

## 2. Implementation

- [x] 2.1 Add shared `src/context.js` / `src/core/runtime.js` foundations for constants, process helpers, and low-level utilities.
- [x] 2.2 Extract low-risk seams into `src/output`, `src/git`, `src/scaffold`, `src/hooks`, and `src/toolchain`.
- [x] 2.3 Extract higher-coupling seams into `src/sandbox`, `src/finish`, and `src/cli`.
- [x] 2.4 Reduce `bin/multiagent-safety.js` to a thin launcher that boots `src/cli/main.js`.
- [x] 2.5 Update publish packaging / metadata so installed CLIs ship the new `src/**` runtime.

## 3. Verification

- [x] 3.1 Add/update targeted regression coverage for the thin entrypoint, representative command routes, and package shipping of `src/**`.
- [x] 3.2 Run syntax checks for the entrypoint and extracted modules (`node --check bin/multiagent-safety.js` plus `node --check` on `src/**`).
- [x] 3.3 Run focused install/metadata/command regression suites.
- [x] 3.4 Run `npm pack --dry-run` to confirm `src/**` ships in the package.
- [x] 3.5 Run `openspec validate agent-codex-decompose-cli-monolith-2026-04-22-11-06 --type change --strict`.
- [x] 3.6 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [x] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/decompose-cli-monolith-2026-04-22-11-06 --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
- [x] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
- [x] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).

Completion handoff: PR https://github.com/recodeee/gitguardex/pull/294 state=`MERGED` merged_at=`2026-04-22T10:38:31Z`; `git worktree list` no longer shows `.omx/agent-worktrees/agent__codex__decompose-cli-monolith-2026-04-22-11-06`; `git branch -a --list 'agent/codex/decompose-cli-monolith-2026-04-22-11-06' 'origin/agent/codex/decompose-cli-monolith-2026-04-22-11-06'` returns no refs after `git fetch --prune origin`.
86 changes: 86 additions & 0 deletions src/cli/dispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const {
TOOL_NAME,
COMMAND_TYPO_ALIASES,
DEPRECATED_COMMAND_ALIASES,
SUGGESTIBLE_COMMANDS,
} = require('../context');

function levenshteinDistance(a, b) {
const rows = a.length + 1;
const cols = b.length + 1;
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));

for (let i = 0; i < rows; i += 1) matrix[i][0] = i;
for (let j = 0; j < cols; j += 1) matrix[0][j] = j;

for (let i = 1; i < rows; i += 1) {
for (let j = 1; j < cols; j += 1) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost,
);
}
}
return matrix[a.length][b.length];
}

function maybeSuggestCommand(command) {
let best = null;
let bestDistance = Number.POSITIVE_INFINITY;

for (const candidate of SUGGESTIBLE_COMMANDS) {
const dist = levenshteinDistance(command, candidate);
if (dist < bestDistance) {
bestDistance = dist;
best = candidate;
}
}

if (best && bestDistance <= 2) {
return best;
}

return null;
}

function normalizeCommandOrThrow(command) {
if (COMMAND_TYPO_ALIASES.has(command)) {
const mapped = COMMAND_TYPO_ALIASES.get(command);
console.log(`[${TOOL_NAME}] Interpreting '${command}' as '${mapped}'.`);
return mapped;
}
return command;
}

function warnDeprecatedAlias(aliasName) {
const entry = DEPRECATED_COMMAND_ALIASES.get(aliasName);
if (!entry) return;
console.error(
`[${TOOL_NAME}] '${aliasName}' is deprecated and will be removed in a future major release. ` +
`Use: ${entry.hint}`,
);
}

function extractFlag(args, ...names) {
const flagSet = new Set(names);
let found = false;
const remaining = [];
for (const arg of args) {
if (flagSet.has(arg)) {
found = true;
} else {
remaining.push(arg);
}
}
return { found, remaining };
}

module.exports = {
levenshteinDistance,
maybeSuggestCommand,
normalizeCommandOrThrow,
warnDeprecatedAlias,
extractFlag,
};
Loading