@xdxer/dingtalk-agent 0.1.5-beta.2 → 0.1.5-beta.4

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.en.md +92 -63
  3. package/README.md +92 -63
  4. package/dist/bin/dingtalk-agent.js +142 -15
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-audit.js +69 -55
  7. package/dist/src/agent-audit.js.map +1 -1
  8. package/dist/src/agent-enhance.js +49 -30
  9. package/dist/src/agent-enhance.js.map +1 -1
  10. package/dist/src/agent-platform.js +1 -0
  11. package/dist/src/agent-platform.js.map +1 -1
  12. package/dist/src/bootstrap.js +6 -2
  13. package/dist/src/bootstrap.js.map +1 -1
  14. package/dist/src/development-workspace.js +108 -18
  15. package/dist/src/development-workspace.js.map +1 -1
  16. package/dist/src/multica-deploy.js +128 -18
  17. package/dist/src/multica-deploy.js.map +1 -1
  18. package/dist/src/multica-provider.js +157 -2
  19. package/dist/src/multica-provider.js.map +1 -1
  20. package/dist/src/opencode-provider.js +17 -5
  21. package/dist/src/opencode-provider.js.map +1 -1
  22. package/dist/src/schedule-plan.js +105 -0
  23. package/dist/src/schedule-plan.js.map +1 -0
  24. package/dist/src/skills.js +2 -0
  25. package/dist/src/skills.js.map +1 -1
  26. package/dist/src/workspace.js +11 -6
  27. package/dist/src/workspace.js.map +1 -1
  28. package/docs/ARCHITECTURE.md +142 -24
  29. package/docs/INSTALLATION.md +1 -1
  30. package/docs/PRIOR-ART.md +4 -0
  31. package/docs/SELF-TEST.md +1 -1
  32. package/docs/architecture/agent-platform-connection-layer.svg +120 -0
  33. package/docs/architecture/digital-employee-composition.svg +92 -0
  34. package/docs/architecture/dingtalk-agent-architecture.svg +125 -0
  35. package/docs/assets/digital-employee-at-work.svg +77 -0
  36. package/docs/schemas/multica-workspace-run.schema.json +150 -33
  37. package/docs/schemas/project.schema.json +27 -0
  38. package/docs/schemas/workspace-scaffold.schema.json +37 -0
  39. package/examples/agents/README.md +9 -7
  40. package/examples/agents/fde-coach/AGENTS.md +2 -34
  41. package/examples/agents/fde-coach/agent/AGENTS.md +35 -0
  42. package/examples/agents/fde-coach/agent.bindings.json +10 -0
  43. package/examples/agents/release-manager/AGENTS.md +2 -34
  44. package/examples/agents/release-manager/agent/AGENTS.md +35 -0
  45. package/examples/agents/release-manager/agent.bindings.json +10 -0
  46. package/lab/project-workspace/fake-multica-provider.mjs +43 -8
  47. package/lab/project-workspace/opencode-provider-suite.json +1 -1
  48. package/lab/robot-eval/suite.json +1 -1
  49. package/package.json +2 -2
  50. package/skills/core/dingtalk-agent-compose/SKILL.md +25 -18
  51. package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
  52. package/skills/core/dingtalk-agent-compose/assets/REPOSITORY.template.md +10 -0
  53. package/skills/core/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +2 -2
  54. package/skills/core/dingtalk-agent-compose/assets/agent.bindings.local.template.json +2 -2
  55. package/skills/core/dingtalk-agent-compose/assets/hosts/opencode/opencode.template.json +2 -1
  56. package/skills/core/dingtalk-agent-compose/evals/evals.json +1 -1
  57. package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +6 -6
  58. package/skills/core/dingtalk-agent-compose/references/drive-and-schedules.md +124 -0
  59. package/skills/core/dingtalk-agent-compose/references/host-loading-contract.md +10 -12
  60. package/skills/core/dingtalk-agent-compose/references/hosts/claude-code.md +13 -12
  61. package/skills/core/dingtalk-agent-compose/references/hosts/opencode.md +13 -12
  62. package/skills/core/dingtalk-basic-behavior/SKILL.md +11 -7
  63. package/skills/core/dingtalk-basic-behavior/references/event-to-behavior.md +15 -1
  64. package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +5 -0
  65. package/skills/core/dingtalk-basic-behavior/references/risk-authority-and-privacy.md +12 -0
  66. package/skills/platforms/deap/PLATFORM.md +30 -1
  67. package/skills/platforms/multica-dingtalk/PLATFORM.md +25 -0
  68. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +3 -2
  69. package/skills/platforms/multica-dingtalk/multica-external/SKILL.md +15 -0
  70. package/docs/assets/agent-delivery-lifecycle.svg +0 -103
  71. /package/examples/agents/fde-coach/{skills → agent/skills}/fde-coach/SKILL.md +0 -0
  72. /package/examples/agents/release-manager/{skills → agent/skills}/release-manager/SKILL.md +0 -0
