agentic-dev 0.2.22 → 0.2.23

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.
@@ -21,6 +21,7 @@ on:
21
21
  push:
22
22
  paths:
23
23
  - "sdd/02_plan/**"
24
+ - "sdd/99_toolchain/01_automation/github-project-kit/**"
24
25
  - ".agentic-dev/orchestration.json"
25
26
  - ".agentic-dev/runtime/**"
26
27
  workflow_dispatch:
@@ -31,6 +32,7 @@ jobs:
31
32
  permissions:
32
33
  contents: read
33
34
  issues: write
35
+ pull-requests: write
34
36
  repository-projects: write
35
37
  steps:
36
38
  - name: Checkout
@@ -41,9 +43,25 @@ jobs:
41
43
  with:
42
44
  node-version: "22"
43
45
 
44
- - name: Start orchestration server
46
+ - name: Resolve orchestration GitHub auth
45
47
  env:
46
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
48
+ AGENTIC_GITHUB_TOKEN_SECRET: \${{ secrets.AGENTIC_GITHUB_TOKEN }}
49
+ FALLBACK_GITHUB_TOKEN: \${{ github.token }}
50
+ run: |
51
+ TOKEN="$AGENTIC_GITHUB_TOKEN_SECRET"
52
+ if [ -z "$TOKEN" ]; then
53
+ TOKEN="$FALLBACK_GITHUB_TOKEN"
54
+ fi
55
+ if [ -z "$TOKEN" ]; then
56
+ echo "No GitHub token available for orchestration" >&2
57
+ exit 1
58
+ fi
59
+ echo "::add-mask::$TOKEN"
60
+ echo "AGENTIC_GITHUB_TOKEN=$TOKEN" >> "$GITHUB_ENV"
61
+ echo "GH_TOKEN=$TOKEN" >> "$GITHUB_ENV"
62
+ echo "GITHUB_TOKEN=$TOKEN" >> "$GITHUB_ENV"
63
+
64
+ - name: Start orchestration server
47
65
  run: |
48
66
  npx --yes tsx .agentic-dev/runtime/server.ts > /tmp/agentic-orchestration.log 2>&1 &
49
67
  echo $! > /tmp/agentic-orchestration.pid
@@ -60,29 +78,20 @@ jobs:
60
78
  run: curl -fsS -X POST http://127.0.0.1:4310/sync/ir
61
79
 
62
80
  - name: Sync GitHub project tasks
63
- env:
64
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
65
81
  run: curl -fsS -X POST http://127.0.0.1:4310/sync/tasks
66
82
 
67
83
  - name: Plan multi-agent queue
68
- env:
69
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
70
84
  run: curl -fsS -X POST http://127.0.0.1:4310/queue/plan
71
85
 
72
86
  - name: Build dispatch plan
73
- env:
74
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
75
87
  run: curl -fsS -X POST http://127.0.0.1:4310/queue/dispatch
76
88
 
77
89
  - name: Execute dispatch queue
78
90
  env:
79
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
80
91
  AGENTIC_AGENT_TIMEOUT_MS: "30000"
81
92
  run: curl -fsS -X POST http://127.0.0.1:4310/queue/execute
82
93
 
83
94
  - name: Close completed tasks
84
- env:
85
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
86
95
  run: curl -fsS -X POST http://127.0.0.1:4310/tasks/close
87
96
 
88
97
  - name: Stop orchestration server
@@ -125,12 +134,97 @@ export function orchestrationConfig() {
125
134
  return readJson(path.resolve(".agentic-dev/orchestration.json"), {});
126
135
  }
127
136
 
137
+ export function githubAuthEnv() {
138
+ const env = { ...process.env };
139
+ const token = String(env.AGENTIC_GITHUB_TOKEN || env.GH_TOKEN || env.GITHUB_TOKEN || "").trim();
140
+ if (token) {
141
+ env.AGENTIC_GITHUB_TOKEN = env.AGENTIC_GITHUB_TOKEN || token;
142
+ env.GH_TOKEN = token;
143
+ env.GITHUB_TOKEN = token;
144
+ }
145
+ return env;
146
+ }
147
+
148
+ export function projectContractPath() {
149
+ return path.resolve("sdd/99_toolchain/01_automation/github-project-kit/project-contract.json");
150
+ }
151
+
152
+ export function roleMapPath() {
153
+ return path.resolve("sdd/99_toolchain/01_automation/github-project-kit/role-map.json");
154
+ }
155
+
156
+ export function loadProjectContract() {
157
+ return readJson(projectContractPath(), null);
158
+ }
159
+
160
+ export function loadRoleMap() {
161
+ return readJson(roleMapPath(), { roles: [] });
162
+ }
163
+
164
+ export function artifactContractPath() {
165
+ return path.resolve("sdd/99_toolchain/01_automation/github-project-kit/artifact-contract.json");
166
+ }
167
+
168
+ export function loadArtifactContract() {
169
+ return readJson(artifactContractPath(), null);
170
+ }
171
+
172
+ export function taskCatalogPath() {
173
+ const contract = loadArtifactContract();
174
+ return path.resolve(
175
+ String(contract?.outputs?.task_catalog || "sdd/99_toolchain/01_automation/github-project-kit/task-catalog.json"),
176
+ );
177
+ }
178
+
179
+ export function taskSyncStatePath() {
180
+ const contract = loadArtifactContract();
181
+ return path.resolve(
182
+ String(contract?.outputs?.task_sync_state || "sdd/99_toolchain/01_automation/github-project-kit/task-sync-state.json"),
183
+ );
184
+ }
185
+
186
+ export function loadTaskCatalog() {
187
+ return readJson(taskCatalogPath(), { tasks: [] });
188
+ }
189
+
190
+ export function loadTaskSyncState() {
191
+ return readJson(taskSyncStatePath(), { issues: {} });
192
+ }
193
+
194
+ export function hasProjectContract() {
195
+ const contract = loadProjectContract();
196
+ return Boolean(contract && contract.project_id && contract.project_number && contract.repository);
197
+ }
198
+
199
+ export function hasArtifactBacklogKit() {
200
+ return Boolean(
201
+ hasProjectContract() &&
202
+ fs.existsSync(artifactContractPath()) &&
203
+ fs.existsSync(path.resolve("scripts/agentic-core/artifact_backlog.py")),
204
+ );
205
+ }
206
+
207
+ export function runArtifactBacklog() {
208
+ const output = execFileSync("python3", ["scripts/agentic-core/artifact_backlog.py", "run", "--repo-root", "."], {
209
+ encoding: "utf-8",
210
+ env: githubAuthEnv(),
211
+ }).trim();
212
+ return output ? JSON.parse(output) : {};
213
+ }
214
+
128
215
  export function ghJson(args) {
129
- return JSON.parse(execFileSync("gh", args, { encoding: "utf-8" }));
216
+ const output = execFileSync("gh", args, {
217
+ encoding: "utf-8",
218
+ env: githubAuthEnv(),
219
+ }).trim();
220
+ return output ? JSON.parse(output) : {};
130
221
  }
