@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.en.md +5 -2
  3. package/README.md +5 -2
  4. package/dist/bin/dingtalk-agent.js +16 -5
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-platform.js +0 -1
  7. package/dist/src/agent-platform.js.map +1 -1
  8. package/dist/src/development-workspace.js +25 -8
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/doctor.js +6 -1
  11. package/dist/src/doctor.js.map +1 -1
  12. package/dist/src/multica-deploy.js +265 -50
  13. package/dist/src/multica-deploy.js.map +1 -1
  14. package/dist/src/multica-provider.js +1 -2
  15. package/dist/src/multica-provider.js.map +1 -1
  16. package/dist/src/opencode-provider.js +3 -1
  17. package/dist/src/opencode-provider.js.map +1 -1
  18. package/dist/src/skill-manager.js +2 -3
  19. package/dist/src/skill-manager.js.map +1 -1
  20. package/docs/ARCHITECTURE.md +12 -0
  21. package/docs/schemas/multica-deployment-receipt.schema.json +2 -2
  22. package/docs/schemas/multica-workspace-run-plan.schema.json +31 -0
  23. package/docs/schemas/multica-workspace-run.schema.json +44 -0
  24. package/docs/schemas/project.schema.json +10 -2
  25. package/lab/project-workspace/fake-multica-provider.mjs +39 -10
  26. package/lab/project-workspace/multica-readonly.fixture.json +0 -12
  27. package/lab/project-workspace/project.fixture.json +0 -4
  28. package/lab/robot-eval/suite.json +1 -1
  29. package/package.json +1 -2
  30. package/skills/README.md +0 -1
  31. package/skills/core/dingtalk-agent-compose/SKILL.md +5 -5
  32. package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
  33. package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +1 -1
  34. package/skills/core/dingtalk-agent-eval/SKILL.md +1 -1
  35. package/skills/core/dingtalk-agent-eval/references/interactive-debug-channels.md +9 -3
  36. package/skills/core/dingtalk-basic-behavior/SKILL.md +15 -3
  37. package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +54 -0
  38. package/skills/core/dingtalk-basic-behavior/references/truth-and-recovery.md +4 -2
  39. package/skills/platforms/multica-dingtalk/PLATFORM.md +6 -5
  40. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +4 -4
  41. package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/multica-deployment-contract.md +14 -6
  42. 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 { 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({
@@ -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
- 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');
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 { definition, definitionHash, instructions, instructionsHash, skills, deploymentHash };
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
- 'Use only the trusted DTA Multica Boot required_skills list.',
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 readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
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, 250);
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 replyWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
611
- .map((item) => smokeToolInput(item)).filter((input) => optionalString(input.filePath).endsWith('/reply.md'))
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, ...replyWrites].find(matchesResponse) || '';
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
- marker, markerHash: sha256(marker), issueId, taskId, taskStatus, requiredSkills, loadedSkills,
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 safeSmokeReplyPath(optionalString(input.filePath)) &&
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 safeSmokeReplyPath(command.slice(commentPrefix.length));
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 safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
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 safeSmokeReplyPath(command.slice(statusFirstPrefix.length));
843
+ return safeSmokeResponsePath(command.slice(statusFirstPrefix.length));
676
844
  }
677
845
  if (command.startsWith(cleanupFirstPrefix)) {
678
- return safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length));
846
+ return safeSmokeResponsePath(command.slice(cleanupFirstPrefix.length));
679
847
  }
680
848
  return false;
681
849
  }
682
- function safeSmokeReplyPath(value) {
683
- if (value === './reply.md')
684
- return true;
685
- if (!value.startsWith('/') || !value.endsWith('/reply.md'))
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);