@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.en.md +10 -5
  3. package/README.md +10 -5
  4. package/dist/bin/dingtalk-agent.js +16 -5
  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 +0 -1
  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 +67 -23
  15. package/dist/src/development-workspace.js.map +1 -1
  16. package/dist/src/doctor.js +6 -1
  17. package/dist/src/doctor.js.map +1 -1
  18. package/dist/src/multica-deploy.js +287 -54
  19. package/dist/src/multica-deploy.js.map +1 -1
  20. package/dist/src/multica-provider.js +1 -2
  21. package/dist/src/multica-provider.js.map +1 -1
  22. package/dist/src/opencode-provider.js +20 -6
  23. package/dist/src/opencode-provider.js.map +1 -1
  24. package/dist/src/skill-manager.js +2 -3
  25. package/dist/src/skill-manager.js.map +1 -1
  26. package/dist/src/skills.js +2 -0
  27. package/dist/src/skills.js.map +1 -1
  28. package/dist/src/workspace.js +11 -6
  29. package/dist/src/workspace.js.map +1 -1
  30. package/docs/ARCHITECTURE.md +22 -7
  31. package/docs/INSTALLATION.md +1 -1
  32. package/docs/schemas/multica-deployment-receipt.schema.json +2 -2
  33. package/docs/schemas/multica-workspace-run-plan.schema.json +31 -0
  34. package/docs/schemas/multica-workspace-run.schema.json +44 -0
  35. package/docs/schemas/project.schema.json +15 -2
  36. package/examples/agents/README.md +8 -6
  37. package/examples/agents/fde-coach/AGENTS.md +2 -34
  38. package/examples/agents/fde-coach/agent/AGENTS.md +35 -0
  39. package/examples/agents/fde-coach/agent.bindings.json +10 -0
  40. package/examples/agents/release-manager/AGENTS.md +2 -34
  41. package/examples/agents/release-manager/agent/AGENTS.md +35 -0
  42. package/examples/agents/release-manager/agent.bindings.json +10 -0
  43. package/lab/project-workspace/fake-multica-provider.mjs +62 -14
  44. package/lab/project-workspace/multica-readonly.fixture.json +0 -12
  45. package/lab/project-workspace/project.fixture.json +0 -4
  46. package/lab/robot-eval/suite.json +1 -1
  47. package/package.json +1 -2
  48. package/skills/README.md +0 -1
  49. package/skills/core/dingtalk-agent-compose/SKILL.md +23 -17
  50. package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
  51. package/skills/core/dingtalk-agent-compose/assets/REPOSITORY.template.md +10 -0
  52. package/skills/core/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +2 -2
  53. package/skills/core/dingtalk-agent-compose/assets/agent.bindings.local.template.json +2 -2
  54. package/skills/core/dingtalk-agent-compose/assets/hosts/opencode/opencode.template.json +2 -1
  55. package/skills/core/dingtalk-agent-compose/evals/evals.json +1 -1
  56. package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +7 -7
  57. package/skills/core/dingtalk-agent-compose/references/host-loading-contract.md +10 -12
  58. package/skills/core/dingtalk-agent-compose/references/hosts/claude-code.md +13 -12
  59. package/skills/core/dingtalk-agent-compose/references/hosts/opencode.md +13 -12
  60. package/skills/core/dingtalk-agent-eval/SKILL.md +1 -1
  61. package/skills/core/dingtalk-agent-eval/references/interactive-debug-channels.md +9 -3
  62. package/skills/core/dingtalk-basic-behavior/SKILL.md +18 -3
  63. package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +56 -0
  64. package/skills/core/dingtalk-basic-behavior/references/truth-and-recovery.md +4 -2
  65. package/skills/platforms/multica-dingtalk/PLATFORM.md +6 -5
  66. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +4 -4
  67. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/multica-deployment-contract.md +14 -6
  68. package/skills/platforms/multica-dingtalk/dingtalk-agent-boot-multica/SKILL.md +0 -40
  69. /package/examples/agents/fde-coach/{skills → agent/skills}/fde-coach/SKILL.md +0 -0
  70. /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 { MULTICA_BOOT_SKILL, MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
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) + 4,
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 === 'dingtalk-basic-behavior'
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
- const required = skills.map((item) => ({ name: item.name, sha256: item.hash }));
365
- const deploymentSourceHash = digest(stableStringify({ definitionHash, required }));
366
- const instructions = [
367
- '<!-- DTA Multica Boot: managed; do not edit remotely -->',
368
- `definition_sha256=${definitionHash}`,
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 { definition, definitionHash, instructions, instructionsHash, skills, deploymentHash };
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
- 'Use only the trusted DTA Multica Boot required_skills list.',
568
- 'Do not call DWS, Shell, network, or write tools.',
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 readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
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, 250);
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 replyWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
611
- .map((item) => smokeToolInput(item)).filter((input) => optionalString(input.filePath).endsWith('/reply.md'))
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 responseEvidence = [...texts, ...replyWrites].find(matchesResponse) || '';
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
- marker, markerHash: sha256(marker), issueId, taskId, taskStatus, requiredSkills, loadedSkills,
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 safeSmokeReplyPath(optionalString(input.filePath)) &&
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 safeSmokeReplyPath(command.slice(commentPrefix.length));
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 safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
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 safeSmokeReplyPath(command.slice(statusFirstPrefix.length));
847
+ return safeSmokeCleanup(command.slice(statusFirstPrefix.length));
676
848
  }
677
849
  if (command.startsWith(cleanupFirstPrefix)) {
678
- return safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length));
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 safeSmokeReplyPath(value) {
683
- if (value === './reply.md')
684
- return true;
685
- if (!value.startsWith('/') || !value.endsWith('/reply.md'))
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);