@kody-ade/kody-engine 0.4.224 → 0.4.225

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 (114) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/kody.js +779 -831
  3. package/dist/executables/run/profile.json +1 -1
  4. package/dist/executables/types.ts +3 -1
  5. package/package.json +1 -1
  6. package/dist/duties/duty-scheduler/duty.md +0 -3
  7. package/dist/duties/duty-scheduler/profile.json +0 -6
  8. package/dist/duties/duty-tick/duty.md +0 -3
  9. package/dist/duties/duty-tick/profile.json +0 -6
  10. package/dist/duties/duty-tick-scripted/duty.md +0 -3
  11. package/dist/duties/duty-tick-scripted/profile.json +0 -6
  12. package/dist/duties/fix/duty.md +0 -10
  13. package/dist/duties/fix/profile.json +0 -6
  14. package/dist/duties/fix-ci/duty.md +0 -10
  15. package/dist/duties/fix-ci/profile.json +0 -6
  16. package/dist/duties/goal-scheduler/duty.md +0 -3
  17. package/dist/duties/goal-scheduler/profile.json +0 -6
  18. package/dist/duties/goal-tick/duty.md +0 -3
  19. package/dist/duties/goal-tick/profile.json +0 -6
  20. package/dist/duties/init/duty.md +0 -10
  21. package/dist/duties/init/profile.json +0 -6
  22. package/dist/duties/job-live-verify/duty.md +0 -3
  23. package/dist/duties/job-live-verify/profile.json +0 -6
  24. package/dist/duties/merge/duty.md +0 -10
  25. package/dist/duties/merge/profile.json +0 -6
  26. package/dist/duties/npm-publish/duty.md +0 -17
  27. package/dist/duties/npm-publish/profile.json +0 -6
  28. package/dist/duties/plan-verify/duty.md +0 -3
  29. package/dist/duties/plan-verify/profile.json +0 -6
  30. package/dist/duties/preview-build/duty.md +0 -10
  31. package/dist/duties/preview-build/profile.json +0 -6
  32. package/dist/duties/probe-skill/duty.md +0 -3
  33. package/dist/duties/probe-skill/profile.json +0 -6
  34. package/dist/duties/qa-goal/duty.md +0 -3
  35. package/dist/duties/qa-goal/profile.json +0 -6
  36. package/dist/duties/release/duty.md +0 -10
  37. package/dist/duties/release/profile.json +0 -6
  38. package/dist/duties/release-deploy/duty.md +0 -10
  39. package/dist/duties/release-deploy/profile.json +0 -6
  40. package/dist/duties/release-prepare/duty.md +0 -10
  41. package/dist/duties/release-prepare/profile.json +0 -6
  42. package/dist/duties/release-publish/duty.md +0 -10
  43. package/dist/duties/release-publish/profile.json +0 -6
  44. package/dist/duties/resolve/duty.md +0 -10
  45. package/dist/duties/resolve/profile.json +0 -6
  46. package/dist/duties/revert/duty.md +0 -10
  47. package/dist/duties/revert/profile.json +0 -6
  48. package/dist/duties/sync/duty.md +0 -10
  49. package/dist/duties/sync/profile.json +0 -6
  50. package/dist/duties/task-job-fail-once/duty.md +0 -3
  51. package/dist/duties/task-job-fail-once/profile.json +0 -6
  52. package/dist/duties/task-job-pass-a/duty.md +0 -3
  53. package/dist/duties/task-job-pass-a/profile.json +0 -6
  54. package/dist/duties/task-job-pass-b/duty.md +0 -3
  55. package/dist/duties/task-job-pass-b/profile.json +0 -6
  56. package/dist/duties/task-jobs/duty.md +0 -3
  57. package/dist/duties/task-jobs/profile.json +0 -6
  58. package/dist/duties/worker-ask/duty.md +0 -10
  59. package/dist/duties/worker-ask/profile.json +0 -6
  60. package/dist/executables/duty-scheduler/profile.json +0 -57
  61. package/dist/executables/duty-tick/profile.json +0 -78
  62. package/dist/executables/duty-tick/prompt.md +0 -77
  63. package/dist/executables/duty-tick/prompts/locked.md +0 -50
  64. package/dist/executables/duty-tick-scripted/profile.json +0 -72
  65. package/dist/executables/fix/profile.json +0 -91
  66. package/dist/executables/fix/prompt.md +0 -90
  67. package/dist/executables/fix-ci/profile.json +0 -71
  68. package/dist/executables/fix-ci/prompt.md +0 -78
  69. package/dist/executables/goal-scheduler/profile.json +0 -53
  70. package/dist/executables/goal-scheduler/scheduler.sh +0 -82
  71. package/dist/executables/goal-tick/profile.json +0 -69
  72. package/dist/executables/init/profile.json +0 -36
  73. package/dist/executables/job-live-verify/profile.json +0 -36
  74. package/dist/executables/job-live-verify/prompt.md +0 -42
  75. package/dist/executables/merge/profile.json +0 -54
  76. package/dist/executables/npm-publish/profile.json +0 -73
  77. package/dist/executables/npm-publish/publish.sh +0 -93
  78. package/dist/executables/plan-verify/profile.json +0 -38
  79. package/dist/executables/plan-verify/prompt.md +0 -72
  80. package/dist/executables/preview-build/profile.json +0 -61
  81. package/dist/executables/preview-health/profile.json +0 -48
  82. package/dist/executables/preview-health/tick.sh +0 -319
  83. package/dist/executables/probe-skill/profile.json +0 -35
  84. package/dist/executables/probe-skill/prompt.md +0 -31
  85. package/dist/executables/probe-skill/skills/probe-skill-marker/SKILL.md +0 -18
  86. package/dist/executables/qa-goal/profile.json +0 -65
  87. package/dist/executables/release/deploy.sh +0 -157
  88. package/dist/executables/release/prepare.sh +0 -353
  89. package/dist/executables/release/profile.json +0 -95
  90. package/dist/executables/release/publish.sh +0 -88
  91. package/dist/executables/release/release.sh +0 -230
  92. package/dist/executables/release/wait.sh +0 -88
  93. package/dist/executables/release-deploy/deploy.sh +0 -222
  94. package/dist/executables/release-deploy/profile.json +0 -53
  95. package/dist/executables/release-prepare/prepare.sh +0 -368
  96. package/dist/executables/release-prepare/profile.json +0 -70
  97. package/dist/executables/release-publish/profile.json +0 -53
  98. package/dist/executables/release-publish/publish.sh +0 -137
  99. package/dist/executables/resolve/apply-prefer.sh +0 -54
  100. package/dist/executables/resolve/profile.json +0 -107
  101. package/dist/executables/resolve/prompt.md +0 -51
  102. package/dist/executables/revert/profile.json +0 -82
  103. package/dist/executables/revert/revert.sh +0 -33
  104. package/dist/executables/sync/profile.json +0 -48
  105. package/dist/executables/task-job-fail-once/profile.json +0 -44
  106. package/dist/executables/task-job-fail-once/prompt.md +0 -1
  107. package/dist/executables/task-job-pass-a/profile.json +0 -43
  108. package/dist/executables/task-job-pass-a/prompt.md +0 -1
  109. package/dist/executables/task-job-pass-b/profile.json +0 -43
  110. package/dist/executables/task-job-pass-b/prompt.md +0 -1
  111. package/dist/executables/task-jobs/profile.json +0 -41
  112. package/dist/executables/task-jobs/prompt.md +0 -1
  113. package/dist/executables/worker-ask/profile.json +0 -70
  114. package/dist/executables/worker-ask/prompt.md +0 -60
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.224",
18
+ version: "0.4.225",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -205,6 +205,7 @@ function loadConfig(projectDir = process.cwd()) {
205
205
  agent: {
206
206
  model: String(agent.model),
207
207
  ...parsePerExecutable(agent.perExecutable),
208
+ ...parsePerExecutableReasoningEffort(agent.perExecutableReasoningEffort),
208
209
  ...parseAgentReasoningEffort(agent.reasoningEffort)
209
210
  },
210
211
  issueContext: parseIssueContext(raw.issueContext),
@@ -216,6 +217,7 @@ function loadConfig(projectDir = process.cwd()) {
216
217
  classify: parseClassifyConfig(raw.classify),
217
218
  release: parseReleaseConfig(raw.release),
218
219
  jobs: parseJobsConfig(raw.jobs),
220
+ company: parseCompanyConfig(raw.company),
219
221
  access: parseAccessConfig(raw.access)
220
222
  };
221
223
  }
@@ -262,6 +264,28 @@ function parseJobsConfig(raw) {
262
264
  }
263
265
  return Object.keys(out).length > 0 ? out : void 0;
264
266
  }
267
+ function parseCompanyConfig(raw) {
268
+ if (!raw || typeof raw !== "object") return void 0;
269
+ const r = raw;
270
+ const out = {};
271
+ if (r.activeDuties !== void 0) out.activeDuties = parseSlugArray(r.activeDuties, "company.activeDuties");
272
+ if (r.activeGoals !== void 0) out.activeGoals = parseSlugArray(r.activeGoals, "company.activeGoals");
273
+ return Object.keys(out).length > 0 ? out : void 0;
274
+ }
275
+ function parseSlugArray(raw, field) {
276
+ if (!Array.isArray(raw)) throw new Error(`kody.config.json: ${field} must be an array of strings`);
277
+ const out = [];
278
+ for (const value of raw) {
279
+ if (typeof value !== "string") throw new Error(`kody.config.json: ${field} entries must be strings`);
280
+ const slug = value.trim();
281
+ if (!slug) continue;
282
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) {
283
+ throw new Error(`kody.config.json: ${field} contains invalid slug "${value}"`);
284
+ }
285
+ out.push(slug);
286
+ }
287
+ return [...new Set(out)];
288
+ }
265
289
  function recordValue(raw) {
266
290
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0;
267
291
  }
@@ -282,6 +306,15 @@ function parsePerExecutable(raw) {
282
306
  }
283
307
  return Object.keys(out).length > 0 ? { perExecutable: out } : {};
284
308
  }