@@ -3,7 +3,7 @@ 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
8
  import { MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
9
9
  export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
@@ -63,7 +63,11 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
63
63
  throw new Error(`Multica workspace run 前置条件不完整: ${plan.blocking.join(', ')}`);
64
64
  }
65
65
  const command = options.command || env.DTA_MULTICA_BIN || 'multica';
66
- const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
66
+ // A real Issue-channel run does agent work plus issue get/comment/status side-tasks and
67
+ // routinely needs minutes; the old 30s default timed out on almost every honest run. Wait
68
+ // 4 minutes by default (still capped at 5). A run that overruns is resumable via
69
+ // `workspace run --status --run-id` against the same Issue — no duplicate Issue.
70
+ const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
67
71
  const calls = [];
68
72
  const inspection = inspectMulticaWorkspace(info.root, name, {
69
73
  execute: true, yes: true, persist: false, command, env,
@@ -86,6 +90,18 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
86
90
  '--description', normalizedPrompt, '--assignee-id', plan.agentId, '--output', 'json',
87
91
  ]), 'workspace-run.issue.create', 'write', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue');
88
92
  const issueId = resourceId(issue.id, 'workspace run issue.id');
93
+ return readbackWorkspaceRun({
94
+ root: info.root, name, plan, issueId, runId, command, env, timeoutMs, calls, out: options.out,
95
+ });
96
+ }
97
+ /**
98
+ * Poll an existing Issue's assignee-agent task to a terminal state, then reconstruct the
99
+ * run report from its trace. Shared by the initial `workspace run` (right after it creates the
100
+ * Issue) and `workspace run --status --run-id` (resume against the SAME Issue, no new write) so
101
+ * a timed-out run reconciles instead of spawning a duplicate Issue.
102
+ */
103
+ function readbackWorkspaceRun(ctx) {
104
+ const { root, name, plan, issueId, runId, command, env, timeoutMs, calls } = ctx;
89
105
  const started = Date.now();
90
106
  const pollIntervalMs = 2_000;
91
107
  const pollCalls = [];
@@ -95,7 +111,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
95
111
  pollAttempts += 1;
96
112
  const runs = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
97
113
  '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');
114
+ ]), 'workspace-run.issue.runs', 'read', root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
99
115
  task = runs.filter((item) => !optionalString(item.agent_id) ||
100
116
  optionalString(item.agent_id) === plan.agentId)
101
117
  .sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
@@ -111,7 +127,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
111
127
  if (taskId && ['completed', 'failed', 'cancelled'].includes(taskStatus)) {
112
128
  messages = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
113
129
  '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');
130
+ ]), 'workspace-run.issue.messages', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
115
131
  }
116
132
  const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
117
133
  const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
@@ -125,7 +141,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
125
141
  const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
126
142
  const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
127
143
  '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');
144
+ ]), 'workspace-run.issue.readback', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
129
145
  const issueStatus = optionalString(issueReadback.status);
130
146
  const failures = [];
131
147
  if (!task)
@@ -165,17 +181,88 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
165
181
  calls,
166
182
  evidencePath: '',
167
183
  remoteRead: true,
168
- remoteWrite: true,
184
+ remoteWrite: calls.some((call) => call.kind === 'write'),
169
185
  triggerWrite: false,
170
186
  sideEffect: 'multica-issue-read-write',
171
187
  dingtalkSideEffect: false,
172
188
  };
173
189
  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));
190
+ report.evidencePath = relative(root, safeProjectPath(root, ctx.out || defaultEvidence, false));
175
191
  assertReportRedacted(report);
176
- atomicJson(safeProjectPath(info.root, report.evidencePath, false), report);
192
+ atomicJson(safeProjectPath(root, report.evidencePath, false), report);
177
193
  return report;
178
194
  }
