@kody-ade/kody-engine 0.4.224 → 0.4.226

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 +825 -832
  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.226",
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,72 @@ 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 = parseGoalActivations(r.activeGoals);
273
+ return Object.keys(out).length > 0 ? out : void 0;
274
+ }
275
+ function parseGoalActivations(raw) {
276
+ if (!Array.isArray(raw)) throw new Error(`kody.config.json: company.activeGoals must be an array`);
277
+ const out = [];
278
+ const seen = /* @__PURE__ */ new Set();
279
+ for (const value of raw) {
280
+ if (typeof value === "string") {
281
+ const slug = parseSlug(value, "company.activeGoals");
282
+ if (!slug) continue;
283
+ if (!seen.has(slug)) {
284
+ seen.add(slug);
285
+ out.push(slug);
286
+ }
287
+ continue;
288
+ }
289
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
290
+ throw new Error(`kody.config.json: company.activeGoals entries must be strings or goal schedule objects`);
291
+ }
292
+ const r = value;
293
+ const template = typeof r.template === "string" ? parseSlug(r.template, "company.activeGoals.template") : "";
294
+ if (!template) throw new Error(`kody.config.json: company.activeGoals object requires template`);
295
+ const entry = { template };
296
+ if (r.every !== void 0) {
297
+ if (typeof r.every !== "string" || !/^[1-9][0-9]*[mhdw]$/.test(r.every.trim())) {
298
+ throw new Error(`kody.config.json: company.activeGoals every must look like "1d", "1w", "15m", or "2h"`);
299
+ }
300
+ entry.every = r.every.trim();
301
+ }
302
+ if (r.idPrefix !== void 0) {
303
+ if (typeof r.idPrefix !== "string") throw new Error(`kody.config.json: company.activeGoals idPrefix must be a string`);
304
+ const idPrefix = parseSlug(r.idPrefix, "company.activeGoals.idPrefix");
305
+ if (idPrefix) entry.idPrefix = idPrefix;
306
+ }
307
+ const facts = recordValue(r.facts);
308
+ if (r.facts !== void 0 && !facts) throw new Error(`kody.config.json: company.activeGoals facts must be an object`);
309
+ if (facts) entry.facts = facts;
310
+ out.push(entry);
311
+ }
312
+ return out;
313
+ }
314
+ function parseSlugArray(raw, field) {
315
+ if (!Array.isArray(raw)) throw new Error(`kody.config.json: ${field} must be an array of strings`);
316
+ const out = [];
317
+ for (const value of raw) {
318
+ const slug = parseSlug(value, field);
319
+ if (!slug) continue;
320
+ out.push(slug);
321
+ }
322
+ return [...new Set(out)];
323
+ }
324
+ function parseSlug(value, field) {
325
+ if (typeof value !== "string") throw new Error(`kody.config.json: ${field} entries must be strings`);
326
+ const slug = value.trim();
327
+ if (!slug) return "";
328
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) {
329
+ throw new Error(`kody.config.json: ${field} contains invalid slug "${value}"`);
330
+ }
331
+ return slug;
332
+ }
265
333
  function recordValue(raw) {
266
334
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0;
267
335
  }
@@ -282,6 +350,15 @@ function parsePerExecutable(raw) {
282
350
  }
283
351
  return Object.keys(out).length > 0 ? { perExecutable: out } : {};
284
352
  }
