@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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -2
  3. package/abai/identity.md +1 -0
  4. package/abai/learnabai.md +474 -0
  5. package/abai/use.md +1 -0
  6. package/abaigram.yaml +208 -0
  7. package/dist/abaify.d.ts +68 -0
  8. package/dist/abaify.d.ts.map +1 -0
  9. package/dist/abaify.js +310 -0
  10. package/dist/abaify.js.map +1 -0
  11. package/dist/abaigram.d.ts +18 -0
  12. package/dist/abaigram.d.ts.map +1 -0
  13. package/dist/abaigram.js +153 -0
  14. package/dist/abaigram.js.map +1 -0
  15. package/dist/app.d.ts +19 -0
  16. package/dist/app.d.ts.map +1 -0
  17. package/dist/app.js +29 -0
  18. package/dist/app.js.map +1 -0
  19. package/dist/bin.d.ts +45 -0
  20. package/dist/bin.d.ts.map +1 -0
  21. package/dist/bin.js +274 -0
  22. package/dist/bin.js.map +1 -0
  23. package/dist/check.d.ts +119 -0
  24. package/dist/check.d.ts.map +1 -0
  25. package/dist/check.js +416 -0
  26. package/dist/check.js.map +1 -0
  27. package/dist/command.d.ts +41 -0
  28. package/dist/command.d.ts.map +1 -0
  29. package/dist/command.js +179 -0
  30. package/dist/command.js.map +1 -0
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/io.d.ts +12 -0
  36. package/dist/io.d.ts.map +1 -0
  37. package/dist/io.js +20 -0
  38. package/dist/io.js.map +1 -0
  39. package/dist/learnabai.d.ts +60 -0
  40. package/dist/learnabai.d.ts.map +1 -0
  41. package/dist/learnabai.js +197 -0
  42. package/dist/learnabai.js.map +1 -0
  43. package/dist/new.d.ts +26 -0
  44. package/dist/new.d.ts.map +1 -0
  45. package/dist/new.js +87 -0
  46. package/dist/new.js.map +1 -0
  47. package/dist/repo.d.ts +99 -0
  48. package/dist/repo.d.ts.map +1 -0
  49. package/dist/repo.js +482 -0
  50. package/dist/repo.js.map +1 -0
  51. package/dist/run.d.ts +6 -0
  52. package/dist/run.d.ts.map +1 -0
  53. package/dist/run.js +47 -0
  54. package/dist/run.js.map +1 -0
  55. package/dist/subject.d.ts +15 -0
  56. package/dist/subject.d.ts.map +1 -0
  57. package/dist/subject.js +53 -0
  58. package/dist/subject.js.map +1 -0
  59. package/dist/template-ts.d.ts +21 -0
  60. package/dist/template-ts.d.ts.map +1 -0
  61. package/dist/template-ts.js +322 -0
  62. package/dist/template-ts.js.map +1 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +3 -0
  66. package/dist/version.js.map +1 -0
  67. package/examples/hello/abai/identity.md +1 -0
  68. package/examples/hello/abaigram.yaml +24 -0
  69. package/examples/hello/hello.ts +18 -0
  70. 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: []
@@ -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