akm-cli 0.9.0-beta.53 → 0.9.0-beta.55
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/dist/cli/clack.js +56 -0
- package/dist/cli/confirm.js +1 -1
- package/dist/cli.js +5 -3
- package/dist/commands/agent/contribute-cli.js +2 -3
- package/dist/commands/env/env-cli.js +187 -202
- package/dist/commands/env/secret-cli.js +109 -121
- package/dist/commands/feedback-cli.js +152 -155
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/html-report.js +33 -10
- package/dist/commands/health/improve-metrics.js +754 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +18 -0
- package/dist/commands/health/windows.js +196 -0
- package/dist/commands/health.js +15 -1492
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/collapse-detector.js +3 -2
- package/dist/commands/improve/consolidate.js +636 -633
- package/dist/commands/improve/dedup.js +1 -1
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +228 -0
- package/dist/commands/improve/distill/quality-gate.js +233 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill.js +49 -575
- package/dist/commands/improve/extract-cli.js +74 -76
- package/dist/commands/improve/extract.js +6 -4
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +3 -2
- package/dist/commands/improve/improve-cli.js +14 -13
- package/dist/commands/improve/improve-result-file.js +2 -1
- package/dist/commands/improve/improve.js +6 -5
- package/dist/commands/improve/loop-stages.js +19 -21
- package/dist/commands/improve/outcome-loop.js +18 -16
- package/dist/commands/improve/preparation.js +23 -5
- package/dist/commands/improve/procedural.js +10 -31
- package/dist/commands/improve/recombine.js +19 -43
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +48 -0
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +2 -2
- package/dist/commands/proposal/drain.js +1 -1
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +3 -3
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +1 -1
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposals.js +5 -920
- package/dist/commands/read/curate.js +4 -4
- package/dist/commands/read/remember-cli.js +132 -137
- package/dist/commands/read/search-cli.js +7 -5
- package/dist/commands/read/search.js +7 -3
- package/dist/commands/read/show.js +3 -5
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +12 -0
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +2 -2
- package/dist/commands/tasks/default-tasks.js +12 -0
- package/dist/commands/tasks/tasks-cli.js +1 -2
- package/dist/commands/wiki-cli.js +2 -3
- package/dist/core/common.js +3 -3
- package/dist/core/config/config-schema.js +6 -0
- package/dist/core/config/config.js +12 -0
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/events.js +2 -1
- package/dist/core/logs-db.js +8 -13
- package/dist/core/paths.js +14 -14
- package/dist/core/state-db.js +13 -1140
- package/dist/core/warn.js +21 -0
- package/dist/indexer/db/db.js +72 -709
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +3 -2
- package/dist/indexer/feedback/utility-policy.js +85 -0
- package/dist/indexer/graph/graph-extraction.js +2 -1
- package/dist/indexer/index-writer-lock.js +18 -0
- package/dist/indexer/indexer.js +94 -27
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/spawn.js +15 -66
- package/dist/llm/client.js +12 -0
- package/dist/llm/embedder.js +26 -2
- package/dist/llm/embedders/local.js +7 -1
- package/dist/output/text/helpers.js +13 -0
- package/dist/scripts/migrate-storage.js +6903 -7424
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +49 -44
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +25 -1608
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +130 -0
- package/dist/storage/repositories/index-db.js +4 -7
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +3 -3
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/runner.js +11 -1
- package/package.json +2 -2
- package/dist/commands/improve/homeostatic.js +0 -497
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Convert a raw `ProposalRow` to the public `Proposal` shape.
|
|
6
|
+
*/
|
|
7
|
+
export function proposalRowToProposal(row) {
|
|
8
|
+
let frontmatter;
|
|
9
|
+
if (row.frontmatter_json) {
|
|
10
|
+
try {
|
|
11
|
+
frontmatter = JSON.parse(row.frontmatter_json);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* ignore corrupt frontmatter JSON */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let meta = {};
|
|
18
|
+
try {
|
|
19
|
+
meta = JSON.parse(row.metadata_json);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* ignore */
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
ref: row.ref,
|
|
27
|
+
status: row.status,
|
|
28
|
+
source: row.source,
|
|
29
|
+
...(typeof meta.sourceRun === "string" ? { sourceRun: meta.sourceRun } : {}),
|
|
30
|
+
createdAt: row.created_at,
|
|
31
|
+
updatedAt: row.updated_at,
|
|
32
|
+
payload: {
|
|
33
|
+
content: row.content,
|
|
34
|
+
...(frontmatter !== undefined ? { frontmatter } : {}),
|
|
35
|
+
},
|
|
36
|
+
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
37
|
+
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
38
|
+
...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
|
|
39
|
+
...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
|
|
40
|
+
...(typeof meta.eligibilitySource === "string"
|
|
41
|
+
? { eligibilitySource: meta.eligibilitySource }
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Convert a public `Proposal` to column values ready for an INSERT/UPDATE.
|
|
47
|
+
* The `stash_dir` comes from the call site (proposals.ts has it in scope).
|
|
48
|
+
*/
|
|
49
|
+
export function proposalToRowValues(proposal, stashDir) {
|
|
50
|
+
// Fields that have no dedicated column live in metadata_json.
|
|
51
|
+
const metaObj = {};
|
|
52
|
+
if (proposal.sourceRun !== undefined)
|
|
53
|
+
metaObj.sourceRun = proposal.sourceRun;
|
|
54
|
+
if (proposal.review !== undefined)
|
|
55
|
+
metaObj.review = proposal.review;
|
|
56
|
+
if (proposal.confidence !== undefined)
|
|
57
|
+
metaObj.confidence = proposal.confidence;
|
|
58
|
+
if (proposal.gateDecision !== undefined)
|
|
59
|
+
metaObj.gateDecision = proposal.gateDecision;
|
|
60
|
+
if (proposal.backupContent !== undefined)
|
|
61
|
+
metaObj.backupContent = proposal.backupContent;
|
|
62
|
+
if (proposal.eligibilitySource !== undefined)
|
|
63
|
+
metaObj.eligibilitySource = proposal.eligibilitySource;
|
|
64
|
+
return {
|
|
65
|
+
id: proposal.id,
|
|
66
|
+
stash_dir: stashDir,
|
|
67
|
+
ref: proposal.ref,
|
|
68
|
+
status: proposal.status,
|
|
69
|
+
source: proposal.source,
|
|
70
|
+
created_at: proposal.createdAt,
|
|
71
|
+
updated_at: proposal.updatedAt,
|
|
72
|
+
content: proposal.payload.content,
|
|
73
|
+
frontmatter_json: proposal.payload.frontmatter ? JSON.stringify(proposal.payload.frontmatter) : null,
|
|
74
|
+
metadata_json: JSON.stringify(metaObj),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Upsert a proposal row. Called by the proposal write path when state.db is
|
|
79
|
+
* the active backend.
|
|
80
|
+
*/
|
|
81
|
+
export function upsertProposal(db, proposal, stashDir) {
|
|
82
|
+
const v = proposalToRowValues(proposal, stashDir);
|
|
83
|
+
db.prepare(`
|
|
84
|
+
INSERT INTO proposals
|
|
85
|
+
(id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
87
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
88
|
+
stash_dir = excluded.stash_dir,
|
|
89
|
+
ref = excluded.ref,
|
|
90
|
+
status = excluded.status,
|
|
91
|
+
source = excluded.source,
|
|
92
|
+
updated_at = excluded.updated_at,
|
|
93
|
+
content = excluded.content,
|
|
94
|
+
frontmatter_json = excluded.frontmatter_json,
|
|
95
|
+
metadata_json = excluded.metadata_json
|
|
96
|
+
`).run(v.id, v.stash_dir, v.ref, v.status, v.source, v.created_at, v.updated_at, v.content, v.frontmatter_json, v.metadata_json);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* List proposals, optionally filtered by stashDir, status, and/or ref.
|
|
100
|
+
*
|
|
101
|
+
* Results are ordered by `created_at ASC` (matching the historical
|
|
102
|
+
* `listProposals()` sort), with `rowid` as a deterministic tiebreak so two
|
|
103
|
+
* proposals created in the same millisecond list in insertion order.
|
|
104
|
+
*/
|
|
105
|
+
export function listStateProposals(db, options = {}) {
|
|
106
|
+
const conditions = [];
|
|
107
|
+
const params = [];
|
|
108
|
+
if (options.stashDir) {
|
|
109
|
+
conditions.push("stash_dir = ?");
|
|
110
|
+
params.push(options.stashDir);
|
|
111
|
+
}
|
|
112
|
+
if (options.status) {
|
|
113
|
+
conditions.push("status = ?");
|
|
114
|
+
params.push(options.status);
|
|
115
|
+
}
|
|
116
|
+
if (options.ref) {
|
|
117
|
+
conditions.push("ref = ?");
|
|
118
|
+
params.push(options.ref);
|
|
119
|
+
}
|
|
120
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
121
|
+
const rows = db
|
|
122
|
+
.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
|
|
123
|
+
content, frontmatter_json, metadata_json
|
|
124
|
+
FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`)
|
|
125
|
+
.all(...params);
|
|
126
|
+
return rows.map(proposalRowToProposal);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read every proposal's `gateDecision` record across all stashes (#612).
|
|
130
|
+
*
|
|
131
|
+
* Calibration reads the auto-accept gate's per-proposal decisions regardless of
|
|
132
|
+
* the proposal's current lifecycle status — a proposal that was auto-accepted
|
|
133
|
+
* is now `accepted`, an auto-rejected one stays `pending`, so filtering by
|
|
134
|
+
* status would drop half the join. Rows without a `gateDecision` (created
|
|
135
|
+
* before #577, or never gated) are skipped. The result is ordered by
|
|
136
|
+
* `decidedAt ASC` for deterministic downstream aggregation, falling back to
|
|
137
|
+
* `created_at` ordering from the SQL layer for rows with equal/missing
|
|
138
|
+
* timestamps.
|
|
139
|
+
*/
|
|
140
|
+
export function listProposalGateDecisions(db) {
|
|
141
|
+
const rows = db.prepare("SELECT metadata_json FROM proposals ORDER BY created_at ASC, rowid ASC").all();
|
|
142
|
+
const decisions = [];
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
let meta;
|
|
145
|
+
try {
|
|
146
|
+
meta = JSON.parse(row.metadata_json);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const decision = meta.gateDecision;
|
|
152
|
+
if (decision && typeof decision === "object" && typeof decision.outcome === "string") {
|
|
153
|
+
decisions.push(decision);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
decisions.sort((a, b) => new Date(a.decidedAt).getTime() - new Date(b.decidedAt).getTime());
|
|
157
|
+
return decisions;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Look up a single proposal by id, optionally scoped to one stash root.
|
|
161
|
+
* Returns undefined when not found.
|
|
162
|
+
*/
|
|
163
|
+
export function getStateProposal(db, id, stashDir) {
|
|
164
|
+
const sql = `SELECT id, stash_dir, ref, status, source, created_at, updated_at,
|
|
165
|
+
content, frontmatter_json, metadata_json
|
|
166
|
+
FROM proposals WHERE id = ?${stashDir ? " AND stash_dir = ?" : ""}`;
|
|
167
|
+
const row = (stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id));
|
|
168
|
+
return row ? proposalRowToProposal(row) : undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Find PENDING proposal ids in one stash whose id starts with `idPrefix`.
|
|
172
|
+
* Backs the UUID-prefix form of `akm proposal show/accept/... <prefix>` —
|
|
173
|
+
* prefix resolution is deliberately scoped to the live (pending) queue,
|
|
174
|
+
* mirroring the historical behaviour of scanning only the live directory.
|
|
175
|
+
*
|
|
176
|
+
* `%` / `_` / `\` in the prefix are escaped so the LIKE pattern is literal.
|
|
177
|
+
*/
|
|
178
|
+
export function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
|
|
179
|
+
const escaped = idPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
180
|
+
const rows = db
|
|
181
|
+
.prepare(`SELECT id FROM proposals
|
|
182
|
+
WHERE stash_dir = ? AND status = 'pending' AND id LIKE ? ESCAPE '\\'
|
|
183
|
+
ORDER BY id ASC`)
|
|
184
|
+
.all(stashDir, `${escaped}%`);
|
|
185
|
+
return rows.map((r) => r.id);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether the legacy filesystem proposal import has already run for `stashDir`.
|
|
189
|
+
* See migration 005 (`proposal_fs_imports`).
|
|
190
|
+
*/
|
|
191
|
+
export function hasImportedFsProposals(db, stashDir) {
|
|
192
|
+
// Drivers disagree on the no-row sentinel (bun:sqlite → null,
|
|
193
|
+
// better-sqlite3 → undefined) — Boolean() covers both.
|
|
194
|
+
return Boolean(db.prepare("SELECT 1 FROM proposal_fs_imports WHERE stash_dir = ?").get(stashDir));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Record that the legacy filesystem proposal import completed for `stashDir`
|
|
198
|
+
* so subsequent invocations skip the directory walk. INSERT OR REPLACE keeps
|
|
199
|
+
* the call idempotent.
|
|
200
|
+
*/
|
|
201
|
+
export function recordFsProposalsImport(db, stashDir, importedCount) {
|
|
202
|
+
db.prepare("INSERT OR REPLACE INTO proposal_fs_imports (stash_dir, imported_at, imported_count) VALUES (?, ?, ?)").run(stashDir, new Date().toISOString(), importedCount);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Insert a proposal row ONLY when the id is not already present (used by the
|
|
206
|
+
* legacy filesystem import so re-runs never clobber rows that have since been
|
|
207
|
+
* mutated through the canonical store). Returns true when a row was inserted.
|
|
208
|
+
*/
|
|
209
|
+
export function insertProposalIfAbsent(db, proposal, stashDir) {
|
|
210
|
+
const v = proposalToRowValues(proposal, stashDir);
|
|
211
|
+
const result = db
|
|
212
|
+
.prepare(`
|
|
213
|
+
INSERT OR IGNORE INTO proposals
|
|
214
|
+
(id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
216
|
+
`)
|
|
217
|
+
.run(v.id, v.stash_dir, v.ref, v.status, v.source, v.created_at, v.updated_at, v.content, v.frontmatter_json, v.metadata_json);
|
|
218
|
+
const changes = result.changes ?? 0;
|
|
219
|
+
return Number(changes) > 0;
|
|
220
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Record an induction of a recombine hypothesis and return the new consecutive
|
|
6
|
+
* count. INSERT … ON CONFLICT increments the streak, but the `last_run` guard
|
|
7
|
+
* makes a repeated call within the SAME run idempotent (no double-increment if
|
|
8
|
+
* the same ref appears twice in one run). On insert the streak starts at 1.
|
|
9
|
+
*/
|
|
10
|
+
export function recordRecombineInduction(db, input) {
|
|
11
|
+
const row = db
|
|
12
|
+
.prepare(`
|
|
13
|
+
INSERT INTO recombine_hypotheses
|
|
14
|
+
(hypothesis_ref, signature, member_key, consecutive_count, first_seen_at, last_seen_at, last_run)
|
|
15
|
+
VALUES (?, ?, ?, 1, ?, ?, ?)
|
|
16
|
+
ON CONFLICT(hypothesis_ref) DO UPDATE SET
|
|
17
|
+
consecutive_count = consecutive_count + (CASE WHEN last_run IS excluded.last_run THEN 0 ELSE 1 END),
|
|
18
|
+
last_seen_at = excluded.last_seen_at,
|
|
19
|
+
last_run = excluded.last_run,
|
|
20
|
+
signature = excluded.signature,
|
|
21
|
+
member_key = excluded.member_key
|
|
22
|
+
RETURNING consecutive_count
|
|
23
|
+
`)
|
|
24
|
+
.get(input.hypothesisRef, input.signature, input.memberKey, input.seenAt, input.seenAt, input.run);
|
|
25
|
+
return row?.consecutive_count ?? 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* #633 — find an existing pending (non-promoted) hypothesis row whose cluster
|
|
29
|
+
* is the SAME generalization as a newly-induced one, matched by SIGNATURE plus
|
|
30
|
+
* a Jaccard membership-overlap test, rather than an exact member-set hash.
|
|
31
|
+
*
|
|
32
|
+
* In a growing stash any added/removed memory changes the exact member set, so
|
|
33
|
+
* the ref hash (and member_key) shift every run → a fresh row at count=1 → the
|
|
34
|
+
* streak never reaches `confirmThreshold` and nothing ever promotes. Matching
|
|
35
|
+
* on overlap lets a drifting-but-stable cluster keep accumulating under one row.
|
|
36
|
+
*
|
|
37
|
+
* Returns the matched row with the HIGHEST overlap (ties broken by most-recent
|
|
38
|
+
* `last_seen_at`), or `undefined` when none clears `minOverlap`. Already-promoted
|
|
39
|
+
* rows are ignored so a confirmed lesson is not reopened by a later induction.
|
|
40
|
+
*
|
|
41
|
+
* @param memberKey the candidate cluster's membership fingerprint
|
|
42
|
+
* (sorted member entryKeys joined by `|`).
|
|
43
|
+
* @param minOverlap Jaccard threshold in [0,1]; a candidate matches when
|
|
44
|
+
* |A∩B| / |A∪B| >= minOverlap.
|
|
45
|
+
*/
|
|
46
|
+
export function findMatchingRecombineHypothesis(db, input) {
|
|
47
|
+
const candidateMembers = new Set(input.memberKey.split("|").filter((m) => m.length > 0));
|
|
48
|
+
if (candidateMembers.size === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const rows = db
|
|
51
|
+
.prepare("SELECT * FROM recombine_hypotheses WHERE signature = ? AND promoted_at IS NULL ORDER BY last_seen_at DESC")
|
|
52
|
+
.all(input.signature);
|
|
53
|
+
let best;
|
|
54
|
+
let bestOverlap = -1;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const rowMembers = row.member_key.split("|").filter((m) => m.length > 0);
|
|
57
|
+
if (rowMembers.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
let intersection = 0;
|
|
60
|
+
for (const m of rowMembers) {
|
|
61
|
+
if (candidateMembers.has(m))
|
|
62
|
+
intersection += 1;
|
|
63
|
+
}
|
|
64
|
+
const union = candidateMembers.size + rowMembers.length - intersection;
|
|
65
|
+
const overlap = union === 0 ? 0 : intersection / union;
|
|
66
|
+
// rows are ordered last_seen_at DESC, so a strict `>` keeps the most-recent
|
|
67
|
+
// row on ties.
|
|
68
|
+
if (overlap >= input.minOverlap && overlap > bestOverlap) {
|
|
69
|
+
best = row;
|
|
70
|
+
bestOverlap = overlap;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return best;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fetch a single recombine hypothesis row, or `undefined` when the ref has
|
|
77
|
+
* never been induced. Normalizes bun:sqlite null → undefined like
|
|
78
|
+
* {@link getExtractedSession}.
|
|
79
|
+
*/
|
|
80
|
+
export function getRecombineHypothesis(db, hypothesisRef) {
|
|
81
|
+
const row = db
|
|
82
|
+
.prepare("SELECT * FROM recombine_hypotheses WHERE hypothesis_ref = ?")
|
|
83
|
+
.get(hypothesisRef);
|
|
84
|
+
return row ?? undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mark a hypothesis promoted: stamp `promoted_at` and reset the consecutive
|
|
88
|
+
* count to 0, so it must re-accumulate a full confirmation streak before it can
|
|
89
|
+
* promote again. The `promoted_at` non-null state is the double-promotion guard.
|
|
90
|
+
*/
|
|
91
|
+
export function markRecombineHypothesisPromoted(db, hypothesisRef, promotedAt) {
|
|
92
|
+
db.prepare("UPDATE recombine_hypotheses SET promoted_at = ?, consecutive_count = 0 WHERE hypothesis_ref = ?").run(promotedAt, hypothesisRef);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* #658 — does any current-run cluster match this hypothesis row under the SAME
|
|
96
|
+
* signature + Jaccard-overlap rule used for re-induction? A match means the
|
|
97
|
+
* cluster genuinely re-formed this run (it was merely cap-displaced out of the
|
|
98
|
+
* processed top-`maxClustersPerRun` slice), so its streak must NOT be reset.
|
|
99
|
+
*/
|
|
100
|
+
function hypothesisMatchesAnyPresentCluster(row, presentClusters, minOverlap) {
|
|
101
|
+
const rowMembers = row.member_key.split("|").filter((m) => m.length > 0);
|
|
102
|
+
if (rowMembers.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
const rowSet = new Set(rowMembers);
|
|
105
|
+
for (const cluster of presentClusters) {
|
|
106
|
+
if (cluster.signature !== row.signature)
|
|
107
|
+
continue;
|
|
108
|
+
const clusterMembers = cluster.memberKey.split("|").filter((m) => m.length > 0);
|
|
109
|
+
if (clusterMembers.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
let intersection = 0;
|
|
112
|
+
for (const m of clusterMembers) {
|
|
113
|
+
if (rowSet.has(m))
|
|
114
|
+
intersection += 1;
|
|
115
|
+
}
|
|
116
|
+
const union = rowSet.size + clusterMembers.length - intersection;
|
|
117
|
+
const overlap = union === 0 ? 0 : intersection / union;
|
|
118
|
+
if (overlap >= minOverlap)
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Decay-to-zero every NON-promoted hypothesis NOT re-induced in the current run.
|
|
125
|
+
*
|
|
126
|
+
* A generalization that stops being supported by the corpus has lost its
|
|
127
|
+
* confirmation streak, so we hard-reset `consecutive_count` to 0 (the
|
|
128
|
+
* alternative — `count - 1` floored at 0 — tolerates a single noisy run but
|
|
129
|
+
* blurs the "consecutive" semantics; hard-reset is the conservative choice).
|
|
130
|
+
*
|
|
131
|
+
* Only rows whose `hypothesis_ref` is NOT in `seenRefs` AND whose `last_run` is
|
|
132
|
+
* NOT the current run are decayed. Already-promoted rows are left alone.
|
|
133
|
+
*
|
|
134
|
+
* #658 — CAP-AWARE decay. The recombine pass only re-inducts (and thus marks
|
|
135
|
+
* `seen`) the top-`maxClustersPerRun` clusters, but a cluster genuinely
|
|
136
|
+
* re-forms every run even when it is displaced below that cap. Resetting such a
|
|
137
|
+
* row treats a SCHEDULING miss as a SUBSTANCE miss and traps the hypothesis
|
|
138
|
+
* below `confirmThreshold` forever. When `opts.presentClusters` is supplied, a
|
|
139
|
+
* row is SPARED from decay if it Jaccard-matches any present cluster (same
|
|
140
|
+
* signature, overlap >= `opts.minOverlap`) — i.e. its cluster re-formed this run
|
|
141
|
+
* but was cap-displaced. This does NOT advance the streak (only re-induction in
|
|
142
|
+
* the processed slice does that, via {@link recordRecombineInduction}), so the
|
|
143
|
+
* recurrence bar for promotion is unchanged; it only stops the cap from
|
|
144
|
+
* manufacturing artificial misses. Omitting `presentClusters` preserves the
|
|
145
|
+
* pre-#658 hard-reset-after-one-miss behaviour exactly.
|
|
146
|
+
*
|
|
147
|
+
* Returns the number of rows reset.
|
|
148
|
+
*/
|
|
149
|
+
export function decayUnseenRecombineHypotheses(db, currentRun, seenRefs, opts) {
|
|
150
|
+
// #658 — when cap-aware sparing is requested, fold the cap-displaced rows into
|
|
151
|
+
// the "seen" exclusion set: the underlying reset SQL already protects every
|
|
152
|
+
// ref it is given, so sparing == treating a spared row exactly like a seen
|
|
153
|
+
// row for this sweep (its count is left untouched, never advanced).
|
|
154
|
+
let effectiveSeen = seenRefs;
|
|
155
|
+
if (opts && opts.presentClusters.length > 0) {
|
|
156
|
+
const candidates = db
|
|
157
|
+
.prepare("SELECT hypothesis_ref, signature, member_key FROM recombine_hypotheses WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0")
|
|
158
|
+
.all(currentRun);
|
|
159
|
+
const seenSet = new Set(seenRefs);
|
|
160
|
+
for (const row of candidates) {
|
|
161
|
+
if (seenSet.has(row.hypothesis_ref))
|
|
162
|
+
continue;
|
|
163
|
+
if (hypothesisMatchesAnyPresentCluster(row, opts.presentClusters, opts.minOverlap)) {
|
|
164
|
+
seenSet.add(row.hypothesis_ref);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
effectiveSeen = [...seenSet];
|
|
168
|
+
}
|
|
169
|
+
return decayUnseenRecombineHypothesesInner(db, currentRun, effectiveSeen);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The raw reset sweep shared by the cap-aware wrapper above. Resets every
|
|
173
|
+
* non-promoted row from a prior run whose ref is NOT in `seenRefs`. Kept private
|
|
174
|
+
* so the param-ceiling chunking logic lives in one place.
|
|
175
|
+
*/
|
|
176
|
+
function decayUnseenRecombineHypothesesInner(db, currentRun, seenRefs) {
|
|
177
|
+
// Reset every eligible row, then exclude the seen refs in chunks to respect
|
|
178
|
+
// the ~999 SQLite param ceiling. With no seen refs we reset all non-promoted
|
|
179
|
+
// rows from prior runs in a single statement.
|
|
180
|
+
if (seenRefs.length === 0) {
|
|
181
|
+
const res = db
|
|
182
|
+
.prepare("UPDATE recombine_hypotheses SET consecutive_count = 0 WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0")
|
|
183
|
+
.run(currentRun);
|
|
184
|
+
return Number(res.changes);
|
|
185
|
+
}
|
|
186
|
+
// A single NOT IN keeps the exclusion atomic (a chunked NOT IN would let a ref
|
|
187
|
+
// excluded by one chunk still be reset by another chunk's statement). The
|
|
188
|
+
// recombine pass caps RE-INDUCED clusters at `maxClustersPerRun` (a handful) —
|
|
189
|
+
// but with #658 cap-aware sparing the caller folds every cap-displaced
|
|
190
|
+
// (present-but-unprocessed) hypothesis into `effectiveSeen` too, so on a large
|
|
191
|
+
// stash `seenRefs` here can carry MANY spared refs, not just the handful that
|
|
192
|
+
// were processed. We cap defensively at ~900 (under SQLite's ~999 param
|
|
193
|
+
// ceiling): if `effectiveSeen` somehow exceeds it we fall back to resetting all
|
|
194
|
+
// eligible rows — which re-introduces the cap-displacement trap for THAT run
|
|
195
|
+
// (spared rows get decayed because the NOT IN protection is dropped). That is a
|
|
196
|
+
// rare, bounded degradation; a stash with >900 simultaneously-spared
|
|
197
|
+
// hypotheses is far beyond current scale.
|
|
198
|
+
if (seenRefs.length > 900) {
|
|
199
|
+
const res = db
|
|
200
|
+
.prepare("UPDATE recombine_hypotheses SET consecutive_count = 0 WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0")
|
|
201
|
+
.run(currentRun);
|
|
202
|
+
return Number(res.changes);
|
|
203
|
+
}
|
|
204
|
+
const placeholders = seenRefs.map(() => "?").join(",");
|
|
205
|
+
const res = db
|
|
206
|
+
.prepare(`UPDATE recombine_hypotheses SET consecutive_count = 0
|
|
207
|
+
WHERE promoted_at IS NULL
|
|
208
|
+
AND (last_run IS NULL OR last_run != ?)
|
|
209
|
+
AND consecutive_count != 0
|
|
210
|
+
AND hypothesis_ref NOT IN (${placeholders})`)
|
|
211
|
+
.run(currentRun, ...seenRefs);
|
|
212
|
+
return Number(res.changes);
|
|
213
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Upsert a task history row.
|
|
6
|
+
*/
|
|
7
|
+
export function upsertTaskHistory(db, row) {
|
|
8
|
+
// INSERT OR IGNORE: if a run with the same (task_id, started_at) was already
|
|
9
|
+
// imported (e.g. by the migration script), skip it silently.
|
|
10
|
+
db.prepare(`
|
|
11
|
+
INSERT OR IGNORE INTO task_history
|
|
12
|
+
(task_id, status, started_at, completed_at, failed_at, log_path,
|
|
13
|
+
target_kind, target_ref, metadata_json)
|
|
14
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
15
|
+
`).run(row.task_id, row.status, row.started_at, row.completed_at ?? null, row.failed_at ?? null, row.log_path ?? null, row.target_kind ?? null, row.target_ref ?? null, row.metadata_json);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Look up a task history row by task_id. Returns undefined when not found.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Return the most recent run for a given task_id, or undefined if no runs exist.
|
|
22
|
+
*/
|
|
23
|
+
export function getTaskHistory(db, taskId) {
|
|
24
|
+
return db
|
|
25
|
+
.prepare(`SELECT id, task_id, status, started_at, completed_at, failed_at, log_path,
|
|
26
|
+
target_kind, target_ref, metadata_json
|
|
27
|
+
FROM task_history WHERE task_id = ? ORDER BY started_at DESC LIMIT 1`)
|
|
28
|
+
.get(taskId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Return all runs for a given task_id, newest first.
|
|
32
|
+
*/
|
|
33
|
+
export function getTaskHistoryRuns(db, taskId, limit = 50) {
|
|
34
|
+
return db
|
|
35
|
+
.prepare(`SELECT id, task_id, status, started_at, completed_at, failed_at, log_path,
|
|
36
|
+
target_kind, target_ref, metadata_json
|
|
37
|
+
FROM task_history WHERE task_id = ? ORDER BY started_at DESC LIMIT ?`)
|
|
38
|
+
.all(taskId, limit);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Query task history rows by started_at range and/or status.
|
|
42
|
+
*/
|
|
43
|
+
export function queryTaskHistory(db, options = {}) {
|
|
44
|
+
const conditions = [];
|
|
45
|
+
const params = [];
|
|
46
|
+
if (options.since) {
|
|
47
|
+
conditions.push("started_at >= ?");
|
|
48
|
+
params.push(options.since);
|
|
49
|
+
}
|
|
50
|
+
if (options.until) {
|
|
51
|
+
conditions.push("started_at <= ?");
|
|
52
|
+
params.push(options.until);
|
|
53
|
+
}
|
|
54
|
+
if (options.status) {
|
|
55
|
+
conditions.push("status = ?");
|
|
56
|
+
params.push(options.status);
|
|
57
|
+
}
|
|
58
|
+
if (options.targetKind) {
|
|
59
|
+
conditions.push("target_kind = ?");
|
|
60
|
+
params.push(options.targetKind);
|
|
61
|
+
}
|
|
62
|
+
if (options.targetRef) {
|
|
63
|
+
conditions.push("target_ref = ?");
|
|
64
|
+
params.push(options.targetRef);
|
|
65
|
+
}
|
|
66
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
67
|
+
return db
|
|
68
|
+
.prepare(`SELECT task_id, status, started_at, completed_at, failed_at, log_path,
|
|
69
|
+
target_kind, target_ref, metadata_json
|
|
70
|
+
FROM task_history ${where} ORDER BY started_at DESC`)
|
|
71
|
+
.all(...params);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Read COMPLETED `akm-improve` task_history runs whose `started_at` falls in
|
|
75
|
+
* `[since, until)` (or `started_at >= since` when `until` is omitted), ordered
|
|
76
|
+
* oldest-first by `started_at`. Only rows with a non-null `completed_at` are
|
|
77
|
+
* returned (in-flight runs are excluded). The `task_id = 'akm-improve'`
|
|
78
|
+
* predicate is fixed because the only caller (commands/health.ts
|
|
79
|
+
* `loadTaskIntervals`) builds wall-time intervals for the improve cron task.
|
|
80
|
+
*
|
|
81
|
+
* Owns the SQL formerly inlined in commands/health.ts. Note the bound is
|
|
82
|
+
* EXCLUSIVE on the upper end (`started_at < ?`) — callers pass an already
|
|
83
|
+
* widened window; this helper does not widen.
|
|
84
|
+
*
|
|
85
|
+
* Connection-lifetime rule (WS5): `.all()` materializes a plain array before
|
|
86
|
+
* returning.
|
|
87
|
+
*/
|
|
88
|
+
export function queryCompletedTaskIntervals(db, since, until) {
|
|
89
|
+
const sql = until
|
|
90
|
+
? "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND started_at < ? AND completed_at IS NOT NULL ORDER BY started_at"
|
|
91
|
+
: "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND completed_at IS NOT NULL ORDER BY started_at";
|
|
92
|
+
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
93
|
+
}
|
|
@@ -57,8 +57,8 @@ export function resolveJournalMode(raw) {
|
|
|
57
57
|
* `process.env.AKM_SQLITE_JOURNAL_MODE`. Read at call time (per open) so tests
|
|
58
58
|
* that set the env per-case see the right value and we avoid stale-env flakes.
|
|
59
59
|
*/
|
|
60
|
-
export function resolveConfiguredJournalMode() {
|
|
61
|
-
return resolveJournalMode(
|
|
60
|
+
export function resolveConfiguredJournalMode(env = process.env) {
|
|
61
|
+
return resolveJournalMode(env.AKM_SQLITE_JOURNAL_MODE);
|
|
62
62
|
}
|
|
63
63
|
function warnInvalidJournalModeOnce(raw) {
|
|
64
64
|
if (warnedInvalid)
|
|
@@ -111,7 +111,7 @@ export function isNetworkFilesystem(fsType) {
|
|
|
111
111
|
* WAL path emits no `synchronous` pragma, exactly as before.
|
|
112
112
|
*/
|
|
113
113
|
export function applyStandardPragmas(db, opts = {}) {
|
|
114
|
-
let mode = resolveConfiguredJournalMode();
|
|
114
|
+
let mode = resolveConfiguredJournalMode(opts.env);
|
|
115
115
|
// Network-FS fallback only fires for the WAL default and only when we have a
|
|
116
116
|
// directory to probe. An explicitly-requested DELETE/TRUNCATE is never
|
|
117
117
|
// overridden, and a failed/unsupported probe (undefined) keeps WAL.
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
import { CRON_BACKEND } from "./cron.js";
|
|
5
5
|
import { LAUNCHD_BACKEND } from "./launchd.js";
|
|
6
6
|
import { SCHTASKS_BACKEND } from "./schtasks.js";
|
|
7
|
+
let backendsOverrides;
|
|
8
|
+
/** TEST-ONLY. Swap backend selection; pass undefined to restore the real implementations. */
|
|
9
|
+
export function _setBackendsForTests(fakes) {
|
|
10
|
+
backendsOverrides = fakes;
|
|
11
|
+
}
|
|
7
12
|
export function selectBackend(options = {}) {
|
|
13
|
+
if (backendsOverrides?.selectBackend)
|
|
14
|
+
return backendsOverrides.selectBackend(options);
|
|
8
15
|
const platform = options.platform ?? process.platform;
|
|
9
16
|
switch (platform) {
|
|
10
17
|
case "win32":
|
|
@@ -16,6 +23,8 @@ export function selectBackend(options = {}) {
|
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
export function backendNameForPlatform(platform = process.platform) {
|
|
26
|
+
if (backendsOverrides?.backendNameForPlatform)
|
|
27
|
+
return backendsOverrides.backendNameForPlatform(platform);
|
|
19
28
|
if (platform === "win32")
|
|
20
29
|
return "schtasks";
|
|
21
30
|
if (platform === "darwin")
|
package/dist/tasks/runner.js
CHANGED
|
@@ -33,13 +33,14 @@ import { loadConfig } from "../core/config/config.js";
|
|
|
33
33
|
import { NotFoundError, rethrowIfTestIsolationError } from "../core/errors.js";
|
|
34
34
|
import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../core/logs-db.js";
|
|
35
35
|
import { getTaskLogDir } from "../core/paths.js";
|
|
36
|
-
import {
|
|
36
|
+
import { withStateDb } from "../core/state-db.js";
|
|
37
37
|
import { error } from "../core/warn.js";
|
|
38
38
|
import { requireAgentProfile, runAgent } from "../integrations/agent/index.js";
|
|
39
39
|
import { resolveProcessAgentProfile } from "../integrations/agent/config.js";
|
|
40
40
|
import { resolveRunner } from "../integrations/agent/runner.js";
|
|
41
41
|
import { spawn } from "../runtime.js";
|
|
42
42
|
import { resolveAssetPath } from "../sources/resolve.js";
|
|
43
|
+
import { getTaskHistory, queryTaskHistory, upsertTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
43
44
|
import { startWorkflowRun } from "../workflows/runtime/runs.js";
|
|
44
45
|
import { parseTaskDocument } from "./parser.js";
|
|
45
46
|
export async function runTask(id, options = {}) {
|
|
@@ -131,6 +132,11 @@ async function runCommandTask(input) {
|
|
|
131
132
|
stdout: "pipe",
|
|
132
133
|
stderr: "pipe",
|
|
133
134
|
cwd: process.env.HOME ?? "/tmp",
|
|
135
|
+
// Stamp task-runner provenance so any akm invocation in the command tree
|
|
136
|
+
// records usage events as machine traffic, not user demand (DRIFT-6).
|
|
137
|
+
// A more specific stamp already in the environment (e.g. improve's
|
|
138
|
+
// AKM_EVENT_SOURCE=improve on its child spawns) still wins in children.
|
|
139
|
+
env: { ...process.env, AKM_EVENT_SOURCE: process.env.AKM_EVENT_SOURCE ?? "task" },
|
|
134
140
|
});
|
|
135
141
|
let timer;
|
|
136
142
|
let timedOut = false;
|
|
@@ -351,6 +357,10 @@ async function runPromptTask(input) {
|
|
|
351
357
|
timeoutMs: agentTimeoutMs,
|
|
352
358
|
cwd: stashDir,
|
|
353
359
|
...agentOptions,
|
|
360
|
+
// Stamp task-runner provenance for any akm invocation the agent makes
|
|
361
|
+
// (DRIFT-6: agent-task traffic must not be recorded as user demand).
|
|
362
|
+
// Caller-supplied env still wins on conflicts.
|
|
363
|
+
env: { AKM_EVENT_SOURCE: "task", ...agentOptions?.env },
|
|
354
364
|
});
|
|
355
365
|
const finishedAt = now();
|
|
356
366
|
const log = renderPromptLog({ task, profileName: profile.name, result });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"sweep:tmp": "bun scripts/sweep-test-tmp.ts",
|
|
59
59
|
"test": "bash scripts/test-unit.sh",
|
|
60
60
|
"test:unit": "bash scripts/test-unit.sh",
|
|
61
|
-
"test:integration": "bun run sweep:tmp && bun test --
|
|
61
|
+
"test:integration": "bun run sweep:tmp && bun test --timeout=30000 ./tests/integration",
|
|
62
62
|
"test:node-smoke": "bun scripts/node-smoke.ts",
|
|
63
63
|
"test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
|
|
64
64
|
"test:time": "bun scripts/test-timing-report.ts",
|