@gr8ful/spf 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -9
- package/assets/skill/references/config.md +12 -5
- package/assets/skill/references/observability.md +57 -12
- package/assets/templates/ts-opencode.spf.config.yaml +54 -0
- package/dist/chains/index.js +1 -1
- package/dist/chains/simple_sdlc.d.ts +2 -2
- package/dist/chains/simple_sdlc.js +13 -13
- package/dist/chains/steps.d.ts +2 -2
- package/dist/chains/steps.js +35 -19
- package/dist/cli/commands/abort.d.ts +1 -1
- package/dist/cli/commands/abort.js +30 -3
- package/dist/cli/commands/doctor.js +121 -8
- package/dist/cli/commands/estimate.js +3 -3
- package/dist/cli/commands/events.js +4 -4
- package/dist/cli/commands/fanout.js +93 -21
- package/dist/cli/commands/loop.js +31 -32
- package/dist/cli/commands/migrate.js +8 -1
- package/dist/cli/commands/phases.js +2 -2
- package/dist/cli/commands/sessions.js +2 -2
- package/dist/cli/commands/trace.d.ts +28 -8
- package/dist/cli/commands/trace.js +28 -15
- package/dist/cli/commands/ui.js +15 -5
- package/dist/cli/commands/watch.js +91 -30
- package/dist/cli/index.js +3 -1
- package/dist/cli/interview.d.ts +1 -0
- package/dist/cli/interview.js +95 -5
- package/dist/core/agent_opencode.d.ts +247 -0
- package/dist/core/agent_opencode.js +590 -0
- package/dist/core/agents.d.ts +12 -12
- package/dist/core/agents.js +113 -46
- package/dist/core/console.d.ts +12 -12
- package/dist/core/console.js +25 -25
- package/dist/core/data_types.d.ts +356 -15
- package/dist/core/data_types.js +180 -7
- package/dist/core/fanout.d.ts +1 -1
- package/dist/core/fanout.js +1 -1
- package/dist/core/gates.js +14 -1
- package/dist/core/issues/github_provider.d.ts +66 -2
- package/dist/core/issues/github_provider.js +161 -2
- package/dist/core/issues/jira_provider.d.ts +50 -9
- package/dist/core/issues/jira_provider.js +62 -2
- package/dist/core/paths.d.ts +41 -4
- package/dist/core/paths.js +32 -3
- package/dist/core/quality.d.ts +7 -7
- package/dist/core/quality.js +16 -10
- package/dist/core/refine.js +2 -2
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +39 -27
- package/dist/core/session.d.ts +2 -2
- package/dist/core/session.js +39 -18
- package/dist/core/sqlite.d.ts +14 -7
- package/dist/core/sqlite.js +14 -7
- package/dist/core/trace_db.d.ts +118 -0
- package/dist/core/trace_db.js +278 -0
- package/dist/core/tracer.d.ts +64 -34
- package/dist/core/tracer.js +141 -69
- package/dist/core/watch.d.ts +4 -4
- package/dist/core/watch.js +2 -2
- package/dist/ui/server/app.js +10 -10
- package/dist/ui/server/db.d.ts +89 -21
- package/dist/ui/server/db.js +235 -99
- package/dist/ui/server/serve.d.ts +5 -1
- package/dist/ui/server/serve.js +4 -5
- package/package.json +1 -1
- package/web/assets/index-CQ3k1Y1-.css +1 -0
- package/web/assets/index-CU8tom6S.js +21 -0
- package/web/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
- package/web/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
- package/web/assets/overpass-latin-700-normal-CQX2QTgM.woff2 +0 -0
- package/web/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
- package/web/assets/overpass-mono-latin-700-normal-D6nRBrbd.woff2 +0 -0
- package/web/index.html +33 -2
- package/web/logo.svg +4 -4
- package/web/assets/index-C7nF068F.css +0 -1
- package/web/assets/index-mzSArcnQ.js +0 -11
- package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
- package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
|
@@ -18,13 +18,22 @@
|
|
|
18
18
|
* one paragraph of plain text, nothing richer.
|
|
19
19
|
*
|
|
20
20
|
* State is modeled as Jira labels (`<prefix>:ready`, etc.), mirroring
|
|
21
|
-
* `github_provider.ts` exactly
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* `github_provider.ts` exactly — labels are spf's ACTUAL state machine and
|
|
22
|
+
* always get written, unconditionally. One caveat, verified against
|
|
23
|
+
* Atlassian's own docs: colons ARE a legal label character and JQL matches
|
|
24
|
+
* on them fine, they just don't show up in Jira's label autocomplete UI —
|
|
25
|
+
* cosmetic only, not a functional issue.
|
|
26
|
+
*
|
|
27
|
+
* Native workflow status is a separate, OPTIONAL, best-effort layer on top
|
|
28
|
+
* (`syncStatus()`), driven entirely by the configured `statusMap` — empty
|
|
29
|
+
* by default, so an existing config's behavior is unchanged. It's optional
|
|
30
|
+
* rather than baked into every `transition()` call unconditionally because
|
|
31
|
+
* Jira workflows vary by project/scheme (status names, which transitions
|
|
32
|
+
* are reachable from where) in a way labels never do; a project that wants
|
|
33
|
+
* its board's Status column to move when spf changes a label opts in with
|
|
34
|
+
* `watch.jira.status_map`, and a misconfigured or unreachable entry there
|
|
35
|
+
* degrades to a logged warning, never a thrown error — a status-sync miss
|
|
36
|
+
* must never block the label update `spf watch` actually depends on.
|
|
28
37
|
*
|
|
29
38
|
* `ensureLabels()` is a no-op that reports the labels this run will use:
|
|
30
39
|
* Jira labels are freeform strings with no color/description registry to
|
|
@@ -58,7 +67,7 @@
|
|
|
58
67
|
* Jira API error at publish time — a genuine platform difference, not
|
|
59
68
|
* something this file tries to paper over.
|
|
60
69
|
*/
|
|
61
|
-
import type { JiraIssueTypeMap } from "../data_types.ts";
|
|
70
|
+
import type { JiraIssueTypeMap, JiraStatusMap } from "../data_types.ts";
|
|
62
71
|
import type { EnsureLabelsResult, Issue, IssueAuthoringKind, IssueAuthoringProvider, IssueComment, IssueProvider, WatchMarker, WatchState } from "./provider.ts";
|
|
63
72
|
export declare class JiraProvider implements IssueProvider, IssueAuthoringProvider {
|
|
64
73
|
private readonly baseUrl;
|
|
@@ -67,8 +76,9 @@ export declare class JiraProvider implements IssueProvider, IssueAuthoringProvid
|
|
|
67
76
|
private readonly email;
|
|
68
77
|
private readonly apiToken;
|
|
69
78
|
private readonly issueTypes;
|
|
79
|
+
private readonly statusMap;
|
|
70
80
|
constructor(baseUrl: string, // e.g. "https://your-domain.atlassian.net", no trailing slash
|
|
71
|
-
projectKey: string, labelPrefix: string, email: string, apiToken: string, issueTypes: JiraIssueTypeMap);
|
|
81
|
+
projectKey: string, labelPrefix: string, email: string, apiToken: string, issueTypes: JiraIssueTypeMap, statusMap?: JiraStatusMap);
|
|
72
82
|
private authHeader;
|
|
73
83
|
private jira;
|
|
74
84
|
private label;
|
|
@@ -145,6 +155,37 @@ export declare class JiraProvider implements IssueProvider, IssueAuthoringProvid
|
|
|
145
155
|
to?: WatchState;
|
|
146
156
|
}): Promise<boolean>;
|
|
147
157
|
transition(issue: Issue, to: WatchState, detail?: string): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Best-effort native workflow-status sync — a no-op unless `statusMap`
|
|
160
|
+
* configures a name for `to`. Looked up per call, not cached: the
|
|
161
|
+
* available transitions are FROM-status-dependent, so the same target
|
|
162
|
+
* status can need a different transition id depending where the issue
|
|
163
|
+
* currently sits, and this same issue's status keeps moving across calls
|
|
164
|
+
* as it advances through the build lane. Every failure mode here
|
|
165
|
+
* (unconfigured, unreachable, or a rejected transition) is logged and
|
|
166
|
+
* swallowed, never thrown — see this file's module comment on why a
|
|
167
|
+
* status-sync miss must never break the label update callers depend on.
|
|
168
|
+
*/
|
|
169
|
+
private syncStatus;
|
|
170
|
+
/**
|
|
171
|
+
* Read-only validation of the configured `status_map` against this
|
|
172
|
+
* project's real statuses — what `spf watch init` and `spf watch`'s own
|
|
173
|
+
* startup check should call to catch a misnamed status before an
|
|
174
|
+
* unattended run silently no-ops its status sync every time, the same
|
|
175
|
+
* role `validateIssueTypes()` plays for `issue_types`.
|
|
176
|
+
*
|
|
177
|
+
* Uses `/rest/api/3/project/{key}/statuses`, which groups statuses by
|
|
178
|
+
* issue type — Jira workflows can differ per issue type within one
|
|
179
|
+
* project. A configured name is "exists" if ANY issue type in the project
|
|
180
|
+
* has it: good enough to catch a typo, not a guarantee every issue type
|
|
181
|
+
* this map is used against can actually reach it (that's what
|
|
182
|
+
* `syncStatus()`'s own per-call transition lookup is for).
|
|
183
|
+
*/
|
|
184
|
+
validateStatusMap(): Promise<Array<{
|
|
185
|
+
state: string;
|
|
186
|
+
jiraStatus: string;
|
|
187
|
+
exists: boolean;
|
|
188
|
+
}>>;
|
|
148
189
|
comment(issue: Issue, body: string): Promise<void>;
|
|
149
190
|
/** The single fetch every comment-reading method (`findMarkerComment`, `listComments`) builds on. */
|
|
150
191
|
private fetchComments;
|
|
@@ -52,14 +52,16 @@ export class JiraProvider {
|
|
|
52
52
|
email;
|
|
53
53
|
apiToken;
|
|
54
54
|
issueTypes;
|
|
55
|
+
statusMap;
|
|
55
56
|
constructor(baseUrl, // e.g. "https://your-domain.atlassian.net", no trailing slash
|
|
56
|
-
projectKey, labelPrefix, email, apiToken, issueTypes) {
|
|
57
|
+
projectKey, labelPrefix, email, apiToken, issueTypes, statusMap = {}) {
|
|
57
58
|
this.baseUrl = baseUrl;
|
|
58
59
|
this.projectKey = projectKey;
|
|
59
60
|
this.labelPrefix = labelPrefix;
|
|
60
61
|
this.email = email;
|
|
61
62
|
this.apiToken = apiToken;
|
|
62
63
|
this.issueTypes = issueTypes;
|
|
64
|
+
this.statusMap = statusMap;
|
|
63
65
|
}
|
|
64
66
|
authHeader() {
|
|
65
67
|
return `Basic ${Buffer.from(`${this.email}:${this.apiToken}`).toString("base64")}`;
|
|
@@ -213,8 +215,9 @@ export class JiraProvider {
|
|
|
213
215
|
return Object.entries(this.issueTypes).map(([kind, jiraType]) => ({ kind, jiraType, exists: available.has(jiraType) }));
|
|
214
216
|
}
|
|
215
217
|
async claim(issue, opts) {
|
|
218
|
+
const toState = opts?.to ?? "working";
|
|
216
219
|
const from = this.label(opts?.from ?? "ready");
|
|
217
|
-
const to = this.label(
|
|
220
|
+
const to = this.label(toState);
|
|
218
221
|
const next = issue.labels.filter((l) => l !== from);
|
|
219
222
|
next.push(to);
|
|
220
223
|
await this.jira(`/rest/api/3/issue/${issue.id}`, { method: "PUT", body: JSON.stringify({ fields: { labels: next } }) });
|
|
@@ -226,15 +229,72 @@ export class JiraProvider {
|
|
|
226
229
|
revert.push(from);
|
|
227
230
|
await this.jira(`/rest/api/3/issue/${issue.id}`, { method: "PUT", body: JSON.stringify({ fields: { labels: revert } }) }).catch(() => undefined);
|
|
228
231
|
}
|
|
232
|
+
else {
|
|
233
|
+
await this.syncStatus(issue, toState);
|
|
234
|
+
}
|
|
229
235
|
return claimed;
|
|
230
236
|
}
|
|
231
237
|
async transition(issue, to, detail) {
|
|
232
238
|
const next = issue.labels.filter((l) => !STATES.some((s) => this.label(s) === l));
|
|
233
239
|
next.push(this.label(to));
|
|
234
240
|
await this.jira(`/rest/api/3/issue/${issue.id}`, { method: "PUT", body: JSON.stringify({ fields: { labels: next } }) });
|
|
241
|
+
await this.syncStatus(issue, to);
|
|
235
242
|
if (detail)
|
|
236
243
|
await this.comment(issue, detail);
|
|
237
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Best-effort native workflow-status sync — a no-op unless `statusMap`
|
|
247
|
+
* configures a name for `to`. Looked up per call, not cached: the
|
|
248
|
+
* available transitions are FROM-status-dependent, so the same target
|
|
249
|
+
* status can need a different transition id depending where the issue
|
|
250
|
+
* currently sits, and this same issue's status keeps moving across calls
|
|
251
|
+
* as it advances through the build lane. Every failure mode here
|
|
252
|
+
* (unconfigured, unreachable, or a rejected transition) is logged and
|
|
253
|
+
* swallowed, never thrown — see this file's module comment on why a
|
|
254
|
+
* status-sync miss must never break the label update callers depend on.
|
|
255
|
+
*/
|
|
256
|
+
async syncStatus(issue, to) {
|
|
257
|
+
const statusName = this.statusMap[to];
|
|
258
|
+
if (!statusName)
|
|
259
|
+
return;
|
|
260
|
+
let transitions;
|
|
261
|
+
try {
|
|
262
|
+
({ transitions } = await this.jira(`/rest/api/3/issue/${issue.id}/transitions`));
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
console.error(`spf watch: ${issue.id} — couldn't fetch available Jira transitions to sync status ${JSON.stringify(statusName)}; label already updated — ${err instanceof Error ? err.message : String(err)}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const match = transitions.find((t) => t.to.name === statusName);
|
|
269
|
+
if (!match) {
|
|
270
|
+
console.error(`spf watch: ${issue.id} has no available transition to Jira status ${JSON.stringify(statusName)} (watch.jira.status_map.${to}) from its current status — skipping status sync, label already updated`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await this.jira(`/rest/api/3/issue/${issue.id}/transitions`, { method: "POST", body: JSON.stringify({ transition: { id: match.id } }) }).catch((err) => {
|
|
274
|
+
console.error(`spf watch: ${issue.id} — Jira transition to ${JSON.stringify(statusName)} failed; label already updated — ${err instanceof Error ? err.message : String(err)}`);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Read-only validation of the configured `status_map` against this
|
|
279
|
+
* project's real statuses — what `spf watch init` and `spf watch`'s own
|
|
280
|
+
* startup check should call to catch a misnamed status before an
|
|
281
|
+
* unattended run silently no-ops its status sync every time, the same
|
|
282
|
+
* role `validateIssueTypes()` plays for `issue_types`.
|
|
283
|
+
*
|
|
284
|
+
* Uses `/rest/api/3/project/{key}/statuses`, which groups statuses by
|
|
285
|
+
* issue type — Jira workflows can differ per issue type within one
|
|
286
|
+
* project. A configured name is "exists" if ANY issue type in the project
|
|
287
|
+
* has it: good enough to catch a typo, not a guarantee every issue type
|
|
288
|
+
* this map is used against can actually reach it (that's what
|
|
289
|
+
* `syncStatus()`'s own per-call transition lookup is for).
|
|
290
|
+
*/
|
|
291
|
+
async validateStatusMap() {
|
|
292
|
+
const result = await this.jira(`/rest/api/3/project/${encodeURIComponent(this.projectKey)}/statuses`);
|
|
293
|
+
const available = new Set(result.flatMap((t) => t.statuses.map((s) => s.name)));
|
|
294
|
+
return Object.entries(this.statusMap)
|
|
295
|
+
.filter((entry) => Boolean(entry[1]))
|
|
296
|
+
.map(([state, jiraStatus]) => ({ state, jiraStatus, exists: available.has(jiraStatus) }));
|
|
297
|
+
}
|
|
238
298
|
async comment(issue, body) {
|
|
239
299
|
await this.jira(`/rest/api/3/issue/${issue.id}/comment`, { method: "POST", body: JSON.stringify({ body: toAdf(body) }) });
|
|
240
300
|
}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* levels below the package root, so the same relative path resolves either
|
|
17
17
|
* way), with `assets/` a direct child of it.
|
|
18
18
|
*/
|
|
19
|
+
import { type NormalizedObservabilityDb, type ObservabilityDbInput } from "./data_types.ts";
|
|
19
20
|
export declare const PACKAGE_ROOT: string;
|
|
20
21
|
export declare const ASSETS_DIR: string;
|
|
21
22
|
/** The prebuilt SPA — a sibling of dist/ and assets/, built once at publish time (see app/). */
|
|
@@ -54,9 +55,22 @@ export interface DataPaths {
|
|
|
54
55
|
data_dir: string;
|
|
55
56
|
/** Repo-root-relative, forward-slash — for matching against git's own path output. */
|
|
56
57
|
data_dir_rel: string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The normalized `observability.db` descriptor — `core/trace_db.ts`'s
|
|
60
|
+
* `createTraceDb()` (called by both `Tracer` and `SfDb`) is built against
|
|
61
|
+
* exactly this shape. For `kind: "sqlite"`, `path` here is already
|
|
62
|
+
* resolved ABSOLUTE (byte-for-byte the same value as `db_path` below); for
|
|
63
|
+
* `kind: "d1"` there is no local path at all.
|
|
64
|
+
*/
|
|
65
|
+
db: NormalizedObservabilityDb;
|
|
66
|
+
/** Absolute local sqlite path, or `null` for a `kind: "d1"` config — there is no local file to open. */
|
|
67
|
+
db_path: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Absolute. Anchored directly off `data_dir` (NOT off `db_path`) — session
|
|
70
|
+
* JSONL/envelope artifact files always live on the local filesystem
|
|
71
|
+
* regardless of where the queryable trace mirror (`db` above) lives, so
|
|
72
|
+
* this must resolve even when `db_path` is `null` for a d1-backed repo.
|
|
73
|
+
*/
|
|
60
74
|
sessions_dir: string;
|
|
61
75
|
}
|
|
62
76
|
/**
|
|
@@ -70,8 +84,31 @@ export interface DataPaths {
|
|
|
70
84
|
*
|
|
71
85
|
* Not otherwise side-effect-free: see `healBrokenDataDir` above, which every
|
|
72
86
|
* caller needs run before its own first `mkdirSync` against `data_dir`.
|
|
87
|
+
*
|
|
88
|
+
* `rawDb` accepts every form `ObservabilityConfigSchema.db` accepts (bare
|
|
89
|
+
* string, `{kind:"sqlite",...}`, `{kind:"d1",...}`) — normalized here via
|
|
90
|
+
* `resolveObservabilityDb` — so every existing call site
|
|
91
|
+
* (`resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db)`)
|
|
92
|
+
* keeps compiling and behaving unchanged with no call-site edit needed.
|
|
93
|
+
*
|
|
94
|
+
* For a `"sqlite"`-kind db, `db_path`/`db.path` resolve BYTE-FOR-BYTE
|
|
95
|
+
* identically to before this PR: `path.resolve(repo_root, path)`.
|
|
96
|
+
*
|
|
97
|
+
* For a `"d1"`-kind db there is no local file at all — `db_path` is `null`,
|
|
98
|
+
* and `db` carries the resolved `{kind:"d1", database_id, account_id_env,
|
|
99
|
+
* api_token_env}` for `core/trace_db.ts`'s `createTraceDb()` to open (PR 2:
|
|
100
|
+
* SPF #66). `sessions_dir` has no `db_path` to anchor off in this case, so it
|
|
101
|
+
* falls back to `data_dir/sessions` — session JSONL/envelope artifacts still
|
|
102
|
+
* always live on disk regardless of where the queryable trace mirror lives.
|
|
103
|
+
*
|
|
104
|
+
* For a `"sqlite"`-kind db, `sessions_dir` stays what it always was — ALWAYS
|
|
105
|
+
* a sibling of `db_path` (`dirname(db_path)/sessions`), never `data_dir`
|
|
106
|
+
* directly — `migrate.ts` and `SfDb` both depend on that: a repo whose db
|
|
107
|
+
* lives outside `data_dir` (`observability.db: elsewhere/custom.db` next to
|
|
108
|
+
* `defaults.data_dir: .spf/data`) still has its sessions found. Relocating
|
|
109
|
+
* `data_dir` alone must never orphan an existing db's session artifacts.
|
|
73
110
|
*/
|
|
74
|
-
export declare function resolveDataPaths(anchor: RepoAnchor, rawDataDir: string,
|
|
111
|
+
export declare function resolveDataPaths(anchor: RepoAnchor, rawDataDir: string, rawDb: ObservabilityDbInput): DataPaths;
|
|
75
112
|
/**
|
|
76
113
|
* Resolve a prompt ref (a `prompt_engineering.system`/`.user` value from
|
|
77
114
|
* config) to an absolute path, trying every root a ref could sensibly be
|
package/dist/core/paths.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, lstatSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { findRepoRoot } from "./git_helper.js";
|
|
22
|
+
import { resolveObservabilityDb } from "./data_types.js";
|
|
22
23
|
export const PACKAGE_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
23
24
|
export const ASSETS_DIR = path.join(PACKAGE_ROOT, "assets");
|
|
24
25
|
/** The prebuilt SPA — a sibling of dist/ and assets/, built once at publish time (see app/). */
|
|
@@ -121,14 +122,42 @@ function healBrokenDataDir(data_dir) {
|
|
|
121
122
|
*
|
|
122
123
|
* Not otherwise side-effect-free: see `healBrokenDataDir` above, which every
|
|
123
124
|
* caller needs run before its own first `mkdirSync` against `data_dir`.
|
|
125
|
+
*
|
|
126
|
+
* `rawDb` accepts every form `ObservabilityConfigSchema.db` accepts (bare
|
|
127
|
+
* string, `{kind:"sqlite",...}`, `{kind:"d1",...}`) — normalized here via
|
|
128
|
+
* `resolveObservabilityDb` — so every existing call site
|
|
129
|
+
* (`resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db)`)
|
|
130
|
+
* keeps compiling and behaving unchanged with no call-site edit needed.
|
|
131
|
+
*
|
|
132
|
+
* For a `"sqlite"`-kind db, `db_path`/`db.path` resolve BYTE-FOR-BYTE
|
|
133
|
+
* identically to before this PR: `path.resolve(repo_root, path)`.
|
|
134
|
+
*
|
|
135
|
+
* For a `"d1"`-kind db there is no local file at all — `db_path` is `null`,
|
|
136
|
+
* and `db` carries the resolved `{kind:"d1", database_id, account_id_env,
|
|
137
|
+
* api_token_env}` for `core/trace_db.ts`'s `createTraceDb()` to open (PR 2:
|
|
138
|
+
* SPF #66). `sessions_dir` has no `db_path` to anchor off in this case, so it
|
|
139
|
+
* falls back to `data_dir/sessions` — session JSONL/envelope artifacts still
|
|
140
|
+
* always live on disk regardless of where the queryable trace mirror lives.
|
|
141
|
+
*
|
|
142
|
+
* For a `"sqlite"`-kind db, `sessions_dir` stays what it always was — ALWAYS
|
|
143
|
+
* a sibling of `db_path` (`dirname(db_path)/sessions`), never `data_dir`
|
|
144
|
+
* directly — `migrate.ts` and `SfDb` both depend on that: a repo whose db
|
|
145
|
+
* lives outside `data_dir` (`observability.db: elsewhere/custom.db` next to
|
|
146
|
+
* `defaults.data_dir: .spf/data`) still has its sessions found. Relocating
|
|
147
|
+
* `data_dir` alone must never orphan an existing db's session artifacts.
|
|
124
148
|
*/
|
|
125
|
-
export function resolveDataPaths(anchor, rawDataDir,
|
|
149
|
+
export function resolveDataPaths(anchor, rawDataDir, rawDb) {
|
|
126
150
|
const data_dir = path.resolve(anchor.repo_root, rawDataDir);
|
|
127
151
|
healBrokenDataDir(data_dir);
|
|
128
152
|
const data_dir_rel = path.relative(anchor.repo_root, data_dir).split(path.sep).join("/");
|
|
129
|
-
const
|
|
153
|
+
const normalized = resolveObservabilityDb(rawDb);
|
|
154
|
+
if (normalized.kind === "d1") {
|
|
155
|
+
const sessions_dir = path.resolve(data_dir, "sessions");
|
|
156
|
+
return { data_dir, data_dir_rel, db: normalized, db_path: null, sessions_dir };
|
|
157
|
+
}
|
|
158
|
+
const db_path = path.resolve(anchor.repo_root, normalized.path);
|
|
130
159
|
const sessions_dir = path.resolve(path.dirname(db_path), "sessions");
|
|
131
|
-
return { data_dir, data_dir_rel, db_path, sessions_dir };
|
|
160
|
+
return { data_dir, data_dir_rel, db: { kind: "sqlite", path: db_path }, db_path, sessions_dir };
|
|
132
161
|
}
|
|
133
162
|
/**
|
|
134
163
|
* Resolve a prompt ref (a `prompt_engineering.system`/`.user` value from
|
package/dist/core/quality.d.ts
CHANGED
|
@@ -22,10 +22,10 @@ export interface RunLike {
|
|
|
22
22
|
context_handoff_dir: string;
|
|
23
23
|
repo_root: string;
|
|
24
24
|
console: {
|
|
25
|
-
note: (message: string) => void
|
|
25
|
+
note: (message: string) => Promise<void>;
|
|
26
26
|
};
|
|
27
27
|
tracer: {
|
|
28
|
-
event: (record: EventRecord) => string
|
|
28
|
+
event: (record: EventRecord) => Promise<string>;
|
|
29
29
|
};
|
|
30
30
|
adw_id: string;
|
|
31
31
|
}
|
|
@@ -41,7 +41,7 @@ export declare function resolveSuite(run: RunLike, suiteName: string): QualityCh
|
|
|
41
41
|
* QualityNotConfigured (via resolveSuite) if the suite or any of its checks
|
|
42
42
|
* isn't in the config — before any check runs, let alone any agent spawns.
|
|
43
43
|
*/
|
|
44
|
-
export declare function runSuite(run: RunLike, suiteName: string): QualityResult
|
|
44
|
+
export declare function runSuite(run: RunLike, suiteName: string): Promise<QualityResult>;
|
|
45
45
|
/**
|
|
46
46
|
* The `test` suite alone — the deterministic test phase most chains use.
|
|
47
47
|
*
|
|
@@ -50,9 +50,9 @@ export declare function runSuite(run: RunLike, suiteName: string): QualityResult
|
|
|
50
50
|
* subprocess already knows; the repair loop is unchanged, because a failure
|
|
51
51
|
* still reaches the builder through `asEnvelope` below.
|
|
52
52
|
*/
|
|
53
|
-
export declare function runTests(run: RunLike): QualityResult
|
|
53
|
+
export declare function runTests(run: RunLike): Promise<QualityResult>;
|
|
54
54
|
/** Every configured check, across every configured suite's union — the `all` suite. */
|
|
55
|
-
export declare function runQuality(run: RunLike): QualityResult
|
|
55
|
+
export declare function runQuality(run: RunLike): Promise<QualityResult>;
|
|
56
56
|
/**
|
|
57
57
|
* Log a deterministic block's verdict — the same shape every chain uses.
|
|
58
58
|
*
|
|
@@ -60,8 +60,8 @@ export declare function runQuality(run: RunLike): QualityResult;
|
|
|
60
60
|
* hand; one copy here instead of one per chain.
|
|
61
61
|
*/
|
|
62
62
|
export declare function record(ph: {
|
|
63
|
-
log: (payload: Record<string, unknown>) => void
|
|
64
|
-
}, result: QualityResult): void
|
|
63
|
+
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
64
|
+
}, result: QualityResult): Promise<void>;
|
|
65
65
|
/**
|
|
66
66
|
* Wrap a deterministic result so an agent can be handed it directly.
|
|
67
67
|
*
|
package/dist/core/quality.js
CHANGED
|
@@ -56,13 +56,13 @@ function shellJoin(argv) {
|
|
|
56
56
|
.map((arg) => (/[\s"'$`\\!*?[\](){}<>|&;#~]/.test(arg) ? `'${arg.replace(/'/g, `'\\''`)}'` : arg))
|
|
57
57
|
.join(" ");
|
|
58
58
|
}
|
|
59
|
-
function runCheck(spec, run) {
|
|
59
|
+
async function runCheck(spec, run) {
|
|
60
60
|
const phase = run.phases[run.phases.length - 1];
|
|
61
61
|
const outputDir = checkDir(run, spec.name);
|
|
62
62
|
const outputArtifact = path.join(outputDir, "command.log");
|
|
63
63
|
const command = shellJoin(spec.argv);
|
|
64
64
|
const env = operatorEnv(); // the engineer's own shell environment
|
|
65
|
-
run.console.note(`quality ${spec.name}: ${command}`);
|
|
65
|
+
await run.console.note(`quality ${spec.name}: ${command}`);
|
|
66
66
|
const startedAt = nowIso();
|
|
67
67
|
const clock = performance.now();
|
|
68
68
|
let stdout = "";
|
|
@@ -100,7 +100,7 @@ function runCheck(spec, run) {
|
|
|
100
100
|
writeFileSync(outputArtifact, `$ ${command}\nexit: ${returncode}\nduration_seconds: ${duration.toFixed(3)}\n` +
|
|
101
101
|
`\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}\n`);
|
|
102
102
|
const passed = returncode === 0;
|
|
103
|
-
run.tracer.event(makeEventRecord({
|
|
103
|
+
await run.tracer.event(makeEventRecord({
|
|
104
104
|
adw_id: run.adw_id,
|
|
105
105
|
phase_id: phase.phase_id,
|
|
106
106
|
type: "tool_call",
|
|
@@ -116,7 +116,7 @@ function runCheck(spec, run) {
|
|
|
116
116
|
started_at: startedAt,
|
|
117
117
|
ended_at: nowIso(),
|
|
118
118
|
}));
|
|
119
|
-
run.console.note(`quality ${spec.name}: ${passed ? "passed" : "failed"} (exit ${returncode}, ${duration.toFixed(1)}s)`);
|
|
119
|
+
await run.console.note(`quality ${spec.name}: ${passed ? "passed" : "failed"} (exit ${returncode}, ${duration.toFixed(1)}s)`);
|
|
120
120
|
return {
|
|
121
121
|
name: spec.name,
|
|
122
122
|
area: spec.area,
|
|
@@ -139,9 +139,15 @@ function runCheck(spec, run) {
|
|
|
139
139
|
* QualityNotConfigured (via resolveSuite) if the suite or any of its checks
|
|
140
140
|
* isn't in the config — before any check runs, let alone any agent spawns.
|
|
141
141
|
*/
|
|
142
|
-
export function runSuite(run, suiteName) {
|
|
142
|
+
export async function runSuite(run, suiteName) {
|
|
143
143
|
const specs = resolveSuite(run, suiteName);
|
|
144
|
-
|
|
144
|
+
// Sequential, not Promise.all: check output/duration/tool_call ordering has
|
|
145
|
+
// always been one-at-a-time (spawnSync itself was always sequential) — a
|
|
146
|
+
// parallel run would interleave `run.console.note`/`run.tracer.event`
|
|
147
|
+
// calls across checks with no ordering guarantee.
|
|
148
|
+
const checks = [];
|
|
149
|
+
for (const spec of specs)
|
|
150
|
+
checks.push(await runCheck(spec, run));
|
|
145
151
|
// A failure is the command, its exit code, and what it actually printed —
|
|
146
152
|
// everything a builder needs to repair without opening a log or being told
|
|
147
153
|
// what the error "means" by a parser that guessed.
|
|
@@ -163,11 +169,11 @@ export function runSuite(run, suiteName) {
|
|
|
163
169
|
* subprocess already knows; the repair loop is unchanged, because a failure
|
|
164
170
|
* still reaches the builder through `asEnvelope` below.
|
|
165
171
|
*/
|
|
166
|
-
export function runTests(run) {
|
|
172
|
+
export async function runTests(run) {
|
|
167
173
|
return runSuite(run, "test");
|
|
168
174
|
}
|
|
169
175
|
/** Every configured check, across every configured suite's union — the `all` suite. */
|
|
170
|
-
export function runQuality(run) {
|
|
176
|
+
export async function runQuality(run) {
|
|
171
177
|
return runSuite(run, "all");
|
|
172
178
|
}
|
|
173
179
|
/**
|
|
@@ -176,9 +182,9 @@ export function runQuality(run) {
|
|
|
176
182
|
* Every quality/test phase in every chain reported this same summary by
|
|
177
183
|
* hand; one copy here instead of one per chain.
|
|
178
184
|
*/
|
|
179
|
-
export function record(ph, result) {
|
|
185
|
+
export async function record(ph, result) {
|
|
180
186
|
const passed = result.checks.filter((c) => c.passed).length;
|
|
181
|
-
ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
|
|
187
|
+
await ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
|
|
182
188
|
}
|
|
183
189
|
/**
|
|
184
190
|
* Wrap a deterministic result so an agent can be handed it directly.
|
package/dist/core/refine.js
CHANGED
|
@@ -41,7 +41,7 @@ export function resolveAuthoringProvider(cfg) {
|
|
|
41
41
|
if (!email || !token) {
|
|
42
42
|
throw new Error('JIRA_EMAIL and JIRA_API_TOKEN must both be set — the refine lane needs an Atlassian account email plus an API token (id.atlassian.com -> Security -> API tokens)');
|
|
43
43
|
}
|
|
44
|
-
return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token, cfg.watch.jira.issue_types);
|
|
44
|
+
return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token, cfg.watch.jira.issue_types, cfg.watch.jira.status_map);
|
|
45
45
|
}
|
|
46
46
|
if (cfg.watch.issue_provider !== "github") {
|
|
47
47
|
throw new Error(`watch.issue_provider ${JSON.stringify(cfg.watch.issue_provider)} does not support issue authoring — the refine lane needs "github" or "jira"`);
|
|
@@ -61,7 +61,7 @@ export function resolveAuthoringProvider(cfg) {
|
|
|
61
61
|
if (!token) {
|
|
62
62
|
throw new Error('GITHUB_TOKEN is not set — the refine lane needs a classic PAT with "repo" scope (or "public_repo" for a public-only repo)');
|
|
63
63
|
}
|
|
64
|
-
return new GitHubProvider(repo, cfg.watch.label_prefix, token);
|
|
64
|
+
return new GitHubProvider(repo, cfg.watch.label_prefix, token, cfg.watch.github.project_number, cfg.watch.github.status_map);
|
|
65
65
|
}
|
|
66
66
|
function typeLabel(labelPrefix, kind) {
|
|
67
67
|
return `${labelPrefix}:type:${kind}`;
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -20,13 +20,19 @@ interface AgentMapEntry {
|
|
|
20
20
|
coding_agent: string;
|
|
21
21
|
}
|
|
22
22
|
export interface PhaseHandle {
|
|
23
|
-
log(payload: Record<string, unknown>): void
|
|
23
|
+
log(payload: Record<string, unknown>): Promise<void>;
|
|
24
24
|
call<T extends EnvelopeBase>(call: AgentCall<T>): Promise<T>;
|
|
25
25
|
}
|
|
26
26
|
export interface RunInit {
|
|
27
27
|
cfg: SFConfig;
|
|
28
28
|
adwId: string;
|
|
29
29
|
tracer: Tracer;
|
|
30
|
+
/**
|
|
31
|
+
* `tracer.maxPhaseSeq(adwId)`, resolved by the caller BEFORE constructing
|
|
32
|
+
* `Run` — a real trace-db read now (a network call for a D1-backed repo),
|
|
33
|
+
* and `Run`'s own constructor cannot be async. See `session.ts`'s `ensure()`.
|
|
34
|
+
*/
|
|
35
|
+
startSeq: number;
|
|
30
36
|
engineer: string;
|
|
31
37
|
/** Absolute. Resolved once, upstream, by paths.resolveAnchor(). */
|
|
32
38
|
repoRoot: string;
|
|
@@ -75,7 +81,7 @@ export declare class Run {
|
|
|
75
81
|
private agentMapPath;
|
|
76
82
|
constructor(init: RunInit);
|
|
77
83
|
saveAgentMap(agent: string, entry: AgentMapEntry): void;
|
|
78
|
-
addUsage(tokens: number, cost: number): void
|
|
84
|
+
addUsage(tokens: number, cost: number): Promise<void>;
|
|
79
85
|
phase<T>(params: PhaseParams, fn: (ph: PhaseHandle) => Promise<T>): Promise<T>;
|
|
80
86
|
/**
|
|
81
87
|
* Finalize the run and return its exit code. Call this exactly once.
|
|
@@ -86,6 +92,6 @@ export declare class Run {
|
|
|
86
92
|
* so the exit code, the session status, and the banner are decided
|
|
87
93
|
* together and cannot disagree.
|
|
88
94
|
*/
|
|
89
|
-
finish(accepted?: boolean, reason?: string): number
|
|
95
|
+
finish(accepted?: boolean, reason?: string): Promise<number>;
|
|
90
96
|
}
|
|
91
97
|
export {};
|
package/dist/core/runner.js
CHANGED
|
@@ -14,8 +14,20 @@ import * as agents from "./agents.js";
|
|
|
14
14
|
import { makeGit } from "./git_helper.js";
|
|
15
15
|
import { Console } from "./console.js";
|
|
16
16
|
import { Tracer } from "./tracer.js";
|
|
17
|
-
import { makeEventRecord } from "./data_types.js";
|
|
17
|
+
import { makeEventRecord, resolveObservabilityDb } from "./data_types.js";
|
|
18
18
|
import { ensureDir, nowIso } from "./utils.js";
|
|
19
|
+
/**
|
|
20
|
+
* `Console.sessionFinished`'s trailing `db` row wants a short, human-facing
|
|
21
|
+
* label, not a filesystem path specifically — `cfg.observability.db` is now
|
|
22
|
+
* a discriminated shape (see `data_types.ts`'s migration note), so this
|
|
23
|
+
* normalizes it to whichever of "the sqlite path" or "the D1 database id"
|
|
24
|
+
* is actually meaningful to show, rather than assuming it's always a path
|
|
25
|
+
* string the way this call site did before that field existed.
|
|
26
|
+
*/
|
|
27
|
+
function describeObservabilityDb(db) {
|
|
28
|
+
const resolved = resolveObservabilityDb(db);
|
|
29
|
+
return resolved.kind === "sqlite" ? resolved.path : `d1:${resolved.database_id}`;
|
|
30
|
+
}
|
|
19
31
|
class PhaseHandleImpl {
|
|
20
32
|
run;
|
|
21
33
|
phase;
|
|
@@ -23,17 +35,17 @@ class PhaseHandleImpl {
|
|
|
23
35
|
this.run = run;
|
|
24
36
|
this.phase = phase;
|
|
25
37
|
}
|
|
26
|
-
log(payload) {
|
|
27
|
-
this.run.tracer.event(makeEventRecord({
|
|
38
|
+
async log(payload) {
|
|
39
|
+
await this.run.tracer.event(makeEventRecord({
|
|
28
40
|
adw_id: this.run.adw_id,
|
|
29
41
|
phase_id: this.phase.phase_id,
|
|
30
42
|
type: "log",
|
|
31
43
|
name: this.phase.params.name,
|
|
32
44
|
payload,
|
|
33
45
|
}));
|
|
34
|
-
this.run.console.note(Object.entries(payload).map(([k, v]) => `${k}: ${v}`).join(", "));
|
|
46
|
+
await this.run.console.note(Object.entries(payload).map(([k, v]) => `${k}: ${v}`).join(", "));
|
|
35
47
|
if (this.phase.params.kind === "engineer" && "input" in payload) {
|
|
36
|
-
this.run.tracer.sessionRequest(this.run.adw_id, String(payload.input));
|
|
48
|
+
await this.run.tracer.sessionRequest(this.run.adw_id, String(payload.input));
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
async call(call) {
|
|
@@ -80,7 +92,7 @@ export class Run {
|
|
|
80
92
|
this.notify = init.notifier ?? null;
|
|
81
93
|
this.console = new Console(init.tracer, init.adwId, this.notify, init.chainName || "adw", init.sink, init.observer);
|
|
82
94
|
this.engineer = init.engineer;
|
|
83
|
-
this.seq = init.
|
|
95
|
+
this.seq = init.startSeq;
|
|
84
96
|
this.repo_root = init.repoRoot;
|
|
85
97
|
this.spf_dir = init.sfDir;
|
|
86
98
|
this.git = makeGit(init.repoRoot);
|
|
@@ -95,12 +107,12 @@ export class Run {
|
|
|
95
107
|
this.agent_map[agent] = entry;
|
|
96
108
|
writeFileSync(this.agentMapPath, JSON.stringify(this.agent_map, null, 2));
|
|
97
109
|
}
|
|
98
|
-
// ── usage (run totals mirror what the tracer accumulates in
|
|
99
|
-
addUsage(tokens, cost) {
|
|
110
|
+
// ── usage (run totals mirror what the tracer accumulates in the trace db) ─
|
|
111
|
+
async addUsage(tokens, cost) {
|
|
100
112
|
this.tokens += tokens;
|
|
101
113
|
this.cost += cost;
|
|
102
|
-
this.tracer.sessionAddUsage(this.adw_id, tokens, cost);
|
|
103
|
-
this.console.notifyUsage(this.tokens, this.cost);
|
|
114
|
+
await this.tracer.sessionAddUsage(this.adw_id, tokens, cost);
|
|
115
|
+
await this.console.notifyUsage(this.tokens, this.cost);
|
|
104
116
|
}
|
|
105
117
|
// ── the phase primitive ─────────────────────────────────────────────────
|
|
106
118
|
async phase(params, fn) {
|
|
@@ -117,35 +129,35 @@ export class Run {
|
|
|
117
129
|
ended_at: null,
|
|
118
130
|
};
|
|
119
131
|
this.phases.push(phase);
|
|
120
|
-
this.tracer.phaseUpsert(phase);
|
|
121
|
-
this.tracer.event(makeEventRecord({
|
|
132
|
+
await this.tracer.phaseUpsert(phase);
|
|
133
|
+
await this.tracer.event(makeEventRecord({
|
|
122
134
|
adw_id: this.adw_id,
|
|
123
135
|
phase_id: phase.phase_id,
|
|
124
136
|
type: "phase_start",
|
|
125
137
|
name: params.name,
|
|
126
138
|
payload: { kind: params.kind, owner: params.owner, description: params.description },
|
|
127
139
|
}));
|
|
128
|
-
this.console.phaseStarted(phase);
|
|
140
|
+
await this.console.phaseStarted(phase);
|
|
129
141
|
const clock = performance.now();
|
|
130
142
|
try {
|
|
131
143
|
const result = await fn(new PhaseHandleImpl(this, phase));
|
|
132
144
|
phase.status = "success";
|
|
133
145
|
phase.ended_at = nowIso();
|
|
134
|
-
this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "phase_end", name: params.name, payload: { status: "success" } }));
|
|
135
|
-
this.tracer.phaseUpsert(phase);
|
|
136
|
-
this.console.phaseEnded(phase, (performance.now() - clock) / 1000);
|
|
146
|
+
await this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "phase_end", name: params.name, payload: { status: "success" } }));
|
|
147
|
+
await this.tracer.phaseUpsert(phase);
|
|
148
|
+
await this.console.phaseEnded(phase, (performance.now() - clock) / 1000);
|
|
137
149
|
return result;
|
|
138
150
|
}
|
|
139
151
|
catch (error) {
|
|
140
152
|
phase.status = "fail"; // success must be earned
|
|
141
153
|
phase.error = String(error?.message ?? error).slice(0, 1000);
|
|
142
154
|
phase.ended_at = nowIso();
|
|
143
|
-
this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "error", name: params.name, payload: { error: phase.error } }));
|
|
144
|
-
this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "phase_end", name: params.name, payload: { status: "fail" } }));
|
|
145
|
-
this.tracer.phaseUpsert(phase);
|
|
146
|
-
this.tracer.sessionFinish(this.adw_id, false);
|
|
147
|
-
this.console.phaseEnded(phase, (performance.now() - clock) / 1000);
|
|
148
|
-
this.console.sessionFinished(false, this.tokens, this.cost, this.cfg.observability.db);
|
|
155
|
+
await this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "error", name: params.name, payload: { error: phase.error } }));
|
|
156
|
+
await this.tracer.event(makeEventRecord({ adw_id: this.adw_id, phase_id: phase.phase_id, type: "phase_end", name: params.name, payload: { status: "fail" } }));
|
|
157
|
+
await this.tracer.phaseUpsert(phase);
|
|
158
|
+
await this.tracer.sessionFinish(this.adw_id, false);
|
|
159
|
+
await this.console.phaseEnded(phase, (performance.now() - clock) / 1000);
|
|
160
|
+
await this.console.sessionFinished(false, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db));
|
|
149
161
|
throw error;
|
|
150
162
|
}
|
|
151
163
|
}
|
|
@@ -158,22 +170,22 @@ export class Run {
|
|
|
158
170
|
* so the exit code, the session status, and the banner are decided
|
|
159
171
|
* together and cannot disagree.
|
|
160
172
|
*/
|
|
161
|
-
finish(accepted = true, reason = "") {
|
|
173
|
+
async finish(accepted = true, reason = "") {
|
|
162
174
|
const phasesOk = this.phases.length > 0 && this.phases.every((p) => p.status === "success");
|
|
163
175
|
const ok = phasesOk && accepted;
|
|
164
176
|
if (phasesOk && !accepted) {
|
|
165
177
|
const note = reason || "the run's acceptance criterion was not met";
|
|
166
|
-
this.tracer.event(makeEventRecord({
|
|
178
|
+
await this.tracer.event(makeEventRecord({
|
|
167
179
|
adw_id: this.adw_id,
|
|
168
180
|
phase_id: this.phases.length > 0 ? this.phases[this.phases.length - 1].phase_id : "",
|
|
169
181
|
type: "error",
|
|
170
182
|
name: "not_accepted",
|
|
171
183
|
payload: { reason: note },
|
|
172
184
|
}));
|
|
173
|
-
this.console.note(`not accepted: ${note}`);
|
|
185
|
+
await this.console.note(`not accepted: ${note}`);
|
|
174
186
|
}
|
|
175
|
-
this.tracer.sessionFinish(this.adw_id, ok);
|
|
176
|
-
this.console.sessionFinished(ok, this.tokens, this.cost, this.cfg.observability.db);
|
|
187
|
+
await this.tracer.sessionFinish(this.adw_id, ok);
|
|
188
|
+
await this.console.sessionFinished(ok, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db));
|
|
177
189
|
return ok ? 0 : 1;
|
|
178
190
|
}
|
|
179
191
|
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { SFConfig } from "./data_types.ts";
|
|
|
30
30
|
* `releaseOtelExporter`, and harmless for the same reason: that process
|
|
31
31
|
* exits right after anyway.
|
|
32
32
|
*/
|
|
33
|
-
export declare function finalize(adwId: string | null | undefined): void
|
|
33
|
+
export declare function finalize(adwId: string | null | undefined): Promise<void>;
|
|
34
34
|
/** Tests only: which adw_ids the process-wide signal handler currently considers active. */
|
|
35
35
|
export declare function activeRunIdsForTest(): string[];
|
|
36
36
|
/**
|
|
@@ -49,4 +49,4 @@ export declare function ensure(cfg: SFConfig, adwId?: string | null, cwd?: strin
|
|
|
49
49
|
renderHooks?: {
|
|
50
50
|
sink?: (line: string) => void;
|
|
51
51
|
observer?: RunObserver | null;
|
|
52
|
-
}): Run
|
|
52
|
+
}): Promise<Run>;
|