@xdxer/dingtalk-agent 0.1.5-beta.1 → 0.1.5-beta.3
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 +47 -0
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/dist/bin/dingtalk-agent.js +16 -5
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/agent-audit.js +69 -55
- package/dist/src/agent-audit.js.map +1 -1
- package/dist/src/agent-enhance.js +49 -30
- package/dist/src/agent-enhance.js.map +1 -1
- package/dist/src/agent-platform.js +0 -1
- package/dist/src/agent-platform.js.map +1 -1
- package/dist/src/bootstrap.js +6 -2
- package/dist/src/bootstrap.js.map +1 -1
- package/dist/src/development-workspace.js +67 -23
- 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 +287 -54
- 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 +20 -6
- 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/dist/src/skills.js +2 -0
- package/dist/src/skills.js.map +1 -1
- package/dist/src/workspace.js +11 -6
- package/dist/src/workspace.js.map +1 -1
- package/docs/ARCHITECTURE.md +22 -7
- package/docs/INSTALLATION.md +1 -1
- 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 +15 -2
- package/examples/agents/README.md +8 -6
- package/examples/agents/fde-coach/AGENTS.md +2 -34
- package/examples/agents/fde-coach/agent/AGENTS.md +35 -0
- package/examples/agents/fde-coach/agent.bindings.json +10 -0
- package/examples/agents/release-manager/AGENTS.md +2 -34
- package/examples/agents/release-manager/agent/AGENTS.md +35 -0
- package/examples/agents/release-manager/agent.bindings.json +10 -0
- package/lab/project-workspace/fake-multica-provider.mjs +62 -14
- 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 +23 -17
- package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
- package/skills/core/dingtalk-agent-compose/assets/REPOSITORY.template.md +10 -0
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +2 -2
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.local.template.json +2 -2
- package/skills/core/dingtalk-agent-compose/assets/hosts/opencode/opencode.template.json +2 -1
- package/skills/core/dingtalk-agent-compose/evals/evals.json +1 -1
- package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +7 -7
- package/skills/core/dingtalk-agent-compose/references/host-loading-contract.md +10 -12
- package/skills/core/dingtalk-agent-compose/references/hosts/claude-code.md +13 -12
- package/skills/core/dingtalk-agent-compose/references/hosts/opencode.md +13 -12
- 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 +18 -3
- package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +56 -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
- /package/examples/agents/fde-coach/{skills → agent/skills}/fde-coach/SKILL.md +0 -0
- /package/examples/agents/release-manager/{skills → agent/skills}/release-manager/SKILL.md +0 -0
|
@@ -3,11 +3,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
|
-
import { WORKSPACE_STATE_ROOT, inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
6
|
+
import { WORKSPACE_STATE_ROOT, inspectAgentProject, projectSkillRef, 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({
|
|
@@ -344,15 +499,9 @@ function loadDeploymentSource(root) {
|
|
|
344
499
|
const definitionHash = sha256(definition);
|
|
345
500
|
const skillRoots = [];
|
|
346
501
|
for (const name of info.project.skills) {
|
|
347
|
-
const ref = name
|
|
348
|
-
? `.agents/skills/${name}` : `skills/${name}`;
|
|
502
|
+
const ref = projectSkillRef(info.project, name);
|
|
349
503
|
skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
|
|
350
504
|
}
|
|
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
505
|
if (skillRoots.length > MAX_DEPLOY_SKILLS) {
|
|
357
506
|
throw new Error(`Multica deploy 最多支持 ${MAX_DEPLOY_SKILLS} 个受管 Skills`);
|
|
358
507
|
}
|
|
@@ -361,26 +510,17 @@ function loadDeploymentSource(root) {
|
|
|
361
510
|
if (new Set(skills.map((item) => item.name)).size !== skills.length) {
|
|
362
511
|
throw new Error('Multica deploy required Skills 不能重名');
|
|
363
512
|
}
|
|
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');
|
|
513
|
+
// Agent instructions are the human-authored Definition, byte for byte. Deployment
|
|
514
|
+
// hashes and the managed Skill set belong to the control plane and its Receipt;
|
|
515
|
+
// injecting them into the System Prompt makes the role unreadable and creates a
|
|
516
|
+
// second runtime protocol beside the Host's native Skill assignment.
|
|
517
|
+
const instructions = definition;
|
|
378
518
|
const instructionsHash = sha256(instructions);
|
|
379
519
|
const deploymentHash = digest(stableStringify({
|
|
380
520
|
definitionHash, instructionsHash,
|
|
381
521
|
skills: skills.map((item) => ({ name: item.name, hash: item.hash })),
|
|
382
522
|
}));
|
|
383
|
-
return {
|
|
523
|
+
return { definitionHash, instructions, instructionsHash, skills, deploymentHash };
|
|
384
524
|
}
|
|
385
525
|
function loadSkillBundle(name, root) {
|
|
386
526
|
const actual = realpathSync(root);
|
|
@@ -560,24 +700,51 @@ function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, t
|
|
|
560
700
|
function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
561
701
|
const marker = `DTA-MULTICA-LOAD-${randomUUID()}`;
|
|
562
702
|
const requiredSkills = source.skills.map((item) => item.name).sort();
|
|
703
|
+
const expectedResponse = JSON.stringify({
|
|
704
|
+
schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills,
|
|
705
|
+
});
|
|
563
706
|
const issue = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
564
707
|
'issue', 'create', '--title', `[DTA load smoke] ${marker}`,
|
|
565
708
|
'--description', [
|
|
566
709
|
'DTA_MULTICA_LOAD_SMOKE@1', `marker=${marker}`,
|
|
567
|
-
|
|
568
|
-
'
|
|
710
|
+
`required_skills=${JSON.stringify(requiredSkills)}`,
|
|
711
|
+
'This is a deployment control-plane verification task.',
|
|
712
|
+
'Use the Host native Skill tool exactly once for every required skill above, in the listed order.',
|
|
713
|
+
'Do not call DWS, network, or business tools. Only the Host Issue control-plane calls needed to read this Issue and persist the exact result are exempt.',
|
|
714
|
+
`Return exactly this JSON and nothing else: ${expectedResponse}`,
|
|
569
715
|
].join('\n'),
|
|
570
716
|
'--assignee-id', agentId, '--output', 'json',
|
|
571
717
|
]), 'smoke.issue.create', 'write', root, env, timeoutMs, calls).stdout, 'smoke issue');
|
|
572
718
|
const issueId = resourceId(issue.id, 'smoke issue.id');
|
|
719
|
+
let smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
720
|
+
// Multica persists the Issue before attempting the initial agent enqueue. If
|
|
721
|
+
// that enqueue loses a transient readiness race, the API still returns a
|
|
722
|
+
// valid assigned todo Issue but issue runs remains empty forever. Recover
|
|
723
|
+
// exactly that observable state once through an explicit ID-bound mention on
|
|
724
|
+
// the same Issue. A visible task — queued, running or terminal — is never
|
|
725
|
+
// duplicated. The mention route is used instead of issue rerun because it is
|
|
726
|
+
// the platform's normal first-run trigger and does not require a prior task.
|
|
727
|
+
if (smoke.status === 'pending' && !smoke.taskId) {
|
|
728
|
+
runProvider(command, scopedArgs(plan, [
|
|
729
|
+
'issue', 'comment', 'add', issueId,
|
|
730
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Execute the deployment verification exactly as specified in this Issue description.`,
|
|
731
|
+
'--output', 'json',
|
|
732
|
+
]), 'smoke.issue.recover', 'write', root, env, timeoutMs, calls);
|
|
733
|
+
smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
734
|
+
}
|
|
573
735
|
if (noWait)
|
|
574
|
-
return
|
|
736
|
+
return smoke;
|
|
575
737
|
const started = Date.now();
|
|
576
|
-
let smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
577
738
|
while (smoke.status === 'pending' && Date.now() - started < smokeTimeoutMs) {
|
|
578
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
739
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
579
740
|
smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
580
741
|
}
|
|
742
|
+
if (smoke.status === 'pending' && !smoke.taskDiagnostic) {
|
|
743
|
+
const detail = smoke.taskId
|
|
744
|
+
? `task ${smoke.taskId} 在 ${smokeTimeoutMs}ms 内未进入终态(当前 ${smoke.taskStatus || 'unknown'})`
|
|
745
|
+
: `Issue ${issueId} 在 ${smokeTimeoutMs}ms 内未创建关联 task`;
|
|
746
|
+
return { ...smoke, taskDiagnostic: detail };
|
|
747
|
+
}
|
|
581
748
|
return smoke;
|
|
582
749
|
}
|
|
583
750
|
function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls) {
|
|
@@ -607,10 +774,15 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
|
|
|
607
774
|
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
608
775
|
const finalText = texts.at(-1) || '';
|
|
609
776
|
const matchesResponse = (value) => smokeResponseMatches(value, marker, requiredSkills);
|
|
610
|
-
const
|
|
611
|
-
.map((item) => smokeToolInput(item)).filter((input) => optionalString(input.filePath)
|
|
777
|
+
const responseWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
|
|
778
|
+
.map((item) => smokeToolInput(item)).filter((input) => safeSmokeResponsePath(optionalString(input.filePath)))
|
|
612
779
|
.map((input) => optionalString(input.content)).filter(Boolean);
|
|
613
|
-
const
|
|
780
|
+
const responseShells = toolUses.filter((item) => optionalString(item.tool) === 'bash')
|
|
781
|
+
.map((item) => optionalString(smokeToolInput(item).command))
|
|
782
|
+
.filter((command) => safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
|
|
783
|
+
.map(() => JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills }));
|
|
784
|
+
const responseEvidence = [...texts, ...responseWrites, ...responseShells]
|
|
785
|
+
.find(matchesResponse) || '';
|
|
614
786
|
const responsePassed = Boolean(responseEvidence);
|
|
615
787
|
const toolTracePassed = toolUses.every((item) => allowedSmokeToolUse(item, issueId, marker, requiredSkills)) && stableStringify(skillNames) === stableStringify(requiredSkills);
|
|
616
788
|
const failures = [];
|
|
@@ -621,8 +793,8 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
|
|
|
621
793
|
if (!responsePassed)
|
|
622
794
|
failures.push('smoke.response');
|
|
623
795
|
return {
|
|
624
|
-
status: failures.length ? 'failed' : 'passed',
|
|
625
|
-
|
|
796
|
+
status: failures.length ? 'failed' : 'passed', marker, markerHash: sha256(marker),
|
|
797
|
+
issueId, taskId, taskStatus, taskDiagnostic: remoteTaskDiagnostic(run), requiredSkills, loadedSkills,
|
|
626
798
|
toolTracePassed, responsePassed, responseHash: sha256(responseEvidence || finalText), failures,
|
|
627
799
|
};
|
|
628
800
|
}
|
|
@@ -647,7 +819,7 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
|
647
819
|
return true;
|
|
648
820
|
const input = smokeToolInput(item);
|
|
649
821
|
if (tool === 'write') {
|
|
650
|
-
return
|
|
822
|
+
return safeSmokeResponsePath(optionalString(input.filePath)) &&
|
|
651
823
|
smokeResponseMatches(optionalString(input.content), marker, requiredSkills);
|
|
652
824
|
}
|
|
653
825
|
if (tool !== 'bash')
|
|
@@ -663,30 +835,45 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
|
663
835
|
return true;
|
|
664
836
|
const commentPrefix = `multica issue comment add ${issueId} --content-file `;
|
|
665
837
|
if (command.startsWith(commentPrefix)) {
|
|
666
|
-
return
|
|
838
|
+
return safeSmokeResponsePath(command.slice(commentPrefix.length));
|
|
667
839
|
}
|
|
668
840
|
const cleanupFirstPrefix = 'rm ';
|
|
669
841
|
const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
|
|
670
842
|
if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
|
|
671
|
-
return
|
|
843
|
+
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
|
|
672
844
|
}
|
|
673
845
|
const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
|
|
674
846
|
if (command.startsWith(statusFirstPrefix)) {
|
|
675
|
-
return
|
|
847
|
+
return safeSmokeCleanup(command.slice(statusFirstPrefix.length));
|
|
676
848
|
}
|
|
677
849
|
if (command.startsWith(cleanupFirstPrefix)) {
|
|
678
|
-
return
|
|
850
|
+
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length));
|
|
679
851
|
}
|
|
852
|
+
if (safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
|
|
853
|
+
return true;
|
|
680
854
|
return false;
|
|
681
855
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
856
|
+
function safeCombinedSmokeResponse(command, issueId, marker, requiredSkills) {
|
|
857
|
+
const response = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills });
|
|
858
|
+
return command === [
|
|
859
|
+
"cat > ./reply.md << 'REPLY_EOF'",
|
|
860
|
+
response,
|
|
861
|
+
'REPLY_EOF',
|
|
862
|
+
`multica issue comment add ${issueId} --content-file ./reply.md && rm ./reply.md`,
|
|
863
|
+
].join('\n');
|
|
864
|
+
}
|
|
865
|
+
function safeSmokeResponsePath(value) {
|
|
866
|
+
const names = new Set(['reply.md', 'result.md', 'result.json']);
|
|
867
|
+
if (value.startsWith('./'))
|
|
868
|
+
return names.has(value.slice(2));
|
|
869
|
+
if (!value.startsWith('/') || !names.has(value.split('/').at(-1) || ''))
|
|
686
870
|
return false;
|
|
687
871
|
return value.split('/').every((part) => !part ||
|
|
688
872
|
(part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
|
|
689
873
|
}
|
|
874
|
+
function safeSmokeCleanup(value) {
|
|
875
|
+
return safeSmokeResponsePath(value.startsWith('-f ') ? value.slice(3) : value);
|
|
876
|
+
}
|
|
690
877
|
function rollbackConfirmed(root, plan, command, env, timeoutMs, calls, rollback, temp) {
|
|
691
878
|
const report = emptyRollback(true);
|
|
692
879
|
const attempt = (id, argv) => {
|
|
@@ -1356,7 +1543,7 @@ function assertReportRedacted(value) {
|
|
|
1356
1543
|
}
|
|
1357
1544
|
function emptySmoke(status, requiredSkills) {
|
|
1358
1545
|
return {
|
|
1359
|
-
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '',
|
|
1546
|
+
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '', taskDiagnostic: '',
|
|
1360
1547
|
requiredSkills: [...requiredSkills].sort(), loadedSkills: [],
|
|
1361
1548
|
toolTracePassed: false, responsePassed: false, responseHash: '', failures: [],
|
|
1362
1549
|
};
|
|
@@ -1432,6 +1619,58 @@ function stringField(value, label) {
|
|
|
1432
1619
|
function optionalString(value) {
|
|
1433
1620
|
return typeof value === 'string' ? value : '';
|
|
1434
1621
|
}
|
|
1622
|
+
function safeRemoteOutput(value, maxBytes) {
|
|
1623
|
+
const redacted = value
|
|
1624
|
+
.replace(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g, '[redacted-private-key]')
|
|
1625
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{20,}/gi, 'Bearer [redacted]')
|
|
1626
|
+
.replace(/\b(?:mul_|mcn_|sk-)[A-Za-z0-9._~+\/-]{12,}/gi, '[redacted-token]');
|
|
1627
|
+
const bytes = Buffer.from(redacted, 'utf8');
|
|
1628
|
+
return bytes.length <= maxBytes ? redacted : `${bytes.subarray(0, maxBytes).toString('utf8')}…`;
|
|
1629
|
+
}
|
|
1630
|
+
function uniqueCalls(calls) {
|
|
1631
|
+
const seen = new Set();
|
|
1632
|
+
return calls.filter((call) => {
|
|
1633
|
+
const key = stableStringify({
|
|
1634
|
+
id: call.id,
|
|
1635
|
+
kind: call.kind,
|
|
1636
|
+
argv: call.argv,
|
|
1637
|
+
exitCode: call.exitCode,
|
|
1638
|
+
stdoutHash: call.stdoutHash,
|
|
1639
|
+
stderrHash: call.stderrHash,
|
|
1640
|
+
passed: call.passed,
|
|
1641
|
+
ambiguous: call.ambiguous,
|
|
1642
|
+
});
|
|
1643
|
+
if (seen.has(key))
|
|
1644
|
+
return false;
|
|
1645
|
+
seen.add(key);
|
|
1646
|
+
return true;
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
function remoteTaskDiagnostic(task) {
|
|
1650
|
+
if (!task)
|
|
1651
|
+
return '';
|
|
1652
|
+
const values = [];
|
|
1653
|
+
const add = (label, value) => {
|
|
1654
|
+
if (typeof value === 'string' && value.trim())
|
|
1655
|
+
values.push(`${label}=${value.trim()}`);
|
|
1656
|
+
else if (typeof value === 'number' || typeof value === 'boolean')
|
|
1657
|
+
values.push(`${label}=${value}`);
|
|
1658
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1659
|
+
const nested = value;
|
|
1660
|
+
for (const key of ['code', 'category', 'message', 'reason', 'detail']) {
|
|
1661
|
+
if (nested[key] !== undefined)
|
|
1662
|
+
add(`${label}.${key}`, nested[key]);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
for (const key of [
|
|
1667
|
+
'error', 'error_message', 'failure_reason', 'status_message', 'reason', 'message', 'detail',
|
|
1668
|
+
]) {
|
|
1669
|
+
if (task[key] !== undefined)
|
|
1670
|
+
add(key, task[key]);
|
|
1671
|
+
}
|
|
1672
|
+
return safeRemoteOutput(values.join('; '), 2_000);
|
|
1673
|
+
}
|
|
1435
1674
|
function resourceId(value, label) {
|
|
1436
1675
|
const item = stringField(value, label);
|
|
1437
1676
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(item) ||
|
|
@@ -1454,12 +1693,6 @@ function requireMulticaWorkspace(start, name, cliVersion = '') {
|
|
|
1454
1693
|
}
|
|
1455
1694
|
return workspace;
|
|
1456
1695
|
}
|
|
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
1696
|
function safeProjectPath(root, ref, mustExist) {
|
|
1464
1697
|
const rootReal = realpathSync(resolve(root));
|
|
1465
1698
|
const candidate = resolve(rootReal, ref);
|