@defrex/autobuild 0.8.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 +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ab init` — vendor the canonical default skills into a repo (SPEC §16.3,
|
|
3
|
+
* D11). Copies, not references: per-repo customization is the point, so each
|
|
4
|
+
* complete skill tree lands in the Agent Skills standard
|
|
5
|
+
* `.agents/skills/ab-<name>/`, where the repo may edit every file freely. Pi
|
|
6
|
+
* discovers that project directory
|
|
7
|
+
* directly; Claude discovers the same skill through a
|
|
8
|
+
* `.claude/skills/ab-<name>` symlink, so there is only one editable copy.
|
|
9
|
+
* Alongside the live copy, init records the PRISTINE installed bytes under
|
|
10
|
+
* `.agents/skills/.ab-pristine/` — repo-versioned, the base of `ab upgrade`'s
|
|
11
|
+
* three-way merges (src/cli/upgrade.ts).
|
|
12
|
+
*
|
|
13
|
+
* Init runs OUTSIDE build sessions: it takes a repo path, not a build, and
|
|
14
|
+
* needs no AB_* environment. It is safe to re-run — an existing
|
|
15
|
+
* autobuild.toml is never overwritten, and an installed skill with local
|
|
16
|
+
* edits is never clobbered (`force: true` is the explicit human override).
|
|
17
|
+
*/
|
|
18
|
+
import type { Dirent, Stats } from 'node:fs'
|
|
19
|
+
import {
|
|
20
|
+
cp,
|
|
21
|
+
lstat,
|
|
22
|
+
mkdir,
|
|
23
|
+
readdir,
|
|
24
|
+
readFile,
|
|
25
|
+
readlink,
|
|
26
|
+
rename,
|
|
27
|
+
rm,
|
|
28
|
+
rmdir,
|
|
29
|
+
stat,
|
|
30
|
+
symlink,
|
|
31
|
+
unlink,
|
|
32
|
+
writeFile,
|
|
33
|
+
} from 'node:fs/promises'
|
|
34
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
35
|
+
import { distributionRoot } from '../distribution'
|
|
36
|
+
import { installedSkillName, SKILL_NAMESPACE } from '../skills'
|
|
37
|
+
import { createProductionRuntimes } from '../ports/runner/production'
|
|
38
|
+
import type { RuntimeRegistry } from '../ports/runner/runtime'
|
|
39
|
+
import {
|
|
40
|
+
launchSetupAgent,
|
|
41
|
+
probeInitRuntimes,
|
|
42
|
+
selectSetupRuntime,
|
|
43
|
+
SETUP_RUNTIME_PREFERENCE,
|
|
44
|
+
type SetupAgentLauncher,
|
|
45
|
+
} from './init-agent'
|
|
46
|
+
|
|
47
|
+
export { SKILL_NAMESPACE }
|
|
48
|
+
|
|
49
|
+
/** Agent Skills standard project directory for vendored skills. */
|
|
50
|
+
export const AGENTS_SKILLS_DIR = join('.agents', 'skills')
|
|
51
|
+
|
|
52
|
+
/** The unsupported project path used by earlier autobuild releases. */
|
|
53
|
+
export const LEGACY_AGENT_SKILLS_DIR = join('.agent', 'skills')
|
|
54
|
+
|
|
55
|
+
/** Claude-compatible discovery links point at the canonical skills. */
|
|
56
|
+
export const CLAUDE_SKILLS_DIR = join('.claude', 'skills')
|
|
57
|
+
|
|
58
|
+
/** Where init records pristine installs, under `.agents/skills/`. */
|
|
59
|
+
export const PRISTINE_DIR = '.ab-pristine'
|
|
60
|
+
|
|
61
|
+
/** Repository-local state is always excluded by a fresh/repeated init. */
|
|
62
|
+
export const LOCAL_STATE_IGNORE_RULE = '.autobuild/'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The autobuild distribution root, resolved relative to THIS module file
|
|
66
|
+
* so `ab init` works from any cwd. Its `skills/` and `templates/` directories
|
|
67
|
+
* are the canonical source.
|
|
68
|
+
*/
|
|
69
|
+
export function defaultDistRoot(): string {
|
|
70
|
+
return distributionRoot()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertSkillRelativePath(file: string): void {
|
|
74
|
+
if (
|
|
75
|
+
file === '' ||
|
|
76
|
+
file.startsWith('/') ||
|
|
77
|
+
file.startsWith('\\') ||
|
|
78
|
+
file.split(/[\\/]/).some((segment) => segment === '' || segment === '.' || segment === '..')
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(`invalid skill-relative file path "${file}"`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Live path for one file in an installed skill directory. */
|
|
85
|
+
export function installedSkillFilePath(
|
|
86
|
+
targetRepo: string,
|
|
87
|
+
installName: string,
|
|
88
|
+
file: string,
|
|
89
|
+
): string {
|
|
90
|
+
assertSkillRelativePath(file)
|
|
91
|
+
return join(targetRepo, AGENTS_SKILLS_DIR, installName, ...file.split('/'))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Live install path: `<target>/.agents/skills/ab-<name>/SKILL.md`. */
|
|
95
|
+
export function installedSkillPath(targetRepo: string, installName: string): string {
|
|
96
|
+
return installedSkillFilePath(targetRepo, installName, 'SKILL.md')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Claude discovery path: a directory symlink to the live `.agents` skill. */
|
|
100
|
+
export function claudeSkillPath(targetRepo: string, installName: string): string {
|
|
101
|
+
return join(targetRepo, CLAUDE_SKILLS_DIR, installName)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Pristine path for one distributed file in a skill directory. */
|
|
105
|
+
export function pristineSkillFilePath(
|
|
106
|
+
targetRepo: string,
|
|
107
|
+
installName: string,
|
|
108
|
+
file: string,
|
|
109
|
+
): string {
|
|
110
|
+
assertSkillRelativePath(file)
|
|
111
|
+
return join(targetRepo, AGENTS_SKILLS_DIR, PRISTINE_DIR, installName, ...file.split('/'))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Pristine record: `<target>/.agents/skills/.ab-pristine/ab-<name>/SKILL.md`. */
|
|
115
|
+
export function pristineSkillPath(targetRepo: string, installName: string): string {
|
|
116
|
+
return pristineSkillFilePath(targetRepo, installName, 'SKILL.md')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function legacyInstalledSkillPath(targetRepo: string, installName: string): string {
|
|
120
|
+
return join(targetRepo, CLAUDE_SKILLS_DIR, installName, 'SKILL.md')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyPristineSkillPath(targetRepo: string, installName: string): string {
|
|
124
|
+
return join(targetRepo, CLAUDE_SKILLS_DIR, PRISTINE_DIR, installName, 'SKILL.md')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read a file's text, or undefined when it does not exist. */
|
|
128
|
+
export async function readIfExists(path: string): Promise<string | undefined> {
|
|
129
|
+
try {
|
|
130
|
+
return await readFile(path, 'utf8')
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Ensure the exact repository-local state rule exists without rewriting any
|
|
139
|
+
* existing ignore bytes. Re-running is duplicate-free, including when the
|
|
140
|
+
* original file had no trailing newline.
|
|
141
|
+
*/
|
|
142
|
+
export async function ensureLocalStateIgnored(targetRepo: string): Promise<boolean> {
|
|
143
|
+
const path = join(targetRepo, '.gitignore')
|
|
144
|
+
const current = await readIfExists(path)
|
|
145
|
+
if (current !== undefined) {
|
|
146
|
+
const rules = current.split(/\r?\n/)
|
|
147
|
+
if (rules.includes(LOCAL_STATE_IGNORE_RULE)) return false
|
|
148
|
+
const separator = current === '' || current.endsWith('\n') ? '' : '\n'
|
|
149
|
+
await writeFile(path, `${current}${separator}${LOCAL_STATE_IGNORE_RULE}\n`)
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
await mkdir(targetRepo, { recursive: true })
|
|
153
|
+
await writeFile(path, `${LOCAL_STATE_IGNORE_RULE}\n`)
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The skills a model may trigger from its description (§16.3). Everything else
|
|
159
|
+
* gets `disable-model-invocation: true`: phase skills are invoked explicitly by
|
|
160
|
+
* the runner or a human, and a model must never start a pipeline phase by
|
|
161
|
+
* pattern-matching a description. Membership here is reserved for skills that
|
|
162
|
+
* drive NO phase — `spec` is the conversation that writes a spec before a build
|
|
163
|
+
* exists, `tickets` is how "move ticket X to ready" becomes an action, and
|
|
164
|
+
* `guide` is read-only reference material about the system. For these, model
|
|
165
|
+
* invocation is precisely the point. Keep this set small; widening it needs the
|
|
166
|
+
* §16.3 criterion, not convenience.
|
|
167
|
+
*/
|
|
168
|
+
export const MODEL_INVOCABLE_SKILLS = new Set(['spec', 'tickets', 'guide'])
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Rewrite a canonical skill's YAML frontmatter for installation (§16.3):
|
|
172
|
+
* `name` becomes the namespaced `ab-<name>`, and every skill outside
|
|
173
|
+
* `MODEL_INVOCABLE_SKILLS` gets `disable-model-invocation: true`. The
|
|
174
|
+
* description and the body below the frontmatter are preserved verbatim.
|
|
175
|
+
* Deliberately minimal and line-based: two known keys do not justify a YAML
|
|
176
|
+
* dependency.
|
|
177
|
+
*/
|
|
178
|
+
export function rewriteSkillSource(source: string, skillName: string): string {
|
|
179
|
+
const lines = source.split('\n')
|
|
180
|
+
if (lines[0] !== '---') {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`skill "${skillName}" has no YAML frontmatter — expected the file to open with '---'`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
const close = lines.indexOf('---', 1)
|
|
186
|
+
if (close === -1) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`skill "${skillName}" has unterminated YAML frontmatter — no closing '---' found`,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
const front = lines
|
|
192
|
+
.slice(1, close)
|
|
193
|
+
.filter((line) => !line.startsWith('disable-model-invocation:'))
|
|
194
|
+
.map((line) => (line.startsWith('name:') ? `name: ${installedSkillName(skillName)}` : line))
|
|
195
|
+
if (!MODEL_INVOCABLE_SKILLS.has(skillName)) {
|
|
196
|
+
front.push('disable-model-invocation: true')
|
|
197
|
+
}
|
|
198
|
+
return ['---', ...front, ...lines.slice(close)].join('\n')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface DistSkillFile {
|
|
202
|
+
/** POSIX-style path relative to `skills/<name>/`. */
|
|
203
|
+
path: string
|
|
204
|
+
/** Install-ready text. Only SKILL.md receives frontmatter rewriting. */
|
|
205
|
+
content: string
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface DistSkill {
|
|
209
|
+
/** Bare name in the distribution (`plan`). */
|
|
210
|
+
name: string
|
|
211
|
+
/** Namespaced install name (`ab-plan`). */
|
|
212
|
+
installName: string
|
|
213
|
+
/** SKILL.md install content, retained as a convenience for existing callers. */
|
|
214
|
+
content: string
|
|
215
|
+
/** Every regular file in the canonical skill tree, path-sorted. */
|
|
216
|
+
files: DistSkillFile[]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readSkillTree(root: string, prefix = ''): Promise<DistSkillFile[]> {
|
|
220
|
+
const entries = await readdir(join(root, ...prefix.split('/').filter(Boolean)), {
|
|
221
|
+
withFileTypes: true,
|
|
222
|
+
})
|
|
223
|
+
const files: DistSkillFile[] = []
|
|
224
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
225
|
+
const path = prefix === '' ? entry.name : `${prefix}/${entry.name}`
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
files.push(...(await readSkillTree(root, path)))
|
|
228
|
+
} else if (entry.isFile()) {
|
|
229
|
+
assertSkillRelativePath(path)
|
|
230
|
+
files.push({
|
|
231
|
+
path,
|
|
232
|
+
content: await readFile(join(root, ...path.split('/')), 'utf8'),
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return files.sort((a, b) => a.path.localeCompare(b.path))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Enumerate complete `<distRoot>/skills/<name>/` trees, sorted, install-ready. */
|
|
240
|
+
export async function readDistSkills(distRoot: string): Promise<DistSkill[]> {
|
|
241
|
+
const skillsDir = join(distRoot, 'skills')
|
|
242
|
+
const entries = await readdir(skillsDir, { withFileTypes: true })
|
|
243
|
+
const skills: DistSkill[] = []
|
|
244
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
245
|
+
if (!entry.isDirectory()) continue
|
|
246
|
+
const root = join(skillsDir, entry.name)
|
|
247
|
+
const files = await readSkillTree(root)
|
|
248
|
+
const skillFile = files.find((file) => file.path === 'SKILL.md')
|
|
249
|
+
if (skillFile === undefined) continue
|
|
250
|
+
skillFile.content = rewriteSkillSource(skillFile.content, entry.name)
|
|
251
|
+
skills.push({
|
|
252
|
+
name: entry.name,
|
|
253
|
+
installName: installedSkillName(entry.name),
|
|
254
|
+
content: skillFile.content,
|
|
255
|
+
files,
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
return skills
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** True when two existing directory paths resolve to the same filesystem entry. */
|
|
262
|
+
async function sameDirectoryEntry(left: string, right: string): Promise<boolean> {
|
|
263
|
+
let leftStat: Stats
|
|
264
|
+
let rightStat: Stats
|
|
265
|
+
try {
|
|
266
|
+
;[leftStat, rightStat] = await Promise.all([stat(left), stat(right)])
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
269
|
+
throw error
|
|
270
|
+
}
|
|
271
|
+
return (
|
|
272
|
+
leftStat.isDirectory() &&
|
|
273
|
+
rightStat.isDirectory() &&
|
|
274
|
+
leftStat.dev === rightStat.dev &&
|
|
275
|
+
leftStat.ino === rightStat.ino
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function symlinkPointsAt(link: string, target: string): Promise<boolean> {
|
|
280
|
+
try {
|
|
281
|
+
const linkStat = await lstat(link)
|
|
282
|
+
if (!linkStat.isSymbolicLink()) return false
|
|
283
|
+
return resolve(dirname(link), await readlink(link)) === resolve(target)
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
286
|
+
throw error
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Recognize a root alias even while its target is still missing, and
|
|
292
|
+
* materialize that target so later writes through either spelling succeed.
|
|
293
|
+
*/
|
|
294
|
+
async function prepareAliasedSkillRoots(targetRepo: string): Promise<boolean> {
|
|
295
|
+
const agentsRoot = join(targetRepo, AGENTS_SKILLS_DIR)
|
|
296
|
+
const claudeRoot = join(targetRepo, CLAUDE_SKILLS_DIR)
|
|
297
|
+
if (await sameDirectoryEntry(agentsRoot, claudeRoot)) return true
|
|
298
|
+
if (await symlinkPointsAt(claudeRoot, agentsRoot)) {
|
|
299
|
+
await mkdir(agentsRoot, { recursive: true })
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
if (await symlinkPointsAt(agentsRoot, claudeRoot)) {
|
|
303
|
+
await mkdir(claudeRoot, { recursive: true })
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** An actionable collision with a distinct, user-owned Claude discovery entry. */
|
|
310
|
+
export class ClaudeSkillDiscoveryConflict extends Error {
|
|
311
|
+
readonly skill: string
|
|
312
|
+
|
|
313
|
+
constructor(
|
|
314
|
+
targetRepo: string,
|
|
315
|
+
installName: string,
|
|
316
|
+
kind: 'directory' | 'foreign-symlink' = 'directory',
|
|
317
|
+
) {
|
|
318
|
+
const claude = relative(targetRepo, claudeSkillPath(targetRepo, installName))
|
|
319
|
+
const canonical = relative(targetRepo, dirname(installedSkillPath(targetRepo, installName)))
|
|
320
|
+
const conflict =
|
|
321
|
+
kind === 'foreign-symlink'
|
|
322
|
+
? `${claude} is a foreign symlink rather than an Autobuild-owned link to ${canonical}`
|
|
323
|
+
: `${claude} is a distinct real directory from ${canonical}`
|
|
324
|
+
super(
|
|
325
|
+
`cannot create Claude discovery link for "${installName}": ${conflict}; ` +
|
|
326
|
+
`move or remove ${claude}, then rerun the command`,
|
|
327
|
+
)
|
|
328
|
+
this.name = 'ClaudeSkillDiscoveryConflict'
|
|
329
|
+
this.skill = installName
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Ensure Claude discovers the canonical `.agents` skill through a symlink. */
|
|
334
|
+
export async function ensureClaudeSkillLink(
|
|
335
|
+
targetRepo: string,
|
|
336
|
+
installName: string,
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
const link = claudeSkillPath(targetRepo, installName)
|
|
339
|
+
const target = dirname(installedSkillPath(targetRepo, installName))
|
|
340
|
+
// A repository-level alias already gives Claude the canonical tree. Check
|
|
341
|
+
// before touching a per-skill path: on a fresh upgrade its target root may
|
|
342
|
+
// not yet exist, and creating through the alias would create a self-link.
|
|
343
|
+
if (await prepareAliasedSkillRoots(targetRepo)) return
|
|
344
|
+
|
|
345
|
+
await mkdir(dirname(link), { recursive: true })
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const linkStat = await lstat(link)
|
|
349
|
+
if (linkStat.isSymbolicLink()) {
|
|
350
|
+
const current = resolve(dirname(link), await readlink(link))
|
|
351
|
+
if (current === resolve(target) || (await sameDirectoryEntry(link, target))) return
|
|
352
|
+
await unlink(link)
|
|
353
|
+
} else if (linkStat.isDirectory()) {
|
|
354
|
+
if (await sameDirectoryEntry(link, target)) return
|
|
355
|
+
throw new ClaudeSkillDiscoveryConflict(targetRepo, installName)
|
|
356
|
+
} else {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`cannot create Claude link for "${installName}": ` +
|
|
359
|
+
`${CLAUDE_SKILLS_DIR}/${installName} exists and is not a directory symlink`,
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await symlink(relative(dirname(link), target), link, 'dir')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Recursively move non-conflicting entries from an obsolete directory into
|
|
371
|
+
* its replacement. Existing destination entries always win, so migration
|
|
372
|
+
* cannot clobber data; conflicting source entries remain for manual recovery.
|
|
373
|
+
*/
|
|
374
|
+
async function moveMissingEntries(source: string, destination: string): Promise<void> {
|
|
375
|
+
let entries: Dirent[]
|
|
376
|
+
try {
|
|
377
|
+
entries = await readdir(source, { withFileTypes: true })
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
380
|
+
throw error
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await mkdir(destination, { recursive: true })
|
|
384
|
+
for (const entry of entries) {
|
|
385
|
+
const from = join(source, entry.name)
|
|
386
|
+
const to = join(destination, entry.name)
|
|
387
|
+
let destinationStat: Stats
|
|
388
|
+
try {
|
|
389
|
+
destinationStat = await lstat(to)
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
392
|
+
await rename(from, to)
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const sourceStat = await lstat(from)
|
|
397
|
+
if (
|
|
398
|
+
sourceStat.isDirectory() &&
|
|
399
|
+
!sourceStat.isSymbolicLink() &&
|
|
400
|
+
destinationStat.isDirectory() &&
|
|
401
|
+
!destinationStat.isSymbolicLink()
|
|
402
|
+
) {
|
|
403
|
+
await moveMissingEntries(from, to)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await rmdir(source)
|
|
409
|
+
} catch (error) {
|
|
410
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
411
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY') throw error
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Migrate the unsupported `.agent/skills` project layout used by older
|
|
417
|
+
* releases into the Agent Skills standard `.agents/skills` directory. The
|
|
418
|
+
* whole tree is considered so local `ab-*` additions, supporting files, and
|
|
419
|
+
* pristine merge bases move together. Safe and idempotent.
|
|
420
|
+
*/
|
|
421
|
+
export async function migrateLegacyAgentSkills(targetRepo: string): Promise<void> {
|
|
422
|
+
await prepareAliasedSkillRoots(targetRepo)
|
|
423
|
+
await moveMissingEntries(
|
|
424
|
+
join(targetRepo, LEGACY_AGENT_SKILLS_DIR),
|
|
425
|
+
join(targetRepo, AGENTS_SKILLS_DIR),
|
|
426
|
+
)
|
|
427
|
+
try {
|
|
428
|
+
await rmdir(join(targetRepo, '.agent'))
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
431
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY') throw error
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Move a pre-canonical-layout `.claude` install into `.agents` without losing
|
|
437
|
+
* local edits or the pristine merge base. Returns the migrated live bytes.
|
|
438
|
+
*/
|
|
439
|
+
export async function migrateLegacySkill(
|
|
440
|
+
targetRepo: string,
|
|
441
|
+
installName: string,
|
|
442
|
+
warning?: (line: string) => void,
|
|
443
|
+
): Promise<string | undefined> {
|
|
444
|
+
// When the roots are aliases, `.claude` is not a legacy copy: it is the
|
|
445
|
+
// canonical live and pristine tree viewed through another path.
|
|
446
|
+
if (await prepareAliasedSkillRoots(targetRepo)) return undefined
|
|
447
|
+
|
|
448
|
+
let migrated: string | undefined
|
|
449
|
+
if ((await readIfExists(installedSkillPath(targetRepo, installName))) === undefined) {
|
|
450
|
+
const legacy = await readIfExists(legacyInstalledSkillPath(targetRepo, installName))
|
|
451
|
+
if (legacy !== undefined) {
|
|
452
|
+
const legacyDir = dirname(legacyInstalledSkillPath(targetRepo, installName))
|
|
453
|
+
const live = installedSkillPath(targetRepo, installName)
|
|
454
|
+
await mkdir(dirname(dirname(live)), { recursive: true })
|
|
455
|
+
await cp(legacyDir, dirname(live), { recursive: true })
|
|
456
|
+
// The complete directory now lives under `.agents`; clear the old
|
|
457
|
+
// discovery location so ensureClaudeSkillLink can replace it.
|
|
458
|
+
await rm(legacyDir, { recursive: true, force: true })
|
|
459
|
+
migrated = legacy
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const legacyPristineDir = dirname(legacyPristineSkillPath(targetRepo, installName))
|
|
464
|
+
try {
|
|
465
|
+
const stat = await lstat(legacyPristineDir)
|
|
466
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
467
|
+
await moveMissingEntries(
|
|
468
|
+
legacyPristineDir,
|
|
469
|
+
dirname(pristineSkillPath(targetRepo, installName)),
|
|
470
|
+
)
|
|
471
|
+
try {
|
|
472
|
+
await rmdir(join(targetRepo, CLAUDE_SKILLS_DIR, PRISTINE_DIR))
|
|
473
|
+
} catch (error) {
|
|
474
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
475
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY') throw error
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
await lstat(legacyPristineDir)
|
|
479
|
+
warning?.(
|
|
480
|
+
`${installName}: warning — conflicting legacy pristine files remain at ` +
|
|
481
|
+
`${relative(targetRepo, legacyPristineDir)} for manual recovery`,
|
|
482
|
+
)
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
489
|
+
}
|
|
490
|
+
return migrated
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Installed `ab-*` skill directories under `.agents/skills`, sorted. */
|
|
494
|
+
export async function listInstalledSkills(targetRepo: string): Promise<string[]> {
|
|
495
|
+
const dir = join(targetRepo, AGENTS_SKILLS_DIR)
|
|
496
|
+
let entries: Dirent[]
|
|
497
|
+
try {
|
|
498
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
501
|
+
throw error
|
|
502
|
+
}
|
|
503
|
+
const names: string[] = []
|
|
504
|
+
for (const entry of entries) {
|
|
505
|
+
if (!entry.isDirectory() || !entry.name.startsWith(SKILL_NAMESPACE)) continue
|
|
506
|
+
if ((await readIfExists(join(dir, entry.name, 'SKILL.md'))) === undefined) continue
|
|
507
|
+
names.push(entry.name)
|
|
508
|
+
}
|
|
509
|
+
return names.sort()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Write one live skill file and create its parent directories. */
|
|
513
|
+
export async function writeInstalledSkillFile(
|
|
514
|
+
targetRepo: string,
|
|
515
|
+
installName: string,
|
|
516
|
+
file: string,
|
|
517
|
+
content: string,
|
|
518
|
+
): Promise<void> {
|
|
519
|
+
const live = installedSkillFilePath(targetRepo, installName, file)
|
|
520
|
+
await mkdir(dirname(live), { recursive: true })
|
|
521
|
+
await writeFile(live, content)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Write one pristine skill file and create its parent directories. */
|
|
525
|
+
export async function writePristineFile(
|
|
526
|
+
targetRepo: string,
|
|
527
|
+
installName: string,
|
|
528
|
+
file: string,
|
|
529
|
+
content: string,
|
|
530
|
+
): Promise<void> {
|
|
531
|
+
const pristine = pristineSkillFilePath(targetRepo, installName, file)
|
|
532
|
+
await mkdir(dirname(pristine), { recursive: true })
|
|
533
|
+
await writeFile(pristine, content)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Write a skill's complete distributed tree and Claude discovery link. */
|
|
537
|
+
export async function installSkillTree(
|
|
538
|
+
targetRepo: string,
|
|
539
|
+
skill: Pick<DistSkill, 'installName' | 'files'>,
|
|
540
|
+
): Promise<void> {
|
|
541
|
+
for (const file of skill.files) {
|
|
542
|
+
await writeInstalledSkillFile(targetRepo, skill.installName, file.path, file.content)
|
|
543
|
+
await writePristineFile(targetRepo, skill.installName, file.path, file.content)
|
|
544
|
+
}
|
|
545
|
+
await ensureClaudeSkillLink(targetRepo, skill.installName)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Backward-compatible SKILL.md-only install helper. */
|
|
549
|
+
export async function installSkillFiles(
|
|
550
|
+
targetRepo: string,
|
|
551
|
+
installName: string,
|
|
552
|
+
content: string,
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
await installSkillTree(targetRepo, {
|
|
555
|
+
installName,
|
|
556
|
+
files: [{ path: 'SKILL.md', content }],
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Update only the pristine SKILL.md record for a skill. */
|
|
561
|
+
export async function writePristine(
|
|
562
|
+
targetRepo: string,
|
|
563
|
+
installName: string,
|
|
564
|
+
content: string,
|
|
565
|
+
): Promise<void> {
|
|
566
|
+
await writePristineFile(targetRepo, installName, 'SKILL.md', content)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export type InitSkillAction = 'installed' | 'kept' | 'unchanged' | 'overwritten'
|
|
570
|
+
export type InitConfigAction = 'written' | 'skipped'
|
|
571
|
+
|
|
572
|
+
export interface SkillDiscoveryConflict {
|
|
573
|
+
skill: string
|
|
574
|
+
message: string
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export interface InitReport {
|
|
578
|
+
/** What happened to autobuild.toml. */
|
|
579
|
+
config: InitConfigAction
|
|
580
|
+
/** Per-skill outcome, keyed by the namespaced install name. */
|
|
581
|
+
skills: Array<{ skill: string; action: InitSkillAction }>
|
|
582
|
+
/** User-owned Claude entries that prevented discovery-link maintenance. */
|
|
583
|
+
discoveryConflicts: SkillDiscoveryConflict[]
|
|
584
|
+
/** Interactive setup child status, or one when discovery conflicts remain. */
|
|
585
|
+
exitCode: number
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function setupPrompt(configExists: boolean): string {
|
|
589
|
+
const preface = configExists
|
|
590
|
+
? 'Review and improve the existing autobuild.toml. Preserve intentional repository choices; do not replace it with a generic template.'
|
|
591
|
+
: 'Complete the new minimal autobuild.toml skeleton using this repository and the user as your sources of truth.'
|
|
592
|
+
return (
|
|
593
|
+
`${preface}\n\n` +
|
|
594
|
+
'Read .agents/skills/ab-guide/references/setup.md and follow it to configure Autobuild for this repository.\n'
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderSkeleton(template: string, runtime: string): string {
|
|
599
|
+
const token = '@ab-init/runtime@'
|
|
600
|
+
const occurrences = template.split(token).length - 1
|
|
601
|
+
// Test/plugin distributions from before agent-driven init can still carry an
|
|
602
|
+
// explicit runtime. Preserve those valid templates rather than making skill
|
|
603
|
+
// installation depend on a newly introduced rendering token.
|
|
604
|
+
if (occurrences === 0) return template
|
|
605
|
+
if (occurrences !== 1) {
|
|
606
|
+
throw new Error(`autobuild.toml template must contain at most one ${token} token`)
|
|
607
|
+
}
|
|
608
|
+
return template.replace(token, runtime)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function abInit(opts: {
|
|
612
|
+
targetRepo: string
|
|
613
|
+
distRoot?: string
|
|
614
|
+
stdout?: (line: string) => void
|
|
615
|
+
force?: boolean
|
|
616
|
+
runtimes?: RuntimeRegistry
|
|
617
|
+
env?: Readonly<Record<string, string | undefined>>
|
|
618
|
+
interactive?: boolean
|
|
619
|
+
launcher?: SetupAgentLauncher
|
|
620
|
+
signal?: AbortSignal
|
|
621
|
+
}): Promise<InitReport> {
|
|
622
|
+
const distRoot = opts.distRoot ?? defaultDistRoot()
|
|
623
|
+
const stdout = opts.stdout ?? (() => {})
|
|
624
|
+
const force = opts.force ?? false
|
|
625
|
+
const env = opts.env ?? process.env
|
|
626
|
+
const runtimes = opts.runtimes ?? createProductionRuntimes().runtimes
|
|
627
|
+
const configPath = join(opts.targetRepo, 'autobuild.toml')
|
|
628
|
+
const configExists = (await readIfExists(configPath)) !== undefined
|
|
629
|
+
const probes = await probeInitRuntimes(runtimes, opts.targetRepo, env)
|
|
630
|
+
const selectedRuntime = selectSetupRuntime(probes)
|
|
631
|
+
const skeletonRuntime = selectedRuntime ?? SETUP_RUNTIME_PREFERENCE[0]
|
|
632
|
+
|
|
633
|
+
await migrateLegacyAgentSkills(opts.targetRepo)
|
|
634
|
+
|
|
635
|
+
let config: InitConfigAction
|
|
636
|
+
if (!configExists) {
|
|
637
|
+
const template = await readFile(join(distRoot, 'templates', 'autobuild.toml'), 'utf8')
|
|
638
|
+
await mkdir(opts.targetRepo, { recursive: true })
|
|
639
|
+
await writeFile(configPath, renderSkeleton(template, skeletonRuntime))
|
|
640
|
+
config = 'written'
|
|
641
|
+
} else {
|
|
642
|
+
config = 'skipped'
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await ensureLocalStateIgnored(opts.targetRepo)
|
|
646
|
+
|
|
647
|
+
const attention: string[] = []
|
|
648
|
+
const discoveryConflictMap = new Map<string, SkillDiscoveryConflict>()
|
|
649
|
+
const containDiscoveryConflict = (error: unknown): void => {
|
|
650
|
+
if (!(error instanceof ClaudeSkillDiscoveryConflict)) throw error
|
|
651
|
+
discoveryConflictMap.set(error.skill, { skill: error.skill, message: error.message })
|
|
652
|
+
}
|
|
653
|
+
const skills: InitReport['skills'] = []
|
|
654
|
+
for (const skill of await readDistSkills(distRoot)) {
|
|
655
|
+
const migrated = await migrateLegacySkill(opts.targetRepo, skill.installName, (line) => {
|
|
656
|
+
attention.push(line)
|
|
657
|
+
})
|
|
658
|
+
const rootLocal =
|
|
659
|
+
migrated ?? (await readIfExists(installedSkillPath(opts.targetRepo, skill.installName)))
|
|
660
|
+
let divergent = false
|
|
661
|
+
for (const file of skill.files) {
|
|
662
|
+
const livePath = installedSkillFilePath(opts.targetRepo, skill.installName, file.path)
|
|
663
|
+
const local =
|
|
664
|
+
file.path === 'SKILL.md' && migrated !== undefined ? migrated : await readIfExists(livePath)
|
|
665
|
+
if (local === undefined || local === file.content) {
|
|
666
|
+
await writeInstalledSkillFile(opts.targetRepo, skill.installName, file.path, file.content)
|
|
667
|
+
await writePristineFile(opts.targetRepo, skill.installName, file.path, file.content)
|
|
668
|
+
} else if (force) {
|
|
669
|
+
divergent = true
|
|
670
|
+
await writeInstalledSkillFile(opts.targetRepo, skill.installName, file.path, file.content)
|
|
671
|
+
await writePristineFile(opts.targetRepo, skill.installName, file.path, file.content)
|
|
672
|
+
} else {
|
|
673
|
+
divergent = true
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const action: InitSkillAction = divergent
|
|
677
|
+
? force
|
|
678
|
+
? 'overwritten'
|
|
679
|
+
: 'kept'
|
|
680
|
+
: rootLocal === undefined
|
|
681
|
+
? 'installed'
|
|
682
|
+
: 'unchanged'
|
|
683
|
+
try {
|
|
684
|
+
await ensureClaudeSkillLink(opts.targetRepo, skill.installName)
|
|
685
|
+
} catch (error) {
|
|
686
|
+
containDiscoveryConflict(error)
|
|
687
|
+
}
|
|
688
|
+
skills.push({ skill: skill.installName, action })
|
|
689
|
+
if (action === 'kept' || action === 'overwritten')
|
|
690
|
+
attention.push(`${skill.installName}: ${action}`)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
for (const name of await listInstalledSkills(opts.targetRepo)) {
|
|
694
|
+
if (discoveryConflictMap.has(name)) continue
|
|
695
|
+
try {
|
|
696
|
+
await ensureClaudeSkillLink(opts.targetRepo, name)
|
|
697
|
+
} catch (error) {
|
|
698
|
+
containDiscoveryConflict(error)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const discoveryConflicts = [...discoveryConflictMap.values()]
|
|
702
|
+
|
|
703
|
+
const skillCounts: Record<InitSkillAction, number> = {
|
|
704
|
+
installed: 0,
|
|
705
|
+
unchanged: 0,
|
|
706
|
+
kept: 0,
|
|
707
|
+
overwritten: 0,
|
|
708
|
+
}
|
|
709
|
+
for (const skill of skills) skillCounts[skill.action] += 1
|
|
710
|
+
|
|
711
|
+
stdout(`autobuild.toml: ${config}`)
|
|
712
|
+
stdout(
|
|
713
|
+
`Skills: ${skillCounts.installed} installed, ${skillCounts.unchanged} unchanged, ` +
|
|
714
|
+
`${skillCounts.kept} kept, ${skillCounts.overwritten} overwritten`,
|
|
715
|
+
)
|
|
716
|
+
for (const line of attention) stdout(line)
|
|
717
|
+
if (discoveryConflicts.length > 0) {
|
|
718
|
+
stdout('Claude discovery conflicts:')
|
|
719
|
+
for (const conflict of discoveryConflicts) stdout(` ${conflict.message}`)
|
|
720
|
+
}
|
|
721
|
+
stdout('')
|
|
722
|
+
stdout('Runtime probes:')
|
|
723
|
+
for (const probe of probes) {
|
|
724
|
+
stdout(` ${probe.runtime}: ${probe.usable ? 'usable' : 'unusable'} — ${probe.reason}`)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (discoveryConflicts.length > 0) {
|
|
728
|
+
return { config, skills, discoveryConflicts, exitCode: 1 }
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// The handoff is deliberately a stable pointer rather than embedded guide
|
|
732
|
+
// content. Installation remains successful for an older or partial
|
|
733
|
+
// distribution whose guide tree does not contain the reference yet.
|
|
734
|
+
const prompt = setupPrompt(configExists)
|
|
735
|
+
if (selectedRuntime !== undefined && opts.interactive === true) {
|
|
736
|
+
stdout('')
|
|
737
|
+
stdout(`Starting setup agent with ${selectedRuntime}…`)
|
|
738
|
+
const exitCode = await (opts.launcher ?? launchSetupAgent)({
|
|
739
|
+
runtime: selectedRuntime,
|
|
740
|
+
prompt,
|
|
741
|
+
cwd: opts.targetRepo,
|
|
742
|
+
env,
|
|
743
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
744
|
+
})
|
|
745
|
+
return { config, skills, discoveryConflicts, exitCode }
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
stdout('')
|
|
749
|
+
stdout(
|
|
750
|
+
selectedRuntime === undefined
|
|
751
|
+
? 'No usable interactive runtime was detected. Run the following prompt in a coding agent:'
|
|
752
|
+
: 'No interactive terminal was detected. Run the following prompt in a coding agent:',
|
|
753
|
+
)
|
|
754
|
+
stdout(prompt)
|
|
755
|
+
return { config, skills, discoveryConflicts, exitCode: 0 }
|
|
756
|
+
}
|