309
+ function parsePerExecutableReasoningEffort(raw) {
310
+ if (!raw || typeof raw !== "object") return {};
311
+ const out = {};
312
+ for (const [k, v] of Object.entries(raw)) {
313
+ const effort = typeof v === "string" ? parseReasoningEffort(v) : null;
314
+ if (effort) out[k] = effort;
315
+ }
316
+ return Object.keys(out).length > 0 ? { perExecutableReasoningEffort: out } : {};
317
+ }
285
318
  function parseAgentReasoningEffort(raw) {
286
319
  if (typeof raw !== "string") return {};
287
320
  return { reasoningEffort: parseReasoningEffort(raw) ?? void 0 };
@@ -562,7 +595,7 @@ var init_events = __esm({
562
595
  // src/verify.ts
563
596
  import { spawn } from "child_process";
564
597
  function runCommand(command, cwd) {
565
- return new Promise((resolve6) => {
598
+ return new Promise((resolve7) => {
566
599
  const start = Date.now();
567
600
  const child = spawn(command, {
568
601
  cwd,
@@ -591,11 +624,11 @@ function runCommand(command, cwd) {
591
624
  child.on("exit", (code) => {
592
625
  clearTimeout(timer);
593
626
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
594
- resolve6({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
627
+ resolve7({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
595
628
  });
596
629
  child.on("error", (err) => {
597
630
  clearTimeout(timer);
598
- resolve6({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
631
+ resolve7({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
599
632
  });
600
633
  });
601
634
  }
@@ -1450,7 +1483,7 @@ var init_dutyMcp = __esm({
1450
1483
  TRUST_STATE_BRANCH = "kody-state";
1451
1484
  THREAD_BODY_MAX = 4e3;
1452
1485
  CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
1453
- DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "goal-tick", "worker-ask", "chat"];
1486
+ DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "worker-ask", "chat"];
1454
1487
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
1455
1488
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
1456
1489
  GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
@@ -1513,7 +1546,7 @@ var init_repoWorkspace = __esm({
1513
1546
  defaultCloneRepo = (repo, token, dir) => {
1514
1547
  fs4.mkdirSync(path4.dirname(dir), { recursive: true });
1515
1548
  const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
1516
- return new Promise((resolve6, reject) => {
1549
+ return new Promise((resolve7, reject) => {
1517
1550
  const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
1518
1551
  stdio: "inherit"
1519
1552
  });
@@ -1529,7 +1562,7 @@ var init_repoWorkspace = __esm({
1529
1562
  spawnSync("git", ["-C", dir, "config", "user.email", email]);
1530
1563
  } catch {
1531
1564
  }
1532
- resolve6();
1565
+ resolve7();
1533
1566
  });
1534
1567
  child.on("error", reject);
1535
1568
  });
@@ -1814,10 +1847,10 @@ async function runAgent(opts) {
1814
1847
  let timer;
1815
1848
  let next;
1816
1849
  if (turnTimeoutMs > 0) {
1817
- const timeoutPromise = new Promise((resolve6) => {
1850
+ const timeoutPromise = new Promise((resolve7) => {
1818
1851
  timer = setTimeout(() => {
1819
1852
  timedOut = true;
1820
- resolve6({ done: true, value: void 0 });
1853
+ resolve7({ done: true, value: void 0 });
1821
1854
  }, turnTimeoutMs);
1822
1855
  });
1823
1856
  next = await Promise.race([nextPromise, timeoutPromise]);
@@ -2055,6 +2088,8 @@ function resolveCompanyStore() {
2055
2088
  return { repo, ref };
2056
2089
  }
2057
2090
  function fetchCompanyStore(repo, ref) {
2091
+ const localRoot = localStoreRoot(repo);
2092
+ if (localRoot) return localRoot;
2058
2093
  const url = repoToGitUrl(repo);
2059
2094
  const cacheDir = path6.join(cacheRoot(), cacheKey(repo, ref));
2060
2095
  try {
@@ -2075,6 +2110,12 @@ function fetchCompanyStore(repo, ref) {
2075
2110
  return null;
2076
2111
  }
2077
2112
  }
2113
+ function localStoreRoot(repo) {
2114
+ if (!path6.isAbsolute(repo) && !repo.startsWith(".")) return null;
2115
+ const root = path6.resolve(repo);
2116
+ if (!fs6.existsSync(path6.join(root, ".kody"))) return null;
2117
+ return root;
2118
+ }
2078
2119
  function repoToGitUrl(repo) {
2079
2120
  if (/^(https?:|ssh:|git@|file:)/.test(repo)) return repo;
2080
2121
  if (isGitHubShorthand(repo)) {
@@ -3205,6 +3246,7 @@ function parseClaudeCode(p, raw) {
3205
3246
  permissionMode,
3206
3247
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
3207
3248
  maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
3249
+ reasoningEffort: typeof r.reasoningEffort === "string" ? parseReasoningEffort(r.reasoningEffort) : null,
3208
3250
  maxTurnTimeoutSec: typeof r.maxTurnTimeoutSec === "number" ? r.maxTurnTimeoutSec : null,
3209
3251
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
3210
3252
  cacheable: r.cacheable === true,
@@ -3388,6 +3430,7 @@ var VALID_INPUT_TYPES, VALID_PERMISSION_MODES, VALID_ROLES, VALID_CONTAINER_CHIL
3388
3430
  var init_profile = __esm({
3389
3431
  "src/profile.ts"() {
3390
3432
  "use strict";
3433
+ init_config();
3391
3434
  init_dutyMcp();
3392
3435
  init_lifecycles();
3393
3436
  init_profile_error();
@@ -4554,6 +4597,91 @@ here \u2014 never substitute it for the truth.`;
4554
4597
  }
4555
4598
  });
4556
4599
 
4600
+ // src/dutyReport.ts
4601
+ function parseDutyReportsFromText(text) {
4602
+ const reports = [];
4603
+ for (const match of text.matchAll(REPORT_LINE)) {
4604
+ const raw = match[1]?.trim();
4605
+ if (!raw) continue;
4606
+ try {
4607
+ const parsed = parseDutyReport(JSON.parse(raw));
4608
+ if (parsed) reports.push(parsed);
4609
+ } catch {
4610
+ }
4611
+ }
4612
+ return reports;
4613
+ }
4614
+ function parseDutyReport(raw) {
4615
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4616
+ const obj = raw;
4617
+ const target = parseTarget(obj.target);
4618
+ if (!target) return null;
4619
+ const evidence = parseBooleanRecord(obj.evidence);
4620
+ const facts = parseFacts(obj.facts);
4621
+ if (!evidence && !facts) return null;
4622
+ return {
4623
+ target,
4624
+ ...evidence ? { evidence } : {},
4625
+ ...facts ? { facts } : {}
4626
+ };
4627
+ }
4628
+ function applyDutyReportToGoalState(state, report) {
4629
+ const priorFacts = parseFacts(state.extra.facts) ?? {};
4630
+ const nextFacts = { ...priorFacts };
4631
+ for (const [key, value] of Object.entries(report.facts ?? {})) {
4632
+ if (CONTROL_FACT_KEYS.has(key)) continue;
4633
+ nextFacts[key] = value;
4634
+ }
4635
+ for (const [key, value] of Object.entries(report.evidence ?? {})) {
4636
+ nextFacts[key] = value;
4637
+ }
4638
+ const pending = nextFacts.pendingEvidence;
4639
+ if (typeof pending === "string" && Object.hasOwn(report.evidence ?? {}, pending)) {
4640
+ delete nextFacts.pendingEvidence;
4641
+ }
4642
+ return {
4643
+ ...state,
4644
+ extra: {
4645
+ ...state.extra,
4646
+ facts: nextFacts
4647
+ }
4648
+ };
4649
+ }
4650
+ function parseTarget(raw) {
4651
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4652
+ const target = raw;
4653
+ if (target.type !== "goal" && target.type !== "task" && target.type !== "duty") return null;
4654
+ if (typeof target.id !== "string" || target.id.trim().length === 0) return null;
4655
+ return { type: target.type, id: target.id.trim() };
4656
+ }
4657
+ function parseBooleanRecord(raw) {
4658
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4659
+ const out = {};
4660
+ for (const [key, value] of Object.entries(raw)) {
4661
+ if (typeof key !== "string" || key.length === 0 || typeof value !== "boolean") return null;
4662
+ out[key] = value;
4663
+ }
4664
+ return out;
4665
+ }
4666
+ function parseFacts(raw) {
4667
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4668
+ const out = {};
4669
+ for (const [key, value] of Object.entries(raw)) {
4670
+ if (typeof key !== "string" || key.length === 0) return null;
4671
+ if (CONTROL_FACT_KEYS.has(key)) continue;
4672
+ out[key] = value;
4673
+ }
4674
+ return out;
4675
+ }
4676
+ var REPORT_LINE, CONTROL_FACT_KEYS;
4677
+ var init_dutyReport = __esm({
4678
+ "src/dutyReport.ts"() {
4679
+ "use strict";
4680
+ REPORT_LINE = /^KODY_DUTY_REPORT=(.+)$/gm;
4681
+ CONTROL_FACT_KEYS = /* @__PURE__ */ new Set(["blockers", "destination", "duties", "route", "stage", "state"]);
4682
+ }
4683
+ });
4684
+
4557
4685
  // src/lifecycleLabels.ts
4558
4686
  function groupOf(label) {
4559
4687
  const idx = label.indexOf(":");
@@ -5301,6 +5429,291 @@ var init_advanceFlow = __esm({
5301
5429
  }
5302
5430
  });
5303
5431
 
5432
+ // src/goal/manager.ts
5433
+ function evidenceSatisfied(goal, evidence) {
5434
+ return goal.facts[evidence] === true;
5435
+ }
5436
+ function firstMissingEvidence(goal) {
5437
+ return goal.destination.evidence.find((evidence) => !evidenceSatisfied(goal, evidence));
5438
+ }
5439
+ function pushBlocker(goal, reason) {
5440
+ if (!goal.blockers.includes(reason)) goal.blockers.push(reason);
5441
+ }
5442
+ function isSimpleGoal(goal) {
5443
+ return goal.type === SIMPLE_GOAL_TYPE;
5444
+ }
5445
+ function applySimpleGoalTaskSummary(goal, summary) {
5446
+ goal.facts.simpleAttachedTaskCount = summary.total;
5447
+ goal.facts.simpleOpenTaskCount = summary.open;
5448
+ goal.facts[SIMPLE_GOAL_EVIDENCE] = summary.total > 0 && summary.open === 0;
5449
+ }
5450
+ function isFactReference(value) {
5451
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5452
+ const record = value;
5453
+ return Object.keys(record).length === 1 && typeof record.fact === "string" && record.fact.length > 0;
5454
+ }
5455
+ function isCliArgValue(value) {
5456
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
5457
+ }
5458
+ function resolveRouteArgs(goal, step) {
5459
+ const cliArgs = {};
5460
+ for (const [name, value] of Object.entries(step.args ?? {})) {
5461
+ if (!isFactReference(value)) {
5462
+ cliArgs[name] = value;
5463
+ continue;
5464
+ }
5465
+ const factValue = goal.facts[value.fact];
5466
+ if (factValue === void 0 || factValue === null) {
5467
+ return { ok: false, reason: `route arg ${name} needs missing fact ${value.fact}` };
5468
+ }
5469
+ if (!isCliArgValue(factValue)) {
5470
+ return { ok: false, reason: `route arg ${name} needs scalar fact ${value.fact}` };
5471
+ }
5472
+ cliArgs[name] = factValue;
5473
+ }
5474
+ return { ok: true, cliArgs };
5475
+ }
5476
+ function planManagedGoalTick(goal) {
5477
+ goal.blockers = [];
5478
+ const missing = firstMissingEvidence(goal);
5479
+ if (!missing) {
5480
+ goal.stage = "done";
5481
+ delete goal.facts.pendingEvidence;
5482
+ return { kind: "done" };
5483
+ }
5484
+ const pending = goal.facts.pendingEvidence;
5485
+ if (pending === missing) {
5486
+ const stage = typeof goal.stage === "string" ? goal.stage : "waiting";
5487
+ return { kind: "wait", evidence: missing, stage, reason: `waiting for evidence: ${missing}` };
5488
+ }
5489
+ const step = goal.route.find((candidate) => candidate.evidence === missing);
5490
+ if (!step) {
5491
+ if (isSimpleGoal(goal) && missing === SIMPLE_GOAL_EVIDENCE) {
5492
+ goal.stage = "waiting";
5493
+ delete goal.facts.pendingEvidence;
5494
+ const total = typeof goal.facts.simpleAttachedTaskCount === "number" ? goal.facts.simpleAttachedTaskCount : 0;
5495
+ const open = typeof goal.facts.simpleOpenTaskCount === "number" ? goal.facts.simpleOpenTaskCount : 0;
5496
+ return {
5497
+ kind: "wait",
5498
+ evidence: missing,
5499
+ stage: "waiting",
5500
+ reason: total === 0 ? "waiting for labelled tasks" : `waiting for ${open} open labelled task(s)`
5501
+ };
5502
+ }
5503
+ const reason = `no route step for evidence: ${missing}`;
5504
+ goal.stage = "blocked";
5505
+ pushBlocker(goal, reason);
5506
+ return { kind: "blocked", evidence: missing, stage: "blocked", reason };
5507
+ }
5508
+ if (!goal.duties.includes(step.duty)) {
5509
+ const reason = `route duty ${step.duty} is not attached to this goal`;
5510
+ goal.stage = "blocked";
5511
+ pushBlocker(goal, reason);
5512
+ return { kind: "blocked", evidence: missing, stage: step.stage, reason };
5513
+ }
5514
+ const resolved = resolveRouteArgs(goal, step);
5515
+ if (!resolved.ok) {
5516
+ goal.stage = "blocked";
5517
+ pushBlocker(goal, resolved.reason);
5518
+ return { kind: "blocked", evidence: missing, stage: step.stage, reason: resolved.reason };
5519
+ }
5520
+ goal.stage = step.stage;
5521
+ goal.facts.pendingEvidence = missing;
5522
+ return {
5523
+ kind: "dispatch",
5524
+ evidence: missing,
5525
+ stage: step.stage,
5526
+ duty: step.duty,
5527
+ executable: step.executable,
5528
+ cliArgs: resolved.cliArgs
5529
+ };
5530
+ }
5531
+ function asStringArray(value) {
5532
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return null;
5533
+ return [...value];
5534
+ }
5535
+ function asRecord(value) {
5536
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5537
+ return { ...value };
5538
+ }
5539
+ function asRoute(value) {
5540
+ if (!Array.isArray(value)) return null;
5541
+ const route = [];
5542
+ for (const item of value) {
5543
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
5544
+ const raw = item;
5545
+ if (typeof raw.evidence !== "string" || typeof raw.stage !== "string" || typeof raw.duty !== "string") {
5546
+ return null;
5547
+ }
5548
+ const args = raw.args === void 0 ? void 0 : asRecord(raw.args);
5549
+ if (raw.args !== void 0 && !args) return null;
5550
+ route.push({
5551
+ evidence: raw.evidence,
5552
+ stage: raw.stage,
5553
+ duty: raw.duty,
5554
+ executable: typeof raw.executable === "string" ? raw.executable : void 0,
5555
+ args: args ?? void 0
5556
+ });
5557
+ }
5558
+ return route;
5559
+ }
5560
+ function managedGoalFromState(state) {
5561
+ const extra = state.extra;
5562
+ const destination = asRecord(extra.destination);
5563
+ const evidence = asStringArray(destination?.evidence);
5564
+ const duties = asStringArray(extra.duties);
5565
+ const route = asRoute(extra.route);
5566
+ const facts = asRecord(extra.facts);
5567
+ const blockers = asStringArray(extra.blockers);
5568
+ if (typeof extra.type !== "string" || !destination || typeof destination.outcome !== "string" || !evidence || !duties || !route || !facts || !blockers) {
5569
+ return null;
5570
+ }
5571
+ return {
5572
+ type: extra.type,
5573
+ destination: { outcome: destination.outcome, evidence },
5574
+ duties,
5575
+ route,
5576
+ stage: typeof extra.stage === "string" ? extra.stage : void 0,
5577
+ facts,
5578
+ blockers
5579
+ };
5580
+ }
5581
+ function writeManagedGoalToState(state, goal) {
5582
+ return {
5583
+ ...state,
5584
+ extra: {
5585
+ ...state.extra,
5586
+ type: goal.type,
5587
+ destination: goal.destination,
5588
+ duties: goal.duties,
5589
+ route: goal.route,
5590
+ stage: goal.stage,
5591
+ facts: goal.facts,
5592
+ blockers: goal.blockers
5593
+ }
5594
+ };
5595
+ }
5596
+ var SIMPLE_GOAL_TYPE, SIMPLE_GOAL_EVIDENCE;
5597
+ var init_manager = __esm({
5598
+ "src/goal/manager.ts"() {
5599
+ "use strict";
5600
+ SIMPLE_GOAL_TYPE = "simple";
5601
+ SIMPLE_GOAL_EVIDENCE = "labelledTasksComplete";
5602
+ }
5603
+ });
5604
+
5605
+ // src/goal/state.ts
5606
+ import * as fs24 from "fs";
5607
+ import * as path21 from "path";
5608
+ function parseGoalState(filePath, raw) {
5609
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5610
+ throw new GoalStateError(filePath, "must be JSON object");
5611
+ }
5612
+ const r = raw;
5613
+ const stateValue = r.state;
5614
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5615
+ throw new GoalStateError(
5616
+ filePath,
5617
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5618
+ );
5619
+ }
5620
+ const parsed = {
5621
+ state: stateValue,
5622
+ extra: {}
5623
+ };
5624
+ for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
5625
+ const v = r[ts];
5626
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5627
+ }
5628
+ const known = /* @__PURE__ */ new Set(["state", "updatedAt", "createdAt", "startedAt"]);
5629
+ for (const [k, v] of Object.entries(r)) {
5630
+ if (!known.has(k)) parsed.extra[k] = v;
5631
+ }
5632
+ return parsed;
5633
+ }
5634
+ function serializeGoalState(s) {
5635
+ const obj = { ...s.extra, state: s.state };
5636
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5637
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5638
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5639
+ return `${JSON.stringify(obj, null, 2)}
5640
+ `;
5641
+ }
5642
+ function nowIso() {
5643
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5644
+ }
5645
+ var VALID_STATES, GoalStateError;
5646
+ var init_state2 = __esm({
5647
+ "src/goal/state.ts"() {
5648
+ "use strict";
5649
+ VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5650
+ GoalStateError = class extends Error {
5651
+ constructor(path45, message) {
5652
+ super(`Invalid goal state at ${path45}:
5653
+ ${message}`);
5654
+ this.path = path45;
5655
+ this.name = "GoalStateError";
5656
+ }
5657
+ path;
5658
+ };
5659
+ }
5660
+ });
5661
+
5662
+ // src/scripts/advanceManagedGoal.ts
5663
+ function readSimpleGoalTaskSummary(goalId, cwd) {
5664
+ const raw = gh(
5665
+ ["issue", "list", "--state", "all", "--label", `goal:${goalId}`, "--limit", "1000", "--json", "number,state"],
5666
+ { cwd }
5667
+ );
5668
+ const issues = JSON.parse(raw);
5669
+ const total = issues.length;
5670
+ const open = issues.filter((issue) => String(issue.state ?? "").toLowerCase() === "open").length;
5671
+ return { total, open };
5672
+ }
5673
+ var advanceManagedGoal;
5674
+ var init_advanceManagedGoal = __esm({
5675
+ "src/scripts/advanceManagedGoal.ts"() {
5676
+ "use strict";
5677
+ init_manager();
5678
+ init_state2();
5679
+ init_issue();
5680
+ advanceManagedGoal = async (ctx) => {
5681
+ ctx.skipAgent = true;
5682
+ const goal = ctx.data.goal;
5683
+ if (!goal?.raw) {
5684
+ ctx.output.exitCode = 1;
5685
+ ctx.output.reason = "advanceManagedGoal requires loadGoalState first";
5686
+ return;
5687
+ }
5688
+ ctx.data.goalOriginalStateText = serializeGoalState(goal.raw);
5689
+ const managed = managedGoalFromState(goal.raw);
5690
+ if (!managed) {
5691
+ ctx.output.reason = "goal has no managed-goal contract; nothing to advance";
5692
+ return;
5693
+ }
5694
+ if (isSimpleGoal(managed)) {
5695
+ applySimpleGoalTaskSummary(managed, readSimpleGoalTaskSummary(goal.id, ctx.cwd));
5696
+ }
5697
+ const decision = planManagedGoalTick(managed);
5698
+ ctx.data.managedGoalDecision = decision;
5699
+ if (decision.kind === "done") {
5700
+ goal.state = "done";
5701
+ }
5702
+ goal.raw = writeManagedGoalToState({ ...goal.raw, state: goal.state }, managed);
5703
+ if (decision.kind === "blocked" || decision.kind === "wait" || decision.kind === "idle" || decision.kind === "done") {
5704
+ ctx.output.reason = decision.kind === "done" ? "managed goal complete" : decision.reason;
5705
+ return;
5706
+ }
5707
+ ctx.output.nextDispatch = {
5708
+ duty: decision.duty,
5709
+ executable: decision.executable,
5710
+ cliArgs: decision.cliArgs
5711
+ };
5712
+ ctx.output.reason = `dispatch ${decision.duty} for ${decision.evidence}`;
5713
+ };
5714
+ }
5715
+ });
5716
+
5304
5717
  // src/stateBranch.ts
5305
5718
  function is404(err) {
5306
5719
  const msg = err instanceof Error ? err.message : String(err);
@@ -5418,6 +5831,118 @@ var init_appendCompanyActivity = __esm({
5418
5831
  }
5419
5832
  });
5420
5833
 
5834
+ // src/goal/stateStore.ts
5835
+ function statePath(goalId) {
5836
+ return `.kody/goals/instances/${goalId}/state.json`;
5837
+ }
5838
+ function is4042(err) {
5839
+ const msg = err instanceof Error ? err.message : String(err);
5840
+ return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5841
+ }
5842
+ function fetchGoalState(owner, repo, goalId, cwd) {
5843
+ const filePath = statePath(goalId);
5844
+ let raw;
5845
+ try {
5846
+ raw = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5847
+ } catch (err) {
5848
+ if (is4042(err)) return null;
5849
+ throw err;
5850
+ }
5851
+ const o = JSON.parse(raw);
5852
+ if (!o.content) return null;
5853
+ const decoded = Buffer.from(o.content, "base64").toString("utf-8");
5854
+ return parseGoalState(filePath, JSON.parse(decoded));
5855
+ }
5856
+ function putGoalState(owner, repo, goalId, state, message, cwd) {
5857
+ ensureStateBranch(owner, repo, cwd);
5858
+ const filePath = statePath(goalId);
5859
+ const content = Buffer.from(serializeGoalState(state), "utf-8").toString("base64");
5860
+ for (let attempt = 1; attempt <= 3; attempt++) {
5861
+ let sha;
5862
+ try {
5863
+ const cur = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5864
+ const o = JSON.parse(cur);
5865
+ if (o.sha) sha = o.sha;
5866
+ } catch (err) {
5867
+ if (!is4042(err)) throw err;
5868
+ }
5869
+ const payload = { message, content, branch: STATE_BRANCH };
5870
+ if (sha) payload.sha = sha;
5871
+ try {
5872
+ gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
5873
+ cwd,
5874
+ input: JSON.stringify(payload)
5875
+ });
5876
+ return;
5877
+ } catch (err) {
5878
+ const msg = err instanceof Error ? err.message : String(err);
5879
+ const conflict = /HTTP 409/i.test(msg) || /HTTP 422/i.test(msg) || /does not match|but expected/i.test(msg);
5880
+ if (!conflict || attempt === 3) throw err;
5881
+ }
5882
+ }
5883
+ }
5884
+ var init_stateStore = __esm({
5885
+ "src/goal/stateStore.ts"() {
5886
+ "use strict";
5887
+ init_issue();
5888
+ init_stateBranch();
5889
+ init_state2();
5890
+ }
5891
+ });
5892
+
5893
+ // src/scripts/applyDutyReports.ts
5894
+ function collectReports(raw, agentResult) {
5895
+ const out = [];
5896
+ if (Array.isArray(raw)) {
5897
+ for (const item of raw) {
5898
+ if (isDutyReport(item)) out.push(item);
5899
+ }
5900
+ }
5901
+ if (agentResult?.finalText) out.push(...parseDutyReportsFromText(agentResult.finalText));
5902
+ return out;
5903
+ }
5904
+ function isDutyReport(raw) {
5905
+ return !!raw && typeof raw === "object" && !Array.isArray(raw) && !!raw.target && (raw.target.type === "goal" || raw.target.type === "task" || raw.target.type === "duty") && typeof raw.target.id === "string";
5906
+ }
5907
+ function describeMessage(report) {
5908
+ const keys = [
5909
+ ...Object.keys(report.evidence ?? {}).map((key) => `evidence:${key}`),
5910
+ ...Object.keys(report.facts ?? {}).map((key) => `fact:${key}`)
5911
+ ];
5912
+ return `chore(goals): apply duty report for ${report.target.id}${keys.length ? ` (${keys.join(", ")})` : ""}`;
5913
+ }
5914
+ var applyDutyReports;
5915
+ var init_applyDutyReports = __esm({
5916
+ "src/scripts/applyDutyReports.ts"() {
5917
+ "use strict";
5918
+ init_dutyReport();
5919
+ init_state2();
5920
+ init_stateStore();
5921
+ applyDutyReports = async (ctx, _profile, agentResult) => {
5922
+ const reports = collectReports(ctx.data.dutyReports, agentResult);
5923
+ if (reports.length === 0) return;
5924
+ const owner = ctx.config.github?.owner;
5925
+ const repo = ctx.config.github?.repo;
5926
+ if (!owner || !repo) {
5927
+ process.stderr.write("[kody duty-report] missing github owner/repo; cannot apply reports\n");
5928
+ return;
5929
+ }
5930
+ const goalReports = reports.filter((report) => report.target.type === "goal");
5931
+ for (const report of goalReports) {
5932
+ const prior = fetchGoalState(owner, repo, report.target.id, ctx.cwd);
5933
+ if (!prior) {
5934
+ process.stderr.write(`[kody duty-report] goal ${report.target.id} missing on kody-state; report skipped
5935
+ `);
5936
+ continue;
5937
+ }
5938
+ const next = applyDutyReportToGoalState(prior, report);
5939
+ if (serializeGoalState(next) === serializeGoalState(prior)) continue;
5940
+ putGoalState(owner, repo, report.target.id, { ...next, updatedAt: nowIso() }, describeMessage(report), ctx.cwd);
5941
+ }
5942
+ };
5943
+ }
5944
+ });
5945
+
5421
5946
  // src/coverage.ts
5422
5947
  import { execFileSync as execFileSync10 } from "child_process";
5423
5948
  function patternToRegex(pattern) {
@@ -5584,11 +6109,11 @@ var init_classifyByLabel = __esm({
5584
6109
  });
5585
6110
 
5586
6111
  // src/scripts/commitAndPush.ts
5587
- import * as fs24 from "fs";
5588
- import * as path21 from "path";
6112
+ import * as fs25 from "fs";
6113
+ import * as path22 from "path";
5589
6114
  function sentinelPathForStage(cwd, profileName) {
5590
6115
  const runId = resolveRunId();
5591
- return path21.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6116
+ return path22.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5592
6117
  }
5593
6118
  var DEFAULT_COMMIT_MESSAGE, commitAndPush2;
5594
6119
  var init_commitAndPush = __esm({
@@ -5605,9 +6130,9 @@ var init_commitAndPush = __esm({
5605
6130
  }
5606
6131
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
5607
6132
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
5608
- if (sentinel && fs24.existsSync(sentinel)) {
6133
+ if (sentinel && fs25.existsSync(sentinel)) {
5609
6134
  try {
5610
- const replay = JSON.parse(fs24.readFileSync(sentinel, "utf-8"));
6135
+ const replay = JSON.parse(fs25.readFileSync(sentinel, "utf-8"));
5611
6136
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
5612
6137
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
5613
6138
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5660,8 +6185,8 @@ var init_commitAndPush = __esm({
5660
6185
  const result = ctx.data.commitResult;
5661
6186
  if (sentinel && result?.committed) {
5662
6187
  try {
5663
- fs24.mkdirSync(path21.dirname(sentinel), { recursive: true });
5664
- fs24.writeFileSync(
6188
+ fs25.mkdirSync(path22.dirname(sentinel), { recursive: true });
6189
+ fs25.writeFileSync(
5665
6190
  sentinel,
5666
6191
  JSON.stringify(
5667
6192
  {
@@ -5682,148 +6207,17 @@ var init_commitAndPush = __esm({
5682
6207
  }
5683
6208
  });
5684
6209
 
5685
- // src/goal/state.ts
5686
- import * as fs25 from "fs";
5687
- import * as path22 from "path";
5688
- function parseGoalState(filePath, raw) {
5689
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5690
- throw new GoalStateError(filePath, "must be a JSON object");
5691
- }
5692
- const r = raw;
5693
- const stateValue = r.state;
5694
- if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5695
- throw new GoalStateError(
5696
- filePath,
5697
- `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5698
- );
5699
- }
5700
- const parsed = {
5701
- state: stateValue,
5702
- extra: {}
5703
- };
5704
- if (typeof r.mergeApproved === "boolean") {
5705
- parsed.mergeApproved = r.mergeApproved;
5706
- }
5707
- if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5708
- parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5709
- }
5710
- for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
5711
- const v = r[ts];
5712
- if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5713
- }
5714
- const known = /* @__PURE__ */ new Set(["state", "mergeApproved", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
5715
- for (const [k, v] of Object.entries(r)) {
5716
- if (!known.has(k)) parsed.extra[k] = v;
5717
- }
5718
- return parsed;
5719
- }
5720
- function serializeGoalState(s) {
5721
- const obj = { ...s.extra, state: s.state };
5722
- if (s.mergeApproved !== void 0) obj.mergeApproved = s.mergeApproved;
5723
- if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5724
- if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5725
- if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5726
- if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5727
- return `${JSON.stringify(obj, null, 2)}
5728
- `;
5729
- }
5730
- function nowIso() {
5731
- return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5732
- }
5733
- var VALID_STATES, GoalStateError;
5734
- var init_state2 = __esm({
5735
- "src/goal/state.ts"() {
5736
- "use strict";
5737
- VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5738
- GoalStateError = class extends Error {
5739
- constructor(path45, message) {
5740
- super(`Invalid goal state at ${path45}:
5741
- ${message}`);
5742
- this.path = path45;
5743
- this.name = "GoalStateError";
5744
- }
5745
- path;
5746
- };
5747
- }
5748
- });
5749
-
5750
- // src/goal/stateStore.ts
5751
- function statePath(goalId) {
5752
- return `.kody/goals/${goalId}/state.json`;
5753
- }
5754
- function is4042(err) {
5755
- const msg = err instanceof Error ? err.message : String(err);
5756
- return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5757
- }
5758
- function fetchGoalState(owner, repo, goalId, cwd) {
5759
- const filePath = statePath(goalId);
5760
- let raw;
5761
- try {
5762
- raw = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5763
- } catch (err) {
5764
- if (is4042(err)) return null;
5765
- throw err;
5766
- }
5767
- const o = JSON.parse(raw);
5768
- if (!o.content) return null;
5769
- const decoded = Buffer.from(o.content, "base64").toString("utf-8");
5770
- return parseGoalState(filePath, JSON.parse(decoded));
5771
- }
5772
- function putGoalState(owner, repo, goalId, state, message, cwd) {
5773
- ensureStateBranch(owner, repo, cwd);
5774
- const filePath = statePath(goalId);
5775
- const content = Buffer.from(serializeGoalState(state), "utf-8").toString("base64");
5776
- for (let attempt = 1; attempt <= 3; attempt++) {
5777
- let sha;
5778
- try {
5779
- const cur = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5780
- const o = JSON.parse(cur);
5781
- if (o.sha) sha = o.sha;
5782
- } catch (err) {
5783
- if (!is4042(err)) throw err;
5784
- }
5785
- const payload = { message, content, branch: STATE_BRANCH };
5786
- if (sha) payload.sha = sha;
5787
- try {
5788
- gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
5789
- cwd,
5790
- input: JSON.stringify(payload)
5791
- });
5792
- return;
5793
- } catch (err) {
5794
- const msg = err instanceof Error ? err.message : String(err);
5795
- const conflict = /HTTP 409/i.test(msg) || /HTTP 422/i.test(msg) || /does not match|but expected/i.test(msg);
5796
- if (!conflict || attempt === 3) throw err;
5797
- }
5798
- }
5799
- }
5800
- var init_stateStore = __esm({
5801
- "src/goal/stateStore.ts"() {
5802
- "use strict";
5803
- init_issue();
5804
- init_stateBranch();
5805
- init_state2();
5806
- }
5807
- });
5808
-
5809
6210
  // src/scripts/commitGoalState.ts
5810
6211
  function describeCommitMessage(goal) {
5811
- if (goal.state === "closed") return `chore(goals): abandon ${goal.id} (cleanup complete)`;
5812
- if (goal.state === "awaiting-merge") return `chore(goals): park ${goal.id} awaiting merge`;
5813
- if (goal.state === "done") return `chore(goals): mark ${goal.id} done`;
5814
- if (goal.lastDispatchedIssue !== void 0) {
5815
- return `chore(goals): dispatched #${goal.lastDispatchedIssue} for ${goal.id}`;
5816
- }
5817
- if (goal.phase === "in-flight") {
5818
- return `chore(goals): tick ${goal.id} (waiting for in-flight task)`;
5819
- }
5820
- return `chore(goals): tick ${goal.id} (idle)`;
6212
+ if (goal.state === "done") return `chore(goals): complete ${goal.id}`;
6213
+ return `chore(goals): update ${goal.id}`;
5821
6214
  }
5822
- var commitGoalState, STATE_BRANCH_LABEL;
6215
+ var STATE_BRANCH_LABEL, commitGoalState;
5823
6216
  var init_commitGoalState = __esm({
5824
6217
  "src/scripts/commitGoalState.ts"() {
5825
6218
  "use strict";
5826
6219
  init_stateStore();
6220
+ STATE_BRANCH_LABEL = "kody-state";
5827
6221
  commitGoalState = async (ctx) => {
5828
6222
  const goal = ctx.data.goal;
5829
6223
  if (!goal) return;
@@ -5833,7 +6227,7 @@ var init_commitGoalState = __esm({
5833
6227
  const owner = ctx.config.github?.owner;
5834
6228
  const repo = ctx.config.github?.repo;
5835
6229
  if (!owner || !repo) {
5836
- process.stderr.write(`[goal-tick] commitGoalState: missing github owner/repo; cannot persist ${goal.id}
6230
+ process.stderr.write(`[goal-manager] commitGoalState: missing github owner/repo; cannot persist ${goal.id}
5837
6231
  `);
5838
6232
  return;
5839
6233
  }
@@ -5841,12 +6235,11 @@ var init_commitGoalState = __esm({
5841
6235
  putGoalState(owner, repo, goal.id, updated, describeCommitMessage(goal), ctx.cwd);
5842
6236
  } catch (err) {
5843
6237
  process.stderr.write(
5844
- `[goal-tick] commitGoalState: persist to ${STATE_BRANCH_LABEL} failed (${err instanceof Error ? err.message : String(err)}); will retry next tick
6238
+ `[goal-manager] commitGoalState: persist to ${STATE_BRANCH_LABEL} failed (${err instanceof Error ? err.message : String(err)}); will retry next tick
5845
6239
  `
5846
6240
  );
5847
6241
  }
5848
6242
  };
5849
- STATE_BRANCH_LABEL = "kody-state";
5850
6243
  }
5851
6244
  });
5852
6245
 
@@ -6301,6 +6694,25 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
6301
6694
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
6302
6695
  return { number: Number(m[1]), url };
6303
6696
  }
6697
+ function buildQaManagedGoal(goalId, verdict, opened, failed) {
6698
+ return {
6699
+ type: "qa",
6700
+ destination: {
6701
+ outcome: `QA findings filed for ${goalId}`,
6702
+ evidence: ["qaFindingsFiled"]
6703
+ },
6704
+ duties: ["qa-goal"],
6705
+ route: [],
6706
+ stage: "filed",
6707
+ facts: {
6708
+ qaFindingsFiled: true,
6709
+ verdict,
6710
+ findingsOpened: opened,
6711
+ findingsFailed: failed
6712
+ },
6713
+ blockers: failed > 0 ? [`${failed} QA finding issue(s) failed to open`] : []
6714
+ };
6715
+ }
6304
6716
  async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
6305
6717
  const { markdown, data, jsonError } = splitReport(finalText);
6306
6718
  const verdict = detectVerdict(markdown);
@@ -6404,7 +6816,13 @@ ${markdown}`, ctx.cwd);
6404
6816
  }
6405
6817
  }
6406
6818
  const now = nowIso();
6407
- const goalState = { state: "active", startedAt: now, updatedAt: now, extra: { version: 1 } };
6819
+ const managedGoal = buildQaManagedGoal(goalId, verdict, opened.length, failed.length);
6820
+ const goalState = {
6821
+ state: "active",
6822
+ startedAt: now,
6823
+ updatedAt: now,
6824
+ extra: { version: 1, ...managedGoal }
6825
+ };
6408
6826
  try {
6409
6827
  putGoalState(
6410
6828
  ctx.config.github.owner,
@@ -6415,377 +6833,104 @@ ${markdown}`, ctx.cwd);
6415
6833
  ctx.cwd
6416
6834
  );
6417
6835
  } catch (err) {
6418
- process.stderr.write(
6419
- `[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
6420
- [createQaGoal] goal-scheduler will not see ${goalId} until this succeeds.
6421
- `
6422
- );
6423
- }
6424
- const repoUrl = `https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}`;
6425
- if (manifestIssueNumber !== null) {
6426
- const verb = manifestUpdated ? manifestCreated ? "OPENED" : "UPDATED" : "TARGETED";
6427
- process.stdout.write(
6428
- `
6429
- QA_GOAL_${verb}=${repoUrl}/issues/${manifestIssueNumber} (id: ${goalId}, verdict: ${verdict})
6430
- `
6431
- );
6432
- } else {
6433
- process.stdout.write(`
6434
- QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
6435
- `);
6436
- }
6437
- for (const o of opened) {
6438
- process.stdout.write(`QA_FINDING_OPENED=${o.url} (severity: ${o.severity})
6439
- `);
6440
- }
6441
- if (failed.length > 0) {
6442
- process.stdout.write(`QA_FINDINGS_FAILED=${failed.length} (see stderr above)
6443
- `);
6444
- }
6445
- ctx.data.action = qaAction(verdict, {
6446
- goalId,
6447
- manifestIssue: manifestIssueNumber ?? void 0,
6448
- findingsOpened: opened.length,
6449
- findingsFailed: failed.length,
6450
- mode: explicitGoal ? "goal-attach" : manifestCreated ? "goal-create" : "goal-append"
6451
- });
6452
- ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6453
- }
6454
- var MANIFEST_LABEL, MANIFEST_TITLE, MANIFEST_START, MANIFEST_END, FINDING_LABEL, REPORT_JSON_OPEN, REPORT_JSON_CLOSE, SEVERITY_COLORS, createQaGoal;
6455
- var init_createQaGoal = __esm({
6456
- "src/scripts/createQaGoal.ts"() {
6457
- "use strict";
6458
- init_state2();
6459
- init_stateStore();
6460
- init_issue();
6461
- init_postReviewResult();
6462
- MANIFEST_LABEL = "kody:goals-manifest";
6463
- MANIFEST_TITLE = "Kody Goals Manifest";
6464
- MANIFEST_START = "<!-- kody-goals-start -->";
6465
- MANIFEST_END = "<!-- kody-goals-end -->";
6466
- FINDING_LABEL = "kody:qa-finding";
6467
- REPORT_JSON_OPEN = "<!-- KODY_QA_REPORT_JSON";
6468
- REPORT_JSON_CLOSE = "-->";
6469
- SEVERITY_COLORS = {
6470
- P0: "b60205",
6471
- P1: "d93f0b",
6472
- P2: "fbca04",
6473
- P3: "0e8a16"
6474
- };
6475
- createQaGoal = async (ctx, _profile, agentResult) => {
6476
- if (!agentResult || agentResult.outcome !== "completed") {
6477
- const reason = agentResult?.error ?? "agent did not complete";
6478
- process.stderr.write(`[createQaGoal] ${reason}
6479
- `);
6480
- ctx.output.exitCode = 1;
6481
- ctx.output.reason = reason;
6482
- ctx.data.action = failedAction2(reason);
6483
- return;
6484
- }
6485
- const finalText = agentResult.finalText.trim();
6486
- if (!finalText) {
6487
- process.stderr.write("[createQaGoal] agent produced no report body\n");
6488
- ctx.output.exitCode = 1;
6489
- ctx.output.reason = "empty report body";
6490
- ctx.data.action = failedAction2("empty report body");
6491
- return;
6492
- }
6493
- const { markdown } = splitReport(finalText);
6494
- const verdict = detectVerdict(markdown);
6495
- const existingIssue = ctx.args.issue;
6496
- if (typeof existingIssue === "number" && existingIssue > 0) {
6497
- try {
6498
- postIssueComment(existingIssue, finalText, ctx.cwd);
6499
- } catch (err) {
6500
- const msg = err instanceof Error ? err.message : String(err);
6501
- ctx.output.exitCode = 4;
6502
- ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
6503
- ctx.data.action = failedAction2(ctx.output.reason);
6504
- return;
6505
- }
6506
- process.stdout.write(
6507
- `
6508
- QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
6509
- `
6510
- );
6511
- ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
6512
- ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6513
- return;
6514
- }
6515
- await promoteReportToGoal(ctx, finalText, ctx.args.scope, ctx.args.goal);
6516
- };
6517
- }
6518
- });
6519
-
6520
- // src/goal/labels.ts
6521
- function goalLabel(goalId) {
6522
- return `goal:${goalId}`;
6523
- }
6524
- var QA_GATE_LABEL;
6525
- var init_labels = __esm({
6526
- "src/goal/labels.ts"() {
6527
- "use strict";
6528
- QA_GATE_LABEL = "kody:qa-gate";
6529
- }
6530
- });
6531
-
6532
- // src/goal/operations.ts
6533
- function fail(err) {
6534
- if (err instanceof Error) {
6535
- const lines = err.message.split("\n").filter(Boolean);
6536
- return { ok: false, error: lines[0] ?? err.message };
6537
- }
6538
- return { ok: false, error: String(err) };
6539
- }
6540
- function listGoalIssues(goalId, cwd) {
6541
- try {
6542
- const out = gh(
6543
- [
6544
- "api",
6545
- `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
6546
- "--jq",
6547
- `[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), isQaGate: ([.labels[].name] | any(. == "${QA_GATE_LABEL}"))}]`
6548
- ],
6549
- { cwd }
6550
- );
6551
- return { ok: true, value: JSON.parse(out) };
6552
- } catch (err) {
6553
- return fail(err);
6554
- }
6555
- }
6556
- function listOpenPrs(cwd) {
6557
- try {
6558
- const out = gh(
6559
- ["pr", "list", "--state", "open", "--limit", "200", "--json", "number,url,isDraft,headRefName,baseRefName,body"],
6560
- { cwd }
6561
- );
6562
- return { ok: true, value: JSON.parse(out) };
6563
- } catch (err) {
6564
- return fail(err);
6565
- }
6566
- }
6567
- function pairIssuesWithPrs(issues, openPrs) {
6568
- const prByIssue = /* @__PURE__ */ new Map();
6569
- for (const pr of openPrs) claimPrForIssue(pr, prByIssue);
6570
- return issues.map((i) => {
6571
- const pr = prByIssue.get(i.number);
6572
- let prState = "absent";
6573
- if (pr) prState = pr.isDraft ? "draft" : "ready";
6574
- return { number: i.number, state: i.state, prState, isQaGate: i.isQaGate };
6575
- });
6576
- }
6577
- function claimPrForIssue(pr, prByIssue) {
6578
- for (const issueNum of extractClosesIssues(pr.body)) {
6579
- if (!prByIssue.has(issueNum)) {
6580
- prByIssue.set(issueNum, pr);
6581
- return;
6582
- }
6583
- }
6584
- const headMatch = pr.headRefName.match(/^(\d+)-/);
6585
- if (headMatch) {
6586
- const n = Number.parseInt(headMatch[1], 10);
6587
- if (Number.isFinite(n) && !prByIssue.has(n)) prByIssue.set(n, pr);
6588
- }
6589
- }
6590
- function extractClosesIssues(body) {
6591
- const out = [];
6592
- const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
6593
- for (let m = re.exec(body); m !== null; m = re.exec(body)) {
6594
- const n = Number.parseInt(m[1], 10);
6595
- if (Number.isFinite(n)) out.push(n);
6596
- }
6597
- return out;
6598
- }
6599
- function pickLeafPr(prs) {
6600
- if (prs.length === 0) return void 0;
6601
- const bases = new Set(prs.map((p) => p.baseRefName));
6602
- const leaves = prs.filter((p) => !bases.has(p.headRefName));
6603
- return leaves.sort((a, b) => b.number - a.number)[0];
6604
- }
6605
- function commentOnIssue(issueNumber, body, cwd) {
6606
- try {
6607
- gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
6608
- return { ok: true };
6609
- } catch (err) {
6610
- return fail(err);
6611
- }
6612
- }
6613
- function dispatchTaskRun(issueNumber, base, cwd) {
6614
- try {
6615
- gh(
6616
- [
6617
- "workflow",
6618
- "run",
6619
- GOAL_TASK_WORKFLOW,
6620
- "-f",
6621
- `issue_number=${issueNumber}`,
6622
- "-f",
6623
- "duty=classify",
6624
- "-f",
6625
- `base=${base}`
6626
- ],
6627
- { cwd }
6628
- );
6629
- return { ok: true };
6630
- } catch (err) {
6631
- return fail(err);
6632
- }
6633
- }
6634
- function closeIssue(issueNumber, options, cwd) {
6635
- try {
6636
- if (options.comment) {
6637
- gh(["issue", "comment", String(issueNumber), "--body", options.comment], { cwd });
6638
- }
6639
- const args = ["issue", "close", String(issueNumber)];
6640
- if (options.reason) args.push("--reason", options.reason);
6641
- gh(args, { cwd });
6642
- return { ok: true };
6643
- } catch (err) {
6644
- return fail(err);
6645
- }
6646
- }
6647
- function closePr(prNumber, comment, cwd) {
6648
- try {
6649
- gh(["pr", "close", String(prNumber), "--comment", comment], { cwd });
6650
- return { ok: true };
6651
- } catch (err) {
6652
- return fail(err);
6653
- }
6654
- }
6655
- function mergePrSquash(prNumber, cwd) {
6656
- try {
6657
- gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
6658
- return { ok: true };
6659
- } catch (err) {
6660
- return fail(err);
6661
- }
6662
- }
6663
- function editPrBase(prNumber, baseBranch, cwd) {
6664
- try {
6665
- gh(["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`], { cwd });
6666
- return { ok: true };
6667
- } catch (err) {
6668
- return fail(err);
6669
- }
6670
- }
6671
- function branchContains(leafHead, candidateHead, cwd) {
6672
- if (leafHead === candidateHead) return { ok: true, value: true };
6673
- try {
6674
- const out = gh(
6675
- [
6676
- "api",
6677
- `repos/{owner}/{repo}/compare/${encodeURIComponent(leafHead)}...${encodeURIComponent(candidateHead)}`,
6678
- "--jq",
6679
- ".ahead_by"
6680
- ],
6681
- { cwd }
6682
- );
6683
- return { ok: true, value: Number.parseInt(out.trim(), 10) === 0 };
6684
- } catch (err) {
6685
- return fail(err);
6836
+ process.stderr.write(
6837
+ `[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
6838
+ [createQaGoal] goal-scheduler will not see ${goalId} until this succeeds.
6839
+ `
6840
+ );
6686
6841
  }
6687
- }
6688
- function markPrReady(prNumber, cwd) {
6689
- try {
6690
- gh(["pr", "ready", String(prNumber)], { cwd });
6691
- return { ok: true };
6692
- } catch (err) {
6693
- return fail(err);
6842
+ const repoUrl = `https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}`;
6843
+ if (manifestIssueNumber !== null) {
6844
+ const verb = manifestUpdated ? manifestCreated ? "OPENED" : "UPDATED" : "TARGETED";
6845
+ process.stdout.write(
6846
+ `
6847
+ QA_GOAL_${verb}=${repoUrl}/issues/${manifestIssueNumber} (id: ${goalId}, verdict: ${verdict})
6848
+ `
6849
+ );
6850
+ } else {
6851
+ process.stdout.write(`
6852
+ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
6853
+ `);
6694
6854
  }
6695
- }
6696
- var GOAL_TASK_WORKFLOW;
6697
- var init_operations = __esm({
6698
- "src/goal/operations.ts"() {
6699
- "use strict";
6700
- init_issue();
6701
- init_labels();
6702
- GOAL_TASK_WORKFLOW = "kody.yml";
6855
+ for (const o of opened) {
6856
+ process.stdout.write(`QA_FINDING_OPENED=${o.url} (severity: ${o.severity})
6857
+ `);
6703
6858
  }
6704
- });
6705
-
6706
- // src/goal/phase.ts
6707
- function derivePhase(snap) {
6708
- if (!snap.lifecycleState) return "missing";
6709
- if (snap.lifecycleState === "abandoned") return "abandoned";
6710
- if (snap.lifecycleState === "closed" || snap.lifecycleState === "done") return "terminal";
6711
- if (snap.lifecycleState === "awaiting-merge") return "awaiting-merge";
6712
- const tasks = snap.childTasks.filter((t) => !t.isQaGate);
6713
- const qaGateOpen = snap.childTasks.some((t) => t.isQaGate && t.state === "OPEN");
6714
- const hasInFlight = tasks.some((t) => t.state === "OPEN" && t.prState === "draft");
6715
- if (hasInFlight) return "in-flight";
6716
- if (tasks.length === 0) return "idle";
6717
- const dispatchable = tasks.some((t) => t.state === "OPEN" && t.prState === "absent");
6718
- if (dispatchable) return "ready-to-dispatch";
6719
- const allDone = tasks.every((t) => t.state === "CLOSED" || t.prState === "ready");
6720
- if (allDone) return qaGateOpen ? "idle" : "all-done";
6721
- return "idle";
6722
- }
6723
- function pickNextDispatchable(snap) {
6724
- return snap.childTasks.filter((t) => !t.isQaGate && t.state === "OPEN" && t.prState === "absent").sort((a, b) => a.number - b.number)[0];
6725
- }
6726
- var init_phase = __esm({
6727
- "src/goal/phase.ts"() {
6728
- "use strict";
6859
+ if (failed.length > 0) {
6860
+ process.stdout.write(`QA_FINDINGS_FAILED=${failed.length} (see stderr above)
6861
+ `);
6729
6862
  }
6730
- });
6731
-
6732
- // src/scripts/deriveGoalPhase.ts
6733
- function filterGoalTaskPrs(prs, taskIssueNumbers) {
6734
- const taskSet = new Set(taskIssueNumbers);
6735
- return prs.filter((pr) => {
6736
- for (const n of extractClosesIssues(pr.body)) {
6737
- if (taskSet.has(n)) return true;
6738
- }
6739
- const headMatch = pr.headRefName.match(/^(\d+)-/);
6740
- if (headMatch) {
6741
- const n = Number.parseInt(headMatch[1], 10);
6742
- if (Number.isFinite(n) && taskSet.has(n)) return true;
6743
- }
6744
- return false;
6863
+ ctx.data.action = qaAction(verdict, {
6864
+ goalId,
6865
+ manifestIssue: manifestIssueNumber ?? void 0,
6866
+ findingsOpened: opened.length,
6867
+ findingsFailed: failed.length,
6868
+ mode: explicitGoal ? "goal-attach" : manifestCreated ? "goal-create" : "goal-append"
6745
6869
  });
6870
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6746
6871
  }
6747
- var deriveGoalPhase;
6748
- var init_deriveGoalPhase = __esm({
6749
- "src/scripts/deriveGoalPhase.ts"() {
6872
+ var MANIFEST_LABEL, MANIFEST_TITLE, MANIFEST_START, MANIFEST_END, FINDING_LABEL, REPORT_JSON_OPEN, REPORT_JSON_CLOSE, SEVERITY_COLORS, createQaGoal;
6873
+ var init_createQaGoal = __esm({
6874
+ "src/scripts/createQaGoal.ts"() {
6750
6875
  "use strict";
6751
- init_operations();
6752
- init_phase();
6753
- deriveGoalPhase = async (ctx) => {
6754
- const goal = ctx.data.goal;
6755
- if (!goal) return;
6756
- const issues = listGoalIssues(goal.id, ctx.cwd);
6757
- if (!issues.ok) {
6758
- process.stderr.write(`[goal-tick] deriveGoalPhase: list issues failed: ${issues.error}
6876
+ init_state2();
6877
+ init_stateStore();
6878
+ init_issue();
6879
+ init_postReviewResult();
6880
+ MANIFEST_LABEL = "kody:goals-manifest";
6881
+ MANIFEST_TITLE = "Kody Goals Manifest";
6882
+ MANIFEST_START = "<!-- kody-goals-start -->";
6883
+ MANIFEST_END = "<!-- kody-goals-end -->";
6884
+ FINDING_LABEL = "kody:qa-finding";
6885
+ REPORT_JSON_OPEN = "<!-- KODY_QA_REPORT_JSON";
6886
+ REPORT_JSON_CLOSE = "-->";
6887
+ SEVERITY_COLORS = {
6888
+ P0: "b60205",
6889
+ P1: "d93f0b",
6890
+ P2: "fbca04",
6891
+ P3: "0e8a16"
6892
+ };
6893
+ createQaGoal = async (ctx, _profile, agentResult) => {
6894
+ if (!agentResult || agentResult.outcome !== "completed") {
6895
+ const reason = agentResult?.error ?? "agent did not complete";
6896
+ process.stderr.write(`[createQaGoal] ${reason}
6759
6897
  `);
6760
- goal.childTasks = [];
6761
- goal.openTaskPrs = [];
6762
- goal.phase = "idle";
6898
+ ctx.output.exitCode = 1;
6899
+ ctx.output.reason = reason;
6900
+ ctx.data.action = failedAction2(reason);
6763
6901
  return;
6764
6902
  }
6765
- const rawIssues = issues.value ?? [];
6766
- const allPrs = listOpenPrs(ctx.cwd);
6767
- if (!allPrs.ok) {
6768
- process.stderr.write(`[goal-tick] deriveGoalPhase: list PRs failed: ${allPrs.error}
6769
- `);
6770
- goal.childTasks = rawIssues.map((i) => ({ ...i, prState: "absent" }));
6771
- goal.openTaskPrs = [];
6772
- goal.phase = "idle";
6903
+ const finalText = agentResult.finalText.trim();
6904
+ if (!finalText) {
6905
+ process.stderr.write("[createQaGoal] agent produced no report body\n");
6906
+ ctx.output.exitCode = 1;
6907
+ ctx.output.reason = "empty report body";
6908
+ ctx.data.action = failedAction2("empty report body");
6773
6909
  return;
6774
6910
  }
6775
- const taskPrs = filterGoalTaskPrs(
6776
- allPrs.value ?? [],
6777
- rawIssues.map((i) => i.number)
6778
- );
6779
- goal.openTaskPrs = taskPrs;
6780
- goal.leafPr = pickLeafPr(taskPrs);
6781
- goal.childTasks = pairIssuesWithPrs(rawIssues, taskPrs);
6782
- goal.phase = derivePhase({
6783
- lifecycleState: goal.state,
6784
- childTasks: goal.childTasks
6785
- });
6786
- process.stdout.write(
6787
- `[goal-tick] phase=${goal.phase} goal=${goal.id} tasks=${rawIssues.length} stack=${taskPrs.length}` + (goal.leafPr ? ` leaf=#${goal.leafPr.number}` : "") + "\n"
6788
- );
6911
+ const { markdown } = splitReport(finalText);
6912
+ const verdict = detectVerdict(markdown);
6913
+ const existingIssue = ctx.args.issue;
6914
+ if (typeof existingIssue === "number" && existingIssue > 0) {
6915
+ try {
6916
+ postIssueComment(existingIssue, finalText, ctx.cwd);
6917
+ } catch (err) {
6918
+ const msg = err instanceof Error ? err.message : String(err);
6919
+ ctx.output.exitCode = 4;
6920
+ ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
6921
+ ctx.data.action = failedAction2(ctx.output.reason);
6922
+ return;
6923
+ }
6924
+ process.stdout.write(
6925
+ `
6926
+ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
6927
+ `
6928
+ );
6929
+ ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
6930
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6931
+ return;
6932
+ }
6933
+ await promoteReportToGoal(ctx, finalText, ctx.args.scope, ctx.args.goal);
6789
6934
  };
6790
6935
  }
6791
6936
  });
@@ -8003,12 +8148,18 @@ function parseDutyFilter(raw) {
8003
8148
  function filterSlugs(slugs, onlyDuty) {
8004
8149
  return onlyDuty ? slugs.filter((slug) => slug === onlyDuty) : slugs;
8005
8150
  }
8006
- function listDutySlugs(roots) {
8151
+ function listActivatedDutySlugs(projectRoot, storeRoot, activeStoreDuties) {
8007
8152
  const seen = /* @__PURE__ */ new Set();
8008
8153
  const out = [];
8009
- for (const root of roots) {
8010
- for (const slug of listDutyFolderSlugs(root)) {
8011
- if (seen.has(slug)) continue;
8154
+ for (const slug of listDutyFolderSlugs(projectRoot)) {
8155
+ if (seen.has(slug)) continue;
8156
+ seen.add(slug);
8157
+ out.push(slug);
8158
+ }
8159
+ const active = new Set(activeStoreDuties ?? []);
8160
+ if (storeRoot && active.size > 0) {
8161
+ for (const slug of listDutyFolderSlugs(storeRoot)) {
8162
+ if (!active.has(slug) || seen.has(slug)) continue;
8012
8163
  seen.add(slug);
8013
8164
  out.push(slug);
8014
8165
  }
@@ -8086,8 +8237,10 @@ var init_dispatchDutyFileTicks = __esm({
8086
8237
  const onlyDuty = parseDutyFilter(ctx.args.duty);
8087
8238
  const jobsPath = path28.join(ctx.cwd, jobsDir);
8088
8239
  const storeJobsPath = getCompanyStoreDutiesRoot();
8089
- const dutyRoots = [jobsPath, ...storeJobsPath ? [storeJobsPath] : []];
8090
- const slugs = filterSlugs(listDutySlugs(dutyRoots), onlyDuty);
8240
+ const slugs = filterSlugs(
8241
+ listActivatedDutySlugs(jobsPath, storeJobsPath, ctx.config.company?.activeDuties),
8242
+ onlyDuty
8243
+ );
8091
8244
  ctx.data.jobSlugCount = slugs.length;
8092
8245
  if (slugs.length === 0) {
8093
8246
  const filter = onlyDuty ? ` matching ${onlyDuty}` : "";
@@ -8278,38 +8431,6 @@ var init_dispatchDutyTicks = __esm({
8278
8431
  }
8279
8432
  });
8280
8433
 
8281
- // src/scripts/dispatchNextTask.ts
8282
- var dispatchNextTask;
8283
- var init_dispatchNextTask = __esm({
8284
- "src/scripts/dispatchNextTask.ts"() {
8285
- "use strict";
8286
- init_operations();
8287
- init_phase();
8288
- dispatchNextTask = async (ctx) => {
8289
- const goal = ctx.data.goal;
8290
- if (!goal?.childTasks) return;
8291
- const next = pickNextDispatchable({
8292
- lifecycleState: goal.state,
8293
- childTasks: goal.childTasks
8294
- });
8295
- if (!next) {
8296
- process.stdout.write("[goal-tick] no dispatchable task \u2014 idle\n");
8297
- return;
8298
- }
8299
- const base = goal.leafPr?.headRefName ?? goal.defaultBranch;
8300
- process.stdout.write(`[goal-tick] dispatching #${next.number} via workflow_dispatch (classify, --base ${base})
8301
- `);
8302
- const res = dispatchTaskRun(next.number, base, ctx.cwd);
8303
- if (!res.ok) {
8304
- process.stderr.write(`[goal-tick] dispatchNextTask: workflow_dispatch on #${next.number} failed: ${res.error}
8305
- `);
8306
- return;
8307
- }
8308
- goal.lastDispatchedIssue = next.number;
8309
- };
8310
- }
8311
- });
8312
-
8313
8434
  // src/scripts/dispatchNextTaskJob.ts
8314
8435
  function taskJobToJob(job, issueArg) {
8315
8436
  const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
@@ -8687,133 +8808,6 @@ var init_failOnceTaskJob = __esm({
8687
8808
  }
8688
8809
  });
8689
8810
 
8690
- // src/scripts/finalizeGoal.ts
8691
- function prIssueNumbers(pr) {
8692
- const nums = new Set(extractClosesIssues(pr.body));
8693
- const headMatch = pr.headRefName.match(/^(\d+)-/);
8694
- if (headMatch) {
8695
- const n = Number.parseInt(headMatch[1], 10);
8696
- if (Number.isFinite(n)) nums.add(n);
8697
- }
8698
- return [...nums];
8699
- }
8700
- var finalizeGoal;
8701
- var init_finalizeGoal = __esm({
8702
- "src/scripts/finalizeGoal.ts"() {
8703
- "use strict";
8704
- init_operations();
8705
- finalizeGoal = async (ctx) => {
8706
- const goal = ctx.data.goal;
8707
- if (!goal) return;
8708
- process.stdout.write(`[goal-tick] all task(s) done \u2014 preparing deliverable PR for goal ${goal.id}
8709
- `);
8710
- const leaf = goal.leafPr;
8711
- if (!leaf) {
8712
- process.stdout.write(`[goal-tick] no leaf PR \u2014 marking goal done without a deliverable PR
8713
- `);
8714
- goal.state = "done";
8715
- return;
8716
- }
8717
- if (leaf.baseRefName !== goal.defaultBranch) {
8718
- process.stdout.write(
8719
- `[goal-tick] retargeting leaf PR #${leaf.number} base ${leaf.baseRefName} \u2192 ${goal.defaultBranch}
8720
- `
8721
- );
8722
- const retarget = editPrBase(leaf.number, goal.defaultBranch, ctx.cwd);
8723
- if (!retarget.ok) {
8724
- process.stderr.write(`[goal-tick] finalizeGoal: editPrBase #${leaf.number} failed: ${retarget.error}
8725
- `);
8726
- return;
8727
- }
8728
- }
8729
- if (leaf.isDraft) {
8730
- process.stdout.write(`[goal-tick] promoting draft leaf PR #${leaf.number} \u2192 ready-for-review
8731
- `);
8732
- const ready = markPrReady(leaf.number, ctx.cwd);
8733
- if (!ready.ok) {
8734
- process.stderr.write(`[goal-tick] finalizeGoal: markPrReady #${leaf.number} failed: ${ready.error}
8735
- `);
8736
- return;
8737
- }
8738
- }
8739
- process.stdout.write(
8740
- `[goal-tick] leaf PR #${leaf.number} is the deliverable (cumulative goal diff vs ${goal.defaultBranch}) \u2014 left open for human merge
8741
- `
8742
- );
8743
- const uncarriedIssues = /* @__PURE__ */ new Set();
8744
- const others = (goal.openTaskPrs ?? []).filter((p) => p.number !== leaf.number);
8745
- for (const pr of others) {
8746
- const contained = branchContains(leaf.headRefName, pr.headRefName, ctx.cwd);
8747
- if (!contained.ok || contained.value !== true) {
8748
- const why = contained.ok ? `commits on \`${pr.headRefName}\` are NOT reachable from the deliverable leaf \`${leaf.headRefName}\`` : `could not verify containment (${contained.error})`;
8749
- process.stderr.write(
8750
- `[goal-tick] finalizeGoal: NOT closing PR #${pr.number} \u2014 ${why}; leaving it open (broken stack)
8751
- `
8752
- );
8753
- for (const n of prIssueNumbers(pr)) uncarriedIssues.add(n);
8754
- commentOnIssue(
8755
- pr.number,
8756
- `\u26A0\uFE0F _Stacked-PR finalize: this PR's commits are **not** carried by the goal's deliverable PR #${leaf.number} (the stack chain was broken). Leaving this PR open so its work isn't lost \u2014 review and land it manually._`,
8757
- ctx.cwd
8758
- );
8759
- continue;
8760
- }
8761
- process.stdout.write(`[goal-tick] closing intermediate stacked PR #${pr.number} (carried by deliverable leaf)
8762
- `);
8763
- const closed = closePr(
8764
- pr.number,
8765
- `_Stacked-PR finalize: this PR's content is carried by the goal's single deliverable PR #${leaf.number} (open against \`${goal.defaultBranch}\`, awaiting human merge)._`,
8766
- ctx.cwd
8767
- );
8768
- if (!closed.ok) {
8769
- process.stderr.write(`[goal-tick] finalizeGoal: closePr #${pr.number} failed: ${closed.error}
8770
- `);
8771
- }
8772
- }
8773
- const leafIssues = new Set(prIssueNumbers(leaf));
8774
- const openIssues = (goal.childTasks ?? []).filter((t) => t.state === "OPEN");
8775
- for (const t of openIssues) {
8776
- if (uncarriedIssues.has(t.number)) {
8777
- process.stderr.write(
8778
- `[goal-tick] finalizeGoal: NOT closing task issue #${t.number} \u2014 its PR's work is not carried by the deliverable (broken stack)
8779
- `
8780
- );
8781
- continue;
8782
- }
8783
- if (leafIssues.has(t.number)) {
8784
- process.stdout.write(
8785
- `[goal-tick] leaving leaf task issue #${t.number} OPEN as the review anchor for deliverable PR #${leaf.number}
8786
- `
8787
- );
8788
- commentOnIssue(
8789
- t.number,
8790
- `_Goal \`${goal.id}\` finalized \u2014 this task's PR #${leaf.number} (open against \`${goal.defaultBranch}\`) is the goal's single deliverable and carries every task's changes. This issue stays open as the review anchor; merge PR #${leaf.number} to ship._`,
8791
- ctx.cwd
8792
- );
8793
- continue;
8794
- }
8795
- process.stdout.write(
8796
- `[goal-tick] closing task issue #${t.number} (goal finalized \u2014 carried by PR #${leaf.number})
8797
- `
8798
- );
8799
- const closed = closeIssue(
8800
- t.number,
8801
- {
8802
- comment: `_Goal \`${goal.id}\` finalized \u2014 its single deliverable PR #${leaf.number} (open against \`${goal.defaultBranch}\`) carries this task's changes. Merge that PR to ship._`,
8803
- reason: "completed"
8804
- },
8805
- ctx.cwd
8806
- );
8807
- if (!closed.ok) {
8808
- process.stderr.write(`[goal-tick] finalizeGoal: closeIssue #${t.number} failed: ${closed.error}
8809
- `);
8810
- }
8811
- }
8812
- goal.state = "done";
8813
- };
8814
- }
8815
- });
8816
-
8817
8811
  // src/scripts/finalizeTerminal.ts
8818
8812
  var DONE, FAILED, finalizeTerminal;
8819
8813
  var init_finalizeTerminal = __esm({
@@ -9363,58 +9357,6 @@ var init_fixFlow = __esm({
9363
9357
  }
9364
9358
  });
9365
9359
 
9366
- // src/scripts/handleAbandonedGoal.ts
9367
- var handleAbandonedGoal;
9368
- var init_handleAbandonedGoal = __esm({
9369
- "src/scripts/handleAbandonedGoal.ts"() {
9370
- "use strict";
9371
- init_operations();
9372
- handleAbandonedGoal = async (ctx) => {
9373
- const goal = ctx.data.goal;
9374
- if (!goal || goal.state !== "abandoned") return;
9375
- process.stdout.write(`[goal-tick] ${goal.id} is abandoned \u2014 running cleanup
9376
- `);
9377
- const issues = listGoalIssues(goal.id, ctx.cwd);
9378
- if (!issues.ok) {
9379
- process.stderr.write(`[goal-tick] handleAbandonedGoal: list issues failed: ${issues.error}
9380
- `);
9381
- } else {
9382
- const issueNumbers = /* @__PURE__ */ new Set();
9383
- for (const i of issues.value ?? []) {
9384
- if (i.state !== "OPEN") continue;
9385
- issueNumbers.add(i.number);
9386
- const r = closeIssue(
9387
- i.number,
9388
- {
9389
- comment: "_Goal abandoned \u2014 closing this task without dispatch._",
9390
- reason: "not planned"
9391
- },
9392
- ctx.cwd
9393
- );
9394
- if (!r.ok) {
9395
- process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close #${i.number}: ${r.error}
9396
- `);
9397
- }
9398
- }
9399
- const prs = listOpenPrs(ctx.cwd);
9400
- if (prs.ok) {
9401
- for (const pr of prs.value ?? []) {
9402
- const headMatch = pr.headRefName.match(/^(\d+)-/);
9403
- const headIssue = headMatch ? Number.parseInt(headMatch[1], 10) : NaN;
9404
- if (!Number.isFinite(headIssue) || !issueNumbers.has(headIssue)) continue;
9405
- const r = closePr(pr.number, "_Goal abandoned \u2014 closing stacked PR._", ctx.cwd);
9406
- if (!r.ok) {
9407
- process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close PR #${pr.number}: ${r.error}
9408
- `);
9409
- }
9410
- }
9411
- }
9412
- }
9413
- goal.state = "closed";
9414
- };
9415
- }
9416
- });
9417
-
9418
9360
  // src/scripts/initFlow.ts
9419
9361
  import { execFileSync as execFileSync17 } from "child_process";
9420
9362
  import * as fs31 from "fs";
@@ -9788,7 +9730,7 @@ var init_loadGoalState = __esm({
9788
9730
  try {
9789
9731
  const state = fetchGoalState(owner, repo, goalId, ctx.cwd);
9790
9732
  if (!state) {
9791
- process.stdout.write(`[goal-tick] no goal state for ${goalId} on ${owner}/${repo} \u2014 nothing to tick
9733
+ process.stdout.write(`[goal-manager] no goal state for ${goalId} on ${owner}/${repo}; nothing to tick
9792
9734
  `);
9793
9735
  ctx.skipAgent = true;
9794
9736
  ctx.output.exitCode = 0;
@@ -9798,17 +9740,11 @@ var init_loadGoalState = __esm({
9798
9740
  ctx.data.goal = {
9799
9741
  id: goalId,
9800
9742
  state: state.state,
9801
- lastDispatchedIssue: state.lastDispatchedIssue,
9802
- // Cache the full parsed object so saveGoalState can preserve `extra`.
9803
9743
  raw: state,
9804
- // `phase`, `childTasks`, `openTaskPrs`, `leafPr` are populated by
9805
- // deriveGoalPhase later in the chain. Initialize to undefined so
9806
- // runWhen on `data.goal.phase` can match correctly.
9807
- phase: void 0,
9808
9744
  defaultBranch: ctx.config.git.defaultBranch
9809
9745
  };
9810
9746
  } catch (err) {
9811
- process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
9747
+ process.stdout.write(`[goal-manager] ${err instanceof Error ? err.message : String(err)}
9812
9748
  `);
9813
9749
  ctx.skipAgent = true;
9814
9750
  ctx.output.exitCode = 0;
@@ -9933,23 +9869,7 @@ var init_staff = __esm({
9933
9869
  "use strict";
9934
9870
  init_companyStore();
9935
9871
  DEFAULT_STAFF_DIR = ".kody/staff";
9936
- BUILTIN_PERSONAS = {
9937
- kody: [
9938
- "You are **kody**, the repository's autonomous engineer.",
9939
- "",
9940
- "- You work the way this repo already works: follow its conventions, its",
9941
- " existing patterns, and its tests. Read before you write.",
9942
- "- You ship small, correct, reviewable changes. You don't refactor beyond the",
9943
- " task, and you don't add scope nobody asked for.",
9944
- "- You are honest about outcomes: if something failed, didn't run, or you're",
9945
- " unsure, you say so plainly rather than papering over it.",
9946
- "- The request that triggered you IS your authorization to do the work:",
9947
- " opening a PR, pushing commits, and commenting are exactly what you're for \u2014",
9948
- " do them without waiting for extra approval.",
9949
- "- You still don't weaken security, delete work you didn't create, or take",
9950
- " destructive actions beyond the task you were given."
9951
- ].join("\n")
9952
- };
9872
+ BUILTIN_PERSONAS = {};
9953
9873
  }
9954
9874
  });
9955
9875
 
@@ -10402,6 +10322,37 @@ var init_markFlowSuccess = __esm({
10402
10322
  }
10403
10323
  });
10404
10324
 
10325
+ // src/goal/operations.ts
10326
+ function fail(err) {
10327
+ if (err instanceof Error) {
10328
+ const lines = err.message.split("\n").filter(Boolean);
10329
+ return { ok: false, error: lines[0] ?? err.message };
10330
+ }
10331
+ return { ok: false, error: String(err) };
10332
+ }
10333
+ function commentOnIssue(issueNumber, body, cwd) {
10334
+ try {
10335
+ gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
10336
+ return { ok: true };
10337
+ } catch (err) {
10338
+ return fail(err);
10339
+ }
10340
+ }
10341
+ function mergePrSquash(prNumber, cwd) {
10342
+ try {
10343
+ gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
10344
+ return { ok: true };
10345
+ } catch (err) {
10346
+ return fail(err);
10347
+ }
10348
+ }
10349
+ var init_operations = __esm({
10350
+ "src/goal/operations.ts"() {
10351
+ "use strict";
10352
+ init_issue();
10353
+ }
10354
+ });
10355
+
10405
10356
  // src/scripts/mergeFlow.ts
10406
10357
  function readPr(prNumber, cwd) {
10407
10358
  const out = gh(["pr", "view", String(prNumber), "--json", "state,isDraft,mergeable,mergeStateStatus,title,url"], {
@@ -12159,7 +12110,7 @@ var init_previewBuildHelpers = __esm({
12159
12110
  // src/scripts/previewBuildRun.ts
12160
12111
  import { spawn as spawn4 } from "child_process";
12161
12112
  async function runCmd(cmd, args, opts = {}) {
12162
- await new Promise((resolve6, reject) => {
12113
+ await new Promise((resolve7, reject) => {
12163
12114
  const child = spawn4(cmd, args, {
12164
12115
  cwd: opts.cwd,
12165
12116
  env: { ...process.env, ...opts.env ?? {} },
@@ -12171,7 +12122,7 @@ async function runCmd(cmd, args, opts = {}) {
12171
12122
  }
12172
12123
  child.on("error", reject);
12173
12124
  child.on("close", (code) => {
12174
- if (code === 0) resolve6();
12125
+ if (code === 0) resolve7();
12175
12126
  else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
12176
12127
  });
12177
12128
  });
@@ -12811,29 +12762,21 @@ var init_runTickScript = __esm({
12811
12762
  }
12812
12763
  });
12813
12764
 
12814
- // src/scripts/saveGoalState.ts
12815
- var saveGoalState;
12816
- var init_saveGoalState = __esm({
12817
- "src/scripts/saveGoalState.ts"() {
12765
+ // src/scripts/saveManagedGoalState.ts
12766
+ var saveManagedGoalState;
12767
+ var init_saveManagedGoalState = __esm({
12768
+ "src/scripts/saveManagedGoalState.ts"() {
12818
12769
  "use strict";
12819
12770
  init_state2();
12820
- saveGoalState = async (ctx) => {
12771
+ saveManagedGoalState = async (ctx) => {
12772
+ ctx.skipAgent = true;
12821
12773
  const goal = ctx.data.goal;
12822
- if (!goal) {
12823
- ctx.skipAgent = true;
12824
- return;
12825
- }
12826
- const prev = goal.raw;
12827
- const changed = !prev || prev.state !== goal.state || prev.lastDispatchedIssue !== goal.lastDispatchedIssue;
12828
- const updated = {
12829
- ...prev ?? { state: goal.state, extra: {} },
12830
- state: goal.state,
12831
- lastDispatchedIssue: goal.lastDispatchedIssue,
12832
- updatedAt: changed ? nowIso() : prev?.updatedAt
12833
- };
12774
+ if (!goal?.raw) return;
12775
+ const original = typeof ctx.data.goalOriginalStateText === "string" ? ctx.data.goalOriginalStateText : "";
12776
+ const changedBeforeTimestamp = original !== serializeGoalState(goal.raw);
12777
+ const updated = changedBeforeTimestamp ? { ...goal.raw, updatedAt: nowIso() } : goal.raw;
12834
12778
  ctx.data.goalPersistState = updated;
12835
- ctx.data.goalPersistChanged = changed;
12836
- ctx.skipAgent = true;
12779
+ ctx.data.goalPersistChanged = original !== serializeGoalState(updated);
12837
12780
  };
12838
12781
  }
12839
12782
  });
@@ -13125,7 +13068,7 @@ function stripAnsi2(s) {
13125
13068
  return s.replace(ANSI_RE2, "");
13126
13069
  }
13127
13070
  function runCommand2(command, cwd) {
13128
- return new Promise((resolve6) => {
13071
+ return new Promise((resolve7) => {
13129
13072
  const child = spawn5(command, {
13130
13073
  cwd,
13131
13074
  shell: true,
@@ -13152,11 +13095,11 @@ function runCommand2(command, cwd) {
13152
13095
  }, TEST_TIMEOUT_MS);
13153
13096
  child.on("exit", (code) => {
13154
13097
  clearTimeout(timer);
13155
- resolve6({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
13098
+ resolve7({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
13156
13099
  });
13157
13100
  child.on("error", (err) => {
13158
13101
  clearTimeout(timer);
13159
- resolve6({ exitCode: -1, output: err.message });
13102
+ resolve7({ exitCode: -1, output: err.message });
13160
13103
  });
13161
13104
  });
13162
13105
  }
@@ -13561,21 +13504,21 @@ function lineStream(stream) {
13561
13504
  tryDeliver();
13562
13505
  });
13563
13506
  return {
13564
- next: (timeoutMs) => new Promise((resolve6) => {
13507
+ next: (timeoutMs) => new Promise((resolve7) => {
13565
13508
  if (queue.length > 0) {
13566
- resolve6(queue.shift());
13509
+ resolve7(queue.shift());
13567
13510
  return;
13568
13511
  }
13569
13512
  if (ended) {
13570
- resolve6(null);
13513
+ resolve7(null);
13571
13514
  return;
13572
13515
  }
13573
- waiter = resolve6;
13516
+ waiter = resolve7;
13574
13517
  const t = setTimeout(
13575
13518
  () => {
13576
- if (waiter === resolve6) {
13519
+ if (waiter === resolve7) {
13577
13520
  waiter = null;
13578
- resolve6(null);
13521
+ resolve7(null);
13579
13522
  }
13580
13523
  },
13581
13524
  Math.max(0, timeoutMs)
@@ -13756,7 +13699,9 @@ var init_scripts = __esm({
13756
13699
  "use strict";
13757
13700
  init_abortUnfinishedGitOps();
13758
13701
  init_advanceFlow();
13702
+ init_advanceManagedGoal();
13759
13703
  init_appendCompanyActivity();
13704
+ init_applyDutyReports();
13760
13705
  init_buildSyntheticPlugin();
13761
13706
  init_checkCoverageWithRetry();
13762
13707
  init_classifyByLabel();
@@ -13764,7 +13709,6 @@ var init_scripts = __esm({
13764
13709
  init_commitGoalState();
13765
13710
  init_composePrompt();
13766
13711
  init_createQaGoal();
13767
- init_deriveGoalPhase();
13768
13712
  init_deriveQaScopeFromIssue();
13769
13713
  init_diagMcp();
13770
13714
  init_discoverQaContext();
@@ -13772,16 +13716,13 @@ var init_scripts = __esm({
13772
13716
  init_dispatchClassified();
13773
13717
  init_dispatchDutyFileTicks();
13774
13718
  init_dispatchDutyTicks();
13775
- init_dispatchNextTask();
13776
13719
  init_dispatchNextTaskJob();
13777
13720
  init_ensurePr();
13778
13721
  init_failOnceTaskJob();
13779
- init_finalizeGoal();
13780
13722
  init_finalizeTerminal();
13781
13723
  init_finishFlow();
13782
13724
  init_fixCiFlow();
13783
13725
  init_fixFlow();
13784
- init_handleAbandonedGoal();
13785
13726
  init_initFlow();
13786
13727
  init_loadConventions();
13787
13728
  init_loadCoverageRules();
@@ -13830,7 +13771,7 @@ var init_scripts = __esm({
13830
13771
  init_runPreviewBuild();
13831
13772
  init_runScheduledExecutableTick();
13832
13773
  init_runTickScript();
13833
- init_saveGoalState();
13774
+ init_saveManagedGoalState();
13834
13775
  init_saveTaskState();
13835
13776
  init_setCommentTarget();
13836
13777
  init_setLifecycleLabel();
@@ -13890,12 +13831,9 @@ var init_scripts = __esm({
13890
13831
  runScheduledExecutableTick,
13891
13832
  runTickScript,
13892
13833
  runPreviewBuild,
13834
+ advanceManagedGoal,
13893
13835
  loadGoalState,
13894
- handleAbandonedGoal,
13895
- deriveGoalPhase,
13896
- dispatchNextTask,
13897
- finalizeGoal,
13898
- saveGoalState
13836
+ saveManagedGoalState
13899
13837
  };
13900
13838
  postflightScripts = {
13901
13839
  parseAgentResult: parseAgentResult2,
@@ -13930,6 +13868,7 @@ var init_scripts = __esm({
13930
13868
  finalizeTerminal,
13931
13869
  advanceFlow,
13932
13870
  persistFlowState,
13871
+ applyDutyReports,
13933
13872
  recordClassification,
13934
13873
  dispatchClassified,
13935
13874
  notifyTerminal,
@@ -14120,6 +14059,8 @@ async function runExecutable(profileName, input) {
14120
14059
  }
14121
14060
  const perExecutableModel = config.agent.perExecutable?.[profileName];
14122
14061
  const modelSpec = perExecutableModel ? perExecutableModel : profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
14062
+ const profileHasThinkingTokens = typeof profile.claudeCode.maxThinkingTokens === "number" && profile.claudeCode.maxThinkingTokens > 0;
14063
+ const reasoningEffort = config.agent.perExecutableReasoningEffort?.[profileName] ?? profile.claudeCode.reasoningEffort ?? (profileHasThinkingTokens ? void 0 : config.agent.reasoningEffort);
14123
14064
  let model;
14124
14065
  try {
14125
14066
  model = parseProviderModel(modelSpec);
@@ -14195,6 +14136,7 @@ async function runExecutable(profileName, input) {
14195
14136
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
14196
14137
  agents,
14197
14138
  maxTurns: profile.claudeCode.maxTurns,
14139
+ reasoningEffort,
14198
14140
  maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
14199
14141
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14200
14142
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
@@ -14689,14 +14631,14 @@ async function runShellEntry(entry, ctx, profile) {
14689
14631
  let killTimer;
14690
14632
  let escalateTimer;
14691
14633
  const result = await new Promise(
14692
- (resolve6) => {
14634
+ (resolve7) => {
14693
14635
  let settled = false;
14694
14636
  const settle = (code, signal, spawnErr) => {
14695
14637
  if (settled) return;
14696
14638
  settled = true;
14697
14639
  if (killTimer) clearTimeout(killTimer);
14698
14640
  if (escalateTimer) clearTimeout(escalateTimer);
14699
- resolve6({ code, signal, spawnErr });
14641
+ resolve7({ code, signal, spawnErr });
14700
14642
  };
14701
14643
  child.on("error", (err) => settle(null, null, err));
14702
14644
  child.on("close", (code, signal) => settle(code, signal));
@@ -14732,6 +14674,11 @@ async function runShellEntry(entry, ctx, profile) {
14732
14674
  if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
14733
14675
  const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
14734
14676
  if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
14677
+ const dutyReports = parseDutyReportsFromText(stdout);
14678
+ if (dutyReports.length > 0) {
14679
+ const prior = Array.isArray(ctx.data.dutyReports) ? ctx.data.dutyReports : [];
14680
+ ctx.data.dutyReports = [...prior, ...dutyReports];
14681
+ }
14735
14682
  if (timedOut) {
14736
14683
  ctx.skipAgent = true;
14737
14684
  const seconds = Math.round(timeoutMs / 1e3);
@@ -14781,6 +14728,7 @@ var init_executor = __esm({
14781
14728
  init_config();
14782
14729
  init_container();
14783
14730
  init_discipline();
14731
+ init_dutyReport();
14784
14732
  init_events();
14785
14733
  init_lifecycleLabels();
14786
14734
  init_litellm();
@@ -14791,7 +14739,7 @@ var init_executor = __esm({
14791
14739
  init_subagents();
14792
14740
  init_task_artifacts();
14793
14741
  init_tools();
14794
- MUTATING_POSTFLIGHTS = /* @__PURE__ */ new Set(["commitAndPush", "ensurePr"]);
14742
+ MUTATING_POSTFLIGHTS = /* @__PURE__ */ new Set(["commitAndPush", "ensurePr", "applyDutyReports"]);
14795
14743
  MAX_CHAIN_HOPS = 60;
14796
14744
  DEFAULT_SHELL_TIMEOUT_MS = 3e5;
14797
14745
  SIGKILL_GRACE_MS = 5e3;
@@ -16811,17 +16759,17 @@ function authOk(req, expected) {
16811
16759
  return false;
16812
16760
  }
16813
16761
  function readJsonBody(req) {
16814
- return new Promise((resolve6, reject) => {
16762
+ return new Promise((resolve7, reject) => {
16815
16763
  const chunks = [];
16816
16764
  req.on("data", (c) => chunks.push(c));
16817
16765
  req.on("end", () => {
16818
16766
  const raw = Buffer.concat(chunks).toString("utf-8");
16819
16767
  if (!raw.trim()) {
16820
- resolve6({});
16768
+ resolve7({});
16821
16769
  return;
16822
16770
  }
16823
16771
  try {
16824
- resolve6(JSON.parse(raw));
16772
+ resolve7(JSON.parse(raw));
16825
16773
  } catch (err) {
16826
16774
  reject(err instanceof Error ? err : new Error(String(err)));
16827
16775
  }
@@ -17070,11 +17018,11 @@ async function brainServe(opts) {
17070
17018
  model,
17071
17019
  litellmUrl
17072
17020
  });
17073
- await new Promise((resolve6) => {
17021
+ await new Promise((resolve7) => {
17074
17022
  server.listen(port, "0.0.0.0", () => {
17075
17023
  process.stdout.write(`[brain-serve] listening on 0.0.0.0:${port} (cwd=${opts.cwd})
17076
17024
  `);
17077
- resolve6();
17025
+ resolve7();
17078
17026
  });
17079
17027
  });
17080
17028
  const shutdown = (signal) => {
@@ -17324,14 +17272,14 @@ async function startBrainProxy(opts) {
17324
17272
  const { httpServer, handler } = buildBrainProxy(opts);
17325
17273
  const port = opts.port ?? 0;
17326
17274
  const host = opts.host ?? "127.0.0.1";
17327
- await new Promise((resolve6) => httpServer.listen(port, host, () => resolve6()));
17275
+ await new Promise((resolve7) => httpServer.listen(port, host, () => resolve7()));
17328
17276
  const addr = httpServer.address();
17329
17277
  return {
17330
17278
  httpServer,
17331
17279
  port: addr.port,
17332
17280
  url: `http://${host}:${addr.port}`,
17333
- stop: () => new Promise((resolve6) => {
17334
- httpServer.close(() => resolve6());
17281
+ stop: () => new Promise((resolve7) => {
17282
+ httpServer.close(() => resolve7());
17335
17283
  }),
17336
17284
  handler
17337
17285
  };
@@ -17481,23 +17429,23 @@ function buildMcpHttpServer(opts) {
17481
17429
  httpServer,
17482
17430
  routes,
17483
17431
  port,
17484
- stop: () => new Promise((resolve6) => {
17432
+ stop: () => new Promise((resolve7) => {
17485
17433
  let pending = transports.size;
17486
17434
  if (pending === 0) {
17487
- httpServer.close(() => resolve6());
17435
+ httpServer.close(() => resolve7());
17488
17436
  return;
17489
17437
  }
17490
17438
  for (const transport of transports.values()) {
17491
17439
  void transport.close().finally(() => {
17492
17440
  pending--;
17493
- if (pending === 0) httpServer.close(() => resolve6());
17441
+ if (pending === 0) httpServer.close(() => resolve7());
17494
17442
  });
17495
17443
  }
17496
17444
  })
17497
17445
  };
17498
17446
  }
17499
17447
  function listenMcpHttpServer(server, host = "127.0.0.1") {
17500
- return new Promise((resolve6, reject) => {
17448
+ return new Promise((resolve7, reject) => {
17501
17449
  server.httpServer.once("error", reject);
17502
17450
  server.httpServer.listen(server.port, host, () => {
17503
17451
  server.httpServer.off("error", reject);
@@ -17505,7 +17453,7 @@ function listenMcpHttpServer(server, host = "127.0.0.1") {
17505
17453
  if (addr && typeof addr === "object") {
17506
17454
  server.port = addr.port;
17507
17455
  }
17508
- resolve6();
17456
+ resolve7();
17509
17457
  });
17510
17458
  });
17511
17459
  }
@@ -17644,7 +17592,7 @@ async function waitForNextUserMessage(opts) {
17644
17592
  }
17645
17593
  }
17646
17594
  function sleep2(ms) {
17647
- return new Promise((resolve6) => setTimeout(resolve6, ms));
17595
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
17648
17596
  }
17649
17597
  function currentBranch(cwd) {
17650
17598
  try {
@@ -18759,14 +18707,14 @@ function sendJson2(res, status, body) {
18759
18707
  res.end(JSON.stringify(body));
18760
18708
  }
18761
18709
  function readJsonBody2(req) {
18762
- return new Promise((resolve6, reject) => {
18710
+ return new Promise((resolve7, reject) => {
18763
18711
  const chunks = [];
18764
18712
  req.on("data", (c) => chunks.push(c));
18765
18713
  req.on("end", () => {
18766
18714
  const raw = Buffer.concat(chunks).toString("utf-8");
18767
- if (!raw.trim()) return resolve6({});
18715
+ if (!raw.trim()) return resolve7({});
18768
18716
  try {
18769
- resolve6(JSON.parse(raw));
18717
+ resolve7(JSON.parse(raw));
18770
18718
  } catch (err) {
18771
18719
  reject(err instanceof Error ? err : new Error(String(err)));
18772
18720
  }
@@ -18892,10 +18840,10 @@ async function poolServe() {
18892
18840
  }
18893
18841
  });
18894
18842
  const apiHost = process.env.POOL_API_HOST ?? "::";
18895
- await new Promise((resolve6) => {
18843
+ await new Promise((resolve7) => {
18896
18844
  server.listen(apiPort, apiHost, () => {
18897
18845
  log(`listening on ${apiHost}:${apiPort} (min=${min}, app=${app}, region=${region})`);
18898
- resolve6();
18846
+ resolve7();
18899
18847
  });
18900
18848
  });
18901
18849
  const shutdown = (signal) => {
@@ -18934,17 +18882,17 @@ function authOk2(req, expected) {
18934
18882
  return false;
18935
18883
  }
18936
18884
  function readJsonBody3(req) {
18937
- return new Promise((resolve6, reject) => {
18885
+ return new Promise((resolve7, reject) => {
18938
18886
  const chunks = [];
18939
18887
  req.on("data", (c) => chunks.push(c));
18940
18888
  req.on("end", () => {
18941
18889
  const raw = Buffer.concat(chunks).toString("utf-8");
18942
18890
  if (!raw.trim()) {
18943
- resolve6({});
18891
+ resolve7({});
18944
18892
  return;
18945
18893
  }
18946
18894
  try {
18947
- resolve6(JSON.parse(raw));
18895
+ resolve7(JSON.parse(raw));
18948
18896
  } catch (err) {
18949
18897
  reject(err instanceof Error ? err : new Error(String(err)));
18950
18898
  }
@@ -19023,13 +18971,13 @@ async function defaultRunJob(job) {
19023
18971
  ...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
19024
18972
  ...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
19025
18973
  };
19026
- const run = (cmd, args, cwd) => new Promise((resolve6) => {
18974
+ const run = (cmd, args, cwd) => new Promise((resolve7) => {
19027
18975
  const child = spawn8(cmd, args, { stdio: "inherit", env: childEnv, cwd });
19028
- child.on("exit", (code) => resolve6(code ?? 0));
18976
+ child.on("exit", (code) => resolve7(code ?? 0));
19029
18977
  child.on("error", (err) => {
19030
18978
  process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
19031
18979
  `);
19032
- resolve6(1);
18980
+ resolve7(1);
19033
18981
  });
19034
18982
  });
19035
18983
  process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
@@ -19106,11 +19054,11 @@ async function runnerServe() {
19106
19054
  const port = Number(process.env.PORT ?? DEFAULT_PORT2);
19107
19055
  const server = buildServer2({ apiKey });
19108
19056
  const host = process.env.RUNNER_HOST ?? "::";
19109
- await new Promise((resolve6) => {
19057
+ await new Promise((resolve7) => {
19110
19058
  server.listen(port, host, () => {
19111
19059
  process.stdout.write(`[runner-serve] listening on ${host}:${port} (idle, awaiting job)
19112
19060
  `);
19113
- resolve6();
19061
+ resolve7();
19114
19062
  });
19115
19063
  });
19116
19064
  const shutdown = (signal) => {
@@ -19129,7 +19077,7 @@ async function runnerServe() {
19129
19077
  init_config();
19130
19078
  init_litellm();
19131
19079
  import { spawn as spawn9 } from "child_process";
19132
- function parseTarget(positional) {
19080
+ function parseTarget2(positional) {
19133
19081
  if (!Array.isArray(positional) || positional.length === 0) return "none";
19134
19082
  const first = String(positional[0]).toLowerCase();
19135
19083
  if (first === "vscode" || first === "code") return "vscode";
@@ -19144,7 +19092,7 @@ function buildProxyEnv(url) {
19144
19092
  };
19145
19093
  }
19146
19094
  async function serve(opts) {
19147
- const target = parseTarget(opts.args);
19095
+ const target = parseTarget2(opts.args);
19148
19096
  const model = parseProviderModel(opts.config.agent.model);
19149
19097
  const usesProxy = needsLitellmProxy(model);
19150
19098
  let handle = null;
@@ -19179,14 +19127,14 @@ async function serve(opts) {
19179
19127
  `);
19180
19128
  const args = ["--dangerously-skip-permissions", "--model", model.model];
19181
19129
  const child = spawn9("claude", args, { stdio: "inherit", env: editorEnv, cwd: opts.cwd });
19182
- const exitCode = await new Promise((resolve6) => {
19183
- child.on("exit", (code) => resolve6(code ?? 0));
19130
+ const exitCode = await new Promise((resolve7) => {
19131
+ child.on("exit", (code) => resolve7(code ?? 0));
19184
19132
  child.on("error", (err) => {
19185
19133
  process.stderr.write(`[kody serve] failed to launch Claude Code: ${err.message}
19186
19134
  `);
19187
19135
  process.stderr.write(` Install: https://docs.anthropic.com/claude/docs/claude-code
19188
19136
  `);
19189
- resolve6(1);
19137
+ resolve7(1);
19190
19138
  });
19191
19139
  });
19192
19140
  killProxy();