@broject/drill 0.0.0 → 0.2.3
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 +27 -1
- package/dist/index.d.ts +114 -0
- package/dist/index.js +431 -0
- package/package.json +39 -8
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
# @broject/drill
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scoped descent frames behind `bro drill` — `drill down` opens a narrower
|
|
4
|
+
investigation frame (materialized as a bead), `drill up` closes it with a
|
|
5
|
+
mandatory result + prevention memo so a problem can't silently recur.
|
|
6
|
+
|
|
7
|
+
> You probably want the CLI instead: `bro drill down --title …`.
|
|
8
|
+
> Install this only when building agents that run drill frames.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i @broject/drill
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node ≥ 22 and `bd` (frames are beads). ESM only.
|
|
17
|
+
|
|
18
|
+
## Surface
|
|
19
|
+
|
|
20
|
+
- `drillConnector` — registers `bro drill *` subcommands
|
|
21
|
+
- `PreventionPlan` — the required close-out artifact
|
|
22
|
+
- `parseDrillPlan` / `DRILL_PLAN_KIND` — the unified drill plan payload
|
|
23
|
+
- Re-exports `bd`/`taskStore` primitives from `@broject/core`
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- Docs: https://broject.dev/docs/commands/drill
|
|
28
|
+
- Source: https://github.com/ThePlenkov/bro/tree/main/packages/drill
|
|
29
|
+
- CLI: https://www.npmjs.com/package/@broject/bro
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Connector, bd, bdJson, checkBeads, refKind, taskStore } from "@broject/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface DrillRow {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
priority?: number;
|
|
9
|
+
issue_type?: string;
|
|
10
|
+
created_at?: string;
|
|
11
|
+
updated_at?: string;
|
|
12
|
+
labels?: string[];
|
|
13
|
+
notes?: string;
|
|
14
|
+
ephemeral?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface DrillFrame extends DrillRow {
|
|
17
|
+
parentId?: string;
|
|
18
|
+
depth: number;
|
|
19
|
+
}
|
|
20
|
+
interface DownOptions {
|
|
21
|
+
under?: string;
|
|
22
|
+
/** Force a root frame — by default `down` nests under the current leaf. */
|
|
23
|
+
root?: boolean;
|
|
24
|
+
ephemeral?: boolean;
|
|
25
|
+
type?: string;
|
|
26
|
+
priority?: number;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
interface UpOptions {
|
|
30
|
+
id?: string;
|
|
31
|
+
result: string;
|
|
32
|
+
prevent?: string[];
|
|
33
|
+
evidence?: string[];
|
|
34
|
+
}
|
|
35
|
+
interface UpResult {
|
|
36
|
+
closed: string;
|
|
37
|
+
preventionIds: string[];
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/frames.d.ts
|
|
41
|
+
declare function listDrills(): DrillRow[];
|
|
42
|
+
declare function childrenOf(id: string): DrillRow[];
|
|
43
|
+
/**
|
|
44
|
+
* The active frame: an open drill leaf (no open drill children) on the
|
|
45
|
+
* deepest path. Ties break on most-recently-updated — the frame the agent
|
|
46
|
+
* touched last is almost always the live one.
|
|
47
|
+
*/
|
|
48
|
+
declare function currentFrame(): DrillFrame | undefined;
|
|
49
|
+
/** Descend: create a child frame under `opts.under` or the current leaf;
|
|
50
|
+
* `opts.root` forces a parentless frame even when a leaf is open. */
|
|
51
|
+
declare function drillDown(title: string, opts?: DownOptions): DrillRow;
|
|
52
|
+
interface PreventionPlan {
|
|
53
|
+
/** Normalized item key (`titleKey`) → existing bead id. */
|
|
54
|
+
reuse: Map<string, string>;
|
|
55
|
+
/** Item titles needing a new bead, in order, deduped, trimmed. */
|
|
56
|
+
create: string[];
|
|
57
|
+
}
|
|
58
|
+
/** Fold `--prevent` items against what the frame already recorded: an
|
|
59
|
+
* open prevention bead with the same title is reused, not recreated;
|
|
60
|
+
* repeated items within the list collapse to one bead. Pure — the
|
|
61
|
+
* retry-safety core of drillUp: a mid-flight failure followed by a
|
|
62
|
+
* retry converges instead of duplicating. */
|
|
63
|
+
declare function planPreventions(items: string[], prior: DrillRow[]): PreventionPlan;
|
|
64
|
+
/**
|
|
65
|
+
* Ascend: close the frame with a structured memo. `--result` is mandatory —
|
|
66
|
+
* a drill that returns nothing teaches nothing. Each `--prevent` item lands
|
|
67
|
+
* as a task on the parent frame so prevention work lives in the scope that
|
|
68
|
+
* spawned it.
|
|
69
|
+
*/
|
|
70
|
+
declare function drillUp(opts: UpOptions): UpResult;
|
|
71
|
+
/** Root frames + rendered tree (indented, roots first). */
|
|
72
|
+
declare function drillTree(): string;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/plan.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* Drill plans — a declared descent tree. The agent writes the
|
|
77
|
+
* investigation shape once, `bro run drill.toml` materializes the
|
|
78
|
+
* frames as beads; investigation fills each frame via `drill up`
|
|
79
|
+
* as usual.
|
|
80
|
+
*
|
|
81
|
+
* kind = "drill"
|
|
82
|
+
* title = "why does the cache miss" # root frame
|
|
83
|
+
*
|
|
84
|
+
* [[steps]]
|
|
85
|
+
* title = "check the parser" # child of root
|
|
86
|
+
*
|
|
87
|
+
* [[steps]]
|
|
88
|
+
* title = "narrow the repro"
|
|
89
|
+
* under = 0 # child of steps[0]
|
|
90
|
+
* ephemeral = true
|
|
91
|
+
*/
|
|
92
|
+
interface DrillStep {
|
|
93
|
+
title: string;
|
|
94
|
+
/** index into `steps` — nested under that step; default = root */
|
|
95
|
+
under?: number;
|
|
96
|
+
ephemeral?: boolean;
|
|
97
|
+
description?: string;
|
|
98
|
+
priority?: number;
|
|
99
|
+
type?: string;
|
|
100
|
+
}
|
|
101
|
+
interface DrillPlan {
|
|
102
|
+
title: string;
|
|
103
|
+
steps: DrillStep[];
|
|
104
|
+
}
|
|
105
|
+
/** The `kind` value a drill plan must carry — `bro run` routes on it. */
|
|
106
|
+
declare const PLAN_KIND = "drill";
|
|
107
|
+
/** Validate an already-parsed plan document — the plugin planSchema.
|
|
108
|
+
* Throws one error listing every problem. */
|
|
109
|
+
declare function parseDrillPlan(doc: unknown, source?: string): DrillPlan;
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/connector.d.ts
|
|
112
|
+
declare const drillConnector: Connector;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { PLAN_KIND as DRILL_PLAN_KIND, type DownOptions, type DrillFrame, type DrillPlan, type DrillRow, type DrillStep, type PreventionPlan, type UpOptions, type UpResult, bd, bdJson, checkBeads, childrenOf, currentFrame, drillConnector, drillDown, drillTree, drillUp, listDrills, parseDrillPlan, planPreventions, refKind, taskStore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { bd, bd as bd$1, bdJson, bdJson as bdJson$1, checkBeads, evidenceKind, refKind, refKind as refKind$1, taskStore, taskStore as taskStore$1 } from "@broject/core";
|
|
2
|
+
|
|
3
|
+
//#region src/frames.ts
|
|
4
|
+
const DRILL_LABEL = "drill";
|
|
5
|
+
const PREVENTION_LABEL = "prevention";
|
|
6
|
+
function isDrill(row) {
|
|
7
|
+
return row.labels?.includes(DRILL_LABEL) ?? false;
|
|
8
|
+
}
|
|
9
|
+
function isOpen(row) {
|
|
10
|
+
return row.status !== "closed" && row.status !== "done";
|
|
11
|
+
}
|
|
12
|
+
function listDrills() {
|
|
13
|
+
const persistent = taskStore$1().list({
|
|
14
|
+
labels: [DRILL_LABEL],
|
|
15
|
+
all: true,
|
|
16
|
+
limit: 0
|
|
17
|
+
});
|
|
18
|
+
let wisps = [];
|
|
19
|
+
try {
|
|
20
|
+
wisps = (bdJson$1([
|
|
21
|
+
"mol",
|
|
22
|
+
"wisp",
|
|
23
|
+
"list",
|
|
24
|
+
"--all"
|
|
25
|
+
]).wisps ?? []).filter(isDrill);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
if (!/unknown command|unrecognized command/i.test(msg)) throw err;
|
|
29
|
+
}
|
|
30
|
+
return [...persistent, ...wisps];
|
|
31
|
+
}
|
|
32
|
+
function childrenOf(id) {
|
|
33
|
+
return taskStore$1().children(id);
|
|
34
|
+
}
|
|
35
|
+
/** One `bd dep list` sweep: parent→kids and kid→parent in a single call
|
|
36
|
+
* (was N+1 `bd children`). kids holds DRILL children only — every
|
|
37
|
+
* consumer filters on isDrill anyway; drillUp's any-child close check
|
|
38
|
+
* still uses childrenOf directly. */
|
|
39
|
+
function drillRelations(rows) {
|
|
40
|
+
const kids = /* @__PURE__ */ new Map();
|
|
41
|
+
const parents = /* @__PURE__ */ new Map();
|
|
42
|
+
if (rows.length === 0) return {
|
|
43
|
+
kids,
|
|
44
|
+
parents
|
|
45
|
+
};
|
|
46
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
47
|
+
const edges = taskStore$1().deps(rows.map((r) => r.id), { type: "parent-child" });
|
|
48
|
+
for (const e of edges) {
|
|
49
|
+
const kid = byId.get(e.issue_id);
|
|
50
|
+
const parent = byId.get(e.depends_on_id);
|
|
51
|
+
if (e.type !== "parent-child" || !kid || !parent) continue;
|
|
52
|
+
kids.set(parent.id, [...kids.get(parent.id) ?? [], kid]);
|
|
53
|
+
parents.set(kid.id, parent.id);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
kids,
|
|
57
|
+
parents
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The active frame: an open drill leaf (no open drill children) on the
|
|
62
|
+
* deepest path. Ties break on most-recently-updated — the frame the agent
|
|
63
|
+
* touched last is almost always the live one.
|
|
64
|
+
*/
|
|
65
|
+
function currentFrame() {
|
|
66
|
+
const rows = listDrills().filter(isOpen);
|
|
67
|
+
if (rows.length === 0) return;
|
|
68
|
+
const { kids, parents } = drillRelations(rows);
|
|
69
|
+
const depthOf = (id) => {
|
|
70
|
+
let d = 0;
|
|
71
|
+
let cur = id;
|
|
72
|
+
while ((cur = parents.get(cur)) !== void 0) d += 1;
|
|
73
|
+
return d;
|
|
74
|
+
};
|
|
75
|
+
const leaves = rows.filter((r) => !(kids.get(r.id) ?? []).some((k) => isOpen(k) && isDrill(k)));
|
|
76
|
+
leaves.sort((a, b) => {
|
|
77
|
+
const d = depthOf(b.id) - depthOf(a.id);
|
|
78
|
+
return d !== 0 ? d : (b.updated_at ?? "").localeCompare(a.updated_at ?? "");
|
|
79
|
+
});
|
|
80
|
+
const leaf = leaves[0];
|
|
81
|
+
if (!leaf) return;
|
|
82
|
+
return {
|
|
83
|
+
...leaf,
|
|
84
|
+
parentId: parents.get(leaf.id),
|
|
85
|
+
depth: depthOf(leaf.id)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The bead must be an open drill frame, or an explicit selector is wrong. */
|
|
89
|
+
function requireOpenDrill(id, flag) {
|
|
90
|
+
const row = taskStore$1().get(id);
|
|
91
|
+
if (!row || !isDrill(row) || !isOpen(row)) throw new Error(`${flag} ${id} is not an open drill frame`);
|
|
92
|
+
return row;
|
|
93
|
+
}
|
|
94
|
+
/** Record the claim provenance; on failure, delete the frame — an
|
|
95
|
+
* unclaimed frame violates the claim-on-down invariant, and a retry would
|
|
96
|
+
* create a duplicate. A failed cleanup must not swallow the claim error. */
|
|
97
|
+
function claimFrame(id) {
|
|
98
|
+
try {
|
|
99
|
+
bd$1([
|
|
100
|
+
"provenance",
|
|
101
|
+
"record",
|
|
102
|
+
"--issue",
|
|
103
|
+
id,
|
|
104
|
+
"--kind",
|
|
105
|
+
"claim",
|
|
106
|
+
"--source",
|
|
107
|
+
"bro drill down",
|
|
108
|
+
"--at",
|
|
109
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
110
|
+
]);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
try {
|
|
113
|
+
taskStore$1().remove(id);
|
|
114
|
+
} catch (cleanupErr) {
|
|
115
|
+
throw new Error(`claim failed: ${err instanceof Error ? err.message : err}; cleanup of ${id} also failed (frame left unclaimed): ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Descend: create a child frame under `opts.under` or the current leaf;
|
|
121
|
+
* `opts.root` forces a parentless frame even when a leaf is open. */
|
|
122
|
+
function drillDown(title, opts = {}) {
|
|
123
|
+
let parent;
|
|
124
|
+
if (opts.under) parent = requireOpenDrill(opts.under, "--under").id;
|
|
125
|
+
else if (!opts.root) parent = currentFrame()?.id;
|
|
126
|
+
const row = taskStore$1().create({
|
|
127
|
+
title,
|
|
128
|
+
labels: [DRILL_LABEL],
|
|
129
|
+
parent,
|
|
130
|
+
ephemeral: opts.ephemeral,
|
|
131
|
+
type: opts.type,
|
|
132
|
+
priority: opts.priority,
|
|
133
|
+
description: opts.description
|
|
134
|
+
});
|
|
135
|
+
if (!opts.ephemeral) claimFrame(row.id);
|
|
136
|
+
return row;
|
|
137
|
+
}
|
|
138
|
+
/** Open prevention beads already discovered-from this frame — the
|
|
139
|
+
* dedupe set that makes prevention creation retry-safe. Throws when bd
|
|
140
|
+
* returns an unexpected row shape (e.g. dependency-edge objects instead
|
|
141
|
+
* of hydrated issues): a wrong shape would mask as "no priors" and
|
|
142
|
+
* silently resurrect the duplicate-on-retry bug this query prevents. */
|
|
143
|
+
function priorPreventionRows(frameId) {
|
|
144
|
+
const rows = taskStore$1().deps([frameId], {
|
|
145
|
+
direction: "up",
|
|
146
|
+
type: "discovered-from"
|
|
147
|
+
});
|
|
148
|
+
assertHydratedRows(rows, `bd dep list for ${frameId}`);
|
|
149
|
+
return rows;
|
|
150
|
+
}
|
|
151
|
+
/** Fail loudly when a bd listing returns rows without `id`/`title`/`status` —
|
|
152
|
+
* e.g. dependency-edge objects after a bd upgrade. `context` names the
|
|
153
|
+
* query so the error identifies its source. Exported for tests. */
|
|
154
|
+
function assertHydratedRows(rows, context) {
|
|
155
|
+
for (const row of rows) if (typeof row?.id !== "string" || typeof row?.title !== "string" || typeof row?.status !== "string") throw new Error(`${context} returned an unexpected row shape (expected hydrated issue rows): ${JSON.stringify(row).slice(0, 160)}`);
|
|
156
|
+
}
|
|
157
|
+
/** Same-title dedupe key: "handle race" and " Handle Race " are the
|
|
158
|
+
* same prevention — exact-match would duplicate the bead on retry. */
|
|
159
|
+
function titleKey(title) {
|
|
160
|
+
return title.trim().toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
/** Fold `--prevent` items against what the frame already recorded: an
|
|
163
|
+
* open prevention bead with the same title is reused, not recreated;
|
|
164
|
+
* repeated items within the list collapse to one bead. Pure — the
|
|
165
|
+
* retry-safety core of drillUp: a mid-flight failure followed by a
|
|
166
|
+
* retry converges instead of duplicating. */
|
|
167
|
+
function planPreventions(items, prior) {
|
|
168
|
+
const reuse = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const row of prior) {
|
|
170
|
+
const key = titleKey(row.title ?? "");
|
|
171
|
+
if (key && isOpen(row) && row.labels?.includes(PREVENTION_LABEL) && !reuse.has(key)) reuse.set(key, row.id);
|
|
172
|
+
}
|
|
173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
174
|
+
const create = [];
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
const key = titleKey(item);
|
|
177
|
+
if (!key || reuse.has(key) || seen.has(key)) continue;
|
|
178
|
+
seen.add(key);
|
|
179
|
+
create.push(item.trim());
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
create,
|
|
183
|
+
reuse
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** One prevention bead per item — discovered-from, not --parent:
|
|
187
|
+
* prevention is follow-up work found by the frame, and a child would
|
|
188
|
+
* block the parent's own close. `--title` keeps a `--`-leading item as
|
|
189
|
+
* data, not a flag. `created` holds only beads this call made — the
|
|
190
|
+
* compensation set; `ids` is the full per-item result (reused + new). */
|
|
191
|
+
function createPreventions(frameId, items) {
|
|
192
|
+
const { create, reuse } = planPreventions(items, priorPreventionRows(frameId));
|
|
193
|
+
const created = [];
|
|
194
|
+
const newIds = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const item of create) {
|
|
196
|
+
const row = taskStore$1().create({
|
|
197
|
+
title: item,
|
|
198
|
+
labels: [PREVENTION_LABEL],
|
|
199
|
+
noInheritLabels: true,
|
|
200
|
+
deps: [`discovered-from:${frameId}`]
|
|
201
|
+
});
|
|
202
|
+
created.push(row.id);
|
|
203
|
+
newIds.set(titleKey(item), row.id);
|
|
204
|
+
}
|
|
205
|
+
const ids = [];
|
|
206
|
+
for (const item of items) {
|
|
207
|
+
const key = titleKey(item);
|
|
208
|
+
if (!key) continue;
|
|
209
|
+
const id = reuse.get(key) ?? newIds.get(key);
|
|
210
|
+
if (!id) throw new Error(`internal error: no bead id for prevention item "${item}"`);
|
|
211
|
+
ids.push(id);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
created,
|
|
215
|
+
ids
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** `bd note` appends — check the stored notes first so a retry after a
|
|
219
|
+
* later failure can't duplicate the memo. */
|
|
220
|
+
function noteOnce(frameId, memo) {
|
|
221
|
+
if (taskStore$1().get(frameId)?.notes?.includes(memo)) return;
|
|
222
|
+
taskStore$1().note(frameId, memo);
|
|
223
|
+
}
|
|
224
|
+
/** Evidence refs + handoff event — skipped for wisps (no audit trail).
|
|
225
|
+
* Ref'd events are idempotent in bd (deterministic id); the ref-less
|
|
226
|
+
* handoff gets a fresh `--at` each run, so skip it when already logged. */
|
|
227
|
+
function recordHandoff(frame, evidence) {
|
|
228
|
+
for (const ref of evidence) {
|
|
229
|
+
const kind = refKind$1(ref);
|
|
230
|
+
bd$1([
|
|
231
|
+
"provenance",
|
|
232
|
+
"record",
|
|
233
|
+
"--issue",
|
|
234
|
+
frame.id,
|
|
235
|
+
"--kind",
|
|
236
|
+
evidenceKind(ref),
|
|
237
|
+
"--source",
|
|
238
|
+
"bro drill up",
|
|
239
|
+
"--ref",
|
|
240
|
+
ref,
|
|
241
|
+
"--ref-kind",
|
|
242
|
+
kind
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
if (bdJson$1([
|
|
246
|
+
"provenance",
|
|
247
|
+
"log",
|
|
248
|
+
frame.id
|
|
249
|
+
]).some((e) => e.kind === "handoff" && e.source === "bro drill up")) return;
|
|
250
|
+
bd$1([
|
|
251
|
+
"provenance",
|
|
252
|
+
"record",
|
|
253
|
+
"--issue",
|
|
254
|
+
frame.id,
|
|
255
|
+
"--kind",
|
|
256
|
+
"handoff",
|
|
257
|
+
"--source",
|
|
258
|
+
"bro drill up",
|
|
259
|
+
"--at",
|
|
260
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Ascend: close the frame with a structured memo. `--result` is mandatory —
|
|
265
|
+
* a drill that returns nothing teaches nothing. Each `--prevent` item lands
|
|
266
|
+
* as a task on the parent frame so prevention work lives in the scope that
|
|
267
|
+
* spawned it.
|
|
268
|
+
*/
|
|
269
|
+
function drillUp(opts) {
|
|
270
|
+
if (!opts.result.trim()) throw new Error("drill up requires --result — a frame must return a curated finding");
|
|
271
|
+
const frame = opts.id ? requireOpenDrill(opts.id, "--id") : (() => {
|
|
272
|
+
const leaf = currentFrame();
|
|
273
|
+
if (!leaf) throw new Error("no open drill frame — nothing to ascend from");
|
|
274
|
+
return requireOpenDrill(leaf.id, "--id");
|
|
275
|
+
})();
|
|
276
|
+
const openKids = childrenOf(frame.id).filter(isOpen);
|
|
277
|
+
if (openKids.length > 0) throw new Error(`frame ${frame.id} has open child issue(s): ${openKids.map((k) => k.id).join(", ")} — close them first`);
|
|
278
|
+
const prevents = (opts.prevent ?? []).map((p) => p.trim()).filter((p) => p !== "");
|
|
279
|
+
const memo = [
|
|
280
|
+
"## Result",
|
|
281
|
+
"",
|
|
282
|
+
opts.result,
|
|
283
|
+
...prevents.length ? [
|
|
284
|
+
"",
|
|
285
|
+
"## Prevention",
|
|
286
|
+
"",
|
|
287
|
+
...prevents.map((p) => `- ${p}`)
|
|
288
|
+
] : []
|
|
289
|
+
].join("\n");
|
|
290
|
+
let created = [];
|
|
291
|
+
let preventionIds = [];
|
|
292
|
+
try {
|
|
293
|
+
noteOnce(frame.id, memo);
|
|
294
|
+
const prev = createPreventions(frame.id, prevents);
|
|
295
|
+
created = prev.created;
|
|
296
|
+
preventionIds = prev.ids;
|
|
297
|
+
if (!frame.ephemeral) recordHandoff(frame, opts.evidence ?? []);
|
|
298
|
+
taskStore$1().close(frame.id, "drill up — result handed to parent");
|
|
299
|
+
} catch (err) {
|
|
300
|
+
const orphans = [];
|
|
301
|
+
for (const id of created) try {
|
|
302
|
+
taskStore$1().remove(id);
|
|
303
|
+
} catch {
|
|
304
|
+
orphans.push(id);
|
|
305
|
+
}
|
|
306
|
+
if (orphans.length === 0) throw err;
|
|
307
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
308
|
+
throw new Error(`${msg} — cleanup incomplete: prevention bead(s) left behind: ${orphans.join(", ")}`, { cause: err });
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
closed: frame.id,
|
|
312
|
+
preventionIds
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Root frames + rendered tree (indented, roots first). */
|
|
316
|
+
function drillTree() {
|
|
317
|
+
const rows = listDrills();
|
|
318
|
+
if (rows.length === 0) return "no drill frames";
|
|
319
|
+
const { kids, parents } = drillRelations(rows);
|
|
320
|
+
const roots = rows.filter((r) => !parents.has(r.id));
|
|
321
|
+
const lines = [];
|
|
322
|
+
const walk = (row, depth) => {
|
|
323
|
+
const mark = isOpen(row) ? "●" : "○";
|
|
324
|
+
lines.push(`${" ".repeat(depth)}${mark} ${row.id} ${row.title} [${row.status}]`);
|
|
325
|
+
for (const kid of (kids.get(row.id) ?? []).filter(isDrill)) walk(kid, depth + 1);
|
|
326
|
+
};
|
|
327
|
+
for (const root of roots) walk(root, 0);
|
|
328
|
+
return lines.join("\n");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/plan.ts
|
|
333
|
+
/** The `kind` value a drill plan must carry — `bro run` routes on it. */
|
|
334
|
+
const PLAN_KIND = "drill";
|
|
335
|
+
const STEP_KEYS = new Set([
|
|
336
|
+
"title",
|
|
337
|
+
"under",
|
|
338
|
+
"ephemeral",
|
|
339
|
+
"description",
|
|
340
|
+
"priority",
|
|
341
|
+
"type"
|
|
342
|
+
]);
|
|
343
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
344
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim() !== "";
|
|
345
|
+
const isPosInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
346
|
+
const isInt = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
347
|
+
function checkStep(raw, where, errors) {
|
|
348
|
+
for (const key of Object.keys(raw)) if (!STEP_KEYS.has(key)) errors.push(`${where}: unknown key "${key}"`);
|
|
349
|
+
if (!nonEmpty(raw.title)) errors.push(`${where}: title is required`);
|
|
350
|
+
if (raw.under !== void 0 && !isInt(raw.under)) errors.push(`${where}: under must be a non-negative step index`);
|
|
351
|
+
for (const f of ["description", "type"]) if (raw[f] !== void 0 && typeof raw[f] !== "string") errors.push(`${where}: ${f} must be a string`);
|
|
352
|
+
if (raw.ephemeral !== void 0 && typeof raw.ephemeral !== "boolean") errors.push(`${where}: ephemeral must be a boolean`);
|
|
353
|
+
if (raw.priority !== void 0 && !isPosInt(raw.priority)) errors.push(`${where}: priority must be a positive integer`);
|
|
354
|
+
}
|
|
355
|
+
function parseStep(raw, i, errors) {
|
|
356
|
+
const where = `steps[${i}]`;
|
|
357
|
+
if (!isRecord(raw)) {
|
|
358
|
+
errors.push(`${where}: must be a table`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
checkStep(raw, where, errors);
|
|
362
|
+
if (!nonEmpty(raw.title)) return;
|
|
363
|
+
return {
|
|
364
|
+
title: raw.title.trim(),
|
|
365
|
+
under: typeof raw.under === "number" ? raw.under : void 0,
|
|
366
|
+
ephemeral: raw.ephemeral === true ? true : void 0,
|
|
367
|
+
description: typeof raw.description === "string" ? raw.description : void 0,
|
|
368
|
+
priority: isPosInt(raw.priority) ? raw.priority : void 0,
|
|
369
|
+
type: typeof raw.type === "string" ? raw.type : void 0
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/** Validate an already-parsed plan document — the plugin planSchema.
|
|
373
|
+
* Throws one error listing every problem. */
|
|
374
|
+
function parseDrillPlan(doc, source = "plan") {
|
|
375
|
+
const errors = [];
|
|
376
|
+
if (!isRecord(doc)) throw new Error(`${source}: expected a TOML table`);
|
|
377
|
+
for (const key of Object.keys(doc)) if (key !== "steps" && key !== "kind" && key !== "title") errors.push(`unknown top-level key "${key}"`);
|
|
378
|
+
if (doc.kind !== void 0 && doc.kind !== PLAN_KIND) errors.push(`kind: expected "${PLAN_KIND}", got ${JSON.stringify(doc.kind)}`);
|
|
379
|
+
if (!nonEmpty(doc.title)) errors.push("title: the root frame title is required");
|
|
380
|
+
const steps = [];
|
|
381
|
+
if (doc.steps !== void 0 && !Array.isArray(doc.steps)) errors.push("steps: must be an array of tables ([[steps]])");
|
|
382
|
+
else if (Array.isArray(doc.steps)) doc.steps.forEach((raw, i) => {
|
|
383
|
+
const s = parseStep(raw, i, errors);
|
|
384
|
+
if (!s) return;
|
|
385
|
+
if (s.under !== void 0 && s.under >= steps.length) errors.push(`steps[${i}]: under must index a previous step`);
|
|
386
|
+
steps.push(s);
|
|
387
|
+
});
|
|
388
|
+
if (errors.length > 0) throw new Error(`${source}:\n ${errors.join("\n ")}`);
|
|
389
|
+
return {
|
|
390
|
+
title: doc.title.trim(),
|
|
391
|
+
steps
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/connector.ts
|
|
397
|
+
/** The open-frame line, or null — the same text every surface shares. */
|
|
398
|
+
function frameLine() {
|
|
399
|
+
try {
|
|
400
|
+
const frame = currentFrame();
|
|
401
|
+
if (!frame) return null;
|
|
402
|
+
return `drill frame open: ${frame.id} "${frame.title}" [depth=${frame.depth}] — close with \`bro drill up --result "…"\``;
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const drillConnector = {
|
|
408
|
+
name: "drill",
|
|
409
|
+
hooks: () => ({
|
|
410
|
+
sessionStart() {
|
|
411
|
+
const line = frameLine();
|
|
412
|
+
return line ? [line] : [];
|
|
413
|
+
},
|
|
414
|
+
promptSubmit() {
|
|
415
|
+
const line = frameLine();
|
|
416
|
+
return line ? [line] : [];
|
|
417
|
+
},
|
|
418
|
+
stopGate() {
|
|
419
|
+
const line = frameLine();
|
|
420
|
+
if (!line) return [];
|
|
421
|
+
return [{
|
|
422
|
+
aspect: "drill",
|
|
423
|
+
block: `bro: ${line}`,
|
|
424
|
+
passive: `bro: ${line} (opened outside this session — informational)`
|
|
425
|
+
}];
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
//#endregion
|
|
431
|
+
export { PLAN_KIND as DRILL_PLAN_KIND, bd, bdJson, checkBeads, childrenOf, currentFrame, drillConnector, drillDown, drillTree, drillUp, listDrills, parseDrillPlan, planPreventions, refKind, taskStore };
|
package/package.json
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Placeholder for @broject/drill published by @nx-devkit/prepare-for-release.",
|
|
3
|
-
"license": "MIT",
|
|
4
2
|
"name": "@broject/drill",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@broject/core": "^0.2.0"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "tsx --test src/**/*.test.ts",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsdown": "^0.15.0",
|
|
20
|
+
"tsx": "^4.20.0",
|
|
21
|
+
"typescript": "^5.9.0",
|
|
22
|
+
"smol-toml": "1.7.1"
|
|
8
23
|
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
9
31
|
"repository": {
|
|
10
32
|
"type": "git",
|
|
11
33
|
"url": "git+https://github.com/ThePlenkov/bro.git",
|
|
12
34
|
"directory": "packages/drill"
|
|
13
35
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
}
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"description": "Scoped descent frames for bro — drill down/up over beads with result + prevention memos",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"beads",
|
|
42
|
+
"investigation",
|
|
43
|
+
"agent",
|
|
44
|
+
"bro"
|
|
45
|
+
],
|
|
46
|
+
"homepage": "https://broject.dev/docs"
|
|
47
|
+
}
|