@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,412 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readlinkSync,
|
|
6
|
+
type Stats,
|
|
7
|
+
} from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import sortBy from 'lodash/sortBy'
|
|
11
|
+
import uniq from 'lodash/uniq'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DELETED,
|
|
15
|
+
watchesIgnored,
|
|
16
|
+
type ConfigEntry,
|
|
17
|
+
type GitState,
|
|
18
|
+
type OutsideGitState,
|
|
19
|
+
type WorktreeState,
|
|
20
|
+
} from './after-turn-check'
|
|
21
|
+
import {
|
|
22
|
+
commonDirOf,
|
|
23
|
+
excludeArgs,
|
|
24
|
+
excludeRules,
|
|
25
|
+
git,
|
|
26
|
+
gitOk,
|
|
27
|
+
globalExcludesFile,
|
|
28
|
+
nulSplit,
|
|
29
|
+
withExcludeFile,
|
|
30
|
+
} from './worktree-git'
|
|
31
|
+
|
|
32
|
+
import type { EngineConfig } from '../config/engine-config'
|
|
33
|
+
|
|
34
|
+
/** What the check and the undo of git state need beyond the comparison. */
|
|
35
|
+
export type SavedGit = {
|
|
36
|
+
/** The repo's shared `.git` folder. */
|
|
37
|
+
common_dir: string
|
|
38
|
+
/** Every branch and tag, and the commit or tag object it names. */
|
|
39
|
+
refs: Record<string, string>
|
|
40
|
+
/**
|
|
41
|
+
* The ignore rules from before the turn: the global excludes file, then
|
|
42
|
+
* the shared `.git/info/exclude`. The check lists files with these, not
|
|
43
|
+
* the live ones, so an excludes entry added mid-turn hides nothing.
|
|
44
|
+
*/
|
|
45
|
+
exclude_rules: Uint8Array
|
|
46
|
+
/** The rest of the shared `.git`, to tell what others changed. */
|
|
47
|
+
outside: OutsideGitState
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A snapshot taken before an agent turn: what the after-turn check compares
|
|
52
|
+
* against, plus what it needs to undo a violation exactly, even on a
|
|
53
|
+
* worktree that was not clean.
|
|
54
|
+
*/
|
|
55
|
+
export type TurnSnapshot = WorktreeState & {
|
|
56
|
+
/**
|
|
57
|
+
* Bytes of each path in `files` (`null` if it was deleted, or is under
|
|
58
|
+
* `node_modules`, which the engine's install puts back instead).
|
|
59
|
+
*/
|
|
60
|
+
saved: Record<string, Uint8Array | null>
|
|
61
|
+
saved_git: SavedGit
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every changed path: tracked and staged changes (renamed-from paths
|
|
66
|
+
* included), and untracked files the snapshot's rules don't ignore.
|
|
67
|
+
*/
|
|
68
|
+
const changedPaths = async ({
|
|
69
|
+
cwd,
|
|
70
|
+
exclude_file,
|
|
71
|
+
}: {
|
|
72
|
+
cwd: string
|
|
73
|
+
exclude_file: string
|
|
74
|
+
}): Promise<string[]> => {
|
|
75
|
+
const [status, others] = await Promise.all([
|
|
76
|
+
gitOk({
|
|
77
|
+
cwd,
|
|
78
|
+
args: ['status', '--porcelain=v1', '-z', '--untracked-files=no'],
|
|
79
|
+
}),
|
|
80
|
+
gitOk({
|
|
81
|
+
cwd,
|
|
82
|
+
args: ['ls-files', '--others', '-z', ...excludeArgs(exclude_file)],
|
|
83
|
+
}),
|
|
84
|
+
])
|
|
85
|
+
const parts = nulSplit(status)
|
|
86
|
+
const paths: string[] = []
|
|
87
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
88
|
+
const part = parts[index] ?? ''
|
|
89
|
+
paths.push(part.slice(3))
|
|
90
|
+
if (part[0] === 'R' || part[0] === 'C') {
|
|
91
|
+
index += 1
|
|
92
|
+
paths.push(parts[index] ?? '')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...paths, ...nulSplit(others)]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The ignored paths the after-turn check watches (see `watchesIgnored`). */
|
|
99
|
+
const watchedIgnoredPaths = async ({
|
|
100
|
+
cwd,
|
|
101
|
+
config,
|
|
102
|
+
exclude_file,
|
|
103
|
+
}: {
|
|
104
|
+
cwd: string
|
|
105
|
+
config: EngineConfig
|
|
106
|
+
exclude_file: string
|
|
107
|
+
}): Promise<string[]> =>
|
|
108
|
+
nulSplit(
|
|
109
|
+
await gitOk({
|
|
110
|
+
cwd,
|
|
111
|
+
args: [
|
|
112
|
+
'ls-files',
|
|
113
|
+
'--others',
|
|
114
|
+
'--ignored',
|
|
115
|
+
...excludeArgs(exclude_file),
|
|
116
|
+
'-z',
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
).filter((path) => watchesIgnored({ path, config }))
|
|
120
|
+
|
|
121
|
+
/** Whether a worktree-relative path is under a `node_modules` folder. */
|
|
122
|
+
export const inPackages = (path: string): boolean =>
|
|
123
|
+
path.split('/').includes('node_modules')
|
|
124
|
+
|
|
125
|
+
const hashBytes = (bytes: Uint8Array): string => Bun.hash(bytes).toString(16)
|
|
126
|
+
|
|
127
|
+
/** `lstat`, or `null` when nothing is there. A dangling link still is. */
|
|
128
|
+
const lstatOrNull = (full: string): Stats | null => {
|
|
129
|
+
try {
|
|
130
|
+
return lstatSync(full)
|
|
131
|
+
} catch {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A path's fingerprint: a file's content hash, a link's target, or
|
|
138
|
+
* `DELETED`. Under `node_modules`, where hashing every file each turn is
|
|
139
|
+
* slow, a file gets its size, inode, and change time instead: writing a
|
|
140
|
+
* file always moves its change time, and no process can set it back.
|
|
141
|
+
*/
|
|
142
|
+
const hashPath = async ({
|
|
143
|
+
cwd,
|
|
144
|
+
path,
|
|
145
|
+
}: {
|
|
146
|
+
cwd: string
|
|
147
|
+
path: string
|
|
148
|
+
}): Promise<string> => {
|
|
149
|
+
const full = join(cwd, path)
|
|
150
|
+
const stat = lstatOrNull(full)
|
|
151
|
+
if (stat === null) return DELETED
|
|
152
|
+
if (stat.isSymbolicLink()) return `link:${readlinkSync(full)}`
|
|
153
|
+
if (stat.isDirectory()) return 'dir'
|
|
154
|
+
if (inPackages(path)) {
|
|
155
|
+
const big = lstatSync(full, { bigint: true })
|
|
156
|
+
return `stat:${big.size}:${big.ino}:${big.ctimeNs}:${big.mode}`
|
|
157
|
+
}
|
|
158
|
+
return hashBytes(await Bun.file(full).bytes())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Name, mode, and content hash of each `.git/hooks` entry. */
|
|
162
|
+
const hooksState = async ({
|
|
163
|
+
common_dir,
|
|
164
|
+
}: {
|
|
165
|
+
common_dir: string
|
|
166
|
+
}): Promise<string[]> => {
|
|
167
|
+
const dir = join(common_dir, 'hooks')
|
|
168
|
+
if (!existsSync(dir)) return ['missing']
|
|
169
|
+
const hooks: string[] = []
|
|
170
|
+
for (const name of sortBy(readdirSync(dir))) {
|
|
171
|
+
const stat = lstatSync(join(dir, name))
|
|
172
|
+
hooks.push(
|
|
173
|
+
`${name} ${stat.mode.toString(8)} ${await hashPath({ cwd: dir, path: name })}`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
return hooks
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Every entry of the shared `.git/config`, in file order, includes not followed. */
|
|
180
|
+
const configEntries = async ({
|
|
181
|
+
cwd,
|
|
182
|
+
common_dir,
|
|
183
|
+
}: {
|
|
184
|
+
cwd: string
|
|
185
|
+
common_dir: string
|
|
186
|
+
}): Promise<ConfigEntry[]> =>
|
|
187
|
+
nulSplit(
|
|
188
|
+
await gitOk({
|
|
189
|
+
cwd,
|
|
190
|
+
args: [
|
|
191
|
+
'config',
|
|
192
|
+
'--file',
|
|
193
|
+
join(common_dir, 'config'),
|
|
194
|
+
'--list',
|
|
195
|
+
'-z',
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
).map((entry) => {
|
|
199
|
+
const at = entry.indexOf('\n')
|
|
200
|
+
return at === -1
|
|
201
|
+
? { key: entry, value: null }
|
|
202
|
+
: { key: entry.slice(0, at), value: entry.slice(at + 1) }
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether a config key is in the ticket branch's own section,
|
|
207
|
+
* `branch.<branch>.<name>`. The branch name is matched exactly, and the
|
|
208
|
+
* name after it has no dots, so `branch.a.b.remote` is branch `a.b`'s.
|
|
209
|
+
*/
|
|
210
|
+
const isOwnKey = ({ key, branch }: { key: string; branch: string }) => {
|
|
211
|
+
const prefix = `branch.${branch}.`
|
|
212
|
+
return key.startsWith(prefix) && !key.slice(prefix.length).includes('.')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The shared `.git` as the check sees it: the branch's section, and the rest. */
|
|
216
|
+
const sharedGitState = async ({
|
|
217
|
+
cwd,
|
|
218
|
+
branch,
|
|
219
|
+
common_dir,
|
|
220
|
+
}: {
|
|
221
|
+
cwd: string
|
|
222
|
+
branch: string
|
|
223
|
+
common_dir: string
|
|
224
|
+
}): Promise<{ branch_config: ConfigEntry[]; outside: OutsideGitState }> => {
|
|
225
|
+
const [entries, exclude, hooks] = await Promise.all([
|
|
226
|
+
configEntries({ cwd, common_dir }),
|
|
227
|
+
existsSync(join(common_dir, 'info/exclude'))
|
|
228
|
+
? hashPath({ cwd: common_dir, path: 'info/exclude' })
|
|
229
|
+
: DELETED,
|
|
230
|
+
hooksState({ common_dir }),
|
|
231
|
+
])
|
|
232
|
+
return {
|
|
233
|
+
branch_config: entries.filter(({ key }) => isOwnKey({ key, branch })),
|
|
234
|
+
outside: {
|
|
235
|
+
config: entries
|
|
236
|
+
.filter(({ key }) => !isOwnKey({ key, branch }))
|
|
237
|
+
.map((entry) => JSON.stringify(entry)),
|
|
238
|
+
exclude,
|
|
239
|
+
hooks,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Refs this ticket's worktree is not the one to judge: its own branch, and
|
|
246
|
+
* the engine's run branch and other tickets' branches, which the engine
|
|
247
|
+
* moves while agents work.
|
|
248
|
+
*/
|
|
249
|
+
const isEngineRef = ({ ref, branch }: { ref: string; branch: string }) => {
|
|
250
|
+
const run = branch.replace(/--ticket-\d+$/, '')
|
|
251
|
+
const name = ref.replace(/^refs\/heads\//, '')
|
|
252
|
+
return (
|
|
253
|
+
ref.startsWith('refs/heads/') &&
|
|
254
|
+
(name === branch || name === run || name.startsWith(`${run}--ticket-`))
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Every branch and tag: the object it names, and the commit it peels to. */
|
|
259
|
+
const allRefs = async ({
|
|
260
|
+
cwd,
|
|
261
|
+
}: {
|
|
262
|
+
cwd: string
|
|
263
|
+
}): Promise<{ ref: string; object: string; commit: string }[]> =>
|
|
264
|
+
(
|
|
265
|
+
await gitOk({
|
|
266
|
+
cwd,
|
|
267
|
+
args: [
|
|
268
|
+
'for-each-ref',
|
|
269
|
+
'--format=%(refname) %(objectname) %(*objectname)',
|
|
270
|
+
'refs/heads',
|
|
271
|
+
'refs/tags',
|
|
272
|
+
],
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
.split('\n')
|
|
276
|
+
.filter((line) => line.length > 0)
|
|
277
|
+
.map((line) => {
|
|
278
|
+
const [ref = '', object = '', peeled = ''] = line.split(' ')
|
|
279
|
+
return { ref, object, commit: peeled === '' ? object : peeled }
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The git state an agent must not change, every ref, and the rest of the
|
|
284
|
+
* shared `.git`, which others may change. The reads don't depend on each
|
|
285
|
+
* other, so they run at once.
|
|
286
|
+
*/
|
|
287
|
+
export const gitState = async ({
|
|
288
|
+
cwd,
|
|
289
|
+
branch,
|
|
290
|
+
common_dir,
|
|
291
|
+
}: {
|
|
292
|
+
cwd: string
|
|
293
|
+
branch: string
|
|
294
|
+
common_dir: string
|
|
295
|
+
}): Promise<{
|
|
296
|
+
state: GitState
|
|
297
|
+
refs: Record<string, string>
|
|
298
|
+
outside: OutsideGitState
|
|
299
|
+
}> => {
|
|
300
|
+
const [headLines, refs, stash, shared, staged] = await Promise.all([
|
|
301
|
+
// The commit HEAD names, then the ref it points at (`HEAD` when
|
|
302
|
+
// detached).
|
|
303
|
+
gitOk({
|
|
304
|
+
cwd,
|
|
305
|
+
args: ['rev-parse', 'HEAD', '--symbolic-full-name', 'HEAD'],
|
|
306
|
+
}),
|
|
307
|
+
allRefs({ cwd }),
|
|
308
|
+
git({ cwd, args: ['stash', 'list', '--format=%H %gs'] }),
|
|
309
|
+
sharedGitState({ cwd, branch, common_dir }),
|
|
310
|
+
gitOk({ cwd, args: ['diff', '--cached', '--name-only', '-z'] }),
|
|
311
|
+
])
|
|
312
|
+
const [head = '', headRef = ''] = headLines.trim().split('\n')
|
|
313
|
+
const byRef = Object.fromEntries(
|
|
314
|
+
refs.map(({ ref, object }) => [ref, object])
|
|
315
|
+
)
|
|
316
|
+
return {
|
|
317
|
+
outside: shared.outside,
|
|
318
|
+
refs: byRef,
|
|
319
|
+
state: {
|
|
320
|
+
head,
|
|
321
|
+
head_ref: headRef === '' || headRef === 'HEAD' ? null : headRef,
|
|
322
|
+
branch_ref: byRef[`refs/heads/${branch}`] ?? null,
|
|
323
|
+
refs_at_head: sortBy(
|
|
324
|
+
refs
|
|
325
|
+
.filter(
|
|
326
|
+
({ ref, commit }) =>
|
|
327
|
+
commit === head && !isEngineRef({ ref, branch })
|
|
328
|
+
)
|
|
329
|
+
.map(({ ref }) => ref)
|
|
330
|
+
),
|
|
331
|
+
stash: stash.stdout
|
|
332
|
+
.split('\n')
|
|
333
|
+
.filter((line) => line.includes(`${branch}:`)),
|
|
334
|
+
staged: sortBy(nulSplit(staged)),
|
|
335
|
+
branch_config: shared.branch_config,
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The fingerprint of each changed path, each watched ignored path, and each
|
|
342
|
+
* path in `also` (the paths seen before the turn, so an ignored file that
|
|
343
|
+
* is gone shows as deleted). Ignore rules come from `exclude_file`.
|
|
344
|
+
*/
|
|
345
|
+
export const fileStates = async ({
|
|
346
|
+
cwd,
|
|
347
|
+
config,
|
|
348
|
+
exclude_file,
|
|
349
|
+
also,
|
|
350
|
+
}: {
|
|
351
|
+
cwd: string
|
|
352
|
+
config: EngineConfig
|
|
353
|
+
exclude_file: string
|
|
354
|
+
also?: string[]
|
|
355
|
+
}): Promise<Record<string, string>> => {
|
|
356
|
+
const files: Record<string, string> = {}
|
|
357
|
+
const [changed, ignored] = await Promise.all([
|
|
358
|
+
changedPaths({ cwd, exclude_file }),
|
|
359
|
+
watchedIgnoredPaths({ cwd, config, exclude_file }),
|
|
360
|
+
])
|
|
361
|
+
const paths = uniq([...changed, ...ignored, ...(also ?? [])])
|
|
362
|
+
for (const path of paths) {
|
|
363
|
+
files[path] = await hashPath({ cwd, path })
|
|
364
|
+
}
|
|
365
|
+
return files
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Snapshots a worktree before an agent turn: each changed path's content
|
|
370
|
+
* hash and bytes, the ignored paths that matter, and the git state no agent
|
|
371
|
+
* may change, with what it takes to put that back.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* const before = await snapshotWorktree({ cwd, branch, config })
|
|
375
|
+
* // ... the agent works ...
|
|
376
|
+
* const { violations } = await enforceAfterTurn({ cwd, branch, role, may_edit_tests, config, before })
|
|
377
|
+
*/
|
|
378
|
+
export const snapshotWorktree = async ({
|
|
379
|
+
cwd,
|
|
380
|
+
branch,
|
|
381
|
+
config,
|
|
382
|
+
}: {
|
|
383
|
+
cwd: string
|
|
384
|
+
branch: string
|
|
385
|
+
config: EngineConfig
|
|
386
|
+
}): Promise<TurnSnapshot> => {
|
|
387
|
+
const [common_dir, global] = await Promise.all([
|
|
388
|
+
commonDirOf({ cwd }),
|
|
389
|
+
globalExcludesFile({ cwd }),
|
|
390
|
+
])
|
|
391
|
+
const exclude_rules = await excludeRules({ global, common_dir })
|
|
392
|
+
const [files, { state, refs, outside }] = await Promise.all([
|
|
393
|
+
withExcludeFile({
|
|
394
|
+
rules: exclude_rules,
|
|
395
|
+
use: (exclude_file) => fileStates({ cwd, config, exclude_file }),
|
|
396
|
+
}),
|
|
397
|
+
gitState({ cwd, branch, common_dir }),
|
|
398
|
+
])
|
|
399
|
+
const saved: Record<string, Uint8Array | null> = {}
|
|
400
|
+
for (const [path, hash] of Object.entries(files)) {
|
|
401
|
+
const full = join(cwd, path)
|
|
402
|
+
const plain =
|
|
403
|
+
hash !== DELETED && !inPackages(path) && lstatSync(full).isFile()
|
|
404
|
+
saved[path] = plain ? await Bun.file(full).bytes() : null
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
files,
|
|
408
|
+
saved,
|
|
409
|
+
git: state,
|
|
410
|
+
saved_git: { common_dir, refs, exclude_rules, outside },
|
|
411
|
+
}
|
|
412
|
+
}
|