@amaster.ai/employee-runtime-connector 0.1.0-beta.11 → 0.1.0-beta.13
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/amaster-runtime-daemon.mjs +565 -25
- package/dist/amaster-runtime.mjs +1 -1
- package/package.json +1 -1
|
@@ -2335,7 +2335,7 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync
|
|
|
2335
2335
|
import { homedir, hostname } from "node:os";
|
|
2336
2336
|
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2337
2337
|
import { spawn, spawnSync } from "node:child_process";
|
|
2338
|
-
const CONNECTOR_VERSION = "0.1.0-beta.
|
|
2338
|
+
const CONNECTOR_VERSION = "0.1.0-beta.13";
|
|
2339
2339
|
const MAX_INFERRED_ARTIFACT_UPLOADS = 20;
|
|
2340
2340
|
const MAX_CHECKPOINT_BYTES = 20 * 1024 * 1024;
|
|
2341
2341
|
const CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -3556,11 +3556,13 @@ function runtimeActionsInstruction(options = {}) {
|
|
|
3556
3556
|
authorizationOptionIdGuidance,
|
|
3557
3557
|
"This capability guidance is rendered from the same authorization envelope enforced by runtime preflight. Preflight remains the final authority. task_governance covers comments, parent status, backlog children, artifacts, and interactions; organization_mutation covers agent_hire/create_agent; executable_child covers todo/in_progress child creation.",
|
|
3558
3558
|
"Choose exactly one mutation path for each requested action: direct AMaster API or the runtime action bridge. Never replay the same mutation through both paths.",
|
|
3559
|
+
"Source-truth contract: every externally derived factual or numeric claim in a comment, document, or artifact must include the source title, publication or retrieval date, and URL or durable source identifier next to the claim. A generic source family such as 'industry reports' or 'public data' is not evidence. If the source cannot be accessed or identified, label the claim as unverified or an assumption, name the verification owner and next action, and do not describe the deliverable or issue as verified or complete on the strength of that claim.",
|
|
3560
|
+
"Every Markdown upload must declare sourceTruth either on its top-level deliverables[] entry or on the explicit upload_artifact action. Use status verified, mixed, unverified, or not_applicable. Verified sources require marker, title, an exact YYYY-MM-DD publication/retrieval date, and url or sourceId; put the marker as [SRC-1] next to the claim and repeat the title, date, and URL/source ID in the file. A generic site homepage is not a durable source URL: use the exact report/page URL. A sourceId must be a stable namespace:value identifier without spaces, such as doi:10.1000/example or report:vendor-report-2026-01; descriptive phrases such as 'industry reports' or 'official materials' are not source IDs. Unverified claims require marker, claim, owner, and nextAction; put [ASSUMPTION-1] next to the claim and repeat those fields in the file. Use not_applicable only when the document contains no externally derived factual or numeric claim, and provide a reason. Omitting deliverables[] does not bypass this preflight.",
|
|
3559
3561
|
"If direct AMaster API requests succeed for all requested mutations, finish with a normal summary and do not emit or mention the AMASTER_RUNTIME_ACTIONS marker.",
|
|
3560
3562
|
"If direct AMaster API requests are unavailable from the executor sandbox, do not stop at blocked. Use the runtime action bridge for the unapplied mutations.",
|
|
3561
3563
|
"Use runtime actions as a continuation patch, not as a full replay. If the wake reason or latest comments say a confirmation was accepted, rejected, or that no more child tasks/interactions should be created, do not recreate those earlier actions. Add the final result comment and update the parent task instead.",
|
|
3562
3564
|
"On an accepted request_confirmation or request_checkbox_confirmation continuation, any new confirmation must include payload.target.type and a non-empty payload.target.key. Missing decision identity fields fail before mutation with accepted_confirmation_decision_target_required. The server reuses identical content for the same target identity and rejects changed prompt, options, or labels until the action uses the genuinely new target revision.",
|
|
3563
|
-
"Every request_confirmation and request_checkbox_confirmation should bind the concrete decision to payload.target using {type:\"custom\", key, label, revisionId} or the applicable issue_document target. Keep target.key stable for the same decision purpose and change target.revisionId only when the target content changes. Replace example target values with task-specific values
|
|
3565
|
+
"Every request_confirmation and request_checkbox_confirmation should bind the concrete decision to payload.target using {type:\"custom\", key, label, revisionId} or the applicable issue_document target. Keep target.key stable for the same decision purpose and change target.revisionId only when the target content changes. Replace example target values with task-specific values. For a custom target with no business revision, use the current run id as revisionId. For an existing issue_document target, use only the exact current revision from task context and never invent a revision id. When the same runtime action batch first upserts the target issue document, omit target.revisionId; the connector binds the created revision before posting the confirmation.",
|
|
3564
3566
|
"If the wake reason is issue_children_completed, treat the completed child tasks as already resolved blockers. Do not recreate blocker child tasks, do not repeat the first-run blocker plan, and do not create another review path unless new human input is truly required. Summarize the completed child work and use update_parent status done when no human review is needed.",
|
|
3565
3567
|
"Use update_parent status done when no human review is needed. Prefer creating a pending review path such as request_confirmation, request_checkbox_confirmation, ask_user_questions, or suggest_tasks before status in_review; if a bare in_review update is emitted, AMaster will create a fallback request_confirmation before PATCH.",
|
|
3566
3568
|
"For create_child_task, do not create ownerless executable todo tasks. Set assigneeAgentId only when the target agent id came from the current AMaster task context or an agent_hire action in this same batch. Do not invent agent ids, role ids, or ids from another company. If you are unsure, omit assigneeAgentId and assigneeUserId; AMaster will assign the current executing agent for todo/in_progress children. Use backlog only when a task is intentionally unassigned.",
|
|
@@ -3596,11 +3598,11 @@ function runtimeActionsInstruction(options = {}) {
|
|
|
3596
3598
|
"The runtime action block must be strict JSON parseable by JSON.parse. Do not use ellipses, placeholders such as [...], comments, trailing commas, single quotes, markdown tables outside quoted strings, or raw double quotes inside string values. Escape quotes and newlines in string values, or generate the block with JSON.stringify / @amaster/runtime-sdk before including it in the final assistant message.",
|
|
3597
3599
|
"Before final output, validate the runtime action block with JSON.parse. If it would fail, fix the JSON and only then include AMASTER_RUNTIME_ACTIONS in the final assistant message.",
|
|
3598
3600
|
actionValidatorCommand
|
|
3599
|
-
? `Before final output, write only the JSON envelope to .amaster-runtime-actions.json and run this exact strict-schema validator: ${actionValidatorCommand}. Fix every reported error and rerun until it exits 0. JSON.parse alone is not sufficient. Do not include the marker in the temporary file.`
|
|
3601
|
+
? `Before final output, write only the JSON envelope to .amaster-runtime-actions.json and run this exact strict-schema validator: ${actionValidatorCommand}. Fix every reported error and rerun until it exits 0. JSON.parse alone is not sufficient. Do not include the marker in the temporary file. After it passes, copy the validated file bytes exactly into the fenced final action block; do not retype, reformat, add, or remove any brace. Any post-validation edit invalidates the validation and requires another validator run.`
|
|
3600
3602
|
: "",
|
|
3601
3603
|
`Allowed action types: ${allowedActionTypes}.`,
|
|
3602
|
-
"For upsert_document, write or replace a Markdown document on the current AMaster task. Use {\"type\":\"upsert_document\",\"key\":\"business-brief\",\"title\":\"2026-07-16 每日经营简报\",\"body\":\"# 每日经营简报\\n...\",\"format\":\"markdown\",\"changeSummary\":\"生成每日经营简报\"}. key, body, and format are required; format must be markdown. Never target a different issue.",
|
|
3603
|
-
"For every create_interaction action, set top-level kind to the interaction kind and set payload to a JSON object. Use this complete canonical request_confirmation example: {\"type\":\"create_interaction\",\"kind\":\"request_confirmation\",\"continuationPolicy\":\"
|
|
3604
|
+
"For upsert_document, write or replace a Markdown document on the current AMaster task. Use {\"type\":\"upsert_document\",\"key\":\"business-brief\",\"title\":\"2026-07-16 每日经营简报\",\"body\":\"# 每日经营简报\\n...\",\"format\":\"markdown\",\"changeSummary\":\"生成每日经营简报\"}. key, body, and format are required; format must be markdown. Never target a different issue. Omit baseRevisionId unless the current task context provides the exact revision; the connector resolves the latest revision before applying any batch mutations.",
|
|
3605
|
+
"For every create_interaction action, set top-level kind to the interaction kind and set payload to a JSON object. Use this complete canonical request_confirmation example: {\"type\":\"create_interaction\",\"kind\":\"request_confirmation\",\"continuationPolicy\":\"wake_assignee\",\"payload\":{\"version\":1,\"prompt\":\"Confirm this result?\",\"acceptLabel\":\"确认\",\"rejectLabel\":\"调整\",\"target\":{\"type\":\"custom\",\"key\":\"delivery-review\",\"label\":\"交付结果\",\"revisionId\":\"v1\"}}}. Use wake_assignee when rejection means revise the work; use wake_assignee_on_accept only when rejection intentionally ends the execution path. Never use interactionType.",
|
|
3604
3606
|
"Interactions cannot be updated with PATCH after creation. For direct API calls, include continuationPolicy in the initial POST /api/issues/{issueId}/interactions request and use only the documented POST accept, reject, respond, or cancel resolution endpoint afterward.",
|
|
3605
3607
|
"For create_interaction ask_user_questions, use continuationPolicy wake_assignee, never wake_assignee_on_accept, because answering questions does not produce accepted status. Every question requires 1-10 options; do not emit an open-text question without options. The UI provides an Other text answer alongside the declared options. Use payload {\"version\":1,\"title\":\"...\",\"questions\":[{\"id\":\"question_key\",\"prompt\":\"Question text\",\"selectionMode\":\"single\",\"required\":true,\"options\":[{\"id\":\"option_key\",\"label\":\"Option label\"}]}]}. Do not use question/options.value at the top level.",
|
|
3606
3608
|
"For create_interaction request_confirmation, follow the complete example above and do not use options arrays.",
|
|
@@ -3916,7 +3918,7 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
|
|
|
3916
3918
|
// Built-in executors run on the connector host, so the prompt may reference these host paths.
|
|
3917
3919
|
// A custom wrapper may isolate model tools; until isolation is an explicit capability, this
|
|
3918
3920
|
// validator is same-host only and must fail visibly rather than fall back to a weaker check.
|
|
3919
|
-
const actionValidatorCommand = `${quoteShell(process.execPath)} ${quoteShell(resolve(process.argv[1]))} validate-actions --file .amaster-runtime-actions.json --cwd . --allowed-content-types ${quoteShell(validatorContentTypes.join(","))}`;
|
|
3921
|
+
const actionValidatorCommand = `${quoteShell(process.execPath)} ${quoteShell(resolve(process.argv[1]))} validate-actions --file .amaster-runtime-actions.json --cwd . --allowed-content-types ${quoteShell(validatorContentTypes.join(","))} --require-source-truth true`;
|
|
3920
3922
|
return [
|
|
3921
3923
|
"## AMaster Runtime Connector Task",
|
|
3922
3924
|
"",
|
|
@@ -3961,6 +3963,10 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
|
|
|
3961
3963
|
`path: ${attachmentPath}`,
|
|
3962
3964
|
attachment.contentType ? `contentType: ${attachment.contentType}` : "",
|
|
3963
3965
|
attachment.byteSize ? `bytes: ${attachment.byteSize}` : "",
|
|
3966
|
+
attachment.workProductId ? `workProductId: ${attachment.workProductId}` : "",
|
|
3967
|
+
attachment.evidenceRole ? `evidenceRole: ${attachment.evidenceRole}` : "",
|
|
3968
|
+
Array.isArray(attachment.scenarioKeys) ? `scenarioKeys: ${JSON.stringify(attachment.scenarioKeys)}` : "",
|
|
3969
|
+
attachment.sha256 ? `sha256: ${attachment.sha256}` : "",
|
|
3964
3970
|
].filter(Boolean).join(" | ");
|
|
3965
3971
|
return `- ${details}`;
|
|
3966
3972
|
}),
|
|
@@ -5511,15 +5517,25 @@ function completedChildIssueForRuntimeAction(action, actionContext) {
|
|
|
5511
5517
|
return normalizedTitle ? completedByTitle.get(normalizedTitle) ?? null : null;
|
|
5512
5518
|
}
|
|
5513
5519
|
|
|
5514
|
-
function runtimeActionExecutionPriority(action) {
|
|
5520
|
+
function runtimeActionExecutionPriority(action, options = {}) {
|
|
5515
5521
|
const fields = runtimeActionBusinessFields(asRecord(action));
|
|
5516
5522
|
const type = readRuntimeActionType(fields);
|
|
5517
|
-
|
|
5523
|
+
if (type !== "update_parent") return 0;
|
|
5524
|
+
// A same-batch blocking child makes the parent ineligible for a later
|
|
5525
|
+
// in_progress transition. Preserve the requested transition before those
|
|
5526
|
+
// blocker relations exist; terminal/review dispositions still run last so
|
|
5527
|
+
// they can account for every governance mutation created by the batch.
|
|
5528
|
+
return readActionStatus(fields.status) === "in_progress" && options.createdReviewPath !== true ? -10 : 10;
|
|
5518
5529
|
}
|
|
5519
5530
|
|
|
5520
5531
|
function orderRuntimeActionsForGovernance(actions) {
|
|
5532
|
+
const createdReviewPath = actions.some(willRuntimeActionCreateReviewPath);
|
|
5521
5533
|
return actions
|
|
5522
|
-
.map((action, index) => ({
|
|
5534
|
+
.map((action, index) => ({
|
|
5535
|
+
action,
|
|
5536
|
+
index,
|
|
5537
|
+
priority: runtimeActionExecutionPriority(action, { createdReviewPath }),
|
|
5538
|
+
}))
|
|
5523
5539
|
.sort((left, right) => left.priority - right.priority || left.index - right.index)
|
|
5524
5540
|
.map((entry) => entry.action);
|
|
5525
5541
|
}
|
|
@@ -5728,6 +5744,113 @@ function safeMaterializedAttachmentFilename(attachment, index) {
|
|
|
5728
5744
|
return name && name !== "." && name !== ".." ? name : `${id}.bin`;
|
|
5729
5745
|
}
|
|
5730
5746
|
|
|
5747
|
+
function compareAttachmentLineageCandidates(left, right) {
|
|
5748
|
+
// Do not depend on API array order: prefer explicit current lineage, then primary,
|
|
5749
|
+
// then the newest update, with the UUID as a stable final tie-breaker.
|
|
5750
|
+
const versionDiff = Number(right.artifactVersionState === "current")
|
|
5751
|
+
- Number(left.artifactVersionState === "current");
|
|
5752
|
+
if (versionDiff !== 0) return versionDiff;
|
|
5753
|
+
const primaryDiff = Number(right.isPrimary) - Number(left.isPrimary);
|
|
5754
|
+
if (primaryDiff !== 0) return primaryDiff;
|
|
5755
|
+
const updatedAtDiff = right.updatedAtMs - left.updatedAtMs;
|
|
5756
|
+
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
5757
|
+
return left.workProductId.localeCompare(right.workProductId);
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5760
|
+
async function loadIssueAttachmentLineageCandidates(config, command, runtimeAuth, issueId) {
|
|
5761
|
+
let workProducts;
|
|
5762
|
+
try {
|
|
5763
|
+
workProducts = await runtimeApiJson(
|
|
5764
|
+
runtimeAuth,
|
|
5765
|
+
"GET",
|
|
5766
|
+
`/api/issues/${encodeURIComponent(issueId)}/work-products`,
|
|
5767
|
+
);
|
|
5768
|
+
} catch (err) {
|
|
5769
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
5770
|
+
const details = asRecord(err);
|
|
5771
|
+
await ingestLog(
|
|
5772
|
+
config,
|
|
5773
|
+
command,
|
|
5774
|
+
"system",
|
|
5775
|
+
"warn",
|
|
5776
|
+
"Issue attachment lineage unavailable; materializing attachments without work-product metadata",
|
|
5777
|
+
{
|
|
5778
|
+
issueId,
|
|
5779
|
+
reason: "request_failed",
|
|
5780
|
+
error: truncateText(error.message, 1000),
|
|
5781
|
+
httpStatus: readNumber(details.httpStatus, null),
|
|
5782
|
+
httpMethod: readString(details.httpMethod),
|
|
5783
|
+
httpPath: readString(details.httpPath),
|
|
5784
|
+
},
|
|
5785
|
+
);
|
|
5786
|
+
return new Map();
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
if (!Array.isArray(workProducts)) {
|
|
5790
|
+
await ingestLog(
|
|
5791
|
+
config,
|
|
5792
|
+
command,
|
|
5793
|
+
"system",
|
|
5794
|
+
"warn",
|
|
5795
|
+
"Issue attachment lineage unavailable; materializing attachments without work-product metadata",
|
|
5796
|
+
{ issueId, reason: "invalid_response_shape", responseType: workProducts === null ? "null" : typeof workProducts },
|
|
5797
|
+
);
|
|
5798
|
+
return new Map();
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
const candidatesByAttachmentId = new Map();
|
|
5802
|
+
for (const rawWorkProduct of workProducts) {
|
|
5803
|
+
const workProduct = asRecord(rawWorkProduct);
|
|
5804
|
+
// Canonical keys: attachmentId/sha256 plus artifactVersionState/evidenceRole/scenarioKeys in packages/shared/src/validators/work-product.ts.
|
|
5805
|
+
const metadata = asRecord(workProduct.metadata);
|
|
5806
|
+
const attachmentId = readString(metadata.attachmentId);
|
|
5807
|
+
const workProductId = readString(workProduct.id);
|
|
5808
|
+
const artifactVersionState = readString(metadata.artifactVersionState);
|
|
5809
|
+
if (
|
|
5810
|
+
!attachmentId
|
|
5811
|
+
|| !workProductId
|
|
5812
|
+
|| readString(workProduct.type) !== "artifact"
|
|
5813
|
+
|| readString(workProduct.status) === "archived"
|
|
5814
|
+
|| artifactVersionState === "superseded"
|
|
5815
|
+
) {
|
|
5816
|
+
continue;
|
|
5817
|
+
}
|
|
5818
|
+
const updatedAt = readString(workProduct.updatedAt);
|
|
5819
|
+
const updatedAtMs = updatedAt ? Date.parse(updatedAt) : 0;
|
|
5820
|
+
const candidate = {
|
|
5821
|
+
workProductId,
|
|
5822
|
+
evidenceRole: readString(metadata.evidenceRole),
|
|
5823
|
+
scenarioKeys: Array.isArray(metadata.scenarioKeys)
|
|
5824
|
+
? metadata.scenarioKeys.map(readString).filter(Boolean)
|
|
5825
|
+
: null,
|
|
5826
|
+
declaredSha256: readString(metadata.sha256)?.toLowerCase() ?? null,
|
|
5827
|
+
artifactVersionState,
|
|
5828
|
+
isPrimary: workProduct.isPrimary === true,
|
|
5829
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
|
|
5830
|
+
};
|
|
5831
|
+
const candidates = candidatesByAttachmentId.get(attachmentId) ?? [];
|
|
5832
|
+
candidates.push(candidate);
|
|
5833
|
+
candidatesByAttachmentId.set(attachmentId, candidates);
|
|
5834
|
+
}
|
|
5835
|
+
for (const candidates of candidatesByAttachmentId.values()) {
|
|
5836
|
+
candidates.sort(compareAttachmentLineageCandidates);
|
|
5837
|
+
}
|
|
5838
|
+
return candidatesByAttachmentId;
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
function selectAttachmentLineage(candidates, actualSha256) {
|
|
5842
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
5843
|
+
const selected = candidates.find((candidate) => candidate.declaredSha256 === actualSha256)
|
|
5844
|
+
?? candidates.find((candidate) => candidate.declaredSha256 === null)
|
|
5845
|
+
?? null;
|
|
5846
|
+
if (!selected) return null;
|
|
5847
|
+
return {
|
|
5848
|
+
workProductId: selected.workProductId,
|
|
5849
|
+
evidenceRole: selected.evidenceRole,
|
|
5850
|
+
scenarioKeys: selected.scenarioKeys,
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
|
|
5731
5854
|
async function materializeIssueAttachments(config, command, workspace) {
|
|
5732
5855
|
const runtimeAuth = commandRuntimeAuth(command);
|
|
5733
5856
|
const issueId = commandIssueId(command);
|
|
@@ -5735,6 +5858,12 @@ async function materializeIssueAttachments(config, command, workspace) {
|
|
|
5735
5858
|
|
|
5736
5859
|
const attachments = await runtimeApiJson(runtimeAuth, "GET", `/api/issues/${encodeURIComponent(issueId)}/attachments`);
|
|
5737
5860
|
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
5861
|
+
const lineageCandidatesByAttachmentId = await loadIssueAttachmentLineageCandidates(
|
|
5862
|
+
config,
|
|
5863
|
+
command,
|
|
5864
|
+
runtimeAuth,
|
|
5865
|
+
issueId,
|
|
5866
|
+
);
|
|
5738
5867
|
|
|
5739
5868
|
const targetDir = join(workspace.cwd, "input-attachments");
|
|
5740
5869
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -5757,14 +5886,36 @@ async function materializeIssueAttachments(config, command, workspace) {
|
|
|
5757
5886
|
const targetPath = join(targetDir, filename);
|
|
5758
5887
|
const body = await runtimeApiBuffer(runtimeAuth, contentPath);
|
|
5759
5888
|
writeFileSync(targetPath, body);
|
|
5889
|
+
const attachmentId = readString(attachment.id);
|
|
5890
|
+
const actualSha256 = createHash("sha256").update(body).digest("hex");
|
|
5891
|
+
const lineageCandidates = lineageCandidatesByAttachmentId.get(attachmentId) ?? [];
|
|
5892
|
+
const lineage = selectAttachmentLineage(lineageCandidates, actualSha256);
|
|
5893
|
+
if (lineageCandidates.length > 0 && !lineage) {
|
|
5894
|
+
await ingestLog(
|
|
5895
|
+
config,
|
|
5896
|
+
command,
|
|
5897
|
+
"system",
|
|
5898
|
+
"warn",
|
|
5899
|
+
"Issue attachment lineage SHA mismatch; omitting work-product metadata",
|
|
5900
|
+
{
|
|
5901
|
+
issueId,
|
|
5902
|
+
attachmentId,
|
|
5903
|
+
actualSha256,
|
|
5904
|
+
declaredSha256: lineageCandidates.map((candidate) => candidate.declaredSha256).filter(Boolean),
|
|
5905
|
+
workProductIds: lineageCandidates.map((candidate) => candidate.workProductId),
|
|
5906
|
+
},
|
|
5907
|
+
);
|
|
5908
|
+
}
|
|
5760
5909
|
materialized.push({
|
|
5761
|
-
id:
|
|
5910
|
+
id: attachmentId,
|
|
5762
5911
|
name: readString(attachment.originalFilename) ?? filename,
|
|
5763
5912
|
path: targetPath,
|
|
5764
5913
|
relativePath: relative(workspace.cwd, targetPath),
|
|
5765
5914
|
contentType: readString(attachment.contentType),
|
|
5766
5915
|
byteSize: body.byteLength,
|
|
5767
5916
|
contentPath,
|
|
5917
|
+
sha256: actualSha256,
|
|
5918
|
+
...(lineage ?? {}),
|
|
5768
5919
|
});
|
|
5769
5920
|
}
|
|
5770
5921
|
|
|
@@ -5777,6 +5928,10 @@ async function materializeIssueAttachments(config, command, workspace) {
|
|
|
5777
5928
|
path: attachment.relativePath,
|
|
5778
5929
|
contentType: attachment.contentType ?? null,
|
|
5779
5930
|
byteSize: attachment.byteSize,
|
|
5931
|
+
workProductId: attachment.workProductId ?? null,
|
|
5932
|
+
evidenceRole: attachment.evidenceRole ?? null,
|
|
5933
|
+
scenarioKeys: attachment.scenarioKeys ?? null,
|
|
5934
|
+
sha256: attachment.sha256 ?? null,
|
|
5780
5935
|
})),
|
|
5781
5936
|
});
|
|
5782
5937
|
await ingestLog(config, command, "system", "info", `Materialized ${materialized.length} issue attachment(s) into the execution workspace`, {
|
|
@@ -7094,7 +7249,7 @@ const RUNTIME_ACTION_ALLOWED_FIELDS = new Map([
|
|
|
7094
7249
|
"path", "filePath", "artifactPath", "artifact_path", "title", "summary", "contentType",
|
|
7095
7250
|
"createWorkProduct", "provider", "status", "reviewState", "isPrimary", "healthStatus",
|
|
7096
7251
|
"sourceWorkProductId", "sourceWorkProductIds", "supersedesWorkProductId", "evidenceRole", "scenarioKeys",
|
|
7097
|
-
"idempotencyKey", "upload_artifact",
|
|
7252
|
+
"idempotencyKey", "sourceTruth", "upload_artifact",
|
|
7098
7253
|
])],
|
|
7099
7254
|
["upsert_document", new Set([
|
|
7100
7255
|
"key", "title", "body", "format", "changeSummary", "baseRevisionId", "upsert_document",
|
|
@@ -7160,7 +7315,7 @@ const RUNTIME_ACTION_ARRAY_FIELDS = new Set([
|
|
|
7160
7315
|
]);
|
|
7161
7316
|
const RUNTIME_ACTION_OBJECT_FIELDS = new Set([
|
|
7162
7317
|
"adapterConfig", "instructionsBundle", "runtimeConfig", "permissions", "metadata", "target",
|
|
7163
|
-
"interaction",
|
|
7318
|
+
"interaction", "sourceTruth",
|
|
7164
7319
|
]);
|
|
7165
7320
|
|
|
7166
7321
|
function assertRuntimeActionFieldTypes(action, actionIndex) {
|
|
@@ -7368,6 +7523,45 @@ function assertRuntimeCreateInteractionPayload(kind, payload) {
|
|
|
7368
7523
|
}
|
|
7369
7524
|
}
|
|
7370
7525
|
|
|
7526
|
+
function assertRuntimeConfirmationTarget(target) {
|
|
7527
|
+
const record = asRecord(target);
|
|
7528
|
+
const type = readString(record.type);
|
|
7529
|
+
assertRuntimeActionValid(
|
|
7530
|
+
["custom", "issue_document"].includes(type ?? ""),
|
|
7531
|
+
`confirmation payload target type is unsupported: ${String(record.type)}; use custom or issue_document`,
|
|
7532
|
+
);
|
|
7533
|
+
const key = readString(record.key);
|
|
7534
|
+
const label = readString(record.label);
|
|
7535
|
+
const href = readString(record.href);
|
|
7536
|
+
const revisionId = readString(record.revisionId);
|
|
7537
|
+
const revisionNumber = record.revisionNumber;
|
|
7538
|
+
assertRuntimeActionValid(key, "confirmation payload target requires key");
|
|
7539
|
+
assertRuntimeActionValid(!label || label.length <= 120, "confirmation payload target label must be at most 120 characters");
|
|
7540
|
+
assertRuntimeActionValid(
|
|
7541
|
+
!href || href.startsWith("#") || (href.startsWith("/") && !href.startsWith("//")) || /^https?:\/\//i.test(href),
|
|
7542
|
+
"confirmation payload target href must be a root-relative path, same-page fragment, or http(s) URL",
|
|
7543
|
+
);
|
|
7544
|
+
assertRuntimeActionValid(
|
|
7545
|
+
revisionNumber === undefined || revisionNumber === null || (Number.isInteger(revisionNumber) && revisionNumber > 0),
|
|
7546
|
+
"confirmation payload target revisionNumber must be a positive integer",
|
|
7547
|
+
);
|
|
7548
|
+
if (type === "custom") {
|
|
7549
|
+
assertRuntimeActionValid(key.length <= 120, "confirmation custom target key must be at most 120 characters");
|
|
7550
|
+
assertRuntimeActionValid(!revisionId || revisionId.length <= 255, "confirmation custom target revisionId must be at most 255 characters");
|
|
7551
|
+
return;
|
|
7552
|
+
}
|
|
7553
|
+
const uuidPattern = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
7554
|
+
assertRuntimeActionValid(
|
|
7555
|
+
key.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/.test(key),
|
|
7556
|
+
"confirmation issue_document target key must use lowercase letters, numbers, _ or -",
|
|
7557
|
+
);
|
|
7558
|
+
assertRuntimeActionValid(!revisionId || uuidPattern.test(revisionId), "confirmation issue_document target revisionId must be a UUID");
|
|
7559
|
+
for (const field of ["issueId", "documentId"]) {
|
|
7560
|
+
const value = readString(record[field]);
|
|
7561
|
+
assertRuntimeActionValid(!value || uuidPattern.test(value), `confirmation issue_document target ${field} must be a UUID`);
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
|
|
7371
7565
|
function willRuntimeActionCreateReviewPath(action) {
|
|
7372
7566
|
const fields = runtimeActionBusinessFields(action);
|
|
7373
7567
|
const type = readRuntimeActionType(fields);
|
|
@@ -7572,7 +7766,8 @@ function preflightRuntimeAction(action, preflightContext, actionIndex) {
|
|
|
7572
7766
|
}
|
|
7573
7767
|
if (type === "upload_artifact") {
|
|
7574
7768
|
assertNoPlaceholderRuntimeActionText(fields, ["path", "filePath", "artifactPath", "artifact_path", "title", "summary"]);
|
|
7575
|
-
|
|
7769
|
+
const artifactPath = readRuntimeArtifactPath(fields);
|
|
7770
|
+
assertRuntimeActionValid(artifactPath, "upload_artifact action requires path");
|
|
7576
7771
|
const pathFields = ["path", "filePath", "artifactPath", "artifact_path"].filter((field) => readString(fields[field]));
|
|
7577
7772
|
assertRuntimeActionValid(pathFields.length === 1, "upload_artifact action requires exactly one path field");
|
|
7578
7773
|
const sourceWorkProductId = readString(fields.sourceWorkProductId);
|
|
@@ -7602,7 +7797,14 @@ function preflightRuntimeAction(action, preflightContext, actionIndex) {
|
|
|
7602
7797
|
assertRuntimeActionValid(fields.scenarioKeys.map(readString).filter(Boolean).length === fields.scenarioKeys.length, "upload_artifact scenarioKeys must contain non-empty strings");
|
|
7603
7798
|
}
|
|
7604
7799
|
assertRuntimeArtifactContentType(fields, preflightContext.allowedContentTypes);
|
|
7605
|
-
resolveWorkspaceArtifactPath(fields, { cwd: preflightContext.cwd });
|
|
7800
|
+
const filePath = resolveWorkspaceArtifactPath(fields, { cwd: preflightContext.cwd });
|
|
7801
|
+
if (preflightContext.requireSourceTruth && extname(filePath).toLowerCase() === ".md") {
|
|
7802
|
+
assertRuntimeMarkdownSourceTruth(
|
|
7803
|
+
fields.sourceTruth ?? preflightContext.sourceTruthByPath.get(filePath),
|
|
7804
|
+
`actions[${actionIndex}].sourceTruth`,
|
|
7805
|
+
filePath,
|
|
7806
|
+
);
|
|
7807
|
+
}
|
|
7606
7808
|
return;
|
|
7607
7809
|
}
|
|
7608
7810
|
if (type === "upsert_document") {
|
|
@@ -7648,6 +7850,13 @@ function preflightRuntimeAction(action, preflightContext, actionIndex) {
|
|
|
7648
7850
|
"accepted_confirmation_decision_target_required: an accepted confirmation continuation requires payload.target.type and a non-empty payload.target.key so the server can identify the decision; use a new target revision for genuinely changed content or finish the accepted work",
|
|
7649
7851
|
);
|
|
7650
7852
|
}
|
|
7853
|
+
if (
|
|
7854
|
+
["request_confirmation", "request_checkbox_confirmation"].includes(kind) &&
|
|
7855
|
+
payload.target !== undefined &&
|
|
7856
|
+
payload.target !== null
|
|
7857
|
+
) {
|
|
7858
|
+
assertRuntimeConfirmationTarget(payload.target);
|
|
7859
|
+
}
|
|
7651
7860
|
preflightContext.createdReviewPathForInReview = true;
|
|
7652
7861
|
return;
|
|
7653
7862
|
}
|
|
@@ -7705,6 +7914,49 @@ function preflightRuntimeActionBatch(actions, options = {}) {
|
|
|
7705
7914
|
const actionIndexByObject = new Map(actions.map((action, index) => [action, index]));
|
|
7706
7915
|
assertDelegatedParentWaitDisposition(actions);
|
|
7707
7916
|
const orderedActions = orderRuntimeActionsByClientKeyDependencies(actions);
|
|
7917
|
+
const documentActionIndexByKey = new Map();
|
|
7918
|
+
for (const action of orderedActions) {
|
|
7919
|
+
if (readRuntimeActionType(action) !== "upsert_document") continue;
|
|
7920
|
+
const key = readBoundedActionString(runtimeActionBusinessFields(action), "key", 64);
|
|
7921
|
+
if (!key) continue;
|
|
7922
|
+
const actionIndex = actionIndexByObject.get(action);
|
|
7923
|
+
if (documentActionIndexByKey.has(key)) {
|
|
7924
|
+
const err = runtimeActionValidationError(
|
|
7925
|
+
`upsert_document key ${key} appears more than once in the same runtime action batch`,
|
|
7926
|
+
);
|
|
7927
|
+
err.runtimeActionType = "upsert_document";
|
|
7928
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].key`;
|
|
7929
|
+
throw err;
|
|
7930
|
+
}
|
|
7931
|
+
documentActionIndexByKey.set(key, actionIndex);
|
|
7932
|
+
}
|
|
7933
|
+
for (const action of orderedActions) {
|
|
7934
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7935
|
+
if (readRuntimeActionType(fields) !== "create_interaction") continue;
|
|
7936
|
+
const kind = inferCreateInteractionKind(fields);
|
|
7937
|
+
if (!["request_confirmation", "request_checkbox_confirmation"].includes(kind ?? "")) continue;
|
|
7938
|
+
const target = asRecord(asRecord(fields.payload).target);
|
|
7939
|
+
if (readString(target.type) !== "issue_document") continue;
|
|
7940
|
+
const key = readString(target.key);
|
|
7941
|
+
if (!key) continue;
|
|
7942
|
+
const actionIndex = actionIndexByObject.get(action);
|
|
7943
|
+
const sameBatchDocumentActionIndex = documentActionIndexByKey.get(key);
|
|
7944
|
+
const sameBatchDocumentUpsert = Number.isInteger(sameBatchDocumentActionIndex);
|
|
7945
|
+
const revisionId = readString(target.revisionId);
|
|
7946
|
+
if (sameBatchDocumentUpsert && !revisionId && sameBatchDocumentActionIndex < actionIndex) continue;
|
|
7947
|
+
if (!sameBatchDocumentUpsert && revisionId) continue;
|
|
7948
|
+
const err = runtimeActionValidationError(
|
|
7949
|
+
sameBatchDocumentUpsert
|
|
7950
|
+
? revisionId
|
|
7951
|
+
? `same-batch issue document confirmation for ${key} must omit revisionId so the connector can bind the created revision`
|
|
7952
|
+
: `same-batch issue document confirmation for ${key} requires an earlier upsert_document action so the connector can bind the created revision before applying the interaction`
|
|
7953
|
+
: `issue document confirmation for ${key} requires revisionId when the document is not upserted in the same batch`,
|
|
7954
|
+
);
|
|
7955
|
+
err.runtimeActionType = "create_interaction";
|
|
7956
|
+
err.runtimeActionKind = kind;
|
|
7957
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].payload.target.revisionId`;
|
|
7958
|
+
throw err;
|
|
7959
|
+
}
|
|
7708
7960
|
assertRuntimeActionsAllowedByContinuationScope(orderedActions, options.continuationScope);
|
|
7709
7961
|
assertNoAmbiguousApprovalGatedSideEffects(orderedActions);
|
|
7710
7962
|
const preflightContext = {
|
|
@@ -7715,6 +7967,8 @@ function preflightRuntimeActionBatch(actions, options = {}) {
|
|
|
7715
7967
|
acceptedConfirmationContinuation: options.acceptedConfirmationContinuation === true,
|
|
7716
7968
|
allowedContentTypes: Array.isArray(options.allowedContentTypes) ? options.allowedContentTypes : [],
|
|
7717
7969
|
cwd: readString(options.cwd),
|
|
7970
|
+
requireSourceTruth: options.requireSourceTruth === true,
|
|
7971
|
+
sourceTruthByPath: options.sourceTruthByPath instanceof Map ? options.sourceTruthByPath : new Map(),
|
|
7718
7972
|
finalCommentBodies: [],
|
|
7719
7973
|
};
|
|
7720
7974
|
for (const action of orderedActions) {
|
|
@@ -7754,6 +8008,86 @@ function preflightRuntimeActionBatch(actions, options = {}) {
|
|
|
7754
8008
|
return orderedActions;
|
|
7755
8009
|
}
|
|
7756
8010
|
|
|
8011
|
+
async function preflightRuntimeDocumentRevisions(runtimeAuth, issueId, actions) {
|
|
8012
|
+
const baseRevisionIdsByKey = new Map();
|
|
8013
|
+
const existingDocumentsByKey = new Map();
|
|
8014
|
+
const readExistingDocument = async (key) => {
|
|
8015
|
+
if (existingDocumentsByKey.has(key)) return existingDocumentsByKey.get(key);
|
|
8016
|
+
let existingDocument = null;
|
|
8017
|
+
try {
|
|
8018
|
+
existingDocument = asRecord(await runtimeApiJson(
|
|
8019
|
+
runtimeAuth,
|
|
8020
|
+
"GET",
|
|
8021
|
+
`/api/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`,
|
|
8022
|
+
));
|
|
8023
|
+
} catch (err) {
|
|
8024
|
+
if (err?.httpStatus !== 404) throw err;
|
|
8025
|
+
}
|
|
8026
|
+
existingDocumentsByKey.set(key, existingDocument);
|
|
8027
|
+
return existingDocument;
|
|
8028
|
+
};
|
|
8029
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
8030
|
+
if (readRuntimeActionType(action) !== "upsert_document") continue;
|
|
8031
|
+
const fields = runtimeActionBusinessFields(action);
|
|
8032
|
+
const key = readBoundedActionString(fields, "key", 64);
|
|
8033
|
+
if (!key) continue;
|
|
8034
|
+
const existingDocument = await readExistingDocument(key);
|
|
8035
|
+
|
|
8036
|
+
const explicitBaseRevisionId = readString(fields.baseRevisionId);
|
|
8037
|
+
const currentRevisionId = readString(existingDocument?.latestRevisionId);
|
|
8038
|
+
if (existingDocument && !currentRevisionId) {
|
|
8039
|
+
const err = runtimeActionValidationError(
|
|
8040
|
+
`upsert_document ${key} exists but did not expose latestRevisionId`,
|
|
8041
|
+
);
|
|
8042
|
+
err.runtimeActionType = "upsert_document";
|
|
8043
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].baseRevisionId`;
|
|
8044
|
+
throw err;
|
|
8045
|
+
}
|
|
8046
|
+
if (!existingDocument && explicitBaseRevisionId) {
|
|
8047
|
+
const err = runtimeActionValidationError(
|
|
8048
|
+
`upsert_document ${key} supplied baseRevisionId but the document does not exist`,
|
|
8049
|
+
);
|
|
8050
|
+
err.runtimeActionType = "upsert_document";
|
|
8051
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].baseRevisionId`;
|
|
8052
|
+
throw err;
|
|
8053
|
+
}
|
|
8054
|
+
if (currentRevisionId && explicitBaseRevisionId && explicitBaseRevisionId !== currentRevisionId) {
|
|
8055
|
+
const err = new Error(
|
|
8056
|
+
`upsert_document ${key} baseRevisionId is stale; current revision is ${currentRevisionId}`,
|
|
8057
|
+
);
|
|
8058
|
+
err.httpStatus = 409;
|
|
8059
|
+
err.runtimeActionType = "upsert_document";
|
|
8060
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].baseRevisionId`;
|
|
8061
|
+
throw err;
|
|
8062
|
+
}
|
|
8063
|
+
baseRevisionIdsByKey.set(key, currentRevisionId);
|
|
8064
|
+
}
|
|
8065
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
8066
|
+
const fields = runtimeActionBusinessFields(action);
|
|
8067
|
+
if (readRuntimeActionType(fields) !== "create_interaction") continue;
|
|
8068
|
+
const kind = inferCreateInteractionKind(fields);
|
|
8069
|
+
if (!["request_confirmation", "request_checkbox_confirmation"].includes(kind ?? "")) continue;
|
|
8070
|
+
const target = asRecord(asRecord(fields.payload).target);
|
|
8071
|
+
if (readString(target.type) !== "issue_document") continue;
|
|
8072
|
+
const key = readString(target.key);
|
|
8073
|
+
if (!key || baseRevisionIdsByKey.has(key)) continue;
|
|
8074
|
+
const existingDocument = await readExistingDocument(key);
|
|
8075
|
+
const currentRevisionId = readString(existingDocument?.latestRevisionId);
|
|
8076
|
+
const targetRevisionId = readString(target.revisionId);
|
|
8077
|
+
if (currentRevisionId && currentRevisionId === targetRevisionId) continue;
|
|
8078
|
+
const err = runtimeActionValidationError(
|
|
8079
|
+
currentRevisionId
|
|
8080
|
+
? `issue document confirmation target ${key} is stale; current revision is ${currentRevisionId}`
|
|
8081
|
+
: `issue document confirmation target ${key} does not exist`,
|
|
8082
|
+
);
|
|
8083
|
+
err.runtimeActionType = "create_interaction";
|
|
8084
|
+
err.runtimeActionKind = kind;
|
|
8085
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].payload.target.revisionId`;
|
|
8086
|
+
throw err;
|
|
8087
|
+
}
|
|
8088
|
+
return baseRevisionIdsByKey;
|
|
8089
|
+
}
|
|
8090
|
+
|
|
7757
8091
|
function runtimeActionAuthorizationClass(action) {
|
|
7758
8092
|
// Keep these high-risk route mappings aligned with server-side checks in
|
|
7759
8093
|
// routes/agents.ts (organization_mutation) and routes/issues.ts
|
|
@@ -8157,11 +8491,24 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
8157
8491
|
if (!["request_confirmation", "request_checkbox_confirmation", "ask_user_questions", "suggest_tasks"].includes(kind)) {
|
|
8158
8492
|
throw new Error(`create_interaction action does not support kind: ${kind}`);
|
|
8159
8493
|
}
|
|
8160
|
-
|
|
8494
|
+
let payload = normalizeCreateInteractionPayload(fields, kind);
|
|
8161
8495
|
if (Object.keys(payload).length === 0) {
|
|
8162
8496
|
throw new Error("create_interaction action requires payload");
|
|
8163
8497
|
}
|
|
8164
|
-
const
|
|
8498
|
+
const interactionTarget = asRecord(payload.target);
|
|
8499
|
+
if (
|
|
8500
|
+
["request_confirmation", "request_checkbox_confirmation"].includes(kind) &&
|
|
8501
|
+
readString(interactionTarget.type) === "issue_document" &&
|
|
8502
|
+
!readString(interactionTarget.revisionId)
|
|
8503
|
+
) {
|
|
8504
|
+
const key = readString(interactionTarget.key);
|
|
8505
|
+
const boundTarget = key ? actionContext.issueDocumentTargetsByKey.get(key) : null;
|
|
8506
|
+
if (!boundTarget) {
|
|
8507
|
+
throw new Error(`issue document confirmation target ${String(key)} was not created earlier in the runtime action batch`);
|
|
8508
|
+
}
|
|
8509
|
+
payload = { ...payload, target: boundTarget };
|
|
8510
|
+
}
|
|
8511
|
+
const defaultContinuationPolicy = "wake_assignee";
|
|
8165
8512
|
const createBody = {
|
|
8166
8513
|
kind,
|
|
8167
8514
|
continuationPolicy: readString(fields.continuationPolicy) ?? defaultContinuationPolicy,
|
|
@@ -8210,16 +8557,35 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
8210
8557
|
format: "markdown",
|
|
8211
8558
|
body,
|
|
8212
8559
|
changeSummary: readBoundedActionString(fields, "changeSummary", 500) ?? null,
|
|
8213
|
-
baseRevisionId: readString(fields.baseRevisionId)
|
|
8560
|
+
baseRevisionId: readString(fields.baseRevisionId)
|
|
8561
|
+
?? actionContext.documentBaseRevisionIdsByKey.get(key)
|
|
8562
|
+
?? null,
|
|
8214
8563
|
},
|
|
8215
8564
|
));
|
|
8565
|
+
const latestRevisionId = readString(document.latestRevisionId);
|
|
8566
|
+
const latestRevisionNumber = readNumber(document.latestRevisionNumber, null) ?? undefined;
|
|
8567
|
+
const documentId = readString(document.id);
|
|
8568
|
+
if (actionContext.issueDocumentConfirmationKeysNeedingBinding.has(key)) {
|
|
8569
|
+
if (!documentId || !latestRevisionId || !latestRevisionNumber) {
|
|
8570
|
+
throw new Error(`upsert_document ${key} response did not expose the created document revision`);
|
|
8571
|
+
}
|
|
8572
|
+
actionContext.issueDocumentTargetsByKey.set(key, {
|
|
8573
|
+
type: "issue_document",
|
|
8574
|
+
issueId,
|
|
8575
|
+
documentId,
|
|
8576
|
+
key,
|
|
8577
|
+
revisionId: latestRevisionId,
|
|
8578
|
+
revisionNumber: latestRevisionNumber,
|
|
8579
|
+
});
|
|
8580
|
+
}
|
|
8216
8581
|
return {
|
|
8217
8582
|
type,
|
|
8218
8583
|
...(originalType && originalType !== type ? { originalType } : {}),
|
|
8219
8584
|
issueId,
|
|
8220
8585
|
key,
|
|
8221
|
-
documentId
|
|
8222
|
-
|
|
8586
|
+
documentId,
|
|
8587
|
+
revisionId: latestRevisionId ?? undefined,
|
|
8588
|
+
revisionNumber: latestRevisionNumber,
|
|
8223
8589
|
};
|
|
8224
8590
|
}
|
|
8225
8591
|
|
|
@@ -9100,7 +9466,138 @@ function attachRuntimeCheckpointCandidates(err, envelope, cwd) {
|
|
|
9100
9466
|
}
|
|
9101
9467
|
}
|
|
9102
9468
|
|
|
9103
|
-
function
|
|
9469
|
+
function assertRuntimeMarkdownSourceTruth(sourceTruthValue, fieldPath, filePath) {
|
|
9470
|
+
const sourceTruth = asRecord(sourceTruthValue);
|
|
9471
|
+
if (Object.keys(sourceTruth).length === 0) {
|
|
9472
|
+
throw runtimeDeliverableError(`${fieldPath} is required for Markdown deliverables`);
|
|
9473
|
+
}
|
|
9474
|
+
const allowedFields = new Set(["status", "reason", "sources", "unverifiedClaims"]);
|
|
9475
|
+
for (const field of Object.keys(sourceTruth)) {
|
|
9476
|
+
if (!allowedFields.has(field)) {
|
|
9477
|
+
throw runtimeDeliverableError(`${fieldPath}.${field} is unsupported`);
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
const status = readString(sourceTruth.status);
|
|
9481
|
+
if (!["verified", "mixed", "unverified", "not_applicable"].includes(status ?? "")) {
|
|
9482
|
+
throw runtimeDeliverableError(`${fieldPath}.status must be verified, mixed, unverified, or not_applicable`);
|
|
9483
|
+
}
|
|
9484
|
+
const sources = Array.isArray(sourceTruth.sources) ? sourceTruth.sources : [];
|
|
9485
|
+
const unverifiedClaims = Array.isArray(sourceTruth.unverifiedClaims) ? sourceTruth.unverifiedClaims : [];
|
|
9486
|
+
if (sourceTruth.sources !== undefined && !Array.isArray(sourceTruth.sources)) {
|
|
9487
|
+
throw runtimeDeliverableError(`${fieldPath}.sources must be an array`);
|
|
9488
|
+
}
|
|
9489
|
+
if (sourceTruth.unverifiedClaims !== undefined && !Array.isArray(sourceTruth.unverifiedClaims)) {
|
|
9490
|
+
throw runtimeDeliverableError(`${fieldPath}.unverifiedClaims must be an array`);
|
|
9491
|
+
}
|
|
9492
|
+
if (sources.length > 100 || unverifiedClaims.length > 100) {
|
|
9493
|
+
throw runtimeDeliverableError(`${fieldPath} exceeds the limit of 100 source records per category`);
|
|
9494
|
+
}
|
|
9495
|
+
if (status === "verified" && (sources.length === 0 || unverifiedClaims.length > 0)) {
|
|
9496
|
+
throw runtimeDeliverableError(`${fieldPath} status verified requires sources and forbids unverifiedClaims`);
|
|
9497
|
+
}
|
|
9498
|
+
if (status === "mixed" && (sources.length === 0 || unverifiedClaims.length === 0)) {
|
|
9499
|
+
throw runtimeDeliverableError(`${fieldPath} status mixed requires both sources and unverifiedClaims`);
|
|
9500
|
+
}
|
|
9501
|
+
if (status === "unverified" && (sources.length > 0 || unverifiedClaims.length === 0)) {
|
|
9502
|
+
throw runtimeDeliverableError(`${fieldPath} status unverified requires unverifiedClaims and forbids sources`);
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
const content = readFileSync(filePath, "utf8");
|
|
9506
|
+
const markdownSourceMarkers = new Set(
|
|
9507
|
+
[...content.matchAll(/\[(SRC-[A-Z0-9_-]+)\]/gi)].map((match) => match[1]),
|
|
9508
|
+
);
|
|
9509
|
+
const markdownAssumptionMarkers = new Set(
|
|
9510
|
+
[...content.matchAll(/\[(ASSUMPTION-[A-Z0-9_-]+)\]/gi)].map((match) => match[1]),
|
|
9511
|
+
);
|
|
9512
|
+
if (status === "not_applicable") {
|
|
9513
|
+
if (sources.length > 0 || unverifiedClaims.length > 0 || !readString(sourceTruth.reason)) {
|
|
9514
|
+
throw runtimeDeliverableError(`${fieldPath} status not_applicable requires reason and forbids source records`);
|
|
9515
|
+
}
|
|
9516
|
+
const sourceMarker = markdownSourceMarkers.values().next().value;
|
|
9517
|
+
if (sourceMarker) {
|
|
9518
|
+
throw runtimeDeliverableError(`Markdown marker ${sourceMarker} is missing from sourceTruth.sources`);
|
|
9519
|
+
}
|
|
9520
|
+
const assumptionMarker = markdownAssumptionMarkers.values().next().value;
|
|
9521
|
+
if (assumptionMarker) {
|
|
9522
|
+
throw runtimeDeliverableError(`Markdown marker ${assumptionMarker} is missing from sourceTruth.unverifiedClaims`);
|
|
9523
|
+
}
|
|
9524
|
+
return;
|
|
9525
|
+
}
|
|
9526
|
+
|
|
9527
|
+
const markers = new Set();
|
|
9528
|
+
const declaredSourceMarkers = new Set();
|
|
9529
|
+
const declaredAssumptionMarkers = new Set();
|
|
9530
|
+
for (const [sourceIndex, rawSource] of sources.entries()) {
|
|
9531
|
+
const sourcePath = `${fieldPath}.sources[${sourceIndex}]`;
|
|
9532
|
+
const source = asRecord(rawSource);
|
|
9533
|
+
const marker = readString(source.marker);
|
|
9534
|
+
const title = readString(source.title);
|
|
9535
|
+
const date = readString(source.date);
|
|
9536
|
+
const url = readString(source.url);
|
|
9537
|
+
const sourceId = readString(source.sourceId);
|
|
9538
|
+
if (!marker || !/^SRC-[A-Z0-9_-]+$/i.test(marker)) {
|
|
9539
|
+
throw runtimeDeliverableError(`${sourcePath}.marker must use SRC-*`);
|
|
9540
|
+
}
|
|
9541
|
+
if (markers.has(marker)) throw runtimeDeliverableError(`${sourcePath}.marker must be unique`);
|
|
9542
|
+
markers.add(marker);
|
|
9543
|
+
declaredSourceMarkers.add(marker);
|
|
9544
|
+
if (!title) throw runtimeDeliverableError(`${sourcePath}.title is required`);
|
|
9545
|
+
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
9546
|
+
throw runtimeDeliverableError(`${sourcePath}.date must be a valid YYYY-MM-DD date`);
|
|
9547
|
+
}
|
|
9548
|
+
const parsedDate = new Date(`${date}T00:00:00.000Z`);
|
|
9549
|
+
if (Number.isNaN(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== date) {
|
|
9550
|
+
throw runtimeDeliverableError(`${sourcePath}.date must be a valid YYYY-MM-DD date`);
|
|
9551
|
+
}
|
|
9552
|
+
if (!url && !sourceId) throw runtimeDeliverableError(`${sourcePath} requires url or sourceId`);
|
|
9553
|
+
if (url) {
|
|
9554
|
+
if (!/^https?:\/\//i.test(url)) throw runtimeDeliverableError(`${sourcePath}.url must use http or https`);
|
|
9555
|
+
if (/\s/.test(url)) throw runtimeDeliverableError(`${sourcePath}.url must be one exact URL without spaces`);
|
|
9556
|
+
const parsedUrl = new URL(url);
|
|
9557
|
+
if (parsedUrl.pathname === "/" && !parsedUrl.search && !parsedUrl.hash && !sourceId) {
|
|
9558
|
+
throw runtimeDeliverableError(`${sourcePath}.url must identify a document or page, not only a site root`);
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
if (sourceId && !/^[a-z][a-z0-9._-]*:[a-z0-9][a-z0-9._/+:-]*$/i.test(sourceId)) {
|
|
9562
|
+
throw runtimeDeliverableError(`${sourcePath}.sourceId must use a stable namespace:value identifier without spaces`);
|
|
9563
|
+
}
|
|
9564
|
+
for (const [label, value] of [["marker", `[${marker}]`], ["title", title], ["date", date], [url ? "url" : "sourceId", url ?? sourceId]]) {
|
|
9565
|
+
if (!content.includes(value)) throw runtimeDeliverableError(`${sourcePath}.${label} must appear in the Markdown deliverable`);
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
for (const [claimIndex, rawClaim] of unverifiedClaims.entries()) {
|
|
9569
|
+
const claimPath = `${fieldPath}.unverifiedClaims[${claimIndex}]`;
|
|
9570
|
+
const claim = asRecord(rawClaim);
|
|
9571
|
+
const marker = readString(claim.marker);
|
|
9572
|
+
const claimText = readString(claim.claim);
|
|
9573
|
+
const owner = readString(claim.owner);
|
|
9574
|
+
const nextAction = readString(claim.nextAction);
|
|
9575
|
+
if (!marker || !/^ASSUMPTION-[A-Z0-9_-]+$/i.test(marker)) {
|
|
9576
|
+
throw runtimeDeliverableError(`${claimPath}.marker must use ASSUMPTION-*`);
|
|
9577
|
+
}
|
|
9578
|
+
if (markers.has(marker)) throw runtimeDeliverableError(`${claimPath}.marker must be unique`);
|
|
9579
|
+
markers.add(marker);
|
|
9580
|
+
declaredAssumptionMarkers.add(marker);
|
|
9581
|
+
if (!claimText) throw runtimeDeliverableError(`${claimPath}.claim is required`);
|
|
9582
|
+
if (!owner) throw runtimeDeliverableError(`${claimPath}.owner is required`);
|
|
9583
|
+
if (!nextAction) throw runtimeDeliverableError(`${claimPath}.nextAction is required`);
|
|
9584
|
+
for (const [label, value] of [["marker", `[${marker}]`], ["claim", claimText], ["owner", owner], ["nextAction", nextAction]]) {
|
|
9585
|
+
if (!content.includes(value)) throw runtimeDeliverableError(`${claimPath}.${label} must appear in the Markdown deliverable`);
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
for (const marker of markdownSourceMarkers) {
|
|
9589
|
+
if (!declaredSourceMarkers.has(marker)) {
|
|
9590
|
+
throw runtimeDeliverableError(`Markdown marker ${marker} is missing from sourceTruth.sources`);
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
for (const marker of markdownAssumptionMarkers) {
|
|
9594
|
+
if (!declaredAssumptionMarkers.has(marker)) {
|
|
9595
|
+
throw runtimeDeliverableError(`Markdown marker ${marker} is missing from sourceTruth.unverifiedClaims`);
|
|
9596
|
+
}
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9599
|
+
|
|
9600
|
+
function preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes, options = {}) {
|
|
9104
9601
|
const rawDeliverables = Array.isArray(envelope.deliverables) ? envelope.deliverables : [];
|
|
9105
9602
|
if (rawDeliverables.length > 20) {
|
|
9106
9603
|
throw runtimeDeliverableError("deliverables manifest exceeds the limit of 20");
|
|
@@ -9137,7 +9634,15 @@ function preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes
|
|
|
9137
9634
|
runtimeActionAllowedContentTypes: err?.runtimeActionAllowedContentTypes,
|
|
9138
9635
|
});
|
|
9139
9636
|
}
|
|
9637
|
+
if (options.requireSourceTruth === true && extname(filePath).toLowerCase() === ".md") {
|
|
9638
|
+
assertRuntimeMarkdownSourceTruth(
|
|
9639
|
+
deliverable.sourceTruth,
|
|
9640
|
+
`deliverables[${index}].sourceTruth`,
|
|
9641
|
+
filePath,
|
|
9642
|
+
);
|
|
9643
|
+
}
|
|
9140
9644
|
}
|
|
9645
|
+
normalized.sourceTruth = deliverable.sourceTruth;
|
|
9141
9646
|
deliverables.push(normalized);
|
|
9142
9647
|
if (deliverable.verification !== undefined) verifications.push(deliverable.verification);
|
|
9143
9648
|
}
|
|
@@ -9203,7 +9708,9 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9203
9708
|
const allowedContentTypes = runtimeAttachmentContentTypes(command);
|
|
9204
9709
|
let deliverableManifest;
|
|
9205
9710
|
try {
|
|
9206
|
-
deliverableManifest = preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes
|
|
9711
|
+
deliverableManifest = preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes, {
|
|
9712
|
+
requireSourceTruth: true,
|
|
9713
|
+
});
|
|
9207
9714
|
} catch (err) {
|
|
9208
9715
|
attachRuntimeCheckpointCandidates(err, envelope, cwd);
|
|
9209
9716
|
throw err;
|
|
@@ -9211,11 +9718,12 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9211
9718
|
const explicitUploadPaths = new Set(rawActions
|
|
9212
9719
|
.filter((action) => readRuntimeActionType(action) === "upload_artifact")
|
|
9213
9720
|
.map((action) => readRuntimeArtifactPath(runtimeActionBusinessFields(action)))
|
|
9214
|
-
.filter(Boolean)
|
|
9721
|
+
.filter(Boolean)
|
|
9722
|
+
.map((artifactPath) => realOrResolvedPath(resolve(cwd, expandHomePath(artifactPath)))));
|
|
9215
9723
|
const hasExplicitPrimaryArtifact = rawActions.some((action) =>
|
|
9216
9724
|
readRuntimeActionType(action) === "upload_artifact" && runtimeActionBusinessFields(action).isPrimary === true);
|
|
9217
9725
|
const generatedUploadActions = deliverableManifest.deliverables
|
|
9218
|
-
.filter((deliverable) => deliverable.required && !explicitUploadPaths.has(deliverable.
|
|
9726
|
+
.filter((deliverable) => deliverable.required && !explicitUploadPaths.has(deliverable.path))
|
|
9219
9727
|
.map((deliverable, index) => ({
|
|
9220
9728
|
type: "upload_artifact",
|
|
9221
9729
|
path: deliverable.rawPath,
|
|
@@ -9234,6 +9742,7 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9234
9742
|
let actions = orderRuntimeActionsForGovernance([...generatedUploadActions, ...rawActions]);
|
|
9235
9743
|
const answeredQuestionContinuation = runtimeActionAnsweredQuestionContinuation(command);
|
|
9236
9744
|
const acceptedConfirmationContinuation = runtimeActionAcceptedConfirmationContinuation(command);
|
|
9745
|
+
let documentBaseRevisionIdsByKey;
|
|
9237
9746
|
try {
|
|
9238
9747
|
actions = preflightRuntimeActionBatch(actions, {
|
|
9239
9748
|
answeredQuestionContinuation,
|
|
@@ -9241,8 +9750,14 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9241
9750
|
allowedContentTypes,
|
|
9242
9751
|
continuationScope,
|
|
9243
9752
|
cwd,
|
|
9753
|
+
requireSourceTruth: true,
|
|
9754
|
+
sourceTruthByPath: new Map(deliverableManifest.deliverables.map((deliverable) => [
|
|
9755
|
+
deliverable.path,
|
|
9756
|
+
deliverable.sourceTruth,
|
|
9757
|
+
])),
|
|
9244
9758
|
});
|
|
9245
9759
|
assertRuntimeActionsAllowedByAuthorizationEnvelope(actions, authorizationEnvelope);
|
|
9760
|
+
documentBaseRevisionIdsByKey = await preflightRuntimeDocumentRevisions(runtimeAuth, issueId, actions);
|
|
9246
9761
|
} catch (err) {
|
|
9247
9762
|
if (err && typeof err === "object" && !err.runtimeActionLedger) {
|
|
9248
9763
|
const fieldPath = readString(err.runtimeActionFieldPath);
|
|
@@ -9266,6 +9781,16 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9266
9781
|
const results = [];
|
|
9267
9782
|
const skipped = [];
|
|
9268
9783
|
const actionLedgerEntries = [];
|
|
9784
|
+
const issueDocumentConfirmationKeysNeedingBinding = new Set(actions.flatMap((action) => {
|
|
9785
|
+
const fields = runtimeActionBusinessFields(action);
|
|
9786
|
+
if (readRuntimeActionType(fields) !== "create_interaction") return [];
|
|
9787
|
+
const kind = inferCreateInteractionKind(fields);
|
|
9788
|
+
if (!["request_confirmation", "request_checkbox_confirmation"].includes(kind ?? "")) return [];
|
|
9789
|
+
const target = asRecord(asRecord(fields.payload).target);
|
|
9790
|
+
if (readString(target.type) !== "issue_document" || readString(target.revisionId)) return [];
|
|
9791
|
+
const key = readString(target.key);
|
|
9792
|
+
return key ? [key] : [];
|
|
9793
|
+
}));
|
|
9269
9794
|
const actionContext = {
|
|
9270
9795
|
rootIssueId: issueId,
|
|
9271
9796
|
cwd,
|
|
@@ -9277,6 +9802,9 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
9277
9802
|
createdReviewPathForInReview: false,
|
|
9278
9803
|
acceptedConfirmationContinuation,
|
|
9279
9804
|
answeredQuestionContinuation,
|
|
9805
|
+
documentBaseRevisionIdsByKey,
|
|
9806
|
+
issueDocumentConfirmationKeysNeedingBinding,
|
|
9807
|
+
issueDocumentTargetsByKey: new Map(),
|
|
9280
9808
|
finalCommentBodies: [],
|
|
9281
9809
|
};
|
|
9282
9810
|
for (const [actionIndex, action] of actions.entries()) {
|
|
@@ -9537,6 +10065,7 @@ async function applyInferredArtifactUploads(config, command, parsed, execution,
|
|
|
9537
10065
|
.filter(Boolean)
|
|
9538
10066
|
.map((entry) => resolve(cwd, entry)),
|
|
9539
10067
|
);
|
|
10068
|
+
// Refetch after execution: runtime actions may have created work products since input attachment lineage was loaded.
|
|
9540
10069
|
const existingWorkProducts = await listExistingIssueWorkProducts(runtimeAuth, issueId);
|
|
9541
10070
|
const inferenceText = [
|
|
9542
10071
|
parsed.summary,
|
|
@@ -10134,9 +10663,20 @@ function validateRuntimeActionsFile(config, flags) {
|
|
|
10134
10663
|
assertRuntimeActionValid(Array.isArray(envelope.actions), "runtime action envelope actions must be an array");
|
|
10135
10664
|
assertRuntimeActionValid(envelope.actions.length <= 20, "runtime action envelope actions exceed the limit of 20");
|
|
10136
10665
|
|
|
10137
|
-
|
|
10666
|
+
const requireSourceTruth = flags["require-source-truth"] === "true";
|
|
10667
|
+
const deliverableManifest = preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes, {
|
|
10668
|
+
requireSourceTruth,
|
|
10669
|
+
});
|
|
10138
10670
|
const actions = orderRuntimeActionsForGovernance(envelope.actions.map(asRecord));
|
|
10139
|
-
preflightRuntimeActionBatch(actions, {
|
|
10671
|
+
preflightRuntimeActionBatch(actions, {
|
|
10672
|
+
cwd,
|
|
10673
|
+
allowedContentTypes,
|
|
10674
|
+
requireSourceTruth,
|
|
10675
|
+
sourceTruthByPath: new Map(deliverableManifest.deliverables.map((deliverable) => [
|
|
10676
|
+
deliverable.path,
|
|
10677
|
+
deliverable.sourceTruth,
|
|
10678
|
+
])),
|
|
10679
|
+
});
|
|
10140
10680
|
process.stdout.write(`${JSON.stringify({ valid: true, actionCount: actions.length }, null, 2)}\n`);
|
|
10141
10681
|
}
|
|
10142
10682
|
|
package/dist/amaster-runtime.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { homedir, hostname } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
const CONNECTOR_VERSION = "0.1.0-beta.
|
|
8
|
+
const CONNECTOR_VERSION = "0.1.0-beta.13";
|
|
9
9
|
|
|
10
10
|
const CAPABILITIES = [
|
|
11
11
|
"remote_registration",
|