@kody-ade/kody-engine 0.4.372 → 0.4.374

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.
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.372",
18
+ version: "0.4.374",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -1064,8 +1064,8 @@ function formatAssistant(msg, _opts) {
1064
1064
  const lines = [];
1065
1065
  for (const block of content) {
1066
1066
  if (block.type === "text") {
1067
- const text = block.text.trim();
1068
- if (text) lines.push(text);
1067
+ const text2 = block.text.trim();
1068
+ if (text2) lines.push(text2);
1069
1069
  } else if (block.type === "tool_use") {
1070
1070
  const tu = block;
1071
1071
  lines.push(`\u2192 ${tu.name}${summarizeToolInput(tu.name, tu.input)}`);
@@ -1079,14 +1079,14 @@ function formatUserToolResult(msg, opts) {
1079
1079
  for (const block of content) {
1080
1080
  if (block.type === "tool_result") {
1081
1081
  const tr = block;
1082
- const text = stringifyToolContent(tr.content);
1083
- const lineCount = text.split("\n").length;
1084
- const sizeBytes = text.length;
1082
+ const text2 = stringifyToolContent(tr.content);
1083
+ const lineCount = text2.split("\n").length;
1084
+ const sizeBytes = text2.length;
1085
1085
  const flag = tr.is_error ? " ERROR" : "";
1086
1086
  const summary = ` \u21B3${flag} ${lineCount} lines, ${formatBytes(sizeBytes)}`;
1087
1087
  if (opts.verbose) {
1088
1088
  lines.push(`${summary}
1089
- ${truncate2(text, 4e3)}`);
1089
+ ${truncate2(text2, 4e3)}`);
1090
1090
  } else {
1091
1091
  lines.push(summary);
1092
1092
  }
@@ -1612,7 +1612,7 @@ function cmsHeaders(opts) {
1612
1612
  }
1613
1613
  };
1614
1614
  }
1615
- async function callDashboardCms(opts, path51, init = {}) {
1615
+ async function callDashboardCms(opts, path52, init = {}) {
1616
1616
  const baseUrl = dashboardBaseUrl(opts);
1617
1617
  if (!baseUrl) {
1618
1618
  return {
@@ -1624,7 +1624,7 @@ async function callDashboardCms(opts, path51, init = {}) {
1624
1624
  const headerResult = cmsHeaders(opts);
1625
1625
  if (!headerResult.ok) return headerResult;
1626
1626
  try {
1627
- const res = await fetch(`${baseUrl}${path51}`, {
1627
+ const res = await fetch(`${baseUrl}${path52}`, {
1628
1628
  ...init,
1629
1629
  headers: {
1630
1630
  ...headerResult.headers,
@@ -1696,8 +1696,8 @@ function documentArg(value) {
1696
1696
  function normalizeCmsDocumentIdInput(input) {
1697
1697
  const trimmed = stripWrappingQuotes(input.trim());
1698
1698
  const withoutQuery = trimmed.split(/[?#]/, 1)[0] ?? trimmed;
1699
- const path51 = parseDocumentPath(withoutQuery);
1700
- return path51 ?? parseDocumentIdSegment(withoutQuery) ?? withoutQuery;
1699
+ const path52 = parseDocumentPath(withoutQuery);
1700
+ return path52 ?? parseDocumentIdSegment(withoutQuery) ?? withoutQuery;
1701
1701
  }
1702
1702
  function stripWrappingQuotes(value) {
1703
1703
  let current = value;
@@ -1708,9 +1708,9 @@ function stripWrappingQuotes(value) {
1708
1708
  }
1709
1709
  }
1710
1710
  function parseDocumentPath(value) {
1711
- const path51 = value.startsWith("http://") || value.startsWith("https://") ? urlPathname(value) : value;
1712
- if (!path51?.includes("/content/entries/")) return null;
1713
- const parts = path51.split("/").filter(Boolean).map(decodePathPart);
1711
+ const path52 = value.startsWith("http://") || value.startsWith("https://") ? urlPathname(value) : value;
1712
+ if (!path52?.includes("/content/entries/")) return null;
1713
+ const parts = path52.split("/").filter(Boolean).map(decodePathPart);
1714
1714
  const entriesIndex = parts.findIndex((part, index) => part === "content" && parts[index + 1] === "entries");
1715
1715
  const idPart = parts[entriesIndex + 3];
1716
1716
  if (!idPart || idPart === "new") return null;
@@ -1970,7 +1970,12 @@ function stringList(value) {
1970
1970
  function parseCapabilityWorkflow(value) {
1971
1971
  const stepsRaw = Array.isArray(value) ? value : value && typeof value === "object" && Array.isArray(value.steps) ? value.steps : [];
1972
1972
  const steps = stepsRaw.map(parseWorkflowStep).filter((step) => step !== null);
1973
- return steps.length > 0 ? { steps } : void 0;
1973
+ if (steps.length === 0) return void 0;
1974
+ const startAt = value && typeof value === "object" && !Array.isArray(value) ? stringField(value.startAt) : void 0;
1975
+ return {
1976
+ steps,
1977
+ ...startAt && isSafeSlug(startAt) ? { startAt } : {}
1978
+ };
1974
1979
  }
1975
1980
  function parseWorkflowStep(value) {
1976
1981
  if (typeof value === "string") {
@@ -1982,6 +1987,7 @@ function parseWorkflowStep(value) {
1982
1987
  const capability = stringField(raw.capability ?? raw.action);
1983
1988
  if (!capability || !isSafeSlug(capability)) return null;
1984
1989
  const implementation = stringField(raw.implementation);
1990
+ const id = stringField(raw.id);
1985
1991
  const action = stringField(raw.action);
1986
1992
  const evidence = stringField(raw.evidence);
1987
1993
  const agent = stringField(raw.agent);
@@ -1989,9 +1995,12 @@ function parseWorkflowStep(value) {
1989
1995
  const target = stringField(raw.target);
1990
1996
  const targetFact = stringField(raw.targetFact ?? raw.target_fact);
1991
1997
  const cliArgs = raw.cliArgs;
1998
+ const inputs = parseWorkflowInputs(raw.inputs);
1999
+ const next = parseWorkflowTransitions(raw.next);
1992
2000
  const report = parseReportPublication(raw.report);
1993
2001
  return {
1994
2002
  capability,
2003
+ ...id && isSafeSlug(id) ? { id } : {},
1995
2004
  ...action && isSafeSlug(action) ? { action } : {},
1996
2005
  ...implementation && isSafeSlug(implementation) ? { implementation } : {},
1997
2006
  ...evidence ? { evidence } : {},
@@ -2000,12 +2009,45 @@ function parseWorkflowStep(value) {
2000
2009
  ...agent && isSafeSlug(agent) ? { agent } : {},
2001
2010
  ...reason ? { reason } : {},
2002
2011
  ...cliArgs && typeof cliArgs === "object" && !Array.isArray(cliArgs) ? { cliArgs } : {},
2012
+ ...inputs ? { inputs } : {},
2013
+ ...next ? { next } : {},
2003
2014
  ...isPlainObject(raw.runWhen) ? { runWhen: raw.runWhen } : {},
2004
2015
  ...stringList(raw.continueOn ?? raw.continue_on).length > 0 ? { continueOn: stringList(raw.continueOn ?? raw.continue_on) } : {},
2005
2016
  ...raw.saveReport === true ? { saveReport: true } : {},
2006
2017
  ...report ? { report } : {}
2007
2018
  };
2008
2019
  }
2020
+ function parseWorkflowInputs(value) {
2021
+ if (!isPlainObject(value)) return void 0;
2022
+ const inputs = {};
2023
+ for (const [name, raw] of Object.entries(value)) {
2024
+ if (!isSafeSlug(name) || !isPlainObject(raw)) continue;
2025
+ const from = stringField(raw.from);
2026
+ if (!from) continue;
2027
+ inputs[name] = { from };
2028
+ }
2029
+ return Object.keys(inputs).length > 0 ? inputs : void 0;
2030
+ }
2031
+ function parseWorkflowTransitions(value) {
2032
+ const rawTransitions = Array.isArray(value) ? value : value === void 0 ? [] : [value];
2033
+ const transitions = rawTransitions.map((raw) => {
2034
+ if (typeof raw === "string") {
2035
+ const to2 = raw.trim();
2036
+ return isSafeSlug(to2) ? { to: to2 } : null;
2037
+ }
2038
+ if (!isPlainObject(raw)) return null;
2039
+ const to = stringField(raw.to);
2040
+ if (!to || !isSafeSlug(to)) return null;
2041
+ const maxIterations = typeof raw.maxIterations === "number" && Number.isInteger(raw.maxIterations) && raw.maxIterations > 0 ? raw.maxIterations : void 0;
2042
+ return {
2043
+ to,
2044
+ ...isPlainObject(raw.when) ? { when: raw.when } : {},
2045
+ ...raw.default === true ? { default: true } : {},
2046
+ ...maxIterations ? { maxIterations } : {}
2047
+ };
2048
+ }).filter((transition) => transition !== null);
2049
+ return transitions.length > 0 ? transitions : void 0;
2050
+ }
2009
2051
  function parseReportPublication(value) {
2010
2052
  if (!isPlainObject(value)) return void 0;
2011
2053
  const type = stringField(value.type);
@@ -2317,8 +2359,8 @@ function resolveCapabilityFolder(slug, projectCapabilitiesRoot = getProjectCapab
2317
2359
  }
2318
2360
  return null;
2319
2361
  }
2320
- function getCapabilityActionInputs(action) {
2321
- const resolved = resolveCapabilityAction(action);
2362
+ function getCapabilityActionInputs(action, projectCapabilitiesRoot = getProjectCapabilitiesRoot()) {
2363
+ const resolved = resolveCapabilityAction(action, projectCapabilitiesRoot);
2322
2364
  if (!resolved) return null;
2323
2365
  return getProfileInputs(resolved.implementation);
2324
2366
  }
@@ -2637,20 +2679,20 @@ function readLedger(label) {
2637
2679
  const raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "5", "--json", "number,body"]);
2638
2680
  const issues = JSON.parse(raw);
2639
2681
  if (issues.length === 0) return { found: false, payload: null };
2640
- const issue = issues.sort((a, b) => a.number - b.number)[0];
2641
- const body = issue?.body ?? "";
2682
+ const issue2 = issues.sort((a, b) => a.number - b.number)[0];
2683
+ const body = issue2?.body ?? "";
2642
2684
  const startIdx = body.indexOf(startTag);
2643
2685
  const endIdx = body.indexOf(endTag);
2644
2686
  if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
2645
- return { found: true, issueNumber: issue?.number, payload: null };
2687
+ return { found: true, issueNumber: issue2?.number, payload: null };
2646
2688
  }
2647
2689
  const between = body.slice(startIdx + startTag.length, endIdx);
2648
2690
  const fenceMatch = between.match(/```json\s*([\s\S]*?)```/);
2649
- if (!fenceMatch) return { found: true, issueNumber: issue?.number, payload: null };
2691
+ if (!fenceMatch) return { found: true, issueNumber: issue2?.number, payload: null };
2650
2692
  try {
2651
- return { found: true, issueNumber: issue?.number, payload: JSON.parse(fenceMatch[1]) };
2693
+ return { found: true, issueNumber: issue2?.number, payload: JSON.parse(fenceMatch[1]) };
2652
2694
  } catch {
2653
- return { found: true, issueNumber: issue?.number, payload: null };
2695
+ return { found: true, issueNumber: issue2?.number, payload: null };
2654
2696
  }
2655
2697
  } catch (err) {
2656
2698
  return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
@@ -2698,12 +2740,7 @@ function readCheckRuns(repoSlug, ref, ignoreNames) {
2698
2740
  let rawStatuses = "";
2699
2741
  try {
2700
2742
  rawStatuses = gh(
2701
- [
2702
- "api",
2703
- `repos/${repoSlug}/commits/${sha}/status`,
2704
- "--jq",
2705
- ".statuses[] | {context, state, target_url}"
2706
- ],
2743
+ ["api", `repos/${repoSlug}/commits/${sha}/status`, "--jq", ".statuses[] | {context, state, target_url}"],
2707
2744
  ghOptions
2708
2745
  );
2709
2746
  } catch {
@@ -2745,14 +2782,14 @@ ${marker}`
2745
2782
  return { error: err instanceof Error ? err.message : String(err) };
2746
2783
  }
2747
2784
  }
2748
- function ensureComment(repoSlug, issue, key, body) {
2785
+ function ensureComment(repoSlug, issue2, key, body) {
2749
2786
  const marker = commentMarker(key);
2750
2787
  try {
2751
- const raw = gh(["issue", "view", String(issue), "-R", repoSlug, "--json", "comments"]);
2788
+ const raw = gh(["issue", "view", String(issue2), "-R", repoSlug, "--json", "comments"]);
2752
2789
  const parsed = JSON.parse(raw);
2753
2790
  const already = (parsed.comments ?? []).some((c) => (c.body ?? "").includes(marker));
2754
2791
  if (already) return { posted: false };
2755
- gh(["issue", "comment", String(issue), "-R", repoSlug, "--body-file", "-"], { input: `${body}
2792
+ gh(["issue", "comment", String(issue2), "-R", repoSlug, "--body-file", "-"], { input: `${body}
2756
2793
 
2757
2794
  ${marker}` });
2758
2795
  return { posted: true };
@@ -2794,9 +2831,9 @@ function dispatchWorkflow(workflowFile, capability, issueNumber, repoSlug, ref)
2794
2831
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
2795
2832
  }
2796
2833
  }
2797
- function startCapability(workflowFile, name, issue, repoSlug, ref) {
2834
+ function startCapability(workflowFile, name, issue2, repoSlug, ref) {
2798
2835
  const acceptsIssue = capabilityAcceptsIssue(name);
2799
- const forwardedIssue = acceptsIssue === false ? void 0 : issue;
2836
+ const forwardedIssue = acceptsIssue === false ? void 0 : issue2;
2800
2837
  return dispatchWorkflow(workflowFile, name, forwardedIssue, repoSlug, ref);
2801
2838
  }
2802
2839
  function capabilityAcceptsIssue(capability) {
@@ -2862,8 +2899,8 @@ function capabilityToolDefinitions(opts) {
2862
2899
  handler: async (args) => {
2863
2900
  const pr = Number(args.pr);
2864
2901
  const result = dispatchVerb(workflowFile, opts.repoSlug, verb, pr);
2865
- const text = result.ok ? `Dispatched \`${verb}\` on PR #${pr}. The repair runs in its own workflow_dispatch \u2014 wait for the next tick to see the new headSha.` : `Dispatch failed for \`${verb}\` on PR #${pr}: ${result.error}`;
2866
- return { content: [{ type: "text", text }] };
2902
+ const text2 = result.ok ? `Dispatched \`${verb}\` on PR #${pr}. The repair runs in its own workflow_dispatch \u2014 wait for the next tick to see the new headSha.` : `Dispatch failed for \`${verb}\` on PR #${pr}: ${result.error}`;
2903
+ return { content: [{ type: "text", text: text2 }] };
2867
2904
  }
2868
2905
  });
2869
2906
  const syncTool = makeDispatch(
@@ -2889,8 +2926,8 @@ function capabilityToolDefinitions(opts) {
2889
2926
  const pr = Number(args.pr);
2890
2927
  const body = String(args.body ?? "");
2891
2928
  const result = postRecommendation(opts.repoSlug, pr, opts.operatorMention, body, opts.capabilitySlug);
2892
- const text = result.ok ? result.posted ? `Recommendation posted on PR #${pr}.` : `Recommendation already exists on PR #${pr}; skipped.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2893
- return { content: [{ type: "text", text }] };
2929
+ const text2 = result.ok ? result.posted ? `Recommendation posted on PR #${pr}.` : `Recommendation already exists on PR #${pr}; skipped.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2930
+ return { content: [{ type: "text", text: text2 }] };
2894
2931
  }
2895
2932
  };
2896
2933
  const ledgerTool = {
@@ -2964,10 +3001,10 @@ function capabilityToolDefinitions(opts) {
2964
3001
  body: z3.string().min(1).describe("Comment body markdown.")
2965
3002
  },
2966
3003
  handler: async (args) => {
2967
- const issue = Number(args.issue);
3004
+ const issue2 = Number(args.issue);
2968
3005
  const key = String(args.key ?? "");
2969
3006
  const body = String(args.body ?? "");
2970
- const result = ensureComment(opts.repoSlug, issue, key, body);
3007
+ const result = ensureComment(opts.repoSlug, issue2, key, body);
2971
3008
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2972
3009
  }
2973
3010
  };
@@ -2982,19 +3019,13 @@ function capabilityToolDefinitions(opts) {
2982
3019
  handler: async (args) => {
2983
3020
  const name = String(args.name ?? "");
2984
3021
  const rawIssue = args.issue ?? args.issueNumber;
2985
- const issue = rawIssue == null ? void 0 : Number(rawIssue);
2986
- if (issue !== void 0 && (!Number.isFinite(issue) || issue <= 0)) {
3022
+ const issue2 = rawIssue == null ? void 0 : Number(rawIssue);
3023
+ if (issue2 !== void 0 && (!Number.isFinite(issue2) || issue2 <= 0)) {
2987
3024
  return { content: [{ type: "text", text: "Start failed: `issue` must be a positive number when provided." }] };
2988
3025
  }
2989
- const result = startCapability(
2990
- workflowFile,
2991
- name,
2992
- issue,
2993
- opts.repoSlug,
2994
- opts.defaultBranch
2995
- );
2996
- const text = JSON.stringify(result);
2997
- return { content: [{ type: "text", text }] };
3026
+ const result = startCapability(workflowFile, name, issue2, opts.repoSlug, opts.defaultBranch);
3027
+ const text2 = JSON.stringify(result);
3028
+ return { content: [{ type: "text", text: text2 }] };
2998
3029
  }
2999
3030
  };
3000
3031
  const cmsTools = dashboardCmsToolDefinitions({
@@ -3209,8 +3240,8 @@ function classifySubtype(subtype) {
3209
3240
  if (lower.includes("error")) return "model_error";
3210
3241
  return "generic_failed";
3211
3242
  }
3212
- function isClaudeLoginRequiredText(text) {
3213
- const normalized = text.toLowerCase();
3243
+ function isClaudeLoginRequiredText(text2) {
3244
+ const normalized = text2.toLowerCase();
3214
3245
  return normalized.includes("not logged in") && normalized.includes("/login");
3215
3246
  }
3216
3247
  function resolveTurnTimeoutMs(opts) {
@@ -3451,7 +3482,10 @@ async function runAgent(opts) {
3451
3482
  errorMessage2 = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
3452
3483
  if (typeof iterator.return === "function") {
3453
3484
  try {
3454
- await iterator.return(void 0);
3485
+ await Promise.race([
3486
+ iterator.return(void 0).catch(() => void 0),
3487
+ new Promise((resolve10) => setTimeout(resolve10, 1e4).unref())
3488
+ ]);
3455
3489
  } catch {
3456
3490
  }
3457
3491
  }
@@ -3546,9 +3580,9 @@ async function runAgent(opts) {
3546
3580
  outcome = "completed";
3547
3581
  outcomeKind = "ok";
3548
3582
  sawTerminalSuccess = true;
3549
- const text = (typeof m.result === "string" ? m.result : "").trim();
3550
- if (isClaudeLoginRequiredText(text)) sawLoginRequired = true;
3551
- if (text) resultTexts.push(text);
3583
+ const text2 = (typeof m.result === "string" ? m.result : "").trim();
3584
+ if (isClaudeLoginRequiredText(text2)) sawLoginRequired = true;
3585
+ if (text2) resultTexts.push(text2);
3552
3586
  } else {
3553
3587
  outcome = "failed";
3554
3588
  outcomeKind = classifySubtype(m.subtype);
@@ -3963,9 +3997,9 @@ var init_agencyBoundaryEval = __esm({
3963
3997
  });
3964
3998
 
3965
3999
  // src/capabilityReport.ts
3966
- function parseCapabilityReportsFromText(text) {
4000
+ function parseCapabilityReportsFromText(text2) {
3967
4001
  const reports = [];
3968
- for (const match of text.matchAll(REPORT_LINE)) {
4002
+ for (const match of text2.matchAll(REPORT_LINE)) {
3969
4003
  const raw = match[1]?.trim();
3970
4004
  if (!raw) continue;
3971
4005
  try {
@@ -4093,9 +4127,9 @@ var init_evidenceState = __esm({
4093
4127
  });
4094
4128
 
4095
4129
  // src/capabilityResult.ts
4096
- function parseCapabilityResultsFromText(text) {
4130
+ function parseCapabilityResultsFromText(text2) {
4097
4131
  const results = [];
4098
- for (const match of text.matchAll(RESULT_LINE)) {
4132
+ for (const match of text2.matchAll(RESULT_LINE)) {
4099
4133
  const raw = match[1]?.trim();
4100
4134
  if (!raw) continue;
4101
4135
  try {
@@ -5416,8 +5450,8 @@ function loadProjectConventions(projectDir) {
5416
5450
  return out;
5417
5451
  }
5418
5452
  function parseAgentResult(finalText) {
5419
- const text = (finalText || "").trim();
5420
- if (!text)
5453
+ const text2 = (finalText || "").trim();
5454
+ if (!text2)
5421
5455
  return {
5422
5456
  done: false,
5423
5457
  commitMessage: "",
@@ -5431,7 +5465,7 @@ function parseAgentResult(finalText) {
5431
5465
  const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
5432
5466
  const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "s");
5433
5467
  const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`);
5434
- const scanText = stripFencedCodeBlocks(text);
5468
+ const scanText = stripFencedCodeBlocks(text2);
5435
5469
  const failedMatch = scanText.match(FAILED_RE);
5436
5470
  if (failedMatch) {
5437
5471
  return {
@@ -5446,32 +5480,32 @@ function parseAgentResult(finalText) {
5446
5480
  };
5447
5481
  }
5448
5482
  const hasDoneMarker = DONE_RE.test(scanText);
5449
- const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
5450
- const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
5483
+ const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text2);
5484
+ const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text2);
5451
5485
  const markerMissing = !hasDoneMarker && !hasCommitMsg && !hasPrSummary;
5452
- const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
5486
+ const commitMatch = text2.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
5453
5487
  const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
5454
5488
  const feedbackActions = extractBlock(
5455
- text,
5489
+ text2,
5456
5490
  /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
5457
5491
  /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
5458
5492
  );
5459
5493
  let planDeviations = extractBlock(
5460
- text,
5494
+ text2,
5461
5495
  /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
5462
5496
  /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
5463
5497
  );
5464
5498
  if (!planDeviations) {
5465
- const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
5499
+ const inline = text2.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
5466
5500
  if (inline) planDeviations = inline[1].trim();
5467
5501
  }
5468
5502
  let priorArt = "";
5469
- const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
5503
+ const priorArtInline = text2.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
5470
5504
  if (priorArtInline) priorArt = priorArtInline[1].trim();
5471
- const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
5505
+ const summaryStart = text2.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
5472
5506
  let prSummary = "";
5473
5507
  if (summaryStart !== -1) {
5474
- const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
5508
+ const afterMarker = text2.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
5475
5509
  prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
5476
5510
  }
5477
5511
  return {
@@ -5491,10 +5525,10 @@ function stripMarkdownEmphasis(s) {
5491
5525
  function stripFencedCodeBlocks(s) {
5492
5526
  return s.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "");
5493
5527
  }
5494
- function extractBlock(text, startMarker, endMarker) {
5495
- const startIdx = text.search(startMarker);
5528
+ function extractBlock(text2, startMarker, endMarker) {
5529
+ const startIdx = text2.search(startMarker);
5496
5530
  if (startIdx === -1) return "";
5497
- const afterStart = text.slice(startIdx).replace(startMarker, "");
5531
+ const afterStart = text2.slice(startIdx).replace(startMarker, "");
5498
5532
  const endIdx = afterStart.search(endMarker);
5499
5533
  const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
5500
5534
  return body.replace(/\n\s*```\s*$/g, "").trim();
@@ -5670,9 +5704,9 @@ function collectPages(memoryAbs) {
5670
5704
  }
5671
5705
  function extractQueryTerms(ctx) {
5672
5706
  const terms = [];
5673
- const issue = ctx.data.issue;
5707
+ const issue2 = ctx.data.issue;
5674
5708
  const pr = ctx.data.pr;
5675
- if (issue?.title) terms.push(...tokenize(issue.title));
5709
+ if (issue2?.title) terms.push(...tokenize(issue2.title));
5676
5710
  if (pr?.title) terms.push(...tokenize(pr.title));
5677
5711
  return Array.from(new Set(terms)).slice(0, 20);
5678
5712
  }
@@ -6334,7 +6368,7 @@ function locateLitellmScript() {
6334
6368
  "python3",
6335
6369
  [
6336
6370
  "-c",
6337
- "import os,sys; p=os.path.join(os.path.dirname(sys.implementation),'litellm'); print(p if os.path.exists(p) else '')"
6371
+ "import os,sys,site,sysconfig; c=[os.path.join(os.path.dirname(sys.executable),'litellm'),os.path.join(sysconfig.get_path('scripts'),'litellm'),os.path.join(site.USER_BASE,'bin','litellm')]; m=[p for p in c if os.path.exists(p)]; print(m[0] if m else '')"
6338
6372
  ],
6339
6373
  { encoding: "utf-8", timeout: 1e4 }
6340
6374
  ).trim();
@@ -7686,10 +7720,10 @@ var init_state2 = __esm({
7686
7720
  "use strict";
7687
7721
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
7688
7722
  GoalStateError = class extends Error {
7689
- constructor(path51, message) {
7690
- super(`Invalid goal state at ${path51}:
7723
+ constructor(path52, message) {
7724
+ super(`Invalid goal state at ${path52}:
7691
7725
  ${message}`);
7692
- this.path = path51;
7726
+ this.path = path52;
7693
7727
  this.name = "GoalStateError";
7694
7728
  }
7695
7729
  path;
@@ -7702,9 +7736,9 @@ import * as fs25 from "fs";
7702
7736
  function stageGoalRunLogEvent(data, goalId, event, at = nowIso()) {
7703
7737
  const logs = goalRunLogs(data);
7704
7738
  const existing = logs[goalId];
7705
- const path51 = existing?.path ?? goalRunLogPath(goalId, data);
7739
+ const path52 = existing?.path ?? goalRunLogPath(goalId, data);
7706
7740
  logs[goalId] = {
7707
- path: path51,
7741
+ path: path52,
7708
7742
  events: [...existing?.events ?? [], buildGoalRunLogEvent(data, goalId, event, at)]
7709
7743
  };
7710
7744
  }
@@ -8723,6 +8757,290 @@ var init_typeDefinitions = __esm({
8723
8757
  }
8724
8758
  });
8725
8759
 
8760
+ // src/workflowValidation.ts
8761
+ function validateWorkflow(value, options = {}) {
8762
+ const issues = [];
8763
+ const workflow = asRecord2(value);
8764
+ const rawSteps = Array.isArray(value) ? value : Array.isArray(workflow?.steps) ? workflow.steps : [];
8765
+ const maxSteps = options.maxSteps ?? 100;
8766
+ const maxTransitions = options.maxTransitionsPerStep ?? 20;
8767
+ const maxLoopIterations = options.maxLoopIterations ?? 100;
8768
+ if (rawSteps.length === 0) {
8769
+ issue(issues, "steps_required", "steps", "workflow must contain at least one step");
8770
+ return issues;
8771
+ }
8772
+ if (rawSteps.length > maxSteps) {
8773
+ issue(issues, "too_many_steps", "steps", `workflow has ${rawSteps.length} steps; maximum is ${maxSteps}`);
8774
+ }
8775
+ const graphMode = workflow?.startAt !== void 0 || rawSteps.some((entry) => {
8776
+ const step = asRecord2(entry);
8777
+ return Boolean(step && (step.id !== void 0 || step.next !== void 0 || step.inputs !== void 0));
8778
+ });
8779
+ const steps = rawSteps.map(
8780
+ (entry) => typeof entry === "string" ? { capability: entry } : asRecord2(entry)
8781
+ );
8782
+ const ids = [];
8783
+ steps.forEach((step, index) => {
8784
+ const base = `steps[${index}]`;
8785
+ if (!step) {
8786
+ issue(issues, "invalid_step", base, "workflow step must be a capability name or an object");
8787
+ return;
8788
+ }
8789
+ for (const field of Object.keys(step)) {
8790
+ if (!SUPPORTED_STEP_FIELDS.has(field)) {
8791
+ issue(issues, "unsupported_step_field", `${base}.${field}`, `workflow step field ${field} is not supported`);
8792
+ }
8793
+ }
8794
+ const capability = text(step.capability ?? step.action);
8795
+ if (!capability || !SAFE_NAME.test(capability)) {
8796
+ issue(issues, "invalid_capability", `${base}.capability`, "workflow step must name a valid capability");
8797
+ } else if (options.knownCapabilities && !options.knownCapabilities.has(capability)) {
8798
+ issue(
8799
+ issues,
8800
+ "unknown_capability",
8801
+ `${base}.capability`,
8802
+ `workflow step references unknown capability ${capability}`
8803
+ );
8804
+ }
8805
+ if (graphMode) {
8806
+ const id = text(step.id);
8807
+ if (!id || !SAFE_NAME.test(id)) {
8808
+ issue(issues, "invalid_step_id", `${base}.id`, "graph workflow steps must each have a valid id");
8809
+ } else {
8810
+ ids.push(id);
8811
+ }
8812
+ }
8813
+ validateDataMatch(step.runWhen, `${base}.runWhen`, issues);
8814
+ const inputs = asRecord2(step.inputs);
8815
+ if (step.inputs !== void 0 && !inputs) {
8816
+ issue(issues, "invalid_inputs", `${base}.inputs`, "workflow step inputs must be an object");
8817
+ }
8818
+ if (inputs) {
8819
+ for (const [name, mapping] of Object.entries(inputs)) {
8820
+ const inputPath = `${base}.inputs.${name}`;
8821
+ if (!SAFE_NAME.test(name)) issue(issues, "invalid_input_name", inputPath, `invalid input name ${name}`);
8822
+ const from = text(asRecord2(mapping)?.from);
8823
+ if (!from || !SAFE_DATA_PATH.test(from)) {
8824
+ issue(
8825
+ issues,
8826
+ "invalid_data_path",
8827
+ `${inputPath}.from`,
8828
+ `workflow input ${name} must read from facts, evidence, artifacts, result, workflow, or lastOutcome`
8829
+ );
8830
+ }
8831
+ const declared = capability ? options.capabilityInputs?.get(capability) : void 0;
8832
+ if (declared && !declared.has(name)) {
8833
+ issue(
8834
+ issues,
8835
+ "unknown_capability_input",
8836
+ inputPath,
8837
+ `capability ${capability} does not declare input ${name}`
8838
+ );
8839
+ }
8840
+ }
8841
+ }
8842
+ });
8843
+ if (!graphMode) return issues;
8844
+ const seen = /* @__PURE__ */ new Set();
8845
+ ids.forEach((id, index) => {
8846
+ if (seen.has(id)) issue(issues, "duplicate_step_id", `steps[${index}].id`, `workflow step id ${id} is duplicated`);
8847
+ seen.add(id);
8848
+ });
8849
+ const startAt = text(workflow?.startAt) ?? text(steps[0]?.id);
8850
+ if (!startAt || !seen.has(startAt)) {
8851
+ issue(issues, "missing_start_step", "startAt", `workflow startAt references missing step ${startAt ?? "<none>"}`);
8852
+ }
8853
+ const adjacency = /* @__PURE__ */ new Map();
8854
+ steps.forEach((step, index) => {
8855
+ if (!step) return;
8856
+ const id = text(step.id);
8857
+ if (!id) return;
8858
+ const transitions = transitionList(step.next);
8859
+ adjacency.set(id, []);
8860
+ if (transitions.length > maxTransitions) {
8861
+ issue(
8862
+ issues,
8863
+ "too_many_transitions",
8864
+ `steps[${index}].next`,
8865
+ `workflow step ${id} has ${transitions.length} connections; maximum is ${maxTransitions}`
8866
+ );
8867
+ }
8868
+ const defaults = transitions.filter((transition) => asRecord2(transition)?.default === true);
8869
+ const conditionals = transitions.filter((transition) => asRecord2(transition)?.when !== void 0);
8870
+ const unconditional = transitions.filter((transition) => {
8871
+ const raw = asRecord2(transition);
8872
+ return typeof transition === "string" || Boolean(raw && raw.when === void 0 && raw.default !== true && raw.maxIterations === void 0);
8873
+ });
8874
+ if (defaults.length > 1) {
8875
+ issue(
8876
+ issues,
8877
+ "multiple_default_transitions",
8878
+ `steps[${index}].next`,
8879
+ `workflow step ${id} has more than one default connection`
8880
+ );
8881
+ }
8882
+ if (conditionals.length > 0 && defaults.length !== 1) {
8883
+ issue(
8884
+ issues,
8885
+ "missing_default_transition",
8886
+ `steps[${index}].next`,
8887
+ `workflow step ${id} has conditions and needs one default connection`
8888
+ );
8889
+ }
8890
+ if (unconditional.length > 1 || unconditional.length > 0 && transitions.length > 1) {
8891
+ issue(
8892
+ issues,
8893
+ "ambiguous_transition",
8894
+ `steps[${index}].next`,
8895
+ `workflow step ${id} mixes an unconditional connection with other connections`
8896
+ );
8897
+ }
8898
+ transitions.forEach((transition, transitionIndex) => {
8899
+ const raw = typeof transition === "string" ? { to: transition } : asRecord2(transition);
8900
+ const base = `steps[${index}].next[${transitionIndex}]`;
8901
+ if (!raw) {
8902
+ issue(issues, "invalid_transition", base, "workflow connection must be a step id or an object");
8903
+ return;
8904
+ }
8905
+ for (const field of Object.keys(raw)) {
8906
+ if (!SUPPORTED_TRANSITION_FIELDS.has(field)) {
8907
+ issue(
8908
+ issues,
8909
+ "unsupported_transition_field",
8910
+ `${base}.${field}`,
8911
+ `workflow connection field ${field} is not supported`
8912
+ );
8913
+ }
8914
+ }
8915
+ const target = text(raw.to);
8916
+ if (!target || !SAFE_NAME.test(target)) {
8917
+ issue(issues, "invalid_transition_target", `${base}.to`, "workflow connection must name a valid target step");
8918
+ return;
8919
+ }
8920
+ if (!seen.has(target)) {
8921
+ issue(
8922
+ issues,
8923
+ "missing_transition_target",
8924
+ `${base}.to`,
8925
+ `workflow step ${id} connects to missing step ${target}`
8926
+ );
8927
+ } else {
8928
+ adjacency.get(id)?.push(target);
8929
+ }
8930
+ if (raw.default === true && raw.when !== void 0) {
8931
+ issue(issues, "conflicting_transition", base, "workflow connection cannot be both conditional and default");
8932
+ }
8933
+ if (raw.when !== void 0) validateDataMatch(raw.when, `${base}.when`, issues);
8934
+ const targetIndex = ids.indexOf(target ?? "");
8935
+ const iterations = raw.maxIterations;
8936
+ if (targetIndex >= 0 && targetIndex <= index) {
8937
+ if (!Number.isInteger(iterations) || Number(iterations) < 1) {
8938
+ issue(
8939
+ issues,
8940
+ "unbounded_loop",
8941
+ `${base}.maxIterations`,
8942
+ `workflow loop ${id}->${target} must set maxIterations`
8943
+ );
8944
+ } else if (Number(iterations) > maxLoopIterations) {
8945
+ issue(
8946
+ issues,
8947
+ "loop_limit_too_high",
8948
+ `${base}.maxIterations`,
8949
+ `workflow loop ${id}->${target} exceeds maximum ${maxLoopIterations}`
8950
+ );
8951
+ }
8952
+ } else if (iterations !== void 0 && (!Number.isInteger(iterations) || Number(iterations) < 1)) {
8953
+ issue(issues, "invalid_loop_limit", `${base}.maxIterations`, "maxIterations must be a positive integer");
8954
+ }
8955
+ });
8956
+ });
8957
+ if (startAt && seen.has(startAt)) {
8958
+ const reachable = /* @__PURE__ */ new Set();
8959
+ const pending = [startAt];
8960
+ while (pending.length > 0) {
8961
+ const id = pending.pop();
8962
+ if (reachable.has(id)) continue;
8963
+ reachable.add(id);
8964
+ pending.push(...adjacency.get(id) ?? []);
8965
+ }
8966
+ ids.forEach((id, index) => {
8967
+ if (!reachable.has(id)) issue(issues, "unreachable_step", `steps[${index}]`, `workflow step ${id} is unreachable`);
8968
+ });
8969
+ if (![...reachable].some((id) => (adjacency.get(id) ?? []).length === 0)) {
8970
+ issue(issues, "missing_terminal_step", "steps", "workflow has no reachable final step");
8971
+ }
8972
+ }
8973
+ return issues;
8974
+ }
8975
+ function formatWorkflowValidationIssues(issues) {
8976
+ return issues.map((entry) => `${entry.path}: ${entry.message}`);
8977
+ }
8978
+ function validateDataMatch(value, path52, issues) {
8979
+ if (value === void 0) return;
8980
+ const match = asRecord2(value);
8981
+ if (!match || Object.keys(match).length === 0) {
8982
+ issue(issues, "invalid_condition", path52, "workflow condition must contain at least one match");
8983
+ return;
8984
+ }
8985
+ for (const [field, expected] of Object.entries(match)) {
8986
+ if (!SAFE_DATA_PATH.test(field)) {
8987
+ issue(
8988
+ issues,
8989
+ "invalid_data_path",
8990
+ `${path52}.${field}`,
8991
+ `workflow condition must read from facts, evidence, artifacts, result, workflow, or lastOutcome`
8992
+ );
8993
+ }
8994
+ if (!isComparable(expected)) {
8995
+ issue(issues, "invalid_condition_value", `${path52}.${field}`, "workflow condition value must be a JSON scalar");
8996
+ }
8997
+ }
8998
+ }
8999
+ function transitionList(value) {
9000
+ if (value === void 0) return [];
9001
+ return Array.isArray(value) ? value : [value];
9002
+ }
9003
+ function asRecord2(value) {
9004
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9005
+ }
9006
+ function text(value) {
9007
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
9008
+ }
9009
+ function isComparable(value) {
9010
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) return true;
9011
+ return Array.isArray(value) && value.length > 0 && value.every((item) => isComparable(item) && !Array.isArray(item));
9012
+ }
9013
+ function issue(issues, code, path52, message) {
9014
+ issues.push({ code, path: path52, message });
9015
+ }
9016
+ var SAFE_NAME, SAFE_DATA_PATH, SUPPORTED_STEP_FIELDS, SUPPORTED_TRANSITION_FIELDS;
9017
+ var init_workflowValidation = __esm({
9018
+ "src/workflowValidation.ts"() {
9019
+ "use strict";
9020
+ SAFE_NAME = /^[a-z][a-z0-9-]*$/;
9021
+ SAFE_DATA_PATH = /^(facts|evidence|artifacts|result|workflow|lastOutcome)(?:\.[A-Za-z_][A-Za-z0-9_-]*)+$/;
9022
+ SUPPORTED_STEP_FIELDS = /* @__PURE__ */ new Set([
9023
+ "id",
9024
+ "capability",
9025
+ "action",
9026
+ "implementation",
9027
+ "evidence",
9028
+ "target",
9029
+ "targetFact",
9030
+ "reason",
9031
+ "agent",
9032
+ "cliArgs",
9033
+ "inputs",
9034
+ "next",
9035
+ "runWhen",
9036
+ "continueOn",
9037
+ "saveReport",
9038
+ "report"
9039
+ ]);
9040
+ SUPPORTED_TRANSITION_FIELDS = /* @__PURE__ */ new Set(["to", "when", "default", "maxIterations"]);
9041
+ }
9042
+ });
9043
+
8726
9044
  // src/workflowDefinitions.ts
8727
9045
  import * as fs28 from "fs";
8728
9046
  import * as path26 from "path";
@@ -8739,7 +9057,18 @@ function normalizeWorkflowDefinition(value) {
8739
9057
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
8740
9058
  const raw = value;
8741
9059
  const name = typeof raw.name === "string" ? raw.name.trim() : "";
8742
- const workflow = parseCapabilityWorkflow({ steps: raw.steps });
9060
+ const hasGraphConnections = Array.isArray(raw.steps) && raw.steps.some(
9061
+ (step) => step && typeof step === "object" && !Array.isArray(step) && (step.next !== void 0 || step.inputs !== void 0)
9062
+ );
9063
+ if (hasGraphConnections) {
9064
+ if (validateWorkflow({ steps: raw.steps, ...raw.startAt !== void 0 ? { startAt: raw.startAt } : {} }).length > 0) {
9065
+ return null;
9066
+ }
9067
+ }
9068
+ const workflow = parseCapabilityWorkflow({
9069
+ steps: raw.steps,
9070
+ startAt: raw.startAt
9071
+ });
8743
9072
  const steps = workflow?.steps;
8744
9073
  const capabilities = steps ? steps.map((step) => step.capability) : normalizeWorkflowCapabilities(raw.capabilities);
8745
9074
  if (!name || capabilities.length === 0) return null;
@@ -8749,6 +9078,7 @@ function normalizeWorkflowDefinition(value) {
8749
9078
  capabilities,
8750
9079
  ...raw.runWithoutApproval === true ? { runWithoutApproval: true } : {},
8751
9080
  ...steps ? { steps } : {},
9081
+ ...workflow?.startAt ? { startAt: workflow.startAt } : {},
8752
9082
  ...typeof raw.createdAt === "string" ? { createdAt: raw.createdAt } : {},
8753
9083
  ...typeof raw.updatedAt === "string" ? { updatedAt: raw.updatedAt } : {}
8754
9084
  };
@@ -8790,7 +9120,8 @@ function normalizeWorkflowCapabilities(value) {
8790
9120
  }
8791
9121
  function workflowDefinitionToConfig(workflow) {
8792
9122
  return {
8793
- steps: workflow.steps ?? workflow.capabilities.map((capability) => ({ capability }))
9123
+ steps: workflow.steps ?? workflow.capabilities.map((capability) => ({ capability })),
9124
+ ...workflow.startAt ? { startAt: workflow.startAt } : {}
8794
9125
  };
8795
9126
  }
8796
9127
  function readCompanyStoreWorkflowDefinition(id) {
@@ -8814,6 +9145,7 @@ var init_workflowDefinitions = __esm({
8814
9145
  init_capabilityFolders();
8815
9146
  init_companyStore();
8816
9147
  init_stateRepo();
9148
+ init_workflowValidation();
8817
9149
  WORKFLOW_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,79}$/;
8818
9150
  CAPABILITY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/;
8819
9151
  }
@@ -9636,7 +9968,7 @@ function readSimpleGoalTaskSummary(goalId, cwd) {
9636
9968
  );
9637
9969
  const issues = JSON.parse(raw);
9638
9970
  const total = issues.length;
9639
- const open = issues.filter((issue) => String(issue.state ?? "").toLowerCase() === "open").length;
9971
+ const open = issues.filter((issue2) => String(issue2.state ?? "").toLowerCase() === "open").length;
9640
9972
  return { total, open };
9641
9973
  }
9642
9974
  function previousDispatchWasTargetInstance(managed, previousScheduleState) {
@@ -9685,7 +10017,7 @@ function findExistingGoalIssue(goalId, cwd) {
9685
10017
  const marker = goalIssueMarker(goalId);
9686
10018
  const raw = gh(["issue", "list", "--state", "all", "--limit", "100", "--json", "number,body"], { cwd });
9687
10019
  const issues = JSON.parse(raw);
9688
- const match = issues.find((issue) => typeof issue.number === "number" && issue.body?.includes(marker));
10020
+ const match = issues.find((issue2) => typeof issue2.number === "number" && issue2.body?.includes(marker));
9689
10021
  return match?.number ?? null;
9690
10022
  }
9691
10023
  function createGoalIssue(goal, goalId, cwd) {
@@ -10534,13 +10866,13 @@ function ensureNeedsFixIssue(ctx, goalId, state, evidence, evidenceKey) {
10534
10866
  const evidenceState = parseGoalEvidenceState(state.extra.evidenceState);
10535
10867
  const progress = evidenceState[evidenceKey];
10536
10868
  if (progress?.issue) return state;
10537
- const issue = findExistingNeedsFixIssue(goalId, evidenceKey, ctx.cwd) ?? createNeedsFixIssue(goalId, evidenceKey, evidence, ctx.cwd);
10869
+ const issue2 = findExistingNeedsFixIssue(goalId, evidenceKey, ctx.cwd) ?? createNeedsFixIssue(goalId, evidenceKey, evidence, ctx.cwd);
10538
10870
  const nextEvidenceState = mergeGoalEvidenceProgress(evidenceState, evidenceKey, {
10539
10871
  resultClass: "needsFix",
10540
10872
  attempts: progress?.attempts ?? 1,
10541
10873
  reason: evidence.summary,
10542
- nextAction: `fix issue #${issue}`,
10543
- issue,
10874
+ nextAction: `fix issue #${issue2}`,
10875
+ issue: issue2,
10544
10876
  updatedAt: nowIso()
10545
10877
  });
10546
10878
  return {
@@ -10549,7 +10881,7 @@ function ensureNeedsFixIssue(ctx, goalId, state, evidence, evidenceKey) {
10549
10881
  ...state.extra,
10550
10882
  evidenceState: nextEvidenceState,
10551
10883
  reason: evidence.summary,
10552
- nextAction: `fix issue #${issue}`
10884
+ nextAction: `fix issue #${issue2}`
10553
10885
  }
10554
10886
  };
10555
10887
  }
@@ -10567,7 +10899,7 @@ function findExistingNeedsFixIssue(goalId, evidence, cwd) {
10567
10899
  const marker = needsFixIssueMarker(goalId, evidence);
10568
10900
  const raw = gh(["issue", "list", "--state", "all", "--limit", "100", "--json", "number,body"], { cwd });
10569
10901
  const issues = JSON.parse(raw);
10570
- const match = issues.find((issue) => typeof issue.number === "number" && issue.body?.includes(marker));
10902
+ const match = issues.find((issue2) => typeof issue2.number === "number" && issue2.body?.includes(marker));
10571
10903
  return match?.number ?? null;
10572
10904
  }
10573
10905
  function createNeedsFixIssue(goalId, evidence, result, cwd) {
@@ -11005,8 +11337,8 @@ var init_classifyByLabel = __esm({
11005
11337
  "use strict";
11006
11338
  VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
11007
11339
  classifyByLabel = async (ctx) => {
11008
- const issue = ctx.data.issue;
11009
- const labels = issue?.labels;
11340
+ const issue2 = ctx.data.issue;
11341
+ const labels = issue2?.labels;
11010
11342
  if (!labels || labels.length === 0) return;
11011
11343
  const cfgMap = ctx.config.classify?.labelMap;
11012
11344
  const map = cfgMap ?? defaultLabelMap();
@@ -11054,6 +11386,13 @@ var init_commitAndPush = __esm({
11054
11386
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
11055
11387
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
11056
11388
  if (replay.salvagedFromMissingMarker) ctx.data.salvagedFromMissingMarker = true;
11389
+ if (typeof replay.commitCrash === "string") {
11390
+ ctx.data.commitCrash = replay.commitCrash;
11391
+ if (typeof replay.exitCode === "number" && (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0)) {
11392
+ ctx.output.exitCode = replay.exitCode;
11393
+ }
11394
+ if (!ctx.output.reason && replay.reason) ctx.output.reason = replay.reason;
11395
+ }
11057
11396
  ctx.data.commitIdempotencyReplay = true;
11058
11397
  process.stderr.write(`[kody commitAndPush] idempotency replay (sentinel ${sentinel})
11059
11398
  `);
@@ -11111,6 +11450,9 @@ var init_commitAndPush = __esm({
11111
11450
  changedFiles: ctx.data.changedFiles,
11112
11451
  hasCommitsAhead: ctx.data.hasCommitsAhead,
11113
11452
  salvagedFromMissingMarker: ctx.data.salvagedFromMissingMarker === true,
11453
+ commitCrash: typeof ctx.data.commitCrash === "string" ? ctx.data.commitCrash : void 0,
11454
+ exitCode: ctx.output.exitCode,
11455
+ reason: ctx.output.reason,
11114
11456
  writtenAt: (/* @__PURE__ */ new Date()).toISOString()
11115
11457
  },
11116
11458
  null,
@@ -11309,7 +11651,10 @@ var init_composePrompt = __esm({
11309
11651
  "issue.body",
11310
11652
  "issue.commentsFormatted",
11311
11653
  "pr.body",
11312
- "pr.commentsFormatted"
11654
+ "pr.commentsFormatted",
11655
+ // Prior-art bundles PR diffs and review/issue comments — authorable by any
11656
+ // external GitHub user, so exactly as attacker-controllable as issue bodies.
11657
+ "priorArt"
11313
11658
  ]);
11314
11659
  FENCE_END = "----- END UNTRUSTED INPUT -----";
11315
11660
  composePrompt = async (ctx, profile) => {
@@ -11519,19 +11864,19 @@ function buildGoalName(scope, verdict) {
11519
11864
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
11520
11865
  return `QA: ${focus} \u2014 ${verdictTag} \u2014 ${todayIso()}`.slice(0, 240);
11521
11866
  }
11522
- function splitReport(text) {
11523
- const open = text.indexOf(REPORT_JSON_OPEN);
11867
+ function splitReport(text2) {
11868
+ const open = text2.indexOf(REPORT_JSON_OPEN);
11524
11869
  if (open < 0) {
11525
- const fallback = parseFallbackFindingsJson(text);
11870
+ const fallback = parseFallbackFindingsJson(text2);
11526
11871
  if (fallback) return fallback;
11527
- return { markdown: text.trim(), data: null, jsonError: "no JSON block marker" };
11872
+ return { markdown: text2.trim(), data: null, jsonError: "no JSON block marker" };
11528
11873
  }
11529
- const closeRel = text.slice(open + REPORT_JSON_OPEN.length).indexOf(REPORT_JSON_CLOSE);
11874
+ const closeRel = text2.slice(open + REPORT_JSON_OPEN.length).indexOf(REPORT_JSON_CLOSE);
11530
11875
  if (closeRel < 0) {
11531
- return { markdown: text.slice(0, open).trim(), data: null, jsonError: "JSON block not terminated" };
11876
+ return { markdown: text2.slice(0, open).trim(), data: null, jsonError: "JSON block not terminated" };
11532
11877
  }
11533
11878
  const closeAbs = open + REPORT_JSON_OPEN.length + closeRel;
11534
- const rawJson = text.slice(open + REPORT_JSON_OPEN.length, closeAbs).trim();
11879
+ const rawJson = text2.slice(open + REPORT_JSON_OPEN.length, closeAbs).trim();
11535
11880
  const fenced = rawJson.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
11536
11881
  const cleanJson = fenced ? fenced[1].trim() : rawJson;
11537
11882
  let parsed = null;
@@ -11546,11 +11891,11 @@ function splitReport(text) {
11546
11891
  } catch (err) {
11547
11892
  parseError = err instanceof Error ? err.message : String(err);
11548
11893
  }
11549
- const markdown = text.slice(0, open).trim();
11894
+ const markdown = text2.slice(0, open).trim();
11550
11895
  return { markdown, data: parsed, jsonError: parseError };
11551
11896
  }
11552
- function parseFallbackFindingsJson(text) {
11553
- const fences = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi)];
11897
+ function parseFallbackFindingsJson(text2) {
11898
+ const fences = [...text2.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi)];
11554
11899
  for (let i = fences.length - 1; i >= 0; i--) {
11555
11900
  const match = fences[i];
11556
11901
  const raw = match[1]?.trim();
@@ -11559,18 +11904,18 @@ function parseFallbackFindingsJson(text) {
11559
11904
  const parsed = JSON.parse(raw);
11560
11905
  if (!parsed || !Array.isArray(parsed.findings)) {
11561
11906
  return {
11562
- markdown: removeFence(text, match).trim(),
11907
+ markdown: removeFence(text2, match).trim(),
11563
11908
  data: null,
11564
11909
  jsonError: "fallback JSON missing 'findings' array"
11565
11910
  };
11566
11911
  }
11567
11912
  return {
11568
- markdown: removeFence(text, match).trim(),
11913
+ markdown: removeFence(text2, match).trim(),
11569
11914
  data: { findings: parsed.findings.map((f, idx) => normalizeFallbackFinding(f, idx)) }
11570
11915
  };
11571
11916
  } catch (err) {
11572
11917
  return {
11573
- markdown: removeFence(text, match).trim(),
11918
+ markdown: removeFence(text2, match).trim(),
11574
11919
  data: null,
11575
11920
  jsonError: err instanceof Error ? err.message : String(err)
11576
11921
  };
@@ -11578,10 +11923,10 @@ function parseFallbackFindingsJson(text) {
11578
11923
  }
11579
11924
  return null;
11580
11925
  }
11581
- function removeFence(text, match) {
11926
+ function removeFence(text2, match) {
11582
11927
  const start = match.index ?? -1;
11583
- if (start < 0) return text;
11584
- return `${text.slice(0, start)}${text.slice(start + match[0].length)}`;
11928
+ if (start < 0) return text2;
11929
+ return `${text2.slice(0, start)}${text2.slice(start + match[0].length)}`;
11585
11930
  }
11586
11931
  function normalizeFallbackFinding(raw, idx) {
11587
11932
  const finding = raw && typeof raw === "object" ? raw : {};
@@ -11638,9 +11983,9 @@ function loadManifest(cwd) {
11638
11983
  return { number: null, manifest: { version: 1, goals: [] } };
11639
11984
  }
11640
11985
  if (arr.length === 0) return { number: null, manifest: { version: 1, goals: [] } };
11641
- const issue = arr[0];
11642
- const manifest = parseManifestBody(issue.body);
11643
- return { number: issue.number, manifest };
11986
+ const issue2 = arr[0];
11987
+ const manifest = parseManifestBody(issue2.body);
11988
+ return { number: issue2.number, manifest };
11644
11989
  }
11645
11990
  function parseManifestBody(body) {
11646
11991
  if (!body) return { version: 1, goals: [] };
@@ -11866,8 +12211,8 @@ ${markdown}`, ctx.cwd);
11866
12211
  const failed = [];
11867
12212
  for (const f of findings) {
11868
12213
  try {
11869
- const issue = createTaskIssue(f, goalId, manifestIssueNumber, ctx.cwd);
11870
- opened.push({ ...issue, severity: f.severity });
12214
+ const issue2 = createTaskIssue(f, goalId, manifestIssueNumber, ctx.cwd);
12215
+ opened.push({ ...issue2, severity: f.severity });
11871
12216
  } catch (err) {
11872
12217
  const reason = err instanceof Error ? err.message : String(err);
11873
12218
  failed.push({ title: f.title, reason });
@@ -12009,8 +12354,8 @@ var init_deriveQaScopeFromIssue = __esm({
12009
12354
  if (!Number.isFinite(issueNumber) || issueNumber <= 0) return;
12010
12355
  let title = "";
12011
12356
  try {
12012
- const issue = getIssue(issueNumber, ctx.cwd);
12013
- title = (issue.title ?? "").trim();
12357
+ const issue2 = getIssue(issueNumber, ctx.cwd);
12358
+ title = (issue2.title ?? "").trim();
12014
12359
  } catch (err) {
12015
12360
  process.stderr.write(
12016
12361
  `[kody] deriveQaScopeFromIssue: could not read #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
@@ -12615,28 +12960,28 @@ var init_dispatchCapabilityTicks = __esm({
12615
12960
  process.stdout.write(`[jobs] ticking ${issues.length} issue(s) via ${targetImplementation}
12616
12961
  `);
12617
12962
  const results = [];
12618
- for (const issue of issues) {
12619
- process.stdout.write(`[jobs] \u2192 tick #${issue.number}: ${issue.title}
12963
+ for (const issue2 of issues) {
12964
+ process.stdout.write(`[jobs] \u2192 tick #${issue2.number}: ${issue2.title}
12620
12965
  `);
12621
12966
  try {
12622
12967
  const out = await runJob(
12623
12968
  mintScheduledJob({
12624
12969
  capability: targetImplementation,
12625
12970
  implementation: targetImplementation,
12626
- cliArgs: { [issueArg]: issue.number }
12971
+ cliArgs: { [issueArg]: issue2.number }
12627
12972
  }),
12628
12973
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
12629
12974
  );
12630
- results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
12975
+ results.push({ issue: issue2.number, exitCode: out.exitCode, reason: out.reason });
12631
12976
  if (out.exitCode !== 0) {
12632
- process.stderr.write(`[jobs] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
12977
+ process.stderr.write(`[jobs] tick #${issue2.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
12633
12978
  `);
12634
12979
  }
12635
12980
  } catch (err) {
12636
12981
  const msg = err instanceof Error ? err.message : String(err);
12637
- process.stderr.write(`[jobs] tick #${issue.number} crashed: ${msg}
12982
+ process.stderr.write(`[jobs] tick #${issue2.number} crashed: ${msg}
12638
12983
  `);
12639
- results.push({ issue: issue.number, exitCode: 99, reason: msg });
12984
+ results.push({ issue: issue2.number, exitCode: 99, reason: msg });
12640
12985
  }
12641
12986
  }
12642
12987
  ctx.data.jobTickResults = results;
@@ -12832,23 +13177,34 @@ function firstLine(s) {
12832
13177
  const head = nl === -1 ? trimmed : trimmed.slice(0, nl);
12833
13178
  return head.length > 200 ? `${head.slice(0, 197)}\u2026` : head;
12834
13179
  }
12835
- function findExistingPr(branch, cwd) {
13180
+ function lookupExistingPr(branch, cwd) {
12836
13181
  try {
12837
13182
  const output = gh(
12838
- ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
13183
+ ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body,title,isDraft", "--limit", "1"],
12839
13184
  { cwd, preferRepoToken: true }
12840
13185
  );
12841
13186
  const arr = JSON.parse(output);
12842
13187
  const first = Array.isArray(arr) ? arr[0] : null;
12843
13188
  if (first && typeof first.number === "number" && typeof first.url === "string") {
12844
- const body = typeof first.body === "string" ? first.body : "";
12845
- return { number: first.number, url: first.url, body };
13189
+ return {
13190
+ pr: {
13191
+ number: first.number,
13192
+ url: first.url,
13193
+ body: typeof first.body === "string" ? first.body : "",
13194
+ title: typeof first.title === "string" ? first.title : "",
13195
+ isDraft: first.isDraft === true
13196
+ },
13197
+ error: null
13198
+ };
12846
13199
  }
12847
- return null;
12848
- } catch {
12849
- return null;
13200
+ return { pr: null, error: null };
13201
+ } catch (err) {
13202
+ return { pr: null, error: err instanceof Error ? err.message : String(err) };
12850
13203
  }
12851
13204
  }
13205
+ function findExistingPr(branch, cwd) {
13206
+ return lookupExistingPr(branch, cwd).pr;
13207
+ }
12852
13208
  function recoverSourceIssueNumber(existingBody, branch, prNumber) {
12853
13209
  const bodyMatch = existingBody.match(/\bCloses #(\d+)\b/i);
12854
13210
  if (bodyMatch) {
@@ -12875,16 +13231,42 @@ function git2(args, cwd) {
12875
13231
  stdio: ["pipe", "pipe", "pipe"]
12876
13232
  }).trim();
12877
13233
  }
12878
- function updateExistingPr(existing, body, draft, cwd) {
13234
+ function updateExistingPr(existing, body, draft, cwd, preserveBody) {
12879
13235
  const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
12880
13236
  const [owner, repo] = stripped.split("/");
12881
- try {
12882
- gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
12883
- cwd,
12884
- preferRepoToken: true
12885
- });
12886
- } catch (err) {
12887
- throw new Error(`gh api PATCH #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`);
13237
+ if (!preserveBody) {
13238
+ try {
13239
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
13240
+ cwd,
13241
+ preferRepoToken: true
13242
+ });
13243
+ } catch (err) {
13244
+ throw new Error(`gh api PATCH #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`);
13245
+ }
13246
+ }
13247
+ if (existing.isDraft === true && !draft) {
13248
+ try {
13249
+ gh(["pr", "ready", String(existing.number)], { cwd, preferRepoToken: true });
13250
+ const promotedTitle = existing.title?.replace(/^\[WIP\]\s*/, "");
13251
+ if (promotedTitle && promotedTitle !== existing.title) {
13252
+ gh(
13253
+ [
13254
+ "api",
13255
+ "--method",
13256
+ "PATCH",
13257
+ `repos/${owner}/${repo}/pulls/${existing.number}`,
13258
+ "-f",
13259
+ `title=${promotedTitle}`
13260
+ ],
13261
+ { cwd, preferRepoToken: true }
13262
+ );
13263
+ }
13264
+ } catch (err) {
13265
+ process.stderr.write(
13266
+ `[kody ensurePr] draft\u2192ready promotion of #${existing.number} failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
13267
+ `
13268
+ );
13269
+ }
12888
13270
  }
12889
13271
  return { url: existing.url, number: existing.number, draft, action: "updated" };
12890
13272
  }
@@ -12897,8 +13279,13 @@ function createPr(branch, base, title, body, draft, cwd) {
12897
13279
  return { url, number, draft, action: "created" };
12898
13280
  }
12899
13281
  function recoverFromExistingPr(branch, base, title, body, draft, cwd) {
12900
- const raced = findExistingPr(branch, cwd);
13282
+ const { pr: raced, error: lookupError } = lookupExistingPr(branch, cwd);
12901
13283
  if (raced) return updateExistingPr(raced, body, draft, cwd);
13284
+ if (lookupError) {
13285
+ throw new Error(
13286
+ `refusing phantom-PR recovery for '${branch}': PR lookup failed (${lookupError}) \u2014 a live PR may own this branch`
13287
+ );
13288
+ }
12902
13289
  try {
12903
13290
  git2(["push", "origin", "--delete", branch], cwd);
12904
13291
  } catch {
@@ -12916,7 +13303,7 @@ function ensurePr(opts) {
12916
13303
  const title = buildPrTitle(effectiveOpts.issueNumber, effectiveOpts.issueTitle, effectiveOpts.draft);
12917
13304
  const body = buildPrBody(effectiveOpts);
12918
13305
  if (existing) {
12919
- return updateExistingPr(existing, body, opts.draft, opts.cwd);
13306
+ return updateExistingPr(existing, body, opts.draft, opts.cwd, opts.preserveBodyOnUpdate === true);
12920
13307
  }
12921
13308
  const base = opts.baseBranch && opts.baseBranch.length > 0 ? opts.baseBranch : opts.defaultBranch;
12922
13309
  try {
@@ -13016,10 +13403,10 @@ var init_ensurePr = __esm({
13016
13403
  const failureReason = computeFailureReason(ctx);
13017
13404
  const isFailure = failureReason.length > 0;
13018
13405
  const changedFiles = ctx.data.changedFiles ?? [];
13019
- const issue = ctx.data.issue;
13406
+ const issue2 = ctx.data.issue;
13020
13407
  const pr = ctx.data.pr;
13021
13408
  const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
13022
- const title = issue?.title ?? pr?.title ?? `kody changes`;
13409
+ const title = issue2?.title ?? pr?.title ?? `kody changes`;
13023
13410
  const baseBranch = ctx.data.baseBranch;
13024
13411
  try {
13025
13412
  const result = ensurePr({
@@ -13032,6 +13419,9 @@ var init_ensurePr = __esm({
13032
13419
  changedFiles,
13033
13420
  agentSummary: ctx.data.prSummary,
13034
13421
  baseBranch,
13422
+ // No fresh commit this run → don't rebuild the body of an existing PR;
13423
+ // it would replace the original agent summary with the empty fallback.
13424
+ preserveBodyOnUpdate: !commitResult?.committed,
13035
13425
  cwd: ctx.cwd
13036
13426
  });
13037
13427
  if (!result.url || result.url.trim().length === 0) {
@@ -13067,12 +13457,12 @@ var init_failOnceTaskJob = __esm({
13067
13457
  init_jobIdentity();
13068
13458
  failOnceTaskJob = async (ctx, profile) => {
13069
13459
  ctx.skipAgent = true;
13070
- const issue = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
13460
+ const issue2 = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
13071
13461
  const fallbackJob = {
13072
13462
  capability: profile.action ?? profile.name,
13073
13463
  implementation: profile.name,
13074
13464
  flavor: "instant",
13075
- ...typeof issue === "number" ? { target: issue, cliArgs: { issue } } : { cliArgs: {} }
13465
+ ...typeof issue2 === "number" ? { target: issue2, cliArgs: { issue: issue2 } } : { cliArgs: {} }
13076
13466
  };
13077
13467
  const jobKey = typeof ctx.data.jobKey === "string" ? ctx.data.jobKey : stableJobKey(fallbackJob);
13078
13468
  const state = ctx.data.taskState;
@@ -14058,13 +14448,13 @@ function companyIntentPath(id) {
14058
14448
  assertIntentId(id);
14059
14449
  return `intents/${id}/intent.json`;
14060
14450
  }
14061
- function normalizeCompanyIntent(path51, raw) {
14451
+ function normalizeCompanyIntent(path52, raw) {
14062
14452
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14063
- throw new Error(`${path51}: intent must be JSON object`);
14453
+ throw new Error(`${path52}: intent must be JSON object`);
14064
14454
  }
14065
14455
  const input = raw;
14066
14456
  const id = stringField5(input.id);
14067
- if (!id || !isCompanyIntentId(id)) throw new Error(`${path51}: invalid intent id`);
14457
+ if (!id || !isCompanyIntentId(id)) throw new Error(`${path52}: invalid intent id`);
14068
14458
  const createdAt = stringField5(input.createdAt) || nowIso();
14069
14459
  const updatedAt = stringField5(input.updatedAt) || createdAt;
14070
14460
  const description = stringField5(input.description);
@@ -14104,8 +14494,8 @@ function listCompanyIntents(config, cwd) {
14104
14494
  const records = [];
14105
14495
  for (const entry of entries) {
14106
14496
  if (entry.type !== "dir" || !entry.name || !isCompanyIntentId(entry.name)) continue;
14107
- const path51 = companyIntentPath(entry.name);
14108
- const file = readStateText(config, cwd, path51);
14497
+ const path52 = companyIntentPath(entry.name);
14498
+ const file = readStateText(config, cwd, path52);
14109
14499
  if (!file) continue;
14110
14500
  records.push({
14111
14501
  id: entry.name,
@@ -14314,14 +14704,14 @@ var init_loadIssueContext = __esm({
14314
14704
  if (!ctx.data.commentTargetNumber) ctx.data.commentTargetNumber = issueNumber;
14315
14705
  return;
14316
14706
  }
14317
- const issue = getIssue(issueNumber, ctx.cwd);
14707
+ const issue2 = getIssue(issueNumber, ctx.cwd);
14318
14708
  const cfgCtx = ctx.config.issueContext ?? {};
14319
14709
  const limit = cfgCtx.commentLimit ?? DEFAULT_COMMENT_LIMIT;
14320
14710
  const maxBytes = cfgCtx.commentMaxBytes ?? DEFAULT_COMMENT_MAX_BYTES;
14321
- const commentsFormatted = formatIssueComments(issue.comments, limit, maxBytes);
14322
- const labels = issue.labels ?? [];
14711
+ const commentsFormatted = formatIssueComments(issue2.comments, limit, maxBytes);
14712
+ const labels = issue2.labels ?? [];
14323
14713
  const labelsFormatted = labels.length === 0 ? "(no labels)" : labels.map((l) => `\`${l}\``).join(", ");
14324
- ctx.data.issue = { ...issue, commentsFormatted, labelsFormatted };
14714
+ ctx.data.issue = { ...issue2, commentsFormatted, labelsFormatted };
14325
14715
  ctx.data.commentTargetType = "issue";
14326
14716
  ctx.data.commentTargetNumber = issueNumber;
14327
14717
  };
@@ -14350,11 +14740,11 @@ var init_loadIssueStateComment = __esm({
14350
14740
  if (!owner || !repo) {
14351
14741
  throw new Error("loadIssueStateComment: ctx.config.github.owner/repo must be set");
14352
14742
  }
14353
- const issue = getIssue(issueNumber, ctx.cwd);
14743
+ const issue2 = getIssue(issueNumber, ctx.cwd);
14354
14744
  const loaded = findStateComment(owner, repo, issueNumber, marker, ctx.cwd);
14355
14745
  ctx.data.stateMarker = marker;
14356
- ctx.data.issueIntent = issue.body;
14357
- ctx.data.issueTitle = issue.title;
14746
+ ctx.data.issueIntent = issue2.body;
14747
+ ctx.data.issueTitle = issue2.title;
14358
14748
  ctx.data.issueNumber = String(issueNumber);
14359
14749
  ctx.data.issueStateComment = loaded;
14360
14750
  ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
@@ -14484,15 +14874,15 @@ var init_loadLinkedFinding = __esm({
14484
14874
  if (!pr) return;
14485
14875
  const findingNumber = resolveFindingNumber(pr);
14486
14876
  if (!findingNumber) return;
14487
- let issue;
14877
+ let issue2;
14488
14878
  try {
14489
- issue = getIssue(findingNumber, ctx.cwd);
14879
+ issue2 = getIssue(findingNumber, ctx.cwd);
14490
14880
  } catch {
14491
14881
  return;
14492
14882
  }
14493
- ctx.data.linkedFinding = `Issue #${issue.number}: ${issue.title}
14883
+ ctx.data.linkedFinding = `Issue #${issue2.number}: ${issue2.title}
14494
14884
 
14495
- ${truncate(issue.body, FINDING_BODY_MAX_BYTES)}`;
14885
+ ${truncate(issue2.body, FINDING_BODY_MAX_BYTES)}`;
14496
14886
  };
14497
14887
  }
14498
14888
  });
@@ -14697,8 +15087,8 @@ async function writeGithubStateTextWithConfig(opts) {
14697
15087
  );
14698
15088
  }
14699
15089
  }
14700
- function jsonlLines(text) {
14701
- return text.split("\n").filter((line) => line.length > 0);
15090
+ function jsonlLines(text2) {
15091
+ return text2.split("\n").filter((line) => line.length > 0);
14702
15092
  }
14703
15093
  function renderJsonl(lines) {
14704
15094
  return lines.length > 0 ? `${lines.join("\n")}
@@ -14998,14 +15388,14 @@ var init_loadTaskContext = __esm({
14998
15388
  loadTaskContext = async (ctx) => {
14999
15389
  const runId = resolveRunId();
15000
15390
  const rawIssue = ctx.data.issue;
15001
- const issue = rawIssue ? {
15391
+ const issue2 = rawIssue ? {
15002
15392
  ...rawIssue,
15003
15393
  commentsFormatted: rawIssue.commentsFormatted ?? "",
15004
15394
  labelsFormatted: rawIssue.labelsFormatted ?? ""
15005
15395
  } : void 0;
15006
15396
  const taskContext = buildTaskContext({
15007
15397
  runId,
15008
- issue,
15398
+ issue: issue2,
15009
15399
  conventions: ctx.data.conventions,
15010
15400
  priorArt: typeof ctx.data.priorArt === "string" ? ctx.data.priorArt : "",
15011
15401
  memoryContext: typeof ctx.data.memoryContext === "string" ? ctx.data.memoryContext : "",
@@ -15360,6 +15750,12 @@ var init_notifyTerminal = __esm({
15360
15750
  });
15361
15751
 
15362
15752
  // src/scripts/openAgencyModelReviewPr.ts
15753
+ function isDryRun(ctx) {
15754
+ const arg = ctx.args.dry_run ?? ctx.args.dryRun;
15755
+ if (arg === true) return true;
15756
+ if (typeof arg === "string" && ["1", "true", "yes"].includes(arg.trim().toLowerCase())) return true;
15757
+ return ["1", "true", "yes"].includes((process.env.KODY_DRY_RUN ?? "").trim().toLowerCase());
15758
+ }
15363
15759
  function parseAgencyModelProposal(raw) {
15364
15760
  const jsonText = stripJsonFence(raw);
15365
15761
  let parsed;
@@ -15429,9 +15825,9 @@ function normalizeBundleFiles(ctx, bundle) {
15429
15825
  });
15430
15826
  }
15431
15827
  function stripJsonFence(raw) {
15432
- const text = raw.trim();
15433
- const fence = text.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i);
15434
- return (fence ? fence[1] : text).trim();
15828
+ const text2 = raw.trim();
15829
+ const fence = text2.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i);
15830
+ return (fence ? fence[1] : text2).trim();
15435
15831
  }
15436
15832
  function readIssueNumber(ctx) {
15437
15833
  const issueNumber = ctx.args.issue;
@@ -15441,9 +15837,9 @@ function readIssueNumber(ctx) {
15441
15837
  return issueNumber;
15442
15838
  }
15443
15839
  function readRequiredJsonString(value, field) {
15444
- const text = readJsonString(value, field).trim();
15445
- if (!text) throw new Error(`openAgencyModelReviewPr: ${field} must be a non-empty string`);
15446
- return text;
15840
+ const text2 = readJsonString(value, field).trim();
15841
+ if (!text2) throw new Error(`openAgencyModelReviewPr: ${field} must be a non-empty string`);
15842
+ return text2;
15447
15843
  }
15448
15844
  function readJsonString(value, field) {
15449
15845
  if (typeof value !== "string") throw new Error(`openAgencyModelReviewPr: ${field} must be a string`);
@@ -15523,6 +15919,16 @@ var init_openAgencyModelReviewPr = __esm({
15523
15919
  const stateRepo = parseStateRepo(ctx.config);
15524
15920
  const baseBranch = "main";
15525
15921
  const branch = buildStatePrBranchName(sourceLabel, issueNumber, bundle.title);
15922
+ if (isDryRun(ctx)) {
15923
+ ctx.data.agencyModelReviewPr = {
15924
+ dryRun: true,
15925
+ repo: `${stateRepo.owner}/${stateRepo.repo}`,
15926
+ branch,
15927
+ base: baseBranch,
15928
+ files: normalizedFiles.map((file) => file.targetPath)
15929
+ };
15930
+ return;
15931
+ }
15526
15932
  const baseRef = ghJson(
15527
15933
  ["api", `/repos/${stateRepo.owner}/${stateRepo.repo}/git/ref/heads/${baseBranch}`],
15528
15934
  ctx.cwd
@@ -15635,10 +16041,10 @@ function ensureLabel2(cwd) {
15635
16041
  return false;
15636
16042
  }
15637
16043
  }
15638
- function markIssueWithReportLabel(issue, cwd) {
16044
+ function markIssueWithReportLabel(issue2, cwd) {
15639
16045
  if (!ensureLabel2(cwd)) return;
15640
16046
  try {
15641
- gh(["issue", "edit", String(issue), "--add-label", QA_LABEL], { cwd });
16047
+ gh(["issue", "edit", String(issue2), "--add-label", QA_LABEL], { cwd });
15642
16048
  } catch {
15643
16049
  }
15644
16050
  }
@@ -15777,13 +16183,13 @@ function isPartialEnvelope(x) {
15777
16183
  function escapeRegex(s) {
15778
16184
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
15779
16185
  }
15780
- function extractFencedBlock(text, label) {
16186
+ function extractFencedBlock(text2, label) {
15781
16187
  const re = new RegExp(`\`\`\`${escapeRegex(label)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
15782
- const m = re.exec(text);
16188
+ const m = re.exec(text2);
15783
16189
  return m ? m[1].trim() : null;
15784
16190
  }
15785
- function extractNextStateFromText(text, fenceLabel, prevRev) {
15786
- const inner = extractFencedBlock(text, fenceLabel);
16191
+ function extractNextStateFromText(text2, fenceLabel, prevRev) {
16192
+ const inner = extractFencedBlock(text2, fenceLabel);
15787
16193
  if (inner === null) {
15788
16194
  return { error: `missing \`${fenceLabel}\` fenced block` };
15789
16195
  }
@@ -15902,15 +16308,15 @@ var init_parseJobStateFromAgentResult = __esm({
15902
16308
  });
15903
16309
 
15904
16310
  // src/scripts/parseReproOutput.ts
15905
- function extractTestPath(text) {
15906
- const m = text.match(/^[\s>*_#`~-]*TEST_PATH[\s>*_#`~-]*\s*:\s*(.+?)\s*$/im);
16311
+ function extractTestPath(text2) {
16312
+ const m = text2.match(/^[\s>*_#`~-]*TEST_PATH[\s>*_#`~-]*\s*:\s*(.+?)\s*$/im);
15907
16313
  if (!m) return "";
15908
16314
  return stripMarkdownEmphasis2(m[1] ?? "");
15909
16315
  }
15910
- function extractFailureSignatureBlock(text) {
15911
- const startIdx = text.search(/(?:^|\n)[ \t]*FAILURE_SIGNATURE\s*:[ \t]*/i);
16316
+ function extractFailureSignatureBlock(text2) {
16317
+ const startIdx = text2.search(/(?:^|\n)[ \t]*FAILURE_SIGNATURE\s*:[ \t]*/i);
15912
16318
  if (startIdx === -1) return "";
15913
- const afterMarker = text.slice(startIdx).replace(/^[\s\S]*?FAILURE_SIGNATURE\s*:[ \t]*\n?/i, "");
16319
+ const afterMarker = text2.slice(startIdx).replace(/^[\s\S]*?FAILURE_SIGNATURE\s*:[ \t]*\n?/i, "");
15914
16320
  const stopRe = /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|TEST_PATH)\s*:/i;
15915
16321
  const stopIdx = afterMarker.search(stopRe);
15916
16322
  let block = stopIdx === -1 ? afterMarker : afterMarker.slice(0, stopIdx);
@@ -15927,14 +16333,14 @@ function normalizeFailureSignatureBlock(block) {
15927
16333
  const jsonObject = extractFirstJsonObject(s);
15928
16334
  return jsonObject || s;
15929
16335
  }
15930
- function extractFirstJsonObject(text) {
15931
- const start = text.indexOf("{");
16336
+ function extractFirstJsonObject(text2) {
16337
+ const start = text2.indexOf("{");
15932
16338
  if (start === -1) return "";
15933
16339
  let depth = 0;
15934
16340
  let inString = false;
15935
16341
  let escaped = false;
15936
- for (let i = start; i < text.length; i++) {
15937
- const ch = text[i];
16342
+ for (let i = start; i < text2.length; i++) {
16343
+ const ch = text2[i];
15938
16344
  if (inString) {
15939
16345
  if (escaped) {
15940
16346
  escaped = false;
@@ -15951,7 +16357,7 @@ function extractFirstJsonObject(text) {
15951
16357
  depth++;
15952
16358
  } else if (ch === "}") {
15953
16359
  depth--;
15954
- if (depth === 0) return text.slice(start, i + 1).trim();
16360
+ if (depth === 0) return text2.slice(start, i + 1).trim();
15955
16361
  }
15956
16362
  }
15957
16363
  return "";
@@ -15977,9 +16383,9 @@ var init_parseReproOutput = __esm({
15977
16383
  "use strict";
15978
16384
  parseReproOutput = async (ctx, _profile, agentResult) => {
15979
16385
  if (!agentResult || ctx.data.agentDone === false) return;
15980
- const text = agentResult.finalText ?? "";
15981
- const testPath = extractTestPath(text);
15982
- const signatureRaw = extractFailureSignatureBlock(text);
16386
+ const text2 = agentResult.finalText ?? "";
16387
+ const testPath = extractTestPath(text2);
16388
+ const signatureRaw = extractFailureSignatureBlock(text2);
15983
16389
  if (!testPath) {
15984
16390
  downgrade(ctx, "reproduce missing TEST_PATH line in final message");
15985
16391
  return;
@@ -16158,8 +16564,8 @@ var init_planTaskJobs = __esm({
16158
16564
  ctx.output.reason = "planTaskJobs requires --issue";
16159
16565
  return;
16160
16566
  }
16161
- const issue = ctx.data.issue;
16162
- const specs = parseTaskJobSpecs(issue?.body ?? "");
16567
+ const issue2 = ctx.data.issue;
16568
+ const specs = parseTaskJobSpecs(issue2?.body ?? "");
16163
16569
  if (specs.length === 0) {
16164
16570
  ctx.skipAgent = true;
16165
16571
  ctx.output.exitCode = 64;
@@ -16489,8 +16895,8 @@ var init_promoteQaGoal = __esm({
16489
16895
  }
16490
16896
  let report;
16491
16897
  try {
16492
- const issue = getIssue(issueNum, ctx.cwd);
16493
- const reportComment = [...issue.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
16898
+ const issue2 = getIssue(issueNum, ctx.cwd);
16899
+ const reportComment = [...issue2.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
16494
16900
  if (!reportComment) {
16495
16901
  ctx.output.exitCode = 3;
16496
16902
  ctx.output.reason = `no QA report (${REPORT_JSON_OPEN2} \u2026) found on issue #${issueNum}`;
@@ -16557,9 +16963,9 @@ function latestResult(raw, agentResult) {
16557
16963
  function recordField6(value) {
16558
16964
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
16559
16965
  }
16560
- function resolveDotted(root, path51) {
16561
- if (!path51) return void 0;
16562
- return path51.split(".").reduce((value, key) => recordField6(value)?.[key], root);
16966
+ function resolveDotted(root, path52) {
16967
+ if (!path52) return void 0;
16968
+ return path52.split(".").reduce((value, key) => recordField6(value)?.[key], root);
16563
16969
  }
16564
16970
  function stringValue4(value) {
16565
16971
  return typeof value === "string" && value.trim() ? value.trim() : null;
@@ -16601,13 +17007,7 @@ var init_publishReport = __esm({
16601
17007
  ...publication.reviewArea ? { reviewArea: publication.reviewArea } : {}
16602
17008
  });
16603
17009
  const runId = generatedAt.replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-");
16604
- writeStateText(
16605
- ctx.config,
16606
- ctx.cwd,
16607
- `reports/${slug}/runs/${runId}.md`,
16608
- markdown,
16609
- `chore(reports): add ${slug} run`
16610
- );
17010
+ writeStateText(ctx.config, ctx.cwd, `reports/${slug}/runs/${runId}.md`, markdown, `chore(reports): add ${slug} run`);
16611
17011
  };
16612
17012
  }
16613
17013
  });
@@ -17315,15 +17715,15 @@ var init_runFlow = __esm({
17315
17715
  init_issue();
17316
17716
  runFlow = async (ctx) => {
17317
17717
  const issueNumber = ctx.args.issue;
17318
- const issue = getIssue(issueNumber, ctx.cwd);
17718
+ const issue2 = getIssue(issueNumber, ctx.cwd);
17319
17719
  const cfgCtx = ctx.config.issueContext ?? {};
17320
17720
  const commentsFormatted = formatIssueComments(
17321
- issue.comments,
17721
+ issue2.comments,
17322
17722
  cfgCtx.commentLimit ?? DEFAULT_COMMENT_LIMIT,
17323
17723
  cfgCtx.commentMaxBytes ?? DEFAULT_COMMENT_MAX_BYTES
17324
17724
  );
17325
- ctx.data.issue = { ...issue, commentsFormatted };
17326
- if (issue.isPullRequest) {
17725
+ ctx.data.issue = { ...issue2, commentsFormatted };
17726
+ if (issue2.isPullRequest) {
17327
17727
  ctx.data.commentTargetType = "pr";
17328
17728
  ctx.data.commentTargetNumber = issueNumber;
17329
17729
  ctx.skipAgent = true;
@@ -17347,7 +17747,7 @@ var init_runFlow = __esm({
17347
17747
  }
17348
17748
  const branchInfo = ensureFeatureBranch(
17349
17749
  issueNumber,
17350
- issue.title,
17750
+ issue2.title,
17351
17751
  ctx.config.git.defaultBranch,
17352
17752
  ctx.cwd,
17353
17753
  base ?? void 0
@@ -17596,8 +17996,8 @@ async function flyCreateApp(name, orgSlug, token) {
17596
17996
  });
17597
17997
  if (res.status === 422) return;
17598
17998
  if (!res.ok) {
17599
- const text = await res.text().catch(() => "");
17600
- throw new Error(`createApp ${name}: ${res.status} ${text.slice(0, 200)}`);
17999
+ const text2 = await res.text().catch(() => "");
18000
+ throw new Error(`createApp ${name}: ${res.status} ${text2.slice(0, 200)}`);
17601
18001
  }
17602
18002
  }
17603
18003
  async function flyAllocateSharedIps(appName, token) {
@@ -17704,9 +18104,9 @@ async function flyCreatePreviewMachine(args, token) {
17704
18104
  const { id } = await res.json();
17705
18105
  return id;
17706
18106
  }
17707
- const text = await res.text().catch(() => "");
17708
- lastErr = new Error(`createPreviewMachine ${res.status}: ${text.slice(0, 300)}`);
17709
- if (!/MANIFEST_UNKNOWN|manifest unknown/i.test(text)) break;
18107
+ const text2 = await res.text().catch(() => "");
18108
+ lastErr = new Error(`createPreviewMachine ${res.status}: ${text2.slice(0, 300)}`);
18109
+ if (!/MANIFEST_UNKNOWN|manifest unknown/i.test(text2)) break;
17710
18110
  await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
17711
18111
  }
17712
18112
  throw lastErr ?? new Error("createPreviewMachine failed (unknown)");
@@ -18145,8 +18545,8 @@ var init_setCommentTarget = __esm({
18145
18545
 
18146
18546
  // src/scripts/setLifecycleLabel.ts
18147
18547
  function resolveTargetNumber(args) {
18148
- const issue = args.issue;
18149
- if (typeof issue === "number" && Number.isFinite(issue)) return issue;
18548
+ const issue2 = args.issue;
18549
+ if (typeof issue2 === "number" && Number.isFinite(issue2)) return issue2;
18150
18550
  const pr = args.pr;
18151
18551
  if (typeof pr === "number" && Number.isFinite(pr)) return pr;
18152
18552
  return void 0;
@@ -18361,24 +18761,29 @@ var init_syncFlow = __esm({
18361
18761
  });
18362
18762
 
18363
18763
  // src/scripts/validateAgencyModelProposal.ts
18364
- function validateModelBundle(bundle, expectedKind) {
18764
+ import * as path43 from "path";
18765
+ function validateModelBundle(bundle, expectedKind, options = {}) {
18365
18766
  const failures = [];
18366
- validateOneModel(bundle.model, bundle.files, "model", true, failures, expectedKind);
18767
+ validateOneModel(bundle.model, bundle.files, "model", true, failures, expectedKind, options);
18367
18768
  return failures;
18368
18769
  }
18369
18770
  function readExpectedModelKind(args) {
18370
18771
  const value = args?.modelKind;
18371
18772
  if (typeof value === "string" && isModelKind(value)) return value;
18372
- throw new Error("validateAgencyModelProposal: with.modelKind must be agent, capability, goal, agentLoop, or workflow");
18773
+ throw new Error(
18774
+ "validateAgencyModelProposal: with.modelKind must be intent, operation, agent, capability, goal, agentLoop, or workflow"
18775
+ );
18373
18776
  }
18374
- function validateOneModel(rawModel, files, label, strictSingleModel, failures, expectedKind) {
18777
+ function validateOneModel(rawModel, files, label, strictSingleModel, failures, expectedKind, options = {}) {
18375
18778
  if (!rawModel || typeof rawModel !== "object" || Array.isArray(rawModel)) {
18376
18779
  failures.push(`${label} must be an object`);
18377
18780
  return;
18378
18781
  }
18379
18782
  const model = rawModel;
18380
18783
  const kind = stringField6(model.kind);
18381
- if (!isModelKind(kind)) failures.push(`${label}.kind must be agent, capability, goal, agentLoop, or workflow`);
18784
+ if (!isModelKind(kind)) {
18785
+ failures.push(`${label}.kind must be intent, operation, agent, capability, goal, agentLoop, or workflow`);
18786
+ }
18382
18787
  if (expectedKind && kind !== expectedKind) failures.push(`proposal must output model.kind ${expectedKind}`);
18383
18788
  const slug = stringField6(model.slug);
18384
18789
  if (!isSlug(slug)) failures.push(`${label}.slug must be a lowercase slug`);
@@ -18387,15 +18792,23 @@ function validateOneModel(rawModel, files, label, strictSingleModel, failures, e
18387
18792
  for (const doc of REQUIRED_DOCS[kind]) {
18388
18793
  if (!docsUsed.includes(doc)) failures.push(`${label} docsUsed missing ${doc}`);
18389
18794
  }
18390
- validateFilesForKind(kind, slug, files, strictSingleModel, failures);
18795
+ validateFilesForKind(kind, slug, files, strictSingleModel, failures, options);
18391
18796
  validateModelShape(kind, model, files, slug, failures);
18392
18797
  }
18393
18798
  }
18394
- function validateFilesForKind(kind, slug, files, strictSingleModel, failures) {
18799
+ function validateFilesForKind(kind, slug, files, strictSingleModel, failures, options) {
18395
18800
  const paths = files.map((file) => normalizeBundlePath(file.path));
18396
18801
  if (paths.some((filePath) => filePath === "implementations" || filePath.startsWith("implementations/"))) {
18397
18802
  failures.push("files must not use obsolete implementation storage");
18398
18803
  }
18804
+ if (kind === "intent") {
18805
+ requirePath(paths, `intents/${slug}/intent.json`, "intent state", failures);
18806
+ if (strictSingleModel) rejectOtherRoots(paths, [`intents/${slug}/`], "intent", failures);
18807
+ }
18808
+ if (kind === "operation") {
18809
+ requirePath(paths, `operations/${slug}/operation.json`, "operation contract", failures);
18810
+ if (strictSingleModel) rejectOtherRoots(paths, [`operations/${slug}/`], "operation", failures);
18811
+ }
18399
18812
  if (kind === "agent") {
18400
18813
  requirePath(paths, `agents/${slug}.md`, "agent file", failures);
18401
18814
  if (strictSingleModel) rejectOtherRoots(paths, ["agents/"], "agent", failures);
@@ -18430,6 +18843,7 @@ function validateFilesForKind(kind, slug, files, strictSingleModel, failures) {
18430
18843
  }
18431
18844
  if (kind === "workflow") {
18432
18845
  requirePath(paths, `capabilities/${slug}/profile.json`, "workflow capability profile", failures);
18846
+ requirePath(paths, `capabilities/${slug}/capability.md`, "workflow capability body", failures);
18433
18847
  const profile = parseJsonFile(files, `capabilities/${slug}/profile.json`, failures);
18434
18848
  if (profile) {
18435
18849
  if (profile.capabilityKind !== void 0) {
@@ -18439,11 +18853,73 @@ function validateFilesForKind(kind, slug, files, strictSingleModel, failures) {
18439
18853
  const hasTopLevelSteps = Array.isArray(profile.steps) && profile.steps.length > 0;
18440
18854
  if (!hasWorkflowObject && !hasTopLevelSteps) {
18441
18855
  failures.push("workflow profile must include workflow object or top-level steps");
18856
+ } else {
18857
+ const workflow = hasWorkflowObject ? profile.workflow : { steps: profile.steps, ...profile.startAt !== void 0 ? { startAt: profile.startAt } : {} };
18858
+ const known = options.capabilityRoot ? getCapabilityRoots(options.capabilityRoot).flatMap((root) => listCapabilityFolderSlugs(root)) : [];
18859
+ const uniqueKnown = [...new Set(known)];
18860
+ const capabilityInputs = /* @__PURE__ */ new Map();
18861
+ if (options.capabilityRoot) {
18862
+ for (const capability of uniqueKnown) {
18863
+ const inputs = getCapabilityActionInputs(capability, options.capabilityRoot);
18864
+ if (inputs) {
18865
+ capabilityInputs.set(
18866
+ capability,
18867
+ new Set(inputs.flatMap((input) => [input.name, input.flag.replace(/^--/, "")]))
18868
+ );
18869
+ }
18870
+ }
18871
+ }
18872
+ failures.push(
18873
+ ...formatWorkflowValidationIssues(
18874
+ validateWorkflow(workflow, {
18875
+ ...uniqueKnown.length > 0 ? { knownCapabilities: new Set(uniqueKnown) } : {},
18876
+ ...capabilityInputs.size > 0 ? { capabilityInputs } : {}
18877
+ })
18878
+ )
18879
+ );
18442
18880
  }
18443
18881
  }
18444
18882
  }
18445
18883
  }
18446
18884
  function validateModelShape(kind, model, files, slug, failures) {
18885
+ if (kind === "intent") {
18886
+ const intent = parseJsonFile(files, `intents/${slug}/intent.json`, failures);
18887
+ if (!stringField6(model.direction)) failures.push("intent model must declare direction");
18888
+ if (!stringField6(intent?.for)) failures.push("intent file must declare direction");
18889
+ if (!isFiniteNumber(model.priority)) failures.push("intent model must declare numeric priority");
18890
+ if (!isFiniteNumber(intent?.priority)) failures.push("intent file must declare numeric priority");
18891
+ if (!hasScope(model.scope)) failures.push("intent model scope must include a repo or area");
18892
+ if (!hasScope(intent?.scope)) failures.push("intent file scope must include a repo or area");
18893
+ if (stringArray4(model.principles).length === 0) failures.push("intent model principles must be non-empty");
18894
+ if (stringArray4(intent?.principles).length === 0) failures.push("intent file principles must be non-empty");
18895
+ if (stringArray4(model.successMeasures).length === 0) {
18896
+ failures.push("intent model successMeasures must be non-empty");
18897
+ }
18898
+ if (stringArray4(intent?.metrics).length === 0) failures.push("intent file metrics must be non-empty");
18899
+ if (!recordField7(model.policy)) failures.push("intent model must declare policy");
18900
+ if (!recordField7(intent?.policy)) failures.push("intent file must declare policy");
18901
+ if (stringField6(model.status) !== "paused" || stringField6(intent?.status) !== "paused") {
18902
+ failures.push("intent proposal status must be paused");
18903
+ }
18904
+ if (intent?.version !== 1) failures.push("intent file version must be 1");
18905
+ if (intent && stringField6(intent.id) !== slug) failures.push("intent id must match model.slug");
18906
+ requireStringArrayIncludes(model.doesNotOwn, "operations", "intent doesNotOwn", failures);
18907
+ requireStringArrayIncludes(model.doesNotOwn, "capability implementation", "intent doesNotOwn", failures);
18908
+ }
18909
+ if (kind === "operation") {
18910
+ const operation = parseJsonFile(files, `operations/${slug}/operation.json`, failures);
18911
+ if (!stringField6(model.responsibility)) failures.push("operation model must declare responsibility");
18912
+ if (!stringField6(operation?.responsibility)) failures.push("operation file must declare responsibility");
18913
+ if (stringArray4(model.intentIds).length === 0) failures.push("operation model intentIds must be non-empty");
18914
+ if (stringArray4(operation?.intentIds).length === 0) failures.push("operation file intentIds must be non-empty");
18915
+ if (stringArray4(model.doesNotOwn).length === 0) failures.push("operation model doesNotOwn must be non-empty");
18916
+ if (stringArray4(operation?.doesNotOwn).length === 0) failures.push("operation file doesNotOwn must be non-empty");
18917
+ if (stringField6(model.status) !== "proposed" || stringField6(operation?.status) !== "proposed") {
18918
+ failures.push("operation proposal status must be proposed");
18919
+ }
18920
+ if (operation?.version !== 1) failures.push("operation file version must be 1");
18921
+ if (operation && stringField6(operation.id) !== slug) failures.push("operation id must match model.slug");
18922
+ }
18447
18923
  if (kind === "agent") {
18448
18924
  const agentFile = textFile(files, `agents/${slug}.md`);
18449
18925
  if (!stringArray4(model.owns).includes("identity") && !containsWord(agentFile, "identity")) {
@@ -18565,6 +19041,16 @@ function stringArray4(value) {
18565
19041
  if (!Array.isArray(value)) return [];
18566
19042
  return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
18567
19043
  }
19044
+ function recordField7(value) {
19045
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
19046
+ }
19047
+ function isFiniteNumber(value) {
19048
+ return typeof value === "number" && Number.isFinite(value);
19049
+ }
19050
+ function hasScope(value) {
19051
+ const scope = recordField7(value);
19052
+ return stringArray4(scope?.repos).length > 0 || stringArray4(scope?.areas).length > 0;
19053
+ }
18568
19054
  function requireStringArrayIncludes(value, expected, label, failures) {
18569
19055
  if (!stringArray4(value).includes(expected)) failures.push(`${label} must include ${expected}`);
18570
19056
  }
@@ -18572,14 +19058,19 @@ function isSlug(value) {
18572
19058
  return /^[a-z][a-z0-9-]{0,63}$/.test(value);
18573
19059
  }
18574
19060
  function isModelKind(value) {
18575
- return value === "agent" || value === "capability" || value === "goal" || value === "agentLoop" || value === "workflow";
19061
+ return value === "intent" || value === "operation" || value === "agent" || value === "capability" || value === "goal" || value === "agentLoop" || value === "workflow";
18576
19062
  }
18577
19063
  var REQUIRED_DOCS, validateAgencyModelProposal;
18578
19064
  var init_validateAgencyModelProposal = __esm({
18579
19065
  "src/scripts/validateAgencyModelProposal.ts"() {
18580
19066
  "use strict";
19067
+ init_capabilityFolders();
19068
+ init_registry();
19069
+ init_workflowValidation();
18581
19070
  init_openAgencyModelReviewPr();
18582
19071
  REQUIRED_DOCS = {
19072
+ intent: ["docs/intents.md", "docs/engine-company.md"],
19073
+ operation: ["docs/operations.md", "docs/engine-company.md"],
18583
19074
  agent: ["docs/agents.md"],
18584
19075
  capability: ["docs/capabilities.md", "docs/capability-kind-map.md", "docs/capability-implementations.md"],
18585
19076
  goal: ["docs/goals.md", "docs/jobs-model.md", "docs/capabilities.md"],
@@ -18591,7 +19082,9 @@ var init_validateAgencyModelProposal = __esm({
18591
19082
  const raw = String(ctx.data.prSummary ?? "");
18592
19083
  const bundle = parseAgencyModelProposal(raw);
18593
19084
  const expectedKind = readExpectedModelKind(args);
18594
- const failures = validateModelBundle(bundle, expectedKind);
19085
+ const failures = validateModelBundle(bundle, expectedKind, {
19086
+ capabilityRoot: path43.join(ctx.cwd, ".kody", "capabilities")
19087
+ });
18595
19088
  if (failures.length > 0) {
18596
19089
  throw new Error(`validateAgencyModelProposal: ${failures.join("; ")}`);
18597
19090
  }
@@ -19150,9 +19643,9 @@ var init_writeAgentRunSummary = __esm({
19150
19643
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
19151
19644
  if (!summaryPath) return;
19152
19645
  const implementation = profile.name;
19153
- const issue = ctx.args.issue;
19646
+ const issue2 = ctx.args.issue;
19154
19647
  const pr = ctx.args.pr;
19155
- const target = issue ? `issue #${issue}` : pr ? `PR #${pr}` : "(unknown)";
19648
+ const target = issue2 ? `issue #${issue2}` : pr ? `PR #${pr}` : "(unknown)";
19156
19649
  const prUrl = ctx.output.prUrl;
19157
19650
  const exitCode = ctx.output.exitCode ?? 0;
19158
19651
  const reason = ctx.output.reason;
@@ -19492,38 +19985,38 @@ import { execFileSync as execFileSync24 } from "child_process";
19492
19985
  import * as crypto3 from "crypto";
19493
19986
  import * as fs45 from "fs";
19494
19987
  import * as os7 from "os";
19495
- import * as path43 from "path";
19988
+ import * as path44 from "path";
19496
19989
  function writeLocalFile(cwd, relativePath, content) {
19497
- const fullPath = path43.join(cwd, relativePath);
19498
- fs45.mkdirSync(path43.dirname(fullPath), { recursive: true });
19990
+ const fullPath = path44.join(cwd, relativePath);
19991
+ fs45.mkdirSync(path44.dirname(fullPath), { recursive: true });
19499
19992
  fs45.writeFileSync(fullPath, content);
19500
19993
  }
19501
19994
  function copyPath(source, target) {
19502
19995
  const st = fs45.lstatSync(source);
19503
19996
  fs45.rmSync(target, { recursive: true, force: true });
19504
19997
  if (st.isSymbolicLink()) return;
19505
- fs45.mkdirSync(path43.dirname(target), { recursive: true });
19998
+ fs45.mkdirSync(path44.dirname(target), { recursive: true });
19506
19999
  fs45.cpSync(source, target, { recursive: true, force: true });
19507
20000
  }
19508
20001
  function overlayDirectoryChildren(cwd, sourceDir, localDir) {
19509
20002
  if (!fs45.existsSync(sourceDir)) return;
19510
20003
  for (const entry of fs45.readdirSync(sourceDir, { withFileTypes: true })) {
19511
- const source = path43.join(sourceDir, entry.name);
19512
- const target = path43.join(cwd, localDir, entry.name);
20004
+ const source = path44.join(sourceDir, entry.name);
20005
+ const target = path44.join(cwd, localDir, entry.name);
19513
20006
  copyPath(source, target);
19514
20007
  }
19515
20008
  }
19516
20009
  function hydrateStateWorkspace(config, cwd) {
19517
20010
  if (process.env.VITEST && process.env[TEST_FETCH_ENV] !== "1") return;
19518
20011
  const parsed = parseStateRepo(config);
19519
- const hydrateKey = `${path43.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${parsed.branch}`;
20012
+ const hydrateKey = `${path44.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${parsed.branch}`;
19520
20013
  if (hydratedWorkspaces.has(hydrateKey)) return;
19521
20014
  const snapshotRoot = fetchStateSnapshot(parsed);
19522
20015
  for (const mapping of DIR_MAPPINGS) {
19523
- overlayDirectoryChildren(cwd, path43.join(snapshotRoot, mapping.stateDir), mapping.localDir);
20016
+ overlayDirectoryChildren(cwd, path44.join(snapshotRoot, mapping.stateDir), mapping.localDir);
19524
20017
  }
19525
20018
  for (const mapping of FILE_MAPPINGS) {
19526
- const source = path43.join(snapshotRoot, mapping.statePath);
20019
+ const source = path44.join(snapshotRoot, mapping.statePath);
19527
20020
  if (fs45.existsSync(source) && !fs45.lstatSync(source).isSymbolicLink() && fs45.statSync(source).isFile()) {
19528
20021
  writeLocalFile(cwd, mapping.localPath, fs45.readFileSync(source, "utf-8"));
19529
20022
  }
@@ -19531,11 +20024,11 @@ function hydrateStateWorkspace(config, cwd) {
19531
20024
  hydratedWorkspaces.add(hydrateKey);
19532
20025
  }
19533
20026
  function fetchStateSnapshot(parsed) {
19534
- const cacheDir = path43.join(cacheRoot2(), cacheKey3(parsed));
20027
+ const cacheDir = path44.join(cacheRoot2(), cacheKey3(parsed));
19535
20028
  const url = `https://github.com/${parsed.owner}/${parsed.repo}.git`;
19536
20029
  try {
19537
- fs45.mkdirSync(path43.dirname(cacheDir), { recursive: true });
19538
- if (!fs45.existsSync(path43.join(cacheDir, ".git"))) {
20030
+ fs45.mkdirSync(path44.dirname(cacheDir), { recursive: true });
20031
+ if (!fs45.existsSync(path44.join(cacheDir, ".git"))) {
19539
20032
  fs45.rmSync(cacheDir, { recursive: true, force: true });
19540
20033
  runGit3(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
19541
20034
  }
@@ -19551,10 +20044,10 @@ function fetchStateSnapshot(parsed) {
19551
20044
  `stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${parsed.branch}: ${msg}`
19552
20045
  );
19553
20046
  }
19554
- return path43.join(cacheDir, parsed.basePath);
20047
+ return path44.join(cacheDir, parsed.basePath);
19555
20048
  }
19556
20049
  function cacheRoot2() {
19557
- return process.env[CACHE_ENV2]?.trim() || path43.join(os7.homedir(), ".cache", "kody", "state-repo");
20050
+ return process.env[CACHE_ENV2]?.trim() || path44.join(os7.homedir(), ".cache", "kody", "state-repo");
19558
20051
  }
19559
20052
  function cacheKey3(parsed) {
19560
20053
  return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${parsed.branch}#${parsed.basePath}`).digest("hex").slice(0, 24);
@@ -19594,16 +20087,16 @@ var init_stateWorkspace = __esm({
19594
20087
  "use strict";
19595
20088
  init_stateRepo();
19596
20089
  DIR_MAPPINGS = [
19597
- { stateDir: "capabilities", localDir: path43.join(".kody", "capabilities") },
19598
- { stateDir: "agents", localDir: path43.join(".kody", "agents") },
19599
- { stateDir: "context", localDir: path43.join(".kody", "context") },
19600
- { stateDir: "memory", localDir: path43.join(".kody", "memory") }
20090
+ { stateDir: "capabilities", localDir: path44.join(".kody", "capabilities") },
20091
+ { stateDir: "agents", localDir: path44.join(".kody", "agents") },
20092
+ { stateDir: "context", localDir: path44.join(".kody", "context") },
20093
+ { stateDir: "memory", localDir: path44.join(".kody", "memory") }
19601
20094
  ];
19602
20095
  FILE_MAPPINGS = [
19603
- { statePath: "instructions.md", localPath: path43.join(".kody", "instructions.md") },
19604
- { statePath: "system-prompt.md", localPath: path43.join(".kody", "system-prompt.md") },
19605
- { statePath: "variables.json", localPath: path43.join(".kody", "variables.json") },
19606
- { statePath: "secrets.enc", localPath: path43.join(".kody", "secrets.enc") }
20096
+ { statePath: "instructions.md", localPath: path44.join(".kody", "instructions.md") },
20097
+ { statePath: "system-prompt.md", localPath: path44.join(".kody", "system-prompt.md") },
20098
+ { statePath: "variables.json", localPath: path44.join(".kody", "variables.json") },
20099
+ { statePath: "secrets.enc", localPath: path44.join(".kody", "secrets.enc") }
19607
20100
  ];
19608
20101
  CACHE_ENV2 = "KODY_STATE_REPO_CACHE";
19609
20102
  TEST_FETCH_ENV = "KODY_STATE_WORKSPACE_FETCH_FOR_TESTS";
@@ -19678,7 +20171,8 @@ var init_tools = __esm({
19678
20171
  // src/executor.ts
19679
20172
  import { spawn as spawn7 } from "child_process";
19680
20173
  import * as fs46 from "fs";
19681
- import * as path44 from "path";
20174
+ import * as os8 from "os";
20175
+ import * as path45 from "path";
19682
20176
  function isMutatingPostflight(scriptName) {
19683
20177
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
19684
20178
  }
@@ -19706,9 +20200,9 @@ function collectShellSideChannels(ctx, stdout) {
19706
20200
  }
19707
20201
  }
19708
20202
  function operatorRequestBlock(why) {
19709
- const text = why.trim();
19710
- if (!text) return null;
19711
- const safe = text.replace(/-{3,}\s*END UNTRUSTED INPUT\s*-{3,}/gi, "[END UNTRUSTED INPUT]");
20203
+ const text2 = why.trim();
20204
+ if (!text2) return null;
20205
+ const safe = text2.replace(/-{3,}\s*END UNTRUSTED INPUT\s*-{3,}/gi, "[END UNTRUSTED INPUT]");
19712
20206
  return [
19713
20207
  "## The request that triggered this run",
19714
20208
  "",
@@ -19901,7 +20395,7 @@ async function runImplementation(profileName, input) {
19901
20395
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
19902
20396
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
19903
20397
  const invokeAgent = async (prompt) => {
19904
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path44.isAbsolute(p) ? p : path44.resolve(profile.dir, p)).filter((p) => p.length > 0);
20398
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path45.isAbsolute(p) ? p : path45.resolve(profile.dir, p)).filter((p) => p.length > 0);
19905
20399
  const syntheticPath = ctx.data.syntheticPluginPath;
19906
20400
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
19907
20401
  const agents = loadSubagents(profile);
@@ -19972,6 +20466,11 @@ async function runImplementation(profileName, input) {
19972
20466
  });
19973
20467
  };
19974
20468
  ctx.data.__invokeAgent = invokeAgent;
20469
+ const cc = profile.claudeCode;
20470
+ const declaresPluginParts = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
20471
+ if (declaresPluginParts && !profile.scripts.preflight.some((e) => e.script === "buildSyntheticPlugin")) {
20472
+ profile.scripts.preflight = [{ script: "buildSyntheticPlugin" }, ...profile.scripts.preflight];
20473
+ }
19975
20474
  try {
19976
20475
  for (const entry of profile.scripts.preflight) {
19977
20476
  const preLabel = entry.script ?? entry.shell ?? "<unknown>";
@@ -20145,8 +20644,12 @@ async function runImplementation(profileName, input) {
20145
20644
  nextDispatch: ctx.output.nextDispatch,
20146
20645
  nextJob: ctx.output.nextJob,
20147
20646
  afterNextJob: ctx.output.afterNextJob,
20148
- taskState: ctx.data.taskState
20647
+ taskState: ctx.data.taskState,
20648
+ capabilityResults: Array.isArray(ctx.data.capabilityResults) ? ctx.data.capabilityResults : void 0
20149
20649
  });
20650
+ } catch (err) {
20651
+ const msg = err instanceof Error ? err.message : String(err);
20652
+ return finishAndEnd({ exitCode: 99, reason: ctx.output.reason ?? msg });
20150
20653
  } finally {
20151
20654
  clearStampedLifecycleLabels(profile, ctx);
20152
20655
  if (taskArtifacts) {
@@ -20330,13 +20833,13 @@ function clearStampedLifecycleLabels(profile, ctx) {
20330
20833
  function resolveProfilePath(profileName) {
20331
20834
  const found = resolveImplementation(profileName);
20332
20835
  if (found) return found;
20333
- const here = path44.dirname(new URL(import.meta.url).pathname);
20836
+ const here = path45.dirname(new URL(import.meta.url).pathname);
20334
20837
  const candidates = [
20335
- path44.join(here, "implementations", profileName, "profile.json"),
20838
+ path45.join(here, "implementations", profileName, "profile.json"),
20336
20839
  // same-dir sibling (dev)
20337
- path44.join(here, "..", "implementations", profileName, "profile.json"),
20840
+ path45.join(here, "..", "implementations", profileName, "profile.json"),
20338
20841
  // up one (prod: dist/bin → dist/implementations)
20339
- path44.join(here, "..", "src", "implementations", profileName, "profile.json")
20842
+ path45.join(here, "..", "src", "implementations", profileName, "profile.json")
20340
20843
  // fallback
20341
20844
  ];
20342
20845
  for (const c of candidates) {
@@ -20455,7 +20958,7 @@ function resolveShellTimeoutMs(entry) {
20455
20958
  }
20456
20959
  async function runShellEntry(entry, ctx, profile) {
20457
20960
  const shellName = entry.shell;
20458
- const shellPath = path44.join(profile.dir, shellName);
20961
+ const shellPath = path45.join(profile.dir, shellName);
20459
20962
  if (!fs46.existsSync(shellPath)) {
20460
20963
  ctx.skipAgent = true;
20461
20964
  ctx.output.exitCode = 99;
@@ -20463,7 +20966,11 @@ async function runShellEntry(entry, ctx, profile) {
20463
20966
  return;
20464
20967
  }
20465
20968
  const positional = entry.with ? Object.values(entry.with).map((v) => String(v)) : [];
20466
- const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
20969
+ const outputFile = path45.join(
20970
+ os8.tmpdir(),
20971
+ `kody-shell-output-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
20972
+ );
20973
+ const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", KODY_OUTPUT: outputFile };
20467
20974
  for (const [k, v] of Object.entries(ctx.args)) {
20468
20975
  if (v === void 0 || v === null) continue;
20469
20976
  env[`KODY_ARG_${envKey(k)}`] = String(v);
@@ -20529,7 +21036,25 @@ async function runShellEntry(entry, ctx, profile) {
20529
21036
  ctx.output.reason = `shell '${shellName}' failed to spawn: ${result.spawnErr.message}`;
20530
21037
  return;
20531
21038
  }
20532
- collectShellSideChannels(ctx, stdout);
21039
+ let sideChannelText = "";
21040
+ try {
21041
+ if (fs46.existsSync(outputFile)) {
21042
+ sideChannelText = fs46.readFileSync(outputFile, "utf-8");
21043
+ fs46.rmSync(outputFile, { force: true });
21044
+ }
21045
+ } catch {
21046
+ }
21047
+ if (sideChannelText.trim().length > 0) {
21048
+ collectShellSideChannels(ctx, sideChannelText);
21049
+ } else {
21050
+ if (SHELL_MARKER_RE.test(stdout)) {
21051
+ process.stderr.write(
21052
+ `[kody] shell '${shellName}': KODY_* markers read from stdout are deprecated \u2014 write them to "$KODY_OUTPUT" instead (stdout markers are forgeable by echoed untrusted text)
21053
+ `
21054
+ );
21055
+ }
21056
+ collectShellSideChannels(ctx, stdout);
21057
+ }
20533
21058
  if (timedOut) {
20534
21059
  ctx.skipAgent = true;
20535
21060
  const seconds = Math.round(timeoutMs / 1e3);
@@ -20571,7 +21096,7 @@ function flattenConfig(obj, prefix = "") {
20571
21096
  }
20572
21097
  return out;
20573
21098
  }
20574
- var MUTATING_POSTFLIGHTS, MAX_CHAIN_HOPS, DEFAULT_SHELL_TIMEOUT_MS, SIGKILL_GRACE_MS;
21099
+ var MUTATING_POSTFLIGHTS, SHELL_MARKER_RE, MAX_CHAIN_HOPS, DEFAULT_SHELL_TIMEOUT_MS, SIGKILL_GRACE_MS;
20575
21100
  var init_executor = __esm({
20576
21101
  "src/executor.ts"() {
20577
21102
  "use strict";
@@ -20602,12 +21127,75 @@ var init_executor = __esm({
20602
21127
  "publishReport",
20603
21128
  "openAgencyModelReviewPr"
20604
21129
  ]);
21130
+ SHELL_MARKER_RE = /^KODY_(SKIP_AGENT|PR_URL|REASON|CAPABILITY_REPORT|CAPABILITY_RESULT)=/m;
20605
21131
  MAX_CHAIN_HOPS = 60;
20606
21132
  DEFAULT_SHELL_TIMEOUT_MS = 3e5;
20607
21133
  SIGKILL_GRACE_MS = 5e3;
20608
21134
  }
20609
21135
  });
20610
21136
 
21137
+ // src/workflowRunState.ts
21138
+ function workflowRunStatePath(workflowId, runId) {
21139
+ if (!SAFE_ID.test(workflowId)) throw new Error(`invalid workflow id ${workflowId}`);
21140
+ if (!SAFE_ID.test(runId)) throw new Error(`invalid workflow run id ${runId}`);
21141
+ return `workflows/${workflowId}/runs/${runId}.json`;
21142
+ }
21143
+ function parseWorkflowRunState(raw) {
21144
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
21145
+ const state = raw;
21146
+ if (state.status !== "running" && state.status !== "blocked" && state.status !== "failed" && state.status !== "done")
21147
+ return null;
21148
+ const completedStepIds = Array.isArray(state.completedStepIds) ? state.completedStepIds.filter((value) => typeof value === "string") : [];
21149
+ const transitionCounts = state.transitionCounts && typeof state.transitionCounts === "object" && !Array.isArray(state.transitionCounts) ? Object.fromEntries(
21150
+ Object.entries(state.transitionCounts).filter(
21151
+ (entry) => typeof entry[1] === "number" && Number.isInteger(entry[1]) && entry[1] >= 0
21152
+ )
21153
+ ) : {};
21154
+ const facts = state.facts && typeof state.facts === "object" && !Array.isArray(state.facts) ? state.facts : {};
21155
+ const evidenceEntries = state.evidence && typeof state.evidence === "object" && !Array.isArray(state.evidence) ? Object.entries(state.evidence).filter((entry) => typeof entry[1] === "boolean") : [];
21156
+ const artifacts = Array.isArray(state.artifacts) ? state.artifacts.filter(
21157
+ (artifact) => !!artifact && typeof artifact === "object" && typeof artifact.label === "string" && (artifact.url === void 0 || typeof artifact.url === "string") && (artifact.path === void 0 || typeof artifact.path === "string")
21158
+ ) : [];
21159
+ return {
21160
+ status: state.status,
21161
+ ...typeof state.currentStepId === "string" ? { currentStepId: state.currentStepId } : {},
21162
+ completedStepIds,
21163
+ transitionCounts,
21164
+ facts: { ...facts },
21165
+ evidence: Object.fromEntries(evidenceEntries),
21166
+ artifacts: artifacts.map((artifact) => ({ ...artifact })),
21167
+ ...typeof state.blocker === "string" ? { blocker: state.blocker } : {}
21168
+ };
21169
+ }
21170
+ function readWorkflowRunState(config, cwd, workflowId, runId) {
21171
+ const file = readStateText(config, cwd, workflowRunStatePath(workflowId, runId));
21172
+ if (!file) return null;
21173
+ try {
21174
+ return parseWorkflowRunState(JSON.parse(file.content));
21175
+ } catch {
21176
+ return null;
21177
+ }
21178
+ }
21179
+ function writeWorkflowRunState(config, cwd, workflowId, runId, state) {
21180
+ const path52 = workflowRunStatePath(workflowId, runId);
21181
+ upsertStateText(
21182
+ config,
21183
+ cwd,
21184
+ path52,
21185
+ `${JSON.stringify(state, null, 2)}
21186
+ `,
21187
+ `chore(workflows): update ${workflowId} run ${runId}`
21188
+ );
21189
+ }
21190
+ var SAFE_ID;
21191
+ var init_workflowRunState = __esm({
21192
+ "src/workflowRunState.ts"() {
21193
+ "use strict";
21194
+ init_stateRepo();
21195
+ SAFE_ID = /^[a-z0-9][a-z0-9_-]{0,79}$/;
21196
+ }
21197
+ });
21198
+
20611
21199
  // src/job.ts
20612
21200
  var job_exports = {};
20613
21201
  __export(job_exports, {
@@ -20620,7 +21208,7 @@ __export(job_exports, {
20620
21208
  stableJobKey: () => stableJobKey,
20621
21209
  validateJob: () => validateJob
20622
21210
  });
20623
- import * as path45 from "path";
21211
+ import * as path46 from "path";
20624
21212
  function newJobId(flavor) {
20625
21213
  localJobSeq += 1;
20626
21214
  const runId = process.env.GITHUB_RUN_ID;
@@ -20652,6 +21240,8 @@ function validateJob(input) {
20652
21240
  target: typeof j.target === "number" ? j.target : void 0,
20653
21241
  cliArgs: j.cliArgs ?? {},
20654
21242
  workflowFacts: j.workflowFacts && typeof j.workflowFacts === "object" && !Array.isArray(j.workflowFacts) ? j.workflowFacts : void 0,
21243
+ workflowState: parseWorkflowRunState(j.workflowState) ?? void 0,
21244
+ workflowRunId: typeof j.workflowRunId === "string" && j.workflowRunId.trim() ? j.workflowRunId.trim() : void 0,
20655
21245
  evidence: parseJobEvidence(j),
20656
21246
  flavor: j.flavor,
20657
21247
  force: j.force === true,
@@ -20686,7 +21276,7 @@ function parseJobEvidence(job) {
20686
21276
  async function runJob(job, base) {
20687
21277
  const valid = validateJob(job);
20688
21278
  const action = valid.action ?? valid.capability;
20689
- const projectCapabilitiesRoot = path45.join(base.cwd, ".kody", "capabilities");
21279
+ const projectCapabilitiesRoot = path46.join(base.cwd, ".kody", "capabilities");
20690
21280
  const resolvedCapability = !valid.workflow && action ? resolveCapabilityAction(action, projectCapabilitiesRoot) : null;
20691
21281
  const capabilityIdentity = valid.capability ?? resolvedCapability?.capability;
20692
21282
  const capabilityContext = valid.workflow ? null : loadCapabilityContext(capabilityIdentity, base.cwd);
@@ -20704,8 +21294,17 @@ async function runJob(job, base) {
20704
21294
  const profileName = explicitImplementation ?? capabilitySelectedImplementation;
20705
21295
  if (workflow && shouldRunCapabilityWorkflow(valid, workflow, workflowIdentity, capabilitySelectedImplementation, base)) {
20706
21296
  const workflowCapability = capabilityContext ?? workflowContext;
20707
- const workflowJob = workflowContext && !valid.why ? { ...valid, why: workflowContext.body } : valid;
20708
- return runCapabilityWorkflow(workflowJob, workflow, workflowCapability, base);
21297
+ const persistedState = valid.workflowRunId && workflowIdentity && base.config ? readWorkflowRunState(base.config, base.cwd, workflowIdentity, valid.workflowRunId) : null;
21298
+ const workflowJob = {
21299
+ ...workflowContext && !valid.why ? { ...valid, why: workflowContext.body } : valid,
21300
+ ...valid.workflowState ?? persistedState ? { workflowState: valid.workflowState ?? persistedState ?? void 0 } : {}
21301
+ };
21302
+ const checkpoint = valid.workflowRunId && workflowIdentity && base.config ? (state) => writeWorkflowRunState(base.config, base.cwd, workflowIdentity, valid.workflowRunId, state) : void 0;
21303
+ const result = await runCapabilityWorkflow(workflowJob, workflow, workflowCapability, base, checkpoint);
21304
+ if (valid.workflowRunId && workflowIdentity && base.config && result.workflowState) {
21305
+ writeWorkflowRunState(base.config, base.cwd, workflowIdentity, valid.workflowRunId, result.workflowState);
21306
+ }
21307
+ return result;
20709
21308
  }
20710
21309
  if (!profileName) {
20711
21310
  throw new InvalidJobError(`job capability resolves to no implementation: ${capabilityIdentity ?? action}`);
@@ -20785,7 +21384,22 @@ function shouldRunCapabilityWorkflow(job, workflow, capabilityIdentity, selected
20785
21384
  if (!requestedImplementation) return true;
20786
21385
  return requestedImplementation === selectedImplementation || requestedImplementation === capabilityIdentity || requestedImplementation === job.action;
20787
21386
  }
20788
- async function runCapabilityWorkflow(parent, workflow, capability, base) {
21387
+ async function runCapabilityWorkflow(parent, workflow, capability, base, checkpoint) {
21388
+ const invalid = workflowError(workflow, base);
21389
+ if (invalid) {
21390
+ if (isGraphWorkflow(workflow)) {
21391
+ const state = initialWorkflowState(parent, workflow);
21392
+ state.status = "blocked";
21393
+ state.blocker = invalid;
21394
+ checkpoint?.(state);
21395
+ return { exitCode: 64, reason: invalid, workflowState: state };
21396
+ }
21397
+ return { exitCode: 64, reason: invalid };
21398
+ }
21399
+ if (isGraphWorkflow(workflow)) return runGraphCapabilityWorkflow(parent, workflow, capability, base, checkpoint);
21400
+ return runLinearCapabilityWorkflow(parent, workflow, capability, base);
21401
+ }
21402
+ async function runLinearCapabilityWorkflow(parent, workflow, capability, base) {
20789
21403
  let chainData = {
20790
21404
  ...base.preloadedData ?? {},
20791
21405
  runSubjectType: "workflow",
@@ -20849,6 +21463,200 @@ async function runCapabilityWorkflow(parent, workflow, capability, base) {
20849
21463
  }
20850
21464
  return withWorkflowBoundaryEval(capability, result);
20851
21465
  }
21466
+ function isGraphWorkflow(workflow) {
21467
+ return workflow.startAt !== void 0 || workflow.steps.some((step) => step.id !== void 0 || step.next !== void 0 || step.inputs !== void 0);
21468
+ }
21469
+ function workflowError(workflow, base) {
21470
+ const projectCapabilitiesRoot = path46.join(base.cwd, ".kody", "capabilities");
21471
+ const knownCapabilities = /* @__PURE__ */ new Set();
21472
+ const capabilityInputs = /* @__PURE__ */ new Map();
21473
+ for (const step of workflow.steps) {
21474
+ const action = step.action ?? step.capability;
21475
+ const resolvedAction = resolveCapabilityAction(action, projectCapabilitiesRoot);
21476
+ const resolvedFolder = resolveCapabilityFolder(step.capability, projectCapabilitiesRoot);
21477
+ if (!resolvedAction && !resolvedFolder) continue;
21478
+ knownCapabilities.add(step.capability);
21479
+ const inputs = getCapabilityActionInputs(action, projectCapabilitiesRoot);
21480
+ if (inputs) {
21481
+ capabilityInputs.set(
21482
+ step.capability,
21483
+ new Set(inputs.flatMap((input) => [input.name, input.flag.replace(/^--/, "")]))
21484
+ );
21485
+ }
21486
+ }
21487
+ return formatWorkflowValidationIssues(validateWorkflow(workflow, { knownCapabilities, capabilityInputs }))[0] ?? null;
21488
+ }
21489
+ function initialWorkflowState(parent, workflow) {
21490
+ const prior = parent.workflowState;
21491
+ if (prior?.status === "done") {
21492
+ return {
21493
+ ...prior,
21494
+ status: "done",
21495
+ completedStepIds: [...prior.completedStepIds],
21496
+ transitionCounts: { ...prior.transitionCounts },
21497
+ facts: { ...prior.facts },
21498
+ evidence: { ...prior.evidence },
21499
+ artifacts: prior.artifacts.map((artifact) => ({ ...artifact }))
21500
+ };
21501
+ }
21502
+ const firstStepId = workflow.startAt ?? workflow.steps[0]?.id;
21503
+ const currentStepId = prior?.currentStepId ?? firstStepId;
21504
+ return {
21505
+ status: "running",
21506
+ ...currentStepId ? { currentStepId } : {},
21507
+ completedStepIds: [...prior?.completedStepIds ?? []],
21508
+ transitionCounts: { ...prior?.transitionCounts ?? {} },
21509
+ facts: { ...parent.workflowFacts ?? {}, ...prior?.facts ?? {} },
21510
+ evidence: { ...prior?.evidence ?? {} },
21511
+ artifacts: (prior?.artifacts ?? []).map((artifact) => ({ ...artifact }))
21512
+ };
21513
+ }
21514
+ function workflowChainData(parent, capability, base, state) {
21515
+ return {
21516
+ ...base.preloadedData ?? {},
21517
+ runSubjectType: "workflow",
21518
+ runSubjectId: capability.slug,
21519
+ runSubjectLabel: capability.title,
21520
+ runSubjectWorkflow: capability.slug,
21521
+ workflowCapability: capability.slug,
21522
+ workflowTitle: capability.title,
21523
+ workflowStepCount: capability.config.workflow?.steps.length ?? 0,
21524
+ workflowIssueNumber: workflowIssueNumber(parent),
21525
+ workflowFacts: state.facts,
21526
+ workflowEvidence: state.evidence,
21527
+ workflowArtifacts: state.artifacts,
21528
+ workflowStack: [
21529
+ ...Array.isArray(base.preloadedData?.workflowStack) ? base.preloadedData.workflowStack.filter((entry) => typeof entry === "string") : [],
21530
+ capability.slug
21531
+ ]
21532
+ };
21533
+ }
21534
+ async function runGraphCapabilityWorkflow(parent, workflow, capability, base, checkpoint) {
21535
+ const state = initialWorkflowState(parent, workflow);
21536
+ let chainData = workflowChainData(parent, capability, base, state);
21537
+ let result = { exitCode: 0 };
21538
+ let executedSteps = 0;
21539
+ const maxExecutedSteps = 1e3;
21540
+ while (state.currentStepId) {
21541
+ executedSteps += 1;
21542
+ if (executedSteps > maxExecutedSteps) {
21543
+ const reason = `workflow ${capability.slug} exceeded ${maxExecutedSteps} executed steps`;
21544
+ state.status = "blocked";
21545
+ state.blocker = reason;
21546
+ checkpoint?.(state);
21547
+ return { ...result, exitCode: 64, reason, workflowState: state };
21548
+ }
21549
+ const index = workflow.steps.findIndex((step2) => step2.id === state.currentStepId);
21550
+ const step = workflow.steps[index];
21551
+ if (!step) {
21552
+ const reason = `workflow ${capability.slug} current step ${state.currentStepId} is missing`;
21553
+ state.status = "blocked";
21554
+ state.blocker = reason;
21555
+ checkpoint?.(state);
21556
+ return { ...result, exitCode: 64, reason, workflowState: state };
21557
+ }
21558
+ const label = step.action ?? step.capability;
21559
+ checkpoint?.(state);
21560
+ let child;
21561
+ try {
21562
+ child = workflowStepToJob(step, parent, chainData);
21563
+ } catch (error) {
21564
+ const reason = error instanceof Error ? error.message : String(error);
21565
+ state.status = "blocked";
21566
+ state.blocker = reason;
21567
+ checkpoint?.(state);
21568
+ return { exitCode: 64, reason, workflowState: state };
21569
+ }
21570
+ process.stdout.write(
21571
+ `\u2192 kody: workflow ${capability.slug} step ${index + 1}/${workflow.steps.length} \u2192 ${label}
21572
+
21573
+ `
21574
+ );
21575
+ result = await runJob(child, {
21576
+ ...base,
21577
+ preloadedData: {
21578
+ ...chainData,
21579
+ workflowStep: step.id,
21580
+ workflowStepIndex: index + 1,
21581
+ workflowStepReason: step.reason,
21582
+ workflowContinueOn: step.continueOn ?? []
21583
+ }
21584
+ });
21585
+ mergeWorkflowResults(state, result.capabilityResults);
21586
+ const outcome = workflowOutcome(result);
21587
+ const prUrl = result.prUrl ?? result.taskState?.core.prUrl ?? (typeof chainData.workflowPrUrl === "string" ? chainData.workflowPrUrl : void 0);
21588
+ chainData = {
21589
+ ...workflowChainData(parent, capability, base, state),
21590
+ ...result.taskState ? { taskState: result.taskState } : {},
21591
+ ...outcome ? { workflowLastOutcome: outcome } : {},
21592
+ ...result.capabilityResults?.at(-1) ? { workflowLastResult: result.capabilityResults.at(-1) } : {},
21593
+ ...prUrl ? { workflowPrUrl: prUrl } : {},
21594
+ ...parsePrNumber5(prUrl) ? { workflowPrNumber: parsePrNumber5(prUrl) } : {}
21595
+ };
21596
+ if (!state.completedStepIds.includes(step.id)) state.completedStepIds.push(step.id);
21597
+ if (result.exitCode !== 0 && !canContinueWorkflow(step, outcome)) {
21598
+ state.status = "failed";
21599
+ state.blocker = result.reason ?? `workflow step ${step.id} failed`;
21600
+ checkpoint?.(state);
21601
+ return withWorkflowBoundaryEval(capability, { ...result, workflowState: state });
21602
+ }
21603
+ if (!step.next || step.next.length === 0) {
21604
+ state.status = "done";
21605
+ delete state.currentStepId;
21606
+ delete state.blocker;
21607
+ checkpoint?.(state);
21608
+ return withWorkflowBoundaryEval(capability, { ...result, workflowState: state });
21609
+ }
21610
+ const transition = selectWorkflowTransition(step, chainData, state.transitionCounts);
21611
+ if (!transition) {
21612
+ const reason = `workflow step ${step.id} has no available connection`;
21613
+ state.status = "blocked";
21614
+ state.blocker = reason;
21615
+ checkpoint?.(state);
21616
+ return { ...result, exitCode: 64, reason, workflowState: state };
21617
+ }
21618
+ if (transition.maxIterations !== void 0) {
21619
+ const key = `${step.id}->${transition.to}`;
21620
+ state.transitionCounts[key] = (state.transitionCounts[key] ?? 0) + 1;
21621
+ }
21622
+ state.currentStepId = transition.to;
21623
+ state.status = "running";
21624
+ delete state.blocker;
21625
+ checkpoint?.(state);
21626
+ }
21627
+ state.status = "done";
21628
+ checkpoint?.(state);
21629
+ return withWorkflowBoundaryEval(capability, { ...result, workflowState: state });
21630
+ }
21631
+ function mergeWorkflowResults(state, results) {
21632
+ for (const result of results ?? []) {
21633
+ Object.assign(state.facts, result.facts);
21634
+ Object.assign(state.evidence, result.evidence ?? {});
21635
+ for (const artifact of result.artifacts) {
21636
+ if (!state.artifacts.some(
21637
+ (existing) => existing.label === artifact.label && existing.url === artifact.url && existing.path === artifact.path
21638
+ )) {
21639
+ state.artifacts.push({ ...artifact });
21640
+ }
21641
+ }
21642
+ }
21643
+ }
21644
+ function selectWorkflowTransition(step, data, counts) {
21645
+ let fallback = null;
21646
+ for (const transition of step.next ?? []) {
21647
+ const key = `${step.id}->${transition.to}`;
21648
+ if (transition.maxIterations !== void 0 && (counts[key] ?? 0) >= transition.maxIterations) continue;
21649
+ if (transition.default === true) {
21650
+ fallback ??= transition;
21651
+ continue;
21652
+ }
21653
+ if (!transition.when || conditionMatches(transition.when, workflowConditionContext(data))) return transition;
21654
+ }
21655
+ return fallback;
21656
+ }
21657
+ function conditionMatches(condition, context) {
21658
+ return Object.entries(condition).every(([path52, expected]) => valueMatches(resolveDottedPath2(context, path52), expected));
21659
+ }
20852
21660
  function withWorkflowBoundaryEval(capability, result) {
20853
21661
  const capabilityKind = capability.config.capabilityKind;
20854
21662
  if (!capabilityKind) return result;
@@ -20869,8 +21677,18 @@ function withWorkflowBoundaryEval(capability, result) {
20869
21677
  }
20870
21678
  function workflowStepToJob(step, parent, chainData) {
20871
21679
  const action = step.action ?? step.capability;
21680
+ const mappedArgs = {};
21681
+ const conditionContext = workflowConditionContext(chainData);
21682
+ for (const [name, mapping] of Object.entries(step.inputs ?? {})) {
21683
+ const value = resolveDottedPath2(conditionContext, mapping.from);
21684
+ if (value === void 0) {
21685
+ throw new InvalidJobError(`workflow step ${step.id ?? action} needs missing input ${mapping.from}`);
21686
+ }
21687
+ mappedArgs[name] = value;
21688
+ }
20872
21689
  const rawArgs = {
20873
21690
  ...parent.cliArgs,
21691
+ ...mappedArgs,
20874
21692
  ...step.cliArgs ?? {}
20875
21693
  };
20876
21694
  const targetNumber = workflowStepTargetNumber(step, parent, chainData);
@@ -20904,9 +21722,7 @@ function workflowStepToJob(step, parent, chainData) {
20904
21722
  function shouldRunWorkflowStep(step, data) {
20905
21723
  if (!step.runWhen) return true;
20906
21724
  const context = workflowConditionContext(data);
20907
- return Object.entries(step.runWhen).every(
20908
- ([path51, expected]) => valueMatches(resolveDottedPath2(context, path51), expected)
20909
- );
21725
+ return conditionMatches(step.runWhen, context);
20910
21726
  }
20911
21727
  function canContinueWorkflow(step, outcome) {
20912
21728
  if (!outcome || !step.continueOn || step.continueOn.length === 0) return false;
@@ -20917,10 +21733,16 @@ function workflowOutcome(result) {
20917
21733
  }
20918
21734
  function workflowConditionContext(data) {
20919
21735
  const lastOutcome = data.workflowLastOutcome;
21736
+ const lastResult = data.workflowLastResult;
20920
21737
  return {
20921
21738
  ...data,
21739
+ facts: data.workflowFacts ?? {},
21740
+ evidence: data.workflowEvidence ?? {},
21741
+ artifacts: data.workflowArtifacts ?? [],
21742
+ result: lastResult,
20922
21743
  workflow: {
20923
21744
  lastOutcome,
21745
+ lastResult,
20924
21746
  issueNumber: data.workflowIssueNumber,
20925
21747
  prNumber: data.workflowPrNumber,
20926
21748
  prUrl: data.workflowPrUrl
@@ -20988,7 +21810,7 @@ function composeStepWhy(parentWhy, step) {
20988
21810
  }
20989
21811
  function loadCapabilityContext(slug, cwd) {
20990
21812
  if (!slug) return null;
20991
- return resolveCapabilityFolder(slug, path45.join(cwd, ".kody", "capabilities"));
21813
+ return resolveCapabilityFolder(slug, path46.join(cwd, ".kody", "capabilities"));
20992
21814
  }
20993
21815
  function loadWorkflowContext(slug, base) {
20994
21816
  if (!slug || !base.config || !isWorkflowDefinitionId(slug)) return null;
@@ -21027,6 +21849,8 @@ var init_job = __esm({
21027
21849
  init_executor();
21028
21850
  init_registry();
21029
21851
  init_workflowDefinitions();
21852
+ init_workflowRunState();
21853
+ init_workflowValidation();
21030
21854
  init_jobIdentity();
21031
21855
  init_jobIdentity();
21032
21856
  DEFAULT_INSTANT_AGENT = "kody";
@@ -21149,7 +21973,7 @@ function translateOpenAISseToBrain(opts) {
21149
21973
  // src/servers/brain-serve.ts
21150
21974
  import * as fs49 from "fs";
21151
21975
  import { createServer as createServer2 } from "http";
21152
- import * as path48 from "path";
21976
+ import * as path49 from "path";
21153
21977
 
21154
21978
  // src/chat/loop.ts
21155
21979
  init_agent();
@@ -21679,8 +22503,8 @@ async function runOpenAIChatTurn(args) {
21679
22503
  })
21680
22504
  });
21681
22505
  if (!response.ok) {
21682
- const text = await response.text().catch(() => "");
21683
- const error = `OpenAI-compatible model request failed ${response.status}${text ? `: ${text.slice(0, 500)}` : ""}`;
22506
+ const text2 = await response.text().catch(() => "");
22507
+ const error = `OpenAI-compatible model request failed ${response.status}${text2 ? `: ${text2.slice(0, 500)}` : ""}`;
21684
22508
  await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
21685
22509
  return { exitCode: 99, error };
21686
22510
  }
@@ -21718,8 +22542,8 @@ function extractOpenAIReply(payload) {
21718
22542
  return content.map((part) => {
21719
22543
  if (typeof part === "string") return part;
21720
22544
  if (part && typeof part === "object" && "text" in part) {
21721
- const text = part.text;
21722
- return typeof text === "string" ? text : "";
22545
+ const text2 = part.text;
22546
+ return typeof text2 === "string" ? text2 : "";
21723
22547
  }
21724
22548
  return "";
21725
22549
  }).join("");
@@ -21837,7 +22661,7 @@ init_config();
21837
22661
  // src/kody-cli.ts
21838
22662
  import { execFileSync as execFileSync26 } from "child_process";
21839
22663
  import * as fs47 from "fs";
21840
- import * as path46 from "path";
22664
+ import * as path47 from "path";
21841
22665
 
21842
22666
  // src/app-auth.ts
21843
22667
  import { createSign } from "crypto";
@@ -22108,7 +22932,7 @@ function autoDispatch(opts) {
22108
22932
  }
22109
22933
  if (eventName !== "issue_comment") return null;
22110
22934
  const comment = objectValue(event.comment);
22111
- const issue = objectValue(event.issue);
22935
+ const issue2 = objectValue(event.issue);
22112
22936
  const user = objectValue(comment?.user);
22113
22937
  const rawBody = String(comment?.body ?? "");
22114
22938
  const authorLogin = String(user?.login ?? "");
@@ -22116,9 +22940,9 @@ function autoDispatch(opts) {
22116
22940
  if (!hasKodyMention(rawBody)) return null;
22117
22941
  const isBotAuthor = authorLogin === "kody-bot" || authorType === "Bot";
22118
22942
  if (!associationAllowed(event, opts?.config)) return null;
22119
- const body = rawBody.toLowerCase();
22120
- const targetNum = Number(issue?.number ?? 0);
22121
- const isPr = !!issue?.pull_request;
22943
+ const body = rawBody;
22944
+ const targetNum = Number(issue2?.number ?? 0);
22945
+ const isPr = !!issue2?.pull_request;
22122
22946
  if (!targetNum) return null;
22123
22947
  const afterTag = extractAfterTag(body);
22124
22948
  const firstTokenRaw = extractSubcommand(afterTag);
@@ -22202,7 +23026,7 @@ function autoDispatchTyped(opts) {
22202
23026
  return { kind: "silent", reason: "GHA event payload unreadable" };
22203
23027
  }
22204
23028
  const comment = objectValue(event.comment);
22205
- const issue = objectValue(event.issue);
23029
+ const issue2 = objectValue(event.issue);
22206
23030
  const user = objectValue(comment?.user);
22207
23031
  const rawBody = String(comment?.body ?? "");
22208
23032
  const authorLogin = String(user?.login ?? "");
@@ -22210,12 +23034,12 @@ function autoDispatchTyped(opts) {
22210
23034
  if (!hasKodyMention(rawBody)) {
22211
23035
  return { kind: "silent", reason: "comment does not mention @kody" };
22212
23036
  }
22213
- const targetNum = Number(issue?.number ?? 0);
22214
- const isPr = !!issue?.pull_request;
23037
+ const targetNum = Number(issue2?.number ?? 0);
23038
+ const isPr = !!issue2?.pull_request;
22215
23039
  if (!targetNum) {
22216
23040
  return { kind: "silent", reason: "comment has no associated issue/PR number" };
22217
23041
  }
22218
- const afterTag = extractAfterTag(rawBody.toLowerCase());
23042
+ const afterTag = extractAfterTag(rawBody);
22219
23043
  const tokenRaw = extractSubcommand(afterTag) ?? "";
22220
23044
  if ((authorLogin === "kody-bot" || authorType === "Bot") && tokenRaw && !POLITE_WORDS.has(tokenRaw)) {
22221
23045
  return {
@@ -22315,12 +23139,12 @@ function hasKodyMention(body) {
22315
23139
  function extractAfterTag(body) {
22316
23140
  const m = body.match(KODY_MENTION_RE);
22317
23141
  if (!m || m.index === void 0) return "";
22318
- const at = body.indexOf("@kody", m.index);
23142
+ const at = body.toLowerCase().indexOf("@kody", m.index);
22319
23143
  return body.slice(at + "@kody".length).trim();
22320
23144
  }
22321
23145
  function extractSubcommand(afterTag) {
22322
- const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
22323
- return match ? match[1] : null;
23146
+ const match = afterTag.match(/^([a-zA-Z][a-zA-Z0-9-]{1,40})\b/);
23147
+ return match ? match[1].toLowerCase() : null;
22324
23148
  }
22325
23149
  function extractCommentRest(afterTag, consumedToken) {
22326
23150
  let rest = afterTag;
@@ -22339,7 +23163,7 @@ function parseCommentArgs(rest, inputs) {
22339
23163
  const t = tokens[i];
22340
23164
  if (t.startsWith("--")) {
22341
23165
  const eq = t.indexOf("=");
22342
- const key = eq >= 0 ? t.slice(2, eq) : t.slice(2);
23166
+ const key = (eq >= 0 ? t.slice(2, eq) : t.slice(2)).toLowerCase();
22343
23167
  const inlineValue = eq >= 0 ? t.slice(eq + 1) : void 0;
22344
23168
  const spec = findInputByFlag(inputs, key);
22345
23169
  if (!spec) {
@@ -22359,9 +23183,12 @@ function parseCommentArgs(rest, inputs) {
22359
23183
  if (inlineValue === void 0) i++;
22360
23184
  continue;
22361
23185
  }
22362
- const enumHit = inputs.find((s) => s.type === "enum" && s.values?.includes(t) && args[s.name] === void 0);
23186
+ const tLower = t.toLowerCase();
23187
+ const enumHit = inputs.find(
23188
+ (s) => s.type === "enum" && s.values?.some((v) => v.toLowerCase() === tLower) && args[s.name] === void 0
23189
+ );
22363
23190
  if (enumHit) {
22364
- args[enumHit.name] = t;
23191
+ args[enumHit.name] = enumHit.values.find((v) => v.toLowerCase() === tLower);
22365
23192
  continue;
22366
23193
  }
22367
23194
  if (/^-?\d+$/.test(t)) {
@@ -22371,7 +23198,7 @@ function parseCommentArgs(rest, inputs) {
22371
23198
  continue;
22372
23199
  }
22373
23200
  }
22374
- const boolHit = inputs.find((s) => s.type === "bool" && s.flag === `--${t}` && args[s.name] === void 0);
23201
+ const boolHit = inputs.find((s) => s.type === "bool" && s.flag === `--${tLower}` && args[s.name] === void 0);
22375
23202
  if (boolHit) {
22376
23203
  args[boolHit.name] = true;
22377
23204
  continue;
@@ -22552,7 +23379,8 @@ function routeRunRequest(request) {
22552
23379
  if (intent !== "run" && intent !== "tick") {
22553
23380
  return { kind: "error", error: `workflow target does not support intent '${intent}'` };
22554
23381
  }
22555
- return { kind: "action", action: target.id, cliArgs: {} };
23382
+ const workflowRunId = typeof request.input?.runId === "string" && /^[a-z0-9][a-z0-9_-]{0,79}$/.test(request.input.runId) ? request.input.runId : void 0;
23383
+ return { kind: "action", action: target.id, cliArgs: {}, ...workflowRunId ? { workflowRunId } : {} };
22556
23384
  }
22557
23385
  return { kind: "error", error: "unsupported run request target" };
22558
23386
  }
@@ -22631,9 +23459,9 @@ async function resolveAuthToken(env = process.env) {
22631
23459
  return void 0;
22632
23460
  }
22633
23461
  function detectPackageManager2(cwd) {
22634
- if (fs47.existsSync(path46.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
22635
- if (fs47.existsSync(path46.join(cwd, "yarn.lock"))) return "yarn";
22636
- if (fs47.existsSync(path46.join(cwd, "bun.lockb"))) return "bun";
23462
+ if (fs47.existsSync(path47.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
23463
+ if (fs47.existsSync(path47.join(cwd, "yarn.lock"))) return "yarn";
23464
+ if (fs47.existsSync(path47.join(cwd, "bun.lockb"))) return "bun";
22637
23465
  return "npm";
22638
23466
  }
22639
23467
  function shouldChainScheduledWatch(match) {
@@ -22756,7 +23584,7 @@ async function runCi(argv) {
22756
23584
  return 0;
22757
23585
  }
22758
23586
  const args = parseCiArgs(argv);
22759
- const cwd = args.cwd ? path46.resolve(args.cwd) : process.cwd();
23587
+ const cwd = args.cwd ? path47.resolve(args.cwd) : process.cwd();
22760
23588
  try {
22761
23589
  const n = unpackAllSecrets();
22762
23590
  if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s) from ALL_SECRETS
@@ -22780,6 +23608,7 @@ async function runCi(argv) {
22780
23608
  let manualWorkflowDispatch = false;
22781
23609
  let forceRunAction = null;
22782
23610
  let forceRunCliArgs = {};
23611
+ let forceWorkflowRunId;
22783
23612
  let runRequestFanOut = false;
22784
23613
  let runRequestFanOutForce = false;
22785
23614
  const parsedRunRequest = readRunRequestFromEnv();
@@ -22804,6 +23633,7 @@ async function runCi(argv) {
22804
23633
  } else if (route.kind === "action") {
22805
23634
  forceRunAction = route.action;
22806
23635
  forceRunCliArgs = route.cliArgs;
23636
+ forceWorkflowRunId = route.workflowRunId;
22807
23637
  }
22808
23638
  }
22809
23639
  const envForceAction = (process.env.KODY_FORCE_ACTION ?? "").trim();
@@ -22901,6 +23731,7 @@ async function runCi(argv) {
22901
23731
  capability: route.capability,
22902
23732
  workflow: route.workflow,
22903
23733
  implementation: route.implementation,
23734
+ workflowRunId: forceWorkflowRunId,
22904
23735
  cliArgs: { ...route.cliArgs, ...forceRunCliArgs },
22905
23736
  flavor: "instant",
22906
23737
  force: true
@@ -23190,7 +24021,7 @@ init_repoWorkspace();
23190
24021
  // src/scripts/brainTurnLog.ts
23191
24022
  init_runtimePaths();
23192
24023
  import * as fs48 from "fs";
23193
- import * as path47 from "path";
24024
+ import * as path48 from "path";
23194
24025
  import posixPath4 from "path/posix";
23195
24026
  var live = /* @__PURE__ */ new Map();
23196
24027
  function brainEventsFilePath(dir, chatId) {
@@ -23240,7 +24071,7 @@ function beginTurn(dir, chatId) {
23240
24071
  };
23241
24072
  live.set(chatId, state);
23242
24073
  const p = brainEventsFilePath(dir, chatId);
23243
- fs48.mkdirSync(path47.dirname(p), { recursive: true });
24074
+ fs48.mkdirSync(path48.dirname(p), { recursive: true });
23244
24075
  return (event) => {
23245
24076
  state.seq += 1;
23246
24077
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -23618,7 +24449,7 @@ async function handleChatTurn(req, res, chatId, opts) {
23618
24449
  );
23619
24450
  }
23620
24451
  }
23621
- fs49.mkdirSync(path48.dirname(sessionFile), { recursive: true });
24452
+ fs49.mkdirSync(path49.dirname(sessionFile), { recursive: true });
23622
24453
  appendTurn(sessionFile, {
23623
24454
  role: "user",
23624
24455
  content: message,
@@ -23693,7 +24524,7 @@ async function handleChatTurn(req, res, chatId, opts) {
23693
24524
  function buildServer(opts) {
23694
24525
  const runTurn = opts.runTurn ?? runChatTurn;
23695
24526
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
23696
- const reposRoot = opts.reposRoot ?? path48.join(path48.dirname(path48.resolve(opts.cwd)), "repos");
24527
+ const reposRoot = opts.reposRoot ?? path49.join(path49.dirname(path49.resolve(opts.cwd)), "repos");
23697
24528
  return createServer2(async (req, res) => {
23698
24529
  if (!req.method || !req.url) {
23699
24530
  sendJson(res, 400, { error: "bad request" });
@@ -24297,7 +25128,7 @@ async function loadConfigSafe() {
24297
25128
 
24298
25129
  // src/chat-cli.ts
24299
25130
  import * as fs51 from "fs";
24300
- import * as path50 from "path";
25131
+ import * as path51 from "path";
24301
25132
 
24302
25133
  // src/chat/inbox.ts
24303
25134
  import { execFileSync as execFileSync27 } from "child_process";
@@ -24370,9 +25201,9 @@ function currentBranch(cwd) {
24370
25201
  // src/chat/state-sync.ts
24371
25202
  init_stateRepo();
24372
25203
  import * as fs50 from "fs";
24373
- import * as path49 from "path";
24374
- function jsonlLines2(text) {
24375
- return text.split("\n").filter((line) => line.length > 0);
25204
+ import * as path50 from "path";
25205
+ function jsonlLines2(text2) {
25206
+ return text2.split("\n").filter((line) => line.length > 0);
24376
25207
  }
24377
25208
  function renderJsonl2(lines) {
24378
25209
  return lines.length > 0 ? `${lines.join("\n")}
@@ -24390,7 +25221,7 @@ function syncJsonlFileFromState(opts) {
24390
25221
  const local = fs50.existsSync(opts.localPath) ? fs50.readFileSync(opts.localPath, "utf-8") : "";
24391
25222
  const next = mergeJsonl2(local, remote.content);
24392
25223
  if (next === local) return;
24393
- fs50.mkdirSync(path49.dirname(opts.localPath), { recursive: true });
25224
+ fs50.mkdirSync(path50.dirname(opts.localPath), { recursive: true });
24394
25225
  fs50.writeFileSync(opts.localPath, next);
24395
25226
  }
24396
25227
  function persistJsonlFileToState(opts) {
@@ -24660,7 +25491,7 @@ async function runChat(argv) {
24660
25491
  ${CHAT_HELP}`);
24661
25492
  return 64;
24662
25493
  }
24663
- const cwd = args.cwd ? path50.resolve(args.cwd) : process.cwd();
25494
+ const cwd = args.cwd ? path51.resolve(args.cwd) : process.cwd();
24664
25495
  const sessionId = args.sessionId;
24665
25496
  const runRequest = readRunRequestFromEnv();
24666
25497
  if (runRequest && "request" in runRequest) {
@@ -24840,8 +25671,8 @@ var FlyClient = class {
24840
25671
  get fetch() {
24841
25672
  return this.opts.fetchImpl ?? fetch;
24842
25673
  }
24843
- async call(path51, init = {}) {
24844
- const res = await this.fetch(`${FLY_API_BASE}${path51}`, {
25674
+ async call(path52, init = {}) {
25675
+ const res = await this.fetch(`${FLY_API_BASE}${path52}`, {
24845
25676
  method: init.method ?? "GET",
24846
25677
  headers: {
24847
25678
  Authorization: `Bearer ${this.opts.token}`,
@@ -24851,8 +25682,8 @@ var FlyClient = class {
24851
25682
  });
24852
25683
  if (res.status === 404 && init.allow404) return null;
24853
25684
  if (!res.ok) {
24854
- const text = await res.text().catch(() => "");
24855
- throw new Error(`Fly API ${res.status} on ${path51}: ${text.slice(0, 200) || res.statusText}`);
25685
+ const text2 = await res.text().catch(() => "");
25686
+ throw new Error(`Fly API ${res.status} on ${path52}: ${text2.slice(0, 200) || res.statusText}`);
24856
25687
  }
24857
25688
  if (res.status === 204) return null;
24858
25689
  const raw = await res.text();
@@ -26199,11 +27030,11 @@ function envRunMode(env = process.env) {
26199
27030
  return { ...result, command: "ci", ciArgv: [] };
26200
27031
  }
26201
27032
  if (mode === "issue") {
26202
- const issue = (env.ISSUE_NUMBER ?? "").trim();
26203
- if (!issue) {
27033
+ const issue2 = (env.ISSUE_NUMBER ?? "").trim();
27034
+ if (!issue2) {
26204
27035
  return { ...result, errors: ["KODY_RUN_MODE=issue requires ISSUE_NUMBER"] };
26205
27036
  }
26206
- return { ...result, command: "ci", ciArgv: ["--issue", issue] };
27037
+ return { ...result, command: "ci", ciArgv: ["--issue", issue2] };
26207
27038
  }
26208
27039
  return { ...result, errors: [`unknown KODY_RUN_MODE: ${mode}`] };
26209
27040
  }