@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.
@@ -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.11";
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; when no business revision exists, use the current run id as revisionId instead of omitting target.",
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\":\"wake_assignee_on_accept\",\"payload\":{\"version\":1,\"prompt\":\"Confirm this result?\",\"acceptLabel\":\"确认\",\"rejectLabel\":\"调整\",\"target\":{\"type\":\"custom\",\"key\":\"delivery-review\",\"label\":\"交付结果\",\"revisionId\":\"v1\"}}}. Never use interactionType.",
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
- return type === "update_parent" ? 10 : 0;
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) => ({ action, index, priority: runtimeActionExecutionPriority(action) }))
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: readString(attachment.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
- assertRuntimeActionValid(readRuntimeArtifactPath(fields), "upload_artifact action requires path");
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
- const payload = normalizeCreateInteractionPayload(fields, kind);
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 defaultContinuationPolicy = kind === "request_confirmation" ? "wake_assignee_on_accept" : "wake_assignee";
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) ?? null,
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: readString(document.id),
8222
- revisionNumber: typeof document.latestRevisionNumber === "number" ? document.latestRevisionNumber : undefined,
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 preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes) {
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.rawPath))
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
- preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes);
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, { cwd, allowedContentTypes });
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
 
@@ -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.11";
8
+ const CONNECTOR_VERSION = "0.1.0-beta.13";
9
9
 
10
10
  const CAPABILITIES = [
11
11
  "remote_registration",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/employee-runtime-connector",
3
- "version": "0.1.0-beta.11",
3
+ "version": "0.1.0-beta.13",
4
4
  "description": "AMaster Employee runtime connector CLI and daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",