builder-assistant-engineer 0.2.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.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/analyst/prompt.js +19 -0
- package/dist/analyst/retry.js +38 -0
- package/dist/artifacts/diff.js +47 -0
- package/dist/artifacts/lessons.js +32 -0
- package/dist/artifacts/merge.js +70 -0
- package/dist/artifacts/write.js +44 -0
- package/dist/backends/agent-cli.js +174 -0
- package/dist/backends/api.js +171 -0
- package/dist/backends/index.js +10 -0
- package/dist/backends/manual.js +52 -0
- package/dist/backends/sse.js +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/next.js +103 -0
- package/dist/commands/plan.js +71 -0
- package/dist/commands/replan.js +37 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/status.js +95 -0
- package/dist/config/commands.js +85 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/settings.js +8 -0
- package/dist/config/store.js +38 -0
- package/dist/core/bash.js +105 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/fs.js +35 -0
- package/dist/core/git.js +104 -0
- package/dist/core/gitignore.js +14 -0
- package/dist/core/json.js +13 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/process.js +56 -0
- package/dist/core/prompt-loader.js +27 -0
- package/dist/core/state.js +27 -0
- package/dist/core/which.js +24 -0
- package/dist/detect/agents.js +17 -0
- package/dist/detect/mode.js +11 -0
- package/dist/digest/baseline.js +192 -0
- package/dist/digest/budget.js +31 -0
- package/dist/digest/files.js +167 -0
- package/dist/digest/format.js +18 -0
- package/dist/digest/git.js +19 -0
- package/dist/digest/index.js +100 -0
- package/dist/digest/manifests.js +133 -0
- package/dist/digest/read.js +28 -0
- package/dist/digest/redact.js +37 -0
- package/dist/digest/render.js +138 -0
- package/dist/digest/select.js +116 -0
- package/dist/digest/todos.js +16 -0
- package/dist/digest/tree.js +55 -0
- package/dist/digest/walk.js +72 -0
- package/dist/gates/capture.js +145 -0
- package/dist/gates/contract.js +339 -0
- package/dist/gates/enforce.js +143 -0
- package/dist/gates/findings.js +17 -0
- package/dist/gates/gate.js +315 -0
- package/dist/gates/ignore-rules.js +104 -0
- package/dist/gates/protected-files.js +311 -0
- package/dist/gates/regression.js +219 -0
- package/dist/gates/reports.js +349 -0
- package/dist/gates/results.js +295 -0
- package/dist/gates/runners.js +189 -0
- package/dist/gates/state-guard.js +56 -0
- package/dist/i18n/en.js +288 -0
- package/dist/i18n/es.js +288 -0
- package/dist/i18n/index.js +18 -0
- package/dist/interview/adaptive.js +69 -0
- package/dist/interview/brief.js +27 -0
- package/dist/interview/questions.js +18 -0
- package/dist/interview/render.js +19 -0
- package/dist/interview/reply.js +34 -0
- package/dist/next/attempts.js +151 -0
- package/dist/next/start.js +179 -0
- package/dist/plan/continuation.js +40 -0
- package/dist/plan/evidence.js +239 -0
- package/dist/plan/filter.js +20 -0
- package/dist/plan/parser.js +283 -0
- package/dist/plan/prior.js +32 -0
- package/dist/plan/questions.js +48 -0
- package/dist/plan/repair.js +33 -0
- package/dist/plan/report.js +37 -0
- package/dist/plan/run.js +191 -0
- package/dist/prompts/analyst.md +101 -0
- package/dist/prompts/continue.md +11 -0
- package/dist/prompts/fix-format.md +13 -0
- package/dist/prompts/fix-paths.md +23 -0
- package/dist/prompts/lesson.md +18 -0
- package/dist/prompts/retry.md +9 -0
- package/dist/prompts/review.md +36 -0
- package/dist/prompts/task.md +9 -0
- package/dist/review/changes.js +177 -0
- package/dist/review/diff.js +115 -0
- package/dist/review/integrity.js +150 -0
- package/dist/review/mechanical.js +62 -0
- package/dist/review/parse.js +40 -0
- package/dist/review/reviewer.js +23 -0
- package/dist/review/run.js +134 -0
- package/dist/review/scope.js +55 -0
- package/dist/review/secrets.js +163 -0
- package/dist/review/snapshot.js +38 -0
- package/dist/review/tests.js +140 -0
- package/dist/tasks/attempts.js +71 -0
- package/dist/tasks/checks.js +181 -0
- package/dist/tasks/frontmatter.js +58 -0
- package/dist/tasks/graph.js +27 -0
- package/dist/tasks/handoff.js +62 -0
- package/dist/tasks/learn.js +128 -0
- package/dist/tasks/load.js +37 -0
- package/dist/tasks/metrics.js +40 -0
- package/dist/tasks/runs.js +12 -0
- package/dist/tasks/schema.js +96 -0
- package/dist/tasks/select.js +15 -0
- package/dist/tasks/shell-words.js +176 -0
- package/dist/tasks/status.js +13 -0
- package/dist/tasks/verify.js +181 -0
- package/dist/ui/clack.js +46 -0
- package/dist/ui/prompter.js +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BuilderAssistantEngineer contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# builder-assistant-engineer
|
|
2
|
+
|
|
3
|
+
Turn an idea, or an existing repository, into an execution plan that console AI agents can run on their own: Claude Code, opencode, Codex CLI, Gemini CLI or any other.
|
|
4
|
+
|
|
5
|
+
It acts as your tech lead and architect. It interviews you, analyzes the repository and writes files your agent reads as soon as it opens the repo: project memory (`AGENTS.md`), a phased plan, tasks that are ready-to-run prompts, and specialized subagents. Then it hands tasks to the agent one by one and only marks a task done after a series of [gates](#gates) pass: the task's own checks, no regressions in the project's lint, typecheck, build and tests, no tests removed or skipped, mechanical checks on the diff and a review. The gates read a capture taken before the agent starts, stored outside the repository, so an agent that edits the repository cannot rewrite what checks it. [What the gates protect against](#what-the-gates-protect-against) says exactly where that stops.
|
|
6
|
+
|
|
7
|
+
- Zero friction: `npx builder-assistant-engineer` inside your project. No API key required if you already use an agent CLI.
|
|
8
|
+
- Native output for each agent: `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.claude/agents`, `.opencode/agent`, slash commands.
|
|
9
|
+
- Greenfield and brownfield: proposes a stack for new projects, and inventories stack, architecture, conventions, tests, debt and risks (with file paths as evidence) for existing ones.
|
|
10
|
+
- English and Spanish (`--lang en|es`). MIT. No telemetry.
|
|
11
|
+
|
|
12
|
+
## Quickstart (60 seconds)
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
cd your-project
|
|
16
|
+
npx builder-assistant-engineer init # pick backend, agents and language; answer a short interview
|
|
17
|
+
npx builder-assistant-engineer plan # analyze the repo and write the plan, memory and subagents
|
|
18
|
+
npx builder-assistant-engineer next # run the next task, verify it, review it, mark it done
|
|
19
|
+
npx builder-assistant-engineer status # see progress
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node.js 20.12 or newer, and bash for `next` (on Windows, the Git Bash that comes with Git for Windows). For `init`, `plan` and `next` you also need one of the [backends](#backends): an agent CLI you already use, an API key, or any AI you can copy and paste into.
|
|
23
|
+
|
|
24
|
+
## What you get
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
AGENTS.md project memory for every agent: purpose, stack, commands, architecture, rules
|
|
28
|
+
CLAUDE.md / GEMINI.md import AGENTS.md plus tool-specific rules (only for targeted agents)
|
|
29
|
+
docs/plan/00-overview.md
|
|
30
|
+
docs/plan/01-prd.md
|
|
31
|
+
docs/plan/02-architecture.md brownfield: current state with evidence, target state, migration path
|
|
32
|
+
docs/plan/03-decisions/ADR-001-*.md
|
|
33
|
+
docs/plan/04-roadmap.md phases, demo criteria, dependency graph, task index
|
|
34
|
+
docs/plan/tasks/T-001-*.md one self-contained prompt per task
|
|
35
|
+
.claude/agents/*.md subagents in Claude Code's format, including a reviewer
|
|
36
|
+
.opencode/agent/*.md the same subagents in opencode's format
|
|
37
|
+
.claude/commands/*.md next, review and status commands (and .opencode/command/*.md)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every task works as a prompt on its own:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
claude -p "$(cat docs/plan/tasks/T-003-*.md)"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A task file looks like this:
|
|
47
|
+
|
|
48
|
+
````markdown
|
|
49
|
+
---
|
|
50
|
+
id: T-003
|
|
51
|
+
title: Expose GET/POST /users with consistent errors
|
|
52
|
+
status: pending
|
|
53
|
+
phase: 2
|
|
54
|
+
depends_on: [T-001, T-002]
|
|
55
|
+
size: M
|
|
56
|
+
risk: low
|
|
57
|
+
tests: required
|
|
58
|
+
---
|
|
59
|
+
## Goal
|
|
60
|
+
## Context
|
|
61
|
+
## Scope
|
|
62
|
+
## Steps
|
|
63
|
+
## Acceptance criteria
|
|
64
|
+
## Verification
|
|
65
|
+
```sh
|
|
66
|
+
go test ./...
|
|
67
|
+
make check
|
|
68
|
+
```
|
|
69
|
+
Both commands exit 0.
|
|
70
|
+
## Risks and notes
|
|
71
|
+
## Log
|
|
72
|
+
````
|
|
73
|
+
|
|
74
|
+
The task's state lives in its frontmatter (`pending`, `in_progress`, `done`, `blocked`), so progress is versioned with your code. `tests: required` means the task must run more tests than before or add assertions to a test file. `tests: fix` marks a task whose goal is to repair failing tests: the project's test command must end green. `## Log` starts empty; the agent that finishes the task writes its handoff note there.
|
|
75
|
+
|
|
76
|
+
## Example
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
$ npx builder-assistant-engineer status
|
|
80
|
+
|
|
81
|
+
Phase 0 ████████████████████ 1/1
|
|
82
|
+
✔ T-001 M medium Create a reproducible quality gate 1 attempt(s), 14m
|
|
83
|
+
|
|
84
|
+
Phase 1 ░░░░░░░░░░░░░░░░░░░░ 0/1
|
|
85
|
+
▶ T-002 S low Expose GET /health on a testable Gin router in_progress 2 attempt(s), 9m
|
|
86
|
+
|
|
87
|
+
Phase 2 ░░░░░░░░░░░░░░░░░░░░ 0/2
|
|
88
|
+
○ T-003 M medium Implement the in-memory user store pending
|
|
89
|
+
○ T-004 M low Expose GET/POST /users with consistent errors pending waiting on T-002, T-003
|
|
90
|
+
|
|
91
|
+
1/4 done (25%) · next: T-002 Expose GET /health on a testable Gin router
|
|
92
|
+
|
|
93
|
+
Local metrics
|
|
94
|
+
attempts: 3 over 2 task(s), 1.5 per task
|
|
95
|
+
done on the first attempt: 1/2 (50%)
|
|
96
|
+
regressions caught: 1
|
|
97
|
+
time per done task: 14m on average, 14m in total
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`plan` ends with a short summary. If the analyst has questions, blocking ones are asked right there and the rest are listed; every question and answer is saved to `.bae/interview.md`, and after answering a blocking question you can plan again immediately. A clear brief is expected to produce no questions.
|
|
101
|
+
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
| Command | What it does |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `init` | Detects greenfield or brownfield, lets you choose the backend, target agents (multi-select) and language, runs the interview and saves `.bae/config.json` and `.bae/interview.md`. `--brief <files>` loads a written brief. |
|
|
107
|
+
| `plan` | Builds the repository digest, runs the analyst, checks the paths it cites and writes every artifact. `--only plan\|agents\|memory` writes one group. Shows a diff and asks before writing. |
|
|
108
|
+
| `next` | Takes the first task in progress, or the first pending task whose dependencies are done. Hands it to the agent and marks it done only when every [gate](#gates) passes. `--headless` runs the agent without a session. `--allow-skip` goes on when a check cannot run and records the skip. `--accept-finding <id>` accepts one finding by its id (repeatable). |
|
|
109
|
+
| `status` | Phases, tasks, progress and local metrics: attempts per task, tasks done on the first attempt, regressions caught and time per task. |
|
|
110
|
+
| `replan` | Re-analyzes the repository with the finished work. Keeps done tasks, updates or removes pending ones, never reuses ids, and prepends an entry to `docs/plan/CHANGELOG.md`. |
|
|
111
|
+
| `review [task]` | Runs the mechanical checks and then the generated reviewer, read-only, on a task's diff against its acceptance criteria and `AGENTS.md`. Exits with 1 when the review fails. Defaults to the task in progress. Takes `--accept-finding <id>` too. |
|
|
112
|
+
|
|
113
|
+
Global flags: `--backend <name>`, `--lang en|es`, `--dry-run` (prints exactly what would be sent to the AI and changes nothing) and `-y, --yes` (accepts every confirmation).
|
|
114
|
+
|
|
115
|
+
## Backends
|
|
116
|
+
|
|
117
|
+
| Backend | Runs | `plan` and `review` | `next` | Needs |
|
|
118
|
+
| --- | --- | --- | --- | --- |
|
|
119
|
+
| `claude` | `claude -p` | read-only tools (`Read`, `Grep`, `Glob`) | interactive session; `--headless` uses `--permission-mode acceptEdits` | [Claude Code](https://code.claude.com) |
|
|
120
|
+
| `opencode` | `opencode run` | `plan` agent (no edits) | interactive TUI; `--headless` uses the default build agent | [opencode](https://opencode.ai) |
|
|
121
|
+
| `codex` | `codex exec` | `--sandbox read-only` | interactive session; `--headless` uses `--sandbox workspace-write` | [Codex CLI](https://github.com/openai/codex) |
|
|
122
|
+
| `gemini` | `gemini -p` | `--approval-mode plan` | `gemini -i`; `--headless` uses `--approval-mode auto_edit` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) |
|
|
123
|
+
| `api` | HTTPS | single request with the digest | copy-and-paste flow, since an API cannot edit your repo | an API key |
|
|
124
|
+
| `manual` | you | prints the prompt, copies it to the clipboard, reads the answer from stdin or `.bae/tmp/response.md` | copy-and-paste flow | any AI, even a web chat |
|
|
125
|
+
|
|
126
|
+
`init` detects which agent CLIs are installed and suggests the first one. Agent backends receive the prompt on stdin, get the repository digest, and may explore the repository themselves. The analyst never writes files: the CLI parses its answer and writes them. No backend is ever run with permission bypass (`--dangerously-skip-permissions`, `--yolo` and similar).
|
|
127
|
+
|
|
128
|
+
### API backend
|
|
129
|
+
|
|
130
|
+
| Variable | Meaning |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `ANTHROPIC_API_KEY` | Uses the Anthropic Messages API. The default model is `claude-opus-5`. |
|
|
133
|
+
| `OPENAI_BASE_URL`, `OPENAI_API_KEY` | Any OpenAI-compatible endpoint: OpenAI, OpenRouter, Ollama (`http://localhost:11434/v1`, no key needed) and others. |
|
|
134
|
+
| `BAE_MODEL` | Model to use. Required for OpenAI-compatible endpoints. |
|
|
135
|
+
| `BAE_API_PROVIDER` | `anthropic` or `openai`, when both sets of variables are present. |
|
|
136
|
+
| `ANTHROPIC_BASE_URL` | Optional Anthropic-compatible proxy. |
|
|
137
|
+
|
|
138
|
+
## How `next` works
|
|
139
|
+
|
|
140
|
+
1. Picks the task in progress that still has attempts left, then the first pending task whose `depends_on` are all done, ordered by phase and id. A task in progress that used its attempts goes after the ready ones.
|
|
141
|
+
2. Checks that the task can be verified at all. If its Verification is missing, trivial or refused, the task is marked `blocked` with the reason, and no agent is launched.
|
|
142
|
+
3. When the regression gate is `full`, runs the project's lint, typecheck, build and test commands to record a baseline. Then it marks the task `in_progress` and takes the capture: the current commit, the files that were already uncommitted, the ignore rules, the config, the task, the prompts, the reviewer and every file that defines the checks.
|
|
143
|
+
4. Hands it to the agent. The prompt opens with where the plan stands, the project commands and the handoff notes of the last three finished tasks. By default it opens an interactive session with the task as the first prompt; exit the session when you are done. With `--headless` the agent runs on its own with edits accepted, and stops after `agent.timeoutMinutes`.
|
|
144
|
+
5. Runs the gates below, always in the repository root. In an interactive run the commands are shown and you confirm them, with a warning for anything that looks dangerous. When nobody confirms (`--headless` or `--yes`), only known runners and checks run (see `verify.allow`).
|
|
145
|
+
6. If every gate passes, the task is marked `done`. Otherwise it stays `in_progress` and you see why.
|
|
146
|
+
7. With `--headless`, a failed attempt is retried with the task plus the failure output. A task gets three attempts in total, counted across runs of `next`, including attempts that ended because the agent failed or timed out. After that the task is marked `blocked` with the reason, and the logs stay in the state directory (see [Files and safety](#files-and-safety)).
|
|
147
|
+
|
|
148
|
+
When something a gate needs cannot be had (no git repository, a declined or unusable baseline, a task that was already in progress without a capture, a reviewer with no verdict), `next` stops and says why instead of skipping it. `--allow-skip` goes on without that check and records the skip in the run log and the attempt.
|
|
149
|
+
|
|
150
|
+
## Gates
|
|
151
|
+
|
|
152
|
+
A task that fails a gate is not `done`. The `next` gates run in this order and read only the capture, never `.bae/` or the task file as the agent left them.
|
|
153
|
+
|
|
154
|
+
| Gate | When | What it does |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| Evidence | `plan`, `replan` | Every path cited in the architecture, the ADRs and each task's Context must exist, and every `path:line` range must fit in the file. Files the plan will create are marked `(new)`. Wrong citations trigger one request that asks the analyst to fix only those. In a brownfield plan, a `path:line` citation that is still wrong stops the plan, and nothing is written. Paths without a line that stay unverified are listed under the summary and in `.bae/tmp/plan-report.json`. |
|
|
157
|
+
| Questions | `plan`, `replan` | Blocking questions are asked on the spot; the others are listed. All of them are saved to `.bae/interview.md`, and you can plan again right away with your answers. |
|
|
158
|
+
| Contract | `next` | These must not change while the agent works: the task file outside `## Log`, the other task files, `.bae/`, the agent definitions, settings and memory files, `.gitignore` and `.gitattributes`, `.git/config`, the `package.json` scripts, package manager settings (`.npmrc`, `.yarnrc*`, `bunfig.toml`, `.envrc`), test runner, `tsconfig` and lint configs, runner setup files and the scripts the checks execute. Nothing may be added that would run instead of the project's tool (another Makefile, a local module that shadows one run with `python -m`), and no file may be marked unchanged in the git index. Anything that changed is restored and the attempt fails. The contract is checked after the agent, again after the suite and again after Verification, because both run the agent's code. When the task's Scope lists a `.gitignore`, a `package.json` or a runner, lint or `tsconfig` file, the change stays and the reviewer sees it, unless a script stops running a tool it ran or a setting that leaves tests out is added. Other files of this kind that are not bae's own may change when the Scope lists them and you accept the finding. |
|
|
159
|
+
| Regression | `next` | Runs the project's `lint`, `typecheck`, `build` and `test` commands and compares each one with its own baseline. A command that passed must still pass. For one that already failed, no test may fail that did not fail before, and its counts must not get worse. Test counts and failing names come from the runner's own report where it has one (vitest and jest JSON, pytest and `node --test` JUnit XML, `go test -json`, `dotnet test` TRX, and `cargo test`'s standard output), and from the last summary the runner prints otherwise. A test command whose runner bae does not recognize, or whose counts it cannot read, has an unknown verdict and never passes; `next` stops before launching the agent when it can tell in advance. One that already failed and has no counts only passes green. |
|
|
160
|
+
| Test integrity | `next` | Fewer tests run or more tests skipped than in the baseline blocks the task, unless the task's Scope marks the test file it removes with `(delete)`. From the diff, a deleted test file, a test removed and not added back under the same name, fewer assertion lines in any test file, a new `.skip`, `.only`, `.todo`, `skipIf`, `xit`, `@pytest.mark.skip`, `pytestmark`, `t.Skip`, `#[ignore]` or similar, a changed snapshot and a new runner config that leaves tests out also block. In a test file the Scope lists, those findings go to the reviewer instead, except expected-failure markers, and the reviewer must say why each one is correct or the review fails. A blanket suppression such as `@ts-ignore` blocks; one that names a rule goes to the reviewer. |
|
|
161
|
+
| Verification | `next` | The task's `## Verification` block runs as one bash script with `set -Eeuo pipefail`, so a failure anywhere, including inside a pipe, fails it. A block that runs no runner and no check, or that hides failures with `\|\| true`, is refused. A line that repeats a project command reuses its result, and a command that already failed and did not get worse does not block, unless the task is `tests: fix`. |
|
|
162
|
+
| Handoff note | `next` | A missing note, or one longer than 8 lines, under `## Log` is reported as a warning. |
|
|
163
|
+
| Mechanical review | `next`, `review` | On the task's diff: a `.env` or key file, an added token or key, a hard-coded password, credentials in a connection string or a long random string fails (known example keys are fine); `tests: required` fails unless the task runs more tests or adds assertions; files outside the task's Scope are reported. The AI reviewer only runs when nothing here blocks, and its pass never overrides a blocker. |
|
|
164
|
+
| Review | `next`, `review` | The generated reviewer checks the diff against the acceptance criteria and `AGENTS.md`. The diff puts the Scope files first and lockfiles last, and sits between markers the reviewer is told to treat as data. A reviewer with no valid verdict fails the attempt. A task that changed no files and passes its checks skips the reviewer. |
|
|
165
|
+
| Lessons | `next` | When a task ends `blocked` or fails its review twice, the agent proposes the root cause and a one-line rule. Once you approve it (`--yes` approves), the rule goes to a Lessons learned list in the managed block of `AGENTS.md`, which replans keep. |
|
|
166
|
+
|
|
167
|
+
Every finding a gate can accept has an id, printed next to it, such as `(secret-1a2b3c4d)`. `--accept-finding <id>` accepts it for that run, and the acceptance is recorded in the run log and the attempt. Secrets can always be accepted this way. Contract and test integrity findings on a file can be accepted only when the task's Scope lists that file.
|
|
168
|
+
|
|
169
|
+
### What the gates protect against
|
|
170
|
+
|
|
171
|
+
The gates are built for an agent that makes mistakes or takes shortcuts by editing the repository or running the commands its CLI allows: rewriting a test script, deleting, skipping or swapping a failing test, changing its own task or status, hiding files with `.gitignore`, git attributes, git config or index flags, overriding the review prompt, or crashing before the checks. Four rounds of adversarial review of the gates found 17, then 32, then 64 problems, and then 82 more in a round that also counted correct work the gates stopped; running `next` for real on the eval fixtures and on a real project found 5 more blocks of correct work. Each case that is in scope has an end-to-end test in [`test/e2e/bypass`](test/e2e/bypass) that tries it and checks that the gates stop it, and [`test/e2e/legit`](test/e2e/legit) runs correct and broken changes through real vitest, go, cargo, pytest and dotnet projects. Those tests run in CI on Linux, macOS and Windows. What stays open is listed in [docs/limits.md](docs/limits.md).
|
|
172
|
+
|
|
173
|
+
The checks run the project's tests, and the tests are code the agent may have written. That code runs with your permissions. bae checks its own state and the protected files after running it, restores anything that changed and fails the attempt, but it cannot stop code that attacks your machine on purpose, for example a process that keeps running after the checks end. For the same reason the gates do not protect against an agent with full access to your machine, which can replace `npm`, `bash` or this CLI, edit dependencies inside ignored folders such as `node_modules`, or write to the state directory in your home. Run agents with the permissions their CLI gives them (`--headless` never bypasses them), run untrusted work in a container, and review what they change.
|
|
174
|
+
|
|
175
|
+
Known limits, with the full list in [docs/limits.md](docs/limits.md):
|
|
176
|
+
|
|
177
|
+
- With `gates.regression: task` there is no baseline, so test counts are not compared (the checks on the diff still apply).
|
|
178
|
+
- Files in ignored folders, other git worktrees and submodules are outside what the gates see, although the runner may load them.
|
|
179
|
+
- Rewriting a test to expect the broken value is left to the reviewer.
|
|
180
|
+
- One run decides a regression, so a flaky test can block a task.
|
|
181
|
+
- Interactive runs never block a task on their own; after three failed attempts the task goes behind the ready ones.
|
|
182
|
+
- With `--yes`, a proposed lesson is added to `AGENTS.md` without asking.
|
|
183
|
+
- Headless agents in a folder you have not trusted in Claude Code cannot run commands, because Claude Code ignores the project's permissions there; tasks that need installs then fail their checks.
|
|
184
|
+
|
|
185
|
+
### Configuration
|
|
186
|
+
|
|
187
|
+
`plan` fills the project's commands in `.bae/config.json`, first from the analyst and then from `package.json` scripts, Makefile targets, Python tools, `go.mod` or `Cargo.toml`. It never overwrites a command you set.
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"commands": {
|
|
192
|
+
"test": "npm test",
|
|
193
|
+
"lint": "npm run lint",
|
|
194
|
+
"typecheck": "npm run typecheck",
|
|
195
|
+
"build": "npm run build"
|
|
196
|
+
},
|
|
197
|
+
"gates": { "regression": "full", "timeoutMinutes": 15 },
|
|
198
|
+
"verify": { "allow": ["./scripts/check.sh"] },
|
|
199
|
+
"secrets": { "allow": ["test/fixtures/**"] },
|
|
200
|
+
"agent": { "timeoutMinutes": 45 }
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- `gates.regression` is `full` (default: a baseline of lint, typecheck, build and test before the agent starts, compared after it), `task` (the same commands only after the task, so any failure blocks; cheaper when the suite is already green) or `off` (only the task's own Verification).
|
|
205
|
+
- `verify.allow` lists command prefixes that may run when nobody confirms them, besides the known runners and checks.
|
|
206
|
+
- `secrets.allow` lists path globs that the secret checks skip, such as test fixtures.
|
|
207
|
+
- `agent.timeoutMinutes` stops a headless agent that runs longer. The attempt is recorded.
|
|
208
|
+
- `gates.timeoutMinutes` (15 by default) stops a project command or a Verification block that runs longer; the command then counts as failed.
|
|
209
|
+
|
|
210
|
+
## Customizing prompts
|
|
211
|
+
|
|
212
|
+
The prompts live in [`src/prompts`](src/prompts). To change one for a project, copy it to `.bae/prompts/<name>.md` and edit it; the CLI uses your copy instead. Every `{{variable}}` must stay defined, or the CLI stops with an error. `next` captures the prompts when it starts a task, so an override written while the agent works is removed and fails the attempt.
|
|
213
|
+
|
|
214
|
+
| Prompt | Variables |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `analyst.md` | `mode`, `project_type`, `output_language`, `target_agents`, `interview`, `repo_digest`, `can_explore_repo`, `prior_plan` |
|
|
217
|
+
| `task.md` | `context`, `task`, `task_path`, `max_log_lines`, `suite` (what `next` hands to the agent) |
|
|
218
|
+
| `review.md` | `reviewer`, `agents_md`, `task`, `diff`, `checks`, `output_language` |
|
|
219
|
+
| `retry.md` | `task`, `failure`, `attempt`, `task_path`, `max_log_lines` |
|
|
220
|
+
| `fix-format.md` | `error`, `format`, `previous_response` |
|
|
221
|
+
| `fix-paths.md` | `paths`, `files`, `repo_files` |
|
|
222
|
+
| `continue.md` | `prompt`, `partial`, `next_marker` |
|
|
223
|
+
| `lesson.md` | `reason`, `task`, `failures`, `agents_md`, `output_language` |
|
|
224
|
+
|
|
225
|
+
The analyst answers in a strict format (`<<<SUMMARY>>>`, `<<<QUESTIONS>>>`, `<<<CONFIG>>>` and `<<<FILE: path>>>` blocks). Known slips are repaired locally first: markers with extra spaces or in lowercase, and a missing `<<<END FILE>>>` on the last block when the backend reports that the answer ended normally. If the answer is cut off by the model's output limit, the CLI asks it to continue from the unfinished block (up to three times). If the answer is still malformed, the CLI asks once to fix only the format; if it is still wrong, the raw answer is saved to `.bae/tmp/last-response.md`. Repairs, retries and unverified paths are recorded in `.bae/tmp/plan-report.json`.
|
|
226
|
+
|
|
227
|
+
## Files and safety
|
|
228
|
+
|
|
229
|
+
- `.bae/config.json` holds configuration only; `.bae/interview.md` holds your answers and the analyst's questions, and can be edited by hand. `.bae/tmp/` holds scratch files and is added to `.gitignore`.
|
|
230
|
+
- What the gates trust lives outside the repository, in `~/.bae/<repo-key>/runs/T-NNN/` (`BAE_HOME` moves `~/.bae`): each task's capture with its base commit, baseline and protected files, the run logs, the attempt records (`attempts.jsonl`, the source of the `status` metrics) and the lesson.
|
|
231
|
+
- The digest respects `.gitignore` and `.baeignore`, skips binaries, lockfiles and `node_modules`, never reads `.env` files or keys, redacts tokens and secrets it finds, and stays within a character budget (100,000 by default, `digest.maxChars` in the config) with priority manifests > entry points > docs > the rest.
|
|
232
|
+
- Existing files are merged, not overwritten. Generated memory goes between `<!-- bae:begin -->` and `<!-- bae:end -->` in `AGENTS.md`, `CLAUDE.md` and `GEMINI.md`; your text outside the markers is kept. Plan files are shown as a diff before writing, and done tasks are never touched.
|
|
233
|
+
- Nothing is sent anywhere except to the backend you choose. There is no telemetry.
|
|
234
|
+
|
|
235
|
+
## Global install
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
npm install -g builder-assistant-engineer
|
|
239
|
+
bae status
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`bae` is a short alias that only exists after a global install. Do not run `npx bae`: that is a different npm package.
|
|
243
|
+
|
|
244
|
+
## Contributing
|
|
245
|
+
|
|
246
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadPrompt, renderPrompt } from "../core/prompt-loader.js";
|
|
2
|
+
export const LANGUAGE_NAMES = { en: "English", es: "Spanish" };
|
|
3
|
+
export async function buildAnalystPrompt(cwd, input) {
|
|
4
|
+
const prompt = await loadPrompt("analyst", cwd);
|
|
5
|
+
return renderPrompt(prompt, {
|
|
6
|
+
mode: input.mode,
|
|
7
|
+
project_type: input.projectType,
|
|
8
|
+
output_language: LANGUAGE_NAMES[input.lang],
|
|
9
|
+
target_agents: input.targets.join(", "),
|
|
10
|
+
interview: wrap("interview", input.interview),
|
|
11
|
+
repo_digest: wrap("repo_digest", input.digest),
|
|
12
|
+
can_explore_repo: String(input.canExplore),
|
|
13
|
+
prior_plan: wrap("prior_plan", input.priorPlan),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function wrap(tag, value) {
|
|
17
|
+
const body = value.trim() || "(empty)";
|
|
18
|
+
return `\n<${tag}>\n${body}\n</${tag}>\n`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { FormatError, UserError } from "../core/errors.js";
|
|
3
|
+
import { writeText } from "../core/fs.js";
|
|
4
|
+
import { baePaths } from "../core/paths.js";
|
|
5
|
+
import { loadPrompt, renderPrompt } from "../core/prompt-loader.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
7
|
+
export async function runWithFormatRetry(request) {
|
|
8
|
+
const { backend, options, parse } = request;
|
|
9
|
+
const complete = request.complete ?? (async (text) => text);
|
|
10
|
+
const first = await complete(await backend.run(request.prompt, options));
|
|
11
|
+
try {
|
|
12
|
+
return parse(first);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (!(error instanceof FormatError))
|
|
16
|
+
throw error;
|
|
17
|
+
request.onRetry?.(error);
|
|
18
|
+
const fix = renderPrompt(request.fixPrompt ?? (await loadPrompt("fix-format", options.cwd)), {
|
|
19
|
+
error: error.message,
|
|
20
|
+
format: request.format,
|
|
21
|
+
previous_response: first,
|
|
22
|
+
});
|
|
23
|
+
return parseOrReport(await complete(await backend.run(fix, options)), parse, options.cwd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function parseOrReport(text, parse, cwd) {
|
|
27
|
+
try {
|
|
28
|
+
return parse(text);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!(error instanceof FormatError))
|
|
32
|
+
throw error;
|
|
33
|
+
const path = join(baePaths(cwd).tmp, "last-response.md");
|
|
34
|
+
await writeText(path, text);
|
|
35
|
+
const shown = relative(cwd, path).split("\\").join("/");
|
|
36
|
+
throw new UserError(t("format.failed", { details: error.message, path: shown }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createTwoFilesPatch } from "diff";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
4
|
+
export function renderDiff(change) {
|
|
5
|
+
return patchLines(change).map(colorize).join("\n");
|
|
6
|
+
}
|
|
7
|
+
function patchLines(change) {
|
|
8
|
+
const patch = createTwoFilesPatch(`a/${change.path}`, `b/${change.path}`, change.before ?? "", change.after, "", "", { context: 3 });
|
|
9
|
+
return patch.split("\n").filter((line) => !line.startsWith("====="));
|
|
10
|
+
}
|
|
11
|
+
export function describeChanges(changes) {
|
|
12
|
+
return changes.map(describe).join("\n");
|
|
13
|
+
}
|
|
14
|
+
function describe(change) {
|
|
15
|
+
if (change.kind === "create") {
|
|
16
|
+
return `${pc.green("+")} ${change.path} (${t("artifacts.new", { lines: lineCount(change.after) })})`;
|
|
17
|
+
}
|
|
18
|
+
if (change.kind === "update") {
|
|
19
|
+
const { added, removed } = countChanges(change);
|
|
20
|
+
return `${pc.yellow("~")} ${change.path} (${t("artifacts.updated", { added, removed })})`;
|
|
21
|
+
}
|
|
22
|
+
if (change.kind === "keep")
|
|
23
|
+
return `${pc.cyan("!")} ${change.path} (${t("artifacts.keptDone")})`;
|
|
24
|
+
if (change.kind === "delete")
|
|
25
|
+
return `${pc.red("-")} ${change.path} (${t("artifacts.removed")})`;
|
|
26
|
+
return `${pc.dim("=")} ${change.path} (${t("artifacts.unchanged")})`;
|
|
27
|
+
}
|
|
28
|
+
function countChanges(change) {
|
|
29
|
+
const lines = patchLines(change);
|
|
30
|
+
const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
31
|
+
const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
32
|
+
return { added, removed };
|
|
33
|
+
}
|
|
34
|
+
function colorize(line) {
|
|
35
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
36
|
+
return pc.bold(line);
|
|
37
|
+
if (line.startsWith("+"))
|
|
38
|
+
return pc.green(line);
|
|
39
|
+
if (line.startsWith("-"))
|
|
40
|
+
return pc.red(line);
|
|
41
|
+
if (line.startsWith("@@"))
|
|
42
|
+
return pc.cyan(line);
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
function lineCount(text) {
|
|
46
|
+
return text.trimEnd().split("\n").length;
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
export const LESSONS_BEGIN = "<!-- bae:lessons -->";
|
|
3
|
+
export const LESSONS_END = "<!-- bae:lessons:end -->";
|
|
4
|
+
export function readLessons(text) {
|
|
5
|
+
const start = text.indexOf(LESSONS_BEGIN);
|
|
6
|
+
const end = text.indexOf(LESSONS_END);
|
|
7
|
+
if (start === -1 || end < start)
|
|
8
|
+
return [];
|
|
9
|
+
return text
|
|
10
|
+
.slice(start + LESSONS_BEGIN.length, end)
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.map((line) => line.trim())
|
|
13
|
+
.filter((line) => line.startsWith("- "))
|
|
14
|
+
.map((line) => line.slice(2).trim());
|
|
15
|
+
}
|
|
16
|
+
export function withLessons(body, lessons) {
|
|
17
|
+
const clean = removeLessons(body).trimEnd();
|
|
18
|
+
if (lessons.length === 0)
|
|
19
|
+
return clean;
|
|
20
|
+
const list = lessons.map((lesson) => `- ${lesson}`).join("\n");
|
|
21
|
+
const section = `## ${t("md.lessons")}\n\n${LESSONS_BEGIN}\n${list}\n${LESSONS_END}`;
|
|
22
|
+
return clean ? `${clean}\n\n${section}` : section;
|
|
23
|
+
}
|
|
24
|
+
function removeLessons(body) {
|
|
25
|
+
const start = body.indexOf(LESSONS_BEGIN);
|
|
26
|
+
const end = body.indexOf(LESSONS_END);
|
|
27
|
+
if (start === -1 || end < start)
|
|
28
|
+
return body;
|
|
29
|
+
const heading = /(?:^|\n)## [^\n]*\n\s*$/.exec(body.slice(0, start));
|
|
30
|
+
const from = heading ? heading.index : start;
|
|
31
|
+
return `${body.slice(0, from)}${body.slice(end + LESSONS_END.length)}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readTextIfExists } from "../core/fs.js";
|
|
3
|
+
import { setFrontmatterFields } from "../tasks/frontmatter.js";
|
|
4
|
+
import { loadTaskFiles } from "../tasks/load.js";
|
|
5
|
+
import { TASK_PATH } from "../tasks/schema.js";
|
|
6
|
+
import { LESSONS_BEGIN, readLessons, withLessons } from "./lessons.js";
|
|
7
|
+
export const MANAGED_BEGIN = "<!-- bae:begin -->";
|
|
8
|
+
export const MANAGED_END = "<!-- bae:end -->";
|
|
9
|
+
const MANAGED_FILES = new Set(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
|
|
10
|
+
const IMPORTING_FILES = new Set(["CLAUDE.md", "GEMINI.md"]);
|
|
11
|
+
const IMPORT_LINE = /^@AGENTS\.md[ \t]*$/m;
|
|
12
|
+
const CHANGELOG = "docs/plan/CHANGELOG.md";
|
|
13
|
+
export async function planChanges(cwd, files) {
|
|
14
|
+
const tasks = new Map((await loadTaskFiles(cwd)).map((task) => [task.id, task]));
|
|
15
|
+
return Promise.all(files.map((file) => changeFor(cwd, file, tasks)));
|
|
16
|
+
}
|
|
17
|
+
export function mergeManaged(before, generated, path) {
|
|
18
|
+
const content = path === "AGENTS.md" ? keepLessons(before, generated) : generated;
|
|
19
|
+
const block = (body) => `${MANAGED_BEGIN}\n${body.trim()}\n${MANAGED_END}`;
|
|
20
|
+
const merged = (body) => {
|
|
21
|
+
if (before === undefined)
|
|
22
|
+
return `${block(body)}\n`;
|
|
23
|
+
const start = before.indexOf(MANAGED_BEGIN);
|
|
24
|
+
const end = before.indexOf(MANAGED_END);
|
|
25
|
+
if (start !== -1 && end > start) {
|
|
26
|
+
return before.slice(0, start) + block(body) + before.slice(end + MANAGED_END.length);
|
|
27
|
+
}
|
|
28
|
+
return `${before.trimEnd()}\n\n${block(body)}\n`;
|
|
29
|
+
};
|
|
30
|
+
const result = merged(content);
|
|
31
|
+
if (!IMPORTING_FILES.has(path) || IMPORT_LINE.test(result))
|
|
32
|
+
return result;
|
|
33
|
+
return merged(`@AGENTS.md\n\n${content.trim()}`);
|
|
34
|
+
}
|
|
35
|
+
export function managedBody(text) {
|
|
36
|
+
const start = text.indexOf(MANAGED_BEGIN);
|
|
37
|
+
const end = text.indexOf(MANAGED_END);
|
|
38
|
+
if (start === -1 || end < start)
|
|
39
|
+
return "";
|
|
40
|
+
return text.slice(start + MANAGED_BEGIN.length, end).trim();
|
|
41
|
+
}
|
|
42
|
+
function keepLessons(before, content) {
|
|
43
|
+
const lessons = readLessons(before ?? "");
|
|
44
|
+
if (lessons.length === 0 || content.includes(LESSONS_BEGIN))
|
|
45
|
+
return content;
|
|
46
|
+
return withLessons(content, lessons);
|
|
47
|
+
}
|
|
48
|
+
async function changeFor(cwd, file, tasks) {
|
|
49
|
+
const existing = tasks.get(TASK_PATH.exec(file.path)?.[1] ?? "");
|
|
50
|
+
const path = existing?.path ?? file.path;
|
|
51
|
+
const before = await readTextIfExists(join(cwd, ...path.split("/")));
|
|
52
|
+
if (existing?.task?.meta.status === "done")
|
|
53
|
+
return { path, before, after: before ?? "", kind: "keep" };
|
|
54
|
+
const after = contentFor(file, before, existing);
|
|
55
|
+
const kind = before === undefined ? "create" : before === after ? "unchanged" : "update";
|
|
56
|
+
return { path, before, after, kind };
|
|
57
|
+
}
|
|
58
|
+
function contentFor(file, before, existing) {
|
|
59
|
+
if (MANAGED_FILES.has(file.path))
|
|
60
|
+
return mergeManaged(before, file.content, file.path);
|
|
61
|
+
if (file.path === CHANGELOG)
|
|
62
|
+
return prependChangelog(before, file.content);
|
|
63
|
+
const status = existing?.task?.meta.status;
|
|
64
|
+
return status ? setFrontmatterFields(file.content, { status }) : file.content;
|
|
65
|
+
}
|
|
66
|
+
function prependChangelog(before, content) {
|
|
67
|
+
if (!before || before.includes(content.trim()))
|
|
68
|
+
return content;
|
|
69
|
+
return `${content.trimEnd()}\n\n${before}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeText } from "../core/fs.js";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
|
+
import { describeChanges, renderDiff } from "./diff.js";
|
|
6
|
+
const WRITABLE = new Set(["create", "update", "delete"]);
|
|
7
|
+
export async function confirmChanges(ctx, changes) {
|
|
8
|
+
if (changes.length === 0)
|
|
9
|
+
return [];
|
|
10
|
+
ctx.prompter.note(describeChanges(changes), t("artifacts.changes"));
|
|
11
|
+
const pending = changes.filter((change) => WRITABLE.has(change.kind));
|
|
12
|
+
if (pending.length === 0)
|
|
13
|
+
return [];
|
|
14
|
+
for (const change of pending.filter((candidate) => candidate.kind !== "create")) {
|
|
15
|
+
ctx.print(`${renderDiff(change)}\n`);
|
|
16
|
+
}
|
|
17
|
+
if (ctx.flags.yes)
|
|
18
|
+
return pending;
|
|
19
|
+
const decision = await ctx.prompter.select(t("artifacts.confirm", { count: pending.length }), [
|
|
20
|
+
{ value: "all", label: t("artifacts.all") },
|
|
21
|
+
{ value: "each", label: t("artifacts.each") },
|
|
22
|
+
{ value: "none", label: t("artifacts.none") },
|
|
23
|
+
], "all");
|
|
24
|
+
if (decision === "none")
|
|
25
|
+
return [];
|
|
26
|
+
if (decision === "all")
|
|
27
|
+
return pending;
|
|
28
|
+
const accepted = [];
|
|
29
|
+
for (const change of pending) {
|
|
30
|
+
if (await ctx.prompter.confirm(t("artifacts.confirmOne", { path: change.path }), true)) {
|
|
31
|
+
accepted.push(change);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return accepted;
|
|
35
|
+
}
|
|
36
|
+
export async function applyChanges(cwd, changes) {
|
|
37
|
+
for (const change of changes) {
|
|
38
|
+
const path = join(cwd, ...change.path.split("/"));
|
|
39
|
+
if (change.kind === "delete")
|
|
40
|
+
await rm(path, { force: true });
|
|
41
|
+
else
|
|
42
|
+
await writeText(path, change.after);
|
|
43
|
+
}
|
|
44
|
+
}
|