353
+ function parsePerExecutableReasoningEffort(raw) {
354
+ if (!raw || typeof raw !== "object") return {};
355
+ const out = {};
356
+ for (const [k, v] of Object.entries(raw)) {
357
+ const effort = typeof v === "string" ? parseReasoningEffort(v) : null;
358
+ if (effort) out[k] = effort;
359
+ }
360
+ return Object.keys(out).length > 0 ? { perExecutableReasoningEffort: out } : {};
361
+ }
285
362
  function parseAgentReasoningEffort(raw) {
286
363
  if (typeof raw !== "string") return {};
287
364
  return { reasoningEffort: parseReasoningEffort(raw) ?? void 0 };
@@ -562,7 +639,7 @@ var init_events = __esm({
562
639
  // src/verify.ts
563
640
  import { spawn } from "child_process";
564
641
  function runCommand(command, cwd) {
565
- return new Promise((resolve6) => {
642
+ return new Promise((resolve7) => {
566
643
  const start = Date.now();
567
644
  const child = spawn(command, {
568
645
  cwd,
@@ -591,11 +668,11 @@ function runCommand(command, cwd) {
591
668
  child.on("exit", (code) => {
592
669
  clearTimeout(timer);
593
670
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
594
- resolve6({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
671
+ resolve7({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
595
672
  });
596
673
  child.on("error", (err) => {
597
674
  clearTimeout(timer);
598
- resolve6({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
675
+ resolve7({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
599
676
  });
600
677
  });
601
678
  }
@@ -1450,7 +1527,7 @@ var init_dutyMcp = __esm({
1450
1527
  TRUST_STATE_BRANCH = "kody-state";
1451
1528
  THREAD_BODY_MAX = 4e3;
1452
1529
  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"];
1530
+ DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "worker-ask", "chat"];
1454
1531
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
1455
1532
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
1456
1533
  GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
@@ -1513,7 +1590,7 @@ var init_repoWorkspace = __esm({
1513
1590
  defaultCloneRepo = (repo, token, dir) => {
1514
1591
  fs4.mkdirSync(path4.dirname(dir), { recursive: true });
1515
1592
  const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
1516
- return new Promise((resolve6, reject) => {
1593
+ return new Promise((resolve7, reject) => {
1517
1594
  const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
1518
1595
  stdio: "inherit"
1519
1596
  });
@@ -1529,7 +1606,7 @@ var init_repoWorkspace = __esm({
1529
1606
  spawnSync("git", ["-C", dir, "config", "user.email", email]);
1530
1607
  } catch {
1531
1608
  }
1532
- resolve6();
1609
+ resolve7();
1533
1610
  });
1534
1611
  child.on("error", reject);
1535
1612
  });
@@ -1814,10 +1891,10 @@ async function runAgent(opts) {
1814
1891
  let timer;
1815
1892
  let next;
1816
1893
  if (turnTimeoutMs > 0) {
1817
- const timeoutPromise = new Promise((resolve6) => {
1894
+ const timeoutPromise = new Promise((resolve7) => {
1818
1895
  timer = setTimeout(() => {
1819
1896
  timedOut = true;
1820
- resolve6({ done: true, value: void 0 });
1897
+ resolve7({ done: true, value: void 0 });
1821
1898
  }, turnTimeoutMs);
1822
1899
  });
1823
1900
  next = await Promise.race([nextPromise, timeoutPromise]);
@@ -2055,6 +2132,8 @@ function resolveCompanyStore() {
2055
2132
  return { repo, ref };
2056
2133
  }
2057
2134
  function fetchCompanyStore(repo, ref) {
2135
+ const localRoot = localStoreRoot(repo);
2136
+ if (localRoot) return localRoot;
2058
2137
  const url = repoToGitUrl(repo);
2059
2138
  const cacheDir = path6.join(cacheRoot(), cacheKey(repo, ref));
2060
2139
  try {
@@ -2075,6 +2154,12 @@ function fetchCompanyStore(repo, ref) {
2075
2154
  return null;
2076
2155
  }
2077
2156
  }
2157
+ function localStoreRoot(repo) {
2158
+ if (!path6.isAbsolute(repo) && !repo.startsWith(".")) return null;
2159
+ const root = path6.resolve(repo);
2160
+ if (!fs6.existsSync(path6.join(root, ".kody"))) return null;
2161
+ return root;
2162
+ }
2078
2163
  function repoToGitUrl(repo) {
2079
2164
  if (/^(https?:|ssh:|git@|file:)/.test(repo)) return repo;
2080
2165
  if (isGitHubShorthand(repo)) {
@@ -3205,6 +3290,7 @@ function parseClaudeCode(p, raw) {
3205
3290
  permissionMode,
3206
3291
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
3207
3292
  maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
3293
+ reasoningEffort: typeof r.reasoningEffort === "string" ? parseReasoningEffort(r.reasoningEffort) : null,
3208
3294
  maxTurnTimeoutSec: typeof r.maxTurnTimeoutSec === "number" ? r.maxTurnTimeoutSec : null,
3209
3295
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
3210
3296
  cacheable: r.cacheable === true,
@@ -3388,6 +3474,7 @@ var VALID_INPUT_TYPES, VALID_PERMISSION_MODES, VALID_ROLES, VALID_CONTAINER_CHIL
3388
3474
  var init_profile = __esm({
3389
3475
  "src/profile.ts"() {
3390
3476
  "use strict";
3477
+ init_config();
3391
3478
  init_dutyMcp();
3392
3479
  init_lifecycles();
3393
3480
  init_profile_error();
@@ -4554,6 +4641,91 @@ here \u2014 never substitute it for the truth.`;
4554
4641
  }
4555
4642
  });
4556
4643
 
4644
+ // src/dutyReport.ts
4645
+ function parseDutyReportsFromText(text) {
4646
+ const reports = [];
4647
+ for (const match of text.matchAll(REPORT_LINE)) {
4648
+ const raw = match[1]?.trim();
4649
+ if (!raw) continue;
4650
+ try {
4651
+ const parsed = parseDutyReport(JSON.parse(raw));
4652
+ if (parsed) reports.push(parsed);
4653
+ } catch {
4654
+ }
4655
+ }
4656
+ return reports;
4657
+ }
4658
+ function parseDutyReport(raw) {
4659
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4660
+ const obj = raw;
4661
+ const target = parseTarget(obj.target);
4662
+ if (!target) return null;
4663
+ const evidence = parseBooleanRecord(obj.evidence);
4664
+ const facts = parseFacts(obj.facts);
4665
+ if (!evidence && !facts) return null;
4666
+ return {
4667
+ target,
4668
+ ...evidence ? { evidence } : {},
4669
+ ...facts ? { facts } : {}
4670
+ };
4671
+ }
4672
+ function applyDutyReportToGoalState(state, report) {
4673
+ const priorFacts = parseFacts(state.extra.facts) ?? {};
4674
+ const nextFacts = { ...priorFacts };
4675
+ for (const [key, value] of Object.entries(report.facts ?? {})) {
4676
+ if (CONTROL_FACT_KEYS.has(key)) continue;
4677
+ nextFacts[key] = value;
4678
+ }
4679
+ for (const [key, value] of Object.entries(report.evidence ?? {})) {
4680
+ nextFacts[key] = value;
4681
+ }
4682
+ const pending = nextFacts.pendingEvidence;
4683
+ if (typeof pending === "string" && Object.hasOwn(report.evidence ?? {}, pending)) {
4684
+ delete nextFacts.pendingEvidence;
4685
+ }
4686
+ return {
4687
+ ...state,
4688
+ extra: {
4689
+ ...state.extra,
4690
+ facts: nextFacts
4691
+ }
4692
+ };
4693
+ }
4694
+ function parseTarget(raw) {
4695
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4696
+ const target = raw;
4697
+ if (target.type !== "goal" && target.type !== "task" && target.type !== "duty") return null;
4698
+ if (typeof target.id !== "string" || target.id.trim().length === 0) return null;
4699
+ return { type: target.type, id: target.id.trim() };
4700
+ }
4701
+ function parseBooleanRecord(raw) {
4702
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4703
+ const out = {};
4704
+ for (const [key, value] of Object.entries(raw)) {
4705
+ if (typeof key !== "string" || key.length === 0 || typeof value !== "boolean") return null;
4706
+ out[key] = value;
4707
+ }
4708
+ return out;
4709
+ }
4710
+ function parseFacts(raw) {
4711
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4712
+ const out = {};
4713
+ for (const [key, value] of Object.entries(raw)) {
4714
+ if (typeof key !== "string" || key.length === 0) return null;
4715
+ if (CONTROL_FACT_KEYS.has(key)) continue;
4716
+ out[key] = value;
4717
+ }
4718
+ return out;
4719
+ }
4720
+ var REPORT_LINE, CONTROL_FACT_KEYS;
4721
+ var init_dutyReport = __esm({
4722
+ "src/dutyReport.ts"() {
4723
+ "use strict";
4724
+ REPORT_LINE = /^KODY_DUTY_REPORT=(.+)$/gm;
4725
+ CONTROL_FACT_KEYS = /* @__PURE__ */ new Set(["blockers", "destination", "duties", "route", "stage", "state"]);
4726
+ }
4727
+ });
4728
+
4557
4729
  // src/lifecycleLabels.ts
4558
4730
  function groupOf(label) {
4559
4731
  const idx = label.indexOf(":");
@@ -5301,6 +5473,291 @@ var init_advanceFlow = __esm({
5301
5473
  }
5302
5474
  });
5303
5475
 
5476
+ // src/goal/manager.ts
5477
+ function evidenceSatisfied(goal, evidence) {
5478
+ return goal.facts[evidence] === true;
5479
+ }
5480
+ function firstMissingEvidence(goal) {
5481
+ return goal.destination.evidence.find((evidence) => !evidenceSatisfied(goal, evidence));
5482
+ }
5483
+ function pushBlocker(goal, reason) {
5484
+ if (!goal.blockers.includes(reason)) goal.blockers.push(reason);
5485
+ }
5486
+ function isSimpleGoal(goal) {
5487
+ return goal.type === SIMPLE_GOAL_TYPE;
5488
+ }
5489
+ function applySimpleGoalTaskSummary(goal, summary) {
5490
+ goal.facts.simpleAttachedTaskCount = summary.total;
5491
+ goal.facts.simpleOpenTaskCount = summary.open;
5492
+ goal.facts[SIMPLE_GOAL_EVIDENCE] = summary.total > 0 && summary.open === 0;
5493
+ }
5494
+ function isFactReference(value) {
5495
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5496
+ const record = value;
5497
+ return Object.keys(record).length === 1 && typeof record.fact === "string" && record.fact.length > 0;
5498
+ }
5499
+ function isCliArgValue(value) {
5500
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
5501
+ }
5502
+ function resolveRouteArgs(goal, step) {
5503
+ const cliArgs = {};
5504
+ for (const [name, value] of Object.entries(step.args ?? {})) {
5505
+ if (!isFactReference(value)) {
5506
+ cliArgs[name] = value;
5507
+ continue;
5508
+ }
5509
+ const factValue = goal.facts[value.fact];
5510
+ if (factValue === void 0 || factValue === null) {
5511
+ return { ok: false, reason: `route arg ${name} needs missing fact ${value.fact}` };
5512
+ }
5513
+ if (!isCliArgValue(factValue)) {
5514
+ return { ok: false, reason: `route arg ${name} needs scalar fact ${value.fact}` };
5515
+ }
5516
+ cliArgs[name] = factValue;
5517
+ }
5518
+ return { ok: true, cliArgs };
5519
+ }
5520
+ function planManagedGoalTick(goal) {
5521
+ goal.blockers = [];
5522
+ const missing = firstMissingEvidence(goal);
5523
+ if (!missing) {
5524
+ goal.stage = "done";
5525
+ delete goal.facts.pendingEvidence;
5526
+ return { kind: "done" };
5527
+ }
5528
+ const pending = goal.facts.pendingEvidence;
5529
+ if (pending === missing) {
5530
+ const stage = typeof goal.stage === "string" ? goal.stage : "waiting";
5531
+ return { kind: "wait", evidence: missing, stage, reason: `waiting for evidence: ${missing}` };
5532
+ }
5533
+ const step = goal.route.find((candidate) => candidate.evidence === missing);
5534
+ if (!step) {
5535
+ if (isSimpleGoal(goal) && missing === SIMPLE_GOAL_EVIDENCE) {
5536
+ goal.stage = "waiting";
5537
+ delete goal.facts.pendingEvidence;
5538
+ const total = typeof goal.facts.simpleAttachedTaskCount === "number" ? goal.facts.simpleAttachedTaskCount : 0;
5539
+ const open = typeof goal.facts.simpleOpenTaskCount === "number" ? goal.facts.simpleOpenTaskCount : 0;
5540
+ return {
5541
+ kind: "wait",
5542
+ evidence: missing,
5543
+ stage: "waiting",
5544
+ reason: total === 0 ? "waiting for labelled tasks" : `waiting for ${open} open labelled task(s)`
5545
+ };
5546
+ }
5547
+ const reason = `no route step for evidence: ${missing}`;
5548
+ goal.stage = "blocked";
5549
+ pushBlocker(goal, reason);
5550
+ return { kind: "blocked", evidence: missing, stage: "blocked", reason };
5551
+ }
5552
+ if (!goal.duties.includes(step.duty)) {
5553
+ const reason = `route duty ${step.duty} is not attached to this goal`;
5554
+ goal.stage = "blocked";
5555
+ pushBlocker(goal, reason);
5556
+ return { kind: "blocked", evidence: missing, stage: step.stage, reason };
5557
+ }
5558
+ const resolved = resolveRouteArgs(goal, step);
5559
+ if (!resolved.ok) {
5560
+ goal.stage = "blocked";
5561
+ pushBlocker(goal, resolved.reason);
5562
+ return { kind: "blocked", evidence: missing, stage: step.stage, reason: resolved.reason };
5563
+ }
5564
+ goal.stage = step.stage;
5565
+ goal.facts.pendingEvidence = missing;
5566
+ return {
5567
+ kind: "dispatch",
5568
+ evidence: missing,
5569
+ stage: step.stage,
5570
+ duty: step.duty,
5571
+ executable: step.executable,
5572
+ cliArgs: resolved.cliArgs
5573
+ };
5574
+ }
5575
+ function asStringArray(value) {
5576
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return null;
5577
+ return [...value];
5578
+ }
5579
+ function asRecord(value) {
5580
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5581
+ return { ...value };
5582
+ }
5583
+ function asRoute(value) {
5584
+ if (!Array.isArray(value)) return null;
5585
+ const route = [];
5586
+ for (const item of value) {
5587
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
5588
+ const raw = item;
5589
+ if (typeof raw.evidence !== "string" || typeof raw.stage !== "string" || typeof raw.duty !== "string") {
5590
+ return null;
5591
+ }
5592
+ const args = raw.args === void 0 ? void 0 : asRecord(raw.args);
5593
+ if (raw.args !== void 0 && !args) return null;
5594
+ route.push({
5595
+ evidence: raw.evidence,
5596
+ stage: raw.stage,
5597
+ duty: raw.duty,
5598
+ executable: typeof raw.executable === "string" ? raw.executable : void 0,
5599
+ args: args ?? void 0
5600
+ });
5601
+ }
5602
+ return route;
5603
+ }
5604
+ function managedGoalFromState(state) {
5605
+ const extra = state.extra;
5606
+ const destination = asRecord(extra.destination);
5607
+ const evidence = asStringArray(destination?.evidence);
5608
+ const duties = asStringArray(extra.duties);
5609
+ const route = asRoute(extra.route);
5610
+ const facts = asRecord(extra.facts);
5611
+ const blockers = asStringArray(extra.blockers);
5612
+ if (typeof extra.type !== "string" || !destination || typeof destination.outcome !== "string" || !evidence || !duties || !route || !facts || !blockers) {
5613
+ return null;
5614
+ }
5615
+ return {
5616
+ type: extra.type,
5617
+ destination: { outcome: destination.outcome, evidence },
5618
+ duties,
5619
+ route,
5620
+ stage: typeof extra.stage === "string" ? extra.stage : void 0,
5621
+ facts,
5622
+ blockers
5623
+ };
5624
+ }
5625
+ function writeManagedGoalToState(state, goal) {
5626
+ return {
5627
+ ...state,
5628
+ extra: {
5629
+ ...state.extra,
5630
+ type: goal.type,
5631
+ destination: goal.destination,
5632
+ duties: goal.duties,
5633
+ route: goal.route,
5634
+ stage: goal.stage,
5635
+ facts: goal.facts,
5636
+ blockers: goal.blockers
5637
+ }
5638
+ };
5639
+ }
5640
+ var SIMPLE_GOAL_TYPE, SIMPLE_GOAL_EVIDENCE;
5641
+ var init_manager = __esm({
5642
+ "src/goal/manager.ts"() {
5643
+ "use strict";
5644
+ SIMPLE_GOAL_TYPE = "simple";
5645
+ SIMPLE_GOAL_EVIDENCE = "labelledTasksComplete";
5646
+ }
5647
+ });
5648
+
5649
+ // src/goal/state.ts
5650
+ import * as fs24 from "fs";
5651
+ import * as path21 from "path";
5652
+ function parseGoalState(filePath, raw) {
5653
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5654
+ throw new GoalStateError(filePath, "must be JSON object");
5655
+ }
5656
+ const r = raw;
5657
+ const stateValue = r.state;
5658
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5659
+ throw new GoalStateError(
5660
+ filePath,
5661
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5662
+ );
5663
+ }
5664
+ const parsed = {
5665
+ state: stateValue,
5666
+ extra: {}
5667
+ };
5668
+ for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
5669
+ const v = r[ts];
5670
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5671
+ }
5672
+ const known = /* @__PURE__ */ new Set(["state", "updatedAt", "createdAt", "startedAt"]);
5673
+ for (const [k, v] of Object.entries(r)) {
5674
+ if (!known.has(k)) parsed.extra[k] = v;
5675
+ }
5676
+ return parsed;
5677
+ }
5678
+ function serializeGoalState(s) {
5679
+ const obj = { ...s.extra, state: s.state };
5680
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5681
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5682
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5683
+ return `${JSON.stringify(obj, null, 2)}
5684
+ `;
5685
+ }
5686
+ function nowIso() {
5687
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5688
+ }
5689
+ var VALID_STATES, GoalStateError;
5690
+ var init_state2 = __esm({
5691
+ "src/goal/state.ts"() {
5692
+ "use strict";
5693
+ VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5694
+ GoalStateError = class extends Error {
5695
+ constructor(path45, message) {
5696
+ super(`Invalid goal state at ${path45}:
5697
+ ${message}`);
5698
+ this.path = path45;
5699
+ this.name = "GoalStateError";
5700
+ }
5701
+ path;
5702
+ };
5703
+ }
5704
+ });
5705
+
5706
+ // src/scripts/advanceManagedGoal.ts
5707
+ function readSimpleGoalTaskSummary(goalId, cwd) {
5708
+ const raw = gh(
5709
+ ["issue", "list", "--state", "all", "--label", `goal:${goalId}`, "--limit", "1000", "--json", "number,state"],
5710
+ { cwd }
5711
+ );
5712
+ const issues = JSON.parse(raw);
5713
+ const total = issues.length;
5714
+ const open = issues.filter((issue) => String(issue.state ?? "").toLowerCase() === "open").length;
5715
+ return { total, open };
5716
+ }
5717
+ var advanceManagedGoal;
5718
+ var init_advanceManagedGoal = __esm({
5719
+ "src/scripts/advanceManagedGoal.ts"() {
5720
+ "use strict";
5721
+ init_manager();
5722
+ init_state2();
5723
+ init_issue();
5724
+ advanceManagedGoal = async (ctx) => {
5725
+ ctx.skipAgent = true;
5726
+ const goal = ctx.data.goal;
5727
+ if (!goal?.raw) {
5728
+ ctx.output.exitCode = 1;
5729
+ ctx.output.reason = "advanceManagedGoal requires loadGoalState first";
5730
+ return;
5731
+ }
5732
+ ctx.data.goalOriginalStateText = serializeGoalState(goal.raw);
5733
+ const managed = managedGoalFromState(goal.raw);
5734
+ if (!managed) {
5735
+ ctx.output.reason = "goal has no managed-goal contract; nothing to advance";
5736
+ return;
5737
+ }
5738
+ if (isSimpleGoal(managed)) {
5739
+ applySimpleGoalTaskSummary(managed, readSimpleGoalTaskSummary(goal.id, ctx.cwd));
5740
+ }
5741
+ const decision = planManagedGoalTick(managed);
5742
+ ctx.data.managedGoalDecision = decision;
5743
+ if (decision.kind === "done") {
5744
+ goal.state = "done";
5745
+ }
5746
+ goal.raw = writeManagedGoalToState({ ...goal.raw, state: goal.state }, managed);
5747
+ if (decision.kind === "blocked" || decision.kind === "wait" || decision.kind === "idle" || decision.kind === "done") {
5748
+ ctx.output.reason = decision.kind === "done" ? "managed goal complete" : decision.reason;
5749
+ return;
5750
+ }
5751
+ ctx.output.nextDispatch = {
5752
+ duty: decision.duty,
5753
+ executable: decision.executable,
5754
+ cliArgs: decision.cliArgs
5755
+ };
5756
+ ctx.output.reason = `dispatch ${decision.duty} for ${decision.evidence}`;
5757
+ };
5758
+ }
5759
+ });
5760
+
5304
5761
  // src/stateBranch.ts
5305
5762
  function is404(err) {
5306
5763
  const msg = err instanceof Error ? err.message : String(err);
@@ -5418,6 +5875,118 @@ var init_appendCompanyActivity = __esm({
5418
5875
  }
5419
5876
  });
5420
5877
 
5878
+ // src/goal/stateStore.ts
5879
+ function statePath(goalId) {
5880
+ return `.kody/goals/instances/${goalId}/state.json`;
5881
+ }
5882
+ function is4042(err) {
5883
+ const msg = err instanceof Error ? err.message : String(err);
5884
+ return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5885
+ }
5886
+ function fetchGoalState(owner, repo, goalId, cwd) {
5887
+ const filePath = statePath(goalId);
5888
+ let raw;
5889
+ try {
5890
+ raw = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5891
+ } catch (err) {
5892
+ if (is4042(err)) return null;
5893
+ throw err;
5894
+ }
5895
+ const o = JSON.parse(raw);
5896
+ if (!o.content) return null;
5897
+ const decoded = Buffer.from(o.content, "base64").toString("utf-8");
5898
+ return parseGoalState(filePath, JSON.parse(decoded));
5899
+ }
5900
+ function putGoalState(owner, repo, goalId, state, message, cwd) {
5901
+ ensureStateBranch(owner, repo, cwd);
5902
+ const filePath = statePath(goalId);
5903
+ const content = Buffer.from(serializeGoalState(state), "utf-8").toString("base64");
5904
+ for (let attempt = 1; attempt <= 3; attempt++) {
5905
+ let sha;
5906
+ try {
5907
+ const cur = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5908
+ const o = JSON.parse(cur);
5909
+ if (o.sha) sha = o.sha;
5910
+ } catch (err) {
5911
+ if (!is4042(err)) throw err;
5912
+ }
5913
+ const payload = { message, content, branch: STATE_BRANCH };
5914
+ if (sha) payload.sha = sha;
5915
+ try {
5916
+ gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
5917
+ cwd,
5918
+ input: JSON.stringify(payload)
5919
+ });
5920
+ return;
5921
+ } catch (err) {
5922
+ const msg = err instanceof Error ? err.message : String(err);
5923
+ const conflict = /HTTP 409/i.test(msg) || /HTTP 422/i.test(msg) || /does not match|but expected/i.test(msg);
5924
+ if (!conflict || attempt === 3) throw err;
5925
+ }
5926
+ }
5927
+ }
5928
+ var init_stateStore = __esm({
5929
+ "src/goal/stateStore.ts"() {
5930
+ "use strict";
5931
+ init_issue();
5932
+ init_stateBranch();
5933
+ init_state2();
5934
+ }
5935
+ });
5936
+
5937
+ // src/scripts/applyDutyReports.ts
5938
+ function collectReports(raw, agentResult) {
5939
+ const out = [];
5940
+ if (Array.isArray(raw)) {
5941
+ for (const item of raw) {
5942
+ if (isDutyReport(item)) out.push(item);
5943
+ }
5944
+ }
5945
+ if (agentResult?.finalText) out.push(...parseDutyReportsFromText(agentResult.finalText));
5946
+ return out;
5947
+ }
5948
+ function isDutyReport(raw) {
5949
+ 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";
5950
+ }
5951
+ function describeMessage(report) {
5952
+ const keys = [
5953
+ ...Object.keys(report.evidence ?? {}).map((key) => `evidence:${key}`),
5954
+ ...Object.keys(report.facts ?? {}).map((key) => `fact:${key}`)
5955
+ ];
5956
+ return `chore(goals): apply duty report for ${report.target.id}${keys.length ? ` (${keys.join(", ")})` : ""}`;
5957
+ }
5958
+ var applyDutyReports;
5959
+ var init_applyDutyReports = __esm({
5960
+ "src/scripts/applyDutyReports.ts"() {
5961
+ "use strict";
5962
+ init_dutyReport();
5963
+ init_state2();
5964
+ init_stateStore();
5965
+ applyDutyReports = async (ctx, _profile, agentResult) => {
5966
+ const reports = collectReports(ctx.data.dutyReports, agentResult);
5967
+ if (reports.length === 0) return;
5968
+ const owner = ctx.config.github?.owner;
5969
+ const repo = ctx.config.github?.repo;
5970
+ if (!owner || !repo) {
5971
+ process.stderr.write("[kody duty-report] missing github owner/repo; cannot apply reports\n");
5972
+ return;
5973
+ }
5974
+ const goalReports = reports.filter((report) => report.target.type === "goal");
5975
+ for (const report of goalReports) {
5976
+ const prior = fetchGoalState(owner, repo, report.target.id, ctx.cwd);
5977
+ if (!prior) {
5978
+ process.stderr.write(`[kody duty-report] goal ${report.target.id} missing on kody-state; report skipped
5979
+ `);
5980
+ continue;
5981
+ }
5982
+ const next = applyDutyReportToGoalState(prior, report);
5983
+ if (serializeGoalState(next) === serializeGoalState(prior)) continue;
5984
+ putGoalState(owner, repo, report.target.id, { ...next, updatedAt: nowIso() }, describeMessage(report), ctx.cwd);
5985
+ }
5986
+ };
5987
+ }
5988
+ });
5989
+
5421
5990
  // src/coverage.ts
5422
5991
  import { execFileSync as execFileSync10 } from "child_process";
5423
5992
  function patternToRegex(pattern) {
@@ -5584,11 +6153,11 @@ var init_classifyByLabel = __esm({
5584
6153
  });
5585
6154
 
5586
6155
  // src/scripts/commitAndPush.ts
5587
- import * as fs24 from "fs";
5588
- import * as path21 from "path";
6156
+ import * as fs25 from "fs";
6157
+ import * as path22 from "path";
5589
6158
  function sentinelPathForStage(cwd, profileName) {
5590
6159
  const runId = resolveRunId();
5591
- return path21.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6160
+ return path22.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5592
6161
  }
5593
6162
  var DEFAULT_COMMIT_MESSAGE, commitAndPush2;
5594
6163
  var init_commitAndPush = __esm({
@@ -5605,9 +6174,9 @@ var init_commitAndPush = __esm({
5605
6174
  }
5606
6175
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
5607
6176
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
5608
- if (sentinel && fs24.existsSync(sentinel)) {
6177
+ if (sentinel && fs25.existsSync(sentinel)) {
5609
6178
  try {
5610
- const replay = JSON.parse(fs24.readFileSync(sentinel, "utf-8"));
6179
+ const replay = JSON.parse(fs25.readFileSync(sentinel, "utf-8"));
5611
6180
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
5612
6181
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
5613
6182
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5660,8 +6229,8 @@ var init_commitAndPush = __esm({
5660
6229
  const result = ctx.data.commitResult;
5661
6230
  if (sentinel && result?.committed) {
5662
6231
  try {
5663
- fs24.mkdirSync(path21.dirname(sentinel), { recursive: true });
5664
- fs24.writeFileSync(
6232
+ fs25.mkdirSync(path22.dirname(sentinel), { recursive: true });
6233
+ fs25.writeFileSync(
5665
6234
  sentinel,
5666
6235
  JSON.stringify(
5667
6236
  {
@@ -5682,148 +6251,17 @@ var init_commitAndPush = __esm({
5682
6251
  }
5683
6252
  });
5684
6253
 
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
6254
  // src/scripts/commitGoalState.ts
5810
6255
  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)`;
6256
+ if (goal.state === "done") return `chore(goals): complete ${goal.id}`;
6257
+ return `chore(goals): update ${goal.id}`;
5821
6258
  }
5822
- var commitGoalState, STATE_BRANCH_LABEL;
6259
+ var STATE_BRANCH_LABEL, commitGoalState;
5823
6260
  var init_commitGoalState = __esm({
5824
6261
  "src/scripts/commitGoalState.ts"() {
5825
6262
  "use strict";
5826
6263
  init_stateStore();
6264
+ STATE_BRANCH_LABEL = "kody-state";
5827
6265
  commitGoalState = async (ctx) => {
5828
6266
  const goal = ctx.data.goal;
5829
6267
  if (!goal) return;
@@ -5833,7 +6271,7 @@ var init_commitGoalState = __esm({
5833
6271
  const owner = ctx.config.github?.owner;
5834
6272
  const repo = ctx.config.github?.repo;
5835
6273
  if (!owner || !repo) {
5836
- process.stderr.write(`[goal-tick] commitGoalState: missing github owner/repo; cannot persist ${goal.id}
6274
+ process.stderr.write(`[goal-manager] commitGoalState: missing github owner/repo; cannot persist ${goal.id}
5837
6275
  `);
5838
6276
  return;
5839
6277
  }
@@ -5841,12 +6279,11 @@ var init_commitGoalState = __esm({
5841
6279
  putGoalState(owner, repo, goal.id, updated, describeCommitMessage(goal), ctx.cwd);
5842
6280
  } catch (err) {
5843
6281
  process.stderr.write(
5844
- `[goal-tick] commitGoalState: persist to ${STATE_BRANCH_LABEL} failed (${err instanceof Error ? err.message : String(err)}); will retry next tick
6282
+ `[goal-manager] commitGoalState: persist to ${STATE_BRANCH_LABEL} failed (${err instanceof Error ? err.message : String(err)}); will retry next tick
5845
6283
  `
5846
6284
  );
5847
6285
  }
5848
6286
  };
5849
- STATE_BRANCH_LABEL = "kody-state";
5850
6287
  }
5851
6288
  });
5852
6289
 
@@ -6301,6 +6738,25 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
6301
6738
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
6302
6739
  return { number: Number(m[1]), url };
6303
6740
  }
6741
+ function buildQaManagedGoal(goalId, verdict, opened, failed) {
6742
+ return {
6743
+ type: "qa",
6744
+ destination: {
6745
+ outcome: `QA findings filed for ${goalId}`,
6746
+ evidence: ["qaFindingsFiled"]
6747
+ },
6748
+ duties: ["qa-goal"],
6749
+ route: [],
6750
+ stage: "filed",
6751
+ facts: {
6752
+ qaFindingsFiled: true,
6753
+ verdict,
6754
+ findingsOpened: opened,
6755
+ findingsFailed: failed
6756
+ },
6757
+ blockers: failed > 0 ? [`${failed} QA finding issue(s) failed to open`] : []
6758
+ };
6759
+ }
6304
6760
  async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
6305
6761
  const { markdown, data, jsonError } = splitReport(finalText);
6306
6762
  const verdict = detectVerdict(markdown);
@@ -6404,7 +6860,13 @@ ${markdown}`, ctx.cwd);
6404
6860
  }
6405
6861
  }
6406
6862
  const now = nowIso();
6407
- const goalState = { state: "active", startedAt: now, updatedAt: now, extra: { version: 1 } };
6863
+ const managedGoal = buildQaManagedGoal(goalId, verdict, opened.length, failed.length);
6864
+ const goalState = {
6865
+ state: "active",
6866
+ startedAt: now,
6867
+ updatedAt: now,
6868
+ extra: { version: 1, ...managedGoal }
6869
+ };
6408
6870
  try {
6409
6871
  putGoalState(
6410
6872
  ctx.config.github.owner,
@@ -6415,377 +6877,104 @@ ${markdown}`, ctx.cwd);
6415
6877
  ctx.cwd
6416
6878
  );
6417
6879
  } 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);
6880
+ process.stderr.write(
6881
+ `[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
6882
+ [createQaGoal] goal-scheduler will not see ${goalId} until this succeeds.
6883
+ `
6884
+ );
6686
6885
  }
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);
6886
+ const repoUrl = `https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}`;
6887
+ if (manifestIssueNumber !== null) {
6888
+ const verb = manifestUpdated ? manifestCreated ? "OPENED" : "UPDATED" : "TARGETED";
6889
+ process.stdout.write(
6890
+ `
6891
+ QA_GOAL_${verb}=${repoUrl}/issues/${manifestIssueNumber} (id: ${goalId}, verdict: ${verdict})
6892
+ `
6893
+ );
6894
+ } else {
6895
+ process.stdout.write(`
6896
+ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
6897
+ `);
6694
6898
  }
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";
6899
+ for (const o of opened) {
6900
+ process.stdout.write(`QA_FINDING_OPENED=${o.url} (severity: ${o.severity})
6901
+ `);
6703
6902
  }
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";
6903
+ if (failed.length > 0) {
6904
+ process.stdout.write(`QA_FINDINGS_FAILED=${failed.length} (see stderr above)
6905
+ `);
6729
6906
  }
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;
6907
+ ctx.data.action = qaAction(verdict, {
6908
+ goalId,
6909
+ manifestIssue: manifestIssueNumber ?? void 0,
6910
+ findingsOpened: opened.length,
6911
+ findingsFailed: failed.length,
6912
+ mode: explicitGoal ? "goal-attach" : manifestCreated ? "goal-create" : "goal-append"
6745
6913
  });
6914
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6746
6915
  }
6747
- var deriveGoalPhase;
6748
- var init_deriveGoalPhase = __esm({
6749
- "src/scripts/deriveGoalPhase.ts"() {
6916
+ var MANIFEST_LABEL, MANIFEST_TITLE, MANIFEST_START, MANIFEST_END, FINDING_LABEL, REPORT_JSON_OPEN, REPORT_JSON_CLOSE, SEVERITY_COLORS, createQaGoal;
6917
+ var init_createQaGoal = __esm({
6918
+ "src/scripts/createQaGoal.ts"() {
6750
6919
  "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}
6920
+ init_state2();
6921
+ init_stateStore();
6922
+ init_issue();
6923
+ init_postReviewResult();
6924
+ MANIFEST_LABEL = "kody:goals-manifest";
6925
+ MANIFEST_TITLE = "Kody Goals Manifest";
6926
+ MANIFEST_START = "<!-- kody-goals-start -->";
6927
+ MANIFEST_END = "<!-- kody-goals-end -->";
6928
+ FINDING_LABEL = "kody:qa-finding";
6929
+ REPORT_JSON_OPEN = "<!-- KODY_QA_REPORT_JSON";
6930
+ REPORT_JSON_CLOSE = "-->";
6931
+ SEVERITY_COLORS = {
6932
+ P0: "b60205",
6933
+ P1: "d93f0b",
6934
+ P2: "fbca04",
6935
+ P3: "0e8a16"
6936
+ };
6937
+ createQaGoal = async (ctx, _profile, agentResult) => {
6938
+ if (!agentResult || agentResult.outcome !== "completed") {
6939
+ const reason = agentResult?.error ?? "agent did not complete";
6940
+ process.stderr.write(`[createQaGoal] ${reason}
6759
6941
  `);
6760
- goal.childTasks = [];
6761
- goal.openTaskPrs = [];
6762
- goal.phase = "idle";
6942
+ ctx.output.exitCode = 1;
6943
+ ctx.output.reason = reason;
6944
+ ctx.data.action = failedAction2(reason);
6763
6945
  return;
6764
6946
  }
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";
6947
+ const finalText = agentResult.finalText.trim();
6948
+ if (!finalText) {
6949
+ process.stderr.write("[createQaGoal] agent produced no report body\n");
6950
+ ctx.output.exitCode = 1;
6951
+ ctx.output.reason = "empty report body";
6952
+ ctx.data.action = failedAction2("empty report body");
6773
6953
  return;
6774
6954
  }
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
- );
6955
+ const { markdown } = splitReport(finalText);
6956
+ const verdict = detectVerdict(markdown);
6957
+ const existingIssue = ctx.args.issue;
6958
+ if (typeof existingIssue === "number" && existingIssue > 0) {
6959
+ try {
6960
+ postIssueComment(existingIssue, finalText, ctx.cwd);
6961
+ } catch (err) {
6962
+ const msg = err instanceof Error ? err.message : String(err);
6963
+ ctx.output.exitCode = 4;
6964
+ ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
6965
+ ctx.data.action = failedAction2(ctx.output.reason);
6966
+ return;
6967
+ }
6968
+ process.stdout.write(
6969
+ `
6970
+ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
6971
+ `
6972
+ );
6973
+ ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
6974
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6975
+ return;
6976
+ }
6977
+ await promoteReportToGoal(ctx, finalText, ctx.args.scope, ctx.args.goal);
6789
6978
  };
6790
6979
  }
6791
6980
  });
@@ -8003,12 +8192,18 @@ function parseDutyFilter(raw) {
8003
8192
  function filterSlugs(slugs, onlyDuty) {
8004
8193
  return onlyDuty ? slugs.filter((slug) => slug === onlyDuty) : slugs;
8005
8194
  }
8006
- function listDutySlugs(roots) {
8195
+ function listActivatedDutySlugs(projectRoot, storeRoot, activeStoreDuties) {
8007
8196
  const seen = /* @__PURE__ */ new Set();
8008
8197
  const out = [];
8009
- for (const root of roots) {
8010
- for (const slug of listDutyFolderSlugs(root)) {
8011
- if (seen.has(slug)) continue;
8198
+ for (const slug of listDutyFolderSlugs(projectRoot)) {
8199
+ if (seen.has(slug)) continue;
8200
+ seen.add(slug);
8201
+ out.push(slug);
8202
+ }
8203
+ const active = new Set(activeStoreDuties ?? []);
8204
+ if (storeRoot && active.size > 0) {
8205
+ for (const slug of listDutyFolderSlugs(storeRoot)) {
8206
+ if (!active.has(slug) || seen.has(slug)) continue;
8012
8207
  seen.add(slug);
8013
8208
  out.push(slug);
8014
8209
  }
@@ -8086,8 +8281,10 @@ var init_dispatchDutyFileTicks = __esm({
8086
8281
  const onlyDuty = parseDutyFilter(ctx.args.duty);
8087
8282
  const jobsPath = path28.join(ctx.cwd, jobsDir);
8088
8283
  const storeJobsPath = getCompanyStoreDutiesRoot();
8089
- const dutyRoots = [jobsPath, ...storeJobsPath ? [storeJobsPath] : []];
8090
- const slugs = filterSlugs(listDutySlugs(dutyRoots), onlyDuty);
8284
+ const slugs = filterSlugs(
8285
+ listActivatedDutySlugs(jobsPath, storeJobsPath, ctx.config.company?.activeDuties),
8286
+ onlyDuty
8287
+ );
8091
8288
  ctx.data.jobSlugCount = slugs.length;
8092
8289
  if (slugs.length === 0) {
8093
8290
  const filter = onlyDuty ? ` matching ${onlyDuty}` : "";
@@ -8278,38 +8475,6 @@ var init_dispatchDutyTicks = __esm({
8278
8475
  }
8279
8476
  });
8280
8477
 
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
8478
  // src/scripts/dispatchNextTaskJob.ts
8314
8479
  function taskJobToJob(job, issueArg) {
8315
8480
  const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
@@ -8687,133 +8852,6 @@ var init_failOnceTaskJob = __esm({
8687
8852
  }
8688
8853
  });
8689
8854
 
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
8855
  // src/scripts/finalizeTerminal.ts
8818
8856
  var DONE, FAILED, finalizeTerminal;
8819
8857
  var init_finalizeTerminal = __esm({
@@ -9363,58 +9401,6 @@ var init_fixFlow = __esm({
9363
9401
  }
9364
9402
  });
9365
9403
 
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
9404
  // src/scripts/initFlow.ts
9419
9405
  import { execFileSync as execFileSync17 } from "child_process";
9420
9406
  import * as fs31 from "fs";
@@ -9788,7 +9774,7 @@ var init_loadGoalState = __esm({
9788
9774
  try {
9789
9775
  const state = fetchGoalState(owner, repo, goalId, ctx.cwd);
9790
9776
  if (!state) {
9791
- process.stdout.write(`[goal-tick] no goal state for ${goalId} on ${owner}/${repo} \u2014 nothing to tick
9777
+ process.stdout.write(`[goal-manager] no goal state for ${goalId} on ${owner}/${repo}; nothing to tick
9792
9778
  `);
9793
9779
  ctx.skipAgent = true;
9794
9780
  ctx.output.exitCode = 0;
@@ -9798,17 +9784,11 @@ var init_loadGoalState = __esm({
9798
9784
  ctx.data.goal = {
9799
9785
  id: goalId,
9800
9786
  state: state.state,
9801
- lastDispatchedIssue: state.lastDispatchedIssue,
9802
- // Cache the full parsed object so saveGoalState can preserve `extra`.
9803
9787
  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
9788
  defaultBranch: ctx.config.git.defaultBranch
9809
9789
  };
9810
9790
  } catch (err) {
9811
- process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
9791
+ process.stdout.write(`[goal-manager] ${err instanceof Error ? err.message : String(err)}
9812
9792
  `);
9813
9793
  ctx.skipAgent = true;
9814
9794
  ctx.output.exitCode = 0;
@@ -9933,23 +9913,7 @@ var init_staff = __esm({
9933
9913
  "use strict";
9934
9914
  init_companyStore();
9935
9915
  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
- };
9916
+ BUILTIN_PERSONAS = {};
9953
9917
  }
9954
9918
  });
9955
9919
 
@@ -10402,6 +10366,37 @@ var init_markFlowSuccess = __esm({
10402
10366
  }
10403
10367
  });
10404
10368
 
10369
+ // src/goal/operations.ts
10370
+ function fail(err) {
10371
+ if (err instanceof Error) {
10372
+ const lines = err.message.split("\n").filter(Boolean);
10373
+ return { ok: false, error: lines[0] ?? err.message };
10374
+ }
10375
+ return { ok: false, error: String(err) };
10376
+ }
10377
+ function commentOnIssue(issueNumber, body, cwd) {
10378
+ try {
10379
+ gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
10380
+ return { ok: true };
10381
+ } catch (err) {
10382
+ return fail(err);
10383
+ }
10384
+ }
10385
+ function mergePrSquash(prNumber, cwd) {
10386
+ try {
10387
+ gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
10388
+ return { ok: true };
10389
+ } catch (err) {
10390
+ return fail(err);
10391
+ }
10392
+ }
10393
+ var init_operations = __esm({
10394
+ "src/goal/operations.ts"() {
10395
+ "use strict";
10396
+ init_issue();
10397
+ }
10398
+ });
10399
+
10405
10400
  // src/scripts/mergeFlow.ts
10406
10401
  function readPr(prNumber, cwd) {
10407
10402
  const out = gh(["pr", "view", String(prNumber), "--json", "state,isDraft,mergeable,mergeStateStatus,title,url"], {
@@ -12159,7 +12154,7 @@ var init_previewBuildHelpers = __esm({
12159
12154
  // src/scripts/previewBuildRun.ts
12160
12155
  import { spawn as spawn4 } from "child_process";
12161
12156
  async function runCmd(cmd, args, opts = {}) {
12162
- await new Promise((resolve6, reject) => {
12157
+ await new Promise((resolve7, reject) => {
12163
12158
  const child = spawn4(cmd, args, {
12164
12159
  cwd: opts.cwd,
12165
12160
  env: { ...process.env, ...opts.env ?? {} },
@@ -12171,7 +12166,7 @@ async function runCmd(cmd, args, opts = {}) {
12171
12166
  }
12172
12167
  child.on("error", reject);
12173
12168
  child.on("close", (code) => {
12174
- if (code === 0) resolve6();
12169
+ if (code === 0) resolve7();
12175
12170
  else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
12176
12171
  });
12177
12172
  });
@@ -12811,29 +12806,21 @@ var init_runTickScript = __esm({
12811
12806
  }
12812
12807
  });
12813
12808
 
12814
- // src/scripts/saveGoalState.ts
12815
- var saveGoalState;
12816
- var init_saveGoalState = __esm({
12817
- "src/scripts/saveGoalState.ts"() {
12809
+ // src/scripts/saveManagedGoalState.ts
12810
+ var saveManagedGoalState;
12811
+ var init_saveManagedGoalState = __esm({
12812
+ "src/scripts/saveManagedGoalState.ts"() {
12818
12813
  "use strict";
12819
12814
  init_state2();
12820
- saveGoalState = async (ctx) => {
12815
+ saveManagedGoalState = async (ctx) => {
12816
+ ctx.skipAgent = true;
12821
12817
  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
- };
12818
+ if (!goal?.raw) return;
12819
+ const original = typeof ctx.data.goalOriginalStateText === "string" ? ctx.data.goalOriginalStateText : "";
12820
+ const changedBeforeTimestamp = original !== serializeGoalState(goal.raw);
12821
+ const updated = changedBeforeTimestamp ? { ...goal.raw, updatedAt: nowIso() } : goal.raw;
12834
12822
  ctx.data.goalPersistState = updated;
12835
- ctx.data.goalPersistChanged = changed;
12836
- ctx.skipAgent = true;
12823
+ ctx.data.goalPersistChanged = original !== serializeGoalState(updated);
12837
12824
  };
12838
12825
  }
12839
12826
  });
@@ -13125,7 +13112,7 @@ function stripAnsi2(s) {
13125
13112
  return s.replace(ANSI_RE2, "");
13126
13113
  }
13127
13114
  function runCommand2(command, cwd) {
13128
- return new Promise((resolve6) => {
13115
+ return new Promise((resolve7) => {
13129
13116
  const child = spawn5(command, {
13130
13117
  cwd,
13131
13118
  shell: true,
@@ -13152,11 +13139,11 @@ function runCommand2(command, cwd) {
13152
13139
  }, TEST_TIMEOUT_MS);
13153
13140
  child.on("exit", (code) => {
13154
13141
  clearTimeout(timer);
13155
- resolve6({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
13142
+ resolve7({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
13156
13143
  });
13157
13144
  child.on("error", (err) => {
13158
13145
  clearTimeout(timer);
13159
- resolve6({ exitCode: -1, output: err.message });
13146
+ resolve7({ exitCode: -1, output: err.message });
13160
13147
  });
13161
13148
  });
13162
13149
  }
@@ -13561,21 +13548,21 @@ function lineStream(stream) {
13561
13548
  tryDeliver();
13562
13549
  });
13563
13550
  return {
13564
- next: (timeoutMs) => new Promise((resolve6) => {
13551
+ next: (timeoutMs) => new Promise((resolve7) => {
13565
13552
  if (queue.length > 0) {
13566
- resolve6(queue.shift());
13553
+ resolve7(queue.shift());
13567
13554
  return;
13568
13555
  }
13569
13556
  if (ended) {
13570
- resolve6(null);
13557
+ resolve7(null);
13571
13558
  return;
13572
13559
  }
13573
- waiter = resolve6;
13560
+ waiter = resolve7;
13574
13561
  const t = setTimeout(
13575
13562
  () => {
13576
- if (waiter === resolve6) {
13563
+ if (waiter === resolve7) {
13577
13564
  waiter = null;
13578
- resolve6(null);
13565
+ resolve7(null);
13579
13566
  }
13580
13567
  },
13581
13568
  Math.max(0, timeoutMs)
@@ -13756,7 +13743,9 @@ var init_scripts = __esm({
13756
13743
  "use strict";
13757
13744
  init_abortUnfinishedGitOps();
13758
13745
  init_advanceFlow();
13746
+ init_advanceManagedGoal();
13759
13747
  init_appendCompanyActivity();
13748
+ init_applyDutyReports();
13760
13749
  init_buildSyntheticPlugin();
13761
13750
  init_checkCoverageWithRetry();
13762
13751
  init_classifyByLabel();
@@ -13764,7 +13753,6 @@ var init_scripts = __esm({
13764
13753
  init_commitGoalState();
13765
13754
  init_composePrompt();
13766
13755
  init_createQaGoal();
13767
- init_deriveGoalPhase();
13768
13756
  init_deriveQaScopeFromIssue();
13769
13757
  init_diagMcp();
13770
13758
  init_discoverQaContext();
@@ -13772,16 +13760,13 @@ var init_scripts = __esm({
13772
13760
  init_dispatchClassified();
13773
13761
  init_dispatchDutyFileTicks();
13774
13762
  init_dispatchDutyTicks();
13775
- init_dispatchNextTask();
13776
13763
  init_dispatchNextTaskJob();
13777
13764
  init_ensurePr();
13778
13765
  init_failOnceTaskJob();
13779
- init_finalizeGoal();
13780
13766
  init_finalizeTerminal();
13781
13767
  init_finishFlow();
13782
13768
  init_fixCiFlow();
13783
13769
  init_fixFlow();
13784
- init_handleAbandonedGoal();
13785
13770
  init_initFlow();
13786
13771
  init_loadConventions();
13787
13772
  init_loadCoverageRules();
@@ -13830,7 +13815,7 @@ var init_scripts = __esm({
13830
13815
  init_runPreviewBuild();
13831
13816
  init_runScheduledExecutableTick();
13832
13817
  init_runTickScript();
13833
- init_saveGoalState();
13818
+ init_saveManagedGoalState();
13834
13819
  init_saveTaskState();
13835
13820
  init_setCommentTarget();
13836
13821
  init_setLifecycleLabel();
@@ -13890,12 +13875,9 @@ var init_scripts = __esm({
13890
13875
  runScheduledExecutableTick,
13891
13876
  runTickScript,
13892
13877
  runPreviewBuild,
13878
+ advanceManagedGoal,
13893
13879
  loadGoalState,
13894
- handleAbandonedGoal,
13895
- deriveGoalPhase,
13896
- dispatchNextTask,
13897
- finalizeGoal,
13898
- saveGoalState
13880
+ saveManagedGoalState
13899
13881
  };
13900
13882
  postflightScripts = {
13901
13883
  parseAgentResult: parseAgentResult2,
@@ -13930,6 +13912,7 @@ var init_scripts = __esm({
13930
13912
  finalizeTerminal,
13931
13913
  advanceFlow,
13932
13914
  persistFlowState,
13915
+ applyDutyReports,
13933
13916
  recordClassification,
13934
13917
  dispatchClassified,
13935
13918
  notifyTerminal,
@@ -14120,6 +14103,8 @@ async function runExecutable(profileName, input) {
14120
14103
  }
14121
14104
  const perExecutableModel = config.agent.perExecutable?.[profileName];
14122
14105
  const modelSpec = perExecutableModel ? perExecutableModel : profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
14106
+ const profileHasThinkingTokens = typeof profile.claudeCode.maxThinkingTokens === "number" && profile.claudeCode.maxThinkingTokens > 0;
14107
+ const reasoningEffort = config.agent.perExecutableReasoningEffort?.[profileName] ?? profile.claudeCode.reasoningEffort ?? (profileHasThinkingTokens ? void 0 : config.agent.reasoningEffort);
14123
14108
  let model;
14124
14109
  try {
14125
14110
  model = parseProviderModel(modelSpec);
@@ -14195,6 +14180,7 @@ async function runExecutable(profileName, input) {
14195
14180
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
14196
14181
  agents,
14197
14182
  maxTurns: profile.claudeCode.maxTurns,
14183
+ reasoningEffort,
14198
14184
  maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
14199
14185
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14200
14186
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
@@ -14689,14 +14675,14 @@ async function runShellEntry(entry, ctx, profile) {
14689
14675
  let killTimer;
14690
14676
  let escalateTimer;
14691
14677
  const result = await new Promise(
14692
- (resolve6) => {
14678
+ (resolve7) => {
14693
14679
  let settled = false;
14694
14680
  const settle = (code, signal, spawnErr) => {
14695
14681
  if (settled) return;
14696
14682
  settled = true;
14697
14683
  if (killTimer) clearTimeout(killTimer);
14698
14684
  if (escalateTimer) clearTimeout(escalateTimer);
14699
- resolve6({ code, signal, spawnErr });
14685
+ resolve7({ code, signal, spawnErr });
14700
14686
  };
14701
14687
  child.on("error", (err) => settle(null, null, err));
14702
14688
  child.on("close", (code, signal) => settle(code, signal));
@@ -14732,6 +14718,11 @@ async function runShellEntry(entry, ctx, profile) {
14732
14718
  if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
14733
14719
  const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
14734
14720
  if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
14721
+ const dutyReports = parseDutyReportsFromText(stdout);
14722
+ if (dutyReports.length > 0) {
14723
+ const prior = Array.isArray(ctx.data.dutyReports) ? ctx.data.dutyReports : [];
14724
+ ctx.data.dutyReports = [...prior, ...dutyReports];
14725
+ }
14735
14726
  if (timedOut) {
14736
14727
  ctx.skipAgent = true;
14737
14728
  const seconds = Math.round(timeoutMs / 1e3);
@@ -14781,6 +14772,7 @@ var init_executor = __esm({
14781
14772
  init_config();
14782
14773
  init_container();
14783
14774
  init_discipline();
14775
+ init_dutyReport();
14784
14776
  init_events();
14785
14777
  init_lifecycleLabels();
14786
14778
  init_litellm();
@@ -14791,7 +14783,7 @@ var init_executor = __esm({
14791
14783
  init_subagents();
14792
14784
  init_task_artifacts();
14793
14785
  init_tools();
14794
- MUTATING_POSTFLIGHTS = /* @__PURE__ */ new Set(["commitAndPush", "ensurePr"]);
14786
+ MUTATING_POSTFLIGHTS = /* @__PURE__ */ new Set(["commitAndPush", "ensurePr", "applyDutyReports"]);
14795
14787
  MAX_CHAIN_HOPS = 60;
14796
14788
  DEFAULT_SHELL_TIMEOUT_MS = 3e5;
14797
14789
  SIGKILL_GRACE_MS = 5e3;
@@ -14897,7 +14889,8 @@ async function runJob(job, base) {
14897
14889
  quiet: base.quiet,
14898
14890
  preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
14899
14891
  };
14900
- input.cliArgs = resolvedDuty && profileName === resolvedDuty.executable ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
14892
+ const shouldApplyResolvedDutyArgs = valid.executable === void 0 && resolvedDuty && profileName === resolvedDuty.executable;
14893
+ input.cliArgs = shouldApplyResolvedDutyArgs ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
14901
14894
  const run = base.chain === false ? runExecutable : runExecutableChain;
14902
14895
  return run(profileName, input);
14903
14896
  }
@@ -16811,17 +16804,17 @@ function authOk(req, expected) {
16811
16804
  return false;
16812
16805
  }
16813
16806
  function readJsonBody(req) {
16814
- return new Promise((resolve6, reject) => {
16807
+ return new Promise((resolve7, reject) => {
16815
16808
  const chunks = [];
16816
16809
  req.on("data", (c) => chunks.push(c));
16817
16810
  req.on("end", () => {
16818
16811
  const raw = Buffer.concat(chunks).toString("utf-8");
16819
16812
  if (!raw.trim()) {
16820
- resolve6({});
16813
+ resolve7({});
16821
16814
  return;
16822
16815
  }
16823
16816
  try {
16824
- resolve6(JSON.parse(raw));
16817
+ resolve7(JSON.parse(raw));
16825
16818
  } catch (err) {
16826
16819
  reject(err instanceof Error ? err : new Error(String(err)));
16827
16820
  }
@@ -17070,11 +17063,11 @@ async function brainServe(opts) {
17070
17063
  model,
17071
17064
  litellmUrl
17072
17065
  });
17073
- await new Promise((resolve6) => {
17066
+ await new Promise((resolve7) => {
17074
17067
  server.listen(port, "0.0.0.0", () => {
17075
17068
  process.stdout.write(`[brain-serve] listening on 0.0.0.0:${port} (cwd=${opts.cwd})
17076
17069
  `);
17077
- resolve6();
17070
+ resolve7();
17078
17071
  });
17079
17072
  });
17080
17073
  const shutdown = (signal) => {
@@ -17324,14 +17317,14 @@ async function startBrainProxy(opts) {
17324
17317
  const { httpServer, handler } = buildBrainProxy(opts);
17325
17318
  const port = opts.port ?? 0;
17326
17319
  const host = opts.host ?? "127.0.0.1";
17327
- await new Promise((resolve6) => httpServer.listen(port, host, () => resolve6()));
17320
+ await new Promise((resolve7) => httpServer.listen(port, host, () => resolve7()));
17328
17321
  const addr = httpServer.address();
17329
17322
  return {
17330
17323
  httpServer,
17331
17324
  port: addr.port,
17332
17325
  url: `http://${host}:${addr.port}`,
17333
- stop: () => new Promise((resolve6) => {
17334
- httpServer.close(() => resolve6());
17326
+ stop: () => new Promise((resolve7) => {
17327
+ httpServer.close(() => resolve7());
17335
17328
  }),
17336
17329
  handler
17337
17330
  };
@@ -17481,23 +17474,23 @@ function buildMcpHttpServer(opts) {
17481
17474
  httpServer,
17482
17475
  routes,
17483
17476
  port,
17484
- stop: () => new Promise((resolve6) => {
17477
+ stop: () => new Promise((resolve7) => {
17485
17478
  let pending = transports.size;
17486
17479
  if (pending === 0) {
17487
- httpServer.close(() => resolve6());
17480
+ httpServer.close(() => resolve7());
17488
17481
  return;
17489
17482
  }
17490
17483
  for (const transport of transports.values()) {
17491
17484
  void transport.close().finally(() => {
17492
17485
  pending--;
17493
- if (pending === 0) httpServer.close(() => resolve6());
17486
+ if (pending === 0) httpServer.close(() => resolve7());
17494
17487
  });
17495
17488
  }
17496
17489
  })
17497
17490
  };
17498
17491
  }
17499
17492
  function listenMcpHttpServer(server, host = "127.0.0.1") {
17500
- return new Promise((resolve6, reject) => {
17493
+ return new Promise((resolve7, reject) => {
17501
17494
  server.httpServer.once("error", reject);
17502
17495
  server.httpServer.listen(server.port, host, () => {
17503
17496
  server.httpServer.off("error", reject);
@@ -17505,7 +17498,7 @@ function listenMcpHttpServer(server, host = "127.0.0.1") {
17505
17498
  if (addr && typeof addr === "object") {
17506
17499
  server.port = addr.port;
17507
17500
  }
17508
- resolve6();
17501
+ resolve7();
17509
17502
  });
17510
17503
  });
17511
17504
  }
@@ -17644,7 +17637,7 @@ async function waitForNextUserMessage(opts) {
17644
17637
  }
17645
17638
  }
17646
17639
  function sleep2(ms) {
17647
- return new Promise((resolve6) => setTimeout(resolve6, ms));
17640
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
17648
17641
  }
17649
17642
  function currentBranch(cwd) {
17650
17643
  try {
@@ -18759,14 +18752,14 @@ function sendJson2(res, status, body) {
18759
18752
  res.end(JSON.stringify(body));
18760
18753
  }
18761
18754
  function readJsonBody2(req) {
18762
- return new Promise((resolve6, reject) => {
18755
+ return new Promise((resolve7, reject) => {
18763
18756
  const chunks = [];
18764
18757
  req.on("data", (c) => chunks.push(c));
18765
18758
  req.on("end", () => {
18766
18759
  const raw = Buffer.concat(chunks).toString("utf-8");
18767
- if (!raw.trim()) return resolve6({});
18760
+ if (!raw.trim()) return resolve7({});
18768
18761
  try {
18769
- resolve6(JSON.parse(raw));
18762
+ resolve7(JSON.parse(raw));
18770
18763
  } catch (err) {
18771
18764
  reject(err instanceof Error ? err : new Error(String(err)));
18772
18765
  }
@@ -18892,10 +18885,10 @@ async function poolServe() {
18892
18885
  }
18893
18886
  });
18894
18887
  const apiHost = process.env.POOL_API_HOST ?? "::";
18895
- await new Promise((resolve6) => {
18888
+ await new Promise((resolve7) => {
18896
18889
  server.listen(apiPort, apiHost, () => {
18897
18890
  log(`listening on ${apiHost}:${apiPort} (min=${min}, app=${app}, region=${region})`);
18898
- resolve6();
18891
+ resolve7();
18899
18892
  });
18900
18893
  });
18901
18894
  const shutdown = (signal) => {
@@ -18934,17 +18927,17 @@ function authOk2(req, expected) {
18934
18927
  return false;
18935
18928
  }
18936
18929
  function readJsonBody3(req) {
18937
- return new Promise((resolve6, reject) => {
18930
+ return new Promise((resolve7, reject) => {
18938
18931
  const chunks = [];
18939
18932
  req.on("data", (c) => chunks.push(c));
18940
18933
  req.on("end", () => {
18941
18934
  const raw = Buffer.concat(chunks).toString("utf-8");
18942
18935
  if (!raw.trim()) {
18943
- resolve6({});
18936
+ resolve7({});
18944
18937
  return;
18945
18938
  }
18946
18939
  try {
18947
- resolve6(JSON.parse(raw));
18940
+ resolve7(JSON.parse(raw));
18948
18941
  } catch (err) {
18949
18942
  reject(err instanceof Error ? err : new Error(String(err)));
18950
18943
  }
@@ -19023,13 +19016,13 @@ async function defaultRunJob(job) {
19023
19016
  ...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
19024
19017
  ...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
19025
19018
  };
19026
- const run = (cmd, args, cwd) => new Promise((resolve6) => {
19019
+ const run = (cmd, args, cwd) => new Promise((resolve7) => {
19027
19020
  const child = spawn8(cmd, args, { stdio: "inherit", env: childEnv, cwd });
19028
- child.on("exit", (code) => resolve6(code ?? 0));
19021
+ child.on("exit", (code) => resolve7(code ?? 0));
19029
19022
  child.on("error", (err) => {
19030
19023
  process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
19031
19024
  `);
19032
- resolve6(1);
19025
+ resolve7(1);
19033
19026
  });
19034
19027
  });
19035
19028
  process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
@@ -19106,11 +19099,11 @@ async function runnerServe() {
19106
19099
  const port = Number(process.env.PORT ?? DEFAULT_PORT2);
19107
19100
  const server = buildServer2({ apiKey });
19108
19101
  const host = process.env.RUNNER_HOST ?? "::";
19109
- await new Promise((resolve6) => {
19102
+ await new Promise((resolve7) => {
19110
19103
  server.listen(port, host, () => {
19111
19104
  process.stdout.write(`[runner-serve] listening on ${host}:${port} (idle, awaiting job)
19112
19105
  `);
19113
- resolve6();
19106
+ resolve7();
19114
19107
  });
19115
19108
  });
19116
19109
  const shutdown = (signal) => {
@@ -19129,7 +19122,7 @@ async function runnerServe() {
19129
19122
  init_config();
19130
19123
  init_litellm();
19131
19124
  import { spawn as spawn9 } from "child_process";
19132
- function parseTarget(positional) {
19125
+ function parseTarget2(positional) {
19133
19126
  if (!Array.isArray(positional) || positional.length === 0) return "none";
19134
19127
  const first = String(positional[0]).toLowerCase();
19135
19128
  if (first === "vscode" || first === "code") return "vscode";
@@ -19144,7 +19137,7 @@ function buildProxyEnv(url) {
19144
19137
  };
19145
19138
  }
19146
19139
  async function serve(opts) {
19147
- const target = parseTarget(opts.args);
19140
+ const target = parseTarget2(opts.args);
19148
19141
  const model = parseProviderModel(opts.config.agent.model);
19149
19142
  const usesProxy = needsLitellmProxy(model);
19150
19143
  let handle = null;
@@ -19179,14 +19172,14 @@ async function serve(opts) {
19179
19172
  `);
19180
19173
  const args = ["--dangerously-skip-permissions", "--model", model.model];
19181
19174
  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));
19175
+ const exitCode = await new Promise((resolve7) => {
19176
+ child.on("exit", (code) => resolve7(code ?? 0));
19184
19177
  child.on("error", (err) => {
19185
19178
  process.stderr.write(`[kody serve] failed to launch Claude Code: ${err.message}
19186
19179
  `);
19187
19180
  process.stderr.write(` Install: https://docs.anthropic.com/claude/docs/claude-code
19188
19181
  `);
19189
- resolve6(1);
19182
+ resolve7(1);
19190
19183
  });
19191
19184
  });
19192
19185
  killProxy();