@mrciphersmith/keryx 0.3.48 → 0.3.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +12 -6
  2. package/dist/cli.js +6890 -4481
  3. package/dist/core.js +208 -92
  4. package/package.json +1 -1
package/dist/core.js CHANGED
@@ -23145,6 +23145,7 @@ __export(exports_service2, {
23145
23145
  buildAcKindReport: () => buildAcKindReport,
23146
23146
  classifyAcCriterionNotCheckable: () => classifyAcCriterionNotCheckable,
23147
23147
  classifyNotCheckable: () => classifyNotCheckable,
23148
+ completionFixHint: () => completionFixHint,
23148
23149
  computeAcFacts: () => computeAcFacts,
23149
23150
  confirmPreconditionError: () => confirmPreconditionError,
23150
23151
  createFlowService: () => createFlowService,
@@ -23153,11 +23154,13 @@ __export(exports_service2, {
23153
23154
  extractCriterionTokens: () => extractCriterionTokens,
23154
23155
  factsOnlyVerdict: () => factsOnlyVerdict,
23155
23156
  fencedLines: () => fencedLines,
23157
+ flowDeliveryStatementFrom: () => flowDeliveryStatementFrom,
23156
23158
  flowStatementFrom: () => flowStatementFrom,
23157
23159
  hashDiff: () => hashDiff,
23158
23160
  intentNoteForNewFlow: () => intentNoteForNewFlow,
23159
23161
  isFrozen: () => isFrozen,
23160
23162
  notCheckableVerdict: () => notCheckableVerdict,
23163
+ outcomeBulletsFrom: () => outcomeBulletsFrom,
23161
23164
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
23162
23165
  parseOutcomeAuthor: () => parseOutcomeAuthor,
23163
23166
  proseOutsideFences: () => proseOutsideFences,
@@ -25860,6 +25863,25 @@ function statementFrom(markdown, headings) {
25860
25863
  function flowStatementFrom(description) {
25861
25864
  return statementFrom(description, FLOW_STATEMENT_HEADINGS);
25862
25865
  }
25866
+ function flowDeliveryStatementFrom(description) {
25867
+ return statementFrom(description, [/^expected outcome$/i, /^problem$/i]);
25868
+ }
25869
+ function outcomeBulletsFrom(markdown) {
25870
+ const body = sectionOf(markdown, /^outcome criteri(?:a|on)$/i);
25871
+ if (body === null)
25872
+ return null;
25873
+ const bullets = [];
25874
+ for (const line of body.split(/\r?\n/)) {
25875
+ const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
25876
+ if ((bullet?.[1] ?? line).trim() === OUTCOME_HINT)
25877
+ continue;
25878
+ if (bullet !== null)
25879
+ bullets.push((bullet[1] ?? "").trim());
25880
+ else if (line.trim().length > 0 && bullets.length > 0)
25881
+ bullets[bullets.length - 1] += ` ${line.trim()}`;
25882
+ }
25883
+ return bullets.filter((bullet) => bullet.length > 0);
25884
+ }
25863
25885
  var INIT_INTENT_NOTE = "note: no intent statement extractable from description.md \u2014 the product index will hold no statement for this flow";
25864
25886
  function initIntentNote(description) {
25865
25887
  return flowStatementFrom(description) === null ? INIT_INTENT_NOTE : null;
@@ -26334,6 +26356,63 @@ async function deriveAcKinds(cwd, dir, flowId) {
26334
26356
  const { report } = buildAcKindReport(flowId, await readFile27(acPath(cwd, dir), "utf8"));
26335
26357
  return { ...report.criteria };
26336
26358
  }
26359
+ function prMergeState(pr, gates) {
26360
+ switch (pr.kind) {
26361
+ case "no-pr":
26362
+ return { state: "no-pr", detail: "no PR recorded on the flow" };
26363
+ case "merged-commit": {
26364
+ const merge = gates.find((gate) => gate.name === "main-merge");
26365
+ return merge?.status === "pass" ? { state: "merged", detail: `direct merge: ${merge.detail}` } : { state: "unknown", detail: `direct merge not verified: ${merge?.detail ?? "main-merge gate missing"}` };
26366
+ }
26367
+ case "tracker-unavailable":
26368
+ return { state: "unknown", detail: "tracker unavailable (is `gh` installed and authenticated?)" };
26369
+ case "unevaluable":
26370
+ return { state: "unknown", detail: "the tracker call failed" };
26371
+ case "observed": {
26372
+ if (!pr.exists)
26373
+ return { state: "not-found", detail: "the tracker found no such PR" };
26374
+ const state = pr.state?.toUpperCase();
26375
+ if (state === "MERGED")
26376
+ return { state: "merged", detail: "PR merged" };
26377
+ if (state === "OPEN")
26378
+ return { state: "open", detail: "PR open, not merged" };
26379
+ if (state === "CLOSED")
26380
+ return { state: "closed", detail: "PR closed without merging" };
26381
+ return { state: "unknown", detail: pr.state === null ? "the tracker did not report a PR state" : `unrecognised PR state: ${pr.state}` };
26382
+ }
26383
+ }
26384
+ }
26385
+ var UNCONFIRMED_PREFIX = "unconfirmed: ";
26386
+ var NO_PR_DETAIL = "no PR recorded";
26387
+ function completionAdmitted(status, merged) {
26388
+ return canTransition(status, "completing") || merged && status === "in-progress";
26389
+ }
26390
+ function completionFixHint(gate, flowId) {
26391
+ if (gate.status !== "fail")
26392
+ return;
26393
+ switch (gate.name) {
26394
+ case "acceptance-criteria": {
26395
+ const unconfirmed = gate.detail.startsWith(UNCONFIRMED_PREFIX) ? gate.detail.slice(UNCONFIRMED_PREFIX.length).split(", ") : [];
26396
+ const first = unconfirmed[0];
26397
+ if (first === undefined)
26398
+ return;
26399
+ const more = unconfirmed.length > 1 ? ` (then ${unconfirmed.slice(1).join(", ")})` : "";
26400
+ return `keryx flow ac confirm ${flowId} ${first} --note "<evidence>"${more}`;
26401
+ }
26402
+ case "pull-request":
26403
+ return gate.detail === NO_PR_DETAIL ? `keryx flow implemented ${flowId} --pr <url>` : undefined;
26404
+ case "tasks":
26405
+ return `keryx flow next ${flowId}`;
26406
+ case "owner":
26407
+ return `keryx flow owner set ${flowId} --owner "<name>" --reason "<why>"`;
26408
+ case "health":
26409
+ return "keryx health run";
26410
+ case "confirmation":
26411
+ return `keryx flow confirm ${flowId} (in a terminal)`;
26412
+ default:
26413
+ return;
26414
+ }
26415
+ }
26337
26416
  function recordAttempt(task, outcome, at, detail) {
26338
26417
  const attempts = task.attempts ?? { count: 0, log: [] };
26339
26418
  attempts.count += 1;
@@ -26393,6 +26472,106 @@ function createFlowService(deps) {
26393
26472
  flow.status = "in-progress";
26394
26473
  return save(cwd, dir, flow, event, detail);
26395
26474
  }
26475
+ async function evaluateCompletionGates(cwd, dir, flow, mergedCommit, confirmToken) {
26476
+ const gates = [];
26477
+ try {
26478
+ await assertAcIntact(cwd, dir, flow);
26479
+ const criteria = await readAcCriteria(cwd, dir);
26480
+ const missing = criteria.filter((criterion) => !flow.acConfirmed[criterion]);
26481
+ gates.push(missing.length === 0 && criteria.length > 0 ? { name: "acceptance-criteria", status: "pass", detail: `${criteria.length} confirmed` } : {
26482
+ name: "acceptance-criteria",
26483
+ status: "fail",
26484
+ detail: criteria.length === 0 ? "no criteria found" : `${UNCONFIRMED_PREFIX}${missing.join(", ")}`
26485
+ });
26486
+ } catch {
26487
+ gates.push(unevaluableGate("acceptance-criteria"));
26488
+ }
26489
+ let confirmationToken;
26490
+ let confirmationOutcome;
26491
+ try {
26492
+ const confirmation = await confirmationGate(cwd, dir, flow, confirmToken, completionTarget({ merged: Boolean(mergedCommit), prUrl: flow.pr.url }), deps.now());
26493
+ confirmationOutcome = confirmation.outcome;
26494
+ confirmationToken = confirmation.stored;
26495
+ } catch {
26496
+ confirmationOutcome = unevaluableGate("confirmation");
26497
+ }
26498
+ let evaluatedHeadCommit = mergedCommit ?? undefined;
26499
+ let pr;
26500
+ if (mergedCommit) {
26501
+ pr = { kind: "merged-commit" };
26502
+ try {
26503
+ const merge = deps.mainMergeGate ? await deps.mainMergeGate(cwd, mergedCommit) : await verifyCommitOnMain(cwd, mergedCommit);
26504
+ gates.push({ name: "main-merge", status: merge.status, detail: merge.detail });
26505
+ } catch {
26506
+ gates.push(unevaluableGate("main-merge"));
26507
+ }
26508
+ } else if (!flow.pr.url) {
26509
+ pr = { kind: "no-pr" };
26510
+ gates.push({ name: "pull-request", status: "fail", detail: NO_PR_DETAIL });
26511
+ } else {
26512
+ pr = { kind: "unevaluable" };
26513
+ try {
26514
+ if (deps.tracker && await deps.tracker.detect()) {
26515
+ const status = await deps.tracker.prStatus(flow.pr.url);
26516
+ pr = { kind: "observed", exists: status.exists, state: status.state ?? null };
26517
+ if (typeof status.headSha === "string" && status.headSha !== "") {
26518
+ evaluatedHeadCommit ??= status.headSha;
26519
+ }
26520
+ gates.push(status.exists && status.checksGreen === true ? { name: "pull-request", status: "pass", detail: "PR exists, checks green" } : {
26521
+ name: "pull-request",
26522
+ status: "fail",
26523
+ detail: !status.exists ? "PR not found" : "PR checks not green"
26524
+ });
26525
+ } else {
26526
+ pr = { kind: "tracker-unavailable" };
26527
+ gates.push({
26528
+ name: "pull-request",
26529
+ status: "skipped",
26530
+ detail: "tracker unavailable; verify PR checks manually"
26531
+ });
26532
+ }
26533
+ } catch {
26534
+ gates.push(unevaluableGate("pull-request"));
26535
+ }
26536
+ }
26537
+ try {
26538
+ gates.push(await baseBranchCondition(cwd, flow, mergedCommit ?? undefined, flow.pr.url && deps.tracker && await deps.tracker.detect() ? (await deps.tracker.prStatus(flow.pr.url)).baseRefName : undefined, commitContainedIn));
26539
+ } catch {
26540
+ gates.push(unevaluableGate("base-branch"));
26541
+ }
26542
+ gates.push(taskGate(flow));
26543
+ gates.push(ownerGate(flow));
26544
+ try {
26545
+ gates.push(await reviewGate({
26546
+ cwd,
26547
+ flowDir: dir,
26548
+ flow,
26549
+ tracker: deps.tracker,
26550
+ ...deps.externalCommentsGate ? { externalCommentsGate: deps.externalCommentsGate } : {},
26551
+ ...mergedCommit ? { mergedCommit } : {}
26552
+ }));
26553
+ } catch {
26554
+ gates.push(unevaluableGate("review"));
26555
+ }
26556
+ try {
26557
+ const health = await deps.healthGate(cwd);
26558
+ gates.push(healthGateOutcome(health));
26559
+ } catch {
26560
+ gates.push(unevaluableGate("health"));
26561
+ }
26562
+ if (deps.securityGate) {
26563
+ try {
26564
+ const security = await deps.securityGate(cwd);
26565
+ if (security) {
26566
+ gates.push({ name: "security", status: security.status, detail: security.detail });
26567
+ }
26568
+ } catch {
26569
+ gates.push(unevaluableGate("security"));
26570
+ }
26571
+ }
26572
+ gates.push(confirmationOutcome);
26573
+ return { gates, confirmationToken, evaluatedHeadCommit, pr };
26574
+ }
26396
26575
  async function mutate(cwd, id, run) {
26397
26576
  const dir = await resolveFlowDir(cwd, id);
26398
26577
  return withFileLock(flowLockPath(cwd, dir), async () => {
@@ -26756,6 +26935,27 @@ function createFlowService(deps) {
26756
26935
  return save(cwd, dir, flow, "implemented", detail);
26757
26936
  });
26758
26937
  },
26938
+ async checkComplete({ cwd, id, mergedCommit, confirmToken }) {
26939
+ const { dir, flow } = await load(cwd, id);
26940
+ const merged = Boolean(mergedCommit);
26941
+ const allowed = completionAdmitted(flow.status, merged);
26942
+ const transition = allowed ? { allowed, detail: `${flow.status} \u2192 completing` } : {
26943
+ allowed,
26944
+ detail: flow.status === "done" ? "already done" : flow.status === "in-progress" ? `in-progress: record the PR first (\`keryx flow implemented ${flow.id} --pr <url>\`), or pass a merged commit` : `"${flow.status}" cannot move to completing; complete runs from "implemented" (or "in-progress" with a merged commit)`
26945
+ };
26946
+ const evaluation = await evaluateCompletionGates(cwd, dir, flow, mergedCommit ?? undefined, confirmToken);
26947
+ return {
26948
+ id: flow.id,
26949
+ status: flow.status,
26950
+ updatedAt: flow.updatedAt,
26951
+ checkedAt: now(),
26952
+ transition,
26953
+ merge: prMergeState(evaluation.pr, evaluation.gates),
26954
+ gates: evaluation.gates,
26955
+ passed: allowed && evaluation.gates.every((gate) => gate.status !== "fail"),
26956
+ confirmationRequired: flow.gates?.confirmation === true
26957
+ };
26958
+ },
26759
26959
  async complete({
26760
26960
  cwd,
26761
26961
  id,
@@ -26776,97 +26976,7 @@ function createFlowService(deps) {
26776
26976
  } else {
26777
26977
  flow = await transition(cwd, dir, flow, "completing", "completing");
26778
26978
  }
26779
- const gates = [];
26780
- try {
26781
- await assertAcIntact(cwd, dir, flow);
26782
- const criteria = await readAcCriteria(cwd, dir);
26783
- const missing = criteria.filter((criterion) => !flow.acConfirmed[criterion]);
26784
- gates.push(missing.length === 0 && criteria.length > 0 ? { name: "acceptance-criteria", status: "pass", detail: `${criteria.length} confirmed` } : {
26785
- name: "acceptance-criteria",
26786
- status: "fail",
26787
- detail: criteria.length === 0 ? "no criteria found" : `unconfirmed: ${missing.join(", ")}`
26788
- });
26789
- } catch {
26790
- gates.push(unevaluableGate("acceptance-criteria"));
26791
- }
26792
- let confirmationToken;
26793
- let confirmationOutcome;
26794
- try {
26795
- const confirmation = await confirmationGate(cwd, dir, flow, confirmToken, completionTarget({ merged: Boolean(mergedCommit), prUrl: flow.pr.url }), deps.now());
26796
- confirmationOutcome = confirmation.outcome;
26797
- confirmationToken = confirmation.stored;
26798
- } catch {
26799
- confirmationOutcome = unevaluableGate("confirmation");
26800
- }
26801
- let evaluatedHeadCommit = mergedCommit ?? undefined;
26802
- if (mergedCommit) {
26803
- try {
26804
- const merge = deps.mainMergeGate ? await deps.mainMergeGate(cwd, mergedCommit) : await verifyCommitOnMain(cwd, mergedCommit);
26805
- gates.push({ name: "main-merge", status: merge.status, detail: merge.detail });
26806
- } catch {
26807
- gates.push(unevaluableGate("main-merge"));
26808
- }
26809
- } else if (!flow.pr.url) {
26810
- gates.push({ name: "pull-request", status: "fail", detail: "no PR recorded" });
26811
- } else {
26812
- try {
26813
- if (deps.tracker && await deps.tracker.detect()) {
26814
- const pr = await deps.tracker.prStatus(flow.pr.url);
26815
- if (typeof pr.headSha === "string" && pr.headSha !== "") {
26816
- evaluatedHeadCommit ??= pr.headSha;
26817
- }
26818
- gates.push(pr.exists && pr.checksGreen === true ? { name: "pull-request", status: "pass", detail: "PR exists, checks green" } : {
26819
- name: "pull-request",
26820
- status: "fail",
26821
- detail: !pr.exists ? "PR not found" : "PR checks not green"
26822
- });
26823
- } else {
26824
- gates.push({
26825
- name: "pull-request",
26826
- status: "skipped",
26827
- detail: "tracker unavailable; verify PR checks manually"
26828
- });
26829
- }
26830
- } catch {
26831
- gates.push(unevaluableGate("pull-request"));
26832
- }
26833
- }
26834
- try {
26835
- gates.push(await baseBranchCondition(cwd, flow, mergedCommit ?? undefined, flow.pr.url && deps.tracker && await deps.tracker.detect() ? (await deps.tracker.prStatus(flow.pr.url)).baseRefName : undefined, commitContainedIn));
26836
- } catch {
26837
- gates.push(unevaluableGate("base-branch"));
26838
- }
26839
- gates.push(taskGate(flow));
26840
- gates.push(ownerGate(flow));
26841
- try {
26842
- gates.push(await reviewGate({
26843
- cwd,
26844
- flowDir: dir,
26845
- flow,
26846
- tracker: deps.tracker,
26847
- ...deps.externalCommentsGate ? { externalCommentsGate: deps.externalCommentsGate } : {},
26848
- ...mergedCommit ? { mergedCommit } : {}
26849
- }));
26850
- } catch {
26851
- gates.push(unevaluableGate("review"));
26852
- }
26853
- try {
26854
- const health = await deps.healthGate(cwd);
26855
- gates.push(healthGateOutcome(health));
26856
- } catch {
26857
- gates.push(unevaluableGate("health"));
26858
- }
26859
- if (deps.securityGate) {
26860
- try {
26861
- const security = await deps.securityGate(cwd);
26862
- if (security) {
26863
- gates.push({ name: "security", status: security.status, detail: security.detail });
26864
- }
26865
- } catch {
26866
- gates.push(unevaluableGate("security"));
26867
- }
26868
- }
26869
- gates.push(confirmationOutcome);
26979
+ const { gates, confirmationToken, evaluatedHeadCommit } = await evaluateCompletionGates(cwd, dir, flow, mergedCommit ?? undefined, confirmToken);
26870
26980
  const passed = gates.every((gate) => gate.status !== "fail");
26871
26981
  const tamperDetail = await acTamperDetail(cwd, dir, flow);
26872
26982
  const recordedGates = tamperDetail === undefined ? gates : [
@@ -39815,7 +39925,7 @@ var HELP_GROUPS = [
39815
39925
  kind: "slash",
39816
39926
  name: "/governance",
39817
39927
  group: "Automation",
39818
- summary: "Show the last governance report, or run one in the background."
39928
+ summary: "Flows with their summary and stated effect \u2014 check an open one, complete it once the check passes and the PR is merged; the last report in its own tab, or run one in the background."
39819
39929
  },
39820
39930
  {
39821
39931
  kind: "slash",
@@ -39835,6 +39945,12 @@ var HELP_GROUPS = [
39835
39945
  group: "Automation",
39836
39946
  summary: "Drive this session from a Telegram topic: send lines, get replies, answer approvals. Off by default; [name|off|status]."
39837
39947
  },
39948
+ {
39949
+ kind: "slash",
39950
+ name: "/channels",
39951
+ group: "Automation",
39952
+ summary: "Connect, test or disconnect Telegram for this machine: bot token, one-time code, group. [status]."
39953
+ },
39838
39954
  {
39839
39955
  kind: "cli",
39840
39956
  name: "acp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.48",
3
+ "version": "0.3.51",
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": {