@cruxy/cli 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/agent/loop.d.ts +21 -2
  2. package/dist/agent/loop.js +21 -5
  3. package/dist/approval/classify.js +18 -0
  4. package/dist/approval/index.d.ts +1 -0
  5. package/dist/approval/index.js +1 -0
  6. package/dist/approval/mutex.d.ts +45 -0
  7. package/dist/approval/mutex.js +57 -0
  8. package/dist/approval/prompt.js +11 -0
  9. package/dist/checkpoint/gate.d.ts +65 -0
  10. package/dist/checkpoint/gate.js +86 -0
  11. package/dist/checkpoint/index.d.ts +2 -0
  12. package/dist/checkpoint/index.js +2 -0
  13. package/dist/checkpoint/service.d.ts +9 -0
  14. package/dist/checkpoint/service.js +20 -0
  15. package/dist/checkpoint/set-rollback.d.ts +51 -0
  16. package/dist/checkpoint/set-rollback.js +74 -0
  17. package/dist/cli/commands/rollback.d.ts +11 -6
  18. package/dist/cli/commands/rollback.js +93 -33
  19. package/dist/cli/commands/run.js +104 -21
  20. package/dist/cli/onboard.js +4 -1
  21. package/dist/cli/repl.d.ts +2 -2
  22. package/dist/cli/repl.js +39 -0
  23. package/dist/cli/session-factory.d.ts +27 -4
  24. package/dist/cli/session-factory.js +234 -58
  25. package/dist/config/schema.d.ts +24 -0
  26. package/dist/config/schema.js +9 -0
  27. package/dist/errors/constructors.d.ts +51 -0
  28. package/dist/errors/constructors.js +97 -0
  29. package/dist/errors/types.d.ts +28 -0
  30. package/dist/errors/types.js +38 -0
  31. package/dist/hooks/index.d.ts +1 -0
  32. package/dist/hooks/index.js +1 -0
  33. package/dist/hooks/router.d.ts +58 -0
  34. package/dist/hooks/router.js +136 -0
  35. package/dist/hooks/runner.d.ts +12 -0
  36. package/dist/hooks/runner.js +23 -1
  37. package/dist/indexing/retriever.d.ts +29 -0
  38. package/dist/indexing/retriever.js +26 -0
  39. package/dist/indexing/service.js +3 -1
  40. package/dist/indexing/types.d.ts +7 -0
  41. package/dist/lsp/tools/common.d.ts +34 -7
  42. package/dist/lsp/tools/common.js +33 -11
  43. package/dist/lsp/tools/find-definition.js +2 -2
  44. package/dist/lsp/tools/find-references.js +10 -4
  45. package/dist/lsp/tools/get-diagnostics.js +6 -4
  46. package/dist/mcp/index.d.ts +1 -0
  47. package/dist/mcp/index.js +1 -0
  48. package/dist/mcp/sibling-banner.d.ts +25 -0
  49. package/dist/mcp/sibling-banner.js +34 -0
  50. package/dist/memory/recall.d.ts +24 -0
  51. package/dist/memory/recall.js +54 -0
  52. package/dist/memory/remember-tool.d.ts +3 -0
  53. package/dist/memory/remember-tool.js +11 -1
  54. package/dist/render/diff.js +42 -5
  55. package/dist/sandbox/policy.js +14 -5
  56. package/dist/sandbox/service.d.ts +8 -1
  57. package/dist/sandbox/service.js +4 -1
  58. package/dist/subagent/index.d.ts +1 -0
  59. package/dist/subagent/index.js +1 -0
  60. package/dist/subagent/orchestrator.d.ts +81 -1
  61. package/dist/subagent/orchestrator.js +204 -17
  62. package/dist/subagent/registry-scope.d.ts +13 -0
  63. package/dist/subagent/registry-scope.js +28 -2
  64. package/dist/subagent/semaphore.d.ts +27 -0
  65. package/dist/subagent/semaphore.js +56 -0
  66. package/dist/subagent/spawn-tool.d.ts +57 -0
  67. package/dist/subagent/spawn-tool.js +104 -9
  68. package/dist/subagent/types.d.ts +17 -2
  69. package/dist/testing/run-tests-tool.js +3 -0
  70. package/dist/tools/create-pull-request.d.ts +3 -0
  71. package/dist/tools/create-pull-request.js +50 -4
  72. package/dist/tools/file/apply-patch.js +2 -2
  73. package/dist/tools/file/edit-file.js +2 -2
  74. package/dist/tools/file/glob.d.ts +9 -2
  75. package/dist/tools/file/glob.js +73 -19
  76. package/dist/tools/file/grep-files.d.ts +12 -2
  77. package/dist/tools/file/grep-files.js +113 -38
  78. package/dist/tools/file/paths.d.ts +121 -9
  79. package/dist/tools/file/paths.js +164 -10
  80. package/dist/tools/file/read-file.js +2 -2
  81. package/dist/tools/file/write-file.js +2 -2
  82. package/dist/tools/git-status.d.ts +8 -1
  83. package/dist/tools/git-status.js +43 -11
  84. package/dist/tools/list-files.d.ts +9 -3
  85. package/dist/tools/list-files.js +48 -13
  86. package/dist/tools/search-codebase.d.ts +10 -0
  87. package/dist/tools/search-codebase.js +117 -14
  88. package/dist/tools/shell/exec.js +43 -4
  89. package/dist/tools/types.d.ts +74 -1
  90. package/dist/vcs/git.d.ts +8 -0
  91. package/dist/vcs/git.js +14 -0
  92. package/dist/vcs/github.d.ts +7 -1
  93. package/dist/vcs/github.js +10 -1
  94. package/dist/vcs/service.d.ts +8 -0
  95. package/dist/vcs/service.js +33 -1
  96. package/dist/vcs/types.d.ts +18 -2
  97. package/dist/workspace/add-root.d.ts +27 -0
  98. package/dist/workspace/add-root.js +16 -0
  99. package/dist/workspace/index.d.ts +2 -1
  100. package/dist/workspace/index.js +2 -1
  101. package/dist/workspace/workspace.d.ts +9 -4
  102. package/dist/workspace/workspace.js +9 -4
  103. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { usageError } from "../errors/index.js";
