@bragi-gmbh/codebus 1.3.0 → 1.6.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/dist/ci-hooks/ci-hooks.js +126 -0
- package/dist/ci-hooks/ci-hooks.js.map +1 -0
- package/dist/commands/agent.js +137 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/as.js +1 -1
- package/dist/commands/as.js.map +1 -1
- package/dist/commands/ask.js +1 -1
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/ci-findings.js +129 -0
- package/dist/commands/ci-findings.js.map +1 -0
- package/dist/commands/ci-reply.js +117 -0
- package/dist/commands/ci-reply.js.map +1 -0
- package/dist/commands/ci-steer-escalate.js +65 -0
- package/dist/commands/ci-steer-escalate.js.map +1 -0
- package/dist/commands/cli-mcp-correlation.js +131 -0
- package/dist/commands/cli-mcp-correlation.js.map +1 -0
- package/dist/commands/connector-attach-detach.js +542 -0
- package/dist/commands/connector-attach-detach.js.map +1 -0
- package/dist/commands/connector.js +513 -0
- package/dist/commands/connector.js.map +1 -0
- package/dist/commands/correlation.js +194 -0
- package/dist/commands/correlation.js.map +1 -0
- package/dist/commands/decision.js +63 -0
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/digest.js +14 -3
- package/dist/commands/digest.js.map +1 -1
- package/dist/commands/do.js +127 -0
- package/dist/commands/do.js.map +1 -0
- package/dist/commands/doc.js +276 -0
- package/dist/commands/doc.js.map +1 -0
- package/dist/commands/factory-attention.js +106 -0
- package/dist/commands/factory-attention.js.map +1 -0
- package/dist/commands/factory-context-read.js +171 -0
- package/dist/commands/factory-context-read.js.map +1 -0
- package/dist/commands/factory-interventions.js +102 -0
- package/dist/commands/factory-interventions.js.map +1 -0
- package/dist/commands/factory.js +187 -0
- package/dist/commands/factory.js.map +1 -1
- package/dist/commands/graph-pull.js +130 -0
- package/dist/commands/graph-pull.js.map +1 -0
- package/dist/commands/graph.js +5 -1
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/inbox.js +7 -7
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/ingest.js +1 -1
- package/dist/commands/ingest.js.map +1 -1
- package/dist/commands/install.js +1160 -55
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/integration-ledger.js +81 -0
- package/dist/commands/integration-ledger.js.map +1 -0
- package/dist/commands/knowledge.js +178 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/links.js +1 -1
- package/dist/commands/links.js.map +1 -1
- package/dist/commands/morning.js +50 -1
- package/dist/commands/morning.js.map +1 -1
- package/dist/commands/plan-compiler.js +260 -0
- package/dist/commands/plan-compiler.js.map +1 -0
- package/dist/commands/plan-lint.js +275 -0
- package/dist/commands/plan-lint.js.map +1 -0
- package/dist/commands/plan-score.js +203 -0
- package/dist/commands/plan-score.js.map +1 -0
- package/dist/commands/plan.js +481 -15
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/product-validity.js +168 -0
- package/dist/commands/product-validity.js.map +1 -0
- package/dist/commands/question.js +1 -1
- package/dist/commands/question.js.map +1 -1
- package/dist/commands/read-policy.js +131 -0
- package/dist/commands/read-policy.js.map +1 -0
- package/dist/commands/read.js +1 -1
- package/dist/commands/read.js.map +1 -1
- package/dist/commands/repo.js +106 -8
- package/dist/commands/repo.js.map +1 -1
- package/dist/commands/repository-protection.js +209 -0
- package/dist/commands/repository-protection.js.map +1 -0
- package/dist/commands/resolve.js +2 -2
- package/dist/commands/resolve.js.map +1 -1
- package/dist/commands/self-healing.js +26 -0
- package/dist/commands/self-healing.js.map +1 -0
- package/dist/commands/send.js +1 -1
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/serial-merge.js +303 -0
- package/dist/commands/serial-merge.js.map +1 -0
- package/dist/commands/session.js +1 -1
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/task.js +92 -1
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/verify.js +77 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/config/self-healing-analysis.json +158 -0
- package/dist/errors.js +18 -4
- package/dist/errors.js.map +1 -1
- package/dist/execution/client.js +53 -0
- package/dist/execution/client.js.map +1 -0
- package/dist/execution/local.js +24 -0
- package/dist/execution/local.js.map +1 -0
- package/dist/execution/ssh.js +38 -0
- package/dist/execution/ssh.js.map +1 -0
- package/dist/graph.js +78 -16
- package/dist/graph.js.map +1 -1
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -1
- package/dist/json-lite.js +248 -0
- package/dist/json-lite.js.map +1 -0
- package/dist/knowledge-drift.js +40 -0
- package/dist/knowledge-drift.js.map +1 -0
- package/dist/lint/parallelism.js +414 -0
- package/dist/lint/parallelism.js.map +1 -0
- package/dist/mail-latency.js +11 -1
- package/dist/mail-latency.js.map +1 -1
- package/dist/mcp-ci-findings.js +31 -0
- package/dist/mcp-ci-findings.js.map +1 -0
- package/dist/mcp-correlation.js +290 -0
- package/dist/mcp-correlation.js.map +1 -0
- package/dist/mcp-factory-context-read.js +81 -0
- package/dist/mcp-factory-context-read.js.map +1 -0
- package/dist/mcp-registry.js +202 -1
- package/dist/mcp-registry.js.map +1 -1
- package/dist/mcp.js +94 -5
- package/dist/mcp.js.map +1 -1
- package/dist/openrouter-reservation-guard.js +97 -0
- package/dist/openrouter-reservation-guard.js.map +1 -0
- package/dist/self-healing-analysis-loader.js +150 -0
- package/dist/self-healing-analysis-loader.js.map +1 -0
- package/dist/sender.js +11 -9
- package/dist/sender.js.map +1 -1
- package/dist/toml-lite.js +546 -0
- package/dist/toml-lite.js.map +1 -0
- package/dist/version-check.js +23 -7
- package/dist/version-check.js.map +1 -1
- package/package.json +5 -3
package/dist/commands/plan.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parse } from "yaml";
|
|
1
6
|
import { resolveConfig } from "../config.js";
|
|
2
7
|
import { makeCtx, workspaceId, repoId } from "../client.js";
|
|
3
8
|
import { render } from "../output.js";
|
|
4
9
|
import { CodebusError } from "../errors.js";
|
|
10
|
+
import { planCapsFindings, planCapsLabel } from "./plan-lint.js";
|
|
5
11
|
/**
|
|
6
12
|
* codebus plan … — SP-A planning-layer CLI (fleet-v3 build/w2-sp-a-cli,
|
|
7
13
|
* lane 2 of docs/plans/fleet-v3/sp-a-planning-layer.md §A.3).
|
|
@@ -132,6 +138,317 @@ export async function planReviewRecordCore(ctx, planId, opts) {
|
|
|
132
138
|
if (error)
|
|
133
139
|
throw new CodebusError(error.message);
|
|
134
140
|
}
|
|
141
|
+
export const unavailableTriageTransport = {
|
|
142
|
+
async review() { throw new CodebusError("plan triage requires an injected review transport"); },
|
|
143
|
+
};
|
|
144
|
+
export const trackedTriageTransport = {
|
|
145
|
+
async review(input) {
|
|
146
|
+
const prompt = [
|
|
147
|
+
"Review this immutable CodeBus plan as a read-only adversarial reason-role reviewer.",
|
|
148
|
+
"Return only one compact JSON object: {\"verdict\":\"approve|reject\",\"findings\":[],\"evidence\":[]}.",
|
|
149
|
+
"Reject material safety, dependency, authority, or verification gaps. Do not edit files or execute the plan.",
|
|
150
|
+
JSON.stringify({ revision_digest: input.revisionDigest, plan: input.plan }),
|
|
151
|
+
].join("\n");
|
|
152
|
+
let output;
|
|
153
|
+
try {
|
|
154
|
+
const ownerHome = userInfo().homedir;
|
|
155
|
+
const monitoredWrapper = join(ownerHome, ".local/bin/opencode-mon.sh");
|
|
156
|
+
if (!existsSync(monitoredWrapper))
|
|
157
|
+
throw new Error(`monitoring wrapper not found: ${monitoredWrapper}`);
|
|
158
|
+
const childEnv = { ...process.env, HOME: ownerHome, FACTORY_HOME: ownerHome };
|
|
159
|
+
delete childEnv.REAL;
|
|
160
|
+
delete childEnv.DEV01_TRACKED;
|
|
161
|
+
delete childEnv.AGENT_RUNS_DIR;
|
|
162
|
+
output = execFileSync(monitoredWrapper, [
|
|
163
|
+
"run", "--agent", "plan", "--format", "json", prompt,
|
|
164
|
+
], {
|
|
165
|
+
encoding: "utf8", timeout: 300_000, maxBuffer: 2 * 1024 * 1024,
|
|
166
|
+
env: childEnv,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new CodebusError(`tracked plan triage failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
}
|
|
172
|
+
const records = output.split("\n").filter(Boolean).flatMap((line) => {
|
|
173
|
+
try {
|
|
174
|
+
return [JSON.parse(line)];
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const runId = records.find((record) => typeof record.sessionID === "string")?.sessionID;
|
|
181
|
+
const text = records.flatMap((record) => {
|
|
182
|
+
const candidate = record.part?.text ?? record.text;
|
|
183
|
+
return typeof candidate === "string" ? [candidate] : [];
|
|
184
|
+
}).at(-1);
|
|
185
|
+
if (!runId || !text)
|
|
186
|
+
throw new CodebusError("tracked plan triage returned no auditable result");
|
|
187
|
+
let result;
|
|
188
|
+
try {
|
|
189
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
190
|
+
result = JSON.parse(match?.[0] ?? "");
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new CodebusError("tracked plan triage returned malformed JSON");
|
|
194
|
+
}
|
|
195
|
+
if (!['approve', 'reject'].includes(String(result.verdict)) || !Array.isArray(result.findings)) {
|
|
196
|
+
throw new CodebusError("tracked plan triage returned an invalid verdict");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
verdict: result.verdict,
|
|
200
|
+
findings: result.findings,
|
|
201
|
+
evidence: Array.isArray(result.evidence) ? result.evidence : [],
|
|
202
|
+
cost: undefined,
|
|
203
|
+
runId,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
async function readPlanProjection(ctx, view) {
|
|
208
|
+
const ws = await workspaceId(ctx);
|
|
209
|
+
const { data, error } = await ctx.supabase.from(view).select("*").eq("workspace_id", ws);
|
|
210
|
+
if (error)
|
|
211
|
+
throw new CodebusError(error.message);
|
|
212
|
+
return (data ?? []);
|
|
213
|
+
}
|
|
214
|
+
export async function planObjectListCore(ctx) {
|
|
215
|
+
return readPlanProjection(ctx, "plan_list_v1");
|
|
216
|
+
}
|
|
217
|
+
export async function planObjectWhoCore(ctx) {
|
|
218
|
+
return readPlanProjection(ctx, "plan_who_v1");
|
|
219
|
+
}
|
|
220
|
+
/** The projection owns effective state; observed fields are returned separately and untouched. */
|
|
221
|
+
export async function planObjectStuckCore(ctx) {
|
|
222
|
+
return readPlanProjection(ctx, "plan_stuck_v1");
|
|
223
|
+
}
|
|
224
|
+
export async function planObjectShowCore(ctx, planRef) {
|
|
225
|
+
const ws = await workspaceId(ctx);
|
|
226
|
+
const key = /^CB-PLAN-/i.test(planRef) ? "display_id" : "id";
|
|
227
|
+
const { data: plan, error: planError } = await ctx.supabase.from("plan_list_v1")
|
|
228
|
+
.select("*").eq("workspace_id", ws).eq(key, key === "display_id" ? planRef.toUpperCase() : planRef).single();
|
|
229
|
+
if (planError || !plan)
|
|
230
|
+
throw new CodebusError(`plan not found (or not visible to you): ${planRef}`);
|
|
231
|
+
const { data: assignments, error: assignmentsError } = await ctx.supabase.from("plan_who_v1")
|
|
232
|
+
.select("*").eq("workspace_id", ws).eq("plan_id", plan.id).is("active_to", null);
|
|
233
|
+
if (assignmentsError)
|
|
234
|
+
throw new CodebusError(assignmentsError.message);
|
|
235
|
+
const revisionId = plan.latest_revision_id;
|
|
236
|
+
if (!revisionId)
|
|
237
|
+
return { ...plan, assignments: assignments ?? [], stages: [] };
|
|
238
|
+
const { data: revision, error: revisionError } = await ctx.supabase.from("plan_revisions")
|
|
239
|
+
.select("document").eq("id", revisionId).single();
|
|
240
|
+
if (revisionError)
|
|
241
|
+
throw new CodebusError(revisionError.message);
|
|
242
|
+
const { data: stages, error: stagesError } = await ctx.supabase.from("plan_stages")
|
|
243
|
+
.select("*").eq("revision_id", revisionId).order("ordinal");
|
|
244
|
+
if (stagesError)
|
|
245
|
+
throw new CodebusError(stagesError.message);
|
|
246
|
+
return { ...plan, document: revision?.document ?? null, assignments: assignments ?? [], stages: stages ?? [] };
|
|
247
|
+
}
|
|
248
|
+
export async function planObjectImportCore(ctx, sourceRef, document, idempotencyKey) {
|
|
249
|
+
const ws = await workspaceId(ctx);
|
|
250
|
+
const { data, error } = await ctx.supabase.rpc("plan_import_v1", {
|
|
251
|
+
p_workspace_id: ws,
|
|
252
|
+
p_source_ref: sourceRef,
|
|
253
|
+
p_document: document,
|
|
254
|
+
p_idempotency_key: idempotencyKey,
|
|
255
|
+
});
|
|
256
|
+
if (error)
|
|
257
|
+
throw new CodebusError(error.message);
|
|
258
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
259
|
+
throw new CodebusError("plan import RPC returned an invalid response");
|
|
260
|
+
}
|
|
261
|
+
return data;
|
|
262
|
+
}
|
|
263
|
+
export async function planTriageCore(ctx, planRef, opts, transport = unavailableTriageTransport) {
|
|
264
|
+
const plan = await planObjectShowCore(ctx, planRef);
|
|
265
|
+
const planId = String(plan.id);
|
|
266
|
+
if (String(plan.revision_digest ?? "") !== opts.revisionDigest) {
|
|
267
|
+
throw new CodebusError("stale plan revision digest; triage was not submitted");
|
|
268
|
+
}
|
|
269
|
+
const submission = await transport.review({ plan, planId, revisionDigest: opts.revisionDigest, role: opts.role });
|
|
270
|
+
const latest = await planObjectShowCore(ctx, planRef);
|
|
271
|
+
if (String(latest.revision_digest ?? "") !== opts.revisionDigest) {
|
|
272
|
+
throw new CodebusError("plan revision changed while triaging; result was not submitted");
|
|
273
|
+
}
|
|
274
|
+
const { data, error } = await ctx.supabase.rpc("plan_triage_report_record", {
|
|
275
|
+
p_revision_id: latest.latest_revision_id, p_revision_digest: opts.revisionDigest,
|
|
276
|
+
p_verdict: submission.verdict,
|
|
277
|
+
p_report: { role: opts.role, findings: submission.findings,
|
|
278
|
+
evidence: submission.evidence ?? [], cost: submission.cost ?? null,
|
|
279
|
+
run_id: submission.runId ?? null },
|
|
280
|
+
});
|
|
281
|
+
if (error)
|
|
282
|
+
throw new CodebusError(error.message);
|
|
283
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
284
|
+
throw new CodebusError("plan triage RPC returned an invalid response");
|
|
285
|
+
return { ...data, verdict: submission.verdict, findings: submission.findings };
|
|
286
|
+
}
|
|
287
|
+
export async function planApproveCore(ctx, revisionId, revisionDigest) {
|
|
288
|
+
const { data, error } = await ctx.supabase.rpc("plan_approve", { p_revision_id: revisionId, p_revision_digest: revisionDigest });
|
|
289
|
+
if (error)
|
|
290
|
+
throw new CodebusError(error.message);
|
|
291
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
292
|
+
throw new CodebusError("plan approval RPC returned an invalid response");
|
|
293
|
+
return data;
|
|
294
|
+
}
|
|
295
|
+
export async function planGateProposeCore(ctx, revisionId, revisionDigest, stageId) {
|
|
296
|
+
const { data, error } = await ctx.supabase.rpc("plan_gate_propose", {
|
|
297
|
+
p_revision_id: revisionId, p_revision_digest: revisionDigest, p_stage_id: stageId,
|
|
298
|
+
});
|
|
299
|
+
if (error)
|
|
300
|
+
throw new CodebusError(error.message);
|
|
301
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
302
|
+
throw new CodebusError("plan gate proposal RPC returned an invalid response");
|
|
303
|
+
return data;
|
|
304
|
+
}
|
|
305
|
+
export async function planGateReleaseCore(ctx, opts) {
|
|
306
|
+
const { data, error } = await ctx.supabase.rpc("plan_gate_release", {
|
|
307
|
+
p_gate_id: opts.gateId, p_reason: opts.reason ?? null,
|
|
308
|
+
});
|
|
309
|
+
if (error)
|
|
310
|
+
throw new CodebusError(error.message);
|
|
311
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
312
|
+
throw new CodebusError("plan gate release RPC returned an invalid response");
|
|
313
|
+
return data;
|
|
314
|
+
}
|
|
315
|
+
function readPlanSource(path) {
|
|
316
|
+
try {
|
|
317
|
+
return readFileSync(path, "utf8");
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
321
|
+
throw new CodebusError(`unable to read plan file '${path}': ${detail}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ──────────────────────────────────────────────── Plan Ledger projection
|
|
325
|
+
/**
|
|
326
|
+
* Resolve the task form accepted by `codebus plan <task>` without ever doing
|
|
327
|
+
* an unscoped display-id lookup. Ledger status is keyed by the task UUID in
|
|
328
|
+
* SQL, while the human-facing CLI convention also accepts CB-TASK-… ids.
|
|
329
|
+
*/
|
|
330
|
+
async function resolveLedgerTaskId(ctx, taskRef) {
|
|
331
|
+
if (!/^CB-TASK-/i.test(taskRef))
|
|
332
|
+
return taskRef;
|
|
333
|
+
const ws = await workspaceId(ctx);
|
|
334
|
+
const { data, error } = await ctx.supabase
|
|
335
|
+
.from("tasks")
|
|
336
|
+
.select("id")
|
|
337
|
+
.eq("workspace_id", ws)
|
|
338
|
+
.eq("display_id", taskRef.toUpperCase())
|
|
339
|
+
.single();
|
|
340
|
+
if (error || !data)
|
|
341
|
+
throw new CodebusError(`task not found (or not visible to you): ${taskRef}`);
|
|
342
|
+
return data.id;
|
|
343
|
+
}
|
|
344
|
+
function normalizeCapsRow(data, planId) {
|
|
345
|
+
const row = (data && typeof data === "object" && !Array.isArray(data) ? data : {});
|
|
346
|
+
const num = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value
|
|
347
|
+
: typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value)
|
|
348
|
+
: fallback;
|
|
349
|
+
const intOrNull = (value) => {
|
|
350
|
+
const parsed = num(value, NaN);
|
|
351
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
...(row.workspace_id != null ? { workspace_id: String(row.workspace_id) } : {}),
|
|
355
|
+
plan_id: row.plan_id != null ? String(row.plan_id) : planId ?? null,
|
|
356
|
+
max_budget_usd: num(row.max_budget_usd, 150),
|
|
357
|
+
max_stages: num(row.max_stages, 12),
|
|
358
|
+
max_children_per_stage: intOrNull(row.max_children_per_stage),
|
|
359
|
+
decision_id: row.decision_id != null ? String(row.decision_id) : null,
|
|
360
|
+
decision_display_id: row.decision_display_id != null ? String(row.decision_display_id) : null,
|
|
361
|
+
source: row.source === "decision" ? "decision" : "default",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** Effective admission caps: from plan_caps_effective_v1 (one plan) or
|
|
365
|
+
* plan_caps_for (workspace level, e.g. before a plan exists). */
|
|
366
|
+
export async function planCapsShowCore(ctx, planRef) {
|
|
367
|
+
const ws = await workspaceId(ctx);
|
|
368
|
+
if (!planRef) {
|
|
369
|
+
const { data, error } = await ctx.supabase.rpc("plan_caps_for", { p_workspace_id: ws, p_plan_id: null });
|
|
370
|
+
if (error)
|
|
371
|
+
throw new CodebusError(error.message);
|
|
372
|
+
return normalizeCapsRow(data);
|
|
373
|
+
}
|
|
374
|
+
const plan = await planObjectShowCore(ctx, planRef);
|
|
375
|
+
const { data, error } = await ctx.supabase.from("plan_caps_effective_v1")
|
|
376
|
+
.select("*").eq("plan_id", plan.id).maybeSingle();
|
|
377
|
+
if (error)
|
|
378
|
+
throw new CodebusError(error.message);
|
|
379
|
+
return normalizeCapsRow(data, String(plan.id));
|
|
380
|
+
}
|
|
381
|
+
/** Create a PENDING plan-caps decision; approval stays the owner action
|
|
382
|
+
* (decision accept / decision_transition). */
|
|
383
|
+
export async function planCapsProposeCore(ctx, opts) {
|
|
384
|
+
const ws = await workspaceId(ctx);
|
|
385
|
+
const { data, error } = await ctx.supabase.rpc("plan_caps_propose", {
|
|
386
|
+
p_workspace_id: ws,
|
|
387
|
+
p_max_budget_usd: opts.maxBudgetUsd ?? null,
|
|
388
|
+
p_max_stages: opts.maxStages ?? null,
|
|
389
|
+
p_max_children_per_stage: opts.maxChildrenPerStage ?? null,
|
|
390
|
+
p_applies_to: opts.appliesTo ?? "all",
|
|
391
|
+
p_expires_at: opts.expiresAt ?? null,
|
|
392
|
+
});
|
|
393
|
+
if (error)
|
|
394
|
+
throw new CodebusError(error.message);
|
|
395
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
396
|
+
throw new CodebusError("plan caps proposal RPC returned an invalid response");
|
|
397
|
+
}
|
|
398
|
+
return data;
|
|
399
|
+
}
|
|
400
|
+
export async function planLedgerCore(ctx, taskRef) {
|
|
401
|
+
const taskId = await resolveLedgerTaskId(ctx, taskRef);
|
|
402
|
+
const { data, error } = await ctx.supabase.rpc("plan_ledger_status", { p_task_id: taskId });
|
|
403
|
+
if (error)
|
|
404
|
+
throw new CodebusError(error.message);
|
|
405
|
+
if (!data || typeof data !== "object")
|
|
406
|
+
throw new CodebusError("plan_ledger_status returned an invalid result");
|
|
407
|
+
return data;
|
|
408
|
+
}
|
|
409
|
+
function renderPlanLedgerHuman(result) {
|
|
410
|
+
const task = (result.task ?? {});
|
|
411
|
+
const predicates = (result.predicates ?? {});
|
|
412
|
+
const findings = Array.isArray(result.findings) ? result.findings : [];
|
|
413
|
+
const workItems = Array.isArray(result.work_items) ? result.work_items : [];
|
|
414
|
+
const contested = Array.isArray(result.contested) ? result.contested : [];
|
|
415
|
+
const edges = Array.isArray(result.edges) ? result.edges : [];
|
|
416
|
+
const lines = [
|
|
417
|
+
`task ${task.display_id ?? task.id} — ${task.title ?? "(untitled)"}`,
|
|
418
|
+
` status=${task.status ?? "(unknown)"} done=${task.done ?? false}`,
|
|
419
|
+
` SETTLED=${predicates.settled ?? false} EXHAUSTED=${predicates.exhausted ?? false}`,
|
|
420
|
+
` blocking_risk=${predicates.blocking_risk ?? false} epochs=${predicates.epochs_observed ?? 0}`,
|
|
421
|
+
"",
|
|
422
|
+
"findings:",
|
|
423
|
+
];
|
|
424
|
+
for (const [index, finding] of findings.entries()) {
|
|
425
|
+
const branch = index === findings.length - 1 ? "└─" : "├─";
|
|
426
|
+
const claim = String(finding.claim ?? "").replace(/\s+/g, " ").trim();
|
|
427
|
+
lines.push(` ${branch} [${finding.status}] ${finding.id} ${finding.type}: ${claim}`);
|
|
428
|
+
}
|
|
429
|
+
if (findings.length === 0)
|
|
430
|
+
lines.push(" (none)");
|
|
431
|
+
lines.push("", "dependency edges:");
|
|
432
|
+
for (const edge of edges) {
|
|
433
|
+
lines.push(` ${edge.source_finding_id} -[${edge.edge_type}]-> ${edge.target_finding_id}`);
|
|
434
|
+
}
|
|
435
|
+
if (edges.length === 0)
|
|
436
|
+
lines.push(" (none)");
|
|
437
|
+
lines.push("", "work items:");
|
|
438
|
+
for (const item of workItems) {
|
|
439
|
+
lines.push(` ${item.compilable ? "✓" : "·"} ${item.work_item_id} compilable=${item.compilable}`
|
|
440
|
+
+ (item.hold_reason ? ` hold=${item.hold_reason}` : ""));
|
|
441
|
+
}
|
|
442
|
+
if (workItems.length === 0)
|
|
443
|
+
lines.push(" (none)");
|
|
444
|
+
lines.push("", "contested findings:");
|
|
445
|
+
for (const finding of contested) {
|
|
446
|
+
lines.push(` ! ${finding.id} ${finding.type}: ${finding.claim}`);
|
|
447
|
+
}
|
|
448
|
+
if (contested.length === 0)
|
|
449
|
+
lines.push(" (none)");
|
|
450
|
+
return lines.join("\n");
|
|
451
|
+
}
|
|
135
452
|
// Round-1 review (CB-MSG-005365) MAJOR finding 5: `plan show` used to run a
|
|
136
453
|
// single unbounded PostgREST select for tickets and events. PostgREST's
|
|
137
454
|
// `api.max_rows` (supabase/config.toml, 1000 by default) silently caps ANY
|
|
@@ -296,9 +613,37 @@ function renderPlanShowHuman(r) {
|
|
|
296
613
|
lines.push(render(r.events.map((e) => ({ id: e.id, type: e.event_type, actor: e.actor_agent_id, summary: e.summary })), { json: false }));
|
|
297
614
|
return lines.join("\n");
|
|
298
615
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
616
|
+
export function registerPlan(program, g, dependencies = {}) {
|
|
617
|
+
const plan = g(program.command("plan"))
|
|
618
|
+
.argument("[task_id]", "task UUID or CB-TASK-… for the computed Plan Ledger view")
|
|
619
|
+
.description("planning layer: plan maps, tickets, frontier, and Plan Ledger status (SP-A/PL-3)")
|
|
620
|
+
// Commander keeps options declared on a parent command in the parent
|
|
621
|
+
// command's option bag. Existing plan subcommands declare the same
|
|
622
|
+
// shared options through `g`, and their actions read the child bag, so a
|
|
623
|
+
// parent `--json` would otherwise make `plan create --json` render a
|
|
624
|
+
// table. Copy explicitly supplied parent options into the dispatched
|
|
625
|
+
// child before its action runs; child-local values remain authoritative.
|
|
626
|
+
.hook("preAction", (_parent, actionCommand) => {
|
|
627
|
+
for (const [key, value] of Object.entries(plan.opts())) {
|
|
628
|
+
if (value !== undefined)
|
|
629
|
+
actionCommand.setOptionValue(key, value);
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
.action(async (taskId, o) => {
|
|
633
|
+
// A parent command action is only reached when no registered subcommand
|
|
634
|
+
// matched. Keep the no-argument behavior quiet for compatibility, while
|
|
635
|
+
// making the spec's `codebus plan <task_id>` a direct read surface.
|
|
636
|
+
if (!taskId)
|
|
637
|
+
return;
|
|
638
|
+
const ctx = await makeCtx(resolveConfig(o));
|
|
639
|
+
const result = await planLedgerCore(ctx, taskId);
|
|
640
|
+
if (o.json) {
|
|
641
|
+
console.log(JSON.stringify(result, null, 2));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
console.log(renderPlanLedgerHuman(result));
|
|
645
|
+
}
|
|
646
|
+
});
|
|
302
647
|
g(plan.command("create"))
|
|
303
648
|
.requiredOption("--title <t>")
|
|
304
649
|
.requiredOption("--destination <d>", "what this plan produces")
|
|
@@ -415,23 +760,144 @@ export function registerPlan(program, g) {
|
|
|
415
760
|
await planReviewRecordCore(ctx, planId, { kind: o.kind, verdict: o.verdict, msg: o.msg, loop: Number(o.loop) });
|
|
416
761
|
console.log(render([{ plan_id: planId, kind: o.kind, verdict: o.verdict, loop: Number(o.loop) }], { json: !!o.json }));
|
|
417
762
|
});
|
|
418
|
-
async function
|
|
763
|
+
async function showPlanObject(planRef, o) {
|
|
419
764
|
const ctx = await makeCtx(resolveConfig(o));
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
765
|
+
// Raw UUIDs are the pre-0097 SP-A surface and retain the frontier/event
|
|
766
|
+
// response exactly; CB-PLAN display IDs select the owned-plan object view.
|
|
767
|
+
if (!/^CB-PLAN-/i.test(planRef)) {
|
|
768
|
+
const legacy = await planShowCore(ctx, planRef);
|
|
769
|
+
if (o.json) {
|
|
770
|
+
console.log(JSON.stringify(legacy, null, 2));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
console.log(renderPlanShowHuman(legacy));
|
|
423
774
|
return;
|
|
424
775
|
}
|
|
425
|
-
|
|
776
|
+
const result = await planObjectShowCore(ctx, planRef);
|
|
777
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
426
778
|
}
|
|
427
|
-
g(plan.command("show").argument("<plan>")).action(
|
|
779
|
+
g(plan.command("show").argument("<plan>")).action(showPlanObject);
|
|
428
780
|
// Compatibility restored (Sol round-1, F3/MAJOR + coordinator ruling):
|
|
429
|
-
// `plan status
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
|
|
781
|
+
// `plan status` is a true alias of `plan show` — same handler, same
|
|
782
|
+
// output shape. The new bus_plan_status rollup lives at `plan rollup`
|
|
783
|
+
// instead (below), a brand new verb with no compatibility surface.
|
|
784
|
+
//
|
|
785
|
+
// Issue #350: `plan status` previously called planShowCore directly,
|
|
786
|
+
// bypassing display-id resolution — CB-PLAN-… refs failed with a
|
|
787
|
+
// misleading "plan not found" error. Delegating to showPlanObject
|
|
788
|
+
// (the same handler `plan show` uses) makes status accept both CB-PLAN-…
|
|
789
|
+
// display ids (resolved via planObjectShowCore) and raw UUIDs
|
|
790
|
+
// (frontier view via planShowCore), exactly as documented.
|
|
791
|
+
g(plan.command("status").argument("<plan>")).action(showPlanObject);
|
|
792
|
+
g(plan.command("list")).action(async (o) => {
|
|
793
|
+
const rows = await planObjectListCore(await makeCtx(resolveConfig(o)));
|
|
794
|
+
console.log(render(rows, { json: !!o.json }));
|
|
795
|
+
});
|
|
796
|
+
g(plan.command("who")).action(async (o) => {
|
|
797
|
+
const rows = await planObjectWhoCore(await makeCtx(resolveConfig(o)));
|
|
798
|
+
console.log(render(rows, { json: !!o.json }));
|
|
799
|
+
});
|
|
800
|
+
g(plan.command("stuck")).action(async (o) => {
|
|
801
|
+
const rows = await planObjectStuckCore(await makeCtx(resolveConfig(o)));
|
|
802
|
+
console.log(render(rows, { json: !!o.json }));
|
|
803
|
+
});
|
|
804
|
+
g(plan.command("import").argument("<yaml>"))
|
|
805
|
+
.requiredOption("--key <key>", "idempotency key for replay-safe import")
|
|
806
|
+
.action(async (path, o) => {
|
|
807
|
+
const source = readPlanSource(path);
|
|
808
|
+
let document;
|
|
809
|
+
try {
|
|
810
|
+
document = parse(source);
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
throw new CodebusError(`unable to parse plan file '${path}': ${error instanceof Error ? error.message : String(error)}`);
|
|
814
|
+
}
|
|
815
|
+
const ctx = await makeCtx(resolveConfig(o));
|
|
816
|
+
// Admission caps are owner decisions (0111): resolve the effective
|
|
817
|
+
// workspace caps and refuse before import, naming the decision id or
|
|
818
|
+
// the defaults. plan_import_v1 re-enforces the same caps server-side.
|
|
819
|
+
const capsRow = await planCapsShowCore(ctx);
|
|
820
|
+
const caps = {
|
|
821
|
+
max_budget_usd: capsRow.max_budget_usd,
|
|
822
|
+
max_stages: capsRow.max_stages,
|
|
823
|
+
max_children_per_stage: capsRow.max_children_per_stage,
|
|
824
|
+
decision_id: capsRow.decision_id,
|
|
825
|
+
decision_display_id: capsRow.decision_display_id,
|
|
826
|
+
source: capsRow.source,
|
|
827
|
+
};
|
|
828
|
+
const capFindings = planCapsFindings(document, caps);
|
|
829
|
+
if (capFindings.length > 0) {
|
|
830
|
+
throw new CodebusError(`plan import refused (${planCapsLabel(caps)}): ${capFindings.map((item) => `${item.code} ${item.message}`).join("; ")}`);
|
|
831
|
+
}
|
|
832
|
+
const result = await planObjectImportCore(ctx, path, document, o.key);
|
|
833
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
834
|
+
});
|
|
835
|
+
g(plan.command("triage").argument("<plan>"))
|
|
836
|
+
.requiredOption("--revision <digest>", "exact immutable revision digest")
|
|
837
|
+
.requiredOption("--role <role>", "reason")
|
|
838
|
+
.action(async (planRef, o) => {
|
|
839
|
+
if (o.role !== "reason")
|
|
840
|
+
throw new CodebusError("--role must be reason");
|
|
841
|
+
const result = await planTriageCore(await makeCtx(resolveConfig(o)), planRef, { revisionDigest: o.revision, role: "reason" }, dependencies.triageTransport ?? trackedTriageTransport);
|
|
842
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
843
|
+
});
|
|
844
|
+
g(plan.command("approve").argument("<plan>"))
|
|
845
|
+
.requiredOption("--revision <digest>", "exact immutable revision digest")
|
|
846
|
+
.action(async (planId, o) => {
|
|
847
|
+
const ctx = await makeCtx(resolveConfig(o));
|
|
848
|
+
const planObject = await planObjectShowCore(ctx, planId);
|
|
849
|
+
if (String(planObject.revision_digest) !== o.revision || typeof planObject.latest_revision_id !== "string") {
|
|
850
|
+
throw new CodebusError("stale plan revision digest; approval was not submitted");
|
|
851
|
+
}
|
|
852
|
+
const result = await planApproveCore(ctx, planObject.latest_revision_id, o.revision);
|
|
853
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
854
|
+
});
|
|
855
|
+
const gate = plan.command("gate").description("human plan gate operations");
|
|
856
|
+
g(gate.command("propose").argument("<revision>"))
|
|
857
|
+
.requiredOption("--revision-digest <digest>", "exact immutable revision digest")
|
|
858
|
+
.requiredOption("--stage <stage>", "exact human stage reference")
|
|
859
|
+
.action(async (revisionId, o) => {
|
|
860
|
+
const result = await planGateProposeCore(await makeCtx(resolveConfig(o)), revisionId, o.revisionDigest, o.stage);
|
|
861
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
862
|
+
});
|
|
863
|
+
g(gate.command("release").argument("<gate>"))
|
|
864
|
+
.option("--reason <reason>")
|
|
865
|
+
.action(async (gateId, o) => {
|
|
866
|
+
const result = await planGateReleaseCore(await makeCtx(resolveConfig(o)), {
|
|
867
|
+
gateId, reason: o.reason,
|
|
868
|
+
});
|
|
869
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
870
|
+
});
|
|
871
|
+
const caps = plan.command("caps").description("plan admission caps (owner decisions)");
|
|
872
|
+
g(caps.command("show").argument("[plan]", "plan display ID (CB-PLAN-…) or UUID; omit for workspace-level caps"))
|
|
873
|
+
.action(async (planRef, o) => {
|
|
874
|
+
const result = await planCapsShowCore(await makeCtx(resolveConfig(o)), planRef);
|
|
875
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
876
|
+
});
|
|
877
|
+
g(caps.command("propose"))
|
|
878
|
+
.option("--max-budget-usd <n>", "maximum plan budget in USD")
|
|
879
|
+
.option("--max-stages <n>", "maximum number of normal stages")
|
|
880
|
+
.option("--max-children-per-stage <n>", "maximum dependent stages per stage")
|
|
881
|
+
.option("--plan <id>", "apply to one plan (default: all plans in the workspace)")
|
|
882
|
+
.option("--expires-at <ts>", "ISO timestamp after which the caps lapse")
|
|
883
|
+
.action(async (o) => {
|
|
884
|
+
const num = (value, name) => {
|
|
885
|
+
if (value == null)
|
|
886
|
+
return undefined;
|
|
887
|
+
const parsed = Number(value);
|
|
888
|
+
if (!Number.isFinite(parsed))
|
|
889
|
+
throw new CodebusError(`${name} must be a number`);
|
|
890
|
+
return parsed;
|
|
891
|
+
};
|
|
892
|
+
const result = await planCapsProposeCore(await makeCtx(resolveConfig(o)), {
|
|
893
|
+
maxBudgetUsd: num(o.maxBudgetUsd, "--max-budget-usd"),
|
|
894
|
+
maxStages: num(o.maxStages, "--max-stages"),
|
|
895
|
+
maxChildrenPerStage: num(o.maxChildrenPerStage, "--max-children-per-stage"),
|
|
896
|
+
appliesTo: o.plan,
|
|
897
|
+
expiresAt: o.expiresAt,
|
|
898
|
+
});
|
|
899
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : render([result], { json: false }));
|
|
900
|
+
});
|
|
435
901
|
g(plan.command("rollup").argument("[plan]"))
|
|
436
902
|
.action(async (planId, o) => {
|
|
437
903
|
const ctx = await makeCtx(resolveConfig(o));
|