@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import uniq from 'lodash/uniq'
|
|
6
|
+
|
|
7
|
+
import { refsMadeInTurn, type GitState } from './after-turn-check'
|
|
8
|
+
import { git, gitOk } from './worktree-git'
|
|
9
|
+
import type { SavedGit, TurnSnapshot } from './worktree-snapshot'
|
|
10
|
+
|
|
11
|
+
import { FROZEN_INSTALL } from '../gates/lockfile-install'
|
|
12
|
+
import { runShell } from '../shell/run-command'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Takes a ref an agent made off, keeping its commit under `refs/luca-undone/`
|
|
16
|
+
* so nothing is lost: the refs are shared, and the engine can't tell who
|
|
17
|
+
* made one. A ref that existed before goes back to where it was.
|
|
18
|
+
*/
|
|
19
|
+
const undoRef = async ({
|
|
20
|
+
cwd,
|
|
21
|
+
ref,
|
|
22
|
+
before,
|
|
23
|
+
}: {
|
|
24
|
+
cwd: string
|
|
25
|
+
ref: string
|
|
26
|
+
before: string | undefined
|
|
27
|
+
}): Promise<void> => {
|
|
28
|
+
const now = await git({ cwd, args: ['rev-parse', '--verify', '-q', ref] })
|
|
29
|
+
if (!now.ok) return
|
|
30
|
+
const object = now.stdout.trim()
|
|
31
|
+
if (before !== undefined) {
|
|
32
|
+
await gitOk({ cwd, args: ['update-ref', ref, before, object] })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const kept = ref.replace(/^refs\//, 'refs/luca-undone/')
|
|
36
|
+
await gitOk({ cwd, args: ['update-ref', kept, object] })
|
|
37
|
+
await gitOk({ cwd, args: ['update-ref', '-d', ref, object] })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drops the stash entries an agent made. The stash is shared, so each is
|
|
42
|
+
* found by its commit just before it is dropped, newest first.
|
|
43
|
+
*/
|
|
44
|
+
const dropStashes = async ({
|
|
45
|
+
cwd,
|
|
46
|
+
entries,
|
|
47
|
+
}: {
|
|
48
|
+
cwd: string
|
|
49
|
+
entries: string[]
|
|
50
|
+
}): Promise<void> => {
|
|
51
|
+
const shas = entries.map((entry) => entry.split(' ')[0] ?? '')
|
|
52
|
+
for (const sha of shas) {
|
|
53
|
+
const list = await git({ cwd, args: ['stash', 'list', '--format=%H'] })
|
|
54
|
+
const index = list.stdout.split('\n').indexOf(sha)
|
|
55
|
+
if (index !== -1) {
|
|
56
|
+
await gitOk({
|
|
57
|
+
cwd,
|
|
58
|
+
args: ['stash', 'drop', '-q', `stash@{${index}}`],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether HEAD is now a commit made during the turn: it moved, to a commit
|
|
66
|
+
* no ref named before the turn and that the old HEAD doesn't contain.
|
|
67
|
+
*/
|
|
68
|
+
export const headIsNew = async ({
|
|
69
|
+
cwd,
|
|
70
|
+
before,
|
|
71
|
+
after,
|
|
72
|
+
saved,
|
|
73
|
+
}: {
|
|
74
|
+
cwd: string
|
|
75
|
+
before: GitState
|
|
76
|
+
after: GitState
|
|
77
|
+
saved: SavedGit
|
|
78
|
+
}): Promise<boolean> => {
|
|
79
|
+
if (after.head === before.head || after.head === '') return false
|
|
80
|
+
if (Object.values(saved.refs).includes(after.head)) return false
|
|
81
|
+
const older = await git({
|
|
82
|
+
cwd,
|
|
83
|
+
args: ['merge-base', '--is-ancestor', after.head, before.head],
|
|
84
|
+
})
|
|
85
|
+
return !older.ok
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Puts HEAD, the branch, the index, the refs the agent made, and stashes
|
|
90
|
+
* back; the files stay.
|
|
91
|
+
*/
|
|
92
|
+
export const restoreGit = async ({
|
|
93
|
+
cwd,
|
|
94
|
+
branch,
|
|
95
|
+
before,
|
|
96
|
+
after,
|
|
97
|
+
saved,
|
|
98
|
+
head_is_new,
|
|
99
|
+
}: {
|
|
100
|
+
cwd: string
|
|
101
|
+
branch: string
|
|
102
|
+
before: GitState
|
|
103
|
+
after: GitState
|
|
104
|
+
saved: SavedGit
|
|
105
|
+
head_is_new: boolean
|
|
106
|
+
}): Promise<void> => {
|
|
107
|
+
if (before.head_ref !== null) {
|
|
108
|
+
await gitOk({ cwd, args: ['symbolic-ref', 'HEAD', before.head_ref] })
|
|
109
|
+
}
|
|
110
|
+
// A mixed reset: HEAD and the branch go back, the index is emptied of
|
|
111
|
+
// the agent's staging, and the files in the worktree are left alone.
|
|
112
|
+
await gitOk({ cwd, args: ['reset', '-q', before.head] })
|
|
113
|
+
if (before.branch_ref !== null) {
|
|
114
|
+
await gitOk({
|
|
115
|
+
cwd,
|
|
116
|
+
args: ['update-ref', `refs/heads/${branch}`, before.branch_ref],
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
for (const ref of refsMadeInTurn({ before, after, head_is_new })) {
|
|
120
|
+
await undoRef({ cwd, ref, before: saved.refs[ref] })
|
|
121
|
+
}
|
|
122
|
+
await dropStashes({
|
|
123
|
+
cwd,
|
|
124
|
+
entries: after.stash.filter((entry) => !before.stash.includes(entry)),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Puts one path back how it was before the turn. */
|
|
129
|
+
export const restorePath = async ({
|
|
130
|
+
cwd,
|
|
131
|
+
path,
|
|
132
|
+
before,
|
|
133
|
+
}: {
|
|
134
|
+
cwd: string
|
|
135
|
+
path: string
|
|
136
|
+
before: TurnSnapshot
|
|
137
|
+
}): Promise<void> => {
|
|
138
|
+
const full = join(cwd, path)
|
|
139
|
+
const bytes = before.saved[path]
|
|
140
|
+
if (bytes !== undefined && bytes !== null) {
|
|
141
|
+
await mkdir(dirname(full), { recursive: true })
|
|
142
|
+
await rm(full, { force: true, recursive: true })
|
|
143
|
+
await Bun.write(full, bytes)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (bytes === undefined) {
|
|
147
|
+
const tracked = await git({
|
|
148
|
+
cwd,
|
|
149
|
+
args: ['cat-file', '-e', `${before.git.head}:${path}`],
|
|
150
|
+
})
|
|
151
|
+
if (tracked.ok) {
|
|
152
|
+
await rm(full, { force: true, recursive: true })
|
|
153
|
+
await gitOk({
|
|
154
|
+
cwd,
|
|
155
|
+
args: ['checkout', before.git.head, '--', path],
|
|
156
|
+
})
|
|
157
|
+
await gitOk({ cwd, args: ['reset', '-q', '--', path] })
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await rm(full, { force: true, recursive: true })
|
|
162
|
+
await pruneEmptyFolders({ cwd, path })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Removes the folders a new path left empty, up to the worktree. */
|
|
166
|
+
const pruneEmptyFolders = async ({
|
|
167
|
+
cwd,
|
|
168
|
+
path,
|
|
169
|
+
}: {
|
|
170
|
+
cwd: string
|
|
171
|
+
path: string
|
|
172
|
+
}): Promise<void> => {
|
|
173
|
+
for (let folder = dirname(path); folder !== '.'; folder = dirname(folder)) {
|
|
174
|
+
const full = join(cwd, folder)
|
|
175
|
+
if (!existsSync(full) || readdirSync(full).length > 0) return
|
|
176
|
+
await rm(full, { recursive: true, force: true })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The package folder a path under `node_modules` belongs to, such as
|
|
182
|
+
* `node_modules/@scope/name` for `node_modules/@scope/name/lib/a.js`.
|
|
183
|
+
*/
|
|
184
|
+
const packageFolder = (path: string): string => {
|
|
185
|
+
const parts = path.split('/')
|
|
186
|
+
const at = parts.lastIndexOf('node_modules')
|
|
187
|
+
const scoped = parts[at + 1]?.startsWith('@') === true
|
|
188
|
+
return parts.slice(0, at + (scoped ? 3 : 2)).join('/')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Puts back files an agent changed or deleted under `node_modules`. Their
|
|
193
|
+
* bytes are not kept (there are too many), so each package they belong to
|
|
194
|
+
* is removed and the engine's own install puts it back from the lockfile.
|
|
195
|
+
*
|
|
196
|
+
* @returns Why the install failed, or `null`.
|
|
197
|
+
*/
|
|
198
|
+
export const reinstallPackages = async ({
|
|
199
|
+
cwd,
|
|
200
|
+
paths,
|
|
201
|
+
}: {
|
|
202
|
+
cwd: string
|
|
203
|
+
paths: string[]
|
|
204
|
+
}): Promise<string | null> => {
|
|
205
|
+
for (const folder of uniq(paths.map(packageFolder))) {
|
|
206
|
+
await rm(join(cwd, folder), { force: true, recursive: true })
|
|
207
|
+
}
|
|
208
|
+
const result = await runShell({ command: FROZEN_INSTALL, cwd })
|
|
209
|
+
return result.exit_code === 0
|
|
210
|
+
? null
|
|
211
|
+
: `the engine's install (${FROZEN_INSTALL}) failed, so node_modules may still hold what the agent changed:\n${result.stderr || result.stdout}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Puts the ticket branch's own config section back, key by key: each of its
|
|
216
|
+
* keys now there is unset, then each saved entry is added back in order.
|
|
217
|
+
* Every other key in the shared `.git/config` stays as it is now, since
|
|
218
|
+
* other processes write there too. A key with no value comes back as
|
|
219
|
+
* `true`, which git reads the same.
|
|
220
|
+
*
|
|
221
|
+
* @returns Why the undo failed (such as another process holding the
|
|
222
|
+
* config's lock), or `null`.
|
|
223
|
+
*/
|
|
224
|
+
export const restoreBranchConfig = async ({
|
|
225
|
+
cwd,
|
|
226
|
+
before,
|
|
227
|
+
after,
|
|
228
|
+
common_dir,
|
|
229
|
+
}: {
|
|
230
|
+
cwd: string
|
|
231
|
+
before: GitState
|
|
232
|
+
after: GitState
|
|
233
|
+
common_dir: string
|
|
234
|
+
}): Promise<string | null> => {
|
|
235
|
+
const file = join(common_dir, 'config')
|
|
236
|
+
const steps = [
|
|
237
|
+
...uniq(after.branch_config.map(({ key }) => key)).map((key) => [
|
|
238
|
+
'config',
|
|
239
|
+
'--file',
|
|
240
|
+
file,
|
|
241
|
+
'--unset-all',
|
|
242
|
+
key,
|
|
243
|
+
]),
|
|
244
|
+
...before.branch_config.map(({ key, value }) => [
|
|
245
|
+
'config',
|
|
246
|
+
'--file',
|
|
247
|
+
file,
|
|
248
|
+
'--add',
|
|
249
|
+
key,
|
|
250
|
+
value ?? 'true',
|
|
251
|
+
]),
|
|
252
|
+
]
|
|
253
|
+
for (const args of steps) {
|
|
254
|
+
const result = await git({ cwd, args })
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
return `git ${args.join(' ')} failed, so the ticket branch's settings in the shared .git/config may still hold what the agent changed`
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure: which worktree paths each role may create, change, or delete.
|
|
3
|
+
*/
|
|
4
|
+
import { basename, isAbsolute, normalize, relative, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { isWriter, type GuardArgs } from './guard-roles'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
featureMapPath,
|
|
10
|
+
isFeatureMapFolder,
|
|
11
|
+
type EngineConfig,
|
|
12
|
+
} from '../config/engine-config'
|
|
13
|
+
import { testFilesAmong } from '../gates/test-runner'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Worktree-relative, `..` resolved. `null` when the path leaves the worktree.
|
|
17
|
+
* macOS reaches temp folders through `/var` and `/private/var` alike, so both
|
|
18
|
+
* sides drop the `/private` prefix before they are compared.
|
|
19
|
+
*/
|
|
20
|
+
export const insideWorktree = ({
|
|
21
|
+
worktree,
|
|
22
|
+
path,
|
|
23
|
+
}: {
|
|
24
|
+
worktree: string
|
|
25
|
+
path: string
|
|
26
|
+
}): string | null => {
|
|
27
|
+
const canonical = (text: string) =>
|
|
28
|
+
text.replace(/^\/private\/(var|tmp)(?=\/|$)/, '/$1')
|
|
29
|
+
const root = canonical(resolve(worktree))
|
|
30
|
+
const full = canonical(resolve(root, path))
|
|
31
|
+
const inner = relative(root, full)
|
|
32
|
+
if (inner.startsWith('..') || isAbsolute(inner)) return null
|
|
33
|
+
return inner === '' ? '.' : inner
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isTestFile = ({
|
|
37
|
+
path,
|
|
38
|
+
config,
|
|
39
|
+
}: {
|
|
40
|
+
path: string
|
|
41
|
+
config: EngineConfig
|
|
42
|
+
}): boolean =>
|
|
43
|
+
testFilesAmong({
|
|
44
|
+
files: [path],
|
|
45
|
+
test_file_patterns: config.test_file_patterns,
|
|
46
|
+
}).length > 0
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a clean worktree-relative path is part of the repo's feature map
|
|
50
|
+
* (#583): the map file itself, or, when the map is a folder (#597), any
|
|
51
|
+
* `*.md` page under it.
|
|
52
|
+
*/
|
|
53
|
+
const isFeatureMapPage = ({
|
|
54
|
+
path,
|
|
55
|
+
config,
|
|
56
|
+
}: {
|
|
57
|
+
path: string
|
|
58
|
+
config: EngineConfig
|
|
59
|
+
}): boolean => {
|
|
60
|
+
const map = featureMapPath({ config })
|
|
61
|
+
if (map === null) return false
|
|
62
|
+
return isFeatureMapFolder({ path: map })
|
|
63
|
+
? path.startsWith(`${map}/`) && path.endsWith('.md')
|
|
64
|
+
: path === map
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a clean worktree-relative path matches the repo's
|
|
69
|
+
* `test_helper_patterns` (#619): a non-test file the test-writer may write.
|
|
70
|
+
*/
|
|
71
|
+
export const isTestHelper = ({
|
|
72
|
+
path,
|
|
73
|
+
config,
|
|
74
|
+
}: {
|
|
75
|
+
path: string
|
|
76
|
+
config: EngineConfig
|
|
77
|
+
}): boolean =>
|
|
78
|
+
(config.test_helper_patterns ?? []).some((pattern) =>
|
|
79
|
+
new Bun.Glob(pattern).match(path)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
/** The lockfiles a package install writes. The engine owns them. */
|
|
83
|
+
export const LOCKFILES = [
|
|
84
|
+
'bun.lock',
|
|
85
|
+
'bun.lockb',
|
|
86
|
+
'package-lock.json',
|
|
87
|
+
'npm-shrinkwrap.json',
|
|
88
|
+
'yarn.lock',
|
|
89
|
+
'pnpm-lock.yaml',
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether a worktree-relative path is the package install's: a lockfile, or
|
|
94
|
+
* anything under a `node_modules` folder. Only the engine's install gate
|
|
95
|
+
* writes these; no agent does.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* isInstallPath('bun.lock') // true
|
|
99
|
+
* isInstallPath('packages/app/node_modules/zod/index.js') // true
|
|
100
|
+
* isInstallPath('src/sum.ts') // false
|
|
101
|
+
*/
|
|
102
|
+
export const isInstallPath = (path: string): boolean =>
|
|
103
|
+
LOCKFILES.includes(basename(path)) ||
|
|
104
|
+
normalize(path).split('/').includes('node_modules')
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether an agent may create, change, or delete a worktree-relative path.
|
|
108
|
+
* Test-writers write test files only, the feature map when the repo has
|
|
109
|
+
* one (#583), or any page of a folder map (#597), and files matching
|
|
110
|
+
* `test_helper_patterns` (#619); implementers write anything but test
|
|
111
|
+
* files (unless they may edit tests) and test setup files; reviewers and the
|
|
112
|
+
* learner write nothing. No role writes a test setup file, `.git`, what
|
|
113
|
+
* the package install writes (lockfiles, `node_modules`), or outside its
|
|
114
|
+
* worktree.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* mayWrite({ role: 'test-writer', may_edit_tests: true, path: 'src/sum.test.ts', config }) // true
|
|
118
|
+
* mayWrite({ role: 'implementer', may_edit_tests: false, path: 'src/sum.test.ts', config }) // false
|
|
119
|
+
* mayWrite({ role: 'implementer', may_edit_tests: true, path: 'src/sum.test.ts', config }) // true: a refactor ticket
|
|
120
|
+
*/
|
|
121
|
+
export const mayWrite = ({
|
|
122
|
+
role,
|
|
123
|
+
may_edit_tests,
|
|
124
|
+
path,
|
|
125
|
+
config,
|
|
126
|
+
}: GuardArgs & {
|
|
127
|
+
path: string
|
|
128
|
+
config: EngineConfig
|
|
129
|
+
}): boolean => {
|
|
130
|
+
if (!isWriter(role) || isAbsolute(path)) return false
|
|
131
|
+
const clean = normalize(path).replace(/^\.\//, '')
|
|
132
|
+
if (clean === '.' || clean.startsWith('..')) return false
|
|
133
|
+
if (clean === '.git' || clean.startsWith('.git/')) return false
|
|
134
|
+
if (config.test_setup_files.includes(clean)) return false
|
|
135
|
+
if (isInstallPath(clean)) return false
|
|
136
|
+
const test = isTestFile({ path: clean, config })
|
|
137
|
+
// The test-writer may also write the repo's feature map, when it has one
|
|
138
|
+
// (#583): the file, or any page of a folder map (#597); and its test
|
|
139
|
+
// helpers (#619).
|
|
140
|
+
if (role === 'test-writer') {
|
|
141
|
+
return (
|
|
142
|
+
may_edit_tests &&
|
|
143
|
+
(test ||
|
|
144
|
+
isFeatureMapPage({ path: clean, config }) ||
|
|
145
|
+
isTestHelper({ path: clean, config }))
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
return !test || may_edit_tests
|
|
149
|
+
}
|
package/engine/index.ts
CHANGED
|
@@ -20,8 +20,10 @@ export {
|
|
|
20
20
|
agentOptions,
|
|
21
21
|
BANNED_ENV,
|
|
22
22
|
CLAUDE_MODEL,
|
|
23
|
+
checkLaunchModel,
|
|
23
24
|
checkModel,
|
|
24
25
|
MAX_TURNS,
|
|
26
|
+
MODEL_NAMES,
|
|
25
27
|
resultJsonSchema,
|
|
26
28
|
} from './agents/claude-options'
|
|
27
29
|
export {
|
|
@@ -60,9 +62,13 @@ export {
|
|
|
60
62
|
} from './board/paseo-board-link'
|
|
61
63
|
export {
|
|
62
64
|
bunTestCommands,
|
|
65
|
+
DecisionModelConfigSchema,
|
|
66
|
+
decisionModelOf,
|
|
63
67
|
ENGINE_CONFIG_FILE,
|
|
64
68
|
EngineConfigSchema,
|
|
69
|
+
hasSlowTests,
|
|
65
70
|
loadEngineConfig,
|
|
71
|
+
slowTestCommands,
|
|
66
72
|
testCommands,
|
|
67
73
|
TestResultsSchema,
|
|
68
74
|
type EngineConfig,
|
|
@@ -128,6 +134,7 @@ export {
|
|
|
128
134
|
shipFinalReview,
|
|
129
135
|
} from './core/execute-final-review'
|
|
130
136
|
export { finalFixerPrompt, lensPrompt } from './core/final-review-text'
|
|
137
|
+
export { evidenceSection } from './core/evidence-text'
|
|
131
138
|
export { pullRequestText } from './core/pull-request-text'
|
|
132
139
|
export {
|
|
133
140
|
DEFAULT_MAX_STEPS,
|
|
@@ -192,27 +199,41 @@ export {
|
|
|
192
199
|
} from './intake/intake-checks'
|
|
193
200
|
export * from './intake/intake-schemas'
|
|
194
201
|
export {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
type
|
|
199
|
-
type
|
|
200
|
-
type
|
|
201
|
-
} from './jev/
|
|
202
|
+
clefUrl,
|
|
203
|
+
createClefClient,
|
|
204
|
+
type ClefCredentials,
|
|
205
|
+
type DecisionModelClient,
|
|
206
|
+
type DecisionModelFetch,
|
|
207
|
+
type DecisionModelReply,
|
|
208
|
+
} from './jev/clef-client'
|
|
209
|
+
export {
|
|
210
|
+
CLOUDFLARE_ACCOUNT_ID,
|
|
211
|
+
CLOUDFLARE_API_TOKEN,
|
|
212
|
+
CLOUDFLARE_AUTH_TOKEN,
|
|
213
|
+
DECISION_MODEL_KEYS,
|
|
214
|
+
describeDecisionModelCredentials,
|
|
215
|
+
loadDecisionModelCredentials,
|
|
216
|
+
lucaEnvFile,
|
|
217
|
+
missingKeyLines,
|
|
218
|
+
noCredentialsText,
|
|
219
|
+
parseEnvFile,
|
|
220
|
+
type DecisionModelCredentials,
|
|
221
|
+
type DecisionModelKey,
|
|
222
|
+
} from './jev/decision-model-credentials'
|
|
202
223
|
export {
|
|
203
224
|
JEV_CANDIDATE_SKILLS,
|
|
204
225
|
JEV_FAILURE_TEXT_CHARS,
|
|
205
226
|
JEV_FIXED_MODEL,
|
|
206
|
-
JEV_MODEL_OPTIONS,
|
|
207
227
|
jevAsksAfter,
|
|
208
228
|
jevAsksBefore,
|
|
209
229
|
type JevAsk,
|
|
210
230
|
} from './jev/jev-jobs'
|
|
211
231
|
export * from './jev/jev-schemas'
|
|
212
232
|
export {
|
|
213
|
-
askJevInShadow,
|
|
214
233
|
DEFAULT_JEV_TIMEOUT_MS,
|
|
234
|
+
shadowAsker,
|
|
215
235
|
type JevShadow,
|
|
236
|
+
type ShadowAsker,
|
|
216
237
|
} from './jev/jev-shadow'
|
|
217
238
|
export {
|
|
218
239
|
createJournal,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import uniq from 'lodash/uniq'
|
|
2
|
+
|
|
3
|
+
/** The fix intake asks for when a criterion leaves text to guess. */
|
|
4
|
+
export const PIN_TEXT = 'pin the exact text'
|
|
5
|
+
|
|
6
|
+
/** The fix intake asks for when no agent can test a criterion. */
|
|
7
|
+
export const UNTESTABLE_TEXT =
|
|
8
|
+
'no agent can check this; move it out of the acceptance criteria'
|
|
9
|
+
|
|
10
|
+
/** The fix intake asks for when a criterion describes what stays as it is. */
|
|
11
|
+
export const UNCHANGED_TEXT =
|
|
12
|
+
"describes behaviour that doesn't change, so no test can fail first; move it out of the acceptance criteria (for example into the ticket's notes)"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Text that leaves an agent to guess, caught anywhere in a criterion, inside
|
|
16
|
+
* quotes and code spans too (#541).
|
|
17
|
+
*/
|
|
18
|
+
const PLACEHOLDERS: RegExp[] = [
|
|
19
|
+
/<[^<>\n]+>/g,
|
|
20
|
+
/\be\.g\./gi,
|
|
21
|
+
/\betc\./gi,
|
|
22
|
+
/\bTBD\b/g,
|
|
23
|
+
/\bTODO\b/g,
|
|
24
|
+
/\.\.\.|…/g,
|
|
25
|
+
/(?<![\w'-])[NMXY](?![\w'-])/g,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** Asks no agent can test, caught outside code spans only (#541). */
|
|
29
|
+
const UNTESTABLE_ASKS: RegExp[] = [
|
|
30
|
+
/\bscreenshots?\b/gi,
|
|
31
|
+
/\bchangesets?\b/gi,
|
|
32
|
+
/\bADR[\s#-]*\d+/g,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Phrases for behaviour that doesn't change, caught outside code spans only
|
|
37
|
+
* (#653): no test can fail first on them.
|
|
38
|
+
*/
|
|
39
|
+
const UNCHANGED_PHRASES: RegExp[] = [
|
|
40
|
+
/\bunchanged\b/gi,
|
|
41
|
+
/\bstays? the same\b/gi,
|
|
42
|
+
/\bas (?:today|before)\b/gi,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** Inline code spans: a run of backticks up to the same run. */
|
|
46
|
+
const CODE_SPAN = /(`+)[\s\S]*?\1/g
|
|
47
|
+
|
|
48
|
+
/** Every match of `patterns` in `text`, in pattern order, once each. */
|
|
49
|
+
const found = ({
|
|
50
|
+
text,
|
|
51
|
+
patterns,
|
|
52
|
+
}: {
|
|
53
|
+
text: string
|
|
54
|
+
patterns: RegExp[]
|
|
55
|
+
}): string[] =>
|
|
56
|
+
uniq(
|
|
57
|
+
patterns.flatMap((pattern) =>
|
|
58
|
+
[...text.matchAll(pattern)].map(([match]) => match)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/** `text` as a markdown code span, fenced past any backticks inside it. */
|
|
63
|
+
const asCode = (text: string): string => {
|
|
64
|
+
const longest = Math.max(
|
|
65
|
+
0,
|
|
66
|
+
...[...text.matchAll(/`+/g)].map(([run]) => run.length)
|
|
67
|
+
)
|
|
68
|
+
const fence = '`'.repeat(longest + 1)
|
|
69
|
+
return longest === 0
|
|
70
|
+
? `${fence}${text}${fence}`
|
|
71
|
+
: `${fence} ${text} ${fence}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Intake's problems with one ticket's acceptance criteria: a placeholder
|
|
76
|
+
* anywhere in a criterion, an untestable ask (a screenshot, a changeset,
|
|
77
|
+
* an ADR number) outside its code spans, or, outside its code spans, words
|
|
78
|
+
* for behaviour that doesn't change ("unchanged", "stays the same", "as
|
|
79
|
+
* today", "as before"), once per criterion. Each problem names the ticket,
|
|
80
|
+
* quotes the criterion, and says what to do. Pure. Empty when all are clean.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* criteriaProblems({ ticket: 12, criteria: [{ id: 'AC1', text: 'Retries N times' }] })
|
|
84
|
+
* // ['Acceptance criterion AC1 of #12 leaves text to guess (`N`): pin the exact text. It reads: `Retries N times`']
|
|
85
|
+
*/
|
|
86
|
+
export const criteriaProblems = ({
|
|
87
|
+
ticket,
|
|
88
|
+
criteria,
|
|
89
|
+
}: {
|
|
90
|
+
ticket: number
|
|
91
|
+
criteria: { id: string; text: string }[]
|
|
92
|
+
}): string[] =>
|
|
93
|
+
criteria.flatMap(({ id, text }) => {
|
|
94
|
+
const named = `Acceptance criterion ${id} of #${ticket}`
|
|
95
|
+
const quoted = `It reads: ${asCode(text)}`
|
|
96
|
+
const placeholders = found({ text, patterns: PLACEHOLDERS })
|
|
97
|
+
const prose = text.replace(CODE_SPAN, ' ')
|
|
98
|
+
const asks = found({ text: prose, patterns: UNTESTABLE_ASKS })
|
|
99
|
+
const unchanged =
|
|
100
|
+
found({ text: prose, patterns: UNCHANGED_PHRASES }).length > 0
|
|
101
|
+
return [
|
|
102
|
+
...(placeholders.length === 0
|
|
103
|
+
? []
|
|
104
|
+
: [
|
|
105
|
+
`${named} leaves text to guess (${placeholders.map(asCode).join(', ')}): ${PIN_TEXT}. ${quoted}`,
|
|
106
|
+
]),
|
|
107
|
+
...(asks.length === 0
|
|
108
|
+
? []
|
|
109
|
+
: [
|
|
110
|
+
`${named} asks for ${asks.map(asCode).join(', ')}: ${UNTESTABLE_TEXT}. ${quoted}`,
|
|
111
|
+
]),
|
|
112
|
+
...(unchanged ? [`${named} ${UNCHANGED_TEXT}. ${quoted}`] : []),
|
|
113
|
+
]
|
|
114
|
+
})
|