@amaster.ai/employee-runtime-connector 0.1.0-beta.0 → 0.1.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/amaster-runtime-daemon.mjs +1316 -90
- package/dist/amaster-runtime.mjs +1 -1
- package/package.json +1 -1
|
@@ -264,7 +264,7 @@ function buildConfig(env = process.env, flags = {}) {
|
|
|
264
264
|
0,
|
|
265
265
|
),
|
|
266
266
|
artifactVerifierCommands: splitList(
|
|
267
|
-
flags.artifactVerifierCommands ?? env.AMASTER_ARTIFACT_VERIFIER_COMMANDS ?? "
|
|
267
|
+
flags.artifactVerifierCommands ?? env.AMASTER_ARTIFACT_VERIFIER_COMMANDS ?? "",
|
|
268
268
|
),
|
|
269
269
|
orphanReaperIntervalSeconds: parseNonNegativeInteger(
|
|
270
270
|
flags.orphanReaperIntervalSeconds ?? env.AMASTER_RUNTIME_ORPHAN_REAPER_INTERVAL_SECONDS,
|
|
@@ -585,6 +585,23 @@ function piRuntimeActionText(message) {
|
|
|
585
585
|
return text.includes("AMASTER_RUNTIME_ACTIONS") ? text : "";
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
function piEventHasRuntimeActionMarkerInToolOutput(event) {
|
|
589
|
+
const directMessage = asRecord(event.message);
|
|
590
|
+
if (directMessage.role === "toolResult" && piRuntimeActionText(directMessage)) return true;
|
|
591
|
+
|
|
592
|
+
const eventMessages = Array.isArray(event.messages) ? event.messages : [];
|
|
593
|
+
if (eventMessages.some((message) => {
|
|
594
|
+
const record = asRecord(message);
|
|
595
|
+
return record.role === "toolResult" && Boolean(piRuntimeActionText(record));
|
|
596
|
+
})) return true;
|
|
597
|
+
|
|
598
|
+
const toolResults = Array.isArray(event.toolResults) ? event.toolResults : [];
|
|
599
|
+
if (toolResults.some((result) => Boolean(piRuntimeActionText(result)))) return true;
|
|
600
|
+
|
|
601
|
+
if (event.type === "tool_execution_end" && piRuntimeActionText(event.result)) return true;
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
588
605
|
function appendUniqueText(values, text) {
|
|
589
606
|
const normalized = typeof text === "string" ? text.trim() : "";
|
|
590
607
|
if (!normalized) return false;
|
|
@@ -710,6 +727,7 @@ function parsePiJsonl(stdout) {
|
|
|
710
727
|
let terminalEventType = null;
|
|
711
728
|
let stopReason = null;
|
|
712
729
|
let hasAssistantOutput = false;
|
|
730
|
+
let runtimeActionMarkerInToolOutput = false;
|
|
713
731
|
const messages = [];
|
|
714
732
|
const runtimeActionTexts = [];
|
|
715
733
|
const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
|
|
@@ -717,6 +735,8 @@ function parsePiJsonl(stdout) {
|
|
|
717
735
|
for (const rawLine of String(stdout ?? "").split(/\r?\n/)) {
|
|
718
736
|
const event = parseJsonLine(rawLine.trim());
|
|
719
737
|
if (!event) continue;
|
|
738
|
+
runtimeActionMarkerInToolOutput = piEventHasRuntimeActionMarkerInToolOutput(event)
|
|
739
|
+
|| runtimeActionMarkerInToolOutput;
|
|
720
740
|
if (event.type === "session") {
|
|
721
741
|
sessionId = readString(event.id) ?? sessionId;
|
|
722
742
|
continue;
|
|
@@ -755,6 +775,7 @@ function parsePiJsonl(stdout) {
|
|
|
755
775
|
sessionId,
|
|
756
776
|
summary: messages.at(-1) ?? "",
|
|
757
777
|
runtimeActionText: runtimeActionTexts.join("\n\n"),
|
|
778
|
+
runtimeActionMarkerInToolOutput,
|
|
758
779
|
usage,
|
|
759
780
|
sawTurnEnd,
|
|
760
781
|
terminalEventType,
|
|
@@ -2310,11 +2331,11 @@ function readWorkspaceStatus(cwd, opts = {}) {
|
|
|
2310
2331
|
// ---- bundled entrypoint: src/amaster-runtime-daemon.mjs ----
|
|
2311
2332
|
|
|
2312
2333
|
import { createHash } from "node:crypto";
|
|
2313
|
-
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";
|
|
2314
2335
|
import { homedir, hostname } from "node:os";
|
|
2315
2336
|
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2316
2337
|
import { spawn, spawnSync } from "node:child_process";
|
|
2317
|
-
const CONNECTOR_VERSION = "0.1.0-beta.
|
|
2338
|
+
const CONNECTOR_VERSION = "0.1.0-beta.10";
|
|
2318
2339
|
const MAX_INFERRED_ARTIFACT_UPLOADS = 20;
|
|
2319
2340
|
const MAX_CHECKPOINT_BYTES = 20 * 1024 * 1024;
|
|
2320
2341
|
const CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -3371,6 +3392,73 @@ function renderTaskMarkdown(context) {
|
|
|
3371
3392
|
return readString(context.paperclipTaskMarkdown);
|
|
3372
3393
|
}
|
|
3373
3394
|
|
|
3395
|
+
const LEGACY_DECOMPOSITION_NEGATION_PATTERNS = [
|
|
3396
|
+
/(?:不要|不得|无需|不需要|不用|禁止|避免)(?:再|去)?[^。!?;;.!?\n]{0,120}(?:子任务|子项任务)/i,
|
|
3397
|
+
/\b(?:do\s+not|don't|must\s+not|should\s+not|need\s+not|no\s+need\s+to|avoid)\b[^.!?;\n]{0,120}\b(?:child\s+tasks?|subtasks?)\b/i,
|
|
3398
|
+
];
|
|
3399
|
+
|
|
3400
|
+
const LEGACY_DECOMPOSITION_REQUIREMENT_PATTERNS = [
|
|
3401
|
+
{
|
|
3402
|
+
id: "zh_split_child_tasks",
|
|
3403
|
+
language: "zh",
|
|
3404
|
+
pattern: /(?:拆出|拆分(?:为|成)?|拆解(?:为|成)?)[^。!?;;.!?\n]{0,80}(?:子任务|子项任务)/i,
|
|
3405
|
+
},
|
|
3406
|
+
{
|
|
3407
|
+
id: "zh_create_child_tasks",
|
|
3408
|
+
language: "zh",
|
|
3409
|
+
pattern: /(?:创建|建立|生成|新增)\s*(?:(?:以下|这些|新的|具体|独立|可执行|对应|若干|多个|[一二三四五六七八九十两]+|\d+)\s*){0,5}(?:个|条|项)?\s*(?:子任务|子项任务)(?!\s*(?:报告|清单|对比|列表|状态|概览))/i,
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
id: "en_create_child_tasks",
|
|
3413
|
+
language: "en",
|
|
3414
|
+
pattern: /\bcreate\s+(?:(?:the|these|those|following|new|concrete|independent|executable|one|two|three|four|five|six|seven|eight|nine|ten|several|multiple|\d+)\s+){0,5}(?:child\s+tasks?|subtasks?)\b(?!\s+(?:summar(?:y|ies)|reports?|lists?|overviews?)\b)/i,
|
|
3415
|
+
},
|
|
3416
|
+
{
|
|
3417
|
+
id: "en_split_child_tasks",
|
|
3418
|
+
language: "en",
|
|
3419
|
+
pattern: /\b(?:split|decompose|break\s+down)\b[^.!?;\n]{0,80}\b(?:child\s+tasks?|subtasks?)\b/i,
|
|
3420
|
+
},
|
|
3421
|
+
];
|
|
3422
|
+
|
|
3423
|
+
function legacyTaskDecompositionRequirement(context) {
|
|
3424
|
+
const issue = asRecord(context.paperclipIssue);
|
|
3425
|
+
if (readString(issue.workMode) === "planning") return { required: false, source: "planning" };
|
|
3426
|
+
const fields = [
|
|
3427
|
+
{ field: "title", text: readString(issue.title) },
|
|
3428
|
+
{ field: "description", text: readString(issue.description) },
|
|
3429
|
+
];
|
|
3430
|
+
for (const { field, text } of fields) {
|
|
3431
|
+
if (!text) continue;
|
|
3432
|
+
const clauses = text
|
|
3433
|
+
.split(/[。!?;.!?;\n]+/)
|
|
3434
|
+
.map((clause) => clause.trim())
|
|
3435
|
+
.filter(Boolean);
|
|
3436
|
+
for (const [clauseIndex, clause] of clauses.entries()) {
|
|
3437
|
+
if (LEGACY_DECOMPOSITION_NEGATION_PATTERNS.some((pattern) => pattern.test(clause))) continue;
|
|
3438
|
+
const matched = LEGACY_DECOMPOSITION_REQUIREMENT_PATTERNS.find(({ pattern }) => pattern.test(clause));
|
|
3439
|
+
if (!matched) continue;
|
|
3440
|
+
return {
|
|
3441
|
+
required: true,
|
|
3442
|
+
source: "legacy_text_hard_gate",
|
|
3443
|
+
match: {
|
|
3444
|
+
field,
|
|
3445
|
+
clauseIndex,
|
|
3446
|
+
pattern: matched.id,
|
|
3447
|
+
language: matched.language,
|
|
3448
|
+
charCount: clause.length,
|
|
3449
|
+
sha256Prefix: createHash("sha256").update(clause).digest("hex").slice(0, 12),
|
|
3450
|
+
},
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return { required: false, source: "none" };
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
function commandLegacyTaskDecompositionRequirement(command) {
|
|
3458
|
+
const payload = asRecord(command.payload);
|
|
3459
|
+
return legacyTaskDecompositionRequirement(asRecord(payload.contextSnapshot));
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3374
3462
|
function normalizeAgentInstructionsFiles(bundle) {
|
|
3375
3463
|
const files = asRecord(bundle.files);
|
|
3376
3464
|
const selected = [];
|
|
@@ -3427,25 +3515,69 @@ function canRuntimeAgentOrchestrateOrganization(context) {
|
|
|
3427
3515
|
}
|
|
3428
3516
|
|
|
3429
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));
|
|
3529
|
+
const authorizationOptionIdEntries = knownActionClasses
|
|
3530
|
+
.map((actionClass) => [actionClass, authorizationEnvelope.authorizationOptionIds?.get(actionClass)])
|
|
3531
|
+
.filter(([, optionId]) => optionId);
|
|
3532
|
+
const authorizationOptionIdGuidance = authorizationOptionIdEntries.length === knownActionClasses.length
|
|
3533
|
+
? `Authorization checkbox option ids: ${authorizationOptionIdEntries
|
|
3534
|
+
.map(([actionClass, optionId]) => `${actionClass}=${optionId}`)
|
|
3535
|
+
.join(", ")}. Use these exact ids when requesting authorization; do not invent another prefix.`
|
|
3536
|
+
: "Authorization checkbox option ids are unavailable for this wake. Do not invent option ids that grant organization_mutation or executable_child authorization.";
|
|
3430
3537
|
const allowedActionTypes = [
|
|
3431
3538
|
"add_comment",
|
|
3432
3539
|
"update_parent",
|
|
3433
3540
|
"create_child_task",
|
|
3434
3541
|
"upload_artifact",
|
|
3542
|
+
"upsert_document",
|
|
3435
3543
|
...(options.canOrchestrateOrganization ? ["agent_hire"] : []),
|
|
3436
3544
|
"create_interaction",
|
|
3437
3545
|
].join(", ");
|
|
3438
3546
|
const currentAgentAdapterType = readString(options.currentAgentAdapterType);
|
|
3547
|
+
const artifactVerifierCommands = Array.isArray(options.artifactVerifierCommands)
|
|
3548
|
+
? options.artifactVerifierCommands.map(readString).filter(Boolean)
|
|
3549
|
+
: [];
|
|
3550
|
+
const actionValidatorCommand = readString(options.actionValidatorCommand);
|
|
3439
3551
|
return [
|
|
3440
3552
|
"Runtime action bridge:",
|
|
3441
|
-
|
|
3553
|
+
`Allowed runtime action classes for this wake: ${allowedActionClasses.join(", ") || "none"}.`,
|
|
3554
|
+
`Denied runtime action classes for this wake: ${deniedActionClasses.join(", ") || "none"}.`,
|
|
3555
|
+
`Wake authorization source: ${authorizationEnvelope.sourceKind}:${authorizationEnvelope.sourceId} revision ${authorizationEnvelope.sourceRevision}.`,
|
|
3556
|
+
authorizationOptionIdGuidance,
|
|
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
|
+
"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
|
+
"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
|
+
"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.",
|
|
3442
3561
|
"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
|
+
"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.",
|
|
3443
3564
|
"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.",
|
|
3444
3565
|
"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.",
|
|
3445
3566
|
"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.",
|
|
3446
|
-
"
|
|
3567
|
+
"Create a child only for an independently executable residual work item. Do not create a nested child merely to prove that child creation works, and do not treat the current issue title as an instruction to recreate the issue itself. If the current child already embodies the requested test or work item, complete it directly.",
|
|
3568
|
+
"Every root create_child_task with status todo or in_progress must establish the parent disposition in the same batch. Set blockParentUntilDone true when the parent depends on that child. Only omit the wait when the same batch also closes the parent's independent scope with update_parent status done or cancelled. parentId alone is structural and never proves that the parent is waiting; missing both paths fails with delegated_parent_wait_required.",
|
|
3569
|
+
"Creating child tasks and delegating them to existing agents is task execution, not organization orchestration. Use assignee ids only from the current task context; server permissions still decide whether an assignment is allowed.",
|
|
3570
|
+
"Runtime actions use a strict schema. Unknown fields, unsupported action types, duplicate clientKey values, unknown client-key references, and client-key dependency cycles reject the entire batch before any mutation. blockedByIssueIds is supported only on create_child_task for existing issue UUIDs supplied by the current task context; connector preflight checks UUID shape, while the server validates same-company existence and permissions. Do not place it on update_parent or invent fields that are not in the canonical action examples.",
|
|
3571
|
+
"Use native JSON scalar types exactly: booleans such as create_child_task.blockParentUntilDone, top-level deliverables[].required, and create_interaction payload questions[].required must be true or false without quotes. Numbers such as minSelected/maxSelected/version must be numeric literals without quotes. String forms such as \"true\" or \"1\" reject the whole batch; AMaster does not silently coerce them.",
|
|
3572
|
+
"For same-batch relationships, assign a unique clientKey to each agent_hire or create_child_task producer. Use reportsToAgentClientKey for a new agent's manager, assigneeAgentClientKey for a child owner, parentClientKey for a nested child, and blockedByClientKeys for first-class child blockers. Use blockedByIssueIds only when a new child depends on existing issue UUIDs supplied by the current task context; connector preflight checks UUID shape and the server validates same-company existence. References may point forward within the same batch; every referenced key must resolve to the correct resource kind in that batch.",
|
|
3573
|
+
options.explicitDecompositionRequired
|
|
3574
|
+
? "This standard task explicitly requires child task decomposition. The runtime action batch must include create_child_task or one suggest_tasks interaction unless child tasks already exist. A checklist in a comment or artifact is not decomposition. Create independently executable internal preparation tasks now. Use backlog for recorded work that should not start now, and use suggest_tasks for work that must wait for human approval. When a pending interaction is created, keep the parent in_review rather than in_progress or done."
|
|
3575
|
+
: "",
|
|
3576
|
+
options.canOrchestrateOrganization
|
|
3577
|
+
? "Pending-interaction batch gate: before emitting JSON, scan every agent_hire and create_child_task. Every co-batched hire must independently set executeBeforeConfirmation true; every create_child_task must independently be backlog, set executeBeforeConfirmation true, or move into suggest_tasks. blockParentUntilDone=false and a flagged assignee hire do not exempt a child. Otherwise move the hires to a later continuation and use suggest_tasks for work that must wait for acceptance. This applies to every pending interaction, including budget or business approval."
|
|
3578
|
+
: "Pending-interaction batch gate: before emitting JSON, scan every create_child_task. Every create_child_task must independently be backlog, set executeBeforeConfirmation true, or move into suggest_tasks. blockParentUntilDone=false does not exempt a child. Use suggest_tasks for work that must wait for acceptance.",
|
|
3447
3579
|
options.canOrchestrateOrganization
|
|
3448
|
-
? "For AMaster organization-building requests, such as creating other roles, hiring digital employees, building the first team, or assigning work to roles, do not stop at a strategy document. Include agent_hire for each needed role and create_child_task for that role's first concrete assignment. If approval
|
|
3580
|
+
? "For AMaster organization-building requests, such as creating other roles, hiring digital employees, building the first team, or assigning work to roles, do not stop at a strategy document. Include agent_hire for each needed role and create_child_task for that role's first concrete assignment. agent_hire already creates the native pending hire approval; do not also ask whether to approve that hire in a create_interaction action in the same batch. If approval must happen before any hire record exists, create only a proposal interaction and hire in a later continuation. These organization batches must include add_comment or update_parent.comment explaining what was created, requested, assigned, or blocked."
|
|
3449
3581
|
: "",
|
|
3450
3582
|
options.canOrchestrateOrganization
|
|
3451
3583
|
? `For agent_hire, set adapterType explicitly. Default to the current executing agent adapterType${currentAgentAdapterType ? ` (${currentAgentAdapterType})` : ""} unless the task names a different approved adapter.`
|
|
@@ -3453,20 +3585,41 @@ function runtimeActionsInstruction(options = {}) {
|
|
|
3453
3585
|
options.canOrchestrateOrganization
|
|
3454
3586
|
? `For agent_hire, role must be one of: ${AGENT_HIRE_SCHEMA_ROLES.join(", ")}. Put custom business role names in name/title/capabilities/instructionsBundle, not in role. Map product/customer insight to research or product, growth/revenue to marketing or sales, finance/budget to finance, engineering delivery to engineering, legal/compliance to legal, customer success/support to customer_support. capabilities must be a string, not an array. instructionsBundle must use {entryFile:"AGENTS.md", files: {"AGENTS.md": "..."}}, and instructionsBundle.files must contain at least one file.`
|
|
3455
3587
|
: "",
|
|
3588
|
+
options.canOrchestrateOrganization
|
|
3589
|
+
? "Every agent_hire must declare exactly one reporting disposition. For an existing manager from the current task context, set reportsTo to that manager UUID. For a manager hired in the same batch, set reportsToAgentClientKey to the manager's clientKey. Only a genuinely company-root role may set topLevel true. reportsToAgentId is unsupported; use reportsTo for an existing manager UUID."
|
|
3590
|
+
: "",
|
|
3456
3591
|
options.canOrchestrateOrganization
|
|
3457
3592
|
? "When assigning a newly hired agent's first task in the same action batch, set clientKey on the agent_hire action and set assigneeAgentClientKey or agentClientKey on the create_child_task action to that same key. Do not leave the first task assigned to the current CEO/executing agent unless the CEO should actually do the work."
|
|
3458
3593
|
: "",
|
|
3459
|
-
"
|
|
3460
|
-
"
|
|
3461
|
-
"
|
|
3594
|
+
"Fallback output format: put the marker AMASTER_RUNTIME_ACTIONS in the final assistant message, then exactly one fenced json object. Use this literal shape with real action objects: {\"version\":1,\"actions\":[{\"type\":\"add_comment\",\"body\":\"Progress or result summary\"}]}.",
|
|
3595
|
+
"Never use write, bash, cat, printf, echo, or any other tool to emit the marker or JSON. You may prepare and validate the envelope in a temporary workspace file, but the final assistant message itself must contain the exact marker and JSON; tool output is ignored by the runtime action bridge.",
|
|
3596
|
+
"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
|
+
"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
|
+
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.`
|
|
3600
|
+
: "",
|
|
3462
3601
|
`Allowed action types: ${allowedActionTypes}.`,
|
|
3463
|
-
"For
|
|
3464
|
-
"For create_interaction
|
|
3465
|
-
"For
|
|
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
|
+
"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
|
+
"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
|
+
"For create_interaction request_confirmation, follow the complete example above and do not use options arrays.",
|
|
3607
|
+
"For create_interaction request_checkbox_confirmation, use payload {\"version\":1,\"prompt\":\"Select approved items\",\"options\":[{\"id\":\"option_key\",\"label\":\"Option label\"}],\"minSelected\":1,\"maxSelected\":2,\"acceptLabel\":\"确认\",\"rejectLabel\":\"调整\",\"target\":{\"type\":\"custom\",\"key\":\"approved-items\",\"label\":\"待批准项目\",\"revisionId\":\"v1\"}}.",
|
|
3466
3608
|
"For create_interaction suggest_tasks, use payload {\"version\":1,\"tasks\":[{\"clientKey\":\"task_key\",\"title\":\"Task title\",\"description\":\"Task description\",\"priority\":\"medium\"}]}.",
|
|
3609
|
+
"When a suggested task should start automatically after acceptance, set assigneeAgentId to an agent id from the current task context. Set assigneeUserId only when a known user must own the task. Omit both assignee fields only when the accepted task should remain intentionally unassigned in backlog. Never substitute a role name for an assignee id.",
|
|
3610
|
+
"An external input or human approval cannot exist only as free-text owner/next-step prose. Create a human-owned create_child_task with assigneeUserId and blockParentUntilDone=true (or another explicit interaction/dependency path), then keep the parent blocked or in_review until it resolves.",
|
|
3467
3611
|
"Do not copy placeholder names, placeholder summaries, placeholder action arrays, or placeholder paths. Every title, body, role, idempotencyKey, artifact path, and task description must be specific to the current issue and company.",
|
|
3468
3612
|
"Only include upload_artifact for a file that you actually created in the execution workspace and whose path exists relative to cwd. If no file was created, omit upload_artifact.",
|
|
3469
|
-
"
|
|
3613
|
+
"upload_artifact actions do not accept required. Artifact required belongs only to top-level deliverables entries, not action objects.",
|
|
3614
|
+
"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.",
|
|
3615
|
+
"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.",
|
|
3616
|
+
"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.",
|
|
3617
|
+
"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.",
|
|
3618
|
+
"For required deliverables, add top-level deliverables entries using {\"path\":\"relative/file\",\"required\":true}.",
|
|
3619
|
+
"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.",
|
|
3620
|
+
artifactVerifierCommands.length > 0
|
|
3621
|
+
? `Executable deliverable verification is enabled. Allowed verifier commands: ${artifactVerifierCommands.join(", ")}. For runnable outputs, add verification {\"command\":\"${artifactVerifierCommands[0]}\",\"args\":[\"script.js\"],\"timeoutSeconds\":30,\"expectedExitCode\":0,\"expectedOutputs\":[\"result.json\"]}. Commands are argv-only and run without a shell.`
|
|
3622
|
+
: "Executable deliverable verification is disabled by runtime policy. Do not include verification blocks. Required deliverables are still checked for presence and upload completeness.",
|
|
3470
3623
|
"Only include actions for the current PAPERCLIP_TASK_ID. Never include PAPERCLIP_API_KEY or any token in the JSON.",
|
|
3471
3624
|
].filter(Boolean).join("\n");
|
|
3472
3625
|
}
|
|
@@ -3751,14 +3904,25 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
|
|
|
3751
3904
|
const agentInstructions = renderAgentInstructionsBundle(asRecord(payload.agentInstructionsBundle));
|
|
3752
3905
|
const runtimeAuth = commandRuntimeAuth(command);
|
|
3753
3906
|
const hasRuntimeApi = Boolean(readString(runtimeAuth.apiUrl) && readString(runtimeAuth.apiKey));
|
|
3754
|
-
const
|
|
3907
|
+
const authorizationEnvelope = runtimeActionAuthorizationEnvelope(command);
|
|
3908
|
+
const canOrchestrateOrganization = canRuntimeAgentOrchestrateOrganization(context)
|
|
3909
|
+
&& authorizationEnvelope.allowedActionClasses.has("organization_mutation");
|
|
3910
|
+
const decompositionRequirement = commandLegacyTaskDecompositionRequirement(command);
|
|
3911
|
+
const explicitDecompositionRequired = decompositionRequirement.required
|
|
3912
|
+
&& runtimeActionChildIssueSummaryEntries(command).length === 0;
|
|
3755
3913
|
const executingAgent = asRecord(context.paperclipExecutingAgent);
|
|
3756
3914
|
const currentAgentAdapterType = readString(executingAgent.adapterType);
|
|
3915
|
+
const validatorContentTypes = runtimeAttachmentContentTypes(command);
|
|
3916
|
+
// Built-in executors run on the connector host, so the prompt may reference these host paths.
|
|
3917
|
+
// A custom wrapper may isolate model tools; until isolation is an explicit capability, this
|
|
3918
|
+
// 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(","))}`;
|
|
3757
3920
|
return [
|
|
3758
3921
|
"## AMaster Runtime Connector Task",
|
|
3759
3922
|
"",
|
|
3760
3923
|
"You are executing a task dispatched by AMaster Employee from the central control plane.",
|
|
3761
3924
|
"Work only inside the declared workspace. Make concrete progress and finish with a concise result summary.",
|
|
3925
|
+
"Do not install operating-system or user-global packages, and do not use host package managers such as brew, apt, yum, or global pip/npm installs. Use tools already available or workspace-local dependencies or virtual environments. If a required renderer or evaluator is unavailable, keep the source artifact, record the exact verification gap, and do not mutate the host.",
|
|
3762
3926
|
"",
|
|
3763
3927
|
`- command id: ${command.commandId}`,
|
|
3764
3928
|
`- run id: ${commandRunId(command) ?? "unknown"}`,
|
|
@@ -3766,13 +3930,22 @@ function buildCommandPrompt(command, workspace, materializedAttachments = [], op
|
|
|
3766
3930
|
issueLine ? `- issue: ${issueLine}` : "",
|
|
3767
3931
|
`- workspace: ${cwd}`,
|
|
3768
3932
|
workspaceContext.managed ? `- source workspace: ${workspaceContext.sourceWorkspacePath}` : "",
|
|
3769
|
-
workspaceContext.managed
|
|
3933
|
+
workspaceContext.managed
|
|
3934
|
+
? "Use the execution workspace as cwd. Never recursively scan the source workspace, the user's home directory, or any path outside the execution workspace. Only inspect a specific external source path when the task explicitly requires that exact path."
|
|
3935
|
+
: "",
|
|
3770
3936
|
readString(context.wakeReason) ? `- wake reason: ${readString(context.wakeReason)}` : "",
|
|
3771
3937
|
hasRuntimeApi
|
|
3772
3938
|
? "- AMaster API env: PAPERCLIP_API_URL, PAPERCLIP_API_KEY, AMASTER_BOARD_API_KEY, PAPERCLIP_RUN_ID, PAPERCLIP_TASK_ID, PAPERCLIP_AGENT_ID, PAPERCLIP_COMPANY_ID are available to update the task, create child tasks, and report progress. Treat API keys as secrets; never print them or include them in runtime actions."
|
|
3773
3939
|
: "",
|
|
3774
3940
|
"",
|
|
3775
|
-
hasRuntimeApi ? runtimeActionsInstruction({
|
|
3941
|
+
hasRuntimeApi ? runtimeActionsInstruction({
|
|
3942
|
+
canOrchestrateOrganization,
|
|
3943
|
+
currentAgentAdapterType,
|
|
3944
|
+
explicitDecompositionRequired,
|
|
3945
|
+
artifactVerifierCommands: options.artifactVerifierCommands,
|
|
3946
|
+
actionValidatorCommand,
|
|
3947
|
+
authorizationEnvelope,
|
|
3948
|
+
}) : "",
|
|
3776
3949
|
"",
|
|
3777
3950
|
agentInstructions,
|
|
3778
3951
|
"",
|
|
@@ -5183,9 +5356,10 @@ function extractRuntimeActionEnvelope(text) {
|
|
|
5183
5356
|
const structuredEnvelope = tryExtractStructuredRuntimeActionEnvelope(text);
|
|
5184
5357
|
if (structuredEnvelope) return structuredEnvelope;
|
|
5185
5358
|
|
|
5186
|
-
const
|
|
5187
|
-
if (
|
|
5188
|
-
const
|
|
5359
|
+
const markerMatch = /^[ \t]*AMASTER_RUNTIME_ACTIONS[ \t]*$/m.exec(text);
|
|
5360
|
+
if (!markerMatch) return null;
|
|
5361
|
+
const markerEnd = markerMatch.index + markerMatch[0].length;
|
|
5362
|
+
const jsonStart = text.indexOf("{", markerEnd);
|
|
5189
5363
|
if (jsonStart === -1) {
|
|
5190
5364
|
const err = new Error("AMASTER_RUNTIME_ACTIONS JSON could not be parsed: missing JSON object");
|
|
5191
5365
|
err.runtimeActionType = "parse";
|
|
@@ -5204,7 +5378,9 @@ function extractRuntimeActionEnvelope(text) {
|
|
|
5204
5378
|
|
|
5205
5379
|
function tryExtractStructuredRuntimeActionEnvelope(text) {
|
|
5206
5380
|
const source = String(text ?? "");
|
|
5207
|
-
|
|
5381
|
+
// Completion detection is permissive about fenced JSON, but the protocol
|
|
5382
|
+
// marker itself must still be the only content on its line.
|
|
5383
|
+
const markerRe = /(^|\r?\n)[ \t]*AMASTER_RUNTIME_ACTIONS[ \t]*(?:\r?\n|$)/g;
|
|
5208
5384
|
let match;
|
|
5209
5385
|
while ((match = markerRe.exec(source)) !== null) {
|
|
5210
5386
|
let cursor = markerRe.lastIndex;
|
|
@@ -5244,6 +5420,15 @@ function parseRuntimeActionEnvelopeJson(jsonText) {
|
|
|
5244
5420
|
}
|
|
5245
5421
|
}
|
|
5246
5422
|
|
|
5423
|
+
function hasMalformedRuntimeActionMarkerIntent(text) {
|
|
5424
|
+
const value = String(text ?? "");
|
|
5425
|
+
if (!value.includes("AMASTER_RUNTIME_ACTIONS")) return false;
|
|
5426
|
+
const hasNonCanonicalMarkerLine = value
|
|
5427
|
+
.split(/\r?\n/)
|
|
5428
|
+
.some((line) => line.includes("AMASTER_RUNTIME_ACTIONS") && line.trim() !== "AMASTER_RUNTIME_ACTIONS");
|
|
5429
|
+
return hasNonCanonicalMarkerLine && /["']actions["']\s*:/.test(value);
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5247
5432
|
function readBoundedActionString(record, key, maxChars) {
|
|
5248
5433
|
const value = readString(record[key]);
|
|
5249
5434
|
return value ? truncateText(value, maxChars) : null;
|
|
@@ -5607,6 +5792,97 @@ async function materializeIssueAttachments(config, command, workspace) {
|
|
|
5607
5792
|
return materialized;
|
|
5608
5793
|
}
|
|
5609
5794
|
|
|
5795
|
+
function safeArtifactInputSourceDir(entry, index) {
|
|
5796
|
+
const raw = readString(entry.sourceIssueIdentifier) ?? readString(entry.sourceIssueId) ?? `source-${index + 1}`;
|
|
5797
|
+
const safe = raw.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+|\.+$/g, "").slice(0, 120);
|
|
5798
|
+
return safe && safe !== "." && safe !== ".." ? safe : `source-${index + 1}`;
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
async function materializeRequiredArtifactInputs(config, command, workspace) {
|
|
5802
|
+
const runtimeAuth = commandRuntimeAuth(command);
|
|
5803
|
+
if (!readString(runtimeAuth.apiUrl) || !readString(runtimeAuth.apiKey)) return [];
|
|
5804
|
+
const context = asRecord(asRecord(command.payload).contextSnapshot);
|
|
5805
|
+
const issue = asRecord(context.paperclipIssue);
|
|
5806
|
+
const manifest = asRecord(issue.artifactInputManifest);
|
|
5807
|
+
if (Object.keys(manifest).length === 0) return [];
|
|
5808
|
+
if (manifest.version !== 1 || !Array.isArray(manifest.entries)) {
|
|
5809
|
+
throw new Error("artifact_input_manifest_invalid: expected version 1 entries array");
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
const targetRoot = join(workspace.cwd, "input-artifacts");
|
|
5813
|
+
rmSync(targetRoot, { recursive: true, force: true });
|
|
5814
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
5815
|
+
const usedPaths = new Set();
|
|
5816
|
+
const materialized = [];
|
|
5817
|
+
for (const [index, rawEntry] of manifest.entries.entries()) {
|
|
5818
|
+
const entry = asRecord(rawEntry);
|
|
5819
|
+
const workProductId = readString(entry.workProductId);
|
|
5820
|
+
const attachmentId = readString(entry.attachmentId);
|
|
5821
|
+
const sha256 = readString(entry.sha256);
|
|
5822
|
+
const contentPath = readString(entry.contentPath);
|
|
5823
|
+
const byteSize = readNumber(entry.byteSize, null);
|
|
5824
|
+
if (!workProductId || !attachmentId || !/^[a-f0-9]{64}$/i.test(sha256 ?? "") || !contentPath || byteSize === null) {
|
|
5825
|
+
throw new Error(`artifact_input_manifest_invalid: entry ${index} is incomplete`);
|
|
5826
|
+
}
|
|
5827
|
+
const expectedContentPath = `/api/attachments/${attachmentId}/content`;
|
|
5828
|
+
if (contentPath !== expectedContentPath) {
|
|
5829
|
+
throw new Error(`artifact_input_manifest_invalid: entry ${index} contentPath does not match attachmentId`);
|
|
5830
|
+
}
|
|
5831
|
+
const body = await runtimeApiBuffer(runtimeAuth, contentPath);
|
|
5832
|
+
const actualSha256 = createHash("sha256").update(body).digest("hex");
|
|
5833
|
+
if (body.byteLength !== byteSize || actualSha256 !== sha256) {
|
|
5834
|
+
throw new Error(
|
|
5835
|
+
`artifact_input_integrity_mismatch: workProductId=${workProductId} expectedBytes=${byteSize} actualBytes=${body.byteLength} expectedSha256=${sha256} actualSha256=${actualSha256}`,
|
|
5836
|
+
);
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
const sourceDir = safeArtifactInputSourceDir(entry, index);
|
|
5840
|
+
let filename = safeMaterializedAttachmentFilename({
|
|
5841
|
+
id: attachmentId,
|
|
5842
|
+
originalFilename: readString(entry.originalFilename),
|
|
5843
|
+
}, index);
|
|
5844
|
+
let relativePath = join("input-artifacts", sourceDir, filename).split(/[\\/]+/).join("/");
|
|
5845
|
+
if (usedPaths.has(relativePath)) {
|
|
5846
|
+
const ext = extname(filename);
|
|
5847
|
+
const stem = ext ? filename.slice(0, -ext.length) : filename;
|
|
5848
|
+
filename = `${stem}-${index + 1}${ext}`;
|
|
5849
|
+
relativePath = join("input-artifacts", sourceDir, filename).split(/[\\/]+/).join("/");
|
|
5850
|
+
}
|
|
5851
|
+
usedPaths.add(relativePath);
|
|
5852
|
+
const targetPath = join(workspace.cwd, relativePath);
|
|
5853
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
5854
|
+
writeFileSync(targetPath, body);
|
|
5855
|
+
chmodSync(targetPath, 0o444);
|
|
5856
|
+
materialized.push({
|
|
5857
|
+
id: attachmentId,
|
|
5858
|
+
workProductId,
|
|
5859
|
+
sourceIssueId: readString(entry.sourceIssueId),
|
|
5860
|
+
sourceIssueIdentifier: readString(entry.sourceIssueIdentifier),
|
|
5861
|
+
name: readString(entry.originalFilename) ?? filename,
|
|
5862
|
+
path: targetPath,
|
|
5863
|
+
relativePath,
|
|
5864
|
+
contentType: readString(entry.contentType),
|
|
5865
|
+
byteSize: body.byteLength,
|
|
5866
|
+
sha256,
|
|
5867
|
+
contentPath,
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
const localManifest = {
|
|
5872
|
+
version: 1,
|
|
5873
|
+
entries: materialized.map(({ path: _path, relativePath, ...entry }) => ({ ...entry, path: relativePath })),
|
|
5874
|
+
};
|
|
5875
|
+
const manifestPath = join(targetRoot, "artifact-input-manifest.json");
|
|
5876
|
+
writeFileSync(manifestPath, JSON.stringify(localManifest, null, 2) + "\n");
|
|
5877
|
+
chmodSync(manifestPath, 0o444);
|
|
5878
|
+
updateWorkspaceManifest(workspaceManifestPath(workspace), { artifactInputManifest: localManifest });
|
|
5879
|
+
await ingestLog(config, command, "system", "info", `Materialized ${materialized.length} required artifact input(s) into the execution workspace`, {
|
|
5880
|
+
artifactInputCount: materialized.length,
|
|
5881
|
+
artifactInputs: localManifest.entries,
|
|
5882
|
+
});
|
|
5883
|
+
return materialized;
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5610
5886
|
function issueCheckpointDir(workspace) {
|
|
5611
5887
|
return join(dirname(workspace.runDir), "checkpoint");
|
|
5612
5888
|
}
|
|
@@ -5906,7 +6182,14 @@ function runtimeChildActionFingerprint(action, issueId, parentIssueId) {
|
|
|
5906
6182
|
title,
|
|
5907
6183
|
status: readActionStatus(action.status) ?? null,
|
|
5908
6184
|
priority: readActionPriority(action.priority) ?? null,
|
|
6185
|
+
blockParentUntilDone: action.blockParentUntilDone === true || readActionStatus(action.status) === "blocked",
|
|
5909
6186
|
parentClientKey: readString(action.parentClientKey) ?? null,
|
|
6187
|
+
inputWorkProductIds: Array.isArray(action.inputWorkProductIds)
|
|
6188
|
+
? action.inputWorkProductIds.map(readString).filter(Boolean).sort()
|
|
6189
|
+
: [],
|
|
6190
|
+
blockedByClientKeys: Array.isArray(action.blockedByClientKeys)
|
|
6191
|
+
? action.blockedByClientKeys.map(readString).filter(Boolean).sort()
|
|
6192
|
+
: [],
|
|
5910
6193
|
})}`;
|
|
5911
6194
|
}
|
|
5912
6195
|
|
|
@@ -5924,6 +6207,9 @@ function runtimeAgentHireActionFingerprint(action, companyId, issueId, body) {
|
|
|
5924
6207
|
adapterType: readString(body.adapterType) ?? null,
|
|
5925
6208
|
sourceIssueId: readString(body.sourceIssueId) ?? null,
|
|
5926
6209
|
sourceIssueIds: runtimeActionIssueIdList(body.sourceIssueIds).sort(),
|
|
6210
|
+
reportsTo: readString(body.reportsTo) ?? null,
|
|
6211
|
+
reportsToAgentClientKey: readString(action.reportsToAgentClientKey) ?? null,
|
|
6212
|
+
topLevel: action.topLevel === true,
|
|
5927
6213
|
})}`;
|
|
5928
6214
|
}
|
|
5929
6215
|
|
|
@@ -5971,21 +6257,6 @@ function runtimeActionDoneNeedsExternalHuman(action, actionContext) {
|
|
|
5971
6257
|
].some((pattern) => pattern.test(normalized));
|
|
5972
6258
|
}
|
|
5973
6259
|
|
|
5974
|
-
function runtimeActionExternalHumanPatch(comment) {
|
|
5975
|
-
const owner = "人工负责人";
|
|
5976
|
-
const action = "补充或授权外部输入后再继续;涉及个人信息、付款、报名提交或审批时由人工处理";
|
|
5977
|
-
const guard = [
|
|
5978
|
-
"AMaster 已检测到本次运行仍依赖人工授权或外部输入,任务已转为 blocked / external_owner_action。本次运行不会自动发送飞书消息;请通过人工协作或已配置的飞书 CLI 通知对应负责人处理,收到明确授权后再继续。",
|
|
5979
|
-
"",
|
|
5980
|
-
`external owner: ${owner}`,
|
|
5981
|
-
`external action: ${action}`,
|
|
5982
|
-
].join("\n");
|
|
5983
|
-
return {
|
|
5984
|
-
status: "blocked",
|
|
5985
|
-
comment: comment ? `${comment}\n\n${guard}` : guard,
|
|
5986
|
-
};
|
|
5987
|
-
}
|
|
5988
|
-
|
|
5989
6260
|
function runtimeActionReviewConfirmation(action, runtimeAuth, command, actionContext, opts = {}) {
|
|
5990
6261
|
const runId = runtimeActionRunId(runtimeAuth) ?? commandRunId(command) ?? "unknown-run";
|
|
5991
6262
|
const comment = readBoundedActionString(action, "comment", 20_000)
|
|
@@ -6030,8 +6301,13 @@ function runtimeActionReviewConfirmation(action, runtimeAuth, command, actionCon
|
|
|
6030
6301
|
payload: {
|
|
6031
6302
|
version: 1,
|
|
6032
6303
|
prompt: copy.prompt,
|
|
6033
|
-
|
|
6034
|
-
|
|
6304
|
+
resolutionMode: "review",
|
|
6305
|
+
acceptLabel: "接受并继续",
|
|
6306
|
+
requestChangesLabel: "退回修改",
|
|
6307
|
+
requestChangesReasonLabel: "说明需要补充或修改的内容",
|
|
6308
|
+
rejectLabel: "拒绝并停止",
|
|
6309
|
+
rejectRequiresReason: true,
|
|
6310
|
+
rejectReasonLabel: "说明停止该任务的原因",
|
|
6035
6311
|
detailsMarkdown,
|
|
6036
6312
|
target: {
|
|
6037
6313
|
type: "custom",
|
|
@@ -6091,7 +6367,7 @@ function runtimeActionAcceptedConfirmationContinuation(command) {
|
|
|
6091
6367
|
if (context.acceptedConfirmationContinuation === true) return true;
|
|
6092
6368
|
if (readString(context.wakeReason) === "request_confirmation_accepted") return true;
|
|
6093
6369
|
if (
|
|
6094
|
-
readString(context.interactionKind)
|
|
6370
|
+
["request_confirmation", "request_checkbox_confirmation"].includes(readString(context.interactionKind) ?? "") &&
|
|
6095
6371
|
readString(context.interactionStatus) === "accepted"
|
|
6096
6372
|
) {
|
|
6097
6373
|
return true;
|
|
@@ -6100,7 +6376,7 @@ function runtimeActionAcceptedConfirmationContinuation(command) {
|
|
|
6100
6376
|
const interactions = Array.isArray(wake.interactions) ? wake.interactions : [];
|
|
6101
6377
|
return interactions.some((entry) => {
|
|
6102
6378
|
const interaction = asRecord(entry);
|
|
6103
|
-
return readString(interaction.kind)
|
|
6379
|
+
return ["request_confirmation", "request_checkbox_confirmation"].includes(readString(interaction.kind) ?? "") &&
|
|
6104
6380
|
readString(interaction.status) === "accepted";
|
|
6105
6381
|
});
|
|
6106
6382
|
}
|
|
@@ -6130,6 +6406,31 @@ function runtimeActionContinuationScope(command) {
|
|
|
6130
6406
|
return ["revision_only", "interaction_accepted", "message_only"].includes(scope ?? "") ? scope : null;
|
|
6131
6407
|
}
|
|
6132
6408
|
|
|
6409
|
+
function runtimeActionAuthorizationEnvelope(command) {
|
|
6410
|
+
const payload = asRecord(command.payload);
|
|
6411
|
+
const context = asRecord(payload.contextSnapshot);
|
|
6412
|
+
const rawEnvelope = asRecord(context.authorizationEnvelope);
|
|
6413
|
+
const allowedActionClasses = Array.isArray(rawEnvelope.allowedActionClasses)
|
|
6414
|
+
? rawEnvelope.allowedActionClasses.map(readString).filter(Boolean)
|
|
6415
|
+
: [];
|
|
6416
|
+
const effectiveAllowedActionClasses = allowedActionClasses.length > 0
|
|
6417
|
+
? allowedActionClasses
|
|
6418
|
+
: ["task_governance"];
|
|
6419
|
+
const rawAuthorizationOptionIds = asRecord(rawEnvelope.authorizationOptionIds);
|
|
6420
|
+
const authorizationOptionIds = new Map(
|
|
6421
|
+
["task_governance", "organization_mutation", "executable_child"]
|
|
6422
|
+
.map((actionClass) => [actionClass, readString(rawAuthorizationOptionIds[actionClass])])
|
|
6423
|
+
.filter(([, optionId]) => optionId),
|
|
6424
|
+
);
|
|
6425
|
+
return {
|
|
6426
|
+
allowedActionClasses: new Set(effectiveAllowedActionClasses),
|
|
6427
|
+
authorizationOptionIds,
|
|
6428
|
+
sourceKind: readString(rawEnvelope.sourceKind) ?? "implicit_default",
|
|
6429
|
+
sourceId: readString(rawEnvelope.sourceId) ?? commandRunId(command),
|
|
6430
|
+
sourceRevision: readString(rawEnvelope.sourceRevision) ?? "missing_envelope",
|
|
6431
|
+
};
|
|
6432
|
+
}
|
|
6433
|
+
|
|
6133
6434
|
function runtimeActionInReviewNeedsFollowUpText(text) {
|
|
6134
6435
|
const normalized = String(text ?? "")
|
|
6135
6436
|
.replace(/\s+/g, "")
|
|
@@ -6292,6 +6593,35 @@ function runtimeActionParentIssueId(action, actionContext, fallbackIssueId) {
|
|
|
6292
6593
|
return parentIssueId;
|
|
6293
6594
|
}
|
|
6294
6595
|
|
|
6596
|
+
function runtimeActionReportsToAgentId(action, actionContext) {
|
|
6597
|
+
const reportsToAgentClientKey = readString(action.reportsToAgentClientKey);
|
|
6598
|
+
if (!reportsToAgentClientKey) return readString(action.reportsTo);
|
|
6599
|
+
const reportsTo = actionContext.agentIdByClientKey.get(reportsToAgentClientKey);
|
|
6600
|
+
if (!reportsTo) {
|
|
6601
|
+
throw new Error(`agent_hire action references unknown reportsToAgentClientKey: ${reportsToAgentClientKey}`);
|
|
6602
|
+
}
|
|
6603
|
+
return reportsTo;
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6606
|
+
function runtimeActionBlockedByIssueIds(action, actionContext) {
|
|
6607
|
+
const explicitIssueIds = Array.isArray(action.blockedByIssueIds)
|
|
6608
|
+
? action.blockedByIssueIds.map((rawIssueId) => readString(rawIssueId)).filter(Boolean)
|
|
6609
|
+
: [];
|
|
6610
|
+
if (action.blockedByClientKeys === undefined) return [...new Set(explicitIssueIds)];
|
|
6611
|
+
const clientKeyIssueIds = action.blockedByClientKeys.map((rawClientKey, index) => {
|
|
6612
|
+
const clientKey = readString(rawClientKey);
|
|
6613
|
+
if (!clientKey) {
|
|
6614
|
+
throw runtimeActionValidationError(`create_child_task blockedByClientKeys[${index}] must be a non-empty string`);
|
|
6615
|
+
}
|
|
6616
|
+
const blockerIssueId = actionContext.issueIdByClientKey.get(clientKey);
|
|
6617
|
+
if (!blockerIssueId) {
|
|
6618
|
+
throw new Error(`create_child_task action references unknown blockedByClientKeys entry: ${clientKey}`);
|
|
6619
|
+
}
|
|
6620
|
+
return blockerIssueId;
|
|
6621
|
+
});
|
|
6622
|
+
return [...new Set([...explicitIssueIds, ...clientKeyIssueIds])];
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6295
6625
|
const RUNTIME_EXECUTOR_ADAPTER_TYPE_ALIASES = new Map([
|
|
6296
6626
|
["pi", "pi_local"],
|
|
6297
6627
|
["pi_local", "pi_local"],
|
|
@@ -6513,9 +6843,14 @@ function normalizeRequestConfirmationPayload(payload, action) {
|
|
|
6513
6843
|
return {
|
|
6514
6844
|
version: normalized.version === 1 ? normalized.version : 1,
|
|
6515
6845
|
prompt,
|
|
6846
|
+
...(readString(normalized.resolutionMode) === "review" ? { resolutionMode: "review" } : {}),
|
|
6516
6847
|
...(readString(normalized.acceptLabel) ?? readString(action.acceptLabel)
|
|
6517
6848
|
? { acceptLabel: readString(normalized.acceptLabel) ?? readString(action.acceptLabel) }
|
|
6518
6849
|
: {}),
|
|
6850
|
+
...(readString(normalized.requestChangesLabel) ? { requestChangesLabel: readString(normalized.requestChangesLabel) } : {}),
|
|
6851
|
+
...(readString(normalized.requestChangesReasonLabel)
|
|
6852
|
+
? { requestChangesReasonLabel: readString(normalized.requestChangesReasonLabel) }
|
|
6853
|
+
: {}),
|
|
6519
6854
|
...(readString(normalized.rejectLabel) ?? readString(action.rejectLabel)
|
|
6520
6855
|
? { rejectLabel: readString(normalized.rejectLabel) ?? readString(action.rejectLabel) }
|
|
6521
6856
|
: {}),
|
|
@@ -6734,6 +7069,245 @@ function assertRuntimeActionValid(condition, message) {
|
|
|
6734
7069
|
if (!condition) throw runtimeActionValidationError(message);
|
|
6735
7070
|
}
|
|
6736
7071
|
|
|
7072
|
+
const RUNTIME_ACTION_COMMON_FIELDS = new Set(["type", "action", "name", "payload"]);
|
|
7073
|
+
const RUNTIME_ACTION_ALLOWED_FIELDS = new Map([
|
|
7074
|
+
["create_child_task", new Set([
|
|
7075
|
+
"title", "description", "status", "priority", "assigneeAgentId", "assigneeUserId",
|
|
7076
|
+
"assigneeAgentClientKey", "agentClientKey", "blockParentUntilDone", "executeBeforeConfirmation",
|
|
7077
|
+
"clientKey", "idempotencyKey", "parentClientKey", "blockedByIssueIds", "blockedByClientKeys", "create_child_task",
|
|
7078
|
+
"inputWorkProductIds",
|
|
7079
|
+
])],
|
|
7080
|
+
["agent_hire", new Set([
|
|
7081
|
+
"clientKey", "idempotencyKey", "key", "companyId", "role", "title", "icon", "reportsTo",
|
|
7082
|
+
"reportsToAgentClientKey", "topLevel", "capabilities", "desiredSkills", "adapterType", "adapterConfig",
|
|
7083
|
+
"instructionsBundle", "runtimeConfig", "defaultEnvironmentId", "budgetMonthlyCents", "permissions",
|
|
7084
|
+
"metadata", "sourceIssueId", "sourceIssueIds", "executeBeforeConfirmation", "agent_hire", "agent_hire/create_agent",
|
|
7085
|
+
])],
|
|
7086
|
+
["create_agent", new Set([
|
|
7087
|
+
"clientKey", "idempotencyKey", "key", "companyId", "role", "title", "icon", "reportsTo",
|
|
7088
|
+
"reportsToAgentClientKey", "topLevel", "capabilities", "desiredSkills", "adapterType", "adapterConfig",
|
|
7089
|
+
"instructionsBundle", "runtimeConfig", "defaultEnvironmentId", "budgetMonthlyCents", "permissions",
|
|
7090
|
+
"metadata", "sourceIssueId", "sourceIssueIds", "executeBeforeConfirmation", "create_agent",
|
|
7091
|
+
])],
|
|
7092
|
+
["add_comment", new Set(["body", "content", "text", "message", "add_comment"])],
|
|
7093
|
+
["upload_artifact", new Set([
|
|
7094
|
+
"path", "filePath", "artifactPath", "artifact_path", "title", "summary", "contentType",
|
|
7095
|
+
"createWorkProduct", "provider", "status", "reviewState", "isPrimary", "healthStatus",
|
|
7096
|
+
"sourceWorkProductId", "sourceWorkProductIds", "supersedesWorkProductId", "evidenceRole", "scenarioKeys",
|
|
7097
|
+
"idempotencyKey", "upload_artifact",
|
|
7098
|
+
])],
|
|
7099
|
+
["upsert_document", new Set([
|
|
7100
|
+
"key", "title", "body", "format", "changeSummary", "baseRevisionId", "upsert_document",
|
|
7101
|
+
])],
|
|
7102
|
+
["create_interaction", new Set([
|
|
7103
|
+
"kind", "continuationPolicy", "title", "summary", "idempotencyKey", "interaction", "version",
|
|
7104
|
+
"prompt", "question", "body", "content", "text", "message", "description", "acceptLabel",
|
|
7105
|
+
"requestChangesLabel", "requestChangesReasonLabel", "rejectLabel", "rejectRequiresReason",
|
|
7106
|
+
"rejectReasonLabel", "allowDeclineReason", "declineReasonPlaceholder", "detailsMarkdown",
|
|
7107
|
+
"supersedeOnUserComment", "target", "artifactRefs", "tasks", "options", "defaultSelectedOptionIds",
|
|
7108
|
+
"minSelected", "maxSelected", "questions", "submitLabel", "selectionMode", "required", "helpText",
|
|
7109
|
+
"create_interaction",
|
|
7110
|
+
])],
|
|
7111
|
+
["update_parent", new Set(["status", "comment", "force", "update_parent"])],
|
|
7112
|
+
["update_company_profile", new Set([
|
|
7113
|
+
"companyId", "tagline", "stage", "targetCustomers", "products", "currentPriority", "businessModel",
|
|
7114
|
+
"regions", "brandTone", "markAutoDerived", "idempotencyKey", "update_company_profile",
|
|
7115
|
+
])],
|
|
7116
|
+
["create_routine", new Set([
|
|
7117
|
+
"companyId", "title", "description", "cronExpression", "priority", "concurrencyPolicy", "catchUpPolicy",
|
|
7118
|
+
"assigneeAgentId", "timezone", "idempotencyKey", "create_routine",
|
|
7119
|
+
])],
|
|
7120
|
+
]);
|
|
7121
|
+
|
|
7122
|
+
function runtimeActionReceivedType(value) {
|
|
7123
|
+
if (value === null) return "null";
|
|
7124
|
+
if (Array.isArray(value)) return "array";
|
|
7125
|
+
return typeof value;
|
|
7126
|
+
}
|
|
7127
|
+
|
|
7128
|
+
function assertNoUnsupportedRuntimeActionFields(action, actionIndex) {
|
|
7129
|
+
const type = readRuntimeActionType(action);
|
|
7130
|
+
const allowed = RUNTIME_ACTION_ALLOWED_FIELDS.get(type);
|
|
7131
|
+
assertRuntimeActionValid(allowed, `unsupported AMaster runtime action type: ${type ?? "missing"}`);
|
|
7132
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7133
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
7134
|
+
if (type === "create_interaction" && field === "interactionType") continue;
|
|
7135
|
+
if (RUNTIME_ACTION_COMMON_FIELDS.has(field) || allowed.has(field)) continue;
|
|
7136
|
+
const fieldPath = `actions[${actionIndex}].${field}`;
|
|
7137
|
+
const receivedType = runtimeActionReceivedType(value);
|
|
7138
|
+
const managerFieldHint = ["agent_hire", "create_agent"].includes(type ?? "") && field === "reportsToAgentId"
|
|
7139
|
+
? "; reportsToAgentId is unsupported; use reportsTo for an existing manager UUID or reportsToAgentClientKey for a manager hired in the same batch"
|
|
7140
|
+
: "";
|
|
7141
|
+
const err = runtimeActionValidationError(
|
|
7142
|
+
`${type} action has unsupported field ${field} at ${fieldPath}; received ${receivedType}${managerFieldHint}`,
|
|
7143
|
+
);
|
|
7144
|
+
err.runtimeActionFieldPath = fieldPath;
|
|
7145
|
+
err.runtimeActionReceivedType = receivedType;
|
|
7146
|
+
throw err;
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
const RUNTIME_ACTION_BOOLEAN_FIELDS = new Set([
|
|
7151
|
+
"blockParentUntilDone", "executeBeforeConfirmation", "createWorkProduct", "isPrimary",
|
|
7152
|
+
"force", "markAutoDerived", "rejectRequiresReason", "allowDeclineReason", "supersedeOnUserComment",
|
|
7153
|
+
"required", "topLevel",
|
|
7154
|
+
]);
|
|
7155
|
+
const RUNTIME_ACTION_NUMBER_FIELDS = new Set(["budgetMonthlyCents", "minSelected", "maxSelected", "version"]);
|
|
7156
|
+
const RUNTIME_ACTION_ARRAY_FIELDS = new Set([
|
|
7157
|
+
"blockedByIssueIds", "blockedByClientKeys", "desiredSkills", "sourceIssueIds", "artifactRefs", "tasks",
|
|
7158
|
+
"options", "defaultSelectedOptionIds", "questions", "sourceWorkProductIds", "scenarioKeys",
|
|
7159
|
+
"inputWorkProductIds",
|
|
7160
|
+
]);
|
|
7161
|
+
const RUNTIME_ACTION_OBJECT_FIELDS = new Set([
|
|
7162
|
+
"adapterConfig", "instructionsBundle", "runtimeConfig", "permissions", "metadata", "target",
|
|
7163
|
+
"interaction",
|
|
7164
|
+
]);
|
|
7165
|
+
|
|
7166
|
+
function assertRuntimeActionFieldTypes(action, actionIndex) {
|
|
7167
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7168
|
+
const type = readRuntimeActionType(fields) ?? "unknown";
|
|
7169
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
7170
|
+
if (value === undefined) continue;
|
|
7171
|
+
if (SINGLE_KEY_RUNTIME_ACTION_TYPES.has(field)) continue;
|
|
7172
|
+
if (type === "create_interaction" && field === "payload") continue;
|
|
7173
|
+
let expectedType = null;
|
|
7174
|
+
let valid = true;
|
|
7175
|
+
if (RUNTIME_ACTION_BOOLEAN_FIELDS.has(field)) {
|
|
7176
|
+
expectedType = "boolean";
|
|
7177
|
+
valid = typeof value === "boolean";
|
|
7178
|
+
} else if (RUNTIME_ACTION_NUMBER_FIELDS.has(field)) {
|
|
7179
|
+
expectedType = "number";
|
|
7180
|
+
valid = typeof value === "number" && Number.isFinite(value);
|
|
7181
|
+
} else if (RUNTIME_ACTION_ARRAY_FIELDS.has(field)) {
|
|
7182
|
+
expectedType = "array";
|
|
7183
|
+
valid = Array.isArray(value);
|
|
7184
|
+
} else if (field === "capabilities") {
|
|
7185
|
+
expectedType = "string or array";
|
|
7186
|
+
valid = typeof value === "string" || Array.isArray(value);
|
|
7187
|
+
} else if (RUNTIME_ACTION_OBJECT_FIELDS.has(field) || field === "payload") {
|
|
7188
|
+
expectedType = "object";
|
|
7189
|
+
valid = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7190
|
+
} else if (!RUNTIME_ACTION_COMMON_FIELDS.has(field) && value !== null) {
|
|
7191
|
+
expectedType = "string";
|
|
7192
|
+
valid = typeof value === "string";
|
|
7193
|
+
}
|
|
7194
|
+
if (valid || !expectedType) continue;
|
|
7195
|
+
const fieldPath = `actions[${actionIndex}].${field}`;
|
|
7196
|
+
const err = runtimeActionValidationError(
|
|
7197
|
+
`${type} action field ${fieldPath} must be ${expectedType}; received ${runtimeActionReceivedType(value)}`,
|
|
7198
|
+
);
|
|
7199
|
+
err.runtimeActionFieldPath = fieldPath;
|
|
7200
|
+
err.runtimeActionReceivedType = runtimeActionReceivedType(value);
|
|
7201
|
+
throw err;
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
function runtimeActionClientKeyProducerKind(type) {
|
|
7206
|
+
if (["agent_hire", "create_agent"].includes(type ?? "")) return "agent";
|
|
7207
|
+
if (type === "create_child_task") return "issue";
|
|
7208
|
+
return null;
|
|
7209
|
+
}
|
|
7210
|
+
|
|
7211
|
+
function runtimeActionClientKeyReferences(fields) {
|
|
7212
|
+
const type = readRuntimeActionType(fields);
|
|
7213
|
+
const references = [];
|
|
7214
|
+
if (["agent_hire", "create_agent"].includes(type ?? "")) {
|
|
7215
|
+
const clientKey = readString(fields.reportsToAgentClientKey);
|
|
7216
|
+
if (clientKey) references.push({ clientKey, expectedKind: "agent", field: "reportsToAgentClientKey" });
|
|
7217
|
+
}
|
|
7218
|
+
if (type === "create_child_task") {
|
|
7219
|
+
const assigneeClientKey = readString(fields.assigneeAgentClientKey) ?? readString(fields.agentClientKey);
|
|
7220
|
+
if (assigneeClientKey) references.push({ clientKey: assigneeClientKey, expectedKind: "agent", field: "assigneeAgentClientKey" });
|
|
7221
|
+
const parentClientKey = readString(fields.parentClientKey);
|
|
7222
|
+
if (parentClientKey) references.push({ clientKey: parentClientKey, expectedKind: "issue", field: "parentClientKey" });
|
|
7223
|
+
if (fields.blockedByClientKeys !== undefined) {
|
|
7224
|
+
assertRuntimeActionValid(Array.isArray(fields.blockedByClientKeys), "create_child_task blockedByClientKeys must be an array");
|
|
7225
|
+
for (const [index, value] of fields.blockedByClientKeys.entries()) {
|
|
7226
|
+
const clientKey = readString(value);
|
|
7227
|
+
assertRuntimeActionValid(clientKey, `create_child_task blockedByClientKeys[${index}] must be a non-empty string`);
|
|
7228
|
+
references.push({ clientKey, expectedKind: "issue", field: `blockedByClientKeys[${index}]` });
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
return references;
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7235
|
+
function throwRuntimeActionGraphValidation(message, action, actionIndex, fieldPath) {
|
|
7236
|
+
const err = runtimeActionValidationError(message);
|
|
7237
|
+
err.runtimeActionType = readRuntimeActionType(action) ?? "unknown";
|
|
7238
|
+
err.runtimeActionOriginalType = readRuntimeActionOriginalType(action);
|
|
7239
|
+
err.runtimeActionFieldPath = fieldPath ?? `actions[${actionIndex}]`;
|
|
7240
|
+
throw err;
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
function orderRuntimeActionsByClientKeyDependencies(actions) {
|
|
7244
|
+
const producers = new Map();
|
|
7245
|
+
for (const [index, action] of actions.entries()) {
|
|
7246
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7247
|
+
const type = readRuntimeActionType(fields);
|
|
7248
|
+
const kind = runtimeActionClientKeyProducerKind(type);
|
|
7249
|
+
const clientKey = readString(fields.clientKey);
|
|
7250
|
+
if (!clientKey || !kind) continue;
|
|
7251
|
+
if (producers.has(clientKey)) {
|
|
7252
|
+
throwRuntimeActionGraphValidation(
|
|
7253
|
+
`runtime action clientKey must be unique: ${clientKey}`,
|
|
7254
|
+
action,
|
|
7255
|
+
index,
|
|
7256
|
+
`actions[${index}].clientKey`,
|
|
7257
|
+
);
|
|
7258
|
+
}
|
|
7259
|
+
producers.set(clientKey, { index, kind, type });
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
const dependenciesByIndex = actions.map(() => new Set());
|
|
7263
|
+
const dependentsByIndex = actions.map(() => new Set());
|
|
7264
|
+
for (const [index, action] of actions.entries()) {
|
|
7265
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7266
|
+
for (const reference of runtimeActionClientKeyReferences(fields)) {
|
|
7267
|
+
const producer = producers.get(reference.clientKey);
|
|
7268
|
+
if (!producer) {
|
|
7269
|
+
throwRuntimeActionGraphValidation(
|
|
7270
|
+
`${readRuntimeActionType(fields)} action references unknown ${reference.field}: ${reference.clientKey}`,
|
|
7271
|
+
action,
|
|
7272
|
+
index,
|
|
7273
|
+
`actions[${index}].${reference.field}`,
|
|
7274
|
+
);
|
|
7275
|
+
}
|
|
7276
|
+
if (producer.kind !== reference.expectedKind) {
|
|
7277
|
+
throwRuntimeActionGraphValidation(
|
|
7278
|
+
`${readRuntimeActionType(fields)} action ${reference.field} must reference a ${reference.expectedKind} clientKey: ${reference.clientKey}`,
|
|
7279
|
+
action,
|
|
7280
|
+
index,
|
|
7281
|
+
`actions[${index}].${reference.field}`,
|
|
7282
|
+
);
|
|
7283
|
+
}
|
|
7284
|
+
dependenciesByIndex[index].add(producer.index);
|
|
7285
|
+
dependentsByIndex[producer.index].add(index);
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
|
|
7289
|
+
const ready = actions.map((_, index) => index).filter((index) => dependenciesByIndex[index].size === 0);
|
|
7290
|
+
const orderedIndexes = [];
|
|
7291
|
+
while (ready.length > 0) {
|
|
7292
|
+
ready.sort((left, right) => left - right);
|
|
7293
|
+
const index = ready.shift();
|
|
7294
|
+
orderedIndexes.push(index);
|
|
7295
|
+
for (const dependentIndex of dependentsByIndex[index]) {
|
|
7296
|
+
dependenciesByIndex[dependentIndex].delete(index);
|
|
7297
|
+
if (dependenciesByIndex[dependentIndex].size === 0) ready.push(dependentIndex);
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
if (orderedIndexes.length !== actions.length) {
|
|
7301
|
+
const cycleIndex = dependenciesByIndex.findIndex((dependencies) => dependencies.size > 0);
|
|
7302
|
+
throwRuntimeActionGraphValidation(
|
|
7303
|
+
"runtime action clientKey dependency graph contains a cycle",
|
|
7304
|
+
actions[cycleIndex],
|
|
7305
|
+
cycleIndex,
|
|
7306
|
+
);
|
|
7307
|
+
}
|
|
7308
|
+
return orderedIndexes.map((index) => actions[index]);
|
|
7309
|
+
}
|
|
7310
|
+
|
|
6737
7311
|
function assertRuntimeInteractionOptions(options, path) {
|
|
6738
7312
|
assertRuntimeActionValid(Array.isArray(options) && options.length > 0 && options.length <= 10, `${path} requires 1-10 options`);
|
|
6739
7313
|
const ids = new Set();
|
|
@@ -6811,6 +7385,16 @@ function willRuntimeActionCreateRootBlockingChild(action) {
|
|
|
6811
7385
|
return fields.blockParentUntilDone === true || childStatus === "blocked";
|
|
6812
7386
|
}
|
|
6813
7387
|
|
|
7388
|
+
function willRuntimeActionCreateHumanOwnedInputPath(action) {
|
|
7389
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7390
|
+
if (readRuntimeActionType(fields) !== "create_child_task") return false;
|
|
7391
|
+
const childStatus = readActionStatus(fields.status) ?? "todo";
|
|
7392
|
+
return Boolean(
|
|
7393
|
+
readString(fields.assigneeUserId) &&
|
|
7394
|
+
(fields.blockParentUntilDone === true || childStatus === "blocked"),
|
|
7395
|
+
);
|
|
7396
|
+
}
|
|
7397
|
+
|
|
6814
7398
|
function willRuntimeActionCreatePendingInteraction(action) {
|
|
6815
7399
|
const fields = runtimeActionBusinessFields(action);
|
|
6816
7400
|
if (readRuntimeActionType(fields) !== "create_interaction") return false;
|
|
@@ -6826,8 +7410,22 @@ function runtimeActionIsAmbiguousApprovalGatedChild(action) {
|
|
|
6826
7410
|
return childStatus !== "backlog";
|
|
6827
7411
|
}
|
|
6828
7412
|
|
|
6829
|
-
function
|
|
7413
|
+
function assertNoAmbiguousApprovalGatedSideEffects(actions) {
|
|
6830
7414
|
if (!actions.some(willRuntimeActionCreatePendingInteraction)) return;
|
|
7415
|
+
const ambiguousHire = actions.find((action) => {
|
|
7416
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7417
|
+
return ["agent_hire", "create_agent"].includes(readRuntimeActionType(fields) ?? "") &&
|
|
7418
|
+
fields.executeBeforeConfirmation !== true;
|
|
7419
|
+
});
|
|
7420
|
+
if (ambiguousHire) {
|
|
7421
|
+
const error = runtimeActionValidationError(
|
|
7422
|
+
"approval-gated agent hire actions require the native hire approval only, a separate proposal interaction, or executeBeforeConfirmation true",
|
|
7423
|
+
);
|
|
7424
|
+
error.runtimeActionType = readRuntimeActionType(ambiguousHire) ?? "agent_hire";
|
|
7425
|
+
error.runtimeActionOriginalType = readRuntimeActionOriginalType(ambiguousHire);
|
|
7426
|
+
error.runtimeActionReason = error.message;
|
|
7427
|
+
throw error;
|
|
7428
|
+
}
|
|
6831
7429
|
const ambiguousChild = actions.find(runtimeActionIsAmbiguousApprovalGatedChild);
|
|
6832
7430
|
if (!ambiguousChild) return;
|
|
6833
7431
|
const error = new Error(
|
|
@@ -6839,6 +7437,50 @@ function assertNoAmbiguousApprovalGatedChildren(actions) {
|
|
|
6839
7437
|
throw error;
|
|
6840
7438
|
}
|
|
6841
7439
|
|
|
7440
|
+
function assertDelegatedParentWaitDisposition(actions) {
|
|
7441
|
+
const closesIndependentParentScope = actions.some((action) => {
|
|
7442
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7443
|
+
if (readRuntimeActionType(fields) !== "update_parent") return false;
|
|
7444
|
+
return ["done", "cancelled"].includes(readActionStatus(fields.status) ?? "");
|
|
7445
|
+
});
|
|
7446
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
7447
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7448
|
+
if (readRuntimeActionType(fields) !== "create_child_task") continue;
|
|
7449
|
+
if (readString(fields.parentClientKey)) continue;
|
|
7450
|
+
const status = readActionStatus(fields.status) ?? "todo";
|
|
7451
|
+
if (!["todo", "in_progress"].includes(status)) continue;
|
|
7452
|
+
if (fields.blockParentUntilDone === true || closesIndependentParentScope) continue;
|
|
7453
|
+
const err = runtimeActionValidationError(
|
|
7454
|
+
"delegated_parent_wait_required: root executable child tasks require blockParentUntilDone true, or the same batch must close the parent's independent scope with update_parent done/cancelled",
|
|
7455
|
+
);
|
|
7456
|
+
err.runtimeActionType = "create_child_task";
|
|
7457
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}].blockParentUntilDone`;
|
|
7458
|
+
err.runtimeActionReason = err.message;
|
|
7459
|
+
throw err;
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
|
|
7463
|
+
function assertExplicitTaskDecomposition(command, actions) {
|
|
7464
|
+
const requirement = commandLegacyTaskDecompositionRequirement(command);
|
|
7465
|
+
if (!requirement.required) return;
|
|
7466
|
+
if (runtimeActionChildIssueSummaryEntries(command).length > 0) return;
|
|
7467
|
+
const hasDecompositionAction = actions.some((action) => {
|
|
7468
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7469
|
+
const type = readRuntimeActionType(fields);
|
|
7470
|
+
return type === "create_child_task"
|
|
7471
|
+
|| (type === "create_interaction" && inferCreateInteractionKind(fields) === "suggest_tasks");
|
|
7472
|
+
});
|
|
7473
|
+
if (hasDecompositionAction) return;
|
|
7474
|
+
const error = new Error(
|
|
7475
|
+
"task explicitly requires child task decomposition under the legacy text hard gate; emit create_child_task or suggest_tasks instead of only describing a checklist",
|
|
7476
|
+
);
|
|
7477
|
+
error.runtimeActionType = "decomposition";
|
|
7478
|
+
error.runtimeActionReason = error.message;
|
|
7479
|
+
error.runtimeActionDecompositionRequirementSource = requirement.source;
|
|
7480
|
+
error.runtimeActionDecompositionMatch = requirement.match;
|
|
7481
|
+
throw error;
|
|
7482
|
+
}
|
|
7483
|
+
|
|
6842
7484
|
function assertRuntimeActionsAllowedByContinuationScope(actions, scope) {
|
|
6843
7485
|
if (scope === "message_only" && actions.length > 0) {
|
|
6844
7486
|
const err = new Error("message_only continuation forbids runtime action mutations");
|
|
@@ -6861,9 +7503,64 @@ function assertRuntimeActionsAllowedByContinuationScope(actions, scope) {
|
|
|
6861
7503
|
}
|
|
6862
7504
|
}
|
|
6863
7505
|
|
|
6864
|
-
function preflightRuntimeAction(action, preflightContext) {
|
|
7506
|
+
function preflightRuntimeAction(action, preflightContext, actionIndex) {
|
|
6865
7507
|
const type = readRuntimeActionType(action);
|
|
6866
7508
|
const fields = runtimeActionBusinessFields(action);
|
|
7509
|
+
assertNoUnsupportedRuntimeActionFields(action, actionIndex);
|
|
7510
|
+
assertRuntimeActionFieldTypes(action, actionIndex);
|
|
7511
|
+
if (type === "create_child_task") {
|
|
7512
|
+
assertNoPlaceholderRuntimeActionText(fields, ["title", "description"]);
|
|
7513
|
+
assertRuntimeActionValid(readBoundedActionString(fields, "title", 240), "create_child_task action requires title");
|
|
7514
|
+
if (fields.status !== undefined) {
|
|
7515
|
+
assertRuntimeActionValid(readActionStatus(fields.status), `create_child_task status is unsupported: ${String(fields.status)}`);
|
|
7516
|
+
}
|
|
7517
|
+
if (fields.priority !== undefined) {
|
|
7518
|
+
assertRuntimeActionValid(readActionPriority(fields.priority), `create_child_task priority is unsupported: ${String(fields.priority)}`);
|
|
7519
|
+
}
|
|
7520
|
+
assertRuntimeActionValid(
|
|
7521
|
+
!readString(fields.assigneeAgentId) || (!readString(fields.assigneeAgentClientKey) && !readString(fields.agentClientKey)),
|
|
7522
|
+
"create_child_task action cannot combine assigneeAgentId with assigneeAgentClientKey/agentClientKey",
|
|
7523
|
+
);
|
|
7524
|
+
if (fields.blockedByIssueIds !== undefined) {
|
|
7525
|
+
for (const [index, rawIssueId] of fields.blockedByIssueIds.entries()) {
|
|
7526
|
+
const issueId = readString(rawIssueId);
|
|
7527
|
+
assertRuntimeActionValid(
|
|
7528
|
+
issueId && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(issueId),
|
|
7529
|
+
`create_child_task blockedByIssueIds[${index}] must be a UUID`,
|
|
7530
|
+
);
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
if (fields.inputWorkProductIds !== undefined) {
|
|
7534
|
+
const seen = new Set();
|
|
7535
|
+
for (const [index, rawWorkProductId] of fields.inputWorkProductIds.entries()) {
|
|
7536
|
+
const workProductId = readString(rawWorkProductId);
|
|
7537
|
+
assertRuntimeActionValid(
|
|
7538
|
+
workProductId && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(workProductId),
|
|
7539
|
+
`create_child_task inputWorkProductIds[${index}] must be a UUID`,
|
|
7540
|
+
);
|
|
7541
|
+
assertRuntimeActionValid(
|
|
7542
|
+
!seen.has(workProductId),
|
|
7543
|
+
`create_child_task inputWorkProductIds[${index}] duplicates ${workProductId}`,
|
|
7544
|
+
);
|
|
7545
|
+
seen.add(workProductId);
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
return;
|
|
7549
|
+
}
|
|
7550
|
+
if (["agent_hire", "create_agent"].includes(type ?? "")) {
|
|
7551
|
+
assertNoPlaceholderRuntimeActionText(fields, ["name", "role", "title", "instructionsBundle"]);
|
|
7552
|
+
assertRuntimeActionValid(readBoundedActionString(fields, "name", 240), `${type} action requires name`);
|
|
7553
|
+
const reportingDispositionCount = [
|
|
7554
|
+
Boolean(readString(fields.reportsTo)),
|
|
7555
|
+
Boolean(readString(fields.reportsToAgentClientKey)),
|
|
7556
|
+
fields.topLevel === true,
|
|
7557
|
+
].filter(Boolean).length;
|
|
7558
|
+
assertRuntimeActionValid(
|
|
7559
|
+
reportingDispositionCount === 1,
|
|
7560
|
+
`${type} action requires exactly one of reportsTo, reportsToAgentClientKey, or topLevel true`,
|
|
7561
|
+
);
|
|
7562
|
+
return;
|
|
7563
|
+
}
|
|
6867
7564
|
if (type === "add_comment") {
|
|
6868
7565
|
assertNoPlaceholderRuntimeActionText(fields, ["body", "content", "text", "message"]);
|
|
6869
7566
|
const body = readBoundedActionString(fields, "body", 20_000)
|
|
@@ -6874,7 +7571,52 @@ function preflightRuntimeAction(action, preflightContext) {
|
|
|
6874
7571
|
return;
|
|
6875
7572
|
}
|
|
6876
7573
|
if (type === "upload_artifact") {
|
|
7574
|
+
assertNoPlaceholderRuntimeActionText(fields, ["path", "filePath", "artifactPath", "artifact_path", "title", "summary"]);
|
|
7575
|
+
assertRuntimeActionValid(readRuntimeArtifactPath(fields), "upload_artifact action requires path");
|
|
7576
|
+
const pathFields = ["path", "filePath", "artifactPath", "artifact_path"].filter((field) => readString(fields[field]));
|
|
7577
|
+
assertRuntimeActionValid(pathFields.length === 1, "upload_artifact action requires exactly one path field");
|
|
7578
|
+
const sourceWorkProductId = readString(fields.sourceWorkProductId);
|
|
7579
|
+
const sourceWorkProductIds = Array.isArray(fields.sourceWorkProductIds)
|
|
7580
|
+
? fields.sourceWorkProductIds.map(readString).filter(Boolean)
|
|
7581
|
+
: [];
|
|
7582
|
+
const supersedesWorkProductId = readString(fields.supersedesWorkProductId);
|
|
7583
|
+
assertRuntimeActionValid(
|
|
7584
|
+
Boolean(sourceWorkProductId) === Boolean(supersedesWorkProductId),
|
|
7585
|
+
"upload_artifact parent replacement requires both sourceWorkProductId and supersedesWorkProductId",
|
|
7586
|
+
);
|
|
7587
|
+
const evidenceRole = readString(fields.evidenceRole);
|
|
7588
|
+
assertRuntimeActionValid(
|
|
7589
|
+
!evidenceRole || ["source", "derived", "review"].includes(evidenceRole),
|
|
7590
|
+
`upload_artifact evidenceRole is unsupported: ${String(fields.evidenceRole)}`,
|
|
7591
|
+
);
|
|
7592
|
+
assertRuntimeActionValid(
|
|
7593
|
+
!["derived", "review"].includes(evidenceRole ?? "") || sourceWorkProductIds.length > 0 || Boolean(sourceWorkProductId),
|
|
7594
|
+
`upload_artifact evidenceRole=${evidenceRole} requires sourceWorkProductIds`,
|
|
7595
|
+
);
|
|
7596
|
+
if (fields.sourceWorkProductIds !== undefined) {
|
|
7597
|
+
assertRuntimeActionValid(Array.isArray(fields.sourceWorkProductIds), "upload_artifact sourceWorkProductIds must be an array");
|
|
7598
|
+
assertRuntimeActionValid(sourceWorkProductIds.length === fields.sourceWorkProductIds.length, "upload_artifact sourceWorkProductIds must contain non-empty strings");
|
|
7599
|
+
}
|
|
7600
|
+
if (fields.scenarioKeys !== undefined) {
|
|
7601
|
+
assertRuntimeActionValid(Array.isArray(fields.scenarioKeys), "upload_artifact scenarioKeys must be an array");
|
|
7602
|
+
assertRuntimeActionValid(fields.scenarioKeys.map(readString).filter(Boolean).length === fields.scenarioKeys.length, "upload_artifact scenarioKeys must contain non-empty strings");
|
|
7603
|
+
}
|
|
6877
7604
|
assertRuntimeArtifactContentType(fields, preflightContext.allowedContentTypes);
|
|
7605
|
+
resolveWorkspaceArtifactPath(fields, { cwd: preflightContext.cwd });
|
|
7606
|
+
return;
|
|
7607
|
+
}
|
|
7608
|
+
if (type === "upsert_document") {
|
|
7609
|
+
assertNoPlaceholderRuntimeActionText(fields, ["key", "title", "body", "changeSummary"]);
|
|
7610
|
+
const key = readBoundedActionString(fields, "key", 64);
|
|
7611
|
+
const body = readBoundedActionString(fields, "body", 524_288);
|
|
7612
|
+
assertRuntimeActionValid(key && /^[a-z0-9][a-z0-9_-]*$/.test(key), "upsert_document key must use lowercase letters, numbers, _ or -");
|
|
7613
|
+
assertRuntimeActionValid(body, "upsert_document action requires body");
|
|
7614
|
+
assertRuntimeActionValid(fields.format === "markdown", "upsert_document format must be markdown");
|
|
7615
|
+
const baseRevisionId = readString(fields.baseRevisionId);
|
|
7616
|
+
assertRuntimeActionValid(
|
|
7617
|
+
!baseRevisionId || /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(baseRevisionId),
|
|
7618
|
+
"upsert_document baseRevisionId must be a UUID",
|
|
7619
|
+
);
|
|
6878
7620
|
return;
|
|
6879
7621
|
}
|
|
6880
7622
|
if (type === "create_interaction") {
|
|
@@ -6885,12 +7627,27 @@ function preflightRuntimeAction(action, preflightContext) {
|
|
|
6885
7627
|
["request_confirmation", "request_checkbox_confirmation", "ask_user_questions", "suggest_tasks"].includes(kind),
|
|
6886
7628
|
`create_interaction action does not support kind: ${kind}`,
|
|
6887
7629
|
);
|
|
7630
|
+
assertRuntimeActionValid(
|
|
7631
|
+
kind !== "ask_user_questions" || readString(fields.continuationPolicy) !== "wake_assignee_on_accept",
|
|
7632
|
+
"ask_user_questions continuationPolicy cannot be wake_assignee_on_accept; use wake_assignee so answering the questions resumes the assignee",
|
|
7633
|
+
);
|
|
6888
7634
|
const payload = normalizeCreateInteractionPayload(fields, kind);
|
|
6889
7635
|
assertRuntimeActionValid(
|
|
6890
7636
|
Object.keys(payload).length > 0,
|
|
6891
7637
|
`create_interaction.${kind} action requires payload; expected shape is available in runtimeActionDiagnostics.expectedShape`,
|
|
6892
7638
|
);
|
|
6893
7639
|
assertRuntimeCreateInteractionPayload(kind, payload);
|
|
7640
|
+
const confirmationTarget = asRecord(payload.target);
|
|
7641
|
+
if (
|
|
7642
|
+
preflightContext.acceptedConfirmationContinuation &&
|
|
7643
|
+
["request_confirmation", "request_checkbox_confirmation"].includes(kind) &&
|
|
7644
|
+
(!readString(confirmationTarget.type) || !readString(confirmationTarget.key))
|
|
7645
|
+
) {
|
|
7646
|
+
assertRuntimeActionValid(
|
|
7647
|
+
false,
|
|
7648
|
+
"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
|
+
);
|
|
7650
|
+
}
|
|
6894
7651
|
preflightContext.createdReviewPathForInReview = true;
|
|
6895
7652
|
return;
|
|
6896
7653
|
}
|
|
@@ -6899,6 +7656,20 @@ function preflightRuntimeAction(action, preflightContext) {
|
|
|
6899
7656
|
const status = readActionStatus(fields.status);
|
|
6900
7657
|
const comment = readBoundedActionString(fields, "comment", 20_000);
|
|
6901
7658
|
assertRuntimeActionValid(status || comment, "update_parent action requires status or comment");
|
|
7659
|
+
if (fields.status !== undefined) {
|
|
7660
|
+
assertRuntimeActionValid(status, `update_parent status is unsupported: ${String(fields.status)}`);
|
|
7661
|
+
}
|
|
7662
|
+
if (
|
|
7663
|
+
status === "done" &&
|
|
7664
|
+
runtimeActionDoneNeedsExternalHuman(fields, preflightContext) &&
|
|
7665
|
+
!preflightContext.createdHumanOwnedInputPath &&
|
|
7666
|
+
fields.force !== true
|
|
7667
|
+
) {
|
|
7668
|
+
assertRuntimeActionValid(
|
|
7669
|
+
false,
|
|
7670
|
+
"external_input_disposition_required: external input cannot be represented only by a free-text owner; create a human-owned blocking child task or another first-class external dependency path",
|
|
7671
|
+
);
|
|
7672
|
+
}
|
|
6902
7673
|
if (
|
|
6903
7674
|
status === "in_review" &&
|
|
6904
7675
|
!preflightContext.createdReviewPathForInReview &&
|
|
@@ -6911,22 +7682,45 @@ function preflightRuntimeAction(action, preflightContext) {
|
|
|
6911
7682
|
) {
|
|
6912
7683
|
preflightContext.createdReviewPathForInReview = true;
|
|
6913
7684
|
}
|
|
7685
|
+
return;
|
|
7686
|
+
}
|
|
7687
|
+
if (type === "update_company_profile") {
|
|
7688
|
+
const profileFields = ["tagline", "stage", "targetCustomers", "products", "currentPriority", "businessModel", "regions", "brandTone"];
|
|
7689
|
+
assertRuntimeActionValid(
|
|
7690
|
+
profileFields.some((field) => fields[field] === null || readBoundedActionString(fields, field, 2000)),
|
|
7691
|
+
"update_company_profile action requires at least one profile field",
|
|
7692
|
+
);
|
|
7693
|
+
return;
|
|
7694
|
+
}
|
|
7695
|
+
if (type === "create_routine") {
|
|
7696
|
+
assertRuntimeActionValid(readBoundedActionString(fields, "title", 240), "create_routine action requires title");
|
|
7697
|
+
assertRuntimeActionValid(readBoundedActionString(fields, "cronExpression", 120), "create_routine action requires cronExpression");
|
|
7698
|
+
if (fields.priority !== undefined) {
|
|
7699
|
+
assertRuntimeActionValid(readActionPriority(fields.priority), `create_routine priority is unsupported: ${String(fields.priority)}`);
|
|
7700
|
+
}
|
|
6914
7701
|
}
|
|
6915
7702
|
}
|
|
6916
7703
|
|
|
6917
7704
|
function preflightRuntimeActionBatch(actions, options = {}) {
|
|
6918
|
-
|
|
6919
|
-
|
|
7705
|
+
const actionIndexByObject = new Map(actions.map((action, index) => [action, index]));
|
|
7706
|
+
assertDelegatedParentWaitDisposition(actions);
|
|
7707
|
+
const orderedActions = orderRuntimeActionsByClientKeyDependencies(actions);
|
|
7708
|
+
assertRuntimeActionsAllowedByContinuationScope(orderedActions, options.continuationScope);
|
|
7709
|
+
assertNoAmbiguousApprovalGatedSideEffects(orderedActions);
|
|
6920
7710
|
const preflightContext = {
|
|
6921
7711
|
createdReviewPathForInReview: false,
|
|
6922
7712
|
createdRootBlockingChildForInReview: false,
|
|
7713
|
+
createdHumanOwnedInputPath: false,
|
|
6923
7714
|
answeredQuestionContinuation: options.answeredQuestionContinuation === true,
|
|
7715
|
+
acceptedConfirmationContinuation: options.acceptedConfirmationContinuation === true,
|
|
6924
7716
|
allowedContentTypes: Array.isArray(options.allowedContentTypes) ? options.allowedContentTypes : [],
|
|
7717
|
+
cwd: readString(options.cwd),
|
|
6925
7718
|
finalCommentBodies: [],
|
|
6926
7719
|
};
|
|
6927
|
-
for (const action of
|
|
7720
|
+
for (const action of orderedActions) {
|
|
7721
|
+
const actionIndex = actionIndexByObject.get(action);
|
|
6928
7722
|
try {
|
|
6929
|
-
preflightRuntimeAction(action, preflightContext);
|
|
7723
|
+
preflightRuntimeAction(action, preflightContext, actionIndex);
|
|
6930
7724
|
const fields = runtimeActionBusinessFields(action);
|
|
6931
7725
|
if (readRuntimeActionType(fields) === "add_comment") {
|
|
6932
7726
|
const body = readBoundedActionString(fields, "body", 20_000)
|
|
@@ -6938,6 +7732,9 @@ function preflightRuntimeActionBatch(actions, options = {}) {
|
|
|
6938
7732
|
if (willRuntimeActionCreateRootBlockingChild(action)) {
|
|
6939
7733
|
preflightContext.createdRootBlockingChildForInReview = true;
|
|
6940
7734
|
}
|
|
7735
|
+
if (willRuntimeActionCreateHumanOwnedInputPath(action)) {
|
|
7736
|
+
preflightContext.createdHumanOwnedInputPath = true;
|
|
7737
|
+
}
|
|
6941
7738
|
if (willRuntimeActionCreateReviewPath(action)) {
|
|
6942
7739
|
preflightContext.createdReviewPathForInReview = true;
|
|
6943
7740
|
}
|
|
@@ -6954,6 +7751,40 @@ function preflightRuntimeActionBatch(actions, options = {}) {
|
|
|
6954
7751
|
throw err;
|
|
6955
7752
|
}
|
|
6956
7753
|
}
|
|
7754
|
+
return orderedActions;
|
|
7755
|
+
}
|
|
7756
|
+
|
|
7757
|
+
function runtimeActionAuthorizationClass(action) {
|
|
7758
|
+
// Keep these high-risk route mappings aligned with server-side checks in
|
|
7759
|
+
// routes/agents.ts (organization_mutation) and routes/issues.ts
|
|
7760
|
+
// (executable_child). Unknown action types intentionally remain governance.
|
|
7761
|
+
const fields = runtimeActionBusinessFields(action);
|
|
7762
|
+
const type = readRuntimeActionType(fields);
|
|
7763
|
+
if (["agent_hire", "create_agent"].includes(type ?? "")) return "organization_mutation";
|
|
7764
|
+
if (
|
|
7765
|
+
type === "create_child_task" &&
|
|
7766
|
+
["todo", "in_progress"].includes(readActionStatus(fields.status) ?? "todo")
|
|
7767
|
+
) return "executable_child";
|
|
7768
|
+
return "task_governance";
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
function assertRuntimeActionsAllowedByAuthorizationEnvelope(actions, envelope) {
|
|
7772
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
7773
|
+
const requiredAuthorization = runtimeActionAuthorizationClass(action);
|
|
7774
|
+
if (envelope.allowedActionClasses.has(requiredAuthorization)) continue;
|
|
7775
|
+
const type = readRuntimeActionType(runtimeActionBusinessFields(action)) ?? "unknown";
|
|
7776
|
+
const err = runtimeActionValidationError(
|
|
7777
|
+
`${type} action at index ${actionIndex} requires authorization class ${requiredAuthorization}; ` +
|
|
7778
|
+
`source ${envelope.sourceKind ?? "unknown"}:${envelope.sourceId ?? "unknown"} revision ${envelope.sourceRevision ?? "unknown"}`,
|
|
7779
|
+
);
|
|
7780
|
+
err.runtimeActionType = type;
|
|
7781
|
+
err.runtimeActionFieldPath = `actions[${actionIndex}]`;
|
|
7782
|
+
err.runtimeActionRequiredAuthorization = requiredAuthorization;
|
|
7783
|
+
err.runtimeActionAuthorizationSourceKind = envelope.sourceKind;
|
|
7784
|
+
err.runtimeActionAuthorizationSourceId = envelope.sourceId;
|
|
7785
|
+
err.runtimeActionAuthorizationSourceRevision = envelope.sourceRevision;
|
|
7786
|
+
throw err;
|
|
7787
|
+
}
|
|
6957
7788
|
}
|
|
6958
7789
|
|
|
6959
7790
|
function normalizeRuntimeActionPlaceholderText(value) {
|
|
@@ -6984,6 +7815,7 @@ const SINGLE_KEY_RUNTIME_ACTION_TYPES = new Set([
|
|
|
6984
7815
|
"create_child_task",
|
|
6985
7816
|
"create_interaction",
|
|
6986
7817
|
"create_routine",
|
|
7818
|
+
"upsert_document",
|
|
6987
7819
|
"update_company_profile",
|
|
6988
7820
|
"update_parent",
|
|
6989
7821
|
"upload_artifact",
|
|
@@ -7056,6 +7888,10 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7056
7888
|
priority: readActionPriority(fields.priority) ?? "medium",
|
|
7057
7889
|
assigneeAgentId: runtimeChildAssigneeAgentId(fields, runtimeAuth, command, actionContext),
|
|
7058
7890
|
assigneeUserId: readString(fields.assigneeUserId),
|
|
7891
|
+
blockedByIssueIds: runtimeActionBlockedByIssueIds(fields, actionContext),
|
|
7892
|
+
...(Array.isArray(fields.inputWorkProductIds)
|
|
7893
|
+
? { inputWorkProductIds: fields.inputWorkProductIds.map(readString).filter(Boolean) }
|
|
7894
|
+
: {}),
|
|
7059
7895
|
blockParentUntilDone,
|
|
7060
7896
|
originKind: "amaster_runtime_child_task",
|
|
7061
7897
|
originId: issueId,
|
|
@@ -7066,6 +7902,23 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7066
7902
|
idempotencyKey: originFingerprint,
|
|
7067
7903
|
});
|
|
7068
7904
|
const createdIssueId = readString(asRecord(created).id);
|
|
7905
|
+
const persistedAssigneeAgentId = readString(asRecord(created).assigneeAgentId);
|
|
7906
|
+
if (body.assigneeAgentId && persistedAssigneeAgentId !== body.assigneeAgentId) {
|
|
7907
|
+
throw new Error(
|
|
7908
|
+
`runtime_action_postcondition_failed: child assignee mismatch; expected ${body.assigneeAgentId}, received ${persistedAssigneeAgentId ?? "missing"}`,
|
|
7909
|
+
);
|
|
7910
|
+
}
|
|
7911
|
+
if (body.blockedByIssueIds.length > 0) {
|
|
7912
|
+
const persistedBlockerIds = Array.isArray(asRecord(created).blockedBy)
|
|
7913
|
+
? asRecord(created).blockedBy.map((entry) => readString(asRecord(entry).id)).filter(Boolean)
|
|
7914
|
+
: [];
|
|
7915
|
+
const missingBlockerIds = body.blockedByIssueIds.filter((blockerId) => !persistedBlockerIds.includes(blockerId));
|
|
7916
|
+
if (missingBlockerIds.length > 0) {
|
|
7917
|
+
throw new Error(
|
|
7918
|
+
`runtime_action_postcondition_failed: child blockers were not persisted: ${missingBlockerIds.join(", ")}`,
|
|
7919
|
+
);
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7069
7922
|
const clientKey = runtimeActionResultKey(fields);
|
|
7070
7923
|
if (createdIssueId) actionContext.createdChildIssueIdByFingerprint.set(duplicateKey, createdIssueId);
|
|
7071
7924
|
if (createdIssueId && clientKey) actionContext.issueIdByClientKey.set(clientKey, createdIssueId);
|
|
@@ -7105,6 +7958,8 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7105
7958
|
adapterType: runtimeActionCurrentAgentAdapterType(command),
|
|
7106
7959
|
runtimeConfig: runtimeActionCurrentAgentRuntimeConnector(command),
|
|
7107
7960
|
});
|
|
7961
|
+
const reportsTo = runtimeActionReportsToAgentId(fields, actionContext);
|
|
7962
|
+
if (reportsTo) body.reportsTo = reportsTo;
|
|
7108
7963
|
const idempotencyKey = runtimeAgentHireActionFingerprint(fields, companyId, issueId, body);
|
|
7109
7964
|
body.idempotencyKey = idempotencyKey;
|
|
7110
7965
|
const created = asRecord(await runtimeApiJson(
|
|
@@ -7118,6 +7973,16 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7118
7973
|
const approval = asRecord(created.approval);
|
|
7119
7974
|
const clientKey = runtimeActionResultKey(fields);
|
|
7120
7975
|
const agentId = readString(agent.id);
|
|
7976
|
+
if (reportsTo && readString(agent.reportsTo) !== reportsTo) {
|
|
7977
|
+
throw new Error(
|
|
7978
|
+
`runtime_action_postcondition_failed: agent reportsTo mismatch; expected ${reportsTo}, received ${readString(agent.reportsTo) ?? "missing"}`,
|
|
7979
|
+
);
|
|
7980
|
+
}
|
|
7981
|
+
if (fields.topLevel === true && readString(agent.reportsTo)) {
|
|
7982
|
+
throw new Error(
|
|
7983
|
+
`runtime_action_postcondition_failed: top-level agent reportsTo mismatch; expected missing, received ${readString(agent.reportsTo)}`,
|
|
7984
|
+
);
|
|
7985
|
+
}
|
|
7121
7986
|
if (agentId && clientKey) actionContext.agentIdByClientKey.set(clientKey, agentId);
|
|
7122
7987
|
return {
|
|
7123
7988
|
type,
|
|
@@ -7130,6 +7995,8 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7130
7995
|
approvalStatus: readString(approval.status),
|
|
7131
7996
|
sourceIssueId: readString(body.sourceIssueId) ?? null,
|
|
7132
7997
|
sourceIssueIds: runtimeActionIssueIdList(body.sourceIssueIds),
|
|
7998
|
+
reportsTo: reportsTo ?? null,
|
|
7999
|
+
topLevel: fields.topLevel === true,
|
|
7133
8000
|
...(clientKey ? { clientKey } : {}),
|
|
7134
8001
|
reusedExisting: asRecord(created).reusedExisting === true,
|
|
7135
8002
|
};
|
|
@@ -7163,25 +8030,6 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7163
8030
|
{ key: "idempotencyKey", placeholders: ["artifact-key", "artifact key", "upload-artifact-key", "upload artifact key"] },
|
|
7164
8031
|
]);
|
|
7165
8032
|
const rawPath = readRuntimeArtifactPath(fields);
|
|
7166
|
-
if (!rawPath) {
|
|
7167
|
-
const title = readBoundedActionString(fields, "title", 240) ?? "运行产物";
|
|
7168
|
-
const summary = readBoundedActionString(fields, "summary", 20_000);
|
|
7169
|
-
const body = [
|
|
7170
|
-
`AMaster 未上传产物 \`${title}\`,因为 runtime action 没有提供工作区内文件路径。`,
|
|
7171
|
-
summary ? `\n${summary}` : "",
|
|
7172
|
-
"\n请在后续运行中先把文件写入执行工作区,再用 upload_artifact.path 引用该相对路径。",
|
|
7173
|
-
].join("");
|
|
7174
|
-
const created = await runtimeApiJson(runtimeAuth, "POST", `/api/issues/${encodeURIComponent(issueId)}/comments`, { body });
|
|
7175
|
-
actionContext.finalCommentBodies.push(body);
|
|
7176
|
-
return {
|
|
7177
|
-
type,
|
|
7178
|
-
...(originalType && originalType !== type ? { originalType } : {}),
|
|
7179
|
-
status: "skipped",
|
|
7180
|
-
reason: "missing_path",
|
|
7181
|
-
title,
|
|
7182
|
-
commentId: readString(asRecord(created).id),
|
|
7183
|
-
};
|
|
7184
|
-
}
|
|
7185
8033
|
const payload = asRecord(command.payload);
|
|
7186
8034
|
const companyId = readString(payload.companyId) ?? readString(runtimeAuth.companyId);
|
|
7187
8035
|
if (!companyId) throw new Error("upload_artifact action requires companyId");
|
|
@@ -7203,15 +8051,25 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7203
8051
|
const title = readBoundedActionString(fields, "title", 240) ?? filename;
|
|
7204
8052
|
if (fields.createWorkProduct !== false) {
|
|
7205
8053
|
const existingWorkProducts = await listExistingIssueWorkProducts(runtimeAuth, issueId);
|
|
7206
|
-
const existingWorkProduct = existingExplicitArtifactUploadMatch(filePath, title, existingWorkProducts);
|
|
8054
|
+
const existingWorkProduct = existingExplicitArtifactUploadMatch(filePath, title, existingWorkProducts, fields);
|
|
7207
8055
|
if (existingWorkProduct) {
|
|
7208
8056
|
const metadata = asRecord(existingWorkProduct.metadata);
|
|
8057
|
+
const existingWorkProductId = readString(existingWorkProduct.id);
|
|
8058
|
+
const existingAttachmentId = readString(metadata.attachmentId);
|
|
8059
|
+
if (!existingWorkProductId || !existingAttachmentId) {
|
|
8060
|
+
throw new Error("runtime_action_postcondition_failed: reused artifact is missing work product or attachment identity");
|
|
8061
|
+
}
|
|
8062
|
+
await verifyUploadedArtifactPostcondition(runtimeAuth, issueId, {
|
|
8063
|
+
attachmentId: existingAttachmentId,
|
|
8064
|
+
workProductId: existingWorkProductId,
|
|
8065
|
+
sha256,
|
|
8066
|
+
}, fields);
|
|
7209
8067
|
return {
|
|
7210
8068
|
type,
|
|
7211
8069
|
...(originalType && originalType !== type ? { originalType } : {}),
|
|
7212
8070
|
path: filePath,
|
|
7213
|
-
attachmentId:
|
|
7214
|
-
workProductId:
|
|
8071
|
+
attachmentId: existingAttachmentId,
|
|
8072
|
+
workProductId: existingWorkProductId,
|
|
7215
8073
|
sha256: readString(metadata.sha256) ?? sha256,
|
|
7216
8074
|
byteSize: readNumber(metadata.byteSize, fileBytes.byteLength),
|
|
7217
8075
|
originalFilename: readString(metadata.originalFilename) ?? filename,
|
|
@@ -7233,11 +8091,12 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7233
8091
|
if (!attachmentId) throw new Error("upload_artifact attachment response did not include id");
|
|
7234
8092
|
|
|
7235
8093
|
let workProduct = null;
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
8094
|
+
try {
|
|
8095
|
+
if (fields.createWorkProduct !== false) {
|
|
8096
|
+
const contentPath = readString(attachment.contentPath) ?? `/api/attachments/${attachmentId}/content`;
|
|
8097
|
+
const openPath = readString(attachment.openPath) ?? contentPath;
|
|
8098
|
+
const downloadPath = readString(attachment.downloadPath) ?? `${contentPath}?download=1`;
|
|
8099
|
+
workProduct = asRecord(await runtimeApiJson(runtimeAuth, "POST", `/api/issues/${encodeURIComponent(issueId)}/work-products`, {
|
|
7241
8100
|
type: "artifact",
|
|
7242
8101
|
provider: readString(fields.provider) ?? "paperclip",
|
|
7243
8102
|
title,
|
|
@@ -7256,8 +8115,26 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7256
8115
|
openPath,
|
|
7257
8116
|
downloadPath,
|
|
7258
8117
|
originalFilename: readString(attachment.originalFilename) ?? filename,
|
|
8118
|
+
...(readString(fields.sourceWorkProductId) ? { sourceWorkProductId: readString(fields.sourceWorkProductId) } : {}),
|
|
8119
|
+
...(Array.isArray(fields.sourceWorkProductIds) ? { sourceWorkProductIds: fields.sourceWorkProductIds.map(readString).filter(Boolean) } : {}),
|
|
8120
|
+
...(readString(fields.supersedesWorkProductId) ? { supersedesWorkProductId: readString(fields.supersedesWorkProductId) } : {}),
|
|
8121
|
+
...(readString(fields.evidenceRole) ? { evidenceRole: readString(fields.evidenceRole) } : {}),
|
|
8122
|
+
...(Array.isArray(fields.scenarioKeys) ? { scenarioKeys: fields.scenarioKeys.map(readString).filter(Boolean) } : {}),
|
|
7259
8123
|
},
|
|
7260
|
-
|
|
8124
|
+
}));
|
|
8125
|
+
const workProductId = readString(workProduct.id);
|
|
8126
|
+
if (!workProductId) {
|
|
8127
|
+
throw new Error("runtime_action_postcondition_failed: artifact work product response did not include id");
|
|
8128
|
+
}
|
|
8129
|
+
await verifyUploadedArtifactPostcondition(runtimeAuth, issueId, {
|
|
8130
|
+
attachmentId,
|
|
8131
|
+
workProductId,
|
|
8132
|
+
sha256: readString(attachment.sha256) ?? sha256,
|
|
8133
|
+
}, fields);
|
|
8134
|
+
}
|
|
8135
|
+
} catch (err) {
|
|
8136
|
+
await cleanupFailedArtifactUpload(config, command, runtimeAuth, attachmentId, readString(workProduct?.id), err);
|
|
8137
|
+
throw err;
|
|
7261
8138
|
}
|
|
7262
8139
|
|
|
7263
8140
|
return {
|
|
@@ -7315,6 +8192,37 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7315
8192
|
};
|
|
7316
8193
|
}
|
|
7317
8194
|
|
|
8195
|
+
if (type === "upsert_document") {
|
|
8196
|
+
assertNoPlaceholderRuntimeActionText(fields, ["key", "title", "body", "changeSummary"]);
|
|
8197
|
+
const key = readBoundedActionString(fields, "key", 64);
|
|
8198
|
+
if (!key || !/^[a-z0-9][a-z0-9_-]*$/.test(key)) {
|
|
8199
|
+
throw new Error("upsert_document key must use lowercase letters, numbers, _ or -");
|
|
8200
|
+
}
|
|
8201
|
+
const body = readBoundedActionString(fields, "body", 524_288);
|
|
8202
|
+
if (!body) throw new Error("upsert_document action requires body");
|
|
8203
|
+
if (fields.format !== "markdown") throw new Error("upsert_document format must be markdown");
|
|
8204
|
+
const document = asRecord(await runtimeApiJson(
|
|
8205
|
+
runtimeAuth,
|
|
8206
|
+
"PUT",
|
|
8207
|
+
`/api/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`,
|
|
8208
|
+
{
|
|
8209
|
+
title: readBoundedActionString(fields, "title", 200) ?? null,
|
|
8210
|
+
format: "markdown",
|
|
8211
|
+
body,
|
|
8212
|
+
changeSummary: readBoundedActionString(fields, "changeSummary", 500) ?? null,
|
|
8213
|
+
baseRevisionId: readString(fields.baseRevisionId) ?? null,
|
|
8214
|
+
},
|
|
8215
|
+
));
|
|
8216
|
+
return {
|
|
8217
|
+
type,
|
|
8218
|
+
...(originalType && originalType !== type ? { originalType } : {}),
|
|
8219
|
+
issueId,
|
|
8220
|
+
key,
|
|
8221
|
+
documentId: readString(document.id),
|
|
8222
|
+
revisionNumber: typeof document.latestRevisionNumber === "number" ? document.latestRevisionNumber : undefined,
|
|
8223
|
+
};
|
|
8224
|
+
}
|
|
8225
|
+
|
|
7318
8226
|
if (type === "update_parent") {
|
|
7319
8227
|
assertNoPlaceholderRuntimeActionText(fields, ["comment"]);
|
|
7320
8228
|
const patch = {};
|
|
@@ -7330,30 +8238,33 @@ async function applyRuntimeAction(config, command, runtimeAuth, issueId, action,
|
|
|
7330
8238
|
if (Object.keys(patch).length === 0) {
|
|
7331
8239
|
throw new Error("update_parent action requires status or comment");
|
|
7332
8240
|
}
|
|
7333
|
-
if (status === "done" &&
|
|
7334
|
-
|
|
8241
|
+
if (status === "done" && actionContext.createdReviewPathForInReview && fields.force !== true) {
|
|
8242
|
+
patch.status = "in_review";
|
|
8243
|
+
patch.comment = comment
|
|
8244
|
+
? `${comment}\n\nAMaster 已检测到本次运行已创建待确认事项,因此任务保持待确认;请在确认后继续推进。`
|
|
8245
|
+
: "AMaster 已检测到本次运行已创建待确认事项,因此任务保持待确认;请在确认后继续推进。";
|
|
7335
8246
|
const updated = await runtimeApiJson(runtimeAuth, "PATCH", `/api/issues/${encodeURIComponent(issueId)}`, patch);
|
|
7336
8247
|
return {
|
|
7337
8248
|
type,
|
|
7338
8249
|
...(originalType && originalType !== type ? { originalType } : {}),
|
|
7339
8250
|
issueId: readString(asRecord(updated).id),
|
|
7340
|
-
status: readString(asRecord(updated).status) ?? "
|
|
8251
|
+
status: readString(asRecord(updated).status) ?? "in_review",
|
|
7341
8252
|
coercedFromStatus: "done",
|
|
7342
|
-
|
|
8253
|
+
existingReviewPath: true,
|
|
7343
8254
|
};
|
|
7344
8255
|
}
|
|
7345
|
-
if (status === "
|
|
8256
|
+
if (status === "in_progress" && actionContext.createdReviewPathForInReview && fields.force !== true) {
|
|
7346
8257
|
patch.status = "in_review";
|
|
7347
8258
|
patch.comment = comment
|
|
7348
|
-
? `${comment}\n\nAMaster
|
|
7349
|
-
: "AMaster
|
|
8259
|
+
? `${comment}\n\nAMaster detected a pending review interaction in this run, so the task remains in review until that interaction is resolved.`
|
|
8260
|
+
: "AMaster detected a pending review interaction in this run, so the task remains in review until that interaction is resolved.";
|
|
7350
8261
|
const updated = await runtimeApiJson(runtimeAuth, "PATCH", `/api/issues/${encodeURIComponent(issueId)}`, patch);
|
|
7351
8262
|
return {
|
|
7352
8263
|
type,
|
|
7353
8264
|
...(originalType && originalType !== type ? { originalType } : {}),
|
|
7354
8265
|
issueId: readString(asRecord(updated).id),
|
|
7355
8266
|
status: readString(asRecord(updated).status) ?? "in_review",
|
|
7356
|
-
coercedFromStatus: "
|
|
8267
|
+
coercedFromStatus: "in_progress",
|
|
7357
8268
|
existingReviewPath: true,
|
|
7358
8269
|
};
|
|
7359
8270
|
}
|
|
@@ -7582,6 +8493,48 @@ function runtimeActionFailureSummary(action, err) {
|
|
|
7582
8493
|
};
|
|
7583
8494
|
}
|
|
7584
8495
|
|
|
8496
|
+
function runtimeActionLedgerResourceIds(result) {
|
|
8497
|
+
const record = asRecord(result);
|
|
8498
|
+
const resourceIds = {};
|
|
8499
|
+
for (const key of ["issueId", "parentIssueId", "agentId", "approvalId", "commentId", "interactionId", "attachmentId", "workProductId", "routineId", "profileId"]) {
|
|
8500
|
+
const value = readString(record[key]);
|
|
8501
|
+
if (value) resourceIds[key] = value;
|
|
8502
|
+
}
|
|
8503
|
+
return resourceIds;
|
|
8504
|
+
}
|
|
8505
|
+
|
|
8506
|
+
function runtimeActionLedgerPostconditionStatus(action, status, error) {
|
|
8507
|
+
if (status === "failed") {
|
|
8508
|
+
return /runtime_action_postcondition_failed/i.test(String(error instanceof Error ? error.message : error ?? ""))
|
|
8509
|
+
? "failed"
|
|
8510
|
+
: "not_run";
|
|
8511
|
+
}
|
|
8512
|
+
if (status !== "applied" && status !== "reused") return "not_run";
|
|
8513
|
+
return ["create_child_task", "agent_hire", "create_agent", "upload_artifact"].includes(
|
|
8514
|
+
readRuntimeActionType(action) ?? "",
|
|
8515
|
+
)
|
|
8516
|
+
? "passed"
|
|
8517
|
+
: "not_applicable";
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
function runtimeActionLedgerEntry(action, actionIndex, status, result = null, error = null) {
|
|
8521
|
+
const fields = runtimeActionBusinessFields(action);
|
|
8522
|
+
const resultRecord = asRecord(result);
|
|
8523
|
+
const clientKey = readString(fields.clientKey);
|
|
8524
|
+
const resourceIds = runtimeActionLedgerResourceIds(resultRecord);
|
|
8525
|
+
return {
|
|
8526
|
+
actionIndex,
|
|
8527
|
+
actionType: readRuntimeActionType(fields) ?? "unknown",
|
|
8528
|
+
actionKey: clientKey ?? `index:${actionIndex}`,
|
|
8529
|
+
...(clientKey ? { clientKey } : {}),
|
|
8530
|
+
status,
|
|
8531
|
+
reusedExisting: resultRecord.reusedExisting === true,
|
|
8532
|
+
postconditionStatus: runtimeActionLedgerPostconditionStatus(fields, status, error),
|
|
8533
|
+
...(Object.keys(resourceIds).length > 0 ? { resourceIds } : {}),
|
|
8534
|
+
...(error ? { error: truncateText(error instanceof Error ? error.message : String(error), 1000) } : {}),
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
|
|
7585
8538
|
function isSkippedRuntimeActionResult(result) {
|
|
7586
8539
|
return readString(asRecord(result).status) === "skipped";
|
|
7587
8540
|
}
|
|
@@ -7725,6 +8678,16 @@ function runtimeActionExpectedShape(actionType, actionKind) {
|
|
|
7725
8678
|
if (actionType === "upload_artifact") {
|
|
7726
8679
|
return { type: "upload_artifact", path: "relative/path.ext", title: "Artifact title", summary: "Artifact summary" };
|
|
7727
8680
|
}
|
|
8681
|
+
if (actionType === "upsert_document") {
|
|
8682
|
+
return {
|
|
8683
|
+
type: "upsert_document",
|
|
8684
|
+
key: "business-brief",
|
|
8685
|
+
title: "2026-07-16 每日经营简报",
|
|
8686
|
+
body: "# 每日经营简报\n\n经营摘要。",
|
|
8687
|
+
format: "markdown",
|
|
8688
|
+
changeSummary: "生成每日经营简报",
|
|
8689
|
+
};
|
|
8690
|
+
}
|
|
7728
8691
|
if (actionType === "agent_hire" || actionType === "create_agent") {
|
|
7729
8692
|
return {
|
|
7730
8693
|
type: actionType,
|
|
@@ -7755,7 +8718,19 @@ function runtimeActionDiagnostics(err, options = {}) {
|
|
|
7755
8718
|
actionType,
|
|
7756
8719
|
schema: runtimeActionSchemaName(actionType, actionKind),
|
|
7757
8720
|
...(reasonHint ? { reasonHint } : {}),
|
|
8721
|
+
...(readString(err?.runtimeActionDecompositionRequirementSource)
|
|
8722
|
+
? { decompositionRequirementSource: readString(err.runtimeActionDecompositionRequirementSource) }
|
|
8723
|
+
: {}),
|
|
8724
|
+
...(err?.runtimeActionDecompositionMatch
|
|
8725
|
+
? { decompositionMatch: err.runtimeActionDecompositionMatch }
|
|
8726
|
+
: {}),
|
|
7758
8727
|
...(err?.runtimeActionInferenceTrace ? { inferenceTrace: err.runtimeActionInferenceTrace } : {}),
|
|
8728
|
+
...(readString(err?.runtimeActionFieldPath) ? { fieldPath: readString(err.runtimeActionFieldPath) } : {}),
|
|
8729
|
+
...(readString(err?.runtimeActionReceivedType) ? { receivedType: readString(err.runtimeActionReceivedType) } : {}),
|
|
8730
|
+
...(readString(err?.runtimeActionRequiredAuthorization) ? { requiredAuthorization: readString(err.runtimeActionRequiredAuthorization) } : {}),
|
|
8731
|
+
...(readString(err?.runtimeActionAuthorizationSourceKind) ? { authorizationSourceKind: readString(err.runtimeActionAuthorizationSourceKind) } : {}),
|
|
8732
|
+
...(readString(err?.runtimeActionAuthorizationSourceId) ? { authorizationSourceId: readString(err.runtimeActionAuthorizationSourceId) } : {}),
|
|
8733
|
+
...(readString(err?.runtimeActionAuthorizationSourceRevision) ? { authorizationSourceRevision: readString(err.runtimeActionAuthorizationSourceRevision) } : {}),
|
|
7759
8734
|
...(Array.isArray(err?.runtimeActionAllowedContentTypes)
|
|
7760
8735
|
? { allowedContentTypes: err.runtimeActionAllowedContentTypes }
|
|
7761
8736
|
: {}),
|
|
@@ -7765,6 +8740,7 @@ function runtimeActionDiagnostics(err, options = {}) {
|
|
|
7765
8740
|
...(err?.runtimeActionVerificationResult
|
|
7766
8741
|
? { verificationResult: err.runtimeActionVerificationResult }
|
|
7767
8742
|
: {}),
|
|
8743
|
+
...(err?.runtimeActionLedger ? { actionLedger: err.runtimeActionLedger } : {}),
|
|
7768
8744
|
...(runtimeActionExpectedShape(actionType, actionKind) ? { expectedShape: runtimeActionExpectedShape(actionType, actionKind) } : {}),
|
|
7769
8745
|
...(typeof err?.httpStatus === "number" ? { backendStatus: err.httpStatus } : {}),
|
|
7770
8746
|
...(backendErrors.length > 0 ? { backendErrors } : {}),
|
|
@@ -7783,6 +8759,14 @@ function classifyRuntimeActionFailure(err) {
|
|
|
7783
8759
|
...(reason ? { actionFailureReason: reason } : {}),
|
|
7784
8760
|
};
|
|
7785
8761
|
const diagnostics = runtimeActionDiagnostics(err, { retryable: false });
|
|
8762
|
+
if (actionType === "decomposition") {
|
|
8763
|
+
return {
|
|
8764
|
+
errorCode: "task_decomposition_required",
|
|
8765
|
+
errorFamily: "validation",
|
|
8766
|
+
...details,
|
|
8767
|
+
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
8768
|
+
};
|
|
8769
|
+
}
|
|
7786
8770
|
if (actionType === "deliverable_verification") {
|
|
7787
8771
|
return {
|
|
7788
8772
|
errorCode: "artifact_verification_failed",
|
|
@@ -7791,6 +8775,14 @@ function classifyRuntimeActionFailure(err) {
|
|
|
7791
8775
|
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
7792
8776
|
};
|
|
7793
8777
|
}
|
|
8778
|
+
if (actionType === "output_channel") {
|
|
8779
|
+
return {
|
|
8780
|
+
errorCode: readString(err?.runtimeActionErrorCode) ?? "runtime_action_validation_failed",
|
|
8781
|
+
errorFamily: "output_contract",
|
|
8782
|
+
...details,
|
|
8783
|
+
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
8784
|
+
};
|
|
8785
|
+
}
|
|
7794
8786
|
if (
|
|
7795
8787
|
actionType === "create_interaction" &&
|
|
7796
8788
|
/create_interaction\.[a-z_]+ action requires payload|create_interaction action requires payload|non-canonical fields/i.test(haystack)
|
|
@@ -7802,7 +8794,10 @@ function classifyRuntimeActionFailure(err) {
|
|
|
7802
8794
|
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
7803
8795
|
};
|
|
7804
8796
|
}
|
|
7805
|
-
if (
|
|
8797
|
+
if (
|
|
8798
|
+
actionType === "upload_artifact" &&
|
|
8799
|
+
/unsupported artifact content type|requires path|file does not exist|path must be inside/i.test(haystack)
|
|
8800
|
+
) {
|
|
7806
8801
|
return {
|
|
7807
8802
|
errorCode: "runtime_action_validation_failed",
|
|
7808
8803
|
errorFamily: "output_contract",
|
|
@@ -7826,6 +8821,31 @@ function classifyRuntimeActionFailure(err) {
|
|
|
7826
8821
|
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
7827
8822
|
};
|
|
7828
8823
|
}
|
|
8824
|
+
if (/runtime_action_postcondition_failed/i.test(haystack)) {
|
|
8825
|
+
return {
|
|
8826
|
+
errorCode: "runtime_action_postcondition_failed",
|
|
8827
|
+
errorFamily: "output_contract",
|
|
8828
|
+
...details,
|
|
8829
|
+
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
8830
|
+
};
|
|
8831
|
+
}
|
|
8832
|
+
if (err?.httpStatus === 401) {
|
|
8833
|
+
const authenticationDiagnostics = runtimeActionDiagnostics(err, { retryable: true });
|
|
8834
|
+
return {
|
|
8835
|
+
errorCode: "runtime_action_authentication_failed",
|
|
8836
|
+
errorFamily: "authentication",
|
|
8837
|
+
...details,
|
|
8838
|
+
...(authenticationDiagnostics ? { runtimeActionDiagnostics: authenticationDiagnostics } : {}),
|
|
8839
|
+
};
|
|
8840
|
+
}
|
|
8841
|
+
if (err?.httpStatus === 403) {
|
|
8842
|
+
return {
|
|
8843
|
+
errorCode: "runtime_action_permission_denied",
|
|
8844
|
+
errorFamily: "permission",
|
|
8845
|
+
...details,
|
|
8846
|
+
...(diagnostics ? { runtimeActionDiagnostics: diagnostics } : {}),
|
|
8847
|
+
};
|
|
8848
|
+
}
|
|
7829
8849
|
if (/approval-gated executable child tasks/i.test(haystack)) {
|
|
7830
8850
|
return {
|
|
7831
8851
|
errorCode: "runtime_action_validation_failed",
|
|
@@ -7885,6 +8905,27 @@ function piOutputUsageMetadataMissing(parsed) {
|
|
|
7885
8905
|
return totalTokens <= 0;
|
|
7886
8906
|
}
|
|
7887
8907
|
|
|
8908
|
+
function runtimeBusinessDisposition(runtimeActions, inferredArtifactUploads) {
|
|
8909
|
+
const results = Array.isArray(runtimeActions?.results)
|
|
8910
|
+
? runtimeActions.results.map(asRecord).filter((result) => !isSkippedRuntimeActionResult(result))
|
|
8911
|
+
: [];
|
|
8912
|
+
const terminalUpdate = results.find((result) =>
|
|
8913
|
+
readString(result.type) === "update_parent" &&
|
|
8914
|
+
["done", "cancelled", "blocked", "in_review"].includes(readString(result.status) ?? ""));
|
|
8915
|
+
const terminalStatus = readString(terminalUpdate?.status);
|
|
8916
|
+
if (["done", "cancelled"].includes(terminalStatus ?? "")) {
|
|
8917
|
+
return { state: "completed", reason: `parent_${terminalStatus}` };
|
|
8918
|
+
}
|
|
8919
|
+
if (terminalStatus === "blocked") return { state: "blocked", reason: "parent_blocked" };
|
|
8920
|
+
if (terminalStatus === "in_review" || results.some((result) => readString(result.type) === "create_interaction")) {
|
|
8921
|
+
return { state: "waiting", reason: "review_or_interaction_path_recorded" };
|
|
8922
|
+
}
|
|
8923
|
+
if (results.length > 0 || readNumber(inferredArtifactUploads?.appliedCount, 0) > 0) {
|
|
8924
|
+
return { state: "needs_followup", reason: "durable_progress_without_terminal_disposition" };
|
|
8925
|
+
}
|
|
8926
|
+
return { state: "missing", reason: "no_durable_action_or_live_path" };
|
|
8927
|
+
}
|
|
8928
|
+
|
|
7888
8929
|
function piCompletionOutputStopped(parsed, execution) {
|
|
7889
8930
|
return execution?.timedOut !== true
|
|
7890
8931
|
&& execution?.signal === "SIGTERM"
|
|
@@ -8122,10 +9163,29 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8122
9163
|
...executorActionSources,
|
|
8123
9164
|
].filter(Boolean).join("\n\n");
|
|
8124
9165
|
const envelope = extractRuntimeActionEnvelope(actionText);
|
|
9166
|
+
if (!envelope && executorKind === "pi" && parsed.runtimeActionMarkerInToolOutput === true) {
|
|
9167
|
+
const err = runtimeActionValidationError(
|
|
9168
|
+
"AMASTER_RUNTIME_ACTIONS must be emitted in a Pi assistant message; a marker was found only in tool output, and tool output is ignored by the runtime action bridge",
|
|
9169
|
+
);
|
|
9170
|
+
err.runtimeActionType = "output_channel";
|
|
9171
|
+
err.runtimeActionReasonHint = "assistant_message_required";
|
|
9172
|
+
throw err;
|
|
9173
|
+
}
|
|
9174
|
+
if (!envelope && hasMalformedRuntimeActionMarkerIntent(actionText)) {
|
|
9175
|
+
const err = runtimeActionValidationError(
|
|
9176
|
+
"AMASTER_RUNTIME_ACTIONS action intent used a non-canonical marker; the marker must be an unformatted bare line",
|
|
9177
|
+
);
|
|
9178
|
+
err.runtimeActionType = "output_channel";
|
|
9179
|
+
err.runtimeActionReasonHint = "bare_marker_line_required";
|
|
9180
|
+
err.runtimeActionErrorCode = "runtime_action_output_marker_invalid";
|
|
9181
|
+
throw err;
|
|
9182
|
+
}
|
|
8125
9183
|
if (!envelope) return null;
|
|
8126
9184
|
const rawActions = Array.isArray(envelope.actions) ? envelope.actions.map(asRecord) : [];
|
|
8127
9185
|
const continuationScope = runtimeActionContinuationScope(command);
|
|
9186
|
+
const authorizationEnvelope = runtimeActionAuthorizationEnvelope(command);
|
|
8128
9187
|
assertRuntimeActionsAllowedByContinuationScope(rawActions, continuationScope);
|
|
9188
|
+
assertExplicitTaskDecomposition(command, rawActions);
|
|
8129
9189
|
if (
|
|
8130
9190
|
continuationScope === "message_only"
|
|
8131
9191
|
&& Array.isArray(envelope.deliverables)
|
|
@@ -8136,6 +9196,10 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8136
9196
|
const runtimeAuth = runtimeActionAuthForCommand(command);
|
|
8137
9197
|
const issueId = readString(runtimeAuth.issueId) ?? commandIssueId(command);
|
|
8138
9198
|
if (!issueId) throw new Error("AMaster runtime action bridge requires a current issue id");
|
|
9199
|
+
const batchFingerprint = `runtime-action-batch:${stableRuntimeActionHash({
|
|
9200
|
+
runId: runtimeActionRunId(runtimeAuth) ?? commandRunId(command),
|
|
9201
|
+
envelope,
|
|
9202
|
+
})}`;
|
|
8139
9203
|
const allowedContentTypes = runtimeAttachmentContentTypes(command);
|
|
8140
9204
|
let deliverableManifest;
|
|
8141
9205
|
try {
|
|
@@ -8167,20 +9231,41 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8167
9231
|
attachRuntimeCheckpointCandidates(err, envelope, cwd);
|
|
8168
9232
|
throw err;
|
|
8169
9233
|
}
|
|
8170
|
-
|
|
9234
|
+
let actions = orderRuntimeActionsForGovernance([...generatedUploadActions, ...rawActions]);
|
|
8171
9235
|
const answeredQuestionContinuation = runtimeActionAnsweredQuestionContinuation(command);
|
|
9236
|
+
const acceptedConfirmationContinuation = runtimeActionAcceptedConfirmationContinuation(command);
|
|
8172
9237
|
try {
|
|
8173
|
-
preflightRuntimeActionBatch(actions, {
|
|
9238
|
+
actions = preflightRuntimeActionBatch(actions, {
|
|
8174
9239
|
answeredQuestionContinuation,
|
|
9240
|
+
acceptedConfirmationContinuation,
|
|
8175
9241
|
allowedContentTypes,
|
|
8176
9242
|
continuationScope,
|
|
9243
|
+
cwd,
|
|
8177
9244
|
});
|
|
9245
|
+
assertRuntimeActionsAllowedByAuthorizationEnvelope(actions, authorizationEnvelope);
|
|
8178
9246
|
} catch (err) {
|
|
9247
|
+
if (err && typeof err === "object" && !err.runtimeActionLedger) {
|
|
9248
|
+
const fieldPath = readString(err.runtimeActionFieldPath);
|
|
9249
|
+
const failedIndex = Number.parseInt(fieldPath?.match(/^actions\[(\d+)]/)?.[1] ?? "0", 10);
|
|
9250
|
+
err.runtimeActionLedger = {
|
|
9251
|
+
batchFingerprint,
|
|
9252
|
+
traceId: batchFingerprint,
|
|
9253
|
+
status: "validation_failed",
|
|
9254
|
+
actions: actions.map((action, index) => runtimeActionLedgerEntry(
|
|
9255
|
+
action,
|
|
9256
|
+
index,
|
|
9257
|
+
index === failedIndex ? "failed" : "pending",
|
|
9258
|
+
null,
|
|
9259
|
+
index === failedIndex ? err : null,
|
|
9260
|
+
)),
|
|
9261
|
+
};
|
|
9262
|
+
}
|
|
8179
9263
|
attachRuntimeCheckpointCandidates(err, envelope, cwd);
|
|
8180
9264
|
throw err;
|
|
8181
9265
|
}
|
|
8182
9266
|
const results = [];
|
|
8183
9267
|
const skipped = [];
|
|
9268
|
+
const actionLedgerEntries = [];
|
|
8184
9269
|
const actionContext = {
|
|
8185
9270
|
rootIssueId: issueId,
|
|
8186
9271
|
cwd,
|
|
@@ -8190,14 +9275,20 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8190
9275
|
createdChildIssueIdByFingerprint: new Map(),
|
|
8191
9276
|
completedChildIssuesByTitle: runtimeActionCompletedChildIssueMap(command),
|
|
8192
9277
|
createdReviewPathForInReview: false,
|
|
8193
|
-
acceptedConfirmationContinuation
|
|
9278
|
+
acceptedConfirmationContinuation,
|
|
8194
9279
|
answeredQuestionContinuation,
|
|
8195
9280
|
finalCommentBodies: [],
|
|
8196
9281
|
};
|
|
8197
|
-
for (const action of actions) {
|
|
9282
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
8198
9283
|
try {
|
|
8199
9284
|
const result = await applyRuntimeAction(config, command, runtimeAuth, issueId, action, actionContext);
|
|
8200
9285
|
results.push(result);
|
|
9286
|
+
actionLedgerEntries.push(runtimeActionLedgerEntry(
|
|
9287
|
+
action,
|
|
9288
|
+
actionIndex,
|
|
9289
|
+
asRecord(result).reusedExisting === true ? "reused" : "applied",
|
|
9290
|
+
result,
|
|
9291
|
+
));
|
|
8201
9292
|
if (isSkippedRuntimeActionResult(result)) {
|
|
8202
9293
|
skipped.push(result);
|
|
8203
9294
|
}
|
|
@@ -8226,6 +9317,17 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8226
9317
|
err.runtimeActionOriginalType = readRuntimeActionOriginalType(action);
|
|
8227
9318
|
err.runtimeActionKind = readString(action.kind) ?? readString(asRecord(action.payload).kind);
|
|
8228
9319
|
err.runtimeActionReason = truncateText(err instanceof Error ? err.message : String(err), 1000);
|
|
9320
|
+
err.runtimeActionLedger = {
|
|
9321
|
+
batchFingerprint,
|
|
9322
|
+
traceId: batchFingerprint,
|
|
9323
|
+
status: "partial_failed",
|
|
9324
|
+
actions: [
|
|
9325
|
+
...actionLedgerEntries,
|
|
9326
|
+
runtimeActionLedgerEntry(action, actionIndex, "failed", null, err),
|
|
9327
|
+
...actions.slice(actionIndex + 1).map((pendingAction, offset) =>
|
|
9328
|
+
runtimeActionLedgerEntry(pendingAction, actionIndex + offset + 1, "pending")),
|
|
9329
|
+
],
|
|
9330
|
+
};
|
|
8229
9331
|
}
|
|
8230
9332
|
attachRuntimeCheckpointCandidates(err, envelope, cwd);
|
|
8231
9333
|
throw err;
|
|
@@ -8233,6 +9335,7 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8233
9335
|
const skippedResult = runtimeActionFailureSummary(action, err);
|
|
8234
9336
|
skipped.push(skippedResult);
|
|
8235
9337
|
results.push(skippedResult);
|
|
9338
|
+
actionLedgerEntries.push(runtimeActionLedgerEntry(action, actionIndex, "skipped", skippedResult, err));
|
|
8236
9339
|
await ingestLog(config, command, "system", "warn", `Skipped AMaster runtime action: ${skippedResult.reason}`, {
|
|
8237
9340
|
runtimeAction: skippedResult,
|
|
8238
9341
|
});
|
|
@@ -8240,8 +9343,19 @@ async function applyRuntimeActionBridge(config, command, parsed, execution, cwd)
|
|
|
8240
9343
|
}
|
|
8241
9344
|
return {
|
|
8242
9345
|
version: readNumber(envelope.version, 1),
|
|
9346
|
+
markerFound: true,
|
|
9347
|
+
jsonParsed: true,
|
|
9348
|
+
actionsValidated: true,
|
|
9349
|
+
postconditionsPassed: true,
|
|
9350
|
+
actionLedger: {
|
|
9351
|
+
batchFingerprint,
|
|
9352
|
+
traceId: batchFingerprint,
|
|
9353
|
+
status: "completed",
|
|
9354
|
+
actions: actionLedgerEntries,
|
|
9355
|
+
},
|
|
8243
9356
|
requestedCount: actions.length,
|
|
8244
9357
|
appliedCount: results.length - skipped.length,
|
|
9358
|
+
actionsApplied: results.length - skipped.length,
|
|
8245
9359
|
skippedCount: skipped.length,
|
|
8246
9360
|
results,
|
|
8247
9361
|
deliverableManifest: {
|
|
@@ -8272,6 +9386,72 @@ async function listExistingIssueWorkProducts(runtimeAuth, issueId) {
|
|
|
8272
9386
|
}
|
|
8273
9387
|
}
|
|
8274
9388
|
|
|
9389
|
+
async function verifyUploadedArtifactPostcondition(runtimeAuth, issueId, expected, fields) {
|
|
9390
|
+
const [workProducts, attachments] = await Promise.all([
|
|
9391
|
+
runtimeApiJson(runtimeAuth, "GET", `/api/issues/${encodeURIComponent(issueId)}/work-products`),
|
|
9392
|
+
runtimeApiJson(runtimeAuth, "GET", `/api/issues/${encodeURIComponent(issueId)}/attachments`),
|
|
9393
|
+
]);
|
|
9394
|
+
const workProduct = Array.isArray(workProducts)
|
|
9395
|
+
? workProducts.map(asRecord).find((entry) => readString(entry.id) === expected.workProductId)
|
|
9396
|
+
: null;
|
|
9397
|
+
const attachment = Array.isArray(attachments)
|
|
9398
|
+
? attachments.map(asRecord).find((entry) => readString(entry.id) === expected.attachmentId)
|
|
9399
|
+
: null;
|
|
9400
|
+
const metadata = asRecord(workProduct?.metadata);
|
|
9401
|
+
const expectedSourceIds = Array.isArray(fields.sourceWorkProductIds)
|
|
9402
|
+
? fields.sourceWorkProductIds.map(readString).filter(Boolean).sort()
|
|
9403
|
+
: [];
|
|
9404
|
+
const persistedSourceIds = Array.isArray(metadata.sourceWorkProductIds)
|
|
9405
|
+
? metadata.sourceWorkProductIds.map(readString).filter(Boolean).sort()
|
|
9406
|
+
: [];
|
|
9407
|
+
const expectedScenarioKeys = Array.isArray(fields.scenarioKeys)
|
|
9408
|
+
? fields.scenarioKeys.map(readString).filter(Boolean).sort()
|
|
9409
|
+
: [];
|
|
9410
|
+
const persistedScenarioKeys = Array.isArray(metadata.scenarioKeys)
|
|
9411
|
+
? metadata.scenarioKeys.map(readString).filter(Boolean).sort()
|
|
9412
|
+
: [];
|
|
9413
|
+
const failures = [
|
|
9414
|
+
!workProduct ? "work product missing" : null,
|
|
9415
|
+
!attachment ? "attachment missing" : null,
|
|
9416
|
+
readString(metadata.attachmentId) !== expected.attachmentId ? "work product attachmentId mismatch" : null,
|
|
9417
|
+
readString(metadata.sha256) !== expected.sha256 ? "work product SHA mismatch" : null,
|
|
9418
|
+
readString(attachment?.sha256) !== expected.sha256 ? "attachment SHA mismatch" : null,
|
|
9419
|
+
readString(metadata.sourceWorkProductId) !== readString(fields.sourceWorkProductId) ? "sourceWorkProductId mismatch" : null,
|
|
9420
|
+
readString(metadata.supersedesWorkProductId) !== readString(fields.supersedesWorkProductId) ? "supersedesWorkProductId mismatch" : null,
|
|
9421
|
+
readString(metadata.evidenceRole) !== readString(fields.evidenceRole) ? "evidenceRole mismatch" : null,
|
|
9422
|
+
JSON.stringify(persistedSourceIds) !== JSON.stringify(expectedSourceIds) ? "sourceWorkProductIds mismatch" : null,
|
|
9423
|
+
JSON.stringify(persistedScenarioKeys) !== JSON.stringify(expectedScenarioKeys) ? "scenarioKeys mismatch" : null,
|
|
9424
|
+
].filter(Boolean);
|
|
9425
|
+
if (failures.length > 0) {
|
|
9426
|
+
throw new Error(`runtime_action_postcondition_failed: artifact upload readback failed: ${failures.join("; ")}`);
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
async function cleanupFailedArtifactUpload(config, command, runtimeAuth, attachmentId, workProductId, cause) {
|
|
9431
|
+
const cleanupFailures = [];
|
|
9432
|
+
if (workProductId) {
|
|
9433
|
+
try {
|
|
9434
|
+
await runtimeApiJson(runtimeAuth, "DELETE", `/api/work-products/${encodeURIComponent(workProductId)}`);
|
|
9435
|
+
} catch (err) {
|
|
9436
|
+
cleanupFailures.push(`workProduct=${err instanceof Error ? err.message : String(err)}`);
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
try {
|
|
9440
|
+
await runtimeApiJson(runtimeAuth, "DELETE", `/api/attachments/${encodeURIComponent(attachmentId)}`);
|
|
9441
|
+
} catch (err) {
|
|
9442
|
+
cleanupFailures.push(`attachment=${err instanceof Error ? err.message : String(err)}`);
|
|
9443
|
+
}
|
|
9444
|
+
await ingestLog(config, command, "system", "error", "Artifact upload postcondition failed", {
|
|
9445
|
+
attachmentId,
|
|
9446
|
+
workProductId: workProductId ?? null,
|
|
9447
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
9448
|
+
cleanupFailures,
|
|
9449
|
+
});
|
|
9450
|
+
if (cleanupFailures.length > 0) {
|
|
9451
|
+
throw new Error(`${cause instanceof Error ? cause.message : String(cause)}; cleanup failed: ${cleanupFailures.join("; ")}`);
|
|
9452
|
+
}
|
|
9453
|
+
}
|
|
9454
|
+
|
|
8275
9455
|
function existingArtifactUploadMatches(artifact, existingWorkProducts) {
|
|
8276
9456
|
const filename = basename(artifact.path);
|
|
8277
9457
|
const byteSize = statSync(artifact.path).size;
|
|
@@ -8297,7 +9477,7 @@ function existingArtifactUploadMatches(artifact, existingWorkProducts) {
|
|
|
8297
9477
|
});
|
|
8298
9478
|
}
|
|
8299
9479
|
|
|
8300
|
-
function existingExplicitArtifactUploadMatch(filePath, title, existingWorkProducts) {
|
|
9480
|
+
function existingExplicitArtifactUploadMatch(filePath, title, existingWorkProducts, fields = {}) {
|
|
8301
9481
|
const filename = basename(filePath);
|
|
8302
9482
|
const byteSize = statSync(filePath).size;
|
|
8303
9483
|
const sha256 = hashFileSha256(filePath);
|
|
@@ -8308,6 +9488,22 @@ function existingExplicitArtifactUploadMatch(filePath, title, existingWorkProduc
|
|
|
8308
9488
|
if (readString(workProduct.provider) !== "paperclip") return false;
|
|
8309
9489
|
if (readString(workProduct.status) === "archived") return false;
|
|
8310
9490
|
const metadata = asRecord(workProduct.metadata);
|
|
9491
|
+
const sourceWorkProductId = readString(fields.sourceWorkProductId);
|
|
9492
|
+
const sourceWorkProductIds = Array.isArray(fields.sourceWorkProductIds)
|
|
9493
|
+
? fields.sourceWorkProductIds.map(readString).filter(Boolean).sort()
|
|
9494
|
+
: [];
|
|
9495
|
+
const supersedesWorkProductId = readString(fields.supersedesWorkProductId);
|
|
9496
|
+
const evidenceRole = readString(fields.evidenceRole);
|
|
9497
|
+
const scenarioKeys = Array.isArray(fields.scenarioKeys) ? fields.scenarioKeys.map(readString).filter(Boolean).sort() : [];
|
|
9498
|
+
if (sourceWorkProductId || supersedesWorkProductId || sourceWorkProductIds.length > 0 || evidenceRole || scenarioKeys.length > 0) {
|
|
9499
|
+
if (readString(metadata.sourceWorkProductId) !== sourceWorkProductId) return false;
|
|
9500
|
+
if (readString(metadata.supersedesWorkProductId) !== supersedesWorkProductId) return false;
|
|
9501
|
+
const existingSourceIds = Array.isArray(metadata.sourceWorkProductIds) ? metadata.sourceWorkProductIds.map(readString).filter(Boolean).sort() : [];
|
|
9502
|
+
const existingScenarioKeys = Array.isArray(metadata.scenarioKeys) ? metadata.scenarioKeys.map(readString).filter(Boolean).sort() : [];
|
|
9503
|
+
if (JSON.stringify(existingSourceIds) !== JSON.stringify(sourceWorkProductIds)) return false;
|
|
9504
|
+
if (readString(metadata.evidenceRole) !== evidenceRole) return false;
|
|
9505
|
+
if (JSON.stringify(existingScenarioKeys) !== JSON.stringify(scenarioKeys)) return false;
|
|
9506
|
+
}
|
|
8311
9507
|
const existingSha = readString(metadata.sha256) ?? readString(workProduct.sha256);
|
|
8312
9508
|
if (existingSha) return existingSha === sha256;
|
|
8313
9509
|
|
|
@@ -8430,15 +9626,19 @@ async function executeRunCommand(config, command) {
|
|
|
8430
9626
|
});
|
|
8431
9627
|
}
|
|
8432
9628
|
await materializeIssueCheckpoint(config, command, workspace);
|
|
8433
|
-
|
|
9629
|
+
const requiredArtifactInputs = await materializeRequiredArtifactInputs(config, command, workspace);
|
|
9630
|
+
let materializedAttachments = requiredArtifactInputs;
|
|
8434
9631
|
try {
|
|
8435
|
-
materializedAttachments = await materializeIssueAttachments(config, command, workspace);
|
|
9632
|
+
materializedAttachments = [...requiredArtifactInputs, ...await materializeIssueAttachments(config, command, workspace)];
|
|
8436
9633
|
} catch (err) {
|
|
8437
9634
|
await ingestLog(config, command, "system", "warn", `Failed to materialize issue attachments: ${err instanceof Error ? err.message : String(err)}`, {
|
|
8438
9635
|
error: err instanceof Error ? err.message : String(err),
|
|
8439
9636
|
});
|
|
8440
9637
|
}
|
|
8441
|
-
const prompt = buildCommandPrompt(command, workspace, materializedAttachments, {
|
|
9638
|
+
const prompt = buildCommandPrompt(command, workspace, materializedAttachments, {
|
|
9639
|
+
executorKind: executor.kind,
|
|
9640
|
+
artifactVerifierCommands: config.artifactVerifierCommands,
|
|
9641
|
+
});
|
|
8442
9642
|
const invocation = buildExecutorInvocation(executor, command, workspace);
|
|
8443
9643
|
const executorEnv = buildExecutorEnv(config, command, workspace);
|
|
8444
9644
|
if (executor.kind === "pi") {
|
|
@@ -8654,6 +9854,7 @@ async function executeRunCommand(config, command) {
|
|
|
8654
9854
|
? "Executor cancelled by AMaster control plane"
|
|
8655
9855
|
: execution.spawnError ?? parsedErrorMessage ?? runtimeActionError ?? inferredArtifactError ?? (succeeded ? null : `Executor exited with code ${execution.exitCode ?? "unknown"}`);
|
|
8656
9856
|
const artifactUploadSummary = runtimeArtifactUploadSummary(runtimeActions, inferredArtifactUploads);
|
|
9857
|
+
const businessDisposition = runtimeBusinessDisposition(runtimeActions, inferredArtifactUploads);
|
|
8657
9858
|
const costUsage = parsedCostUsage(parsed.usage);
|
|
8658
9859
|
const result = {
|
|
8659
9860
|
executorKind: executor.kind,
|
|
@@ -8724,6 +9925,7 @@ async function executeRunCommand(config, command) {
|
|
|
8724
9925
|
...(runtimeActions ? { runtimeActions } : {}),
|
|
8725
9926
|
...(inferredArtifactUploads ? { inferredArtifactUploads } : {}),
|
|
8726
9927
|
...(artifactUploadSummary ? { artifactUploadSummary } : {}),
|
|
9928
|
+
businessDisposition,
|
|
8727
9929
|
};
|
|
8728
9930
|
const completion = await completeCommand(config, command, cancelled ? "cancelled" : succeeded ? "succeeded" : "failed", result, error ?? undefined);
|
|
8729
9931
|
if (!completion) {
|
|
@@ -8918,6 +10120,26 @@ function statusSummary(config) {
|
|
|
8918
10120
|
}), null, 2)}\n`);
|
|
8919
10121
|
}
|
|
8920
10122
|
|
|
10123
|
+
function validateRuntimeActionsFile(config, flags) {
|
|
10124
|
+
const rawFile = readString(flags.file);
|
|
10125
|
+
if (!rawFile) throw new Error("--file is required");
|
|
10126
|
+
const cwd = resolve(readString(flags.cwd) ?? process.cwd());
|
|
10127
|
+
const file = resolve(cwd, rawFile);
|
|
10128
|
+
const allowedContentTypes = splitList(flags["allowed-content-types"]);
|
|
10129
|
+
const envelope = JSON.parse(readFileSync(file, "utf8"));
|
|
10130
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
10131
|
+
throw runtimeActionValidationError("runtime action envelope must be a JSON object");
|
|
10132
|
+
}
|
|
10133
|
+
assertRuntimeActionValid(envelope.version === 1, "runtime action envelope version must be 1");
|
|
10134
|
+
assertRuntimeActionValid(Array.isArray(envelope.actions), "runtime action envelope actions must be an array");
|
|
10135
|
+
assertRuntimeActionValid(envelope.actions.length <= 20, "runtime action envelope actions exceed the limit of 20");
|
|
10136
|
+
|
|
10137
|
+
preflightRuntimeDeliverables(config, envelope, cwd, allowedContentTypes);
|
|
10138
|
+
const actions = orderRuntimeActionsForGovernance(envelope.actions.map(asRecord));
|
|
10139
|
+
preflightRuntimeActionBatch(actions, { cwd, allowedContentTypes });
|
|
10140
|
+
process.stdout.write(`${JSON.stringify({ valid: true, actionCount: actions.length }, null, 2)}\n`);
|
|
10141
|
+
}
|
|
10142
|
+
|
|
8921
10143
|
async function runLoop(config) {
|
|
8922
10144
|
config = await ensureRegistered(config);
|
|
8923
10145
|
let startupOrphanCheckPending = true;
|
|
@@ -8951,6 +10173,7 @@ Commands:
|
|
|
8951
10173
|
run-once Register if needed, heartbeat, poll once, execute leased commands, then exit.
|
|
8952
10174
|
workspace-gc-dry-run Print managed runtime workdir GC candidates without deleting anything.
|
|
8953
10175
|
status-summary Print safe local daemon storage/outbox summary JSON.
|
|
10176
|
+
validate-actions Validate a JSON action envelope with --file, --cwd, and --allowed-content-types.
|
|
8954
10177
|
start | run Register if needed, then heartbeat and poll in a foreground loop.
|
|
8955
10178
|
|
|
8956
10179
|
Key env:
|
|
@@ -9002,6 +10225,9 @@ async function main() {
|
|
|
9002
10225
|
case "status-summary":
|
|
9003
10226
|
statusSummary(config);
|
|
9004
10227
|
break;
|
|
10228
|
+
case "validate-actions":
|
|
10229
|
+
validateRuntimeActionsFile(config, flags);
|
|
10230
|
+
break;
|
|
9005
10231
|
case "start":
|
|
9006
10232
|
case "run":
|
|
9007
10233
|
await runLoop(config);
|