@abaigram/abaikit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -2
- package/abai/identity.md +1 -0
- package/abai/learnabai.md +474 -0
- package/abai/use.md +1 -0
- package/abaigram.yaml +208 -0
- package/dist/abaify.d.ts +68 -0
- package/dist/abaify.d.ts.map +1 -0
- package/dist/abaify.js +310 -0
- package/dist/abaify.js.map +1 -0
- package/dist/abaigram.d.ts +18 -0
- package/dist/abaigram.d.ts.map +1 -0
- package/dist/abaigram.js +153 -0
- package/dist/abaigram.js.map +1 -0
- package/dist/app.d.ts +19 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +29 -0
- package/dist/app.js.map +1 -0
- package/dist/bin.d.ts +45 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +274 -0
- package/dist/bin.js.map +1 -0
- package/dist/check.d.ts +119 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +416 -0
- package/dist/check.js.map +1 -0
- package/dist/command.d.ts +41 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +179 -0
- package/dist/command.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +12 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +20 -0
- package/dist/io.js.map +1 -0
- package/dist/learnabai.d.ts +60 -0
- package/dist/learnabai.d.ts.map +1 -0
- package/dist/learnabai.js +197 -0
- package/dist/learnabai.js.map +1 -0
- package/dist/new.d.ts +26 -0
- package/dist/new.d.ts.map +1 -0
- package/dist/new.js +87 -0
- package/dist/new.js.map +1 -0
- package/dist/repo.d.ts +99 -0
- package/dist/repo.d.ts.map +1 -0
- package/dist/repo.js +482 -0
- package/dist/repo.js.map +1 -0
- package/dist/run.d.ts +6 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +47 -0
- package/dist/run.js.map +1 -0
- package/dist/subject.d.ts +15 -0
- package/dist/subject.d.ts.map +1 -0
- package/dist/subject.js +53 -0
- package/dist/subject.js.map +1 -0
- package/dist/template-ts.d.ts +21 -0
- package/dist/template-ts.d.ts.map +1 -0
- package/dist/template-ts.js +322 -0
- package/dist/template-ts.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/examples/hello/abai/identity.md +1 -0
- package/examples/hello/abaigram.yaml +24 -0
- package/examples/hello/hello.ts +18 -0
- package/package.json +40 -5
package/abaigram.yaml
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# abaigram.yaml: the manifest of abaikit as an abaigram (abaigrammar 0.1).
|
|
2
|
+
# Generated by `abaikit abai manifest` from the program's declarations in code.
|
|
3
|
+
# Do not edit by hand: `abaikit abai doctor` and the test suite check that this file
|
|
4
|
+
# agrees with the code. Regenerate with `abaikit abai manifest > abaigram.yaml`.
|
|
5
|
+
|
|
6
|
+
abaigrammar: "0.1"
|
|
7
|
+
name: abaikit
|
|
8
|
+
entry: abaikit abai
|
|
9
|
+
modes:
|
|
10
|
+
- turns
|
|
11
|
+
- sidecar
|
|
12
|
+
check:
|
|
13
|
+
run: node dist/bin.js check examples/hello
|
|
14
|
+
content:
|
|
15
|
+
identity: abai/identity.md
|
|
16
|
+
use: abai/use.md
|
|
17
|
+
audit:
|
|
18
|
+
path: ~/.abai/audit/abaikit.jsonl
|
|
19
|
+
deviations:
|
|
20
|
+
- "Touchpoints are awaited promises: Node cannot block its only thread while the agent loop runs on it, so the program's logic pauses at the await while its own timers and I/O callbacks keep running (section 4.3)."
|
|
21
|
+
- "Sized cell writes and event payload copies allocate: JavaScript has no copy into a preallocated byte slot; scalar rows meet section 11.2, sized rows are measured and reported."
|
|
22
|
+
- "A cooperative pause holds the program at its next touchpoint, not inside safepoint(): a safepoint cannot block, since the resume arrives on the same event loop that would be blocked. The flag is still one load at a safepoint, and os pause (the launcher's signal) holds the program anywhere (section 5.5)."
|
|
23
|
+
- "Under launch the child is attached as soon as its endpoint is accepted, a moment after its first statement rather than before it: Node cannot listen in a constructor. A program that wants the agent present from its first line awaits attach() (section 6.3)."
|
|
24
|
+
- "In sidecar mode the region is not shared memory: cells are read by request and events cross the socket, so observation costs the program one serialization per connected harness (section 11.3). Under ABAI_HARNESS=null or exercise the null harness stays in the program's process and no endpoint opens, so a check run's measured overhead leaves that serialization out."
|
|
25
|
+
cells:
|
|
26
|
+
- name: check.runs
|
|
27
|
+
type: int
|
|
28
|
+
access: read
|
|
29
|
+
doc: Conformance checks run by this process.
|
|
30
|
+
- name: check.last_level
|
|
31
|
+
type: int
|
|
32
|
+
access: read
|
|
33
|
+
doc: The level the last check reported, 0 to 4.
|
|
34
|
+
- name: abaify.max_rounds
|
|
35
|
+
type: int
|
|
36
|
+
access: readwrite
|
|
37
|
+
default: 5
|
|
38
|
+
range:
|
|
39
|
+
- 1
|
|
40
|
+
- 20
|
|
41
|
+
doc: Agent rounds abaify gives a retrofit before it stops.
|
|
42
|
+
policy: auto
|
|
43
|
+
events:
|
|
44
|
+
- name: check.completed
|
|
45
|
+
payload:
|
|
46
|
+
repo:
|
|
47
|
+
type: string
|
|
48
|
+
max: 512
|
|
49
|
+
level:
|
|
50
|
+
type: int
|
|
51
|
+
wakes: never
|
|
52
|
+
doc: A conformance check finished.
|
|
53
|
+
- name: repo.changed
|
|
54
|
+
payload:
|
|
55
|
+
path:
|
|
56
|
+
type: string
|
|
57
|
+
max: 512
|
|
58
|
+
how:
|
|
59
|
+
type: string
|
|
60
|
+
max: 32
|
|
61
|
+
wakes: never
|
|
62
|
+
doc: "abaify changed the repository: wrote a file, inserted lines, added a dependency, regenerated the manifest, or installed."
|
|
63
|
+
capabilities:
|
|
64
|
+
- name: learnabai
|
|
65
|
+
params:
|
|
66
|
+
section: string
|
|
67
|
+
result:
|
|
68
|
+
text: string
|
|
69
|
+
dispatch: immediate
|
|
70
|
+
effect: read
|
|
71
|
+
example: {}
|
|
72
|
+
doc: "The guide for an agent: what an abaigram is, how to declare one in TypeScript, how to retrofit a repository, how check judges it; one section or all."
|
|
73
|
+
timeout: 30s
|
|
74
|
+
policy: auto
|
|
75
|
+
- name: check
|
|
76
|
+
params:
|
|
77
|
+
repo: string
|
|
78
|
+
level: int
|
|
79
|
+
mode: string
|
|
80
|
+
entry: string
|
|
81
|
+
runs: int
|
|
82
|
+
result:
|
|
83
|
+
level: int
|
|
84
|
+
passed: bool
|
|
85
|
+
report: string
|
|
86
|
+
dispatch: immediate
|
|
87
|
+
effect: mutate
|
|
88
|
+
policy: auto
|
|
89
|
+
timeout: 1800s
|
|
90
|
+
example:
|
|
91
|
+
repo: examples/hello
|
|
92
|
+
doc: "Verify a repository's conformance (section 12): L1 through its entry, L2 to L4 by running the repository's check.run command under the null harness, an exercise run and the measurement runs per mode. Needs no model; may take minutes."
|
|
93
|
+
- name: new
|
|
94
|
+
params:
|
|
95
|
+
name: string
|
|
96
|
+
lang: string
|
|
97
|
+
level: int
|
|
98
|
+
dir: string
|
|
99
|
+
result:
|
|
100
|
+
path: string
|
|
101
|
+
written: int
|
|
102
|
+
dispatch: immediate
|
|
103
|
+
effect: mutate
|
|
104
|
+
policy: confirm
|
|
105
|
+
doc: Create a fresh abaigram in a language, at a level, with one example of each declaration and the content slots in place.
|
|
106
|
+
timeout: 30s
|
|
107
|
+
- name: repo.list
|
|
108
|
+
params:
|
|
109
|
+
repo: string
|
|
110
|
+
glob: string
|
|
111
|
+
result:
|
|
112
|
+
count: int
|
|
113
|
+
listing: string
|
|
114
|
+
dispatch: immediate
|
|
115
|
+
effect: read
|
|
116
|
+
example:
|
|
117
|
+
repo: examples/hello
|
|
118
|
+
doc: "List a repository's files, skipping dependencies, build output and ignored trees; bounded, and the bound is said first. glob narrows: *.json by name anywhere, src/** or scripts/*.mjs by path."
|
|
119
|
+
timeout: 30s
|
|
120
|
+
policy: auto
|
|
121
|
+
- name: repo.read
|
|
122
|
+
params:
|
|
123
|
+
repo: string
|
|
124
|
+
path: string
|
|
125
|
+
start: int
|
|
126
|
+
lines: int
|
|
127
|
+
result:
|
|
128
|
+
text: string
|
|
129
|
+
total_lines: int
|
|
130
|
+
from: int
|
|
131
|
+
to: int
|
|
132
|
+
dispatch: immediate
|
|
133
|
+
effect: read
|
|
134
|
+
example:
|
|
135
|
+
repo: examples/hello
|
|
136
|
+
path: abaigram.yaml
|
|
137
|
+
doc: "Read part of a file in a repository: a window from line start, 400 lines or 12 KB at most a call; the result says from and to."
|
|
138
|
+
timeout: 30s
|
|
139
|
+
policy: auto
|
|
140
|
+
- name: repo.write
|
|
141
|
+
params:
|
|
142
|
+
repo: string
|
|
143
|
+
path: string
|
|
144
|
+
content: string
|
|
145
|
+
why: string
|
|
146
|
+
result:
|
|
147
|
+
written: bool
|
|
148
|
+
dispatch: immediate
|
|
149
|
+
effect: mutate
|
|
150
|
+
policy: confirm
|
|
151
|
+
doc: Write a new file in a repository (any directory this process can reach; under an MCP client each call is the client's permission prompt); an existing file is refused, since abaify adds and never replaces.
|
|
152
|
+
timeout: 30s
|
|
153
|
+
- name: repo.insert
|
|
154
|
+
params:
|
|
155
|
+
repo: string
|
|
156
|
+
path: string
|
|
157
|
+
anchor: string
|
|
158
|
+
where: string
|
|
159
|
+
text: string
|
|
160
|
+
why: string
|
|
161
|
+
result:
|
|
162
|
+
line: int
|
|
163
|
+
dispatch: immediate
|
|
164
|
+
effect: mutate
|
|
165
|
+
policy: confirm
|
|
166
|
+
doc: Insert text before or after a unique anchor line in an existing file of a repository; no line the file had is removed or reordered.
|
|
167
|
+
timeout: 30s
|
|
168
|
+
- name: repo.add_dependency
|
|
169
|
+
params:
|
|
170
|
+
repo: string
|
|
171
|
+
name: string
|
|
172
|
+
spec: string
|
|
173
|
+
why: string
|
|
174
|
+
result:
|
|
175
|
+
spec: string
|
|
176
|
+
dispatch: immediate
|
|
177
|
+
effect: mutate
|
|
178
|
+
policy: confirm
|
|
179
|
+
doc: Add a dependency to the repository's package.json in sorted place (the file is re-serialized, keys and indentation kept); the spec defaults to how abaikit reaches @abaigram/abai-stub, @abaigram/abai-embedded-harness and @abaigram/abaikit.
|
|
180
|
+
timeout: 30s
|
|
181
|
+
- name: repo.manifest
|
|
182
|
+
params:
|
|
183
|
+
repo: string
|
|
184
|
+
entry: string
|
|
185
|
+
why: string
|
|
186
|
+
result:
|
|
187
|
+
written: bool
|
|
188
|
+
lines: int
|
|
189
|
+
text: string
|
|
190
|
+
dispatch: immediate
|
|
191
|
+
effect: mutate
|
|
192
|
+
policy: confirm
|
|
193
|
+
timeout: 90s
|
|
194
|
+
doc: Run the repository's entry (`<entry> manifest`) and write what it prints to abaigram.yaml, the one file never written by hand.
|
|
195
|
+
- name: repo.install
|
|
196
|
+
params:
|
|
197
|
+
repo: string
|
|
198
|
+
why: string
|
|
199
|
+
result:
|
|
200
|
+
ok: bool
|
|
201
|
+
output: string
|
|
202
|
+
dispatch: immediate
|
|
203
|
+
effect: mutate
|
|
204
|
+
policy: confirm
|
|
205
|
+
timeout: 660s
|
|
206
|
+
doc: Run npm install in the repository (network; the repository's own install scripts run; the lock file and node_modules are rewritten), so the dependencies just added resolve.
|
|
207
|
+
decisions: []
|
|
208
|
+
turns: []
|
package/dist/abaify.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AgentModel } from "@abaigram/abai-embedded-harness";
|
|
2
|
+
import type { Mode } from "@abaigram/abai-stub";
|
|
3
|
+
import type { AbaikitApp } from "./app.ts";
|
|
4
|
+
import type { ParsedArgs } from "./bin.ts";
|
|
5
|
+
import { type CheckReport } from "./check.ts";
|
|
6
|
+
import { type AbaikitIo } from "./io.ts";
|
|
7
|
+
import { type Change } from "./repo.ts";
|
|
8
|
+
export declare const REPORT_FILE = "abai/abaify-report.md";
|
|
9
|
+
/** The brief is thousands of tokens: the model's first answer may take minutes, which is not unreachability. */
|
|
10
|
+
export declare const FIRST_CONTACT_MS = 180000;
|
|
11
|
+
export interface AbaifyOptions {
|
|
12
|
+
repo: string;
|
|
13
|
+
level?: number;
|
|
14
|
+
mode?: Mode;
|
|
15
|
+
rounds?: number;
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
/** A model for the agent; the machine's configured one otherwise. */
|
|
18
|
+
model?: AgentModel;
|
|
19
|
+
}
|
|
20
|
+
export interface Round {
|
|
21
|
+
number: number;
|
|
22
|
+
answer: string;
|
|
23
|
+
check: CheckReport | undefined;
|
|
24
|
+
/** When check could not run: why (no abaigram.yaml yet, for one). */
|
|
25
|
+
checkError?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface AbaifyResult {
|
|
28
|
+
root: string;
|
|
29
|
+
requested: number;
|
|
30
|
+
level: number;
|
|
31
|
+
passed: boolean;
|
|
32
|
+
dryRun: boolean;
|
|
33
|
+
rounds: Round[];
|
|
34
|
+
changes: Change[];
|
|
35
|
+
/** Why the run ended short, when it did: the model gone, an error. */
|
|
36
|
+
failure?: string;
|
|
37
|
+
reportPath?: string;
|
|
38
|
+
report: string;
|
|
39
|
+
}
|
|
40
|
+
interface Inventory {
|
|
41
|
+
packageJson: Record<string, unknown> | undefined;
|
|
42
|
+
entryFile?: {
|
|
43
|
+
path: string;
|
|
44
|
+
head: string;
|
|
45
|
+
to: number;
|
|
46
|
+
total: number;
|
|
47
|
+
};
|
|
48
|
+
guidelines?: {
|
|
49
|
+
path: string;
|
|
50
|
+
head: string;
|
|
51
|
+
};
|
|
52
|
+
summary: string;
|
|
53
|
+
total: number;
|
|
54
|
+
/** Build output present at the root (`dist/`, `build/`, `out/`), so the agent knows whether built paths run. */
|
|
55
|
+
buildOutput: string[];
|
|
56
|
+
}
|
|
57
|
+
/** The first prompt: the task, the repository as it is, the procedure mapped onto the capabilities, the API, the rules. */
|
|
58
|
+
export declare function brief(root: string, level: number, mode: Mode | undefined, inv: Inventory, dryRun: boolean): string;
|
|
59
|
+
/** The report left in the repository: every change with its why, the rounds, the last check. */
|
|
60
|
+
export declare function renderAbaifyReport(result: AbaifyResult, run: number): string;
|
|
61
|
+
export declare function abaify(options: AbaifyOptions, io: AbaikitIo, bundle: AbaikitApp): Promise<AbaifyResult>;
|
|
62
|
+
export declare function runAbaify(parsed: Extract<ParsedArgs, {
|
|
63
|
+
verb: "abaify";
|
|
64
|
+
}>, io: AbaikitIo, bundle: AbaikitApp, deps?: {
|
|
65
|
+
model?: AgentModel;
|
|
66
|
+
}): Promise<number>;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=abaify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abaify.d.ts","sourceRoot":"","sources":["../src/abaify.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAA4B,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,WAAW,EAA8C,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,KAAK,SAAS,EAA4B,MAAM,SAAS,CAAC;AAEnE,OAAO,EAAE,KAAK,MAAM,EAA2B,MAAM,WAAW,CAAC;AAEjE,eAAO,MAAM,WAAW,0BAA0B,CAAC;AAGnD,gHAAgH;AAChH,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAIxC,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,KAAK,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,UAAU,SAAS;IAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACjD,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,gHAAgH;IAChH,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAyDD,2HAA2H;AAC3H,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAkElH;AAaD,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAsC5E;AAaD,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAwG7G;AAED,wBAAsB,SAAS,CAC9B,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,EAC/C,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,UAAU,CAAA;CAAO,GAC/B,OAAO,CAAC,MAAM,CAAC,CA6BjB","sourcesContent":["// abaikit abaify: the agent-driven retrofit (section 13). abaikit's own\n// agent, with the configured model, works through the repo.* capabilities\n// and nothing else: no source line the repository had is ever removed or\n// reordered, and the three files structured tools rewrite (package.json,\n// abaigram.yaml, the lock file) are named as such. The person's invocation\n// is the consent for its writes, said at the start and in the report. Each\n// round the agent works until it stops, then abaikit runs check itself;\n// the level reached ends the loop, anything short of it is the next\n// prompt, up to the abaify.max_rounds knob. A model that cannot be reached\n// ends the run at once, and the report of what changed and why is left in\n// the repository whatever happened.\nimport { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { AgentModel, SessionForm, UserChannel } from \"@abaigram/abai-embedded-harness\";\nimport type { Mode } from \"@abaigram/abai-stub\";\nimport type { AbaikitApp } from \"./app.ts\";\nimport type { ParsedArgs } from \"./bin.ts\";\nimport { type CheckReport, checkRepository, recordCheck, renderReport } from \"./check.ts\";\nimport { type AbaikitIo, errorMessage, UsageError } from \"./io.ts\";\nimport { findSection, loadGuide } from \"./learnabai.ts\";\nimport { type Change, readRepo, summarizeRepo } from \"./repo.ts\";\n\nexport const REPORT_FILE = \"abai/abaify-report.md\";\n/** Lines of the entry file the brief quotes: enough to see where a launcher checks or imports its build. */\nconst HEAD_LINES = 80;\n/** The brief is thousands of tokens: the model's first answer may take minutes, which is not unreachability. */\nexport const FIRST_CONTACT_MS = 180_000;\n/** Scripts a brief names first, when the package has them. */\nconst KNOWN_SCRIPTS = [\"start\", \"build\", \"compile\", \"dev\", \"test\", \"check\", \"lint\", \"typecheck\"];\n\nexport interface AbaifyOptions {\n\trepo: string;\n\tlevel?: number;\n\tmode?: Mode;\n\trounds?: number;\n\tdryRun: boolean;\n\t/** A model for the agent; the machine's configured one otherwise. */\n\tmodel?: AgentModel;\n}\n\nexport interface Round {\n\tnumber: number;\n\tanswer: string;\n\tcheck: CheckReport | undefined;\n\t/** When check could not run: why (no abaigram.yaml yet, for one). */\n\tcheckError?: string;\n}\n\nexport interface AbaifyResult {\n\troot: string;\n\trequested: number;\n\tlevel: number;\n\tpassed: boolean;\n\tdryRun: boolean;\n\trounds: Round[];\n\tchanges: Change[];\n\t/** Why the run ended short, when it did: the model gone, an error. */\n\tfailure?: string;\n\treportPath?: string;\n\treport: string;\n}\n\ninterface Inventory {\n\tpackageJson: Record<string, unknown> | undefined;\n\tentryFile?: { path: string; head: string; to: number; total: number };\n\tguidelines?: { path: string; head: string };\n\tsummary: string;\n\ttotal: number;\n\t/** Build output present at the root (`dist/`, `build/`, `out/`), so the agent knows whether built paths run. */\n\tbuildOutput: string[];\n}\n\nfunction inventory(root: string): Inventory {\n\tlet packageJson: Record<string, unknown> | undefined;\n\ttry {\n\t\tpackageJson = JSON.parse(readFileSync(join(root, \"package.json\"), \"utf8\")) as Record<string, unknown>;\n\t} catch {\n\t\tpackageJson = undefined;\n\t}\n\tconst { total, summary } = summarizeRepo(root);\n\tconst buildOutput = [\"dist\", \"build\", \"out\"].filter(\n\t\t(d) => existsSync(join(root, d)) && statSync(join(root, d)).isDirectory(),\n\t);\n\tconst result: Inventory = { packageJson, summary, total, buildOutput };\n\tconst bin = packageJson?.bin;\n\tconst binTarget =\n\t\ttypeof bin === \"string\"\n\t\t\t? bin\n\t\t\t: bin && typeof bin === \"object\"\n\t\t\t\t? (Object.values(bin as Record<string, string>)[0] ?? undefined)\n\t\t\t\t: undefined;\n\tconst main = typeof packageJson?.main === \"string\" ? packageJson.main : undefined;\n\tfor (const candidate of [\n\t\tbinTarget,\n\t\tmain,\n\t\t\"src/main.ts\",\n\t\t\"src/main.js\",\n\t\t\"src/index.ts\",\n\t\t\"src/index.js\",\n\t\t\"index.js\",\n\t]) {\n\t\tif (candidate && existsSync(join(root, candidate))) {\n\t\t\tconst { text, to, total_lines } = readRepo(root, candidate, 1, HEAD_LINES);\n\t\t\tresult.entryFile = { path: candidate, head: text, to, total: total_lines };\n\t\t\tbreak;\n\t\t}\n\t}\n\tfor (const candidate of [\"AGENTS.md\", \"CONTRIBUTING.md\", \"CLAUDE.md\"]) {\n\t\tif (existsSync(join(root, candidate))) {\n\t\t\tconst { text } = readRepo(root, candidate, 1, 40);\n\t\t\tresult.guidelines = { path: candidate, head: text };\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction pickedScripts(scripts: Record<string, string>): Record<string, string> {\n\tconst picked: Record<string, string> = {};\n\tfor (const name of KNOWN_SCRIPTS) if (scripts[name] !== undefined) picked[name] = scripts[name];\n\tfor (const [name, value] of Object.entries(scripts)) {\n\t\tif (Object.keys(picked).length >= 15) break;\n\t\tif (picked[name] === undefined) picked[name] = value;\n\t}\n\treturn picked;\n}\n\n/** The first prompt: the task, the repository as it is, the procedure mapped onto the capabilities, the API, the rules. */\nexport function brief(root: string, level: number, mode: Mode | undefined, inv: Inventory, dryRun: boolean): string {\n\tconst guide = loadGuide();\n\tconst pkg = inv.packageJson;\n\tconst picked = pkg\n\t\t? JSON.stringify(\n\t\t\t\t{\n\t\t\t\t\tname: pkg.name,\n\t\t\t\t\ttype: pkg.type,\n\t\t\t\t\tbin: pkg.bin,\n\t\t\t\t\tmain: pkg.main,\n\t\t\t\t\tscripts: pickedScripts((pkg.scripts as Record<string, string>) ?? {}),\n\t\t\t\t},\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)\n\t\t: \"(no package.json)\";\n\tconst lines = [\n\t\t`Retrofit the repository at ${root} into an abaigram at level ${level}${mode ? ` in ${mode} mode` : \"\"}.`,\n\t\t\"\",\n\t\t\"You work only through abaikit's capabilities (the abai_call tool), with `repo` set to exactly the path above:\",\n\t\t\"- repo.list (glob narrows: `*.json` by name anywhere, `src/**` or `scripts/*.mjs` by path; the listing is bounded and says so first), repo.read (a window of lines; the result says `from` and `to`).\",\n\t\t\"- repo.write makes a new file only. repo.insert puts text before or after one unique anchor line of an existing file; no line the repository had is ever removed or reordered. repo.add_dependency adds to package.json (a structured edit that re-serializes the file). repo.manifest runs the entry's `manifest` and writes abaigram.yaml: never write that file yourself. repo.install runs npm install (the lock file and node_modules are rewritten). check gives the conformance report.\",\n\t\t\"- You cannot run shell commands or build the repository. Where the procedure below says `npm install`, call repo.add_dependency for both `@abaigram/abai-stub` and `@abaigram/abai-embedded-harness`, then repo.install once. Where it says `<entry> manifest > abaigram.yaml`, call repo.manifest with the entry. Where it says `abaikit check`, call check. Where it says build: \" +\n\t\t\t(inv.buildOutput.length > 0\n\t\t\t\t? `the build output is present (${inv.buildOutput.map((d) => `${d}/`).join(\", \")}), so the built program runs; prefer a cell or event at a real boundary of the program and a check.run that is a real command of it, over a synthetic mode.`\n\t\t\t\t: \"there is no build output, so choose a path that needs no build: a plain `.mjs` module beside a launcher, a cell or event set in code that runs unbuilt.\"),\n\t\t\"- The `entry` you declare must end with the word `abai` (such as `node scripts/x.mjs abai` or `node dist/main.js abai`): the tools append `manifest`, `doctor` and `run` to it, and the stub's runEntry answers only when `abai` comes first. A file you wrote in this run you may write again with repo.write; a file the repository had you may only insert into.\",\n\t\t\"- You have the person's consent for this run; do not ask them.\",\n\t\tdryRun\n\t\t\t? \"- This is a dry run: call the capabilities as you would; nothing is written, repo.manifest and check cannot succeed on files that are not on disk, so propose the whole retrofit and stop.\"\n\t\t\t: \"\",\n\t\t\"\",\n\t\t`Repository shape (${inv.total} files after skipping dependencies, build output and ignored trees; directories with their counts, use repo.list with a glob to see inside one):`,\n\t\t\"```\",\n\t\tinv.summary,\n\t\t\"```\",\n\t\t\"package.json (the known scripts first):\",\n\t\t\"```json\",\n\t\tpicked,\n\t\t\"```\",\n\t];\n\tif (inv.entryFile) {\n\t\tlines.push(\n\t\t\t`The entry file ${inv.entryFile.path}, lines 1 to ${inv.entryFile.to} of ${inv.entryFile.total}:`,\n\t\t\t\"```\",\n\t\t\tinv.entryFile.head,\n\t\t\t\"```\",\n\t\t);\n\t}\n\tif (inv.guidelines) {\n\t\tlines.push(`The repository's own guidelines, ${inv.guidelines.path}, head:`, \"```\", inv.guidelines.head, \"```\");\n\t}\n\tlines.push(\n\t\t\"\",\n\t\t\"The procedure, from abaikit learnabai (read it with the mapping above):\",\n\t\tfindSection(guide, \"retrofit\").text,\n\t\t\"\",\n\t\t\"The declaration API:\",\n\t\tfindSection(guide, \"declare\").text,\n\t\t\"\",\n\t\t\"What never to do:\",\n\t\tfindSection(guide, \"never\").text,\n\t\t\"\",\n\t\t`When you believe level ${level} is reached, call check once; when it reports level ${level} with no violation, answer with the word DONE on its own line followed by a short summary of what you added and why. If you cannot go on, say why.`,\n\t);\n\treturn lines.filter((l) => l !== undefined).join(\"\\n\");\n}\n\nfunction followUp(round: Round, level: number): string {\n\tconst status = round.check ? renderReport(round.check) : `check could not run: ${round.checkError}`;\n\treturn `abaikit ran check after your last round. Level ${level} is not reached yet. The report:\\n\\n${status}\\nFix what it names, through the capabilities, then call check again. Answer DONE with a summary when level ${level} holds with no violation.`;\n}\n\nfunction consentLine(root: string, level: number, dryRun: boolean): string {\n\treturn dryRun\n\t\t? `abaify: dry run on ${root}: the agent proposes a retrofit to level ${level}; nothing is written, though the repository's entry may be run read-only`\n\t\t: `abaify: ${root} to level ${level}, with your consent for this run: the agent may add files, insert lines, add dependencies, run npm install (network, the repository's own install scripts), run the repository's entry and its check.run under the null harness`;\n}\n\n/** The report left in the repository: every change with its why, the rounds, the last check. */\nexport function renderAbaifyReport(result: AbaifyResult, run: number): string {\n\tconst lines = [\n\t\t`# abaify report: ${result.root} (run ${run}, ${new Date().toISOString()})`,\n\t\t\"\",\n\t\tresult.dryRun\n\t\t\t? `Dry run to level ${result.requested}: ${result.changes.length} change${result.changes.length === 1 ? \"\" : \"s\"} proposed, nothing written, no check run.`\n\t\t\t: `Requested level ${result.requested}; reached level ${result.level}; ${result.passed ? \"passed\" : \"not passed\"} after ${result.rounds.length} round${result.rounds.length === 1 ? \"\" : \"s\"}.`,\n\t\tconsentLine(result.root, result.requested, result.dryRun).replace(/^abaify: /, \"Consent: \"),\n\t\t\"\",\n\t];\n\tif (result.failure) lines.push(`The run ended short: ${result.failure}`, \"\");\n\tconst added = result.changes.filter((c) => c.kind === \"added\");\n\tconst rewritten = result.changes.filter((c) => c.kind === \"rewritten\");\n\tlines.push(\"## Added (new files and inserted lines; nothing the repository had was removed or reordered)\", \"\");\n\tlines.push(\n\t\t...(added.length === 0 ? [\"(none)\"] : added.map((c) => `- ${c.how} ${c.path}: ${c.detail}. Why: ${c.why}`)),\n\t);\n\tlines.push(\"\", \"## Rewritten in place by a structured tool\", \"\");\n\tlines.push(\n\t\t...(rewritten.length === 0\n\t\t\t? [\"(none)\"]\n\t\t\t: rewritten.map((c) => `- ${c.how} ${c.path}: ${c.detail}. Why: ${c.why}`)),\n\t);\n\tlines.push(\"\", \"## Rounds\", \"\");\n\tfor (const r of result.rounds) {\n\t\tlines.push(`### Round ${r.number}`, \"\", \"The agent said:\", \"\", r.answer.trim() || \"(nothing)\", \"\");\n\t\tif (!result.dryRun) {\n\t\t\tlines.push(\n\t\t\t\tr.check\n\t\t\t\t\t? `check after the round: level ${r.check.level}, ${r.check.passed ? \"passed\" : \"not passed\"}`\n\t\t\t\t\t: `check could not run: ${r.checkError ?? \"unknown\"}`,\n\t\t\t\t\"\",\n\t\t\t);\n\t\t}\n\t}\n\tconst last = result.rounds.at(-1)?.check;\n\tif (last) lines.push(\"## The last check report\", \"\", \"```\", renderReport(last).trimEnd(), \"```\", \"\");\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\n/** The report appended to the repository's; the run number counts the reports already there. */\nfunction writeReport(root: string, result: AbaifyResult): string {\n\tconst path = join(root, REPORT_FILE);\n\tmkdirSync(join(root, \"abai\"), { recursive: true });\n\tconst previous = existsSync(path) ? readFileSync(path, \"utf8\") : \"\";\n\tconst run = (previous.match(/^# abaify report:/gm) ?? []).length + 1;\n\tresult.report = renderAbaifyReport(result, run);\n\twriteFileSync(path, (previous === \"\" ? \"\" : `${previous.trimEnd()}\\n\\n---\\n\\n`) + result.report);\n\treturn path;\n}\n\nexport async function abaify(options: AbaifyOptions, io: AbaikitIo, bundle: AbaikitApp): Promise<AbaifyResult> {\n\tconst { app, embedding, repo: ctx } = bundle;\n\tconst root = resolve(io.cwd, options.repo);\n\tif (!existsSync(join(root, \"package.json\"))) {\n\t\tthrow new UsageError(\n\t\t\t`${options.repo}: no package.json; abaify retrofits Node repositories (TypeScript or JavaScript) for now`,\n\t\t);\n\t}\n\tif (app.mode !== \"turns\")\n\t\tthrow new UsageError(`abaify runs its agent in-process; unset ABAI_MODE (it is ${app.mode})`);\n\tconst level = options.level ?? 1;\n\tconst rounds = app.knob<number>(\"abaify.max_rounds\");\n\tconst range = app.manifest.cells.find((c) => c.name === \"abaify.max_rounds\")?.range ?? [1, 20];\n\tif (options.rounds !== undefined) {\n\t\tif (options.rounds < range[0] || options.rounds > range[1])\n\t\t\tthrow new UsageError(`--rounds must be ${range[0]} to ${range[1]}`);\n\t\trounds.set(options.rounds);\n\t}\n\tconst say = (text: string) => io.err(text);\n\tsay(consentLine(root, level, options.dryRun));\n\tctx.dryRun = options.dryRun;\n\tctx.cwd = io.cwd;\n\tctx.onChange = (change) => say(`abaify: ${change.how} ${change.path}: ${change.detail}`);\n\tconst changesBefore = ctx.changes.length;\n\tconst result: AbaifyResult = {\n\t\troot,\n\t\trequested: level,\n\t\tlevel: 0,\n\t\tpassed: false,\n\t\tdryRun: options.dryRun,\n\t\trounds: [],\n\t\tchanges: [],\n\t\treport: \"\",\n\t};\n\n\t// the agent: abaikit's own embedded harness, auto consent (the invocation is the person's), a channel that only speaks\n\tconst channel: UserChannel = { say, ask: async () => undefined, confirm: async () => false, close: () => {} };\n\tembedding.options = {\n\t\tmodel: options.model,\n\t\tchannel,\n\t\tharness: { auto: true },\n\t\tconnectTimeoutMs: FIRST_CONTACT_MS,\n\t\taddendum: `You are retrofitting ${root} for a person who asked abaikit to do it; every write is theirs to approve and they approved this run.`,\n\t};\n\tlet form: SessionForm | undefined;\n\ttry {\n\t\tif (!(await app.attach()) || !embedding.current) {\n\t\t\tthrow new Error(\n\t\t\t\t`no agent could be attached: ${app.attachError ?? \"unknown\"} (configure one with \\`abaikit abai config\\`, or follow \\`abaikit learnabai\\` with your own agent)`,\n\t\t\t);\n\t\t}\n\t\tform = embedding.current.form;\n\t\tconst harness = embedding.current.harness;\n\t\tconst inv = inventory(root);\n\t\tlet prompt = brief(root, level, options.mode, inv, options.dryRun);\n\t\t// read once: an agent setting its own knob mid-run changes the next run, not this one\n\t\tconst max = options.dryRun ? 1 : rounds.get();\n\t\tfor (let n = 1; n <= max; n++) {\n\t\t\tsay(`abaify: round ${n} of ${max}`);\n\t\t\tconst answer = await form.prompt(prompt);\n\t\t\tconst round: Round = { number: n, answer, check: undefined };\n\t\t\tresult.rounds.push(round);\n\t\t\t// a run the model could not finish ends the loop: before first contact the form detaches; after it the\n\t\t\t// agent's last message carries the error, and another round would only spend the person's time\n\t\t\tconst last = [...form.agent.state.messages].reverse().find((m) => m.role === \"assistant\");\n\t\t\tconst stop = last && last.role === \"assistant\" ? last.stopReason : undefined;\n\t\t\tif (form.modelFailure !== undefined || !harness.attached) {\n\t\t\t\tresult.failure = `the model could not be reached: ${form.modelFailure ?? \"the harness detached\"}`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (stop === \"error\" || stop === \"aborted\") {\n\t\t\t\tconst why =\n\t\t\t\t\tlast && last.role === \"assistant\" && \"errorMessage\" in last ? String(last.errorMessage ?? stop) : stop;\n\t\t\t\tresult.failure = `the model could not be reached: the request ${stop === \"aborted\" ? \"was aborted\" : \"failed\"} (${why}); run abaify again when it is back`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (!options.dryRun) {\n\t\t\t\tsay(`abaify: check after round ${n}`);\n\t\t\t\ttry {\n\t\t\t\t\tround.check = await checkRepository({ repo: root, cwd: io.cwd, env: io.env, level, mode: options.mode });\n\t\t\t\t\trecordCheck(app, round.check);\n\t\t\t\t\tresult.level = round.check.level;\n\t\t\t\t\tresult.passed = round.check.passed;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tround.checkError = errorMessage(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (result.passed) break;\n\t\t\tprompt = followUp(round, level);\n\t\t}\n\t} catch (error) {\n\t\tresult.failure = result.failure ?? errorMessage(error);\n\t\tthrow error;\n\t} finally {\n\t\tform?.shutdown(\"abaify ended\");\n\t\tctx.dryRun = false;\n\t\tctx.onChange = undefined;\n\t\tresult.changes = ctx.changes.slice(changesBefore);\n\t\t// the report is written whatever happened once the agent was at work, so nothing done goes unrecorded\n\t\tif (options.dryRun) result.report = renderAbaifyReport(result, 1);\n\t\telse if (result.rounds.length > 0 || result.changes.length > 0) result.reportPath = writeReport(root, result);\n\t}\n\tif (options.dryRun) result.passed = true;\n\treturn result;\n}\n\nexport async function runAbaify(\n\tparsed: Extract<ParsedArgs, { verb: \"abaify\" }>,\n\tio: AbaikitIo,\n\tbundle: AbaikitApp,\n\tdeps: { model?: AgentModel } = {},\n): Promise<number> {\n\tconst result = await abaify(\n\t\t{\n\t\t\trepo: parsed.repo,\n\t\t\tlevel: parsed.level,\n\t\t\tmode: parsed.mode,\n\t\t\trounds: parsed.rounds,\n\t\t\tdryRun: parsed.dryRun,\n\t\t\tmodel: deps.model,\n\t\t},\n\t\tio,\n\t\tbundle,\n\t);\n\tio.out(result.report);\n\tif (result.reportPath) io.out(`report: ${result.reportPath}\\n`);\n\tif (result.failure) {\n\t\tio.out(\n\t\t\t`abaify: ended short after ${result.rounds.length} round${result.rounds.length === 1 ? \"\" : \"s\"}: ${result.failure}\\n`,\n\t\t);\n\t\treturn 1;\n\t}\n\tio.out(\n\t\tresult.dryRun\n\t\t\t? `dry run: ${result.changes.length} change${result.changes.length === 1 ? \"\" : \"s\"} proposed, nothing written\\n`\n\t\t\t: result.passed\n\t\t\t\t? `abaify: level ${result.level} reached in ${result.rounds.length} round${result.rounds.length === 1 ? \"\" : \"s\"}\\n`\n\t\t\t\t: `abaify: level ${result.requested} not reached after ${result.rounds.length} round${result.rounds.length === 1 ? \"\" : \"s\"} (reached ${result.level}); the report says what remains\\n`,\n\t);\n\treturn result.passed ? 0 : 1;\n}\n"]}
|
package/dist/abaify.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// abaikit abaify: the agent-driven retrofit (section 13). abaikit's own
|
|
2
|
+
// agent, with the configured model, works through the repo.* capabilities
|
|
3
|
+
// and nothing else: no source line the repository had is ever removed or
|
|
4
|
+
// reordered, and the three files structured tools rewrite (package.json,
|
|
5
|
+
// abaigram.yaml, the lock file) are named as such. The person's invocation
|
|
6
|
+
// is the consent for its writes, said at the start and in the report. Each
|
|
7
|
+
// round the agent works until it stops, then abaikit runs check itself;
|
|
8
|
+
// the level reached ends the loop, anything short of it is the next
|
|
9
|
+
// prompt, up to the abaify.max_rounds knob. A model that cannot be reached
|
|
10
|
+
// ends the run at once, and the report of what changed and why is left in
|
|
11
|
+
// the repository whatever happened.
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { checkRepository, recordCheck, renderReport } from "./check.js";
|
|
15
|
+
import { errorMessage, UsageError } from "./io.js";
|
|
16
|
+
import { findSection, loadGuide } from "./learnabai.js";
|
|
17
|
+
import { readRepo, summarizeRepo } from "./repo.js";
|
|
18
|
+
export const REPORT_FILE = "abai/abaify-report.md";
|
|
19
|
+
/** Lines of the entry file the brief quotes: enough to see where a launcher checks or imports its build. */
|
|
20
|
+
const HEAD_LINES = 80;
|
|
21
|
+
/** The brief is thousands of tokens: the model's first answer may take minutes, which is not unreachability. */
|
|
22
|
+
export const FIRST_CONTACT_MS = 180_000;
|
|
23
|
+
/** Scripts a brief names first, when the package has them. */
|
|
24
|
+
const KNOWN_SCRIPTS = ["start", "build", "compile", "dev", "test", "check", "lint", "typecheck"];
|
|
25
|
+
function inventory(root) {
|
|
26
|
+
let packageJson;
|
|
27
|
+
try {
|
|
28
|
+
packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
packageJson = undefined;
|
|
32
|
+
}
|
|
33
|
+
const { total, summary } = summarizeRepo(root);
|
|
34
|
+
const buildOutput = ["dist", "build", "out"].filter((d) => existsSync(join(root, d)) && statSync(join(root, d)).isDirectory());
|
|
35
|
+
const result = { packageJson, summary, total, buildOutput };
|
|
36
|
+
const bin = packageJson?.bin;
|
|
37
|
+
const binTarget = typeof bin === "string"
|
|
38
|
+
? bin
|
|
39
|
+
: bin && typeof bin === "object"
|
|
40
|
+
? (Object.values(bin)[0] ?? undefined)
|
|
41
|
+
: undefined;
|
|
42
|
+
const main = typeof packageJson?.main === "string" ? packageJson.main : undefined;
|
|
43
|
+
for (const candidate of [
|
|
44
|
+
binTarget,
|
|
45
|
+
main,
|
|
46
|
+
"src/main.ts",
|
|
47
|
+
"src/main.js",
|
|
48
|
+
"src/index.ts",
|
|
49
|
+
"src/index.js",
|
|
50
|
+
"index.js",
|
|
51
|
+
]) {
|
|
52
|
+
if (candidate && existsSync(join(root, candidate))) {
|
|
53
|
+
const { text, to, total_lines } = readRepo(root, candidate, 1, HEAD_LINES);
|
|
54
|
+
result.entryFile = { path: candidate, head: text, to, total: total_lines };
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const candidate of ["AGENTS.md", "CONTRIBUTING.md", "CLAUDE.md"]) {
|
|
59
|
+
if (existsSync(join(root, candidate))) {
|
|
60
|
+
const { text } = readRepo(root, candidate, 1, 40);
|
|
61
|
+
result.guidelines = { path: candidate, head: text };
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function pickedScripts(scripts) {
|
|
68
|
+
const picked = {};
|
|
69
|
+
for (const name of KNOWN_SCRIPTS)
|
|
70
|
+
if (scripts[name] !== undefined)
|
|
71
|
+
picked[name] = scripts[name];
|
|
72
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
73
|
+
if (Object.keys(picked).length >= 15)
|
|
74
|
+
break;
|
|
75
|
+
if (picked[name] === undefined)
|
|
76
|
+
picked[name] = value;
|
|
77
|
+
}
|
|
78
|
+
return picked;
|
|
79
|
+
}
|
|
80
|
+
/** The first prompt: the task, the repository as it is, the procedure mapped onto the capabilities, the API, the rules. */
|
|
81
|
+
export function brief(root, level, mode, inv, dryRun) {
|
|
82
|
+
const guide = loadGuide();
|
|
83
|
+
const pkg = inv.packageJson;
|
|
84
|
+
const picked = pkg
|
|
85
|
+
? JSON.stringify({
|
|
86
|
+
name: pkg.name,
|
|
87
|
+
type: pkg.type,
|
|
88
|
+
bin: pkg.bin,
|
|
89
|
+
main: pkg.main,
|
|
90
|
+
scripts: pickedScripts(pkg.scripts ?? {}),
|
|
91
|
+
}, null, 2)
|
|
92
|
+
: "(no package.json)";
|
|
93
|
+
const lines = [
|
|
94
|
+
`Retrofit the repository at ${root} into an abaigram at level ${level}${mode ? ` in ${mode} mode` : ""}.`,
|
|
95
|
+
"",
|
|
96
|
+
"You work only through abaikit's capabilities (the abai_call tool), with `repo` set to exactly the path above:",
|
|
97
|
+
"- repo.list (glob narrows: `*.json` by name anywhere, `src/**` or `scripts/*.mjs` by path; the listing is bounded and says so first), repo.read (a window of lines; the result says `from` and `to`).",
|
|
98
|
+
"- repo.write makes a new file only. repo.insert puts text before or after one unique anchor line of an existing file; no line the repository had is ever removed or reordered. repo.add_dependency adds to package.json (a structured edit that re-serializes the file). repo.manifest runs the entry's `manifest` and writes abaigram.yaml: never write that file yourself. repo.install runs npm install (the lock file and node_modules are rewritten). check gives the conformance report.",
|
|
99
|
+
"- You cannot run shell commands or build the repository. Where the procedure below says `npm install`, call repo.add_dependency for both `@abaigram/abai-stub` and `@abaigram/abai-embedded-harness`, then repo.install once. Where it says `<entry> manifest > abaigram.yaml`, call repo.manifest with the entry. Where it says `abaikit check`, call check. Where it says build: " +
|
|
100
|
+
(inv.buildOutput.length > 0
|
|
101
|
+
? `the build output is present (${inv.buildOutput.map((d) => `${d}/`).join(", ")}), so the built program runs; prefer a cell or event at a real boundary of the program and a check.run that is a real command of it, over a synthetic mode.`
|
|
102
|
+
: "there is no build output, so choose a path that needs no build: a plain `.mjs` module beside a launcher, a cell or event set in code that runs unbuilt."),
|
|
103
|
+
"- The `entry` you declare must end with the word `abai` (such as `node scripts/x.mjs abai` or `node dist/main.js abai`): the tools append `manifest`, `doctor` and `run` to it, and the stub's runEntry answers only when `abai` comes first. A file you wrote in this run you may write again with repo.write; a file the repository had you may only insert into.",
|
|
104
|
+
"- You have the person's consent for this run; do not ask them.",
|
|
105
|
+
dryRun
|
|
106
|
+
? "- This is a dry run: call the capabilities as you would; nothing is written, repo.manifest and check cannot succeed on files that are not on disk, so propose the whole retrofit and stop."
|
|
107
|
+
: "",
|
|
108
|
+
"",
|
|
109
|
+
`Repository shape (${inv.total} files after skipping dependencies, build output and ignored trees; directories with their counts, use repo.list with a glob to see inside one):`,
|
|
110
|
+
"```",
|
|
111
|
+
inv.summary,
|
|
112
|
+
"```",
|
|
113
|
+
"package.json (the known scripts first):",
|
|
114
|
+
"```json",
|
|
115
|
+
picked,
|
|
116
|
+
"```",
|
|
117
|
+
];
|
|
118
|
+
if (inv.entryFile) {
|
|
119
|
+
lines.push(`The entry file ${inv.entryFile.path}, lines 1 to ${inv.entryFile.to} of ${inv.entryFile.total}:`, "```", inv.entryFile.head, "```");
|
|
120
|
+
}
|
|
121
|
+
if (inv.guidelines) {
|
|
122
|
+
lines.push(`The repository's own guidelines, ${inv.guidelines.path}, head:`, "```", inv.guidelines.head, "```");
|
|
123
|
+
}
|
|
124
|
+
lines.push("", "The procedure, from abaikit learnabai (read it with the mapping above):", findSection(guide, "retrofit").text, "", "The declaration API:", findSection(guide, "declare").text, "", "What never to do:", findSection(guide, "never").text, "", `When you believe level ${level} is reached, call check once; when it reports level ${level} with no violation, answer with the word DONE on its own line followed by a short summary of what you added and why. If you cannot go on, say why.`);
|
|
125
|
+
return lines.filter((l) => l !== undefined).join("\n");
|
|
126
|
+
}
|
|
127
|
+
function followUp(round, level) {
|
|
128
|
+
const status = round.check ? renderReport(round.check) : `check could not run: ${round.checkError}`;
|
|
129
|
+
return `abaikit ran check after your last round. Level ${level} is not reached yet. The report:\n\n${status}\nFix what it names, through the capabilities, then call check again. Answer DONE with a summary when level ${level} holds with no violation.`;
|
|
130
|
+
}
|
|
131
|
+
function consentLine(root, level, dryRun) {
|
|
132
|
+
return dryRun
|
|
133
|
+
? `abaify: dry run on ${root}: the agent proposes a retrofit to level ${level}; nothing is written, though the repository's entry may be run read-only`
|
|
134
|
+
: `abaify: ${root} to level ${level}, with your consent for this run: the agent may add files, insert lines, add dependencies, run npm install (network, the repository's own install scripts), run the repository's entry and its check.run under the null harness`;
|
|
135
|
+
}
|
|
136
|
+
/** The report left in the repository: every change with its why, the rounds, the last check. */
|
|
137
|
+
export function renderAbaifyReport(result, run) {
|
|
138
|
+
const lines = [
|
|
139
|
+
`# abaify report: ${result.root} (run ${run}, ${new Date().toISOString()})`,
|
|
140
|
+
"",
|
|
141
|
+
result.dryRun
|
|
142
|
+
? `Dry run to level ${result.requested}: ${result.changes.length} change${result.changes.length === 1 ? "" : "s"} proposed, nothing written, no check run.`
|
|
143
|
+
: `Requested level ${result.requested}; reached level ${result.level}; ${result.passed ? "passed" : "not passed"} after ${result.rounds.length} round${result.rounds.length === 1 ? "" : "s"}.`,
|
|
144
|
+
consentLine(result.root, result.requested, result.dryRun).replace(/^abaify: /, "Consent: "),
|
|
145
|
+
"",
|
|
146
|
+
];
|
|
147
|
+
if (result.failure)
|
|
148
|
+
lines.push(`The run ended short: ${result.failure}`, "");
|
|
149
|
+
const added = result.changes.filter((c) => c.kind === "added");
|
|
150
|
+
const rewritten = result.changes.filter((c) => c.kind === "rewritten");
|
|
151
|
+
lines.push("## Added (new files and inserted lines; nothing the repository had was removed or reordered)", "");
|
|
152
|
+
lines.push(...(added.length === 0 ? ["(none)"] : added.map((c) => `- ${c.how} ${c.path}: ${c.detail}. Why: ${c.why}`)));
|
|
153
|
+
lines.push("", "## Rewritten in place by a structured tool", "");
|
|
154
|
+
lines.push(...(rewritten.length === 0
|
|
155
|
+
? ["(none)"]
|
|
156
|
+
: rewritten.map((c) => `- ${c.how} ${c.path}: ${c.detail}. Why: ${c.why}`)));
|
|
157
|
+
lines.push("", "## Rounds", "");
|
|
158
|
+
for (const r of result.rounds) {
|
|
159
|
+
lines.push(`### Round ${r.number}`, "", "The agent said:", "", r.answer.trim() || "(nothing)", "");
|
|
160
|
+
if (!result.dryRun) {
|
|
161
|
+
lines.push(r.check
|
|
162
|
+
? `check after the round: level ${r.check.level}, ${r.check.passed ? "passed" : "not passed"}`
|
|
163
|
+
: `check could not run: ${r.checkError ?? "unknown"}`, "");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const last = result.rounds.at(-1)?.check;
|
|
167
|
+
if (last)
|
|
168
|
+
lines.push("## The last check report", "", "```", renderReport(last).trimEnd(), "```", "");
|
|
169
|
+
return `${lines.join("\n")}\n`;
|
|
170
|
+
}
|
|
171
|
+
/** The report appended to the repository's; the run number counts the reports already there. */
|
|
172
|
+
function writeReport(root, result) {
|
|
173
|
+
const path = join(root, REPORT_FILE);
|
|
174
|
+
mkdirSync(join(root, "abai"), { recursive: true });
|
|
175
|
+
const previous = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
176
|
+
const run = (previous.match(/^# abaify report:/gm) ?? []).length + 1;
|
|
177
|
+
result.report = renderAbaifyReport(result, run);
|
|
178
|
+
writeFileSync(path, (previous === "" ? "" : `${previous.trimEnd()}\n\n---\n\n`) + result.report);
|
|
179
|
+
return path;
|
|
180
|
+
}
|
|
181
|
+
export async function abaify(options, io, bundle) {
|
|
182
|
+
const { app, embedding, repo: ctx } = bundle;
|
|
183
|
+
const root = resolve(io.cwd, options.repo);
|
|
184
|
+
if (!existsSync(join(root, "package.json"))) {
|
|
185
|
+
throw new UsageError(`${options.repo}: no package.json; abaify retrofits Node repositories (TypeScript or JavaScript) for now`);
|
|
186
|
+
}
|
|
187
|
+
if (app.mode !== "turns")
|
|
188
|
+
throw new UsageError(`abaify runs its agent in-process; unset ABAI_MODE (it is ${app.mode})`);
|
|
189
|
+
const level = options.level ?? 1;
|
|
190
|
+
const rounds = app.knob("abaify.max_rounds");
|
|
191
|
+
const range = app.manifest.cells.find((c) => c.name === "abaify.max_rounds")?.range ?? [1, 20];
|
|
192
|
+
if (options.rounds !== undefined) {
|
|
193
|
+
if (options.rounds < range[0] || options.rounds > range[1])
|
|
194
|
+
throw new UsageError(`--rounds must be ${range[0]} to ${range[1]}`);
|
|
195
|
+
rounds.set(options.rounds);
|
|
196
|
+
}
|
|
197
|
+
const say = (text) => io.err(text);
|
|
198
|
+
say(consentLine(root, level, options.dryRun));
|
|
199
|
+
ctx.dryRun = options.dryRun;
|
|
200
|
+
ctx.cwd = io.cwd;
|
|
201
|
+
ctx.onChange = (change) => say(`abaify: ${change.how} ${change.path}: ${change.detail}`);
|
|
202
|
+
const changesBefore = ctx.changes.length;
|
|
203
|
+
const result = {
|
|
204
|
+
root,
|
|
205
|
+
requested: level,
|
|
206
|
+
level: 0,
|
|
207
|
+
passed: false,
|
|
208
|
+
dryRun: options.dryRun,
|
|
209
|
+
rounds: [],
|
|
210
|
+
changes: [],
|
|
211
|
+
report: "",
|
|
212
|
+
};
|
|
213
|
+
// the agent: abaikit's own embedded harness, auto consent (the invocation is the person's), a channel that only speaks
|
|
214
|
+
const channel = { say, ask: async () => undefined, confirm: async () => false, close: () => { } };
|
|
215
|
+
embedding.options = {
|
|
216
|
+
model: options.model,
|
|
217
|
+
channel,
|
|
218
|
+
harness: { auto: true },
|
|
219
|
+
connectTimeoutMs: FIRST_CONTACT_MS,
|
|
220
|
+
addendum: `You are retrofitting ${root} for a person who asked abaikit to do it; every write is theirs to approve and they approved this run.`,
|
|
221
|
+
};
|
|
222
|
+
let form;
|
|
223
|
+
try {
|
|
224
|
+
if (!(await app.attach()) || !embedding.current) {
|
|
225
|
+
throw new Error(`no agent could be attached: ${app.attachError ?? "unknown"} (configure one with \`abaikit abai config\`, or follow \`abaikit learnabai\` with your own agent)`);
|
|
226
|
+
}
|
|
227
|
+
form = embedding.current.form;
|
|
228
|
+
const harness = embedding.current.harness;
|
|
229
|
+
const inv = inventory(root);
|
|
230
|
+
let prompt = brief(root, level, options.mode, inv, options.dryRun);
|
|
231
|
+
// read once: an agent setting its own knob mid-run changes the next run, not this one
|
|
232
|
+
const max = options.dryRun ? 1 : rounds.get();
|
|
233
|
+
for (let n = 1; n <= max; n++) {
|
|
234
|
+
say(`abaify: round ${n} of ${max}`);
|
|
235
|
+
const answer = await form.prompt(prompt);
|
|
236
|
+
const round = { number: n, answer, check: undefined };
|
|
237
|
+
result.rounds.push(round);
|
|
238
|
+
// a run the model could not finish ends the loop: before first contact the form detaches; after it the
|
|
239
|
+
// agent's last message carries the error, and another round would only spend the person's time
|
|
240
|
+
const last = [...form.agent.state.messages].reverse().find((m) => m.role === "assistant");
|
|
241
|
+
const stop = last && last.role === "assistant" ? last.stopReason : undefined;
|
|
242
|
+
if (form.modelFailure !== undefined || !harness.attached) {
|
|
243
|
+
result.failure = `the model could not be reached: ${form.modelFailure ?? "the harness detached"}`;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
if (stop === "error" || stop === "aborted") {
|
|
247
|
+
const why = last && last.role === "assistant" && "errorMessage" in last ? String(last.errorMessage ?? stop) : stop;
|
|
248
|
+
result.failure = `the model could not be reached: the request ${stop === "aborted" ? "was aborted" : "failed"} (${why}); run abaify again when it is back`;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (!options.dryRun) {
|
|
252
|
+
say(`abaify: check after round ${n}`);
|
|
253
|
+
try {
|
|
254
|
+
round.check = await checkRepository({ repo: root, cwd: io.cwd, env: io.env, level, mode: options.mode });
|
|
255
|
+
recordCheck(app, round.check);
|
|
256
|
+
result.level = round.check.level;
|
|
257
|
+
result.passed = round.check.passed;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
round.checkError = errorMessage(error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (result.passed)
|
|
264
|
+
break;
|
|
265
|
+
prompt = followUp(round, level);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
result.failure = result.failure ?? errorMessage(error);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
form?.shutdown("abaify ended");
|
|
274
|
+
ctx.dryRun = false;
|
|
275
|
+
ctx.onChange = undefined;
|
|
276
|
+
result.changes = ctx.changes.slice(changesBefore);
|
|
277
|
+
// the report is written whatever happened once the agent was at work, so nothing done goes unrecorded
|
|
278
|
+
if (options.dryRun)
|
|
279
|
+
result.report = renderAbaifyReport(result, 1);
|
|
280
|
+
else if (result.rounds.length > 0 || result.changes.length > 0)
|
|
281
|
+
result.reportPath = writeReport(root, result);
|
|
282
|
+
}
|
|
283
|
+
if (options.dryRun)
|
|
284
|
+
result.passed = true;
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
export async function runAbaify(parsed, io, bundle, deps = {}) {
|
|
288
|
+
const result = await abaify({
|
|
289
|
+
repo: parsed.repo,
|
|
290
|
+
level: parsed.level,
|
|
291
|
+
mode: parsed.mode,
|
|
292
|
+
rounds: parsed.rounds,
|
|
293
|
+
dryRun: parsed.dryRun,
|
|
294
|
+
model: deps.model,
|
|
295
|
+
}, io, bundle);
|
|
296
|
+
io.out(result.report);
|
|
297
|
+
if (result.reportPath)
|
|
298
|
+
io.out(`report: ${result.reportPath}\n`);
|
|
299
|
+
if (result.failure) {
|
|
300
|
+
io.out(`abaify: ended short after ${result.rounds.length} round${result.rounds.length === 1 ? "" : "s"}: ${result.failure}\n`);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
io.out(result.dryRun
|
|
304
|
+
? `dry run: ${result.changes.length} change${result.changes.length === 1 ? "" : "s"} proposed, nothing written\n`
|
|
305
|
+
: result.passed
|
|
306
|
+
? `abaify: level ${result.level} reached in ${result.rounds.length} round${result.rounds.length === 1 ? "" : "s"}\n`
|
|
307
|
+
: `abaify: level ${result.requested} not reached after ${result.rounds.length} round${result.rounds.length === 1 ? "" : "s"} (reached ${result.level}); the report says what remains\n`);
|
|
308
|
+
return result.passed ? 0 : 1;
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=abaify.js.map
|