131
222
 
132
223
  export function gh(args) {
133
- return execFileSync("gh", args, { encoding: "utf-8" }).trim();
224
+ return execFileSync("gh", args, {
225
+ encoding: "utf-8",
226
+ env: githubAuthEnv(),
227
+ }).trim();
134
228
  }
135
229
 
136
230
  export function loadTaskIr() {
@@ -153,6 +247,30 @@ export function loadExecutionJournal() {
153
247
  return readJson(generatedPath("execution-journal.json"), { executions: [] });
154
248
  }
155
249
 
250
+ export function issueUrl(repository, issueNumber) {
251
+ return "https://github.com/" + String(repository || "") + "/issues/" + String(issueNumber || "");
252
+ }
253
+
254
+ export function issueNumberFromUrl(url) {
255
+ const match = String(url || "").match(/\\/issues\\/(\\d+)$/);
256
+ return match ? Number(match[1]) : null;
257
+ }
258
+
259
+ export function normalizeTaskStatus(status) {
260
+ return String(status || "").trim().toLowerCase() === "closed" ? "closed" : "open";
261
+ }
262
+
263
+ export function classifyAgent(title) {
264
+ const lower = String(title || "").toLowerCase();
265
+ if (lower.includes("api") || lower.includes("contract")) return "api";
266
+ if (lower.includes("screen") || lower.includes("ui")) return "ui";
267
+ if (lower.includes("verify") || lower.includes("test") || lower.includes("proof")) return "quality";
268
+ if (lower.includes("workflow") || lower.includes("project") || lower.includes("github")) return "gitops";
269
+ if (lower.includes("arch") || lower.includes("boundary") || lower.includes("structure")) return "architecture";
270
+ if (lower.includes("plan") || lower.includes("spec")) return "specs";
271
+ return "runtime";
272
+ }
273
+
156
274
  export function normalizeProviderProfile(profile) {
157
275
  const normalized = String(profile || "").trim().toLowerCase();
158
276
  switch (normalized) {
@@ -174,6 +292,217 @@ export function normalizeProviderProfile(profile) {
174
292
  return normalized;
175
293
  }
176
294
  }
295
+
296
+ export function pickProvider(title, providers) {
297
+ const normalizedProviders = (Array.isArray(providers) ? providers : []).map(normalizeProviderProfile);
298
+ const lower = String(title || "").toLowerCase();
299
+ if (lower.includes("verify") || lower.includes("test")) {
300
+ return (
301
+ normalizedProviders.find((provider) => provider === "claude-cli" || provider === "anthropic-api") ||
302
+ normalizedProviders[0] ||
303
+ "claude-cli"
304
+ );
305
+ }
306
+ if (lower.includes("api") || lower.includes("contract")) {
307
+ return (
308
+ normalizedProviders.find((provider) => provider === "openai-api" || provider === "anthropic-api") ||
309
+ normalizedProviders[0] ||
310
+ "openai-api"
311
+ );
312
+ }
313
+ return (
314
+ normalizedProviders.find((provider) => provider === "codex-cli" || provider === "openai-api") ||
315
+ normalizedProviders[0] ||
316
+ "codex-cli"
317
+ );
318
+ }
319
+
320
+ export function roleEntry(roleMap, roleId) {
321
+ const roles = Array.isArray(roleMap?.roles) ? roleMap.roles : [];
322
+ return roles.find((entry) => String(entry?.id || "") === String(roleId || "")) || null;
323
+ }
324
+
325
+ export function resolveAgentRole(roleMap, preferredRole) {
326
+ const normalized = String(preferredRole || "").trim() || "runtime";
327
+ if (roleEntry(roleMap, normalized)) return normalized;
328
+ if (roleEntry(roleMap, "runtime")) return "runtime";
329
+ return normalized;
330
+ }
331
+
332
+ export function taskIssueMarker(taskId) {
333
+ return "<!-- agentic-task-key: " + String(taskId || "") + " -->";
334
+ }
335
+
336
+ export function taskIssueTitle(task) {
337
+ return "[agentic-task] " + String(task?.id || "") + " " + String(task?.title || "");
338
+ }
339
+
340
+ export function taskIssueBody(task, assignedAgent) {
341
+ return [
342
+ taskIssueMarker(task.id),
343
+ "<!-- agentic-task-meta " +
344
+ JSON.stringify({
345
+ id: String(task?.id || ""),
346
+ title: String(task?.title || ""),
347
+ source: String(task?.source || ""),
348
+ status: normalizeTaskStatus(task?.status),
349
+ agent_role: String(assignedAgent || "") || null,
350
+ }) +
351
+ " -->",
352
+ "agent_role: " + String(assignedAgent || ""),
353
+ "SDD source: " + String(task?.source || ""),
354
+ "",
355
+ "Managed by agentic-dev orchestration.",
356
+ ].join("\\n");
357
+ }
358
+
359
+ export function findIssueForTask(issues, taskId) {
360
+ return (
361
+ (Array.isArray(issues) ? issues : []).find((issue) => {
362
+ const body = String(issue?.body || "");
363
+ const title = String(issue?.title || "");
364
+ return body.includes(taskIssueMarker(taskId)) || title.startsWith("[agentic-task] " + taskId + " ");
365
+ }) || null
366
+ );
367
+ }
368
+
369
+ export function projectContractAvailable(contract) {
370
+ return Boolean(
371
+ contract &&
372
+ contract.project_id &&
373
+ contract.project_number &&
374
+ contract.owner &&
375
+ contract.repository,
376
+ );
377
+ }
378
+
379
+ export function projectItemIndex(contract) {
380
+ if (!projectContractAvailable(contract)) {
381
+ return new Map();
382
+ }
383
+ const payload = ghJson([
384
+ "project",
385
+ "item-list",
386
+ String(contract.project_number),
387
+ "--owner",
388
+ String(contract.owner),
389
+ "--limit",
390
+ "200",
391
+ "--format",
392
+ "json",
393
+ ]);
394
+ const index = new Map();
395
+ for (const item of Array.isArray(payload?.items) ? payload.items : []) {
396
+ const content = item?.content || {};
397
+ if (content.type === "Issue" && typeof content.number === "number") {
398
+ index.set(content.number, item);
399
+ }
400
+ }
401
+ return index;
402
+ }
403
+
404
+ export function ensureProjectItem(contract, issueNumber, itemIndex) {
405
+ if (!projectContractAvailable(contract)) {
406
+ return null;
407
+ }
408
+ const existing = itemIndex?.get(issueNumber);
409
+ if (existing) {
410
+ return existing;
411
+ }
412
+ const payload = ghJson([
413
+ "project",
414
+ "item-add",
415
+ String(contract.project_number),
416
+ "--owner",
417
+ String(contract.owner),
418
+ "--url",
419
+ issueUrl(contract.repository, issueNumber),
420
+ "--format",
421
+ "json",
422
+ ]);
423
+ if (!payload?.id) {
424
+ throw new Error("unable to add project item for issue #" + String(issueNumber));
425
+ }
426
+ const added = {
427
+ id: String(payload.id),
428
+ content: {
429
+ type: String(payload.type || "Issue"),
430
+ number: Number(issueNumber),
431
+ },
432
+ };
433
+ if (itemIndex) {
434
+ itemIndex.set(Number(issueNumber), added);
435
+ }
436
+ return added;
437
+ }
438
+
439
+ export function setProjectStatus(contract, itemId, statusKey) {
440
+ const fieldId = String(contract?.fields?.status?.id || "");
441
+ const optionId = String(contract?.fields?.status?.options?.[statusKey] || "");
442
+ const projectId = String(contract?.project_id || "");
443
+ if (!itemId || !fieldId || !optionId || !projectId) {
444
+ return false;
445
+ }
446
+ gh([
447
+ "project",
448
+ "item-edit",
449
+ "--id",
450
+ String(itemId),
451
+ "--project-id",
452
+ projectId,
453
+ "--field-id",
454
+ fieldId,
455
+ "--single-select-option-id",
456
+ optionId,
457
+ ]);
458
+ return true;
459
+ }
460
+
461
+ export function setProjectText(contract, itemId, fieldId, text) {
462
+ const projectId = String(contract?.project_id || "");
463
+ if (!itemId || !fieldId || !projectId) {
464
+ return false;
465
+ }
466
+ gh([
467
+ "project",
468
+ "item-edit",
469
+ "--id",
470
+ String(itemId),
471
+ "--project-id",
472
+ projectId,
473
+ "--field-id",
474
+ String(fieldId),
475
+ "--text",
476
+ String(text || ""),
477
+ ]);
478
+ return true;
479
+ }
480
+
481
+ export function setProjectAgentRole(contract, itemId, agentRole) {
482
+ return setProjectText(contract, itemId, String(contract?.fields?.agent_role?.id || ""), String(agentRole || ""));
483
+ }
484
+
485
+ export function buildProjectLogComment(contract, payload = {}) {
486
+ const lines = [String(contract?.structured_comment_prefix || "<!-- agentic-project-log -->")];
487
+ if (payload.agent_role) lines.push("agent_role: " + String(payload.agent_role));
488
+ if (payload.status) lines.push("status: " + String(payload.status));
489
+ if (payload.branch) lines.push("branch: " + String(payload.branch));
490
+ if (payload.summary) lines.push("summary: " + String(payload.summary));
491
+ if (payload.next) lines.push("next: " + String(payload.next));
492
+ return lines.join("\\n");
493
+ }
494
+
495
+ export function parseTaskMeta(body) {
496
+ const match = String(body || "").match(/<!-- agentic-task-meta\\s*([\\s\\S]*?)\\s*-->/);
497
+ if (!match) {
498
+ return {};
499
+ }
500
+ try {
501
+ return JSON.parse(match[1]);
502
+ } catch {
503
+ return {};
504
+ }
505
+ }
177
506
  `;
178
507
  }
179
508
  function sddToIrScript() {
@@ -220,120 +549,323 @@ console.log(\`tasks=\${tasks.length}\`);
220
549
  }
221
550
  function syncProjectTasksScript() {
222
551
  return `#!/usr/bin/env node
223
- import { gh, ghJson, loadTaskIr, orchestrationConfig, writeJson, generatedPath } from "./runtime-lib.ts";
552
+ import {
553
+ artifactContractPath,
554
+ ensureProjectItem,
555
+ findIssueForTask,
556
+ generatedPath,
557
+ gh,
558
+ ghJson,
559
+ hasArtifactBacklogKit,
560
+ issueNumberFromUrl,
561
+ issueUrl,
562
+ loadArtifactContract,
563
+ loadProjectContract,
564
+ loadRoleMap,
565
+ loadTaskCatalog,
566
+ loadTaskSyncState,
567
+ loadTaskIr,
568
+ normalizeTaskStatus,
569
+ orchestrationConfig,
570
+ parseTaskMeta,
571
+ projectContractAvailable,
572
+ projectContractPath,
573
+ projectItemIndex,
574
+ resolveAgentRole,
575
+ roleMapPath,
576
+ runArtifactBacklog,
577
+ setProjectAgentRole,
578
+ setProjectStatus,
579
+ taskIssueBody,
580
+ taskIssueTitle,
581
+ classifyAgent,
582
+ writeJson,
583
+ } from "./runtime-lib.ts";
224
584
 
225
585
  const orchestration = orchestrationConfig();
586
+ const repository = orchestration?.github?.repository?.slug;
587
+ if (!repository) {
588
+ throw new Error("github repository slug is not configured in .agentic-dev/orchestration.json");
589
+ }
590
+
226
591
  const taskIr = loadTaskIr();
227
- const existingIssues = ghJson([
228
- "issue",
229
- "list",
230
- "--repo",
231
- orchestration.github.repository.slug,
232
- "--state",
233
- "all",
234
- "--limit",
235
- "200",
236
- "--json",
237
- "number,title,state",
238
- ]);
239
- const syncResult = { created: [], closed: [], tasks: [] };
592
+ const artifactContract = loadArtifactContract();
593
+ const projectContract = loadProjectContract();
594
+ const roleMap = loadRoleMap();
595
+ function loadIssues() {
596
+ const payload = ghJson([
597
+ "issue",
598
+ "list",
599
+ "--repo",
600
+ repository,
601
+ "--state",
602
+ "all",
603
+ "--limit",
604
+ "200",
605
+ "--json",
606
+ "number,title,state,body,url",
607
+ ]);
608
+ return Array.isArray(payload) ? payload : [];
609
+ }
240
610
 
241
- for (const task of taskIr.tasks) {
242
- const issueTitle = \`[agentic-task] \${task.id} \${task.title}\`;
243
- const found = existingIssues.find((issue) =>
244
- issue.title.startsWith(\`[agentic-task] \${task.id} \`),
245
- );
246
- if (!found && task.status === "open") {
247
- const url = gh([
248
- "issue",
249
- "create",
250
- "--repo",
251
- orchestration.github.repository.slug,
252
- "--title",
253
- issueTitle,
254
- "--body",
255
- \`SDD source: \${task.source}\\n\\nManaged by agentic-dev orchestration.\`,
256
- ]);
257
- syncResult.created.push({ id: task.id, title: task.title, issue_url: url });
611
+ let existingIssues = loadIssues();
612
+ let itemIndex = projectContractAvailable(projectContract) ? projectItemIndex(projectContract) : new Map();
613
+ const syncResult = {
614
+ mode: "checklist",
615
+ artifact_backlog: null,
616
+ created: [],
617
+ updated: [],
618
+ reopened: [],
619
+ closed: [],
620
+ warnings: [],
621
+ project: {
622
+ enabled: projectContractAvailable(projectContract),
623
+ contract_path: projectContractAvailable(projectContract) ? projectContractPath() : null,
624
+ artifact_contract_path: artifactContract ? artifactContractPath() : null,
625
+ role_map_path: roleMapPath(),
626
+ },
627
+ tasks: [],
628
+ };
629
+
630
+ if (hasArtifactBacklogKit()) {
631
+ syncResult.mode = "artifact-backlog";
632
+ syncResult.artifact_backlog = runArtifactBacklog();
633
+ existingIssues = loadIssues();
634
+ itemIndex = projectContractAvailable(projectContract) ? projectItemIndex(projectContract) : new Map();
635
+
636
+ const catalog = loadTaskCatalog();
637
+ const syncState = loadTaskSyncState();
638
+ const catalogTasks = Array.isArray(catalog?.tasks) ? catalog.tasks : [];
639
+
640
+ for (const task of catalogTasks) {
641
+ const meta = parseTaskMeta(task.body);
642
+ const assignedAgent = resolveAgentRole(
643
+ roleMap,
644
+ String(task.agent_role || meta.agent_role || classifyAgent(task.title)),
645
+ );
646
+ const syncedIssue = syncState?.issues?.[task.key];
647
+ const mappedIssueNumber =
648
+ syncedIssue && typeof syncedIssue.number === "number" ? Number(syncedIssue.number) : null;
649
+ const found =
650
+ (mappedIssueNumber
651
+ ? existingIssues.find((issue) => Number(issue?.number) === mappedIssueNumber)
652
+ : null) ||
653
+ findIssueForTask(existingIssues, task.key);
654
+ const taskStatus = normalizeTaskStatus(found?.state === "CLOSED" ? "closed" : "open");
655
+ const issueNumber = found?.number ?? mappedIssueNumber ?? null;
656
+ const issueLink = found?.url || (issueNumber ? issueUrl(repository, issueNumber) : null);
657
+
658
+ let projectItemId = null;
659
+ let projectStatus = null;
660
+ if (issueNumber && projectContractAvailable(projectContract)) {
661
+ try {
662
+ const item = ensureProjectItem(projectContract, Number(issueNumber), itemIndex);
663
+ projectItemId = item?.id || null;
664
+ if (projectItemId) {
665
+ setProjectAgentRole(projectContract, projectItemId, assignedAgent);
666
+ projectStatus = taskStatus === "closed" ? "done" : "todo";
667
+ setProjectStatus(projectContract, projectItemId, projectStatus);
668
+ }
669
+ } catch (error) {
670
+ syncResult.warnings.push({
671
+ id: task.key,
672
+ scope: "artifact-project-sync",
673
+ message: String(error?.message || error),
674
+ });
675
+ }
676
+ }
677
+
678
+ syncResult.tasks.push({
679
+ id: task.key,
680
+ title: task.title,
681
+ source:
682
+ Array.isArray(meta.source_refs) && meta.source_refs.length > 0
683
+ ? String(meta.source_refs[0])
684
+ : "artifact:" + String(task.artifact_type || "task"),
685
+ status: taskStatus,
686
+ agent_role: assignedAgent,
687
+ issue_number: issueNumber,
688
+ issue_url: issueLink,
689
+ project_item_id: projectItemId,
690
+ project_status: projectStatus,
691
+ project_enabled: projectContractAvailable(projectContract),
692
+ task_origin: "artifact-backlog",
693
+ artifact_type: task.artifact_type || null,
694
+ source_refs: Array.isArray(meta.source_refs) ? meta.source_refs : [],
695
+ suggested_commands: Array.isArray(meta.suggested_commands) ? meta.suggested_commands : [],
696
+ target_paths_hint: Array.isArray(meta.target_paths_hint) ? meta.target_paths_hint : [],
697
+ priority: task.priority || null,
698
+ size: task.size || null,
699
+ });
700
+ }
701
+ }
702
+
703
+ if (syncResult.tasks.length === 0) {
704
+ syncResult.mode = "checklist";
705
+
706
+ for (const task of taskIr.tasks) {
707
+ const assignedAgent = resolveAgentRole(roleMap, classifyAgent(task.title));
708
+ const issueTitle = taskIssueTitle(task);
709
+ const issueBody = taskIssueBody(task, assignedAgent);
710
+ const taskStatus = normalizeTaskStatus(task.status);
711
+
712
+ let found = findIssueForTask(existingIssues, task.id);
713
+ let issueNumber = found?.number ?? null;
714
+ let issueLink = found?.url || (issueNumber ? issueUrl(repository, issueNumber) : null);
715
+
716
+ if (!found && taskStatus === "open") {
717
+ const createdUrl = gh([
718
+ "issue",
719
+ "create",
720
+ "--repo",
721
+ repository,
722
+ "--title",
723
+ issueTitle,
724
+ "--body",
725
+ issueBody,
726
+ ]);
727
+ issueNumber = issueNumberFromUrl(createdUrl);
728
+ issueLink = createdUrl || (issueNumber ? issueUrl(repository, issueNumber) : null);
729
+ found = {
730
+ number: issueNumber,
731
+ title: issueTitle,
732
+ state: "OPEN",
733
+ body: issueBody,
734
+ url: issueLink,
735
+ };
736
+ existingIssues.push(found);
737
+ syncResult.created.push({
738
+ id: task.id,
739
+ title: task.title,
740
+ issue_number: issueNumber,
741
+ issue_url: issueLink,
742
+ });
743
+ }
744
+
745
+ if (found && taskStatus === "open" && found.state === "CLOSED") {
746
+ gh([
747
+ "issue",
748
+ "reopen",
749
+ String(found.number),
750
+ "--repo",
751
+ repository,
752
+ ]);
753
+ found.state = "OPEN";
754
+ syncResult.reopened.push({ id: task.id, issue_number: found.number });
755
+ }
756
+
757
+ if (found && (String(found.title || "") !== issueTitle || String(found.body || "") !== issueBody)) {
758
+ gh([
759
+ "issue",
760
+ "edit",
761
+ String(found.number),
762
+ "--repo",
763
+ repository,
764
+ "--title",
765
+ issueTitle,
766
+ "--body",
767
+ issueBody,
768
+ ]);
769
+ found.title = issueTitle;
770
+ found.body = issueBody;
771
+ syncResult.updated.push({ id: task.id, issue_number: found.number });
772
+ }
773
+
774
+ if (found && taskStatus === "closed" && found.state !== "CLOSED") {
775
+ gh([
776
+ "issue",
777
+ "close",
778
+ String(found.number),
779
+ "--repo",
780
+ repository,
781
+ "--comment",
782
+ "Closed from SDD task IR.",
783
+ ]);
784
+ found.state = "CLOSED";
785
+ syncResult.closed.push({ id: task.id, issue_number: found.number });
786
+ }
787
+
788
+ issueNumber = found?.number ?? issueNumber ?? null;
789
+ issueLink = found?.url || issueLink || (issueNumber ? issueUrl(repository, issueNumber) : null);
790
+
791
+ let projectItemId = null;
792
+ let projectStatus = null;
793
+ if (issueNumber && projectContractAvailable(projectContract)) {
794
+ try {
795
+ const item = ensureProjectItem(projectContract, Number(issueNumber), itemIndex);
796
+ projectItemId = item?.id || null;
797
+ if (projectItemId) {
798
+ setProjectAgentRole(projectContract, projectItemId, assignedAgent);
799
+ projectStatus = taskStatus === "closed" ? "done" : "todo";
800
+ setProjectStatus(projectContract, projectItemId, projectStatus);
801
+ }
802
+ } catch (error) {
803
+ syncResult.warnings.push({
804
+ id: task.id,
805
+ scope: "project-sync",
806
+ message: String(error?.message || error),
807
+ });
808
+ }
809
+ }
810
+
811
+ syncResult.tasks.push({
812
+ id: task.id,
813
+ title: task.title,
814
+ source: task.source,
815
+ status: taskStatus,
816
+ agent_role: assignedAgent,
817
+ issue_number: issueNumber,
818
+ issue_url: issueLink,
819
+ project_item_id: projectItemId,
820
+ project_status: projectStatus,
821
+ project_enabled: projectContractAvailable(projectContract),
822
+ task_origin: "checklist",
823
+ });
258
824
  }
259
- if (found && task.status === "closed" && found.state !== "CLOSED") {
260
- gh([
261
- "issue",
262
- "close",
263
- String(found.number),
264
- "--repo",
265
- orchestration.github.repository.slug,
266
- "--comment",
267
- "Closed from SDD task IR.",
268
- ]);
269
- syncResult.closed.push({ id: task.id, issue_number: found.number });
270
- }
271
-
272
- syncResult.tasks.push({
273
- id: task.id,
274
- title: task.title,
275
- source: task.source,
276
- status: task.status,
277
- issue_number: found?.number ?? null,
278
- issue_url: found
279
- ? \`https://github.com/\${orchestration.github.repository.slug}/issues/\${found.number}\`
280
- : null,
281
- });
282
825
  }
283
826
 
284
827
  writeJson(generatedPath("task-sync.json"), syncResult);
285
- writeJson(generatedPath("task-index.json"), { tasks: syncResult.tasks });
286
- console.log(\`synced_tasks=\${taskIr.tasks.length}\`);
828
+ writeJson(generatedPath("task-index.json"), {
829
+ generated_at: new Date().toISOString(),
830
+ project: syncResult.project,
831
+ tasks: syncResult.tasks,
832
+ });
833
+ console.log(\`sync_mode=\${syncResult.mode}\`);
834
+ console.log(\`synced_tasks=\${syncResult.tasks.length}\`);
287
835
  `;
288
836
  }
289
837
  function runQueueScript() {
290
838
  return `#!/usr/bin/env node
291
- import { generatedPath, loadTaskIr, normalizeProviderProfile, orchestrationConfig, writeJson } from "./runtime-lib.ts";
839
+ import {
840
+ classifyAgent,
841
+ generatedPath,
842
+ loadTaskIndex,
843
+ loadTaskIr,
844
+ normalizeTaskStatus,
845
+ orchestrationConfig,
846
+ pickProvider,
847
+ writeJson,
848
+ } from "./runtime-lib.ts";
292
849
 
293
850
  const orchestration = orchestrationConfig();
294
- const taskIr = loadTaskIr();
295
-
296
- function classifyAgent(title) {
297
- const lower = title.toLowerCase();
298
- if (lower.includes("api") || lower.includes("contract")) return "api";
299
- if (lower.includes("screen") || lower.includes("ui")) return "ui";
300
- if (lower.includes("verify") || lower.includes("test") || lower.includes("proof")) return "quality";
301
- if (lower.includes("workflow") || lower.includes("project") || lower.includes("github")) return "gitops";
302
- if (lower.includes("arch") || lower.includes("boundary") || lower.includes("structure")) return "architecture";
303
- if (lower.includes("plan") || lower.includes("spec")) return "specs";
304
- return "runtime";
305
- }
306
-
307
- function pickProvider(title) {
308
- const providers = (Array.isArray(orchestration.providers) ? orchestration.providers : []).map(normalizeProviderProfile);
309
- const lower = title.toLowerCase();
310
- if (lower.includes("verify") || lower.includes("test")) {
311
- return (
312
- providers.find((provider) => provider === "claude-cli" || provider === "anthropic-api") ||
313
- providers[0] ||
314
- "claude-cli"
315
- );
316
- }
317
- if (lower.includes("api") || lower.includes("contract")) {
318
- return (
319
- providers.find((provider) => provider === "openai-api" || provider === "anthropic-api") ||
320
- providers[0] ||
321
- "openai-api"
322
- );
323
- }
324
- return (
325
- providers.find((provider) => provider === "codex-cli" || provider === "openai-api") ||
326
- providers[0] ||
327
- "codex-cli"
328
- );
329
- }
851
+ const taskIndex = loadTaskIndex();
852
+ const indexedTasks = Array.isArray(taskIndex.tasks) ? taskIndex.tasks : [];
853
+ const tasks = indexedTasks.length > 0 ? indexedTasks : loadTaskIr().tasks;
330
854
 
331
- const openTasks = taskIr.tasks.filter((task) => task.status === "open");
332
- const queue = openTasks.map((task) => ({
333
- ...task,
334
- assigned_agent: classifyAgent(task.title),
335
- preferred_provider: pickProvider(task.title),
336
- }));
855
+ const queue = tasks
856
+ .filter((task) => normalizeTaskStatus(task.status) === "open")
857
+ .map((task) => {
858
+ const assignedAgent = String(task.agent_role || classifyAgent(task.title));
859
+ return {
860
+ ...task,
861
+ assigned_agent: assignedAgent,
862
+ agent_role: assignedAgent,
863
+ preferred_provider: pickProvider(task.title, orchestration.providers),
864
+ project_item_id: task.project_item_id ?? null,
865
+ project_status: task.project_status ?? null,
866
+ project_enabled: Boolean(task.project_enabled),
867
+ };
868
+ });
337
869
 
338
870
  const outputPath = generatedPath("agent-queue.json");
339
871
  writeJson(outputPath, { providers: orchestration.providers, queue });
@@ -343,19 +875,32 @@ console.log(\`queued=\${queue.length}\`);
343
875
  }
344
876
  function dispatchQueueScript() {
345
877
  return `#!/usr/bin/env node
346
- import { generatedPath, loadQueue, orchestrationConfig, writeJson } from "./runtime-lib.ts";
878
+ import { generatedPath, loadProjectContract, loadQueue, orchestrationConfig, writeJson } from "./runtime-lib.ts";
347
879
 
348
880
  const orchestration = orchestrationConfig();
881
+ const projectContract = loadProjectContract();
349
882
  const queuePayload = loadQueue();
350
883
 
351
884
  const dispatch = queuePayload.queue.map((task) => ({
352
885
  task_id: task.id,
353
886
  title: task.title,
354
887
  source: task.source,
888
+ source_refs: Array.isArray(task.source_refs) ? task.source_refs : [],
889
+ suggested_commands: Array.isArray(task.suggested_commands) ? task.suggested_commands : [],
890
+ target_paths_hint: Array.isArray(task.target_paths_hint) ? task.target_paths_hint : [],
891
+ priority: task.priority ?? null,
892
+ size: task.size ?? null,
893
+ task_origin: task.task_origin || "checklist",
355
894
  assigned_agent: task.assigned_agent,
895
+ agent_role: task.agent_role || task.assigned_agent,
356
896
  provider: task.preferred_provider,
357
897
  repository: orchestration.github.repository.slug,
358
- project_title: orchestration.github.project.title,
898
+ project_title: orchestration.github.project.title || projectContract?.project_title || "",
899
+ issue_number: task.issue_number ?? null,
900
+ issue_url: task.issue_url ?? null,
901
+ project_item_id: task.project_item_id ?? null,
902
+ project_status: task.project_status ?? null,
903
+ project_enabled: Boolean(task.project_enabled),
359
904
  execution_state: "planned",
360
905
  }));
361
906
 
@@ -373,11 +918,24 @@ function dispatchExecutorScript() {
373
918
  import fs from "node:fs";
374
919
  import path from "node:path";
375
920
  import { spawnSync } from "node:child_process";
376
- import { generatedPath, loadDispatchPlan, loadTaskIndex, normalizeProviderProfile, orchestrationConfig, writeJson } from "./runtime-lib.ts";
921
+ import {
922
+ buildProjectLogComment,
923
+ generatedPath,
924
+ githubAuthEnv,
925
+ loadDispatchPlan,
926
+ loadProjectContract,
927
+ loadTaskIndex,
928
+ normalizeProviderProfile,
929
+ orchestrationConfig,
930
+ setProjectAgentRole,
931
+ setProjectStatus,
932
+ writeJson,
933
+ } from "./runtime-lib.ts";
377
934
 
378
935
  const orchestration = orchestrationConfig();
379
936
  const dispatchPlan = loadDispatchPlan();
380
937
  const taskIndex = loadTaskIndex();
938
+ const projectContract = loadProjectContract();
381
939
  const executionDir = generatedPath("executions");
382
940
  const commandTimeoutMs = Number(process.env.AGENTIC_AGENT_TIMEOUT_MS || 30000);
383
941
  fs.mkdirSync(executionDir, { recursive: true });
@@ -391,17 +949,45 @@ function commentOnIssue(issueNumber, body) {
391
949
  return spawnSync(
392
950
  "gh",
393
951
  ["issue", "comment", String(issueNumber), "--repo", orchestration.github.repository.slug, "--body", body],
394
- { encoding: "utf-8" },
952
+ { encoding: "utf-8", env: githubAuthEnv() },
395
953
  );
396
954
  }
397
955
 
956
+ function updateProjectState(record, statusKey) {
957
+ if (!projectContract || !record.project_item_id) return false;
958
+ try {
959
+ setProjectAgentRole(projectContract, record.project_item_id, record.agent_role || record.assigned_agent);
960
+ setProjectStatus(projectContract, record.project_item_id, statusKey);
961
+ record.project_status = statusKey;
962
+ return true;
963
+ } catch (error) {
964
+ record.warnings.push("project_status_update_failed: " + String(error?.message || error));
965
+ return false;
966
+ }
967
+ }
968
+
398
969
  function buildPrompt(item, taskMeta) {
399
970
  return [
400
971
  \`You are the \${item.assigned_agent} agent working inside repository \${item.repository}.\`,
401
972
  \`GitHub Project: \${item.project_title}\`,
402
973
  \`Task id: \${item.task_id}\`,
403
974
  \`Task title: \${item.title}\`,
975
+ item.agent_role ? \`Agent role: \${item.agent_role}\` : "",
976
+ item.task_origin ? \`Task origin: \${item.task_origin}\` : "",
977
+ item.priority ? \`Priority: \${item.priority}\` : "",
978
+ item.size ? \`Size: \${item.size}\` : "",
979
+ item.project_item_id ? \`Project item id: \${item.project_item_id}\` : "",
980
+ item.project_status ? \`Current project status: \${item.project_status}\` : "",
404
981
  taskMeta?.source ? \`SDD source: \${taskMeta.source}\` : "",
982
+ Array.isArray(item.source_refs) && item.source_refs.length > 0
983
+ ? \`Source refs: \${item.source_refs.join(", ")}\`
984
+ : "",
985
+ Array.isArray(item.target_paths_hint) && item.target_paths_hint.length > 0
986
+ ? \`Target paths hint: \${item.target_paths_hint.join(", ")}\`
987
+ : "",
988
+ Array.isArray(item.suggested_commands) && item.suggested_commands.length > 0
989
+ ? \`Suggested commands: \${item.suggested_commands.join(", ")}\`
990
+ : "",
405
991
  "Follow the repository SDD workflow.",
406
992
  "Implement the task directly in the working tree when changes are required.",
407
993
  "Run relevant verification for the task.",
@@ -550,59 +1136,90 @@ async function runProvider(provider, prompt) {
550
1136
  const executions = [];
551
1137
 
552
1138
  async function main() {
553
- for (const item of dispatchPlan.dispatch) {
554
- const taskMeta = taskIndex.tasks.find((task) => task.id === item.task_id) || {};
555
- const executionPath = path.join(executionDir, \`\${item.task_id}.json\`);
556
- const prompt = buildPrompt(item, taskMeta);
557
- const startedAt = new Date().toISOString();
558
-
559
- const record = {
560
- ...item,
561
- issue_number: taskMeta.issue_number ?? null,
562
- issue_url: taskMeta.issue_url ?? null,
563
- started_at: startedAt,
564
- completed_at: null,
565
- prompt_path: path.relative(process.cwd(), executionPath.replace(/\\.json$/, ".prompt.txt")),
566
- execution_artifact: path.relative(process.cwd(), executionPath),
567
- adapter: "pending",
568
- execution_state: "running",
569
- exit_code: null,
570
- stdout: "",
571
- stderr: "",
572
- };
1139
+ for (const item of dispatchPlan.dispatch) {
1140
+ const taskMeta = taskIndex.tasks.find((task) => task.id === item.task_id) || {};
1141
+ const executionPath = path.join(executionDir, \`\${item.task_id}.json\`);
1142
+ const prompt = buildPrompt(item, taskMeta);
1143
+ const startedAt = new Date().toISOString();
1144
+
1145
+ const record = {
1146
+ ...item,
1147
+ issue_number: item.issue_number ?? taskMeta.issue_number ?? null,
1148
+ issue_url: item.issue_url ?? taskMeta.issue_url ?? null,
1149
+ project_item_id: item.project_item_id ?? taskMeta.project_item_id ?? null,
1150
+ project_status: item.project_status ?? taskMeta.project_status ?? null,
1151
+ agent_role: String(item.agent_role || taskMeta.agent_role || item.assigned_agent || ""),
1152
+ task_origin: item.task_origin || taskMeta.task_origin || "checklist",
1153
+ source_refs: Array.isArray(item.source_refs) ? item.source_refs : [],
1154
+ suggested_commands: Array.isArray(item.suggested_commands) ? item.suggested_commands : [],
1155
+ target_paths_hint: Array.isArray(item.target_paths_hint) ? item.target_paths_hint : [],
1156
+ priority: item.priority ?? null,
1157
+ size: item.size ?? null,
1158
+ started_at: startedAt,
1159
+ completed_at: null,
1160
+ prompt_path: path.relative(process.cwd(), executionPath.replace(/\\.json$/, ".prompt.txt")),
1161
+ execution_artifact: path.relative(process.cwd(), executionPath),
1162
+ adapter: "pending",
1163
+ execution_state: "running",
1164
+ exit_code: null,
1165
+ stdout: "",
1166
+ stderr: "",
1167
+ warnings: [],
1168
+ };
573
1169
 
574
- fs.writeFileSync(executionPath.replace(/\\.json$/, ".prompt.txt"), prompt + "\\n");
575
- const live = await runProvider(String(item.provider || ""), prompt);
576
- record.provider = normalizeProviderProfile(String(item.provider || ""));
577
- record.adapter = live.adapter;
578
- record.exit_code = live.result.status ?? null;
579
- record.stdout = String(live.result.stdout || "");
580
- record.stderr = String(live.result.stderr || "");
581
- record.execution_state = live.result.status === 0 ? "completed" : "failed";
582
- record.completed_at = new Date().toISOString();
583
-
584
- if (record.issue_number) {
585
- const comment = [
586
- "Agentic-dev dispatch execution result",
587
- \`Task: \${record.task_id}\`,
588
- \`Provider: \${record.provider}\`,
589
- \`Adapter: \${record.adapter}\`,
590
- \`State: \${record.execution_state}\`,
591
- \`Execution artifact: \${record.execution_artifact}\`,
592
- ].join("\\n");
593
- commentOnIssue(record.issue_number, comment);
594
- }
595
-
596
- fs.writeFileSync(executionPath, JSON.stringify(record, null, 2) + "\\n");
597
- executions.push(record);
598
- }
599
-
600
- writeJson(generatedPath("execution-journal.json"), {
601
- generated_at: new Date().toISOString(),
602
- executions,
603
- });
604
- console.log(\`executed=\${executions.length}\`);
605
- console.log(\`execution_journal=\${generatedPath("execution-journal.json")}\`);
1170
+ updateProjectState(record, "in_progress");
1171
+ fs.writeFileSync(executionPath.replace(/\\.json$/, ".prompt.txt"), prompt + "\\n");
1172
+
1173
+ const live = await runProvider(String(item.provider || ""), prompt);
1174
+ record.provider = normalizeProviderProfile(String(item.provider || ""));
1175
+ record.adapter = live.adapter;
1176
+ record.exit_code = live.result.status ?? null;
1177
+ record.stdout = String(live.result.stdout || "");
1178
+ record.stderr = String(live.result.stderr || "");
1179
+ record.execution_state = live.result.status === 0 ? "completed" : "failed";
1180
+ record.completed_at = new Date().toISOString();
1181
+
1182
+ if (record.execution_state === "completed") {
1183
+ updateProjectState(record, "in_review");
1184
+ } else if (record.project_item_id) {
1185
+ updateProjectState(record, "todo");
1186
+ }
1187
+
1188
+ if (record.issue_number) {
1189
+ const comment = projectContract
1190
+ ? buildProjectLogComment(projectContract, {
1191
+ agent_role: record.agent_role,
1192
+ status: record.execution_state === "completed" ? "in_review" : "in_progress",
1193
+ summary:
1194
+ "Dispatch finished via " +
1195
+ String(record.provider || "") +
1196
+ " (" +
1197
+ String(record.adapter || "") +
1198
+ "), state=" +
1199
+ String(record.execution_state || ""),
1200
+ next: record.execution_state === "completed" ? "quality" : "retry-or-manual-follow-up",
1201
+ })
1202
+ : [
1203
+ "Agentic-dev dispatch execution result",
1204
+ \`Task: \${record.task_id}\`,
1205
+ \`Provider: \${record.provider}\`,
1206
+ \`Adapter: \${record.adapter}\`,
1207
+ \`State: \${record.execution_state}\`,
1208
+ \`Execution artifact: \${record.execution_artifact}\`,
1209
+ ].join("\\n");
1210
+ commentOnIssue(record.issue_number, comment);
1211
+ }
1212
+
1213
+ fs.writeFileSync(executionPath, JSON.stringify(record, null, 2) + "\\n");
1214
+ executions.push(record);
1215
+ }
1216
+
1217
+ writeJson(generatedPath("execution-journal.json"), {
1218
+ generated_at: new Date().toISOString(),
1219
+ executions,
1220
+ });
1221
+ console.log(\`executed=\${executions.length}\`);
1222
+ console.log(\`execution_journal=\${generatedPath("execution-journal.json")}\`);
606
1223
  }
607
1224
 
608
1225
  main().catch((error) => {
@@ -613,11 +1230,22 @@ main().catch((error) => {
613
1230
  }
614
1231
  function closeTasksScript() {
615
1232
  return `#!/usr/bin/env node
616
- import { gh, ghJson, generatedPath, loadExecutionJournal, orchestrationConfig, writeJson } from "./runtime-lib.ts";
1233
+ import {
1234
+ generatedPath,
1235
+ gh,
1236
+ ghJson,
1237
+ loadExecutionJournal,
1238
+ loadProjectContract,
1239
+ orchestrationConfig,
1240
+ setProjectAgentRole,
1241
+ setProjectStatus,
1242
+ writeJson,
1243
+ } from "./runtime-lib.ts";
617
1244
 
618
1245
  const orchestration = orchestrationConfig();
619
1246
  const executionJournal = loadExecutionJournal();
620
- const issues = ghJson([
1247
+ const projectContract = loadProjectContract();
1248
+ const issuesPayload = ghJson([
621
1249
  "issue",
622
1250
  "list",
623
1251
  "--repo",
@@ -627,34 +1255,50 @@ const issues = ghJson([
627
1255
  "--limit",
628
1256
  "200",
629
1257
  "--json",
630
- "number,title",
1258
+ "number",
631
1259
  ]);
632
- const closed = [];
633
-
634
- const successfulTaskIds = new Set(
635
- executionJournal.executions
636
- .filter((execution) => execution.execution_state === "completed")
637
- .map((execution) => execution.task_id),
1260
+ const openIssueNumbers = new Set(
1261
+ (Array.isArray(issuesPayload) ? issuesPayload : []).map((issue) => issue.number),
638
1262
  );
1263
+ const closed = [];
1264
+ const warnings = [];
1265
+
1266
+ for (const execution of executionJournal.executions.filter((entry) => entry.execution_state === "completed")) {
1267
+ if (projectContract && execution.project_item_id) {
1268
+ try {
1269
+ setProjectAgentRole(projectContract, execution.project_item_id, execution.agent_role || execution.assigned_agent);
1270
+ setProjectStatus(projectContract, execution.project_item_id, "done");
1271
+ } catch (error) {
1272
+ warnings.push({
1273
+ id: execution.task_id,
1274
+ scope: "project-close",
1275
+ message: String(error?.message || error),
1276
+ });
1277
+ }
1278
+ }
639
1279
 
640
- for (const issue of issues) {
641
- const match = issue.title.match(/^\\[agentic-task\\] ([^ ]+) /);
642
- if (!match) continue;
643
- if (!successfulTaskIds.has(match[1])) continue;
644
- gh([
645
- "issue",
646
- "close",
647
- String(issue.number),
648
- "--repo",
649
- orchestration.github.repository.slug,
650
- "--comment",
651
- "Closed from SDD orchestration close pass.",
652
- ]);
653
- closed.push({ id: match[1], issue_number: issue.number });
1280
+ if (!execution.issue_number) continue;
1281
+ if (openIssueNumbers.has(execution.issue_number)) {
1282
+ gh([
1283
+ "issue",
1284
+ "close",
1285
+ String(execution.issue_number),
1286
+ "--repo",
1287
+ orchestration.github.repository.slug,
1288
+ "--comment",
1289
+ "Closed from SDD orchestration close pass.",
1290
+ ]);
1291
+ }
1292
+ closed.push({
1293
+ id: execution.task_id,
1294
+ issue_number: execution.issue_number,
1295
+ project_item_id: execution.project_item_id ?? null,
1296
+ project_status: execution.project_item_id ? "done" : null,
1297
+ });
654
1298
  }
655
1299
 
656
- writeJson(generatedPath("closed-tasks.json"), { closed });
657
- console.log(\`closed_candidates=\${successfulTaskIds.size}\`);
1300
+ writeJson(generatedPath("closed-tasks.json"), { closed, warnings });
1301
+ console.log(\`closed_candidates=\${closed.length}\`);
658
1302
  `;
659
1303
  }
660
1304
  function serverScript() {