1
+ import path from "node:path";
2
+ import { usageError, vcsRemoteChanged } from "../errors/index.js";
2
3
  import { commit, currentBranch, diffAgainst, ensureFeatureBranch, hasChanges, isProtectedBranch, push, stageAll, } from "./git.js";
3
4
  /** Build a PR service from its dependencies. */
4
5
  export function createPrService(deps) {
@@ -8,6 +9,7 @@ export function createPrService(deps) {
8
9
  }
9
10
  async function openPullRequest(deps, opts) {
10
11
  const { cwd, config, forge, generate, requestApproval } = deps;
12
+ const rootName = deps.rootName ?? path.basename(cwd);
11
13
  const protectedExtra = config.git.protectedBranches;
12
14
  const branchNow = currentBranch(cwd);
13
15
  if (branchNow === null) {
@@ -36,8 +38,14 @@ async function openPullRequest(deps, opts) {
36
38
  body: opts.body,
37
39
  });
38
40
  // ── the one gate: show the whole publish plan before anything mutates ──────────
41
+ // `repo` is the target parsed from `origin` NOW; it is both shown to the human
42
+ // (⚖︎JC-4 — root name AND resolved owner/repo) and captured as the approved
43
+ // target that the wrong-repo guard re-checks before the API call.
39
44
  const decision = await requestApproval({
40
45
  kind: "vcs",
46
+ // C.26 Step 4: the acting root, named exactly as the caller selected it — the
47
+ // checkpoint gate attributes the run's git side effects to this root.
48
+ root: rootName,
41
49
  preview: {
42
50
  type: "pr",
43
51
  branch: content.branchName,
@@ -46,6 +54,7 @@ async function openPullRequest(deps, opts) {
46
54
  commitBody: content.commitBody,
47
55
  prTitle: content.prTitle,
48
56
  prBody: content.prBody,
57
+ target: { host: repo.host, owner: repo.owner, repo: repo.repo },
49
58
  },
50
59
  });
51
60
  if (!decision.allow) {
@@ -61,6 +70,18 @@ async function openPullRequest(deps, opts) {
61
70
  commit(cwd, content.commitSubject, content.commitBody, protectedExtra);
62
71
  }
63
72
  push(cwd, head, protectedExtra);
73
+ // ── wrong-repo guard (⚖︎JC-2 take A): re-resolve the origin identity LOCALLY and
74
+ // compare it to what the human approved, immediately before the API call. A
75
+ // concurrent `git remote set-url` on the mutable .git/config would otherwise open
76
+ // the PR against a repo the user never saw. Mismatch → refuse (never open); we do
77
+ // NOT re-gate or warn-and-proceed. The tiny window between this re-check and the
78
+ // HTTP send is an unclosable residual without a lock — named, not chased. (Git
79
+ // pushed to `origin` by name above; the guarded surface here is the API's
80
+ // owner/repo target, which is the value the human actually approved.)
81
+ const current = forge.resolveRepoIdentity(cwd);
82
+ if (!sameIdentity(repo, current)) {
83
+ throw vcsRemoteChanged(repo, current, rootName);
84
+ }
64
85
  const pr = await forge.createPullRequest(repo, {
65
86
  title: content.prTitle,
66
87
  body: content.prBody,
@@ -77,3 +98,14 @@ async function openPullRequest(deps, opts) {
77
98
  alreadyExists: pr.alreadyExists,
78
99
  };
79
100
  }
101
+ /**
102
+ * Are two forge identities the same repository? Host/owner/repo compared
103
+ * case-insensitively, because GitHub treats them so — a case-only remote rewrite
104
+ * is the same repo and must not trip the wrong-repo guard with a false positive.
105
+ */
106
+ function sameIdentity(a, b) {
107
+ const norm = (s) => s.toLowerCase();
108
+ return (norm(a.host) === norm(b.host) &&
109
+ norm(a.owner) === norm(b.owner) &&
110
+ norm(a.repo) === norm(b.repo));
111
+ }
@@ -3,14 +3,21 @@
3
3
  * interface is the swap seam — GitHub ships first, GitLab/Bitbucket slot in later
4
4
  * without touching call sites (same discipline as `VectorStore`/`Embedder`).
5
5
  */
6
- /** Where a repository lives and who owns it, parsed from the `origin` remote. */
7
- export interface RepoInfo {
6
+ /**
7
+ * The forge-target identity parsed from the `origin` remote — just host/owner/repo,
8
+ * no network lookup. This is the value the C.26 Step-4 wrong-repo guard compares
9
+ * across the preview→API-call window (see {@link ForgeProvider.resolveRepoIdentity}).
10
+ */
11
+ export interface RepoIdentity {
8
12
  /** Forge host, e.g. `github.com`. */
9
13
  readonly host: string;
10
14
  /** Repository owner (user or org). */
11
15
  readonly owner: string;
12
16
  /** Repository name (no `.git` suffix). */
13
17
  readonly repo: string;
18
+ }
19
+ /** Where a repository lives and who owns it, parsed from the `origin` remote. */
20
+ export interface RepoInfo extends RepoIdentity {
14
21
  /** Default branch, when the provider can resolve it (PR base fallback). */
15
22
  readonly defaultBranch?: string;
16
23
  }
@@ -40,6 +47,15 @@ export interface ForgeProvider {
40
47
  readonly id: string;
41
48
  /** Parse the repository's `origin` remote into structured {@link RepoInfo}. */
42
49
  getRepoInfo(cwd: string): Promise<RepoInfo>;
50
+ /**
51
+ * Re-parse just the `origin` remote's {@link RepoIdentity} — LOCAL only, no
52
+ * network. The C.26 Step-4 wrong-repo guard calls this immediately before the
53
+ * pull-request API call and compares it against the identity shown at approval;
54
+ * a mismatch (a mid-run `git remote set-url`) refuses the PR. It stays local so
55
+ * the re-check is cheap and cannot fail on a network hiccup. Throws the same
56
+ * coded usage errors as {@link getRepoInfo} when there is no parseable `origin`.
57
+ */
58
+ resolveRepoIdentity(cwd: string): RepoIdentity;
43
59
  /** Open a pull request and return its URL (idempotent on already-exists). */
44
60
  createPullRequest(repo: RepoInfo, spec: PullRequestSpec): Promise<PullRequestResult>;
45
61
  }
@@ -0,0 +1,27 @@
1
+ import type { RootSpec } from "./types.js";
2
+ import { Workspace } from "./workspace.js";
3
+ /**
4
+ * The interactive add-root path (C.26 step 5). Adding a workspace root is an
5
+ * EXPLICIT HUMAN act — it grows the trust surface (a new root can carry its own
6
+ * hooks / MCP / project memory), so it is:
7
+ *
8
+ * - reachable ONLY from the REPL command and the CLI, never as a model tool.
9
+ * The model can act only through the tool registry (`registry.get(name)`),
10
+ * and no tool is registered for this — the allowlist argument is unchanged.
11
+ * - TTY-only: refused (coded) when there is no interactive human to vouch.
12
+ * - held to the SAME validation as `--root`: the new root must exist, be a
13
+ * directory, have a unique name, and NOT nest/overlap an existing root
14
+ * (CRUXY_E_ROOT_OVERLAP) — reusing {@link buildWorkspace}, the single
15
+ * Workspace constructor from user input.
16
+ *
17
+ * Returns the NEW immutable Workspace (the root set only ever grows by
18
+ * constructing a new one). A freshly added root starts UNTRUSTED; its hooks and
19
+ * project memory stay inert until explicitly trusted.
20
+ */
21
+ export interface AddRootOptions {
22
+ /** Base dir for resolving a relative `path` (the session's primary root). */
23
+ cwd: string;
24
+ /** Whether stdin is a TTY — add-root is refused without an interactive human. */
25
+ tty: boolean;
26
+ }
27
+ export declare function addRootToWorkspace(current: Workspace, spec: RootSpec, opts: AddRootOptions): Promise<Workspace>;
@@ -0,0 +1,16 @@
1
+ import { usageError } from "../errors/index.js";
2
+ import { buildWorkspace } from "./workspace.js";
3
+ export async function addRootToWorkspace(current, spec, opts) {
4
+ if (!opts.tty) {
5
+ throw usageError("adding a workspace root is an interactive action and needs a TTY", ["declare it up front with `--root name=path` instead"]);
6
+ }
7
+ // Rebuild from the existing roots (as specs) + the new one, so EVERY
8
+ // buildWorkspace guard — existence, directory, name uniqueness, and the
9
+ // nested/overlap refusal — applies to the addition. Existing roots keep their
10
+ // names, absolute paths, and order (the primary stays first).
11
+ const specs = [
12
+ ...current.roots().map((r) => ({ name: r.name, path: r.absPath })),
13
+ spec,
14
+ ];
15
+ return buildWorkspace(specs, { cwd: opts.cwd });
16
+ }
@@ -1,5 +1,6 @@
1
1
  export type { DeclaredRoot, RootSpec } from "./types.js";
2
- export { Workspace, buildWorkspace, singleRootWorkspace } from "./workspace.js";
2
+ export { Workspace, buildWorkspace, sessionWorkspace } from "./workspace.js";
3
3
  export { PathEscapeError, confineToRoot, isInside, resolveInWorkspace, } from "./resolve.js";
4
4
  export { selectRoot } from "./select.js";
5
5
  export type { RootRef, SelectedRoot } from "./select.js";
6
+ export { addRootToWorkspace, type AddRootOptions } from "./add-root.js";
@@ -1,3 +1,4 @@
1
- export { Workspace, buildWorkspace, singleRootWorkspace } from "./workspace.js";
1
+ export { Workspace, buildWorkspace, sessionWorkspace } from "./workspace.js";
2
2
  export { PathEscapeError, confineToRoot, isInside, resolveInWorkspace, } from "./resolve.js";
3
3
  export { selectRoot } from "./select.js";
4
+ export { addRootToWorkspace } from "./add-root.js";
@@ -49,8 +49,13 @@ export declare function buildWorkspace(specs: readonly RootSpec[], opts?: {
49
49
  cwd?: string;
50
50
  }): Promise<Workspace>;
51
51
  /**
52
- * Build a trivial single-root workspace from one absolute path — the back-compat
53
- * bridge for the many call sites that still pass a single `cwd`. The one root is
54
- * primary and named by its basename.
52
+ * Build THE session's workspace from a single absolute path — the trivial
53
+ * single-root case. This is a *session construction* primitive, not a per-call
54
+ * fallback: after C.26 Step 6 it is called ONLY at the two CLI entry points that
55
+ * turn argv into a session (`cli/commands/run.ts`, `cli/onboard.ts`), never in the
56
+ * middle of a subsystem. Synthesizing one elsewhere would silently scope that
57
+ * subsystem to its own cwd while the fan tools see N roots (a split-brain) — the
58
+ * `no-split-brain` guard fails on any `sessionWorkspace(` outside those two files.
59
+ * The one root is primary and named by its basename.
55
60
  */
56
- export declare function singleRootWorkspace(absPath: string): Workspace;
61
+ export declare function sessionWorkspace(absPath: string): Workspace;
@@ -168,11 +168,16 @@ export async function buildWorkspace(specs, opts = {}) {
168
168
  return new Workspace(declared);
169
169
  }
170
170
  /**
171
- * Build a trivial single-root workspace from one absolute path — the back-compat
172
- * bridge for the many call sites that still pass a single `cwd`. The one root is
173
- * primary and named by its basename.
171
+ * Build THE session's workspace from a single absolute path — the trivial
172
+ * single-root case. This is a *session construction* primitive, not a per-call
173
+ * fallback: after C.26 Step 6 it is called ONLY at the two CLI entry points that
174
+ * turn argv into a session (`cli/commands/run.ts`, `cli/onboard.ts`), never in the
175
+ * middle of a subsystem. Synthesizing one elsewhere would silently scope that
176
+ * subsystem to its own cwd while the fan tools see N roots (a split-brain) — the
177
+ * `no-split-brain` guard fails on any `sessionWorkspace(` outside those two files.
178
+ * The one root is primary and named by its basename.
174
179
  */
175
- export function singleRootWorkspace(absPath) {
180
+ export function sessionWorkspace(absPath) {
176
181
  const abs = path.resolve(absPath);
177
182
  return new Workspace([
178
183
  { name: path.basename(abs) || "root", absPath: abs, primary: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {