agentic-dev 0.2.22 → 0.2.24

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() {
@@ -182,13 +511,30 @@ import fs from "node:fs";
182
511
  import path from "node:path";
183
512
  import { generatedPath, writeJson } from "./runtime-lib.ts";
184
513
 
514
+ const IGNORED_FILE_NAMES = new Set(["README.md", "INDEX.md"]);
515
+ const IGNORED_DIRECTORY_NAMES = new Set(["templates", "99_generated"]);
516
+
517
+ function shouldIncludeFile(file) {
518
+ const baseName = path.basename(file);
519
+ if (IGNORED_FILE_NAMES.has(baseName) || baseName.startsWith("_")) {
520
+ return false;
521
+ }
522
+ const relativePath = path.relative(planRoot, file);
523
+ const segments = relativePath.split(path.sep).filter(Boolean);
524
+ return !segments.some((segment) => IGNORED_DIRECTORY_NAMES.has(segment));
525
+ }
526
+
185
527
  function walk(root) {
186
528
  if (!fs.existsSync(root)) return [];
187
529
  const files = [];
188
530
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
189
531
  const full = path.join(root, entry.name);
190
- if (entry.isDirectory()) files.push(...walk(full));
191
- else if (entry.name.endsWith(".md")) files.push(full);
532
+ if (entry.isDirectory()) {
533
+ if (IGNORED_DIRECTORY_NAMES.has(entry.name)) continue;
534
+ files.push(...walk(full));
535
+ } else if (entry.name.endsWith(".md") && shouldIncludeFile(full)) {
536
+ files.push(full);
537
+ }
192
538
  }
193
539
  return files;
194
540
  }
@@ -220,120 +566,323 @@ console.log(\`tasks=\${tasks.length}\`);
220
566
  }
221
567
  function syncProjectTasksScript() {
222
568
  return `#!/usr/bin/env node
223
- import { gh, ghJson, loadTaskIr, orchestrationConfig, writeJson, generatedPath } from "./runtime-lib.ts";
569
+ import {
570
+ artifactContractPath,
571
+ ensureProjectItem,
572
+ findIssueForTask,
573
+ generatedPath,
574
+ gh,
575
+ ghJson,
576
+ hasArtifactBacklogKit,
577
+ issueNumberFromUrl,
578
+ issueUrl,
579
+ loadArtifactContract,
580
+ loadProjectContract,
581
+ loadRoleMap,
582
+ loadTaskCatalog,
583
+ loadTaskSyncState,
584
+ loadTaskIr,
585
+ normalizeTaskStatus,
586
+ orchestrationConfig,
587
+ parseTaskMeta,
588
+ projectContractAvailable,
589
+ projectContractPath,
590
+ projectItemIndex,
591
+ resolveAgentRole,
592
+ roleMapPath,
593
+ runArtifactBacklog,
594
+ setProjectAgentRole,
595
+ setProjectStatus,
596
+ taskIssueBody,
597
+ taskIssueTitle,
598
+ classifyAgent,
599
+ writeJson,
600
+ } from "./runtime-lib.ts";
224
601
 
225
602
  const orchestration = orchestrationConfig();
603
+ const repository = orchestration?.github?.repository?.slug;
604
+ if (!repository) {
605
+ throw new Error("github repository slug is not configured in .agentic-dev/orchestration.json");
606
+ }
607
+
226
608
  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: [] };
