@odla-ai/cli 0.23.0 → 0.25.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 +8 -0
- package/dist/bin.cjs +582 -185
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-RZDRUVBT.js → chunk-HG63GLGU.js} +583 -186
- package/dist/chunk-HG63GLGU.js.map +1 -0
- package/dist/index.cjs +582 -185
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-RZDRUVBT.js.map +0 -1
|
@@ -540,23 +540,23 @@ function adminAiAuditQuery(filters) {
|
|
|
540
540
|
}
|
|
541
541
|
return `?limit=${filters.limit}`;
|
|
542
542
|
}
|
|
543
|
-
async function readAdminAiAudit(
|
|
544
|
-
const response2 = await
|
|
545
|
-
headers:
|
|
543
|
+
async function readAdminAiAudit(request2) {
|
|
544
|
+
const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
|
|
545
|
+
headers: request2.headers
|
|
546
546
|
});
|
|
547
547
|
const body = await responseBody(response2);
|
|
548
548
|
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
549
|
-
if (
|
|
550
|
-
|
|
549
|
+
if (request2.json) {
|
|
550
|
+
request2.stdout.log(JSON.stringify(body, null, 2));
|
|
551
551
|
return;
|
|
552
552
|
}
|
|
553
553
|
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
554
|
-
|
|
554
|
+
request2.stdout.log("when change target before -> after actor");
|
|
555
555
|
for (const event of events) {
|
|
556
556
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
557
557
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
558
558
|
const route2 = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
|
|
559
|
-
|
|
559
|
+
request2.stdout.log([
|
|
560
560
|
timestamp(event.createdAt),
|
|
561
561
|
String(event.changeKind ?? ""),
|
|
562
562
|
String(event.purpose ?? event.provider ?? ""),
|
|
@@ -598,14 +598,14 @@ function adminAiUsageQuery(filters) {
|
|
|
598
598
|
const query = params.toString();
|
|
599
599
|
return query ? `?${query}` : "";
|
|
600
600
|
}
|
|
601
|
-
async function readAdminAiUsage(
|
|
602
|
-
const res = await
|
|
603
|
-
headers:
|
|
601
|
+
async function readAdminAiUsage(request2) {
|
|
602
|
+
const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
|
|
603
|
+
headers: request2.headers
|
|
604
604
|
});
|
|
605
605
|
const body = await responseBody2(res);
|
|
606
606
|
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
607
|
-
if (
|
|
608
|
-
else printUsage(body,
|
|
607
|
+
if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
|
|
608
|
+
else printUsage(body, request2.stdout);
|
|
609
609
|
}
|
|
610
610
|
function usageLimit(value) {
|
|
611
611
|
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
@@ -2381,10 +2381,10 @@ function installSkill(options = {}) {
|
|
|
2381
2381
|
current.add(target);
|
|
2382
2382
|
targets.set(harness, current);
|
|
2383
2383
|
};
|
|
2384
|
-
const plan = (target,
|
|
2384
|
+
const plan = (target, content2, managedMerge = false, boundary = root) => {
|
|
2385
2385
|
const existing = plans.get(target);
|
|
2386
|
-
if (existing && existing.content !==
|
|
2387
|
-
plans.set(target, { target, content, boundary, managedMerge });
|
|
2386
|
+
if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
|
|
2387
|
+
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
2388
2388
|
};
|
|
2389
2389
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
2390
2390
|
for (const rel of files) plan(join5(targetDir2, rel), readFileSync5(join5(sourceDir, rel), "utf8"), false, boundary);
|
|
@@ -2825,15 +2825,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
2825
2825
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
2826
2826
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
2827
2827
|
while (true) {
|
|
2828
|
-
const
|
|
2828
|
+
const state2 = await requestHostedSecurityJson(
|
|
2829
2829
|
options,
|
|
2830
2830
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
2831
2831
|
{},
|
|
2832
2832
|
"check GitHub connection"
|
|
2833
2833
|
);
|
|
2834
|
-
if (
|
|
2835
|
-
if (
|
|
2836
|
-
throw new Error(`GitHub connection ${
|
|
2834
|
+
if (state2.status !== "pending") {
|
|
2835
|
+
if (state2.status === "connected") return state2;
|
|
2836
|
+
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
2837
2837
|
}
|
|
2838
2838
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
2839
2839
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -3388,11 +3388,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
3388
3388
|
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
3389
3389
|
throw new Error("git object output has an invalid length");
|
|
3390
3390
|
}
|
|
3391
|
-
const
|
|
3391
|
+
const content2 = output.subarray(newline + 1, newline + 1 + length);
|
|
3392
3392
|
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
3393
|
-
total +=
|
|
3393
|
+
total += content2.byteLength;
|
|
3394
3394
|
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3395
|
-
blobs.push(
|
|
3395
|
+
blobs.push(content2);
|
|
3396
3396
|
offset = newline + 1 + length + 1;
|
|
3397
3397
|
}
|
|
3398
3398
|
return blobs;
|
|
@@ -3415,13 +3415,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
3415
3415
|
try {
|
|
3416
3416
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
3417
3417
|
for (const [index, entry] of entries.entries()) {
|
|
3418
|
-
const
|
|
3419
|
-
byteCount +=
|
|
3418
|
+
const content2 = blobs[index];
|
|
3419
|
+
byteCount += content2.byteLength;
|
|
3420
3420
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3421
3421
|
const target = resolve6(targetRoot, entry.path);
|
|
3422
3422
|
if (!target.startsWith(`${resolve6(targetRoot)}${sep2}`)) throw new TypeError("Git tree path escapes workspace");
|
|
3423
3423
|
await mkdir(resolve6(target, ".."), { recursive: true });
|
|
3424
|
-
await writeFile2(target,
|
|
3424
|
+
await writeFile2(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
3425
3425
|
}
|
|
3426
3426
|
return {
|
|
3427
3427
|
root,
|
|
@@ -3799,11 +3799,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
|
3799
3799
|
var MAX_PATCH_BYTES = 256 * 1024;
|
|
3800
3800
|
var MAX_STATE_BYTES = 64 * 1024;
|
|
3801
3801
|
async function createCodePortableCheckpoint(input) {
|
|
3802
|
-
const
|
|
3802
|
+
const state2 = normalizeState(input.state);
|
|
3803
3803
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
3804
3804
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
3805
|
-
const stateDigest = `sha256:${await sha256Hex(canonicalJson(
|
|
3806
|
-
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
3805
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
|
|
3806
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
3807
3807
|
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
3808
3808
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
3809
3809
|
}
|
|
@@ -3824,8 +3824,8 @@ async function verifyCodePortableCheckpoint(value) {
|
|
|
3824
3824
|
return created;
|
|
3825
3825
|
}
|
|
3826
3826
|
function normalizeState(value) {
|
|
3827
|
-
const
|
|
3828
|
-
exact2(
|
|
3827
|
+
const state2 = object(value, "state");
|
|
3828
|
+
exact2(state2, [
|
|
3829
3829
|
"planCursor",
|
|
3830
3830
|
"conversationRefs",
|
|
3831
3831
|
"planningInputDigest",
|
|
@@ -3837,13 +3837,13 @@ function normalizeState(value) {
|
|
|
3837
3837
|
"unresolvedApprovals",
|
|
3838
3838
|
"trustStatus"
|
|
3839
3839
|
]);
|
|
3840
|
-
const planCursor =
|
|
3841
|
-
const conversations = strings(
|
|
3842
|
-
const approvals = strings(
|
|
3843
|
-
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof
|
|
3840
|
+
const planCursor = state2.planCursor;
|
|
3841
|
+
const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
3842
|
+
const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
3843
|
+
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST2.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST2.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST2.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST2.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST2.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
|
|
3844
3844
|
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
3845
3845
|
}
|
|
3846
|
-
const effects =
|
|
3846
|
+
const effects = state2.completedEffects.map((item) => {
|
|
3847
3847
|
const effect = object(item, "completed effect");
|
|
3848
3848
|
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
3849
3849
|
if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
|
|
@@ -3858,18 +3858,18 @@ function normalizeState(value) {
|
|
|
3858
3858
|
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
3859
3859
|
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
3860
3860
|
}
|
|
3861
|
-
const trustStatus =
|
|
3862
|
-
if (trustStatus === "candidate_untrusted" && (
|
|
3861
|
+
const trustStatus = state2.trustStatus;
|
|
3862
|
+
if (trustStatus === "candidate_untrusted" && (state2.verificationDigest !== null || state2.reviewDigest !== null) || trustStatus === "verified" && (state2.verificationDigest === null || state2.reviewDigest !== null) || trustStatus === "reviewed" && (state2.verificationDigest === null || state2.reviewDigest === null)) {
|
|
3863
3863
|
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
3864
3864
|
}
|
|
3865
3865
|
const normalized = {
|
|
3866
3866
|
planCursor,
|
|
3867
3867
|
conversationRefs: conversations,
|
|
3868
|
-
planningInputDigest:
|
|
3869
|
-
buildPolicyDigest:
|
|
3870
|
-
dependencyLayerDigest:
|
|
3871
|
-
verificationDigest:
|
|
3872
|
-
reviewDigest:
|
|
3868
|
+
planningInputDigest: state2.planningInputDigest,
|
|
3869
|
+
buildPolicyDigest: state2.buildPolicyDigest,
|
|
3870
|
+
dependencyLayerDigest: state2.dependencyLayerDigest,
|
|
3871
|
+
verificationDigest: state2.verificationDigest,
|
|
3872
|
+
reviewDigest: state2.reviewDigest,
|
|
3873
3873
|
completedEffects: effects,
|
|
3874
3874
|
unresolvedApprovals: approvals,
|
|
3875
3875
|
trustStatus
|
|
@@ -3986,60 +3986,60 @@ async function createConversionRegistry(config) {
|
|
|
3986
3986
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
3987
3987
|
}
|
|
3988
3988
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
3989
|
-
const
|
|
3989
|
+
const get2 = (id, kind) => {
|
|
3990
3990
|
const policy = policies.get(id);
|
|
3991
3991
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
3992
3992
|
return policy;
|
|
3993
3993
|
};
|
|
3994
3994
|
const checked = (source, id, kind) => {
|
|
3995
|
-
const policy =
|
|
3995
|
+
const policy = get2(id, kind);
|
|
3996
3996
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
3997
3997
|
return policy;
|
|
3998
3998
|
};
|
|
3999
|
-
const
|
|
3999
|
+
const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
4000
4000
|
const operations = Object.freeze({
|
|
4001
4001
|
boolean: async (value, id) => {
|
|
4002
4002
|
const policy = checked(value, id, "boolean");
|
|
4003
|
-
return
|
|
4003
|
+
return emit3(value, policy, requireBoolean(value.value));
|
|
4004
4004
|
},
|
|
4005
4005
|
integer: async (value, id) => {
|
|
4006
4006
|
const policy = checked(value, id, "integer");
|
|
4007
|
-
return
|
|
4007
|
+
return emit3(value, policy, boundedInteger(value.value, policy.output));
|
|
4008
4008
|
},
|
|
4009
4009
|
finiteNumber: async (value, id) => {
|
|
4010
4010
|
const policy = checked(value, id, "finite_number");
|
|
4011
|
-
return
|
|
4011
|
+
return emit3(value, policy, boundedNumber(value.value, policy.output));
|
|
4012
4012
|
},
|
|
4013
4013
|
enum: async (value, id) => {
|
|
4014
4014
|
const policy = checked(value, id, "enum");
|
|
4015
|
-
return
|
|
4015
|
+
return emit3(value, policy, enumMember(value.value, policy.output));
|
|
4016
4016
|
},
|
|
4017
4017
|
date: async (value, id) => {
|
|
4018
4018
|
const policy = checked(value, id, "date");
|
|
4019
|
-
return
|
|
4019
|
+
return emit3(value, policy, canonicalDate(value.value, policy.output));
|
|
4020
4020
|
},
|
|
4021
4021
|
registeredId: async (value, id) => {
|
|
4022
4022
|
const policy = checked(value, id, "registered_id");
|
|
4023
4023
|
const registry = config.registeredIds?.[policy.output.registryId];
|
|
4024
4024
|
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
4025
4025
|
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
4026
|
-
return
|
|
4026
|
+
return emit3(value, policy, output);
|
|
4027
4027
|
},
|
|
4028
4028
|
digest: async (value, id) => {
|
|
4029
4029
|
const policy = checked(value, id, "digest");
|
|
4030
4030
|
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
4031
|
-
return
|
|
4031
|
+
return emit3(value, policy, await sha256Hex(value.value));
|
|
4032
4032
|
},
|
|
4033
4033
|
measure: async (value, metric, id) => {
|
|
4034
4034
|
const policy = checked(value, id, "integer");
|
|
4035
4035
|
const measured = measure(value.value, metric);
|
|
4036
|
-
return
|
|
4036
|
+
return emit3(value, policy, boundedInteger(measured, policy.output));
|
|
4037
4037
|
},
|
|
4038
4038
|
test: async (value, predicateId, id) => {
|
|
4039
4039
|
const policy = checked(value, id, "boolean");
|
|
4040
4040
|
const predicate = config.predicates?.[predicateId];
|
|
4041
4041
|
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
4042
|
-
return
|
|
4042
|
+
return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
4043
4043
|
}
|
|
4044
4044
|
});
|
|
4045
4045
|
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
@@ -4258,11 +4258,11 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
4258
4258
|
const hash = createHash2("sha256");
|
|
4259
4259
|
let bytes = 0;
|
|
4260
4260
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
4261
|
-
const
|
|
4262
|
-
bytes += Buffer.byteLength(file.path) +
|
|
4261
|
+
const content2 = await readFile2(file.target);
|
|
4262
|
+
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
4263
4263
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
4264
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${
|
|
4265
|
-
hash.update(
|
|
4264
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
4265
|
+
hash.update(content2);
|
|
4266
4266
|
}
|
|
4267
4267
|
return `sha256:${hash.digest("hex")}`;
|
|
4268
4268
|
}
|
|
@@ -4357,13 +4357,13 @@ function createCodeRuntimeControlClient(options) {
|
|
|
4357
4357
|
if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
|
|
4358
4358
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
4359
4359
|
}
|
|
4360
|
-
const
|
|
4360
|
+
const request2 = options.fetch ?? fetch;
|
|
4361
4361
|
const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
4362
4362
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4363
4363
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
4364
4364
|
let response2;
|
|
4365
4365
|
try {
|
|
4366
|
-
response2 = await
|
|
4366
|
+
response2 = await request2(`${endpoint}${path}`, {
|
|
4367
4367
|
method: "POST",
|
|
4368
4368
|
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
4369
4369
|
body: JSON.stringify(body),
|
|
@@ -5270,34 +5270,34 @@ function createCodeToolBroker(options) {
|
|
|
5270
5270
|
const policy = createCodePolicyGate(options);
|
|
5271
5271
|
let tail = Promise.resolve();
|
|
5272
5272
|
return {
|
|
5273
|
-
execute(context,
|
|
5274
|
-
const result = tail.then(() => route(context,
|
|
5273
|
+
execute(context, request2) {
|
|
5274
|
+
const result = tail.then(() => route(context, request2, options, recipes, policy));
|
|
5275
5275
|
tail = result.then(() => void 0, () => void 0);
|
|
5276
5276
|
return result;
|
|
5277
5277
|
}
|
|
5278
5278
|
};
|
|
5279
5279
|
}
|
|
5280
|
-
async function route(context,
|
|
5280
|
+
async function route(context, request2, options, recipes, policy) {
|
|
5281
5281
|
try {
|
|
5282
5282
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
5283
|
-
if (
|
|
5284
|
-
if (
|
|
5285
|
-
return await recipe(context,
|
|
5283
|
+
if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
|
|
5284
|
+
if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
|
|
5285
|
+
return await recipe(context, request2, options, recipes, policy);
|
|
5286
5286
|
} catch (reason) {
|
|
5287
|
-
return response(
|
|
5287
|
+
return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
5288
5288
|
}
|
|
5289
5289
|
}
|
|
5290
|
-
async function read(context,
|
|
5291
|
-
exactKeys(
|
|
5292
|
-
const path = stringField(
|
|
5293
|
-
const startLine = optionalInteger(
|
|
5294
|
-
const endLine = optionalInteger(
|
|
5290
|
+
async function read(context, request2, options, policy) {
|
|
5291
|
+
exactKeys(request2.input, ["path", "startLine", "endLine"]);
|
|
5292
|
+
const path = stringField(request2.input, "path");
|
|
5293
|
+
const startLine = optionalInteger(request2.input.startLine) ?? 1;
|
|
5294
|
+
const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
5295
5295
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
5296
5296
|
throw new TypeError("requested line range exceeds its bound");
|
|
5297
5297
|
}
|
|
5298
5298
|
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
5299
|
-
const allowed = await policy.read(policyContext(context,
|
|
5300
|
-
if (!allowed) return response(
|
|
5299
|
+
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
5300
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5301
5301
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
5302
5302
|
const info = await stat2(target);
|
|
5303
5303
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
@@ -5306,40 +5306,40 @@ async function read(context, request, options, policy) {
|
|
|
5306
5306
|
const source = await readFile22(target);
|
|
5307
5307
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
5308
5308
|
const lines = source.toString("utf8").split("\n");
|
|
5309
|
-
const
|
|
5310
|
-
if (Buffer.byteLength(
|
|
5309
|
+
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
5310
|
+
if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
5311
5311
|
throw new TypeError("read result exceeds its byte bound");
|
|
5312
5312
|
}
|
|
5313
|
-
return response(
|
|
5313
|
+
return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
5314
5314
|
}
|
|
5315
|
-
async function patch(context,
|
|
5316
|
-
exactKeys(
|
|
5317
|
-
const value = stringField(
|
|
5315
|
+
async function patch(context, request2, options, policy) {
|
|
5316
|
+
exactKeys(request2.input, ["patch"]);
|
|
5317
|
+
const value = stringField(request2.input, "patch");
|
|
5318
5318
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
5319
5319
|
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
5320
5320
|
throw new TypeError("patch targets a read-only reference source");
|
|
5321
5321
|
}
|
|
5322
|
-
const allowed = await policy.patch(policyContext(context,
|
|
5323
|
-
if (!allowed) return response(
|
|
5322
|
+
const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
|
|
5323
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5324
5324
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
5325
|
-
return response(
|
|
5325
|
+
return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
5326
5326
|
}
|
|
5327
|
-
async function recipe(context,
|
|
5328
|
-
exactKeys(
|
|
5329
|
-
const recipeId = stringField(
|
|
5327
|
+
async function recipe(context, request2, options, recipes, policy) {
|
|
5328
|
+
exactKeys(request2.input, ["recipeId"]);
|
|
5329
|
+
const recipeId = stringField(request2.input, "recipeId");
|
|
5330
5330
|
const digestLimits = {
|
|
5331
5331
|
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
5332
5332
|
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
5333
5333
|
};
|
|
5334
5334
|
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
5335
|
-
const allowed = await policy.recipe(policyContext(context,
|
|
5335
|
+
const allowed = await policy.recipe(policyContext(context, request2, options, {
|
|
5336
5336
|
recipeIds: [...recipes.keys()].sort(),
|
|
5337
5337
|
recipeId,
|
|
5338
5338
|
sourceDigest
|
|
5339
5339
|
}));
|
|
5340
|
-
if (!allowed) return response(
|
|
5340
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5341
5341
|
const selected = recipes.get(recipeId);
|
|
5342
|
-
if (!selected) return response(
|
|
5342
|
+
if (!selected) return response(request2, false, "build recipe is not registered");
|
|
5343
5343
|
const staged = await stageWorkspace(context.workspaceDir, {
|
|
5344
5344
|
maxFiles: digestLimits.maxFiles,
|
|
5345
5345
|
maxBytes: digestLimits.maxBytes
|
|
@@ -5356,7 +5356,7 @@ async function recipe(context, request, options, recipes, policy) {
|
|
|
5356
5356
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
5357
5357
|
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
5358
5358
|
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
5359
|
-
return response(
|
|
5359
|
+
return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
5360
5360
|
${output}` : ""}`, {
|
|
5361
5361
|
recipeId,
|
|
5362
5362
|
exitCode: result.exitCode,
|
|
@@ -5368,10 +5368,10 @@ ${output}` : ""}`, {
|
|
|
5368
5368
|
await staged.cleanup();
|
|
5369
5369
|
}
|
|
5370
5370
|
}
|
|
5371
|
-
function policyContext(context,
|
|
5371
|
+
function policyContext(context, request2, options, extra) {
|
|
5372
5372
|
return {
|
|
5373
5373
|
lease: context.lease,
|
|
5374
|
-
request,
|
|
5374
|
+
request: request2,
|
|
5375
5375
|
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
5376
5376
|
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
5377
5377
|
...extra
|
|
@@ -5421,8 +5421,8 @@ function optionalInteger(value) {
|
|
|
5421
5421
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
5422
5422
|
return value;
|
|
5423
5423
|
}
|
|
5424
|
-
function response(
|
|
5425
|
-
return { requestId:
|
|
5424
|
+
function response(request2, ok, content2, details) {
|
|
5425
|
+
return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
|
|
5426
5426
|
}
|
|
5427
5427
|
function codeCommandMetadata(payload, resume) {
|
|
5428
5428
|
const trusted = record22(payload.trustedBase);
|
|
@@ -5522,13 +5522,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
|
|
|
5522
5522
|
readerId: `code-session:${lease.task.taskId}`,
|
|
5523
5523
|
readOnlyPrefixes: [".odla-references"]
|
|
5524
5524
|
});
|
|
5525
|
-
return role === "coding" ? broker : { execute: (context,
|
|
5525
|
+
return role === "coding" ? broker : { execute: (context, request2) => request2.tool === "sandbox.read" ? broker.execute(context, request2) : Promise.resolve({ requestId: request2.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
5526
5526
|
}
|
|
5527
5527
|
async function handleCodeRuntimeInference(input) {
|
|
5528
|
-
const { command, metadata: metadata2, request, state } = input;
|
|
5529
|
-
if (
|
|
5530
|
-
if (!
|
|
5531
|
-
|
|
5528
|
+
const { command, metadata: metadata2, request: request2, state: state2 } = input;
|
|
5529
|
+
if (state2.tokens >= metadata2.maxTokensPerInteraction) {
|
|
5530
|
+
if (!state2.noticeEmitted) {
|
|
5531
|
+
state2.noticeEmitted = true;
|
|
5532
5532
|
await input.event({
|
|
5533
5533
|
type: "message",
|
|
5534
5534
|
actor: "system",
|
|
@@ -5538,7 +5538,7 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5538
5538
|
return {
|
|
5539
5539
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
5540
5540
|
type: "inference.response",
|
|
5541
|
-
requestId:
|
|
5541
|
+
requestId: request2.requestId,
|
|
5542
5542
|
response: {
|
|
5543
5543
|
id: `budget:${command.commandId}`,
|
|
5544
5544
|
provider: "openai",
|
|
@@ -5552,11 +5552,11 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5552
5552
|
}
|
|
5553
5553
|
const startedAt = Date.now();
|
|
5554
5554
|
const response2 = await input.control.infer(command.sessionId, {
|
|
5555
|
-
requestId:
|
|
5555
|
+
requestId: request2.requestId,
|
|
5556
5556
|
interactionId: command.commandId,
|
|
5557
|
-
call:
|
|
5557
|
+
call: request2.call
|
|
5558
5558
|
});
|
|
5559
|
-
|
|
5559
|
+
state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
|
|
5560
5560
|
await input.event({
|
|
5561
5561
|
type: "usage",
|
|
5562
5562
|
provider: response2.receipt.provider,
|
|
@@ -5565,13 +5565,13 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5565
5565
|
outputTokens: response2.receipt.outputTokens,
|
|
5566
5566
|
durationMs: Date.now() - startedAt,
|
|
5567
5567
|
interactionId: command.commandId,
|
|
5568
|
-
interactionTokens:
|
|
5568
|
+
interactionTokens: state2.tokens,
|
|
5569
5569
|
interactionMaxTokens: metadata2.maxTokensPerInteraction
|
|
5570
5570
|
}).catch(() => void 0);
|
|
5571
5571
|
return {
|
|
5572
5572
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
5573
5573
|
type: "inference.response",
|
|
5574
|
-
requestId:
|
|
5574
|
+
requestId: request2.requestId,
|
|
5575
5575
|
response: response2.response
|
|
5576
5576
|
};
|
|
5577
5577
|
}
|
|
@@ -5931,13 +5931,22 @@ Usage:
|
|
|
5931
5931
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
5932
5932
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
5933
5933
|
odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
|
|
5934
|
-
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
|
|
5934
|
+
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
|
|
5935
5935
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
5936
|
-
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
|
|
5937
|
-
odla-ai pm <goal|task|decision|bug> done <id>
|
|
5936
|
+
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
|
|
5937
|
+
odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
|
|
5938
5938
|
odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
|
|
5939
5939
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
5940
5940
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
5941
|
+
odla-ai pm handoff --app <id> [--json]
|
|
5942
|
+
odla-ai discuss groups [--json]
|
|
5943
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
5944
|
+
odla-ai discuss read <topic> [--json]
|
|
5945
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
|
|
5946
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."]
|
|
5947
|
+
odla-ai discuss resolve <topic> [--reopen]
|
|
5948
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
5949
|
+
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
5941
5950
|
odla-ai whoami [--json]
|
|
5942
5951
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
5943
5952
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6042,6 +6051,13 @@ Commands:
|
|
|
6042
6051
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6043
6052
|
decision, bug. Status changes and comments post to each item's
|
|
6044
6053
|
@odla-ai/chat discussion thread.
|
|
6054
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
6055
|
+
one group per project, topics with replies, @-mentions of people,
|
|
6056
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
6057
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
6058
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
6059
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
6060
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
6045
6061
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6046
6062
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6047
6063
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6781,10 +6797,25 @@ var COMMAND_SURFACE = {
|
|
|
6781
6797
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
6782
6798
|
capabilities: {},
|
|
6783
6799
|
code: { connect: {} },
|
|
6800
|
+
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
6801
|
+
discuss: {
|
|
6802
|
+
groups: {},
|
|
6803
|
+
list: {},
|
|
6804
|
+
topics: {},
|
|
6805
|
+
read: {},
|
|
6806
|
+
post: {},
|
|
6807
|
+
reply: {},
|
|
6808
|
+
resolve: {},
|
|
6809
|
+
who: {},
|
|
6810
|
+
watch: {}
|
|
6811
|
+
},
|
|
6784
6812
|
doctor: {},
|
|
6785
6813
|
help: {},
|
|
6786
6814
|
init: {},
|
|
6787
|
-
pm:
|
|
6815
|
+
pm: {
|
|
6816
|
+
...PM_ENTITIES,
|
|
6817
|
+
handoff: {}
|
|
6818
|
+
},
|
|
6788
6819
|
provision: {},
|
|
6789
6820
|
runbook: {
|
|
6790
6821
|
ask: {},
|
|
@@ -6927,17 +6958,17 @@ async function runHostedSecurity(options) {
|
|
|
6927
6958
|
allowNetwork: false
|
|
6928
6959
|
}
|
|
6929
6960
|
});
|
|
6930
|
-
const
|
|
6931
|
-
await writeSecurityArtifacts(output,
|
|
6932
|
-
const reportDigest = await securityFingerprint(
|
|
6961
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
6962
|
+
await writeSecurityArtifacts(output, report4);
|
|
6963
|
+
const reportDigest = await securityFingerprint(report4);
|
|
6933
6964
|
await hosted.complete({
|
|
6934
6965
|
reportDigest,
|
|
6935
|
-
coverageStatus:
|
|
6936
|
-
confirmed:
|
|
6937
|
-
candidates:
|
|
6966
|
+
coverageStatus: report4.coverageStatus,
|
|
6967
|
+
confirmed: report4.metrics.confirmed,
|
|
6968
|
+
candidates: report4.metrics.candidates
|
|
6938
6969
|
}, { signal: options.signal });
|
|
6939
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
6940
|
-
return Object.freeze({ report:
|
|
6970
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
6971
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
6941
6972
|
}
|
|
6942
6973
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
6943
6974
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -6947,11 +6978,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
6947
6978
|
}
|
|
6948
6979
|
return env;
|
|
6949
6980
|
}
|
|
6950
|
-
async function injectedToken(options,
|
|
6951
|
-
const value = options.token ?? await options.getToken?.(Object.freeze({ ...
|
|
6981
|
+
async function injectedToken(options, request2) {
|
|
6982
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
|
|
6952
6983
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
6953
6984
|
throw new Error(
|
|
6954
|
-
|
|
6985
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
6955
6986
|
);
|
|
6956
6987
|
}
|
|
6957
6988
|
return value;
|
|
@@ -6963,14 +6994,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
6963
6994
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
6964
6995
|
return { ...profile, maxHuntTasks };
|
|
6965
6996
|
}
|
|
6966
|
-
function printSummary(out, appId, env, run,
|
|
6967
|
-
const complete =
|
|
6997
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
6998
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
6968
6999
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
6969
7000
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
6970
7001
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
6971
|
-
out.log(` coverage: ${
|
|
6972
|
-
if (
|
|
6973
|
-
out.log(` findings: confirmed=${
|
|
7002
|
+
out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
7003
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
7004
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
6974
7005
|
out.log(` report: ${resolve9(output, "REPORT.md")}`);
|
|
6975
7006
|
}
|
|
6976
7007
|
function formatBudget(usage) {
|
|
@@ -7974,6 +8005,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7974
8005
|
});
|
|
7975
8006
|
}
|
|
7976
8007
|
|
|
8008
|
+
// src/discuss-actions.ts
|
|
8009
|
+
async function request(ctx, method, path, body) {
|
|
8010
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
8011
|
+
method,
|
|
8012
|
+
headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
|
|
8013
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
8014
|
+
});
|
|
8015
|
+
const data = await res.json().catch(() => ({}));
|
|
8016
|
+
if (!res.ok)
|
|
8017
|
+
throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
8018
|
+
return data;
|
|
8019
|
+
}
|
|
8020
|
+
function emit(ctx, value, human) {
|
|
8021
|
+
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
8022
|
+
else human();
|
|
8023
|
+
}
|
|
8024
|
+
var state = (topic) => topic.resolvedAt ? "resolved" : "open";
|
|
8025
|
+
function bodyWithRefs(post) {
|
|
8026
|
+
if (!post.refs || post.refs.length === 0) return post.body;
|
|
8027
|
+
let out = "";
|
|
8028
|
+
let cursor = 0;
|
|
8029
|
+
for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
|
|
8030
|
+
if (ref.start < cursor || ref.end > post.body.length) continue;
|
|
8031
|
+
out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
|
|
8032
|
+
cursor = ref.end;
|
|
8033
|
+
}
|
|
8034
|
+
return out + post.body.slice(cursor);
|
|
8035
|
+
}
|
|
8036
|
+
function content(parsed) {
|
|
8037
|
+
const markup = stringOpt(parsed.options.markup);
|
|
8038
|
+
if (markup) return { markup };
|
|
8039
|
+
const body = stringOpt(parsed.options.body);
|
|
8040
|
+
if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
|
|
8041
|
+
return { body };
|
|
8042
|
+
}
|
|
8043
|
+
async function discussGroups(ctx) {
|
|
8044
|
+
const result = await request(
|
|
8045
|
+
ctx,
|
|
8046
|
+
"GET",
|
|
8047
|
+
"/groups"
|
|
8048
|
+
);
|
|
8049
|
+
emit(ctx, result, () => {
|
|
8050
|
+
ctx.out.log("app name started");
|
|
8051
|
+
for (const group of result.groups) {
|
|
8052
|
+
ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
|
|
8053
|
+
}
|
|
8054
|
+
});
|
|
8055
|
+
}
|
|
8056
|
+
async function discussList(ctx, parsed) {
|
|
8057
|
+
const query = new URLSearchParams();
|
|
8058
|
+
for (const [flag, param] of [
|
|
8059
|
+
["app", "app"],
|
|
8060
|
+
["q", "q"],
|
|
8061
|
+
["state", "state"],
|
|
8062
|
+
["limit", "limit"],
|
|
8063
|
+
["offset", "offset"]
|
|
8064
|
+
]) {
|
|
8065
|
+
const value = stringOpt(parsed.options[flag]);
|
|
8066
|
+
if (value) query.set(param, value);
|
|
8067
|
+
}
|
|
8068
|
+
const qs = query.toString();
|
|
8069
|
+
const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
8070
|
+
emit(ctx, page, () => {
|
|
8071
|
+
ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
|
|
8072
|
+
ctx.out.log("id state app replies subject");
|
|
8073
|
+
for (const topic of page.topics) {
|
|
8074
|
+
ctx.out.log(
|
|
8075
|
+
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
8080
|
+
async function discussRead(ctx, id) {
|
|
8081
|
+
const result = await request(
|
|
8082
|
+
ctx,
|
|
8083
|
+
"GET",
|
|
8084
|
+
`/topics/${encodeURIComponent(id)}`
|
|
8085
|
+
);
|
|
8086
|
+
emit(ctx, result, () => {
|
|
8087
|
+
ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
|
|
8088
|
+
for (const post of result.posts) {
|
|
8089
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
8090
|
+
ctx.out.log(`
|
|
8091
|
+
\u2014 ${who}`);
|
|
8092
|
+
ctx.out.log(bodyWithRefs(post));
|
|
8093
|
+
for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
|
|
8094
|
+
}
|
|
8095
|
+
});
|
|
8096
|
+
}
|
|
8097
|
+
async function discussPost(ctx, parsed) {
|
|
8098
|
+
const appId = stringOpt(parsed.options.app);
|
|
8099
|
+
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
8100
|
+
const subject = stringOpt(parsed.options.subject);
|
|
8101
|
+
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
8102
|
+
const created = await request(ctx, "POST", "/topics", {
|
|
8103
|
+
appId,
|
|
8104
|
+
subject,
|
|
8105
|
+
...content(parsed)
|
|
8106
|
+
});
|
|
8107
|
+
emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
|
|
8108
|
+
}
|
|
8109
|
+
async function discussReply(ctx, id, parsed) {
|
|
8110
|
+
const created = await request(
|
|
8111
|
+
ctx,
|
|
8112
|
+
"POST",
|
|
8113
|
+
`/topics/${encodeURIComponent(id)}/replies`,
|
|
8114
|
+
content(parsed)
|
|
8115
|
+
);
|
|
8116
|
+
emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
|
|
8117
|
+
}
|
|
8118
|
+
async function discussResolve(ctx, id, resolved) {
|
|
8119
|
+
const result = await request(
|
|
8120
|
+
ctx,
|
|
8121
|
+
"PATCH",
|
|
8122
|
+
`/topics/${encodeURIComponent(id)}`,
|
|
8123
|
+
{ resolved }
|
|
8124
|
+
);
|
|
8125
|
+
emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
|
|
8126
|
+
}
|
|
8127
|
+
async function discussWho(ctx, parsed) {
|
|
8128
|
+
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
8129
|
+
const app = stringOpt(parsed.options.app);
|
|
8130
|
+
if (app) query.set("app", app);
|
|
8131
|
+
const kinds = stringOpt(parsed.options.kinds);
|
|
8132
|
+
if (kinds) query.set("kinds", kinds);
|
|
8133
|
+
const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
|
|
8134
|
+
emit(ctx, found, () => {
|
|
8135
|
+
ctx.out.log("mention kind hint");
|
|
8136
|
+
for (const item of found.items) {
|
|
8137
|
+
ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
|
|
8138
|
+
}
|
|
8139
|
+
if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
|
|
8140
|
+
});
|
|
8141
|
+
}
|
|
8142
|
+
|
|
8143
|
+
// src/discuss-watch.ts
|
|
8144
|
+
var DEFAULT_INTERVAL_MS = 15e3;
|
|
8145
|
+
var DEFAULT_TIMEOUT_MS = 15 * 6e4;
|
|
8146
|
+
function numberOpt2(parsed, flag, fallback) {
|
|
8147
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
8148
|
+
const value = raw == null ? NaN : Number(raw);
|
|
8149
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
8150
|
+
}
|
|
8151
|
+
async function get(ctx, path) {
|
|
8152
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
8153
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
8154
|
+
});
|
|
8155
|
+
const data = await res.json().catch(() => ({}));
|
|
8156
|
+
if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
8157
|
+
return data;
|
|
8158
|
+
}
|
|
8159
|
+
async function discussWatch(ctx, topicId, parsed) {
|
|
8160
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
8161
|
+
const now = ctx.now ?? Date.now;
|
|
8162
|
+
const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
8163
|
+
const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
|
|
8164
|
+
const by = stringOpt(parsed.options.by);
|
|
8165
|
+
const self = stringOpt(parsed.options.self);
|
|
8166
|
+
const deadline = now() + timeoutMs;
|
|
8167
|
+
const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
|
|
8168
|
+
let seen = /* @__PURE__ */ new Set();
|
|
8169
|
+
let since = now();
|
|
8170
|
+
if (topicId) {
|
|
8171
|
+
const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
8172
|
+
seen = new Set(first.posts.map((post) => post.id));
|
|
8173
|
+
}
|
|
8174
|
+
for (; ; ) {
|
|
8175
|
+
if (topicId) {
|
|
8176
|
+
const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
8177
|
+
const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
|
|
8178
|
+
if (added.length > 0) return report2(ctx, { found: true, posts: added });
|
|
8179
|
+
for (const post of posts) seen.add(post.id);
|
|
8180
|
+
} else {
|
|
8181
|
+
const app = stringOpt(parsed.options.app);
|
|
8182
|
+
const { topics } = await get(
|
|
8183
|
+
ctx,
|
|
8184
|
+
`/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
|
|
8185
|
+
);
|
|
8186
|
+
const active = topics.filter((topic) => topic.lastActivityAt > since);
|
|
8187
|
+
if (active.length > 0) return report2(ctx, { found: true, topics: active });
|
|
8188
|
+
since = now();
|
|
8189
|
+
}
|
|
8190
|
+
if (now() >= deadline) return report2(ctx, { found: false });
|
|
8191
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
function report2(ctx, result) {
|
|
8195
|
+
if (ctx.json) {
|
|
8196
|
+
ctx.out.log(JSON.stringify(result, null, 2));
|
|
8197
|
+
} else if (result.found) {
|
|
8198
|
+
for (const post of result.posts ?? []) {
|
|
8199
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
8200
|
+
ctx.out.log(`\u2014 ${who}
|
|
8201
|
+
${post.body}`);
|
|
8202
|
+
}
|
|
8203
|
+
for (const topic of result.topics ?? []) {
|
|
8204
|
+
ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
return result;
|
|
8208
|
+
}
|
|
8209
|
+
var WatchTimeoutError = class extends Error {
|
|
8210
|
+
code = "watch_timeout";
|
|
8211
|
+
constructor() {
|
|
8212
|
+
super("nothing new before the timeout");
|
|
8213
|
+
this.name = "WatchTimeoutError";
|
|
8214
|
+
}
|
|
8215
|
+
};
|
|
8216
|
+
|
|
8217
|
+
// src/discuss-command.ts
|
|
8218
|
+
var ALLOWED = [
|
|
8219
|
+
"config",
|
|
8220
|
+
"token",
|
|
8221
|
+
"email",
|
|
8222
|
+
"json",
|
|
8223
|
+
"app",
|
|
8224
|
+
"subject",
|
|
8225
|
+
"body",
|
|
8226
|
+
"markup",
|
|
8227
|
+
"state",
|
|
8228
|
+
"q",
|
|
8229
|
+
"kinds",
|
|
8230
|
+
"limit",
|
|
8231
|
+
"offset",
|
|
8232
|
+
"reopen",
|
|
8233
|
+
"by",
|
|
8234
|
+
"self",
|
|
8235
|
+
"interval",
|
|
8236
|
+
"timeout"
|
|
8237
|
+
];
|
|
8238
|
+
function requireId(id, action) {
|
|
8239
|
+
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
8240
|
+
return id;
|
|
8241
|
+
}
|
|
8242
|
+
async function buildContext(parsed, deps) {
|
|
8243
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
8244
|
+
const doFetch = deps.fetch ?? fetch;
|
|
8245
|
+
const out = deps.stdout ?? console;
|
|
8246
|
+
const token = await getDeveloperToken(
|
|
8247
|
+
cfg,
|
|
8248
|
+
{
|
|
8249
|
+
configPath: cfg.configPath,
|
|
8250
|
+
token: stringOpt(parsed.options.token),
|
|
8251
|
+
email: stringOpt(parsed.options.email),
|
|
8252
|
+
open: false
|
|
8253
|
+
},
|
|
8254
|
+
doFetch,
|
|
8255
|
+
out
|
|
8256
|
+
);
|
|
8257
|
+
return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
|
|
8258
|
+
}
|
|
8259
|
+
async function discussCommand(parsed, deps = {}) {
|
|
8260
|
+
assertArgs(parsed, ALLOWED, 3);
|
|
8261
|
+
const action = parsed.positionals[1];
|
|
8262
|
+
const id = parsed.positionals[2];
|
|
8263
|
+
if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
8264
|
+
const ctx = await buildContext(parsed, deps);
|
|
8265
|
+
switch (action) {
|
|
8266
|
+
case "groups":
|
|
8267
|
+
return discussGroups(ctx);
|
|
8268
|
+
case "list":
|
|
8269
|
+
case "topics":
|
|
8270
|
+
return discussList(ctx, parsed);
|
|
8271
|
+
case "read":
|
|
8272
|
+
return discussRead(ctx, requireId(id, "read"));
|
|
8273
|
+
case "post":
|
|
8274
|
+
return discussPost(ctx, parsed);
|
|
8275
|
+
case "reply":
|
|
8276
|
+
return discussReply(ctx, requireId(id, "reply"), parsed);
|
|
8277
|
+
case "resolve":
|
|
8278
|
+
return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
|
|
8279
|
+
case "who":
|
|
8280
|
+
return discussWho(ctx, parsed);
|
|
8281
|
+
case "watch": {
|
|
8282
|
+
const result = await discussWatch(ctx, id, parsed);
|
|
8283
|
+
if (!result.found) throw new WatchTimeoutError();
|
|
8284
|
+
return;
|
|
8285
|
+
}
|
|
8286
|
+
default:
|
|
8287
|
+
throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
|
|
7977
8291
|
// src/pm-actions.ts
|
|
7978
8292
|
var FIELD_MAP = {
|
|
7979
8293
|
title: { key: "title" },
|
|
@@ -7982,6 +8296,7 @@ var FIELD_MAP = {
|
|
|
7982
8296
|
column: { key: "column" },
|
|
7983
8297
|
rank: { key: "rank", num: true },
|
|
7984
8298
|
goal: { key: "goalId" },
|
|
8299
|
+
decision: { key: "decisionId" },
|
|
7985
8300
|
assignee: { key: "assigneeId" },
|
|
7986
8301
|
due: { key: "dueAt", num: true },
|
|
7987
8302
|
proof: { key: "proof" },
|
|
@@ -8020,6 +8335,14 @@ function collectFields(parsed, allowClear) {
|
|
|
8020
8335
|
}
|
|
8021
8336
|
return out;
|
|
8022
8337
|
}
|
|
8338
|
+
function collectEntityFields(entity, parsed, allowClear) {
|
|
8339
|
+
const fields = collectFields(parsed, allowClear);
|
|
8340
|
+
if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
|
|
8341
|
+
if (fields.description === void 0) fields.description = fields.body;
|
|
8342
|
+
delete fields.body;
|
|
8343
|
+
}
|
|
8344
|
+
return fields;
|
|
8345
|
+
}
|
|
8023
8346
|
function statusCol(entity, r) {
|
|
8024
8347
|
if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
|
|
8025
8348
|
if (entity === "task") return String(r.column ?? "");
|
|
@@ -8028,13 +8351,20 @@ function statusCol(entity, r) {
|
|
|
8028
8351
|
function printRecord(ctx, entity, r) {
|
|
8029
8352
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
8030
8353
|
}
|
|
8031
|
-
function
|
|
8354
|
+
function emit2(ctx, value, human) {
|
|
8032
8355
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
8033
8356
|
else human();
|
|
8034
8357
|
}
|
|
8035
8358
|
async function pmList(ctx, entity, parsed) {
|
|
8036
8359
|
const q = new URLSearchParams();
|
|
8037
|
-
const filters = {
|
|
8360
|
+
const filters = {
|
|
8361
|
+
status: "status",
|
|
8362
|
+
severity: "severity",
|
|
8363
|
+
column: "column",
|
|
8364
|
+
goal: "goalId",
|
|
8365
|
+
assignee: "assigneeId",
|
|
8366
|
+
decision: "decisionId"
|
|
8367
|
+
};
|
|
8038
8368
|
const app = stringOpt(parsed.options.app);
|
|
8039
8369
|
if (app) q.set("app", app);
|
|
8040
8370
|
for (const [flag, param] of Object.entries(filters)) {
|
|
@@ -8047,7 +8377,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
8047
8377
|
}
|
|
8048
8378
|
const qs = q.toString();
|
|
8049
8379
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
8050
|
-
|
|
8380
|
+
emit2(ctx, page, () => {
|
|
8051
8381
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
8052
8382
|
ctx.out.log("id state app title");
|
|
8053
8383
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -8056,25 +8386,82 @@ async function pmList(ctx, entity, parsed) {
|
|
|
8056
8386
|
async function pmAdd(ctx, entity, parsed) {
|
|
8057
8387
|
const appId = stringOpt(parsed.options.app);
|
|
8058
8388
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
8059
|
-
const input =
|
|
8389
|
+
const input = collectEntityFields(entity, parsed, false);
|
|
8060
8390
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
8391
|
+
if (entity === "bug" && !input.description)
|
|
8392
|
+
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
8061
8393
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
8062
|
-
|
|
8394
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
8063
8395
|
}
|
|
8064
8396
|
async function pmGet(ctx, entity, id) {
|
|
8065
8397
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
8066
|
-
|
|
8398
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
8067
8399
|
}
|
|
8068
8400
|
async function pmSet(ctx, entity, id, parsed) {
|
|
8069
|
-
const patch2 =
|
|
8401
|
+
const patch2 = collectEntityFields(entity, parsed, true);
|
|
8070
8402
|
if (Object.keys(patch2).length === 0)
|
|
8071
8403
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
8072
8404
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
8073
|
-
|
|
8405
|
+
emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
8074
8406
|
}
|
|
8075
|
-
async function pmDone(ctx, entity, id) {
|
|
8076
|
-
const
|
|
8077
|
-
|
|
8407
|
+
async function pmDone(ctx, entity, id, parsed) {
|
|
8408
|
+
const decisionId = stringOpt(parsed.options.decision);
|
|
8409
|
+
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
8410
|
+
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
8411
|
+
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
8412
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
8413
|
+
}
|
|
8414
|
+
async function allRecords(ctx, entity, appId) {
|
|
8415
|
+
const records = [];
|
|
8416
|
+
for (; ; ) {
|
|
8417
|
+
const q = new URLSearchParams({
|
|
8418
|
+
app: appId,
|
|
8419
|
+
limit: "100",
|
|
8420
|
+
offset: String(records.length)
|
|
8421
|
+
});
|
|
8422
|
+
const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
8423
|
+
records.push(...page.records);
|
|
8424
|
+
if (records.length >= page.total || page.records.length === 0) return records;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
async function pmHandoff(ctx, parsed) {
|
|
8428
|
+
const appId = stringOpt(parsed.options.app);
|
|
8429
|
+
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
8430
|
+
const [goals, tasks, bugs] = await Promise.all([
|
|
8431
|
+
allRecords(ctx, "goal", appId),
|
|
8432
|
+
allRecords(ctx, "task", appId),
|
|
8433
|
+
allRecords(ctx, "bug", appId)
|
|
8434
|
+
]);
|
|
8435
|
+
const handoff = {
|
|
8436
|
+
appId,
|
|
8437
|
+
unmetGoals: goals.filter((record5) => record5.status !== "met"),
|
|
8438
|
+
activeTasks: tasks.filter((record5) => record5.column !== "done"),
|
|
8439
|
+
openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
|
|
8440
|
+
};
|
|
8441
|
+
const result = {
|
|
8442
|
+
...handoff,
|
|
8443
|
+
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
8444
|
+
};
|
|
8445
|
+
emit2(ctx, result, () => {
|
|
8446
|
+
if (result.clean) {
|
|
8447
|
+
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
8450
|
+
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
8451
|
+
for (const [label, records] of [
|
|
8452
|
+
["unmet goals", result.unmetGoals],
|
|
8453
|
+
["active tasks", result.activeTasks],
|
|
8454
|
+
["open bugs", result.openBugs]
|
|
8455
|
+
]) {
|
|
8456
|
+
ctx.out.log(`${label}:`);
|
|
8457
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
8458
|
+
else for (const record5 of records) printRecord(
|
|
8459
|
+
ctx,
|
|
8460
|
+
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
8461
|
+
record5
|
|
8462
|
+
);
|
|
8463
|
+
}
|
|
8464
|
+
});
|
|
8078
8465
|
}
|
|
8079
8466
|
async function pmComment(ctx, entity, id, parsed) {
|
|
8080
8467
|
const body = stringOpt(parsed.options.body);
|
|
@@ -8088,7 +8475,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
8088
8475
|
"GET",
|
|
8089
8476
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
8090
8477
|
);
|
|
8091
|
-
|
|
8478
|
+
emit2(ctx, messages, () => {
|
|
8092
8479
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
8093
8480
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
8094
8481
|
});
|
|
@@ -8107,7 +8494,7 @@ var ALIASES = {
|
|
|
8107
8494
|
decision: "decision",
|
|
8108
8495
|
bug: "bug"
|
|
8109
8496
|
};
|
|
8110
|
-
var
|
|
8497
|
+
var ALLOWED2 = [
|
|
8111
8498
|
"config",
|
|
8112
8499
|
"token",
|
|
8113
8500
|
"email",
|
|
@@ -8126,15 +8513,16 @@ var ALLOWED = [
|
|
|
8126
8513
|
"target",
|
|
8127
8514
|
"description",
|
|
8128
8515
|
"desc",
|
|
8516
|
+
"decision",
|
|
8129
8517
|
"limit",
|
|
8130
8518
|
"offset",
|
|
8131
8519
|
"q"
|
|
8132
8520
|
];
|
|
8133
|
-
function
|
|
8521
|
+
function requireId2(id, action) {
|
|
8134
8522
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
8135
8523
|
return id;
|
|
8136
8524
|
}
|
|
8137
|
-
async function
|
|
8525
|
+
async function buildContext2(parsed, deps) {
|
|
8138
8526
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
8139
8527
|
const doFetch = deps.fetch ?? fetch;
|
|
8140
8528
|
const out = deps.stdout ?? console;
|
|
@@ -8148,11 +8536,15 @@ async function buildContext(parsed, deps) {
|
|
|
8148
8536
|
}
|
|
8149
8537
|
async function pmCommand(parsed, deps = {}) {
|
|
8150
8538
|
const word = parsed.positionals[1] ?? "";
|
|
8539
|
+
if (word === "handoff") {
|
|
8540
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
8541
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
8542
|
+
}
|
|
8151
8543
|
const entity = ALIASES[word];
|
|
8152
8544
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
8153
8545
|
const action = parsed.positionals[2] ?? "list";
|
|
8154
|
-
assertArgs(parsed,
|
|
8155
|
-
const ctx = await
|
|
8546
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
8547
|
+
const ctx = await buildContext2(parsed, deps);
|
|
8156
8548
|
const id = parsed.positionals[3];
|
|
8157
8549
|
switch (action) {
|
|
8158
8550
|
case "list":
|
|
@@ -8161,21 +8553,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
8161
8553
|
case "create":
|
|
8162
8554
|
return pmAdd(ctx, entity, parsed);
|
|
8163
8555
|
case "get":
|
|
8164
|
-
return pmGet(ctx, entity,
|
|
8556
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
8165
8557
|
case "set":
|
|
8166
8558
|
case "update":
|
|
8167
8559
|
case "status":
|
|
8168
8560
|
case "move":
|
|
8169
|
-
return pmSet(ctx, entity,
|
|
8561
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
8170
8562
|
case "done":
|
|
8171
|
-
return pmDone(ctx, entity,
|
|
8563
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
8172
8564
|
case "comment":
|
|
8173
|
-
return pmComment(ctx, entity,
|
|
8565
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
8174
8566
|
case "comments":
|
|
8175
|
-
return pmComments(ctx, entity,
|
|
8567
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
8176
8568
|
case "rm":
|
|
8177
8569
|
case "delete":
|
|
8178
|
-
return pmRemove(ctx, entity,
|
|
8570
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
8179
8571
|
default:
|
|
8180
8572
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
8181
8573
|
}
|
|
@@ -8691,7 +9083,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8691
9083
|
return out;
|
|
8692
9084
|
}
|
|
8693
9085
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8694
|
-
function
|
|
9086
|
+
function report3(ctx, impacts) {
|
|
8695
9087
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8696
9088
|
ctx.out.log(
|
|
8697
9089
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -8729,7 +9121,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8729
9121
|
}
|
|
8730
9122
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8731
9123
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8732
|
-
|
|
9124
|
+
report3(ctx, impacts);
|
|
8733
9125
|
}
|
|
8734
9126
|
|
|
8735
9127
|
// src/runbook-lint.ts
|
|
@@ -8962,7 +9354,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
8962
9354
|
}
|
|
8963
9355
|
|
|
8964
9356
|
// src/runbook-command.ts
|
|
8965
|
-
var
|
|
9357
|
+
var ALLOWED3 = [
|
|
8966
9358
|
"config",
|
|
8967
9359
|
"token",
|
|
8968
9360
|
"email",
|
|
@@ -9002,7 +9394,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
9002
9394
|
}
|
|
9003
9395
|
return loadProjectConfig(configPath);
|
|
9004
9396
|
}
|
|
9005
|
-
async function
|
|
9397
|
+
async function buildContext3(parsed, deps, action) {
|
|
9006
9398
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
9007
9399
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
9008
9400
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -9058,8 +9450,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
9058
9450
|
}
|
|
9059
9451
|
async function runbookCommand(parsed, deps = {}) {
|
|
9060
9452
|
const action = parsed.positionals[1] ?? "list";
|
|
9061
|
-
assertArgs(parsed,
|
|
9062
|
-
const ctx = await
|
|
9453
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9454
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
9063
9455
|
const slug = parsed.positionals[2];
|
|
9064
9456
|
switch (action) {
|
|
9065
9457
|
case "list":
|
|
@@ -9250,31 +9642,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
9250
9642
|
url.searchParams.set("job", job.jobId);
|
|
9251
9643
|
out.log(` Studio: ${url.toString()}`);
|
|
9252
9644
|
}
|
|
9253
|
-
function printHostedReport(out,
|
|
9254
|
-
out.log(`security report ${
|
|
9255
|
-
out.log(` coverage: ${
|
|
9256
|
-
out.log(` findings: confirmed=${
|
|
9257
|
-
out.log(` discovery: ${
|
|
9258
|
-
out.log(` validation: ${
|
|
9259
|
-
for (const finding of
|
|
9645
|
+
function printHostedReport(out, report4) {
|
|
9646
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
9647
|
+
out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
9648
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
9649
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
9650
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
9651
|
+
for (const finding of report4.findings) {
|
|
9260
9652
|
const location = finding.locations[0];
|
|
9261
9653
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9262
9654
|
}
|
|
9263
|
-
for (const limitation of
|
|
9655
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9264
9656
|
}
|
|
9265
|
-
function enforceHostedReportGate(
|
|
9657
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9266
9658
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9267
9659
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9268
9660
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9269
9661
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9270
|
-
const confirmed =
|
|
9271
|
-
const leads = failOnCandidates ?
|
|
9272
|
-
const incomplete =
|
|
9662
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
9663
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
9664
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
9273
9665
|
if (confirmed.length || leads.length || incomplete) {
|
|
9274
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
9666
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
9275
9667
|
}
|
|
9276
9668
|
if (emitSuccess) {
|
|
9277
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
9669
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
9278
9670
|
}
|
|
9279
9671
|
}
|
|
9280
9672
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9378,13 +9770,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9378
9770
|
}
|
|
9379
9771
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9380
9772
|
}
|
|
9381
|
-
const
|
|
9773
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9382
9774
|
if (parsed.options.json === true) {
|
|
9383
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
9775
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
9384
9776
|
} else {
|
|
9385
|
-
printHostedReport(context.stdout,
|
|
9777
|
+
printHostedReport(context.stdout, report4);
|
|
9386
9778
|
}
|
|
9387
|
-
enforceHostedReportGate(
|
|
9779
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9388
9780
|
}
|
|
9389
9781
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9390
9782
|
if (parsed.options.source === true) {
|
|
@@ -9425,11 +9817,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9425
9817
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9426
9818
|
fetch: doFetch,
|
|
9427
9819
|
stdout: out,
|
|
9428
|
-
getToken: async (
|
|
9429
|
-
if (
|
|
9820
|
+
getToken: async (request2) => {
|
|
9821
|
+
if (request2.scope === "platform:security:self") {
|
|
9430
9822
|
return getScopedPlatformToken({
|
|
9431
|
-
platform:
|
|
9432
|
-
scope:
|
|
9823
|
+
platform: request2.platform,
|
|
9824
|
+
scope: request2.scope,
|
|
9433
9825
|
email: stringOpt(parsed.options.email),
|
|
9434
9826
|
open,
|
|
9435
9827
|
fetch: doFetch,
|
|
@@ -9438,7 +9830,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9438
9830
|
});
|
|
9439
9831
|
}
|
|
9440
9832
|
const cfg = await loadProjectConfig(configPath);
|
|
9441
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9833
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9442
9834
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9443
9835
|
}
|
|
9444
9836
|
return getDeveloperToken(
|
|
@@ -9451,13 +9843,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9451
9843
|
});
|
|
9452
9844
|
enforceLocalGate(result.report, parsed);
|
|
9453
9845
|
}
|
|
9454
|
-
function enforceLocalGate(
|
|
9846
|
+
function enforceLocalGate(report4, parsed) {
|
|
9455
9847
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9456
9848
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9457
9849
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9458
|
-
const confirmed = findingsAtOrAbove(
|
|
9459
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
9460
|
-
const incomplete =
|
|
9850
|
+
const confirmed = findingsAtOrAbove(report4, failOn);
|
|
9851
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
9852
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
9461
9853
|
if (confirmed.length || leads.length || incomplete) {
|
|
9462
9854
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9463
9855
|
}
|
|
@@ -9496,9 +9888,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9496
9888
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9497
9889
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9498
9890
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9499
|
-
const
|
|
9500
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9501
|
-
else printHostedReport(context.stdout,
|
|
9891
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
9892
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
9893
|
+
else printHostedReport(context.stdout, report4);
|
|
9502
9894
|
return;
|
|
9503
9895
|
}
|
|
9504
9896
|
if (sub !== "run") {
|
|
@@ -9582,7 +9974,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9582
9974
|
|
|
9583
9975
|
// src/cli.ts
|
|
9584
9976
|
function exitCodeFor(err) {
|
|
9585
|
-
|
|
9977
|
+
const code = err?.code;
|
|
9978
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9586
9979
|
}
|
|
9587
9980
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9588
9981
|
const parsed = parseArgv(argv);
|
|
@@ -9630,6 +10023,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9630
10023
|
await pmCommand(parsed, dependencies);
|
|
9631
10024
|
return;
|
|
9632
10025
|
}
|
|
10026
|
+
if (command === "discuss") {
|
|
10027
|
+
await discussCommand(parsed, dependencies);
|
|
10028
|
+
return;
|
|
10029
|
+
}
|
|
9633
10030
|
if (command === "provision") {
|
|
9634
10031
|
await provisionCommand(parsed, dependencies);
|
|
9635
10032
|
return;
|
|
@@ -9749,4 +10146,4 @@ export {
|
|
|
9749
10146
|
exitCodeFor,
|
|
9750
10147
|
runCli
|
|
9751
10148
|
};
|
|
9752
|
-
//# sourceMappingURL=chunk-
|
|
10149
|
+
//# sourceMappingURL=chunk-HG63GLGU.js.map
|