@chllming/wave-orchestration 0.7.1 → 0.7.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 (32) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +8 -8
  3. package/docs/plans/component-cutover-matrix.json +50 -3
  4. package/docs/plans/current-state.md +1 -1
  5. package/docs/plans/end-state-architecture.md +927 -0
  6. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  7. package/docs/plans/migration.md +2 -2
  8. package/docs/plans/waves/wave-1.md +376 -0
  9. package/docs/plans/waves/wave-2.md +292 -0
  10. package/docs/plans/waves/wave-3.md +342 -0
  11. package/docs/plans/waves/wave-4.md +391 -0
  12. package/docs/plans/waves/wave-5.md +382 -0
  13. package/docs/plans/waves/wave-6.md +321 -0
  14. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  15. package/docs/reference/sample-waves.md +4 -4
  16. package/package.json +1 -1
  17. package/releases/manifest.json +19 -0
  18. package/scripts/wave-orchestrator/agent-state.mjs +447 -33
  19. package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
  20. package/scripts/wave-orchestrator/control-cli.mjs +7 -1
  21. package/scripts/wave-orchestrator/coordination.mjs +11 -10
  22. package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
  23. package/scripts/wave-orchestrator/install.mjs +22 -0
  24. package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
  25. package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
  26. package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
  27. package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
  28. package/scripts/wave-orchestrator/launcher.mjs +153 -2922
  29. package/scripts/wave-orchestrator/task-entity.mjs +557 -0
  30. package/scripts/wave-orchestrator/wave-files.mjs +11 -2
  31. package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
  32. package/wave.config.json +1 -1
@@ -403,3 +403,84 @@ export function writeWaveControlDeliveryState(filePath, payload, defaults = {})
403
403
  export function cloneArtifactPayload(value) {
404
404
  return cloneJson(value);
405
405
  }