609
+ const artifactContract = loadArtifactContract();
610
+ const projectContract = loadProjectContract();
611
+ const roleMap = loadRoleMap();
612
+ function loadIssues() {
613
+ const payload = ghJson([
614
+ "issue",
615
+ "list",
616
+ "--repo",
617
+ repository,
618
+ "--state",
619
+ "all",
620
+ "--limit",
621
+ "200",
622
+ "--json",
623
+ "number,title,state,body,url",
624
+ ]);
625
+ return Array.isArray(payload) ? payload : [];
626
+ }
240
627
 
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 });
628
+ let existingIssues = loadIssues();
629
+ let itemIndex = projectContractAvailable(projectContract) ? projectItemIndex(projectContract) : new Map();
630
+ const syncResult = {
631
+ mode: "checklist",
632
+ artifact_backlog: null,
633
+ created: [],
634
+ updated: [],
635
+ reopened: [],
636
+ closed: [],
637
+ warnings: [],
638
+ project: {
639
+ enabled: projectContractAvailable(projectContract),
640
+ contract_path: projectContractAvailable(projectContract) ? projectContractPath() : null,
641
+ artifact_contract_path: artifactContract ? artifactContractPath() : null,
642
+ role_map_path: roleMapPath(),
643
+ },
644
+ tasks: [],
645
+ };
646
+
647
+ if (hasArtifactBacklogKit()) {
648
+ syncResult.mode = "artifact-backlog";
649
+ syncResult.artifact_backlog = runArtifactBacklog();
650
+ existingIssues = loadIssues();
651
+ itemIndex = projectContractAvailable(projectContract) ? projectItemIndex(projectContract) : new Map();
652
+
653
+ const catalog = loadTaskCatalog();
654
+ const syncState = loadTaskSyncState();
655
+ const catalogTasks = Array.isArray(catalog?.tasks) ? catalog.tasks : [];
656
+
657
+ for (const task of catalogTasks) {
658
+ const meta = parseTaskMeta(task.body);
659
+ const assignedAgent = resolveAgentRole(
660
+ roleMap,
661
+ String(task.agent_role || meta.agent_role || classifyAgent(task.title)),
662
+ );
663
+ const syncedIssue = syncState?.issues?.[task.key];
664
+ const mappedIssueNumber =
665
+ syncedIssue && typeof syncedIssue.number === "number" ? Number(syncedIssue.number) : null;
666
+ const found =
667
+ (mappedIssueNumber
668
+ ? existingIssues.find((issue) => Number(issue?.number) === mappedIssueNumber)
669
+ : null) ||
670
+ findIssueForTask(existingIssues, task.key);
671
+ const taskStatus = normalizeTaskStatus(found?.state === "CLOSED" ? "closed" : "open");
672
+ const issueNumber = found?.number ?? mappedIssueNumber ?? null;
673
+ const issueLink = found?.url || (issueNumber ? issueUrl(repository, issueNumber) : null);
674
+
675
+ let projectItemId = null;
676
+ let projectStatus = null;
677
+ if (issueNumber && projectContractAvailable(projectContract)) {
678
+ try {
679
+ const item = ensureProjectItem(projectContract, Number(issueNumber), itemIndex);
680
+ projectItemId = item?.id || null;
681
+ if (projectItemId) {
682
+ setProjectAgentRole(projectContract, projectItemId, assignedAgent);
683
+ projectStatus = taskStatus === "closed" ? "done" : "todo";
684
+ setProjectStatus(projectContract, projectItemId, projectStatus);
685
+ }
686
+ } catch (error) {
687
+ syncResult.warnings.push({
688
+ id: task.key,
689
+ scope: "artifact-project-sync",
690
+ message: String(error?.message || error),
691
+ });
692
+ }
693
+ }
694
+
695
+ syncResult.tasks.push({
696
+ id: task.key,
697
+ title: task.title,
698
+ source:
699
+ Array.isArray(meta.source_refs) && meta.source_refs.length > 0
700
+ ? String(meta.source_refs[0])
701
+ : "artifact:" + String(task.artifact_type || "task"),
702
+ status: taskStatus,
703
+ agent_role: assignedAgent,
704
+ issue_number: issueNumber,
705
+ issue_url: issueLink,
706
+ project_item_id: projectItemId,
707
+ project_status: projectStatus,
708
+ project_enabled: projectContractAvailable(projectContract),
709
+ task_origin: "artifact-backlog",
710
+ artifact_type: task.artifact_type || null,
711
+ source_refs: Array.isArray(meta.source_refs) ? meta.source_refs : [],
712
+ suggested_commands: Array.isArray(meta.suggested_commands) ? meta.suggested_commands : [],
713
+ target_paths_hint: Array.isArray(meta.target_paths_hint) ? meta.target_paths_hint : [],
714
+ priority: task.priority || null,
715
+ size: task.size || null,
716
+ });
717
+ }
718
+ }
719
+
720
+ if (syncResult.tasks.length === 0) {
721
+ syncResult.mode = "checklist";
722
+
723
+ for (const task of taskIr.tasks) {
724
+ const assignedAgent = resolveAgentRole(roleMap, classifyAgent(task.title));
725
+ const issueTitle = taskIssueTitle(task);
726
+ const issueBody = taskIssueBody(task, assignedAgent);
727
+ const taskStatus = normalizeTaskStatus(task.status);
728
+
729
+ let found = findIssueForTask(existingIssues, task.id);
730
+ let issueNumber = found?.number ?? null;
731
+ let issueLink = found?.url || (issueNumber ? issueUrl(repository, issueNumber) : null);
732
+
733
+ if (!found && taskStatus === "open") {
734
+ const createdUrl = gh([
735
+ "issue",
736
+ "create",
737
+ "--repo",
738
+ repository,
739
+ "--title",
740
+ issueTitle,
741
+ "--body",
742
+ issueBody,
743
+ ]);
744
+ issueNumber = issueNumberFromUrl(createdUrl);
745
+ issueLink = createdUrl || (issueNumber ? issueUrl(repository, issueNumber) : null);
746
+ found = {
747
+ number: issueNumber,
748
+ title: issueTitle,
749
+ state: "OPEN",
750
+ body: issueBody,
751
+ url: issueLink,
752
+ };
753
+ existingIssues.push(found);
754
+ syncResult.created.push({
755
+ id: task.id,
756
+ title: task.title,
757
+ issue_number: issueNumber,
758
+ issue_url: issueLink,
759
+ });
760
+ }
761
+
762
+ if (found && taskStatus === "open" && found.state === "CLOSED") {
763
+ gh([
764
+ "issue",
765
+ "reopen",
766
+ String(found.number),
767
+ "--repo",
768
+ repository,
769
+ ]);
770
+ found.state = "OPEN";
771
+ syncResult.reopened.push({ id: task.id, issue_number: found.number });
772
+ }
773
+
774
+ if (found && (String(found.title || "") !== issueTitle || String(found.body || "") !== issueBody)) {
775
+ gh([
776
+ "issue",
777
+ "edit",
778
+ String(found.number),
779
+ "--repo",
780
+ repository,
781
+ "--title",
782
+ issueTitle,
783
+ "--body",
784
+ issueBody,
785
+ ]);
786
+ found.title = issueTitle;
787
+ found.body = issueBody;
788
+ syncResult.updated.push({ id: task.id, issue_number: found.number });
789
+ }
790
+
791
+ if (found && taskStatus === "closed" && found.state !== "CLOSED") {
792
+ gh([
793
+ "issue",
794
+ "close",
795
+ String(found.number),
796
+ "--repo",
797
+ repository,
798
+ "--comment",
799
+ "Closed from SDD task IR.",
800
+ ]);
801
+ found.state = "CLOSED";
802
+ syncResult.closed.push({ id: task.id, issue_number: found.number });
803
+ }
804
+
805
+ issueNumber = found?.number ?? issueNumber ?? null;
806
+ issueLink = found?.url || issueLink || (issueNumber ? issueUrl(repository, issueNumber) : null);
807
+
808
+ let projectItemId = null;
809
+ let projectStatus = null;
810
+ if (issueNumber && projectContractAvailable(projectContract)) {
811
+ try {
812
+ const item = ensureProjectItem(projectContract, Number(issueNumber), itemIndex);
813
+ projectItemId = item?.id || null;
814
+ if (projectItemId) {
815
+ setProjectAgentRole(projectContract, projectItemId, assignedAgent);
816
+ projectStatus = taskStatus === "closed" ? "done" : "todo";
817
+ setProjectStatus(projectContract, projectItemId, projectStatus);
818
+ }
819
+ } catch (error) {
820
+ syncResult.warnings.push({
821
+ id: task.id,
822
+ scope: "project-sync",
823
+ message: String(error?.message || error),
824
+ });
825
+ }
826
+ }
827
+
828
+ syncResult.tasks.push({
829
+ id: task.id,
830
+ title: task.title,
831
+ source: task.source,
832
+ status: taskStatus,
833
+ agent_role: assignedAgent,
834
+ issue_number: issueNumber,
835
+ issue_url: issueLink,
836
+ project_item_id: projectItemId,
837
+ project_status: projectStatus,
838
+ project_enabled: projectContractAvailable(projectContract),
839
+ task_origin: "checklist",
840
+ });
258
841
  }
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
842
  }
