@xdxer/dingtalk-agent 0.1.5-beta.1 → 0.1.5-beta.2
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/CHANGELOG.md +32 -0
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/dist/bin/dingtalk-agent.js +16 -5
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/agent-platform.js +0 -1
- package/dist/src/agent-platform.js.map +1 -1
- package/dist/src/development-workspace.js +25 -8
- package/dist/src/development-workspace.js.map +1 -1
- package/dist/src/doctor.js +6 -1
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/multica-deploy.js +265 -50
- package/dist/src/multica-deploy.js.map +1 -1
- package/dist/src/multica-provider.js +1 -2
- package/dist/src/multica-provider.js.map +1 -1
- package/dist/src/opencode-provider.js +3 -1
- package/dist/src/opencode-provider.js.map +1 -1
- package/dist/src/skill-manager.js +2 -3
- package/dist/src/skill-manager.js.map +1 -1
- package/docs/ARCHITECTURE.md +12 -0
- package/docs/schemas/multica-deployment-receipt.schema.json +2 -2
- package/docs/schemas/multica-workspace-run-plan.schema.json +31 -0
- package/docs/schemas/multica-workspace-run.schema.json +44 -0
- package/docs/schemas/project.schema.json +10 -2
- package/lab/project-workspace/fake-multica-provider.mjs +39 -10
- package/lab/project-workspace/multica-readonly.fixture.json +0 -12
- package/lab/project-workspace/project.fixture.json +0 -4
- package/lab/robot-eval/suite.json +1 -1
- package/package.json +1 -2
- package/skills/README.md +0 -1
- package/skills/core/dingtalk-agent-compose/SKILL.md +5 -5
- package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
- package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +1 -1
- package/skills/core/dingtalk-agent-eval/SKILL.md +1 -1
- package/skills/core/dingtalk-agent-eval/references/interactive-debug-channels.md +9 -3
- package/skills/core/dingtalk-basic-behavior/SKILL.md +15 -3
- package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +54 -0
- package/skills/core/dingtalk-basic-behavior/references/truth-and-recovery.md +4 -2
- package/skills/platforms/multica-dingtalk/PLATFORM.md +6 -5
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +4 -4
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/multica-deployment-contract.md +14 -6
- package/skills/platforms/multica-dingtalk/dingtalk-agent-boot-multica/SKILL.md +0 -40
|
@@ -5,9 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { WORKSPACE_STATE_ROOT, inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
7
7
|
import { digest, stableStringify } from './events.js';
|
|
8
|
-
import {
|
|
9
|
-
import { resolvePackageRoot } from './package-root.js';
|
|
10
|
-
import { bundledSkillPath } from './skill-manager.js';
|
|
8
|
+
import { MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
|
|
11
9
|
export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
|
|
12
10
|
const MAX_DEPLOY_SKILLS = 32;
|
|
13
11
|
const MAX_SKILL_SUPPORTING_FILES = 128;
|
|
@@ -21,6 +19,163 @@ class DeploymentFailure extends Error {
|
|
|
21
19
|
this.ambiguous = ambiguous;
|
|
22
20
|
}
|
|
23
21
|
}
|
|
22
|
+
export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
23
|
+
const normalizedPrompt = prompt.trim();
|
|
24
|
+
if (!normalizedPrompt)
|
|
25
|
+
throw new Error('workspace run prompt 不能为空');
|
|
26
|
+
if (Buffer.byteLength(normalizedPrompt, 'utf8') > 64 * 1024) {
|
|
27
|
+
throw new Error('workspace run prompt 不能超过 65536 bytes');
|
|
28
|
+
}
|
|
29
|
+
rejectCredentialMaterial(normalizedPrompt, 'workspace run prompt');
|
|
30
|
+
const env = options.env || process.env;
|
|
31
|
+
const cliVersion = options.cliVersion || '';
|
|
32
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
33
|
+
requireMulticaWorkspace(info.root, name, cliVersion);
|
|
34
|
+
const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
35
|
+
const blocking = [...providerPlan.blocking];
|
|
36
|
+
if (!providerPlan.expected.agentId)
|
|
37
|
+
blocking.push('agent-id.missing');
|
|
38
|
+
const plan = {
|
|
39
|
+
$schema: 'dingtalk-agent/multica-workspace-run-plan@1',
|
|
40
|
+
workspace: name,
|
|
41
|
+
provider: 'multica',
|
|
42
|
+
profile: providerPlan.profile,
|
|
43
|
+
workspaceId: providerPlan.expected.workspaceId,
|
|
44
|
+
runtimeId: providerPlan.expected.runtimeId,
|
|
45
|
+
agentId: providerPlan.expected.agentId,
|
|
46
|
+
promptHash: sha256(normalizedPrompt),
|
|
47
|
+
operations: ['scope.inspect', 'issue.create', 'issue.runs', 'issue.run-messages', 'evidence.persist'],
|
|
48
|
+
blocking: [...new Set(blocking)].sort(),
|
|
49
|
+
requiresExecute: true,
|
|
50
|
+
requiresExplicitYes: true,
|
|
51
|
+
remoteRead: false,
|
|
52
|
+
remoteWrite: false,
|
|
53
|
+
triggerWrite: false,
|
|
54
|
+
sideEffect: false,
|
|
55
|
+
dingtalkSideEffect: false,
|
|
56
|
+
};
|
|
57
|
+
if (!options.execute)
|
|
58
|
+
return plan;
|
|
59
|
+
if (!options.yes) {
|
|
60
|
+
throw new Error('Multica workspace run 会创建远端 Issue;必须同时传 --execute --yes');
|
|
61
|
+
}
|
|
62
|
+
if (plan.blocking.length) {
|
|
63
|
+
throw new Error(`Multica workspace run 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
66
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
|
|
67
|
+
const calls = [];
|
|
68
|
+
const inspection = inspectMulticaWorkspace(info.root, name, {
|
|
69
|
+
execute: true, yes: true, persist: false, command, env,
|
|
70
|
+
timeoutMs: Math.min(timeoutMs, 30_000), cliVersion,
|
|
71
|
+
});
|
|
72
|
+
appendInspectionCalls(inspection, 'run.preflight', calls);
|
|
73
|
+
const assignedSkills = inspection.resources.assignedSkills
|
|
74
|
+
.filter((item) => item.enabled).map((item) => item.name).sort();
|
|
75
|
+
const expectedSkills = [...providerPlan.expected.skills].sort();
|
|
76
|
+
if (!inspection.passed || inspection.resources.workspace?.id !== plan.workspaceId ||
|
|
77
|
+
inspection.resources.runtime?.id !== plan.runtimeId ||
|
|
78
|
+
inspection.resources.agent?.id !== plan.agentId ||
|
|
79
|
+
stableStringify(assignedSkills) !== stableStringify(expectedSkills)) {
|
|
80
|
+
throw new Error('Multica workspace run scope 或 Skill assignment 与 Project 不一致;未创建 Issue');
|
|
81
|
+
}
|
|
82
|
+
const runId = `multica_workspace_run_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
83
|
+
const marker = `DTA-WORKSPACE-RUN-${randomUUID()}`;
|
|
84
|
+
const issue = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
85
|
+
'issue', 'create', '--title', `[DTA workspace run] ${marker}`,
|
|
86
|
+
'--description', normalizedPrompt, '--assignee-id', plan.agentId, '--output', 'json',
|
|
87
|
+
]), 'workspace-run.issue.create', 'write', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue');
|
|
88
|
+
const issueId = resourceId(issue.id, 'workspace run issue.id');
|
|
89
|
+
const started = Date.now();
|
|
90
|
+
const pollIntervalMs = 2_000;
|
|
91
|
+
const pollCalls = [];
|
|
92
|
+
let pollAttempts = 0;
|
|
93
|
+
let task;
|
|
94
|
+
while (Date.now() - started < timeoutMs) {
|
|
95
|
+
pollAttempts += 1;
|
|
96
|
+
const runs = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
97
|
+
'issue', 'runs', issueId, '--full-id', '--output', 'json',
|
|
98
|
+
]), 'workspace-run.issue.runs', 'read', info.root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
|
|
99
|
+
task = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
100
|
+
optionalString(item.agent_id) === plan.agentId)
|
|
101
|
+
.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
102
|
+
const status = optionalString(task?.status);
|
|
103
|
+
if (['completed', 'failed', 'cancelled'].includes(status))
|
|
104
|
+
break;
|
|
105
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollIntervalMs);
|
|
106
|
+
}
|
|
107
|
+
calls.push(...uniqueCalls(pollCalls));
|
|
108
|
+
const taskId = task?.id ? resourceId(task.id, 'workspace run task.id') : '';
|
|
109
|
+
const taskStatus = optionalString(task?.status) || 'not-created';
|
|
110
|
+
let messages = [];
|
|
111
|
+
if (taskId && ['completed', 'failed', 'cancelled'].includes(taskStatus)) {
|
|
112
|
+
messages = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
113
|
+
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
114
|
+
]), 'workspace-run.issue.messages', 'read', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
|
|
115
|
+
}
|
|
116
|
+
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
117
|
+
const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
|
|
118
|
+
const loadedSkills = toolUses.filter((item) => optionalString(item.tool) === 'skill')
|
|
119
|
+
.map((item) => optionalString(smokeToolInput(item).name)).filter(Boolean);
|
|
120
|
+
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
121
|
+
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
122
|
+
const replyWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
|
|
123
|
+
.map((item) => smokeToolInput(item)).filter((input) => optionalString(input.filePath).endsWith('/reply.md'))
|
|
124
|
+
.map((input) => optionalString(input.content)).filter(Boolean);
|
|
125
|
+
const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
|
|
126
|
+
const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
127
|
+
'issue', 'get', issueId, '--output', 'json',
|
|
128
|
+
]), 'workspace-run.issue.readback', 'read', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
|
|
129
|
+
const issueStatus = optionalString(issueReadback.status);
|
|
130
|
+
const failures = [];
|
|
131
|
+
if (!task)
|
|
132
|
+
failures.push('task.not-created');
|
|
133
|
+
else if (!['completed', 'failed', 'cancelled'].includes(taskStatus))
|
|
134
|
+
failures.push('task.timeout');
|
|
135
|
+
else if (taskStatus !== 'completed')
|
|
136
|
+
failures.push(`task.${taskStatus}`);
|
|
137
|
+
if (!answer)
|
|
138
|
+
failures.push('answer.missing');
|
|
139
|
+
const taskDiagnostic = task
|
|
140
|
+
? remoteTaskDiagnostic(task)
|
|
141
|
+
: `未发现关联 task;Issue 状态=${issueStatus || 'unknown'},已轮询 ${pollAttempts} 次`;
|
|
142
|
+
const report = {
|
|
143
|
+
$schema: 'dingtalk-agent/multica-workspace-run@1',
|
|
144
|
+
passed: failures.length === 0,
|
|
145
|
+
runId,
|
|
146
|
+
workspace: name,
|
|
147
|
+
provider: 'multica',
|
|
148
|
+
profile: plan.profile,
|
|
149
|
+
workspaceId: plan.workspaceId,
|
|
150
|
+
runtimeId: plan.runtimeId,
|
|
151
|
+
agentId: plan.agentId,
|
|
152
|
+
promptHash: plan.promptHash,
|
|
153
|
+
issueId,
|
|
154
|
+
issueStatus,
|
|
155
|
+
taskId,
|
|
156
|
+
taskStatus,
|
|
157
|
+
taskDiagnostic,
|
|
158
|
+
pollAttempts,
|
|
159
|
+
pollIntervalMs,
|
|
160
|
+
answer,
|
|
161
|
+
answerHash: sha256(answer),
|
|
162
|
+
toolNames,
|
|
163
|
+
loadedSkills,
|
|
164
|
+
failures,
|
|
165
|
+
calls,
|
|
166
|
+
evidencePath: '',
|
|
167
|
+
remoteRead: true,
|
|
168
|
+
remoteWrite: true,
|
|
169
|
+
triggerWrite: false,
|
|
170
|
+
sideEffect: 'multica-issue-read-write',
|
|
171
|
+
dingtalkSideEffect: false,
|
|
172
|
+
};
|
|
173
|
+
const defaultEvidence = join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
|
|
174
|
+
report.evidencePath = relative(info.root, safeProjectPath(info.root, options.out || defaultEvidence, false));
|
|
175
|
+
assertReportRedacted(report);
|
|
176
|
+
atomicJson(safeProjectPath(info.root, report.evidencePath, false), report);
|
|
177
|
+
return report;
|
|
178
|
+
}
|
|
24
179
|
export function planMulticaDeployment(start, name, options = {}) {
|
|
25
180
|
const action = options.action || 'apply';
|
|
26
181
|
const env = options.env || process.env;
|
|
@@ -53,12 +208,12 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
53
208
|
: [
|
|
54
209
|
'scope.reinspect', 'skills.readback', 'skills.reconcile', 'agent.reconcile',
|
|
55
210
|
'skills.assign-exact', 'provider.readback', 'load-smoke.create',
|
|
56
|
-
'load-smoke.readback', 'receipt.persist',
|
|
211
|
+
'load-smoke.recover-if-unstarted', 'load-smoke.readback', 'receipt.persist',
|
|
57
212
|
];
|
|
58
213
|
const writeBudget = action === 'retire'
|
|
59
214
|
? { forward: 1, rollback: 0 }
|
|
60
215
|
: {
|
|
61
|
-
forward: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) +
|
|
216
|
+
forward: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 5,
|
|
62
217
|
rollback: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 3,
|
|
63
218
|
};
|
|
64
219
|
const planId = `multica_deploy_plan_${digest(stableStringify({
|
|
@@ -348,11 +503,6 @@ function loadDeploymentSource(root) {
|
|
|
348
503
|
? `.agents/skills/${name}` : `skills/${name}`;
|
|
349
504
|
skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
|
|
350
505
|
}
|
|
351
|
-
const packageRoot = resolvePackageRoot(import.meta.url);
|
|
352
|
-
skillRoots.push({
|
|
353
|
-
name: MULTICA_BOOT_SKILL,
|
|
354
|
-
root: safeExternalSkillRoot(bundledSkillPath(packageRoot, MULTICA_BOOT_SKILL)),
|
|
355
|
-
});
|
|
356
506
|
if (skillRoots.length > MAX_DEPLOY_SKILLS) {
|
|
357
507
|
throw new Error(`Multica deploy 最多支持 ${MAX_DEPLOY_SKILLS} 个受管 Skills`);
|
|
358
508
|
}
|
|
@@ -361,26 +511,17 @@ function loadDeploymentSource(root) {
|
|
|
361
511
|
if (new Set(skills.map((item) => item.name)).size !== skills.length) {
|
|
362
512
|
throw new Error('Multica deploy required Skills 不能重名');
|
|
363
513
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
`deployment_sha256=${deploymentSourceHash}`,
|
|
370
|
-
`required_skills=${JSON.stringify(required)}`,
|
|
371
|
-
'Before every task, use dingtalk-agent-boot-multica and follow its startup order.',
|
|
372
|
-
'Never create a robot, webhook, autopilot, schedule, or other Trigger as part of deployment.',
|
|
373
|
-
'<!-- /DTA Multica Boot -->',
|
|
374
|
-
'',
|
|
375
|
-
definition.trimEnd(),
|
|
376
|
-
'',
|
|
377
|
-
].join('\n');
|
|
514
|
+
// Agent instructions are the human-authored Definition, byte for byte. Deployment
|
|
515
|
+
// hashes and the managed Skill set belong to the control plane and its Receipt;
|
|
516
|
+
// injecting them into the System Prompt makes the role unreadable and creates a
|
|
517
|
+
// second runtime protocol beside the Host's native Skill assignment.
|
|
518
|
+
const instructions = definition;
|
|
378
519
|
const instructionsHash = sha256(instructions);
|
|
379
520
|
const deploymentHash = digest(stableStringify({
|
|
380
521
|
definitionHash, instructionsHash,
|
|
381
522
|
skills: skills.map((item) => ({ name: item.name, hash: item.hash })),
|
|
382
523
|
}));
|
|
383
|
-
return {
|
|
524
|
+
return { definitionHash, instructions, instructionsHash, skills, deploymentHash };
|
|
384
525
|
}
|
|
385
526
|
function loadSkillBundle(name, root) {
|
|
386
527
|
const actual = realpathSync(root);
|
|
@@ -560,24 +701,51 @@ function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, t
|
|
|
560
701
|
function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
561
702
|
const marker = `DTA-MULTICA-LOAD-${randomUUID()}`;
|
|
562
703
|
const requiredSkills = source.skills.map((item) => item.name).sort();
|
|
704
|
+
const expectedResponse = JSON.stringify({
|
|
705
|
+
schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills,
|
|
706
|
+
});
|
|
563
707
|
const issue = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
564
708
|
'issue', 'create', '--title', `[DTA load smoke] ${marker}`,
|
|
565
709
|
'--description', [
|
|
566
710
|
'DTA_MULTICA_LOAD_SMOKE@1', `marker=${marker}`,
|
|
567
|
-
|
|
711
|
+
`required_skills=${JSON.stringify(requiredSkills)}`,
|
|
712
|
+
'This is a deployment control-plane verification task.',
|
|
713
|
+
'Use the Host native Skill tool exactly once for every required skill above, in the listed order.',
|
|
568
714
|
'Do not call DWS, Shell, network, or write tools.',
|
|
715
|
+
`Return exactly this JSON and nothing else: ${expectedResponse}`,
|
|
569
716
|
].join('\n'),
|
|
570
717
|
'--assignee-id', agentId, '--output', 'json',
|
|
571
718
|
]), 'smoke.issue.create', 'write', root, env, timeoutMs, calls).stdout, 'smoke issue');
|
|
572
719
|
const issueId = resourceId(issue.id, 'smoke issue.id');
|
|
720
|
+
let smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
721
|
+
// Multica persists the Issue before attempting the initial agent enqueue. If
|
|
722
|
+
// that enqueue loses a transient readiness race, the API still returns a
|
|
723
|
+
// valid assigned todo Issue but issue runs remains empty forever. Recover
|
|
724
|
+
// exactly that observable state once through an explicit ID-bound mention on
|
|
725
|
+
// the same Issue. A visible task — queued, running or terminal — is never
|
|
726
|
+
// duplicated. The mention route is used instead of issue rerun because it is
|
|
727
|
+
// the platform's normal first-run trigger and does not require a prior task.
|
|
728
|
+
if (smoke.status === 'pending' && !smoke.taskId) {
|
|
729
|
+
runProvider(command, scopedArgs(plan, [
|
|
730
|
+
'issue', 'comment', 'add', issueId,
|
|
731
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Execute the deployment verification exactly as specified in this Issue description.`,
|
|
732
|
+
'--output', 'json',
|
|
733
|
+
]), 'smoke.issue.recover', 'write', root, env, timeoutMs, calls);
|
|
734
|
+
smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
735
|
+
}
|
|
573
736
|
if (noWait)
|
|
574
|
-
return
|
|
737
|
+
return smoke;
|
|
575
738
|
const started = Date.now();
|
|
576
|
-
let smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
577
739
|
while (smoke.status === 'pending' && Date.now() - started < smokeTimeoutMs) {
|
|
578
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
740
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
579
741
|
smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
580
742
|
}
|
|
743
|
+
if (smoke.status === 'pending' && !smoke.taskDiagnostic) {
|
|
744
|
+
const detail = smoke.taskId
|
|
745
|
+
? `task ${smoke.taskId} 在 ${smokeTimeoutMs}ms 内未进入终态(当前 ${smoke.taskStatus || 'unknown'})`
|
|
746
|
+
: `Issue ${issueId} 在 ${smokeTimeoutMs}ms 内未创建关联 task`;
|
|
747
|
+
return { ...smoke, taskDiagnostic: detail };
|
|
748
|
+
}
|
|
581
749
|
return smoke;
|
|
582
750
|
}
|
|
583
751
|
function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls) {
|
|
@@ -607,10 +775,10 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
|
|
|
607
775
|
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
608
776
|
const finalText = texts.at(-1) || '';
|
|
609
777
|
const matchesResponse = (value) => smokeResponseMatches(value, marker, requiredSkills);
|
|
610
|
-
const
|
|
611
|
-
.map((item) => smokeToolInput(item)).filter((input) => optionalString(input.filePath)
|
|
778
|
+
const responseWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
|
|
779
|
+
.map((item) => smokeToolInput(item)).filter((input) => safeSmokeResponsePath(optionalString(input.filePath)))
|
|
612
780
|
.map((input) => optionalString(input.content)).filter(Boolean);
|
|
613
|
-
const responseEvidence = [...texts, ...
|
|
781
|
+
const responseEvidence = [...texts, ...responseWrites].find(matchesResponse) || '';
|
|
614
782
|
const responsePassed = Boolean(responseEvidence);
|
|
615
783
|
const toolTracePassed = toolUses.every((item) => allowedSmokeToolUse(item, issueId, marker, requiredSkills)) && stableStringify(skillNames) === stableStringify(requiredSkills);
|
|
616
784
|
const failures = [];
|
|
@@ -621,8 +789,8 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
|
|
|
621
789
|
if (!responsePassed)
|
|
622
790
|
failures.push('smoke.response');
|
|
623
791
|
return {
|
|
624
|
-
status: failures.length ? 'failed' : 'passed',
|
|
625
|
-
|
|
792
|
+
status: failures.length ? 'failed' : 'passed', marker, markerHash: sha256(marker),
|
|
793
|
+
issueId, taskId, taskStatus, taskDiagnostic: remoteTaskDiagnostic(run), requiredSkills, loadedSkills,
|
|
626
794
|
toolTracePassed, responsePassed, responseHash: sha256(responseEvidence || finalText), failures,
|
|
627
795
|
};
|
|
628
796
|
}
|
|
@@ -647,7 +815,7 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
|
647
815
|
return true;
|
|
648
816
|
const input = smokeToolInput(item);
|
|
649
817
|
if (tool === 'write') {
|
|
650
|
-
return
|
|
818
|
+
return safeSmokeResponsePath(optionalString(input.filePath)) &&
|
|
651
819
|
smokeResponseMatches(optionalString(input.content), marker, requiredSkills);
|
|
652
820
|
}
|
|
653
821
|
if (tool !== 'bash')
|
|
@@ -663,26 +831,27 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
|
663
831
|
return true;
|
|
664
832
|
const commentPrefix = `multica issue comment add ${issueId} --content-file `;
|
|
665
833
|
if (command.startsWith(commentPrefix)) {
|
|
666
|
-
return
|
|
834
|
+
return safeSmokeResponsePath(command.slice(commentPrefix.length));
|
|
667
835
|
}
|
|
668
836
|
const cleanupFirstPrefix = 'rm ';
|
|
669
837
|
const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
|
|
670
838
|
if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
|
|
671
|
-
return
|
|
839
|
+
return safeSmokeResponsePath(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
|
|
672
840
|
}
|
|
673
841
|
const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
|
|
674
842
|
if (command.startsWith(statusFirstPrefix)) {
|
|
675
|
-
return
|
|
843
|
+
return safeSmokeResponsePath(command.slice(statusFirstPrefix.length));
|
|
676
844
|
}
|
|
677
845
|
if (command.startsWith(cleanupFirstPrefix)) {
|
|
678
|
-
return
|
|
846
|
+
return safeSmokeResponsePath(command.slice(cleanupFirstPrefix.length));
|
|
679
847
|
}
|
|
680
848
|
return false;
|
|
681
849
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
850
|
+
function safeSmokeResponsePath(value) {
|
|
851
|
+
const names = new Set(['reply.md', 'result.json']);
|
|
852
|
+
if (value.startsWith('./'))
|
|
853
|
+
return names.has(value.slice(2));
|
|
854
|
+
if (!value.startsWith('/') || !names.has(value.split('/').at(-1) || ''))
|
|
686
855
|
return false;
|
|
687
856
|
return value.split('/').every((part) => !part ||
|
|
688
857
|
(part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
|
|
@@ -1356,7 +1525,7 @@ function assertReportRedacted(value) {
|
|
|
1356
1525
|
}
|
|
1357
1526
|
function emptySmoke(status, requiredSkills) {
|
|
1358
1527
|
return {
|
|
1359
|
-
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '',
|
|
1528
|
+
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '', taskDiagnostic: '',
|
|
1360
1529
|
requiredSkills: [...requiredSkills].sort(), loadedSkills: [],
|
|
1361
1530
|
toolTracePassed: false, responsePassed: false, responseHash: '', failures: [],
|
|
1362
1531
|
};
|
|
@@ -1432,6 +1601,58 @@ function stringField(value, label) {
|
|
|
1432
1601
|
function optionalString(value) {
|
|
1433
1602
|
return typeof value === 'string' ? value : '';
|
|
1434
1603
|
}
|
|
1604
|
+
function safeRemoteOutput(value, maxBytes) {
|
|
1605
|
+
const redacted = value
|
|
1606
|
+
.replace(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g, '[redacted-private-key]')
|
|
1607
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{20,}/gi, 'Bearer [redacted]')
|
|
1608
|
+
.replace(/\b(?:mul_|mcn_|sk-)[A-Za-z0-9._~+\/-]{12,}/gi, '[redacted-token]');
|
|
1609
|
+
const bytes = Buffer.from(redacted, 'utf8');
|
|
1610
|
+
return bytes.length <= maxBytes ? redacted : `${bytes.subarray(0, maxBytes).toString('utf8')}…`;
|
|
1611
|
+
}
|
|
1612
|
+
function uniqueCalls(calls) {
|
|
1613
|
+
const seen = new Set();
|
|
1614
|
+
return calls.filter((call) => {
|
|
1615
|
+
const key = stableStringify({
|
|
1616
|
+
id: call.id,
|
|
1617
|
+
kind: call.kind,
|
|
1618
|
+
argv: call.argv,
|
|
1619
|
+
exitCode: call.exitCode,
|
|
1620
|
+
stdoutHash: call.stdoutHash,
|
|
1621
|
+
stderrHash: call.stderrHash,
|
|
1622
|
+
passed: call.passed,
|
|
1623
|
+
ambiguous: call.ambiguous,
|
|
1624
|
+
});
|
|
1625
|
+
if (seen.has(key))
|
|
1626
|
+
return false;
|
|
1627
|
+
seen.add(key);
|
|
1628
|
+
return true;
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
function remoteTaskDiagnostic(task) {
|
|
1632
|
+
if (!task)
|
|
1633
|
+
return '';
|
|
1634
|
+
const values = [];
|
|
1635
|
+
const add = (label, value) => {
|
|
1636
|
+
if (typeof value === 'string' && value.trim())
|
|
1637
|
+
values.push(`${label}=${value.trim()}`);
|
|
1638
|
+
else if (typeof value === 'number' || typeof value === 'boolean')
|
|
1639
|
+
values.push(`${label}=${value}`);
|
|
1640
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1641
|
+
const nested = value;
|
|
1642
|
+
for (const key of ['code', 'category', 'message', 'reason', 'detail']) {
|
|
1643
|
+
if (nested[key] !== undefined)
|
|
1644
|
+
add(`${label}.${key}`, nested[key]);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
for (const key of [
|
|
1649
|
+
'error', 'error_message', 'failure_reason', 'status_message', 'reason', 'message', 'detail',
|
|
1650
|
+
]) {
|
|
1651
|
+
if (task[key] !== undefined)
|
|
1652
|
+
add(key, task[key]);
|
|
1653
|
+
}
|
|
1654
|
+
return safeRemoteOutput(values.join('; '), 2_000);
|
|
1655
|
+
}
|
|
1435
1656
|
function resourceId(value, label) {
|
|
1436
1657
|
const item = stringField(value, label);
|
|
1437
1658
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(item) ||
|
|
@@ -1454,12 +1675,6 @@ function requireMulticaWorkspace(start, name, cliVersion = '') {
|
|
|
1454
1675
|
}
|
|
1455
1676
|
return workspace;
|
|
1456
1677
|
}
|
|
1457
|
-
function safeExternalSkillRoot(path) {
|
|
1458
|
-
if (!existsSync(path) || lstatSync(path).isSymbolicLink()) {
|
|
1459
|
-
throw new Error(`Canonical Multica Boot Skill 不存在或是符号链接: ${path}`);
|
|
1460
|
-
}
|
|
1461
|
-
return realpathSync(path);
|
|
1462
|
-
}
|
|
1463
1678
|
function safeProjectPath(root, ref, mustExist) {
|
|
1464
1679
|
const rootReal = realpathSync(resolve(root));
|
|
1465
1680
|
const candidate = resolve(rootReal, ref);
|