195
+ /**
196
+ * Resume a `workspace run` by re-reading the SAME Issue's task instead of creating a new one.
197
+ * Loads the persisted run evidence by runId, confirms the workspace scope still resolves to the
198
+ * same target (fail closed on drift), then polls that Issue's assignee-agent task to a terminal
199
+ * state and rewrites the run report. Read-only against Multica — never enqueues a second Issue.
200
+ */
201
+ export function statusMulticaWorkspaceRun(start, name, options = {}) {
202
+ const env = options.env || process.env;
203
+ const cliVersion = options.cliVersion || '';
204
+ const runId = String(options.runId || '').trim();
205
+ if (!/^multica_workspace_run_[0-9]+_[a-f0-9]{8}$/.test(runId)) {
206
+ throw new Error('workspace run --status 需要一个合法的 --run-id(来自上次 run 的 runId)');
207
+ }
208
+ if (!options.execute) {
209
+ throw new Error('workspace run --status 会向 Multica 独立回读同一 Issue;必须传 --execute');
210
+ }
211
+ if (!options.yes)
212
+ throw new Error('workspace run --status 远端回读必须同时传 --execute --yes');
213
+ const info = inspectAgentProject(start, cliVersion);
214
+ requireMulticaWorkspace(info.root, name, cliVersion);
215
+ // The original run persists evidence to `--out` when given, else the default runId path. Resume
216
+ // must look where the run actually wrote, so honor the same `--out` (and rewrite back to it).
217
+ const evidenceRel = options.out || join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
218
+ const evidenceAbs = safeProjectPath(info.root, evidenceRel, false);
219
+ if (!existsSync(evidenceAbs)) {
220
+ throw new Error(`找不到 run 证据 ${evidenceRel};--run-id 必须是本 Workspace 已有的 runId` +
221
+ `${options.out ? '' : '(若原 run 用了 --out,请对 --status 传相同 --out)'}`);
222
+ }
223
+ const prior = JSON.parse(readFileSync(evidenceAbs, 'utf8'));
224
+ if (prior.$schema !== 'dingtalk-agent/multica-workspace-run@1' ||
225
+ prior.provider !== 'multica' || prior.workspace !== name || !prior.issueId) {
226
+ throw new Error('run 证据不是本 Workspace 的 multica run,或缺少 issueId;已 fail closed');
227
+ }
228
+ // The evidence file must belong to THIS runId. Two runs sharing one --out overwrite each
229
+ // other, so resuming the first runId against that path would otherwise read the second run's
230
+ // Issue while reporting the first runId — cross-wiring answer and run. Fail closed on mismatch.
231
+ if (prior.runId !== runId) {
232
+ throw new Error(`run 证据 ${evidenceRel} 属于 runId ${prior.runId},与 --run-id ${runId} 不一致;已 fail closed` +
233
+ `(同一 --out 被后一次 run 覆盖时会这样)`);
234
+ }
235
+ // Re-resolve scope offline and refuse unless the WHOLE frozen target still matches: an env
236
+ // pointing at a different runtime/agent/profile (or an invalid ref surfacing in blocking) must
237
+ // not let the resume mis-record the old task or overwrite the frozen values. workspaceId alone
238
+ // is not enough.
239
+ const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
240
+ const scopeDrift = providerPlan.blocking.length ? ['blocking:' + providerPlan.blocking.join(',')] : [];
241
+ if (providerPlan.profile !== prior.profile)
242
+ scopeDrift.push('profile');
243
+ if (providerPlan.expected.workspaceId !== prior.workspaceId)
244
+ scopeDrift.push('workspace');
245
+ if (providerPlan.expected.runtimeId !== prior.runtimeId)
246
+ scopeDrift.push('runtime');
247
+ if (providerPlan.expected.agentId !== prior.agentId)
248
+ scopeDrift.push('agent');
249
+ if (scopeDrift.length) {
250
+ throw new Error(`workspace run --status: 当前 workspace scope 与 run 证据不一致 [${scopeDrift.join(', ')}];已 fail closed`);
251
+ }
252
+ const command = options.command || env.DTA_MULTICA_BIN || 'multica';
253
+ const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
254
+ const plan = {
255
+ profile: providerPlan.profile,
256
+ workspaceId: providerPlan.expected.workspaceId,
257
+ runtimeId: providerPlan.expected.runtimeId,
258
+ agentId: providerPlan.expected.agentId,
259
+ promptHash: prior.promptHash,
260
+ };
261
+ return readbackWorkspaceRun({
262
+ root: info.root, name, plan, issueId: prior.issueId, runId,
263
+ command, env, timeoutMs, calls: [], out: options.out,
264
+ });
265
+ }
179
266
  export function planMulticaDeployment(start, name, options = {}) {
180
267
  const action = options.action || 'apply';
181
268
  const env = options.env || process.env;
@@ -299,7 +386,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
299
386
  receipt = retireDeployment(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls);
300
387
  }
301
388
  else {
302
- receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 60_000), Boolean(options.noWait), calls, rollback, temp);
389
+ receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 300_000), Boolean(options.noWait), calls, rollback, temp);
303
390
  }
304
391
  }
