@amaster.ai/employee-runtime-connector 0.1.0-beta.7 → 0.1.0-beta.8

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.
@@ -2331,11 +2331,11 @@ function readWorkspaceStatus(cwd, opts = {}) {
2331
2331
  // ---- bundled entrypoint: src/amaster-runtime-daemon.mjs ----
2332
2332
 
2333
2333
  import { createHash } from "node:crypto";
2334
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2334
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
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.7";
2338
+ const CONNECTOR_VERSION = "0.1.0-beta.8";
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;
@@ -3515,11 +3515,23 @@ function canRuntimeAgentOrchestrateOrganization(context) {
3515
3515
  }
3516
3516
 
3517
3517
  function runtimeActionsInstruction(options = {}) {
3518
+ const authorizationEnvelope = options.authorizationEnvelope ?? {
3519
+ allowedActionClasses: new Set(["task_governance"]),
3520
+ sourceKind: "implicit_default",
3521
+ sourceId: "unknown",
3522
+ sourceRevision: "missing_envelope",
3523
+ };
3524
+ const knownActionClasses = ["task_governance", "organization_mutation", "executable_child"];
3525
+ const allowedActionClasses = knownActionClasses.filter((actionClass) =>
3526
+ authorizationEnvelope.allowedActionClasses.has(actionClass));
3527
+ const deniedActionClasses = knownActionClasses.filter((actionClass) =>
3528
+ !authorizationEnvelope.allowedActionClasses.has(actionClass));
3518
3529
  const allowedActionTypes = [
3519
3530
  "add_comment",
3520
3531
  "update_parent",
3521
3532
  "create_child_task",
3522
3533
  "upload_artifact",
3534
+ "upsert_document",
3523
3535
  ...(options.canOrchestrateOrganization ? ["agent_hire"] : []),
3524
3536
  "create_interaction",
3525
3537
  ].join(", ");
@@ -3530,6 +3542,10 @@ function runtimeActionsInstruction(options = {}) {
3530
3542
  const actionValidatorCommand = readString(options.actionValidatorCommand);
3531
3543
  return [
3532
3544
  "Runtime action bridge:",
3545
+ `Allowed runtime action classes for this wake: ${allowedActionClasses.join(", ") || "none"}.`,
3546
+ `Denied runtime action classes for this wake: ${deniedActionClasses.join(", ") || "none"}.`,
3547
+ `Wake authorization source: ${authorizationEnvelope.sourceKind}:${authorizationEnvelope.sourceId} revision ${authorizationEnvelope.sourceRevision}.`,
3548
+ "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.",
3533
3549
  "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.",
3534
3550
  "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.",
3535
3551
  "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.",
@@ -3567,6 +3583,7 @@ function runtimeActionsInstruction(options = {}) {
3567
3583
  ? `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.`
3568
3584
  : "",
3569
3585
  `Allowed action types: ${allowedActionTypes}.`,
3586
+ "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.",
3570
3587
  "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\":\"调整\"}}. Never use interactionType.",
3571
3588
  "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.",
3572
3589
  "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.",
@@ -3580,6 +3597,7 @@ function runtimeActionsInstruction(options = {}) {
3580
3597
  "upload_artifact actions do not accept required. Artifact required belongs only to top-level deliverables entries, not action objects.",
3581
3598
  "When a child artifact is promoted as a replacement for a parent artifact, run upload_artifact on the parent issue and include both sourceWorkProductId (the child artifact) and supersedesWorkProductId (the current parent artifact). Never claim the parent artifact was corrected without this durable lineage.",
3582
3599
  "For derived artifacts, set evidenceRole=derived, sourceWorkProductIds to every consumed input work product, and scenarioKeys to the unchanged source scenario set. For post-implementation review artifacts, set evidenceRole=review and sourceWorkProductIds to the exact current artifacts reviewed; stale or missing sources fail closed.",
3600
+ "When a child must consume current parent artifacts, set create_child_task.inputWorkProductIds to the exact parent work product UUIDs from task context. AMaster persists their attachment/SHA revision manifest and materializes only those required inputs under input-artifacts/. Do not assume parent attachments are implicitly copied into every child workspace.",
3583
3601
  "When creating a review child that must run after sibling implementation or governance tasks, express that ordering with blockedByClientKeys in the same create_child_task batch. Do not start a post-implementation review as an independent sibling without dependencies.",
3584
3602
  "For required deliverables, add top-level deliverables entries using {\"path\":\"relative/file\",\"required\":true}.",
3585
3603
  "update_parent accepts only status, comment, and force. Never put deliverables, artifacts, interactions, blockers, or child-task fields inside update_parent; keep top-level deliverables beside actions and use their dedicated action objects.",
@@ -3870,7 +3888,9 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
3870
3888
  const agentInstructions = renderAgentInstructionsBundle(asRecord(payload.agentInstructionsBundle));
3871
3889
  const runtimeAuth = commandRuntimeAuth(command);
3872
3890
  const hasRuntimeApi = Boolean(readString(runtimeAuth.apiUrl) && readString(runtimeAuth.apiKey));
3873
- const canOrchestrateOrganization = canRuntimeAgentOrchestrateOrganization(context);
3891
+ const authorizationEnvelope = runtimeActionAuthorizationEnvelope(command);
3892
+ const canOrchestrateOrganization = canRuntimeAgentOrchestrateOrganization(context)
3893
+ && authorizationEnvelope.allowedActionClasses.has("organization_mutation");
3874
3894
  const decompositionRequirement = commandLegacyTaskDecompositionRequirement(command);
3875
3895
  const explicitDecompositionRequired = decompositionRequirement.required
3876
3896
  && runtimeActionChildIssueSummaryEntries(command).length === 0;
@@ -3908,6 +3928,7 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
3908
3928
  explicitDecompositionRequired,
3909
3929
  artifactVerifierCommands: options.artifactVerifierCommands,
3910
3930
  actionValidatorCommand,
3931
+ authorizationEnvelope,
3911
3932
  }) : "",
3912
3933
  "",
3913
3934
  agentInstructions,
@@ -5755,6 +5776,97 @@ async function materializeIssueAttachments(config, command, workspace) {
5755
5776
  return materialized;
5756
5777
  }
5757
5778
 
5779
+ function safeArtifactInputSourceDir(entry, index) {
5780
+ const raw = readString(entry.sourceIssueIdentifier) ?? readString(entry.sourceIssueId) ?? `source-${index + 1}`;
5781
+ const safe = raw.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+|\.+$/g, "").slice(0, 120);
5782
+ return safe && safe !== "." && safe !== ".." ? safe : `source-${index + 1}`;
5783
+ }
5784
+
5785
+ async function materializeRequiredArtifactInputs(config, command, workspace) {
5786
+ const runtimeAuth = commandRuntimeAuth(command);
5787
+ if (!readString(runtimeAuth.apiUrl) || !readString(runtimeAuth.apiKey)) return [];
5788
+ const context = asRecord(asRecord(command.payload).contextSnapshot);
5789
+ const issue = asRecord(context.paperclipIssue);
5790
+ const manifest = asRecord(issue.artifactInputManifest);
5791
+ if (Object.keys(manifest).length === 0) return [];
5792
+ if (manifest.version !== 1 || !Array.isArray(manifest.entries)) {
5793
+ throw new Error("artifact_input_manifest_invalid: expected version 1 entries array");
5794
+ }
5795
+
5796
+ const targetRoot = join(workspace.cwd, "input-artifacts");
5797
+ rmSync(targetRoot, { recursive: true, force: true });
5798
+ mkdirSync(targetRoot, { recursive: true });
5799
+ const usedPaths = new Set();
5800
+ const materialized = [];
5801
+ for (const [index, rawEntry] of manifest.entries.entries()) {
5802
+ const entry = asRecord(rawEntry);
5803
+ const workProductId = readString(entry.workProductId);
5804
+ const attachmentId = readString(entry.attachmentId);
5805
+ const sha256 = readString(entry.sha256);
5806
+ const contentPath = readString(entry.contentPath);
5807
+ const byteSize = readNumber(entry.byteSize, null);
5808
+ if (!workProductId || !attachmentId || !/^[a-f0-9]{64}$/i.test(sha256 ?? "") || !contentPath || byteSize === null) {
5809
+ throw new Error(`artifact_input_manifest_invalid: entry ${index} is incomplete`);
5810
+ }
5811
+ const expectedContentPath = `/api/attachments/${attachmentId}/content`;
5812
+ if (contentPath !== expectedContentPath) {
5813
+ throw new Error(`artifact_input_manifest_invalid: entry ${index} contentPath does not match attachmentId`);
5814
+ }
5815
+ const body = await runtimeApiBuffer(runtimeAuth, contentPath);
5816
+ const actualSha256 = createHash("sha256").update(body).digest("hex");
5817
+ if (body.byteLength !== byteSize || actualSha256 !== sha256) {
5818
+ throw new Error(
5819
+ `artifact_input_integrity_mismatch: workProductId=${workProductId} expectedBytes=${byteSize} actualBytes=${body.byteLength} expectedSha256=${sha256} actualSha256=${actualSha256}`,
5820
+ );
5821
+ }
5822
+
5823
+ const sourceDir = safeArtifactInputSourceDir(entry, index);
5824
+ let filename = safeMaterializedAttachmentFilename({
5825
+ id: attachmentId,
5826
+ originalFilename: readString(entry.originalFilename),
5827
+ }, index);
5828
+ let relativePath = join("input-artifacts", sourceDir, filename).split(/[\\/]+/).join("/");
5829
+ if (usedPaths.has(relativePath)) {
5830
+ const ext = extname(filename);
5831
+ const stem = ext ? filename.slice(0, -ext.length) : filename;
5832
+ filename = `${stem}-${index + 1}${ext}`;
5833
+ relativePath = join("input-artifacts", sourceDir, filename).split(/[\\/]+/).join("/");
5834
+ }
5835
+ usedPaths.add(relativePath);
5836
+ const targetPath = join(workspace.cwd, relativePath);
5837
+ mkdirSync(dirname(targetPath), { recursive: true });
5838
+ writeFileSync(targetPath, body);
5839
+ chmodSync(targetPath, 0o444);
5840
+ materialized.push({
5841
+ id: attachmentId,
5842
+ workProductId,
5843
+ sourceIssueId: readString(entry.sourceIssueId),
5844
+ sourceIssueIdentifier: readString(entry.sourceIssueIdentifier),
5845
+ name: readString(entry.originalFilename) ?? filename,
5846
+ path: targetPath,
5847
+ relativePath,
5848
+ contentType: readString(entry.contentType),
5849
+ byteSize: body.byteLength,
5850
+ sha256,
5851
+ contentPath,
5852
+ });
5853
+ }
5854
+
5855
+ const localManifest = {
5856
+ version: 1,
5857
+ entries: materialized.map(({ path: _path, relativePath, ...entry }) => ({ ...entry, path: relativePath })),
5858
+ };
5859
+ const manifestPath = join(targetRoot, "artifact-input-manifest.json");
5860
+ writeFileSync(manifestPath, JSON.stringify(localManifest, null, 2) + "\n");
5861
+ chmodSync(manifestPath, 0o444);
5862
+ updateWorkspaceManifest(workspaceManifestPath(workspace), { artifactInputManifest: localManifest });
5863
+ await ingestLog(config, command, "system", "info", `Materialized ${materialized.length} required artifact input(s) into the execution workspace`, {
5864
+ artifactInputCount: materialized.length,
5865
+ artifactInputs: localManifest.entries,
5866
+ });
5867
+ return materialized;
5868
+ }
5869
+
5758
5870
  function issueCheckpointDir(workspace) {
5759
5871
  return join(dirname(workspace.runDir), "checkpoint");
5760
5872
  }
@@ -6055,6 +6167,9 @@ function runtimeChildActionFingerprint(action, issueId, parentIssueId) {
6055
6167
  status: readActionStatus(action.status) ?? null,
6056
6168
  priority: readActionPriority(action.priority) ?? null,
6057
6169
  parentClientKey: readString(action.parentClientKey) ?? null,
6170
+ inputWorkProductIds: Array.isArray(action.inputWorkProductIds)
6171
+ ? action.inputWorkProductIds.map(readString).filter(Boolean).sort()
6172
+ : [],
6058
6173
  blockedByClientKeys: Array.isArray(action.blockedByClientKeys)
6059
6174
  ? action.blockedByClientKeys.map(readString).filter(Boolean).sort()
6060
6175
  : [],
@@ -6934,6 +7049,7 @@ const RUNTIME_ACTION_ALLOWED_FIELDS = new Map([
6934
7049
  "title", "description", "status", "priority", "assigneeAgentId", "assigneeUserId",
6935
7050
  "assigneeAgentClientKey", "agentClientKey", "blockParentUntilDone", "executeBeforeConfirmation",
6936
7051
  "clientKey", "idempotencyKey", "parentClientKey", "blockedByIssueIds", "blockedByClientKeys", "create_child_task",
7052
+ "inputWorkProductIds",
6937
7053
  ])],
6938
7054
  ["agent_hire", new Set([
6939
7055
  "clientKey", "idempotencyKey", "key", "companyId", "role", "title", "icon", "reportsTo",
@@ -6954,6 +7070,9 @@ const RUNTIME_ACTION_ALLOWED_FIELDS = new Map([
6954
7070
  "sourceWorkProductId", "sourceWorkProductIds", "supersedesWorkProductId", "evidenceRole", "scenarioKeys",
6955
7071
  "idempotencyKey", "upload_artifact",
6956
7072
  ])],
7073
+ ["upsert_document", new Set([
7074
+ "key", "title", "body", "format", "changeSummary", "baseRevisionId", "upsert_document",
7075
+ ])],
6957
7076
  ["create_interaction", new Set([
6958
7077
  "kind", "continuationPolicy", "title", "summary", "idempotencyKey", "interaction", "version",
6959
7078
  "prompt", "question", "body", "content", "text", "message", "description", "acceptLabel",
@@ -7008,6 +7127,7 @@ const RUNTIME_ACTION_NUMBER_FIELDS = new Set(["budgetMonthlyCents", "minSelected
7008
7127
  const RUNTIME_ACTION_ARRAY_FIELDS = new Set([
7009
7128
  "blockedByIssueIds", "blockedByClientKeys", "desiredSkills", "sourceIssueIds", "artifactRefs", "tasks",
7010
7129
  "options", "defaultSelectedOptionIds", "questions", "sourceWorkProductIds", "scenarioKeys",
7130
+ "inputWorkProductIds",
7011
7131
  ]);
7012
7132
  const RUNTIME_ACTION_OBJECT_FIELDS = new Set([
7013
7133
  "adapterConfig", "instructionsBundle", "runtimeConfig", "permissions", "metadata", "target",
@@ -7358,6 +7478,21 @@ function preflightRuntimeAction(action, preflightContext, actionIndex) {
7358
7478
  );
7359
7479
  }
7360
7480
  }
7481
+ if (fields.inputWorkProductIds !== undefined) {
7482
+ const seen = new Set();
7483
+ for (const [index, rawWorkProductId] of fields.inputWorkProductIds.entries()) {
7484
+ const workProductId = readString(rawWorkProductId);
7485
+ assertRuntimeActionValid(
7486
+ workProductId && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(workProductId),
7487
+ `create_child_task inputWorkProductIds[${index}] must be a UUID`,
7488
+ );
7489
+ assertRuntimeActionValid(
7490
+ !seen.has(workProductId),
7491
+ `create_child_task inputWorkProductIds[${index}] duplicates ${workProductId}`,
7492
+ );
7493
+ seen.add(workProductId);
7494
+ }
7495
+ }
7361
7496
  return;
7362
7497
  }
7363
7498
  if (["agent_hire", "create_agent"].includes(type ?? "")) {
@@ -7413,6 +7548,20 @@ function preflightRuntimeAction(action, preflightContext, actionIndex) {
7413
7548
  resolveWorkspaceArtifactPath(fields, { cwd: preflightContext.cwd });
7414
7549
  return;
7415
7550
  }
7551
+ if (type === "upsert_document") {
7552
+ assertNoPlaceholderRuntimeActionText(fields, ["key", "title", "body", "changeSummary"]);
7553
+ const key = readBoundedActionString(fields, "key", 64);
7554
+ const body = readBoundedActionString(fields, "body", 524_288);
7555
+ assertRuntimeActionValid(key && /^[a-z0-9][a-z0-9_-]*$/.test(key), "upsert_document key must use lowercase letters, numbers, _ or -");
7556
+ assertRuntimeActionValid(body, "upsert_document action requires body");
7557
+ assertRuntimeActionValid(fields.format === "markdown", "upsert_document format must be markdown");
7558
+ const baseRevisionId = readString(fields.baseRevisionId);
7559
+ assertRuntimeActionValid(
7560
+ !baseRevisionId || /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(baseRevisionId),
7561
+ "upsert_document baseRevisionId must be a UUID",
7562
+ );
7563
+ return;
7564
+ }
7416
7565
  if (type === "create_interaction") {
7417
7566
  assertNoPlaceholderRuntimeActionText(fields, ["title", "summary", "payload"]);
7418
7567
  assertCanonicalCreateInteractionShape(fields);
@@ -7596,6 +7745,7 @@ const SINGLE_KEY_RUNTIME_ACTION_TYPES = new Set([
7596
7745
  "create_child_task",
7597
7746
  "create_interaction",
7598
7747
  "create_routine",
7748
+ "upsert_document",
7599
7749
  "update_company_profile",
7600
7750
  "update_parent",
7601
7751
  "upload_artifact",
@@ -7669,6 +7819,9 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
7669
7819
  assigneeAgentId: runtimeChildAssigneeAgentId(fields, runtimeAuth, command, actionContext),
7670
7820
  assigneeUserId: readString(fields.assigneeUserId),
7671
7821
  blockedByIssueIds: runtimeActionBlockedByIssueIds(fields, actionContext),
7822
+ ...(Array.isArray(fields.inputWorkProductIds)
7823
+ ? { inputWorkProductIds: fields.inputWorkProductIds.map(readString).filter(Boolean) }
7824
+ : {}),
7672
7825
  blockParentUntilDone,
7673
7826
  originKind: "amaster_runtime_child_task",
7674
7827
  originId: issueId,
@@ -7824,12 +7977,22 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
7824
7977
  const existingWorkProduct = existingExplicitArtifactUploadMatch(filePath, title, existingWorkProducts, fields);
7825
7978
  if (existingWorkProduct) {
7826
7979
  const metadata = asRecord(existingWorkProduct.metadata);
7980
+ const existingWorkProductId = readString(existingWorkProduct.id);
7981
+ const existingAttachmentId = readString(metadata.attachmentId);
7982
+ if (!existingWorkProductId || !existingAttachmentId) {
7983
+ throw new Error("runtime_action_postcondition_failed: reused artifact is missing work product or attachment identity");
7984
+ }
7985
+ await verifyUploadedArtifactPostcondition(runtimeAuth, issueId, {
7986
+ attachmentId: existingAttachmentId,
7987
+ workProductId: existingWorkProductId,
7988
+ sha256,
7989
+ }, fields);
7827
7990
  return {
7828
7991
  type,
7829
7992
  ...(originalType && originalType !== type ? { originalType } : {}),
7830
7993
  path: filePath,
7831
- attachmentId: readString(metadata.attachmentId),
7832
- workProductId: readString(existingWorkProduct.id),
7994
+ attachmentId: existingAttachmentId,
7995
+ workProductId: existingWorkProductId,
7833
7996
  sha256: readString(metadata.sha256) ?? sha256,
7834
7997
  byteSize: readNumber(metadata.byteSize, fileBytes.byteLength),
7835
7998
  originalFilename: readString(metadata.originalFilename) ?? filename,
@@ -7851,11 +8014,12 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
7851
8014
  if (!attachmentId) throw new Error("upload_artifact attachment response did not include id");
7852
8015
 
7853
8016
  let workProduct = null;
7854
- if (fields.createWorkProduct !== false) {
7855
- const contentPath = readString(attachment.contentPath) ?? `/api/attachments/${attachmentId}/content`;
7856
- const openPath = readString(attachment.openPath) ?? contentPath;
7857
- const downloadPath = readString(attachment.downloadPath) ?? `${contentPath}?download=1`;
7858
- workProduct = asRecord(await runtimeApiJson(runtimeAuth, "POST", `/api/issues/${encodeURIComponent(issueId)}/work-products`, {
8017
+ try {
8018
+ if (fields.createWorkProduct !== false) {
8019
+ const contentPath = readString(attachment.contentPath) ?? `/api/attachments/${attachmentId}/content`;
8020
+ const openPath = readString(attachment.openPath) ?? contentPath;
8021
+ const downloadPath = readString(attachment.downloadPath) ?? `${contentPath}?download=1`;
8022
+ workProduct = asRecord(await runtimeApiJson(runtimeAuth, "POST", `/api/issues/${encodeURIComponent(issueId)}/work-products`, {
7859
8023
  type: "artifact",
7860
8024
  provider: readString(fields.provider) ?? "paperclip",
7861
8025
  title,
@@ -7880,10 +8044,20 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
7880
8044
  ...(readString(fields.evidenceRole) ? { evidenceRole: readString(fields.evidenceRole) } : {}),
7881
8045
  ...(Array.isArray(fields.scenarioKeys) ? { scenarioKeys: fields.scenarioKeys.map(readString).filter(Boolean) } : {}),
7882
8046
  },
7883
- }));
7884
- if (!readString(workProduct.id)) {
7885
- throw new Error("runtime_action_postcondition_failed: artifact work product response did not include id");
8047
+ }));
8048
+ const workProductId = readString(workProduct.id);
8049
+ if (!workProductId) {
8050
+ throw new Error("runtime_action_postcondition_failed: artifact work product response did not include id");
8051
+ }
8052
+ await verifyUploadedArtifactPostcondition(runtimeAuth, issueId, {
8053
+ attachmentId,
8054
+ workProductId,
8055
+ sha256: readString(attachment.sha256) ?? sha256,
8056
+ }, fields);
7886
8057
  }
8058
+ } catch (err) {
8059
+ await cleanupFailedArtifactUpload(config, command, runtimeAuth, attachmentId, readString(workProduct?.id), err);
8060
+ throw err;
7887
8061
  }
7888
8062
 
7889
8063
  return {
@@ -7941,6 +8115,37 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
7941
8115
  };
7942
8116
  }
7943
8117
 
8118
+ if (type === "upsert_document") {
8119
+ assertNoPlaceholderRuntimeActionText(fields, ["key", "title", "body", "changeSummary"]);
8120
+ const key = readBoundedActionString(fields, "key", 64);
8121
+ if (!key || !/^[a-z0-9][a-z0-9_-]*$/.test(key)) {
8122
+ throw new Error("upsert_document key must use lowercase letters, numbers, _ or -");
8123
+ }
8124
+ const body = readBoundedActionString(fields, "body", 524_288);
8125
+ if (!body) throw new Error("upsert_document action requires body");
8126
+ if (fields.format !== "markdown") throw new Error("upsert_document format must be markdown");
8127
+ const document = asRecord(await runtimeApiJson(
8128
+ runtimeAuth,
8129
+ "PUT",
8130
+ `/api/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`,
8131
+ {
8132
+ title: readBoundedActionString(fields, "title", 200) ?? null,
8133
+ format: "markdown",
8134
+ body,
8135
+ changeSummary: readBoundedActionString(fields, "changeSummary", 500) ?? null,
8136
+ baseRevisionId: readString(fields.baseRevisionId) ?? null,
8137
+ },
8138
+ ));
8139
+ return {
8140
+ type,
8141
+ ...(originalType && originalType !== type ? { originalType } : {}),
8142
+ issueId,
8143
+ key,
8144
+ documentId: readString(document.id),
8145
+ revisionNumber: typeof document.latestRevisionNumber === "number" ? document.latestRevisionNumber : undefined,
8146
+ };
8147
+ }
8148
+
7944
8149
  if (type === "update_parent") {
7945
8150
  assertNoPlaceholderRuntimeActionText(fields, ["comment"]);
7946
8151
  const patch = {};
@@ -8396,6 +8601,16 @@ function runtimeActionExpectedShape(actionType, actionKind) {
8396
8601
  if (actionType === "upload_artifact") {
8397
8602
  return { type: "upload_artifact", path: "relative/path.ext", title: "Artifact title", summary: "Artifact summary" };
8398
8603
  }
8604
+ if (actionType === "upsert_document") {
8605
+ return {
8606
+ type: "upsert_document",
8607
+ key: "business-brief",
8608
+ title: "2026-07-16 每日经营简报",
8609
+ body: "# 每日经营简报\n\n经营摘要。",
8610
+ format: "markdown",
8611
+ changeSummary: "生成每日经营简报",
8612
+ };
8613
+ }
8399
8614
  if (actionType === "agent_hire" || actionType === "create_agent") {
8400
8615
  return {
8401
8616
  type: actionType,
@@ -8613,6 +8828,27 @@ function piOutputUsageMetadataMissing(parsed) {
8613
8828
  return totalTokens <= 0;
8614
8829
  }
8615
8830
 
8831
+ function runtimeBusinessDisposition(runtimeActions, inferredArtifactUploads) {
8832
+ const results = Array.isArray(runtimeActions?.results)
8833
+ ? runtimeActions.results.map(asRecord).filter((result) => !isSkippedRuntimeActionResult(result))
8834
+ : [];
8835
+ const terminalUpdate = results.find((result) =>
8836
+ readString(result.type) === "update_parent" &&
8837
+ ["done", "cancelled", "blocked", "in_review"].includes(readString(result.status) ?? ""));
8838
+ const terminalStatus = readString(terminalUpdate?.status);
8839
+ if (["done", "cancelled"].includes(terminalStatus ?? "")) {
8840
+ return { state: "completed", reason: `parent_${terminalStatus}` };
8841
+ }
8842
+ if (terminalStatus === "blocked") return { state: "blocked", reason: "parent_blocked" };
8843
+ if (terminalStatus === "in_review" || results.some((result) => readString(result.type) === "create_interaction")) {
8844
+ return { state: "waiting", reason: "review_or_interaction_path_recorded" };
8845
+ }
8846
+ if (results.length > 0 || readNumber(inferredArtifactUploads?.appliedCount, 0) > 0) {
8847
+ return { state: "needs_followup", reason: "durable_progress_without_terminal_disposition" };
8848
+ }
8849
+ return { state: "missing", reason: "no_durable_action_or_live_path" };
8850
+ }
8851
+
8616
8852
  function piCompletionOutputStopped(parsed, execution) {
8617
8853
  return execution?.timedOut !== true
8618
8854
  && execution?.signal === "SIGTERM"
@@ -9071,6 +9307,72 @@ async function listExistingIssueWorkProducts(runtimeAuth, issueId) {
9071
9307
  }
9072
9308
  }
9073
9309
 
9310
+ async function verifyUploadedArtifactPostcondition(runtimeAuth, issueId, expected, fields) {
9311
+ const [workProducts, attachments] = await Promise.all([
9312
+ runtimeApiJson(runtimeAuth, "GET", `/api/issues/${encodeURIComponent(issueId)}/work-products`),
9313
+ runtimeApiJson(runtimeAuth, "GET", `/api/issues/${encodeURIComponent(issueId)}/attachments`),
9314
+ ]);
9315
+ const workProduct = Array.isArray(workProducts)
9316
+ ? workProducts.map(asRecord).find((entry) => readString(entry.id) === expected.workProductId)
9317
+ : null;
9318
+ const attachment = Array.isArray(attachments)
9319
+ ? attachments.map(asRecord).find((entry) => readString(entry.id) === expected.attachmentId)
9320
+ : null;
9321
+ const metadata = asRecord(workProduct?.metadata);
9322
+ const expectedSourceIds = Array.isArray(fields.sourceWorkProductIds)
9323
+ ? fields.sourceWorkProductIds.map(readString).filter(Boolean).sort()
9324
+ : [];
9325
+ const persistedSourceIds = Array.isArray(metadata.sourceWorkProductIds)
9326
+ ? metadata.sourceWorkProductIds.map(readString).filter(Boolean).sort()
9327
+ : [];
9328
+ const expectedScenarioKeys = Array.isArray(fields.scenarioKeys)
9329
+ ? fields.scenarioKeys.map(readString).filter(Boolean).sort()
9330
+ : [];
9331
+ const persistedScenarioKeys = Array.isArray(metadata.scenarioKeys)
9332
+ ? metadata.scenarioKeys.map(readString).filter(Boolean).sort()
9333
+ : [];
9334
+ const failures = [
9335
+ !workProduct ? "work product missing" : null,
9336
+ !attachment ? "attachment missing" : null,
9337
+ readString(metadata.attachmentId) !== expected.attachmentId ? "work product attachmentId mismatch" : null,
9338
+ readString(metadata.sha256) !== expected.sha256 ? "work product SHA mismatch" : null,
9339
+ readString(attachment?.sha256) !== expected.sha256 ? "attachment SHA mismatch" : null,
9340
+ readString(metadata.sourceWorkProductId) !== readString(fields.sourceWorkProductId) ? "sourceWorkProductId mismatch" : null,
9341
+ readString(metadata.supersedesWorkProductId) !== readString(fields.supersedesWorkProductId) ? "supersedesWorkProductId mismatch" : null,
9342
+ readString(metadata.evidenceRole) !== readString(fields.evidenceRole) ? "evidenceRole mismatch" : null,
9343
+ JSON.stringify(persistedSourceIds) !== JSON.stringify(expectedSourceIds) ? "sourceWorkProductIds mismatch" : null,
9344
+ JSON.stringify(persistedScenarioKeys) !== JSON.stringify(expectedScenarioKeys) ? "scenarioKeys mismatch" : null,
9345
+ ].filter(Boolean);
9346
+ if (failures.length > 0) {
9347
+ throw new Error(`runtime_action_postcondition_failed: artifact upload readback failed: ${failures.join("; ")}`);
9348
+ }
9349
+ }
9350
+
9351
+ async function cleanupFailedArtifactUpload(config, command, runtimeAuth, attachmentId, workProductId, cause) {
9352
+ const cleanupFailures = [];
9353
+ if (workProductId) {
9354
+ try {
9355
+ await runtimeApiJson(runtimeAuth, "DELETE", `/api/work-products/${encodeURIComponent(workProductId)}`);
9356
+ } catch (err) {
9357
+ cleanupFailures.push(`workProduct=${err instanceof Error ? err.message : String(err)}`);
9358
+ }
9359
+ }
9360
+ try {
9361
+ await runtimeApiJson(runtimeAuth, "DELETE", `/api/attachments/${encodeURIComponent(attachmentId)}`);
9362
+ } catch (err) {
9363
+ cleanupFailures.push(`attachment=${err instanceof Error ? err.message : String(err)}`);
9364
+ }
9365
+ await ingestLog(config, command, "system", "error", "Artifact upload postcondition failed", {
9366
+ attachmentId,
9367
+ workProductId: workProductId ?? null,
9368
+ cause: cause instanceof Error ? cause.message : String(cause),
9369
+ cleanupFailures,
9370
+ });
9371
+ if (cleanupFailures.length > 0) {
9372
+ throw new Error(`${cause instanceof Error ? cause.message : String(cause)}; cleanup failed: ${cleanupFailures.join("; ")}`);
9373
+ }
9374
+ }
9375
+
9074
9376
  function existingArtifactUploadMatches(artifact, existingWorkProducts) {
9075
9377
  const filename = basename(artifact.path);
9076
9378
  const byteSize = statSync(artifact.path).size;
@@ -9245,9 +9547,10 @@ async function executeRunCommand(config, command) {
9245
9547
  });
9246
9548
  }
9247
9549
  await materializeIssueCheckpoint(config, command, workspace);
9248
- let materializedAttachments = [];
9550
+ const requiredArtifactInputs = await materializeRequiredArtifactInputs(config, command, workspace);
9551
+ let materializedAttachments = requiredArtifactInputs;
9249
9552
  try {
9250
- materializedAttachments = await materializeIssueAttachments(config, command, workspace);
9553
+ materializedAttachments = [...requiredArtifactInputs, ...await materializeIssueAttachments(config, command, workspace)];
9251
9554
  } catch (err) {
9252
9555
  await ingestLog(config, command, "system", "warn", `Failed to materialize issue attachments: ${err instanceof Error ? err.message : String(err)}`, {
9253
9556
  error: err instanceof Error ? err.message : String(err),
@@ -9472,6 +9775,7 @@ async function executeRunCommand(config, command) {
9472
9775
  ? "Executor cancelled by AMaster control plane"
9473
9776
  : execution.spawnError ?? parsedErrorMessage ?? runtimeActionError ?? inferredArtifactError ?? (succeeded ? null : `Executor exited with code ${execution.exitCode ?? "unknown"}`);
9474
9777
  const artifactUploadSummary = runtimeArtifactUploadSummary(runtimeActions, inferredArtifactUploads);
9778
+ const businessDisposition = runtimeBusinessDisposition(runtimeActions, inferredArtifactUploads);
9475
9779
  const costUsage = parsedCostUsage(parsed.usage);
9476
9780
  const result = {
9477
9781
  executorKind: executor.kind,
@@ -9542,6 +9846,7 @@ async function executeRunCommand(config, command) {
9542
9846
  ...(runtimeActions ? { runtimeActions } : {}),
9543
9847
  ...(inferredArtifactUploads ? { inferredArtifactUploads } : {}),
9544
9848
  ...(artifactUploadSummary ? { artifactUploadSummary } : {}),
9849
+ businessDisposition,
9545
9850
  };
9546
9851
  const completion = await completeCommand(config, command, cancelled ? "cancelled" : succeeded ? "succeeded" : "failed", result, error ?? undefined);
9547
9852
  if (!completion) {
@@ -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.7";
8
+ const CONNECTOR_VERSION = "0.1.0-beta.8";
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.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "AMaster Employee runtime connector CLI and daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",