283
843
 
284
844
  writeJson(generatedPath("task-sync.json"), syncResult);
285
- writeJson(generatedPath("task-index.json"), { tasks: syncResult.tasks });
286
- console.log(\`synced_tasks=\${taskIr.tasks.length}\`);
845
+ writeJson(generatedPath("task-index.json"), {
846
+ generated_at: new Date().toISOString(),
847
+ project: syncResult.project,
848
+ tasks: syncResult.tasks,
849
+ });
850
+ console.log(\`sync_mode=\${syncResult.mode}\`);
851
+ console.log(\`synced_tasks=\${syncResult.tasks.length}\`);
287
852
  `;
288
853
  }
289
854
  function runQueueScript() {
290
855
  return `#!/usr/bin/env node
291
- import { generatedPath, loadTaskIr, normalizeProviderProfile, orchestrationConfig, writeJson } from "./runtime-lib.ts";
856
+ import {
857
+ classifyAgent,
858
+ generatedPath,
859
+ loadTaskIndex,
860
+ loadTaskIr,
861
+ normalizeTaskStatus,
862
+ orchestrationConfig,
863
+ pickProvider,
864
+ writeJson,
865
+ } from "./runtime-lib.ts";
292
866
 
293
867
  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
- }
868
+ const taskIndex = loadTaskIndex();
869
+ const indexedTasks = Array.isArray(taskIndex.tasks) ? taskIndex.tasks : [];
870
+ const tasks = indexedTasks.length > 0 ? indexedTasks : loadTaskIr().tasks;
330
871
 
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
- }));
872
+ const queue = tasks
873
+ .filter((task) => normalizeTaskStatus(task.status) === "open")
874
+ .map((task) => {
875
+ const assignedAgent = String(task.agent_role || classifyAgent(task.title));
876
+ return {
877
+ ...task,
878
+ assigned_agent: assignedAgent,
879
+ agent_role: assignedAgent,
880
+ preferred_provider: pickProvider(task.title, orchestration.providers),
881
+ project_item_id: task.project_item_id ?? null,
882
+ project_status: task.project_status ?? null,
883
+ project_enabled: Boolean(task.project_enabled),
884
+ };
885
+ });
337
886
 
338
887
  const outputPath = generatedPath("agent-queue.json");
339
888
  writeJson(outputPath, { providers: orchestration.providers, queue });
@@ -343,19 +892,32 @@ console.log(\`queued=\${queue.length}\`);
343
892
  }
344
893
  function dispatchQueueScript() {
345
894
  return `#!/usr/bin/env node
346
- import { generatedPath, loadQueue, orchestrationConfig, writeJson } from "./runtime-lib.ts";
895
+ import { generatedPath, loadProjectContract, loadQueue, orchestrationConfig, writeJson } from "./runtime-lib.ts";
347
896
 
348
897
  const orchestration = orchestrationConfig();
898
+ const projectContract = loadProjectContract();
349
899
  const queuePayload = loadQueue();
350
900
 
351
901
  const dispatch = queuePayload.queue.map((task) => ({
352
902
  task_id: task.id,
353
903
  title: task.title,
354
904
  source: task.source,
905
+ source_refs: Array.isArray(task.source_refs) ? task.source_refs : [],
906
+ suggested_commands: Array.isArray(task.suggested_commands) ? task.suggested_commands : [],
907
+ target_paths_hint: Array.isArray(task.target_paths_hint) ? task.target_paths_hint : [],
908
+ priority: task.priority ?? null,
909
+ size: task.size ?? null,
910
+ task_origin: task.task_origin || "checklist",
355
911
  assigned_agent: task.assigned_agent,
912
+ agent_role: task.agent_role || task.assigned_agent,
356
913
  provider: task.preferred_provider,
357
914
  repository: orchestration.github.repository.slug,
358
- project_title: orchestration.github.project.title,
915
+ project_title: orchestration.github.project.title || projectContract?.project_title || "",
916
+ issue_number: task.issue_number ?? null,
917
+ issue_url: task.issue_url ?? null,
918
+ project_item_id: task.project_item_id ?? null,
919
+ project_status: task.project_status ?? null,
920
+ project_enabled: Boolean(task.project_enabled),
359
921
  execution_state: "planned",
360
922
  }));
361
923
 
@@ -373,11 +935,24 @@ function dispatchExecutorScript() {
373
935
  import fs from "node:fs";
374
936
  import path from "node:path";
375
937
  import { spawnSync } from "node:child_process";
376
- import { generatedPath, loadDispatchPlan, loadTaskIndex, normalizeProviderProfile, orchestrationConfig, writeJson } from "./runtime-lib.ts";
938
+ import {
939
+ buildProjectLogComment,
940
+ generatedPath,
941
+ githubAuthEnv,
942
+ loadDispatchPlan,
943
+ loadProjectContract,
944
+ loadTaskIndex,
945
+ normalizeProviderProfile,
946
+ orchestrationConfig,
947
+ setProjectAgentRole,
948
+ setProjectStatus,
949
+ writeJson,
950
+ } from "./runtime-lib.ts";
377
951
 
378
952
  const orchestration = orchestrationConfig();
379
953
  const dispatchPlan = loadDispatchPlan();
380
954
  const taskIndex = loadTaskIndex();
955
+ const projectContract = loadProjectContract();
381
956
  const executionDir = generatedPath("executions");
382
957
  const commandTimeoutMs = Number(process.env.AGENTIC_AGENT_TIMEOUT_MS || 30000);
383
958
  fs.mkdirSync(executionDir, { recursive: true });
@@ -391,17 +966,45 @@ function commentOnIssue(issueNumber, body) {
391
966
  return spawnSync(
392
967
  "gh",
393
968
  ["issue", "comment", String(issueNumber), "--repo", orchestration.github.repository.slug, "--body", body],
394
- { encoding: "utf-8" },
969
+ { encoding: "utf-8", env: githubAuthEnv() },
395
970
  );
396
971
  }
397
972
 
973
+ function updateProjectState(record, statusKey) {
974
+ if (!projectContract || !record.project_item_id) return false;
975
+ try {
976
+ setProjectAgentRole(projectContract, record.project_item_id, record.agent_role || record.assigned_agent);
977
+ setProjectStatus(projectContract, record.project_item_id, statusKey);
978
+ record.project_status = statusKey;
979
+ return true;
980
+ } catch (error) {
981
+ record.warnings.push("project_status_update_failed: " + String(error?.message || error));
982
+ return false;
983
+ }
984
+ }
985
+
398
986
  function buildPrompt(item, taskMeta) {
399
987
  return [
400
988
  \`You are the \${item.assigned_agent} agent working inside repository \${item.repository}.\`,
401
989
  \`GitHub Project: \${item.project_title}\`,
402
990
  \`Task id: \${item.task_id}\`,
403
991
  \`Task title: \${item.title}\`,
992
+ item.agent_role ? \`Agent role: \${item.agent_role}\` : "",
993
+ item.task_origin ? \`Task origin: \${item.task_origin}\` : "",
994
+ item.priority ? \`Priority: \${item.priority}\` : "",
995
+ item.size ? \`Size: \${item.size}\` : "",
996
+ item.project_item_id ? \`Project item id: \${item.project_item_id}\` : "",
997
+ item.project_status ? \`Current project status: \${item.project_status}\` : "",
404
998
  taskMeta?.source ? \`SDD source: \${taskMeta.source}\` : "",
999
+ Array.isArray(item.source_refs) && item.source_refs.length > 0
1000
+ ? \`Source refs: \${item.source_refs.join(", ")}\`
1001
+ : "",
1002
+ Array.isArray(item.target_paths_hint) && item.target_paths_hint.length > 0
1003
+ ? \`Target paths hint: \${item.target_paths_hint.join(", ")}\`
1004
+ : "",
1005
+ Array.isArray(item.suggested_commands) && item.suggested_commands.length > 0
1006
+ ? \`Suggested commands: \${item.suggested_commands.join(", ")}\`
1007
+ : "",
405
1008
  "Follow the repository SDD workflow.",
406
1009
  "Implement the task directly in the working tree when changes are required.",
407
1010
  "Run relevant verification for the task.",
@@ -550,59 +1153,90 @@ async function runProvider(provider, prompt) {
550
1153
  const executions = [];
551
1154
 
552
1155
  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
- };
1156
+ for (const item of dispatchPlan.dispatch) {
1157
+ const taskMeta = taskIndex.tasks.find((task) => task.id === item.task_id) || {};
1158
+ const executionPath = path.join(executionDir, \`\${item.task_id}.json\`);
1159
+ const prompt = buildPrompt(item, taskMeta);
1160
+ const startedAt = new Date().toISOString();
1161
+
1162
+ const record = {
1163
+ ...item,
1164
+ issue_number: item.issue_number ?? taskMeta.issue_number ?? null,
1165
+ issue_url: item.issue_url ?? taskMeta.issue_url ?? null,
1166
+ project_item_id: item.project_item_id ?? taskMeta.project_item_id ?? null,
1167
+ project_status: item.project_status ?? taskMeta.project_status ?? null,
1168
+ agent_role: String(item.agent_role || taskMeta.agent_role || item.assigned_agent || ""),
1169
+ task_origin: item.task_origin || taskMeta.task_origin || "checklist",
1170
+ source_refs: Array.isArray(item.source_refs) ? item.source_refs : [],
1171
+ suggested_commands: Array.isArray(item.suggested_commands) ? item.suggested_commands : [],
1172
+ target_paths_hint: Array.isArray(item.target_paths_hint) ? item.target_paths_hint : [],
1173
+ priority: item.priority ?? null,
1174
+ size: item.size ?? null,
1175
+ started_at: startedAt,
1176
+ completed_at: null,
1177
+ prompt_path: path.relative(process.cwd(), executionPath.replace(/\\.json$/, ".prompt.txt")),
1178
+ execution_artifact: path.relative(process.cwd(), executionPath),
1179
+ adapter: "pending",
1180
+ execution_state: "running",
1181
+ exit_code: null,
1182
+ stdout: "",
1183
+ stderr: "",
1184
+ warnings: [],
1185
+ };
573
1186
 
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")}\`);
1187
+ updateProjectState(record, "in_progress");
1188
+ fs.writeFileSync(executionPath.replace(/\\.json$/, ".prompt.txt"), prompt + "\\n");
1189
+
1190
+ const live = await runProvider(String(item.provider || ""), prompt);
1191
+ record.provider = normalizeProviderProfile(String(item.provider || ""));
1192
+ record.adapter = live.adapter;
1193
+ record.exit_code = live.result.status ?? null;
1194
+ record.stdout = String(live.result.stdout || "");
1195
+ record.stderr = String(live.result.stderr || "");
1196
+ record.execution_state = live.result.status === 0 ? "completed" : "failed";
1197
+ record.completed_at = new Date().toISOString();
1198
+
1199
+ if (record.execution_state === "completed") {
1200
+ updateProjectState(record, "in_review");
1201
+ } else if (record.project_item_id) {
1202
+ updateProjectState(record, "todo");
1203
+ }
1204
+
1205
+ if (record.issue_number) {
1206
+ const comment = projectContract
1207
+ ? buildProjectLogComment(projectContract, {
1208
+ agent_role: record.agent_role,
1209
+ status: record.execution_state === "completed" ? "in_review" : "in_progress",
1210
+ summary:
1211
+ "Dispatch finished via " +
1212
+ String(record.provider || "") +
1213
+ " (" +
1214
+ String(record.adapter || "") +
1215
+ "), state=" +
1216
+ String(record.execution_state || ""),
1217
+ next: record.execution_state === "completed" ? "quality" : "retry-or-manual-follow-up",
1218
+ })
1219
+ : [
1220
+ "Agentic-dev dispatch execution result",
1221
+ \`Task: \${record.task_id}\`,
1222
+ \`Provider: \${record.provider}\`,
1223
+ \`Adapter: \${record.adapter}\`,
1224
+ \`State: \${record.execution_state}\`,
1225
+ \`Execution artifact: \${record.execution_artifact}\`,
1226
+ ].join("\\n");
1227
+ commentOnIssue(record.issue_number, comment);
1228
+ }
1229
+
1230
+ fs.writeFileSync(executionPath, JSON.stringify(record, null, 2) + "\\n");
1231
+ executions.push(record);
1232
+ }
1233
+
1234
+ writeJson(generatedPath("execution-journal.json"), {
1235
+ generated_at: new Date().toISOString(),
1236
+ executions,
1237
+ });
1238
+ console.log(\`executed=\${executions.length}\`);
1239
+ console.log(\`execution_journal=\${generatedPath("execution-journal.json")}\`);
606
1240
  }
607
1241
 
608
1242
  main().catch((error) => {
@@ -613,11 +1247,22 @@ main().catch((error) => {
613
1247
  }
614
1248
  function closeTasksScript() {
615
1249
  return `#!/usr/bin/env node
616
- import { gh, ghJson, generatedPath, loadExecutionJournal, orchestrationConfig, writeJson } from "./runtime-lib.ts";
1250
+ import {
1251
+ generatedPath,
1252
+ gh,
1253
+ ghJson,
1254
+ loadExecutionJournal,
1255
+ loadProjectContract,
1256
+ orchestrationConfig,
1257
+ setProjectAgentRole,
1258
+ setProjectStatus,
1259
+ writeJson,
1260
+ } from "./runtime-lib.ts";
617
1261
 
618
1262
  const orchestration = orchestrationConfig();
619
1263
  const executionJournal = loadExecutionJournal();
620
- const issues = ghJson([
1264
+ const projectContract = loadProjectContract();
1265
+ const issuesPayload = ghJson([
621
1266
  "issue",
622
1267
  "list",
623
1268
  "--repo",
@@ -627,34 +1272,50 @@ const issues = ghJson([
627
1272
  "--limit",
628
1273
  "200",
629
1274
  "--json",
630
- "number,title",
1275
+ "number",
631
1276
  ]);
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),
1277
+ const openIssueNumbers = new Set(
1278
+ (Array.isArray(issuesPayload) ? issuesPayload : []).map((issue) => issue.number),
638
1279
  );
1280
+ const closed = [];
1281
+ const warnings = [];
1282
+
1283
+ for (const execution of executionJournal.executions.filter((entry) => entry.execution_state === "completed")) {
1284
+ if (projectContract && execution.project_item_id) {
1285
+ try {
1286
+ setProjectAgentRole(projectContract, execution.project_item_id, execution.agent_role || execution.assigned_agent);
1287
+ setProjectStatus(projectContract, execution.project_item_id, "done");
1288
+ } catch (error) {
1289
+ warnings.push({
1290
+ id: execution.task_id,
1291
+ scope: "project-close",
1292
+ message: String(error?.message || error),
1293
+ });
1294
+ }
1295
+ }
639
1296
 
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 });
1297
+ if (!execution.issue_number) continue;
1298
+ if (openIssueNumbers.has(execution.issue_number)) {
1299
+ gh([
1300
+ "issue",
1301
+ "close",
1302
+ String(execution.issue_number),
1303
+ "--repo",
1304
+ orchestration.github.repository.slug,
1305
+ "--comment",
1306
+ "Closed from SDD orchestration close pass.",
1307
+ ]);
1308
+ }
1309
+ closed.push({
1310
+ id: execution.task_id,
1311
+ issue_number: execution.issue_number,
1312
+ project_item_id: execution.project_item_id ?? null,
1313
+ project_status: execution.project_item_id ? "done" : null,
1314
+ });
654
1315
  }
655
1316
 
656
- writeJson(generatedPath("closed-tasks.json"), { closed });
657
- console.log(\`closed_candidates=\${successfulTaskIds.size}\`);
1317
+ writeJson(generatedPath("closed-tasks.json"), { closed, warnings });
1318
+ console.log(\`closed_candidates=\${closed.length}\`);
658
1319
  `;
659
1320
  }
660
1321
  function serverScript() {