406
+
407
+ // ── Wave 4: Surface class metadata and additional schema normalizers ──
408
+
409
+ export const WAVE_STATE_SCHEMA_VERSION = 1;
410
+ export const TASK_ENTITY_SCHEMA_VERSION = 1;
411
+ export const AGENT_RESULT_ENVELOPE_SCHEMA_VERSION = 1;
412
+ export const RESUME_PLAN_SCHEMA_VERSION = 1;
413
+ export const HUMAN_INPUT_WORKFLOW_SCHEMA_VERSION = 1;
414
+
415
+ export const SURFACE_CLASS_CANONICAL_EVENT = "canonical-event";
416
+ export const SURFACE_CLASS_CANONICAL_SNAPSHOT = "canonical-snapshot";
417
+ export const SURFACE_CLASS_CACHED_DERIVED = "cached-derived";
418
+ export const SURFACE_CLASS_HUMAN_PROJECTION = "human-projection";
419
+ export const SURFACE_CLASSES = new Set([
420
+ SURFACE_CLASS_CANONICAL_EVENT,
421
+ SURFACE_CLASS_CANONICAL_SNAPSHOT,
422
+ SURFACE_CLASS_CACHED_DERIVED,
423
+ SURFACE_CLASS_HUMAN_PROJECTION,
424
+ ]);
425
+
426
+ export const WAVE_STATE_KIND = "wave-state-snapshot";
427
+ export const TASK_ENTITY_KIND = "wave-task-entity";
428
+ export const AGENT_RESULT_ENVELOPE_KIND = "agent-result-envelope";
429
+ export const RESUME_PLAN_KIND = "wave-resume-plan";
430
+ export const HUMAN_INPUT_WORKFLOW_KIND = "human-input-workflow-state";
431
+
432
+ export function normalizeWaveStateSnapshot(payload, defaults = {}) {
433
+ const source = isPlainObject(payload) ? payload : {};
434
+ return {
435
+ schemaVersion: WAVE_STATE_SCHEMA_VERSION,
436
+ kind: WAVE_STATE_KIND,
437
+ _meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
438
+ lane: normalizeText(source.lane, normalizeText(defaults.lane, null)),
439
+ wave: normalizeInteger(source.wave, normalizeInteger(defaults.wave, null)),
440
+ ...source,
441
+ schemaVersion: WAVE_STATE_SCHEMA_VERSION,
442
+ kind: WAVE_STATE_KIND,
443
+ _meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
444
+ generatedAt: normalizeText(source.generatedAt, toIsoTimestamp()),
445
+ };
446
+ }
447
+
448
+ export function readWaveStateSnapshot(filePath, defaults = {}) {
449
+ const payload = readJsonOrNull(filePath);
450
+ if (!payload) {
451
+ return null;
452
+ }
453
+ return normalizeWaveStateSnapshot(payload, defaults);
454
+ }
455
+
456
+ export function writeWaveStateSnapshot(filePath, payload, defaults = {}) {
457
+ const normalized = normalizeWaveStateSnapshot(payload, defaults);
458
+ writeJsonAtomic(filePath, normalized);
459
+ return normalized;
460
+ }
461
+
462
+ export function normalizeAgentResultEnvelope(payload) {
463
+ const source = isPlainObject(payload) ? payload : {};
464
+ return {
465
+ schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
466
+ kind: AGENT_RESULT_ENVELOPE_KIND,
467
+ _meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
468
+ ...source,
469
+ schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
470
+ kind: AGENT_RESULT_ENVELOPE_KIND,
471
+ _meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
472
+ };
473
+ }
474
+
475
+ export function normalizeResumePlan(payload) {
476
+ const source = isPlainObject(payload) ? payload : {};
477
+ return {
478
+ schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
479
+ kind: RESUME_PLAN_KIND,
480
+ _meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
481
+ ...source,
482
+ schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
483
+ kind: RESUME_PLAN_KIND,
484
+ _meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
485
+ };
486
+ }
@@ -315,9 +315,15 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
315
315
  return wave.agents.map((agent) => {
316
316
  const statusPath = statusPathForAgent(lanePaths, wave, agent);
317
317
  const statusRecord = readStatusRecordIfPresent(statusPath);
318
+ const logPath = path.join(lanePaths.logsDir, `wave-${wave.wave}-${agent.slug}.log`);
318
319
  const summary = augmentSummaryWithProofRegistry(
319
320
  agent,
320
- readAgentExecutionSummary(statusPath),
321
+ readAgentExecutionSummary(statusPath, {
322
+ agent,
323
+ statusPath,
324
+ statusRecord,
325
+ logPath: fs.existsSync(logPath) ? logPath : null,
326
+ }),
321
327
  proofRegistry || { entries: [] },
322
328
  );
323
329
  const proofValidation =
@@ -269,6 +269,15 @@ export function buildExecutionPrompt({
269
269
  "- Use `clear` only when no unresolved findings or approvals remain. Use `blocked` only when the wave must stop before integration.",
270
270
  ]
271
271
  : [];
272
+ const coordinationCommand = [
273
+ "pnpm exec wave coord post",
274
+ `--lane ${lane}`,
275
+ `--wave ${wave}`,
276
+ `--agent ${agent.agentId}`,
277
+ '--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
278
+ '--summary "<one-line summary>"',
279
+ '--detail "<short detail>"',
280
+ ].join(" ");
272
281
  const implementationRequirements =
273
282
  ![contQaAgentId, documentationAgentId].includes(agent.agentId) &&
274
283
  !isSecurityReviewAgent(agent) &&
@@ -281,7 +290,8 @@ export function buildExecutionPrompt({
281
290
  "- Emit one final structured component marker per owned component: `[wave-component] component=<id> level=<level> state=<met|gap> detail=<short-note>`.",
282
291
  ]
283
292
  : []),
284
- "- If you leave any material architecture, integration, durability, ops, or docs gap, emit `[wave-gap] kind=<architecture|integration|durability|ops|docs> detail=<short-note>` and make the gap explicit instead of implying completion.",
293
+ "- If the work is incomplete, keep the required proof/doc/component markers and set `state=gap` on the relevant final marker instead of narrating completion.",
294
+ `- Route unresolved architecture, integration, durability, ops, or docs issues through \`${coordinationCommand}\`. Do not append \`[wave-gap]\` lines after the final implementation markers.`,
285
295
  ]
286
296
  : [];
287
297
  const exitContractLines = agent.exitContract
@@ -305,15 +315,6 @@ export function buildExecutionPrompt({
305
315
  '--context "<what you tried, options, and impact>"',
306
316
  "--timeout-seconds 30",
307
317
  ].join(" ");
308
- const coordinationCommand = [
309
- "pnpm exec wave coord post",
310
- `--lane ${lane}`,
311
- `--wave ${wave}`,
312
- `--agent ${agent.agentId}`,
313
- '--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
314
- '--summary "<one-line summary>"',
315
- '--detail "<short detail>"',
316
- ].join(" ");
317
318
  const context7Selection = context7?.selection || agent?.context7Resolved || null;
318
319
  const executorId = agent?.executorResolved?.id || "default";
319
320
  const context7LibrarySummary =
@@ -0,0 +1,289 @@
1
+ import { toIsoTimestamp } from "./shared.mjs";
2
+
3
+ // ── Human Input Workflow State Machine ──
4
+ //
5
+ // States: open -> pending -> answered -> resolved
6
+ // -> escalated -> resolved
7
+
8
+ export const HUMAN_INPUT_STATES = new Set([
9
+ "open",
10
+ "pending",
11
+ "answered",
12
+ "escalated",
13
+ "resolved",
14
+ ]);
15
+
16
+ export const HUMAN_INPUT_VALID_TRANSITIONS = {
17
+ open: ["pending", "escalated", "resolved"],
18
+ pending: ["answered", "escalated", "resolved"],
19
+ answered: ["resolved"],
20
+ escalated: ["answered", "resolved"],
21
+ resolved: [],
22
+ };
23
+
24
+ const BLOCKING_STATES = new Set(["open", "pending", "escalated"]);
25
+
26
+ const DEFAULT_TIMEOUT_POLICY = {
27
+ maxWaitMs: 300000,
28
+ escalateAfterMs: 120000,
29
+ };
30
+
31
+ const DEFAULT_REROUTE_POLICY = {
32
+ rerouteOnTimeout: true,
33
+ rerouteTo: "operator",
34
+ };
35
+
36
+ function isPlainObject(value) {
37
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
38
+ }
39
+
40
+ function normalizeText(value, fallback = null) {
41
+ const normalized = String(value ?? "").trim();
42
+ return normalized || fallback;
43
+ }
44
+
45
+ export function normalizeHumanInputRequest(request, defaults = {}) {
46
+ const source = isPlainObject(request) ? request : {};
47
+ const defaultSource = isPlainObject(defaults) ? defaults : {};
48
+ const now = toIsoTimestamp();
49
+
50
+ const timeoutPolicy = isPlainObject(source.timeoutPolicy)
51
+ ? {
52
+ maxWaitMs: Number.isFinite(source.timeoutPolicy.maxWaitMs)
53
+ ? source.timeoutPolicy.maxWaitMs
54
+ : DEFAULT_TIMEOUT_POLICY.maxWaitMs,
55
+ escalateAfterMs: Number.isFinite(source.timeoutPolicy.escalateAfterMs)
56
+ ? source.timeoutPolicy.escalateAfterMs
57
+ : DEFAULT_TIMEOUT_POLICY.escalateAfterMs,
58
+ }
59
+ : { ...DEFAULT_TIMEOUT_POLICY };
60
+
61
+ const reroutePolicy = isPlainObject(source.reroutePolicy)
62
+ ? {
63
+ rerouteOnTimeout: source.reroutePolicy.rerouteOnTimeout !== false,
64
+ rerouteTo: normalizeText(source.reroutePolicy.rerouteTo, DEFAULT_REROUTE_POLICY.rerouteTo),
65
+ }
66
+ : { ...DEFAULT_REROUTE_POLICY };
67
+
68
+ const rawState = normalizeText(source.state, normalizeText(defaultSource.state, "open"));
69
+ const state = HUMAN_INPUT_STATES.has(rawState) ? rawState : "open";
70
+
71
+ return {
72
+ requestId: normalizeText(source.requestId, normalizeText(defaultSource.requestId, null)),
73
+ kind: normalizeText(source.kind, normalizeText(defaultSource.kind, "human-input")),
74
+ state,
75
+ title: normalizeText(source.title, normalizeText(defaultSource.title, null)),
76
+ detail: normalizeText(source.detail, normalizeText(defaultSource.detail, null)),
77
+ requestedBy: normalizeText(source.requestedBy, normalizeText(defaultSource.requestedBy, null)),
78
+ assignedTo: normalizeText(source.assignedTo, normalizeText(defaultSource.assignedTo, null)),
79
+ timeoutPolicy,
80
+ reroutePolicy,
81
+ createdAt: normalizeText(source.createdAt, normalizeText(defaultSource.createdAt, now)),
82
+ updatedAt: normalizeText(source.updatedAt, normalizeText(defaultSource.updatedAt, now)),
83
+ answeredAt: normalizeText(source.answeredAt, null),
84
+ resolvedAt: normalizeText(source.resolvedAt, null),
85
+ escalatedAt: normalizeText(source.escalatedAt, null),
86
+ answer: normalizeText(source.answer, null),
87
+ resolution: normalizeText(source.resolution, null),
88
+ };
89
+ }
90
+
91
+ export function transitionHumanInputState(currentState, targetState) {
92
+ if (!HUMAN_INPUT_STATES.has(currentState)) {
93
+ throw new Error(`Invalid current state: ${currentState}`);
94
+ }
95
+ if (!HUMAN_INPUT_STATES.has(targetState)) {
96
+ throw new Error(`Invalid target state: ${targetState}`);
97
+ }
98
+ const allowed = HUMAN_INPUT_VALID_TRANSITIONS[currentState];
99
+ if (!allowed || !allowed.includes(targetState)) {
100
+ throw new Error(
101
+ `Invalid transition from "${currentState}" to "${targetState}". Allowed: [${(allowed || []).join(", ")}]`,
102
+ );
103
+ }
104
+ return targetState;
105
+ }
106
+
107
+ export function isHumanInputBlocking(request) {
108
+ const source = isPlainObject(request) ? request : {};
109
+ const state = normalizeText(source.state, "open");
110
+ return BLOCKING_STATES.has(state);
111
+ }
112
+
113
+ export function buildHumanInputRequests(coordinationState, feedbackRequests, options = {}) {
114
+ const results = [];
115
+ const coordState = isPlainObject(coordinationState) ? coordinationState : {};
116
+ const feedbackList = Array.isArray(feedbackRequests) ? feedbackRequests : [];
117
+ const now = toIsoTimestamp();
118
+
119
+ // Process clarification-request records from coordination state
120
+ const clarifications = Array.isArray(coordState.clarifications)
121
+ ? coordState.clarifications
122
+ : [];
123
+ for (const record of clarifications) {
124
+ if (!isPlainObject(record)) continue;
125
+ const kind = normalizeText(record.kind, null);
126
+ if (
127
+ kind !== "clarification-request" &&
128
+ kind !== "human-escalation" &&
129
+ kind !== "human-feedback"
130
+ ) {
131
+ continue;
132
+ }
133
+ const mappedKind =
134
+ kind === "clarification-request"
135
+ ? "clarification"
136
+ : kind === "human-escalation"
137
+ ? "escalation"
138
+ : "feedback";
139
+ const rawStatus = normalizeText(record.status, "open");
140
+ let mappedState = "open";
141
+ if (rawStatus === "in_progress" || rawStatus === "pending") {
142
+ mappedState = "pending";
143
+ } else if (rawStatus === "resolved" || rawStatus === "closed") {
144
+ mappedState = "resolved";
145
+ } else if (rawStatus === "answered") {
146
+ mappedState = "answered";
147
+ }
148
+ results.push(
149
+ normalizeHumanInputRequest({
150
+ requestId: normalizeText(record.id, null),
151
+ kind: mappedKind,
152
+ state: mappedState,
153
+ title: normalizeText(record.summary, null),
154
+ detail: normalizeText(record.detail, null),
155
+ requestedBy: normalizeText(record.agentId, null),
156
+ assignedTo: null,
157
+ createdAt: normalizeText(record.createdAt, now),
158
+ updatedAt: normalizeText(record.updatedAt, now),
159
+ }),
160
+ );
161
+ }
162
+
163
+ // Process human escalations from coordination state
164
+ const humanEscalations = Array.isArray(coordState.humanEscalations)
165
+ ? coordState.humanEscalations
166
+ : [];
167
+ for (const record of humanEscalations) {
168
+ if (!isPlainObject(record)) continue;
169
+ const rawStatus = normalizeText(record.status, "open");
170
+ let mappedState = "escalated";
171
+ if (rawStatus === "resolved" || rawStatus === "closed") {
172
+ mappedState = "resolved";
173
+ } else if (rawStatus === "answered") {
174
+ mappedState = "answered";
175
+ }
176
+ results.push(
177
+ normalizeHumanInputRequest({
178
+ requestId: normalizeText(record.id, null),
179
+ kind: "escalation",
180
+ state: mappedState,
181
+ title: normalizeText(record.summary, null),
182
+ detail: normalizeText(record.detail, null),
183
+ requestedBy: normalizeText(record.agentId, null),
184
+ assignedTo: "operator",
185
+ createdAt: normalizeText(record.createdAt, now),
186
+ updatedAt: normalizeText(record.updatedAt, now),
187
+ escalatedAt: normalizeText(record.createdAt, now),
188
+ }),
189
+ );
190
+ }
191
+
192
+ // Process feedback requests
193
+ for (const record of feedbackList) {
194
+ if (!isPlainObject(record)) continue;
195
+ const rawStatus = normalizeText(record.status, "pending");
196
+ let mappedState = "pending";
197
+ if (rawStatus === "answered") {
198
+ mappedState = "answered";
199
+ } else if (rawStatus === "resolved" || rawStatus === "closed") {
200
+ mappedState = "resolved";
201
+ }
202
+ results.push(
203
+ normalizeHumanInputRequest({
204
+ requestId: normalizeText(record.id, null),
205
+ kind: "feedback",
206
+ state: mappedState,
207
+ title: normalizeText(record.question, null),
208
+ detail: normalizeText(record.context, null),
209
+ requestedBy: normalizeText(record.agentId, null),
210
+ assignedTo: "operator",
211
+ createdAt: normalizeText(record.createdAt, now),
212
+ updatedAt: normalizeText(record.updatedAt, now),
213
+ answeredAt: normalizeText(record.response?.answeredAt, null),
214
+ answer: normalizeText(record.response?.text, null),
215
+ }),
216
+ );
217
+ }
218
+
219
+ return results;
220
+ }
221
+
222
+ export function evaluateHumanInputTimeout(request, now = Date.now()) {
223
+ const source = isPlainObject(request) ? request : {};
224
+ const createdAtMs = Date.parse(source.createdAt || "");
225
+ if (!Number.isFinite(createdAtMs)) {
226
+ return { expired: false, shouldEscalate: false, elapsedMs: 0 };
227
+ }
228
+ const elapsedMs = Math.max(0, now - createdAtMs);
229
+ const policy = isPlainObject(source.timeoutPolicy)
230
+ ? source.timeoutPolicy
231
+ : DEFAULT_TIMEOUT_POLICY;
232
+ const maxWaitMs = Number.isFinite(policy.maxWaitMs)
233
+ ? policy.maxWaitMs
234
+ : DEFAULT_TIMEOUT_POLICY.maxWaitMs;
235
+ const escalateAfterMs = Number.isFinite(policy.escalateAfterMs)
236
+ ? policy.escalateAfterMs
237
+ : DEFAULT_TIMEOUT_POLICY.escalateAfterMs;
238
+ const expired = elapsedMs >= maxWaitMs;
239
+ const shouldEscalate = elapsedMs >= escalateAfterMs;
240
+ return { expired, shouldEscalate, elapsedMs };
241
+ }
242
+
243
+ export function computeHumanInputMetrics(requests) {
244
+ const list = Array.isArray(requests) ? requests : [];
245
+ const counts = { open: 0, pending: 0, answered: 0, escalated: 0, resolved: 0 };
246
+ let blocking = 0;
247
+ let overdueCount = 0;
248
+ let totalResolutionMs = 0;
249
+ let resolvedWithTimesCount = 0;
250
+
251
+ for (const request of list) {
252
+ const source = isPlainObject(request) ? request : {};
253
+ const state = normalizeText(source.state, "open");
254
+ if (state in counts) {
255
+ counts[state] += 1;
256
+ }
257
+ if (BLOCKING_STATES.has(state)) {
258
+ blocking += 1;
259
+ }
260
+ // Check overdue based on timeout policy
261
+ const timeout = evaluateHumanInputTimeout(source);
262
+ if (timeout.expired && BLOCKING_STATES.has(state)) {
263
+ overdueCount += 1;
264
+ }
265
+ // Compute resolution time for resolved requests
266
+ if (state === "resolved" && source.createdAt && source.resolvedAt) {
267
+ const createdMs = Date.parse(source.createdAt);
268
+ const resolvedMs = Date.parse(source.resolvedAt);
269
+ if (Number.isFinite(createdMs) && Number.isFinite(resolvedMs) && resolvedMs >= createdMs) {
270
+ totalResolutionMs += resolvedMs - createdMs;
271
+ resolvedWithTimesCount += 1;
272
+ }
273
+ }
274
+ }
275
+
276
+ return {
277
+ total: list.length,
278
+ open: counts.open,
279
+ pending: counts.pending,
280
+ answered: counts.answered,
281
+ escalated: counts.escalated,
282
+ resolved: counts.resolved,
283
+ blocking,
284
+ overdueCount,
285
+ avgResolutionMs: resolvedWithTimesCount > 0
286
+ ? Math.round(totalResolutionMs / resolvedWithTimesCount)
287
+ : null,
288
+ };
289
+ }
@@ -198,6 +198,28 @@ function copyTemplateFile(relPath) {
198
198
  throw new Error(`Missing packaged template: ${relPath}`);
199
199
  }
200
200
  ensureDirectory(path.dirname(targetPath));
201
+ if (relPath === "docs/plans/component-cutover-matrix.json") {
202
+ const payload = readJsonOrNull(sourcePath);
203
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
204
+ throw new Error(`Invalid packaged template JSON: ${relPath}`);
205
+ }
206
+ const components = Object.fromEntries(
207
+ Object.entries(payload.components || {}).map(([componentId, component]) => [
208
+ componentId,
209
+ {
210
+ ...component,
211
+ promotions: Array.isArray(component?.promotions)
212
+ ? component.promotions.filter((entry) => Number(entry?.wave) === 0)
213
+ : [],
214
+ },
215
+ ]),
216
+ );
217
+ writeJsonAtomic(targetPath, {
218
+ ...payload,
219
+ components,
220
+ });
221
+ return targetPath;
222
+ }
201
223
  fs.copyFileSync(sourcePath, targetPath);
202
224
  return targetPath;
203
225
  }