@mrciphersmith/keryx 0.3.58 → 0.3.61

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/core.js CHANGED
@@ -9801,7 +9801,7 @@ var init_skill_length_ceilings = __esm(() => {
9801
9801
  "orchestration/context-collector": 671,
9802
9802
  "orchestration/feature-analyzer": 447,
9803
9803
  "orchestration/feature-dev": 177,
9804
- "orchestration/flow-orchestrator": 696,
9804
+ "orchestration/flow-orchestrator": 694,
9805
9805
  "orchestration/issue-analyzer": 373,
9806
9806
  "orchestration/job-documenter": 414,
9807
9807
  "orchestration/job-orchestrator": 2243,
@@ -24100,6 +24100,8 @@ __export(exports_service2, {
24100
24100
  AC_KINDS: () => AC_KINDS,
24101
24101
  CONFIRMATION_CAVEAT: () => CONFIRMATION_CAVEAT,
24102
24102
  DEFAULT_OUTCOME_AUTHOR: () => DEFAULT_OUTCOME_AUTHOR,
24103
+ ORIGIN_KINDS: () => ORIGIN_KINDS,
24104
+ ORIGIN_READINGS: () => ORIGIN_READINGS,
24103
24105
  OUTCOME_AUTHORS: () => OUTCOME_AUTHORS,
24104
24106
  OUTCOME_AUTHOR_READINGS: () => OUTCOME_AUTHOR_READINGS,
24105
24107
  OUTCOME_HINT: () => OUTCOME_HINT,
@@ -24119,6 +24121,7 @@ __export(exports_service2, {
24119
24121
  confirmPreconditionError: () => confirmPreconditionError,
24120
24122
  createFlowService: () => createFlowService,
24121
24123
  describeAcKind: () => describeAcKind,
24124
+ effectiveOutcomeAuthor: () => effectiveOutcomeAuthor,
24122
24125
  evaluatedVerdict: () => evaluatedVerdict,
24123
24126
  extractCriterionTokens: () => extractCriterionTokens,
24124
24127
  factsOnlyVerdict: () => factsOnlyVerdict,
@@ -24128,7 +24131,10 @@ __export(exports_service2, {
24128
24131
  hashDiff: () => hashDiff,
24129
24132
  intentNoteForNewFlow: () => intentNoteForNewFlow,
24130
24133
  isFrozen: () => isFrozen,
24134
+ isOriginKind: () => isOriginKind,
24131
24135
  notCheckableVerdict: () => notCheckableVerdict,
24136
+ originDetailLines: () => originDetailLines,
24137
+ originSummary: () => originSummary,
24132
24138
  outcomeBulletsFrom: () => outcomeBulletsFrom,
24133
24139
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
24134
24140
  parseOutcomeAuthor: () => parseOutcomeAuthor,
@@ -24137,12 +24143,15 @@ __export(exports_service2, {
24137
24143
  readAcCheckEnabled: () => readAcCheckEnabled,
24138
24144
  readAcKindRecords: () => readAcKindRecords,
24139
24145
  readFlow: () => readFlow,
24146
+ readOrigin: () => readOrigin,
24147
+ readOriginKind: () => readOriginKind,
24140
24148
  readOutcomeAuthor: () => readOutcomeAuthor,
24141
24149
  renderAcCheckAdvisoryNotice: () => renderAcCheckAdvisoryNotice,
24142
24150
  renderAcCheckReport: () => renderAcCheckReport,
24143
24151
  renderAcKindDistribution: () => renderAcKindDistribution,
24144
24152
  reportFromRecords: () => reportFromRecords,
24145
24153
  resolveFlowDir: () => resolveFlowDir,
24154
+ resolveOrigin: () => resolveOrigin,
24146
24155
  sectionOf: () => sectionOf,
24147
24156
  selectMatchedHunks: () => selectMatchedHunks,
24148
24157
  statementFrom: () => statementFrom,
@@ -24750,7 +24759,7 @@ function unansweredComments(state) {
24750
24759
  // src/flow/review-gate.ts
24751
24760
  var BLOCKING_SEVERITY_FLOORS = ["blocker", "major", "minor"];
24752
24761
  var REVIEW_GATE_SEVERITY_FLOOR_DEFAULT = "minor";
24753
- var REVIEW_ROUND_CAP = 3;
24762
+ var REVIEW_ROUND_CAP = 5;
24754
24763
  var REVIEW_GATE_CONFIG_PATH = ".metaproject/tasks.config.json";
24755
24764
  var DEFAULT_REVIEW_GATE_CONFIG = {
24756
24765
  severityFloor: REVIEW_GATE_SEVERITY_FLOOR_DEFAULT,
@@ -25613,6 +25622,129 @@ function parseOutcomeAuthor(raw, flag = "--outcome-author") {
25613
25622
  const shown = typeof raw === "string" ? `"${raw}"` : "nothing";
25614
25623
  throw new Error(`${flag} must be one of: ${OUTCOME_AUTHORS.join(", ")} (got ${shown}).`);
25615
25624
  }
25625
+
25626
+ // src/flow/origin.ts
25627
+ var ORIGIN_KINDS = ["human-request", "agent-finding", "agent-proposal"];
25628
+ var ORIGIN_READINGS = [...ORIGIN_KINDS, "unknown"];
25629
+ function isOriginKind(value) {
25630
+ return typeof value === "string" && ORIGIN_KINDS.includes(value);
25631
+ }
25632
+ function nonBlank(value) {
25633
+ return typeof value === "string" && value.trim().length > 0;
25634
+ }
25635
+ function readOrigin(value) {
25636
+ if (value === null || typeof value !== "object" || Array.isArray(value))
25637
+ return;
25638
+ const raw = value;
25639
+ if (!isOriginKind(raw.kind))
25640
+ return;
25641
+ const origin = { kind: raw.kind };
25642
+ if (typeof raw.quote === "string" && raw.quote.length > 0)
25643
+ origin.quote = raw.quote;
25644
+ if (typeof raw.source === "string" && raw.source.length > 0)
25645
+ origin.source = raw.source;
25646
+ return origin;
25647
+ }
25648
+ function readOriginKind(value) {
25649
+ return readOrigin(value)?.kind ?? "unknown";
25650
+ }
25651
+ function resolveOrigin(input) {
25652
+ if (!isOriginKind(input.kind)) {
25653
+ return {
25654
+ note: `origin kind "${input.kind}" is not one of: ${ORIGIN_KINDS.join(", ")}; origin stays unknown.`
25655
+ };
25656
+ }
25657
+ const hasQuote = nonBlank(input.quote);
25658
+ const hasSource = nonBlank(input.source);
25659
+ if (input.kind === "human-request") {
25660
+ if (!hasQuote && !hasSource) {
25661
+ return { note: "origin human-request needs a verbatim --quote and a --source; neither was given, origin stays unknown." };
25662
+ }
25663
+ if (!hasQuote) {
25664
+ return { note: "origin human-request needs a verbatim --quote of the human's first message; none was given, origin stays unknown." };
25665
+ }
25666
+ if (!hasSource) {
25667
+ return { note: "origin human-request needs a --source (channel, message id or time); none was given, origin stays unknown." };
25668
+ }
25669
+ } else if (!hasSource) {
25670
+ return { note: `origin ${input.kind} needs a --source (the check, review or test that produced it); none was given, origin stays unknown.` };
25671
+ }
25672
+ const origin = { kind: input.kind };
25673
+ if (hasQuote)
25674
+ origin.quote = input.quote;
25675
+ if (hasSource)
25676
+ origin.source = input.source;
25677
+ return { origin };
25678
+ }
25679
+ function effectiveOutcomeAuthor(flow) {
25680
+ const explicit = readOutcomeAuthor(flow.outcomeAuthor);
25681
+ if (explicit !== "unknown")
25682
+ return explicit;
25683
+ return readOrigin(flow.origin) === undefined ? "unknown" : "agent";
25684
+ }
25685
+ function originSummary(origin) {
25686
+ if (origin === undefined)
25687
+ return "unknown";
25688
+ const parts = [origin.kind];
25689
+ if (origin.quote !== undefined)
25690
+ parts.push(`quote: ${JSON.stringify(origin.quote)}`);
25691
+ if (origin.source !== undefined)
25692
+ parts.push(`source: ${origin.source}`);
25693
+ return parts.join("; ");
25694
+ }
25695
+ function originDetailLines(origin, indent = "") {
25696
+ if (origin === undefined)
25697
+ return [`${indent}origin: unknown`];
25698
+ const lines = [`${indent}origin: ${origin.kind}`];
25699
+ if (origin.quote !== undefined) {
25700
+ const [first = "", ...rest] = origin.quote.split(`
25701
+ `);
25702
+ lines.push(`${indent} quote: \xAB${first}`, ...rest.map((line) => `${indent} ${line}`));
25703
+ lines[lines.length - 1] += "\xBB";
25704
+ }
25705
+ if (origin.source !== undefined)
25706
+ lines.push(`${indent} source: ${origin.source}`);
25707
+ return lines;
25708
+ }
25709
+ var REQUEST_LABEL = "\u0417\u0430\u043F\u0440\u043E\u0441 (\u0434\u043E\u0441\u043B\u043E\u0432\u043D\u043E)";
25710
+ var SOURCE_LABEL = "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A";
25711
+ var FORMALIZATION_LABEL = "\u042D\u0444\u0444\u0435\u043A\u0442 (\u0444\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F \u0430\u0433\u0435\u043D\u0442\u0430)";
25712
+ var OBSERVE_LABEL = "\u041A\u0430\u043A \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0442\u044C (\u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0430\u0433\u0435\u043D\u0442\u0430)";
25713
+ var REQUEST_PLACEHOLDER = "<the human's request, verbatim, no translation or paraphrase>";
25714
+ var SOURCE_PLACEHOLDER = "<where the finding or proposal came from: check, review, test or discussion>";
25715
+ var FORMALIZATION_PLACEHOLDER = "<the effect that has to be true when this flow is done, in your own words>";
25716
+ var OBSERVE_PLACEHOLDER = "<how the effect can be observed once the flow is done>";
25717
+ var ORIGIN_PLACEHOLDER_BULLETS = [
25718
+ `- ${REQUEST_LABEL}: ${REQUEST_PLACEHOLDER}`,
25719
+ `- ${SOURCE_LABEL}: ${SOURCE_PLACEHOLDER}`,
25720
+ `- ${FORMALIZATION_LABEL}: ${FORMALIZATION_PLACEHOLDER}`,
25721
+ `- ${OBSERVE_LABEL}: ${OBSERVE_PLACEHOLDER}`
25722
+ ];
25723
+ function renderOriginBullets(origin) {
25724
+ const first = [];
25725
+ if (origin.kind === "human-request") {
25726
+ first.push(...bulletWithText(REQUEST_LABEL, origin.quote === undefined ? undefined : `\xAB${origin.quote}\xBB`, REQUEST_PLACEHOLDER));
25727
+ if (origin.source !== undefined)
25728
+ first[first.length - 1] += ` (source: ${origin.source})`;
25729
+ } else {
25730
+ first.push(...bulletWithText(SOURCE_LABEL, origin.source, SOURCE_PLACEHOLDER));
25731
+ if (origin.quote !== undefined)
25732
+ first.push(...bulletWithText(REQUEST_LABEL, `\xAB${origin.quote}\xBB`, REQUEST_PLACEHOLDER));
25733
+ }
25734
+ return [
25735
+ ...first,
25736
+ `- ${FORMALIZATION_LABEL}: ${FORMALIZATION_PLACEHOLDER}`,
25737
+ `- ${OBSERVE_LABEL}: ${OBSERVE_PLACEHOLDER}`
25738
+ ];
25739
+ }
25740
+ function bulletWithText(label, text, placeholder) {
25741
+ if (text === undefined)
25742
+ return [`- ${label}: ${placeholder}`];
25743
+ const lines = text.split(`
25744
+ `);
25745
+ const head = `- ${label}: ${lines[0] ?? ""}`;
25746
+ return [head, ...lines.slice(1).map((line) => ` ${line}`)];
25747
+ }
25616
25748
  // src/flow/identity.ts
25617
25749
  function resolveSignerIdentity(input) {
25618
25750
  const stated = input.stated?.trim();
@@ -25908,6 +26040,23 @@ function flowStateSchema() {
25908
26040
  enum: ["agent", "human"],
25909
26041
  description: "Who wrote the outcome criterion. Written by `flow init --outcome-author` (default `agent`; `human` only when the flag says so, never inferred) and changed only by `flow outcome author`, which journals the change. Absent on flows created before the field existed, and read as `unknown` (neither agent nor human). Gates nothing."
25910
26042
  },
26043
+ origin: {
26044
+ type: "object",
26045
+ additionalProperties: false,
26046
+ required: ["kind"],
26047
+ description: "Where the flow came from. Written by `flow init --origin <kind> [--quote] [--source]` and changed only by `flow origin set`, which journals the change. `human-request` is recorded only with a verbatim quote and a source; `agent-finding` and `agent-proposal` only with a source. Absent on flows created before the field existed and on flows whose evidence was not enough, and read as `unknown`. Gates nothing.",
26048
+ properties: {
26049
+ kind: { type: "string", enum: ["human-request", "agent-finding", "agent-proposal"] },
26050
+ quote: {
26051
+ type: "string",
26052
+ description: "The human's request, verbatim and byte for byte: no translation, no paraphrase."
26053
+ },
26054
+ source: {
26055
+ type: "string",
26056
+ description: "Where the request or finding came from: channel, message id, time, or the check that produced it."
26057
+ }
26058
+ }
26059
+ },
25911
26060
  signatures: {
25912
26061
  type: "array",
25913
26062
  description: "Append-only signing record for `flow ac confirm` and a passing `flow complete`. Absent means never signed under this field's existence.",
@@ -26845,17 +26994,24 @@ function outcomeBulletsFrom(markdown) {
26845
26994
  if (body === null)
26846
26995
  return null;
26847
26996
  const bullets = [];
26997
+ let skipping = false;
26848
26998
  for (const line of body.split(/\r?\n/)) {
26849
26999
  const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
26850
27000
  if ((bullet?.[1] ?? line).trim() === OUTCOME_HINT)
26851
27001
  continue;
26852
- if (bullet !== null)
26853
- bullets.push((bullet[1] ?? "").trim());
26854
- else if (line.trim().length > 0 && bullets.length > 0)
27002
+ if (bullet !== null) {
27003
+ skipping = isOriginTemplateBullet(bullet[1] ?? "");
27004
+ if (!skipping)
27005
+ bullets.push((bullet[1] ?? "").trim());
27006
+ } else if (line.trim().length > 0 && !skipping && bullets.length > 0)
26855
27007
  bullets[bullets.length - 1] += ` ${line.trim()}`;
26856
27008
  }
26857
27009
  return bullets.filter((bullet) => bullet.length > 0);
26858
27010
  }
27011
+ function isOriginTemplateBullet(text) {
27012
+ const trimmed = text.trim();
27013
+ return trimmed.startsWith(`${REQUEST_LABEL}:`) || trimmed.startsWith(`${SOURCE_LABEL}:`) || ORIGIN_PLACEHOLDER_BULLETS.some((placeholder) => placeholder.slice(2) === trimmed);
27014
+ }
26859
27015
  var INIT_INTENT_NOTE = "note: no intent statement extractable from description.md \u2014 the product index will hold no statement for this flow";
26860
27016
  function initIntentNote(description) {
26861
27017
  return flowStatementFrom(description) === null ? INIT_INTENT_NOTE : null;
@@ -27417,7 +27573,9 @@ async function flowFoldersInHead(cwd, dirs) {
27417
27573
  }
27418
27574
 
27419
27575
  // src/flow/templates.ts
27420
- function renderDescription(title, source) {
27576
+ function renderDescription(title, source, origin) {
27577
+ const outcome = origin === undefined ? OUTCOME_HINT : renderOriginBullets(origin).join(`
27578
+ `);
27421
27579
  return `# ${title}
27422
27580
 
27423
27581
  Status: draft (flow-init skill formalizes this)
@@ -27433,7 +27591,7 @@ What must be true when this flow is done.
27433
27591
 
27434
27592
  ## Outcome criteria
27435
27593
 
27436
- ${OUTCOME_HINT}
27594
+ ${outcome}
27437
27595
 
27438
27596
  ## Out of Scope
27439
27597
 
@@ -27750,7 +27908,24 @@ function createFlowService(deps) {
27750
27908
  if (input.owner !== undefined && !input.owner.trim()) {
27751
27909
  throw new Error(BLANK_OWNER_MESSAGE);
27752
27910
  }
27753
- const outcomeAuthor = input.outcomeAuthor === undefined ? DEFAULT_OUTCOME_AUTHOR : parseOutcomeAuthor(input.outcomeAuthor);
27911
+ const outcomeAuthor = input.outcomeAuthor === undefined ? undefined : parseOutcomeAuthor(input.outcomeAuthor);
27912
+ let originNote;
27913
+ let origin;
27914
+ let originDraft;
27915
+ if (input.origin !== undefined) {
27916
+ const resolution = resolveOrigin({ kind: input.origin, quote: input.originQuote, source: input.originSource });
27917
+ origin = resolution.origin;
27918
+ originNote = resolution.note;
27919
+ if (isOriginKind(input.origin)) {
27920
+ originDraft = {
27921
+ kind: input.origin,
27922
+ ...input.originQuote?.trim() ? { quote: input.originQuote } : {},
27923
+ ...input.originSource?.trim() ? { source: input.originSource } : {}
27924
+ };
27925
+ }
27926
+ } else if (input.originQuote !== undefined || input.originSource !== undefined) {
27927
+ originNote = "--quote/--source were given without --origin; origin stays unknown.";
27928
+ }
27754
27929
  const trackerReady = deps.tracker ? await deps.tracker.detect() : false;
27755
27930
  const tracker = trackerReady ? deps.tracker : null;
27756
27931
  const issueRef = input.issue && deps.tracker ? deps.tracker.parseRef(input.issue) : null;
@@ -27806,12 +27981,13 @@ function createFlowService(deps) {
27806
27981
  pr: { url: null },
27807
27982
  ...input.baseBranch === undefined ? {} : { baseBranch: input.baseBranch },
27808
27983
  ...input.owner?.trim() ? { owner: ownerIdentity(input.owner.trim(), "`--owner` flag on `flow init`") } : {},
27809
- outcomeAuthor,
27984
+ ...outcomeAuthor !== undefined ? { outcomeAuthor } : origin === undefined ? { outcomeAuthor: DEFAULT_OUTCOME_AUTHOR } : {},
27985
+ ...origin === undefined ? {} : { origin },
27810
27986
  tasks: DEFAULT_TASKS.map((task) => ({ ...task, status: "todo" })),
27811
27987
  history: [{ at: createdAt, event: "created" }]
27812
27988
  };
27813
27989
  const sourceLabel = input.issue ?? "user description";
27814
- await writeFileAtomic(path56.join(absolute, "description.md"), renderDescription(title, sourceLabel));
27990
+ await writeFileAtomic(path56.join(absolute, "description.md"), renderDescription(title, originDraft === undefined ? sourceLabel : `${sourceLabel} (origin: ${originDraft.kind})`, originDraft));
27815
27991
  await writeFileAtomic(path56.join(absolute, "context.md"), context.markdown);
27816
27992
  await writeFileAtomic(path56.join(absolute, "plan.md"), renderPlan());
27817
27993
  await writeFileAtomic(path56.join(absolute, "tasks.md"), renderTasksDoc());
@@ -27819,7 +27995,12 @@ function createFlowService(deps) {
27819
27995
  await writeFileAtomic(path56.join(absolute, "journal.md"), renderJournal(createdAt));
27820
27996
  await writeFlow(input.cwd, dir, flow);
27821
27997
  await recordAllocation(scope, { id, dir, at: createdAt, project: scope.project });
27822
- return { flow, dir: path56.relative(input.cwd, absolute), contextNotes: context.notes };
27998
+ return {
27999
+ flow,
28000
+ dir: path56.relative(input.cwd, absolute),
28001
+ contextNotes: context.notes,
28002
+ ...originNote === undefined ? {} : { originNote }
28003
+ };
27823
28004
  });
27824
28005
  },
27825
28006
  async list({ cwd }) {
@@ -27883,6 +28064,50 @@ function createFlowService(deps) {
27883
28064
  });
27884
28065
  return { flow, previous, changed };
27885
28066
  },
28067
+ async originSet({ cwd, id, kind, reason, quote, source }) {
28068
+ if (!reason?.trim()) {
28069
+ throw new Error('flow origin set requires --reason "<why>"');
28070
+ }
28071
+ validateSingleLineReason(reason);
28072
+ let previous = "unknown";
28073
+ let next = "unknown";
28074
+ let changed = false;
28075
+ let note;
28076
+ const flow = await mutate(cwd, id, async ({ dir, flow: current }) => {
28077
+ const before = readOrigin(current.origin);
28078
+ previous = before?.kind ?? "unknown";
28079
+ let after;
28080
+ if (kind !== "unknown" && !isOriginKind(kind)) {
28081
+ note = `origin kind must be one of: human-request, agent-finding, agent-proposal, unknown (got "${kind}"); the origin is unchanged.`;
28082
+ next = previous;
28083
+ return current;
28084
+ }
28085
+ if (kind !== "unknown") {
28086
+ const resolution = resolveOrigin({
28087
+ kind,
28088
+ quote: quote !== undefined && quote.length > 0 ? quote : before?.quote,
28089
+ source: source !== undefined && source.length > 0 ? source : before?.source
28090
+ });
28091
+ if (resolution.origin === undefined) {
28092
+ note = resolution.note;
28093
+ next = previous;
28094
+ return current;
28095
+ }
28096
+ after = resolution.origin;
28097
+ }
28098
+ next = after?.kind ?? "unknown";
28099
+ if (JSON.stringify(before ?? null) === JSON.stringify(after ?? null)) {
28100
+ return current;
28101
+ }
28102
+ changed = true;
28103
+ if (after === undefined)
28104
+ delete current.origin;
28105
+ else
28106
+ current.origin = after;
28107
+ return save(cwd, dir, current, "origin-set", `${originSummary(before)} -> ${originSummary(after)} (${reason.trim()})`);
28108
+ });
28109
+ return { flow, previous, next, changed, ...note === undefined ? {} : { note } };
28110
+ },
27886
28111
  async freeze({ cwd, id }) {
27887
28112
  return mutate(cwd, id, async ({ dir, flow }) => {
27888
28113
  assertTransition(flow.status, "ready");
@@ -41023,6 +41248,12 @@ var HELP_GROUPS = [
41023
41248
  group: "Managed work",
41024
41249
  summary: "List doc sections that likely went stale because of the working diff, with Jev."
41025
41250
  },
41251
+ {
41252
+ kind: "slash",
41253
+ name: "/flow",
41254
+ group: "Managed work",
41255
+ summary: `Show a flow's origin, or set it with a reason \u2014 /flow origin [<id>] | /flow origin <id> <kind> --reason "...".`
41256
+ },
41026
41257
  {
41027
41258
  kind: "slash",
41028
41259
  name: "/opencomments",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.58",
3
+ "version": "0.3.61",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -81,8 +81,8 @@ flowchart TD
81
81
  H -- "yes" --> I{"Ask user how to finish"}
82
82
  I -- "create PR" --> J["create PR and run review/fix loop"]
83
83
  J --> K{"review clean, PR mergeable?"}
84
- K -- "no, attempts < 3" --> J
85
- K -- "no, attempts = 3 or repetition detected" --> R["enrich context and change fix strategy"]
84
+ K -- "no, attempts < 5" --> J
85
+ K -- "no, attempts = 5 or repetition detected" --> R["enrich context and change fix strategy"]
86
86
  R --> J
87
87
  K -- "yes" --> L["merge PR into recorded base branch"]
88
88
  L --> M["keryx flow implemented --pr"]
@@ -153,7 +153,7 @@ it already tried. The flow package does.
153
153
  ```
154
154
 
155
155
  6. Apply the Phase 4 attempt budget against the **persisted** count. If
156
- `attempts.count` for the task has already reached **three**, do not
156
+ `attempts.count` for the task has already reached **five**, do not
157
157
  re-dispatch the same approach: go to the re-planning step (Phase 4, PR
158
158
  review/fix loop, step 4) and record the decision in `journal.md`.
159
159
  7. Run the repetition check before spending an attempt, whatever the count
@@ -533,7 +533,7 @@ answered it and who is behind it.
533
533
  | `completion: <anything>` as a constraint STRING | **Not an outcome. Refuse it and ask for the typed field.** `constraints[]` is parsed by nothing, so a completion arriving there is never read at all. Such a dispatch is now refused for the missing `completion_outcome` rather than silently accepted — but the refusal is the contract's, not this row's, and a row telling you to honour the string would be a documented bypass of the fence in the file that owns it. |
534
534
 
535
535
  A constraint that would raise this skill's own attempt budget is **not** obeyed.
536
- The three-attempt bound and the `keryx review loop` repetition check are this
536
+ The five-attempt bound and the `keryx review loop` repetition check are this
537
537
  skill's, they are evidence-backed, and a caller asking for "loop until clean" gets
538
538
  the bound plus an escalation — never an unbounded loop.
539
539
 
@@ -551,23 +551,21 @@ the bound plus an escalation — never an unbounded loop.
551
551
  remaining `info` findings in the completion report rather than fixing them
552
552
  under a loop that was not opened for them. A caller may lower the threshold in
553
553
  `constraints`; it cannot raise it to merge over a `minor`.
554
- 3. Allow at most **three** review/fix attempts for the current approach. Count
554
+ 3. Allow at most **five** review/fix attempts for the current approach. Count
555
555
  an attempt when review/check results are available, including a clean result,
556
556
  and record it with `keryx flow task attempt <id> <Tn> --outcome ...` so the
557
557
  count survives a session restart. Read the budget from that task's
558
558
  `attempts.count` in `flow.json`, never from this session's memory.
559
559
 
560
- Three, and the same three that `task-implementer` and `job-orchestrator`
561
- already use. This skill said six, which was an outlier with nothing behind
562
- it. The evidence converges on three: *"the first three to four repair
560
+ Five here by operator decision (message 178020; review findings surface across rounds, and the repetition check below stops a loop that is not converging), three in `task-implementer` and `job-orchestrator`. This skill said six, an outlier with nothing behind it. The evidence for a repair loop converges on three: *"the first three to four repair
563
561
  iterations account for most achievable gains"*
564
562
  ([arXiv:2607.05197](https://arxiv.org/abs/2607.05197)); correctness falls
565
563
  **0.820 -> 0.673** across two forced revisions while cumulative ever-correct
566
564
  is **0.847** ([arXiv:2607.24604](https://arxiv.org/abs/2607.24604)) — the
567
565
  agent finds the fix and then destroys it, throwing away ~15 points by not
568
566
  stopping. Aider hardcodes `max_reflections = 3`; OpenHands' critic uses 3.
569
- Rounds four through six were not buying convergence; they were buying
570
- regressions.
567
+ Rounds four through six of a self-fix loop were not buying convergence; they
568
+ were buying regressions. Review rounds are the exception, as stated above.
571
569
 
572
570
  4. **Before** spending an attempt, and regardless of how much budget is left,
573
571
  run the repetition check:
@@ -583,7 +581,7 @@ the bound plus an escalation — never an unbounded loop.
583
581
  emitting the identical failing output three times must be caught on the
584
582
  second, not after the budget runs out.
585
583
 
586
- 5. If the third attempt is not clean, **or the repetition check escalated
584
+ 5. If the fifth attempt is not clean, **or the repetition check escalated
587
585
  earlier**, do not blindly repeat the same loop. Enrich context from the
588
586
  findings, affected graph, relevant wiki, and health/testing artifacts;
589
587
  identify the likely cycle cause; choose a materially different fix strategy
@@ -1265,8 +1265,8 @@ with evidence, not silence.
1265
1265
 
1266
1266
  Only runs if NEEDS_FIX is true. Default max: **3 iterations** (`max_review_iterations`).
1267
1267
 
1268
- Three is the shared round bound: `task-implementer`, `flow-orchestrator` and
1269
- this skill all use it. *"The first three to four repair iterations account for
1268
+ Three is the self-fix bound `task-implementer` and this skill share
1269
+ (`flow-orchestrator`'s review bound is five, by operator decision). *"The first three to four repair iterations account for
1270
1270
  most achievable gains"* ([arXiv:2607.05197](https://arxiv.org/abs/2607.05197));
1271
1271
  correctness falls **0.820 -> 0.673** across two forced revisions while
1272
1272
  cumulative ever-correct is **0.847**
@@ -106,7 +106,7 @@
106
106
  "minimum": 1,
107
107
  "maximum": 3,
108
108
  "default": 3,
109
- "description": "Max review → fix iterations. Three: the same bound `task-implementer`, `flow-orchestrator` and SKILL.md 2.7 carry, pinned across all of them by src/gdskills/round-bound.test.ts. The default was 2 here, which made the contract disagree with the skill it validates."
109
+ "description": "Max review → fix iterations. Three: the same bound `task-implementer` and SKILL.md 2.7 carry (`flow-orchestrator`'s review bound is five, by operator decision), pinned by src/gdskills/round-bound.test.ts. The default was 2 here, which made the contract disagree with the skill it validates."
110
110
  },
111
111
  "plan_approval": {
112
112
  "type": "boolean",
@@ -362,8 +362,8 @@ non-zero exit the signal; if tests were created or modified they must pass.
362
362
 
363
363
  Maximum 3 self-fix attempts per verification step.
364
364
 
365
- Three, and it is the same three `job-orchestrator` and `flow-orchestrator`
366
- use: one round bound, not four. *"The first three to four repair iterations
365
+ Three, the same three `job-orchestrator` uses; `flow-orchestrator`'s review
366
+ bound is five by operator decision, so two repair bounds, not four. *"The first three to four repair iterations
367
367
  account for most achievable gains"*
368
368
  ([arXiv:2607.05197](https://arxiv.org/abs/2607.05197)); correctness falls
369
369
  **0.820 -> 0.673** across two forced revisions while cumulative ever-correct is
@@ -188,7 +188,7 @@
188
188
  "type": "integer",
189
189
  "minimum": 1,
190
190
  "maximum": 3,
191
- "description": "Fix iteration number. Three, the same bound `flow-orchestrator`, `job-orchestrator` and SKILL.md Phase 5.4 carry and `src/gdskills/round-bound.test.ts` pins; it was 2 here, a fourth disagreeing repair bound."
191
+ "description": "Fix iteration number. Three, the same bound `job-orchestrator` and SKILL.md Phase 5.4 carry and `src/gdskills/round-bound.test.ts` pins (`flow-orchestrator`'s review bound is five); it was 2 here, a fourth disagreeing repair bound."
192
192
  }
193
193
  }
194
194
  },