305
392
  catch (error) {
@@ -499,8 +586,7 @@ function loadDeploymentSource(root) {
499
586
  const definitionHash = sha256(definition);
500
587
  const skillRoots = [];
501
588
  for (const name of info.project.skills) {
502
- const ref = name === 'dingtalk-basic-behavior'
503
- ? `.agents/skills/${name}` : `skills/${name}`;
589
+ const ref = projectSkillRef(info.project, name);
504
590
  skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
505
591
  }
506
592
  if (skillRoots.length > MAX_DEPLOY_SKILLS) {
@@ -711,7 +797,7 @@ function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs
711
797
  `required_skills=${JSON.stringify(requiredSkills)}`,
712
798
  'This is a deployment control-plane verification task.',
713
799
  'Use the Host native Skill tool exactly once for every required skill above, in the listed order.',
714
- 'Do not call DWS, Shell, network, or write tools.',
800
+ '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.',
715
801
  `Return exactly this JSON and nothing else: ${expectedResponse}`,
716
802
  ].join('\n'),
717
803
  '--assignee-id', agentId, '--output', 'json',
@@ -778,7 +864,12 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
778
864
  const responseWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
779
865
  .map((item) => smokeToolInput(item)).filter((input) => safeSmokeResponsePath(optionalString(input.filePath)))
780
866
  .map((input) => optionalString(input.content)).filter(Boolean);
781
- const responseEvidence = [...texts, ...responseWrites].find(matchesResponse) || '';
867
+ const responseShells = toolUses.filter((item) => optionalString(item.tool) === 'bash')
868
+ .map((item) => optionalString(smokeToolInput(item).command))
869
+ .filter((command) => safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
870
+ .map(() => JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills }));
871
+ const responseEvidence = [...texts, ...responseWrites, ...responseShells]
872
+ .find(matchesResponse) || '';
782
873
  const responsePassed = Boolean(responseEvidence);
783
874
  const toolTracePassed = toolUses.every((item) => allowedSmokeToolUse(item, issueId, marker, requiredSkills)) && stableStringify(skillNames) === stableStringify(requiredSkills);
784
875
  const failures = [];
@@ -836,26 +927,45 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
836
927
  const cleanupFirstPrefix = 'rm ';
837
928
  const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
838
929
  if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
839
- return safeSmokeResponsePath(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
930
+ return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
840
931
  }
841
932
  const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
842
933
  if (command.startsWith(statusFirstPrefix)) {
843
- return safeSmokeResponsePath(command.slice(statusFirstPrefix.length));
934
+ return safeSmokeCleanup(command.slice(statusFirstPrefix.length));
844
935
  }
845
936
  if (command.startsWith(cleanupFirstPrefix)) {
846
- return safeSmokeResponsePath(command.slice(cleanupFirstPrefix.length));
937
+ return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length));
847
938
  }
939
+ if (safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
940
+ return true;
848
941
  return false;
849
942
  }
943
+ function safeCombinedSmokeResponse(command, issueId, marker, requiredSkills) {
944
+ const response = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills });
945
+ return command === [
946
+ "cat > ./reply.md << 'REPLY_EOF'",
947
+ response,
948
+ 'REPLY_EOF',
949
+ `multica issue comment add ${issueId} --content-file ./reply.md && rm ./reply.md`,
950
+ ].join('\n');
951
+ }
850
952
  function safeSmokeResponsePath(value) {
851
- const names = new Set(['reply.md', 'result.json']);
953
+ const names = new Set(['reply.md', 'result.md', 'result.json']);
852
954
  if (value.startsWith('./'))
853
955
  return names.has(value.slice(2));
854
- if (!value.startsWith('/') || !names.has(value.split('/').at(-1) || ''))
956
+ // 裸文件名与 `./` 前缀等价:Agent workdir 里写绝对路径、随后用裸名引用同一个文件是
957
+ // 平台的常见写法。只放行精确 basename —— `foo/result.md` 与 `../result.md` 仍然落到
958
+ // 下面的绝对路径分支并被拒绝,白名单没有变宽。
959
+ if (!value.startsWith('/'))
960
+ return names.has(value);
961
+ if (!names.has(value.split('/').at(-1) || ''))
855
962
  return false;
856
963
  return value.split('/').every((part) => !part ||
857
964
  (part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
858
965
  }
966
+ function safeSmokeCleanup(value) {
967
+ return safeSmokeResponsePath(value.startsWith('-f ') ? value.slice(3) : value);
968
+ }
859
969
  function rollbackConfirmed(root, plan, command, env, timeoutMs, calls, rollback, temp) {
860
970
  const report = emptyRollback(true);
861
971
  const attempt = (id, argv) => {