@mrciphersmith/keryx 0.2.84 → 0.2.88

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
@@ -10291,6 +10291,7 @@ var init_wiki_layer = __esm(() => {
10291
10291
  init_fs();
10292
10292
  var exports_service = {};
10293
10293
  __export(exports_service, {
10294
+ baseBranchCondition: () => baseBranchCondition,
10294
10295
  createFlowService: () => createFlowService
10295
10296
  });
10296
10297
  import { mkdir as mkdir5, readFile as readFile9, rename as rename3 } from "fs/promises";
@@ -13098,6 +13099,7 @@ function createFlowService(deps) {
13098
13099
  acChecksum: null,
13099
13100
  acConfirmed: {},
13100
13101
  pr: { url: null },
13102
+ ...input.baseBranch === undefined ? {} : { baseBranch: input.baseBranch },
13101
13103
  tasks: DEFAULT_TASKS.map((task) => ({ ...task, status: "todo" })),
13102
13104
  history: [{ at: createdAt, event: "created" }]
13103
13105
  };
@@ -13306,6 +13308,10 @@ function createFlowService(deps) {
13306
13308
  if (!pr.isDraft) {
13307
13309
  detail += " (warning: PR is not a draft)";
13308
13310
  }
13311
+ if (flow.baseBranch === undefined && typeof pr.baseRefName === "string" && pr.baseRefName !== "") {
13312
+ flow.baseBranch = pr.baseRefName;
13313
+ detail += ` (base: ${pr.baseRefName})`;
13314
+ }
13309
13315
  } else {
13310
13316
  detail += " (tracker unavailable: existence not verified)";
13311
13317
  }
@@ -13357,6 +13363,7 @@ function createFlowService(deps) {
13357
13363
  detail: "tracker unavailable; verify PR checks manually"
13358
13364
  });
13359
13365
  }
13366
+ 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));
13360
13367
  gates.push(taskGate(flow));
13361
13368
  try {
13362
13369
  gates.push(await reviewGate({
@@ -13661,6 +13668,70 @@ function buildIssueComment(flow, gates) {
13661
13668
  ].join(`
13662
13669
  `);
13663
13670
  }
13671
+ async function baseBranchCondition(cwd, flow, mergedCommit, prBase, containedIn) {
13672
+ const recorded = flow.baseBranch;
13673
+ if (recorded === undefined || recorded.trim() === "") {
13674
+ return {
13675
+ name: "base-branch",
13676
+ status: "skipped",
13677
+ detail: "not recorded: this flow never named a base branch, so there is nothing to compare the merge against. Not a pass \u2014 `keryx flow init --base <branch>` or `flow implemented --pr <url>` records one."
13678
+ };
13679
+ }
13680
+ if (mergedCommit !== undefined) {
13681
+ const contained = await containedIn(cwd, mergedCommit, `origin/${recorded}`);
13682
+ if (contained === null) {
13683
+ return {
13684
+ name: "base-branch",
13685
+ status: "fail",
13686
+ detail: `unobserved: origin/${recorded} could not be resolved, so whether ${mergedCommit} landed there is unknown. Fetch the remote (\`git fetch origin ${recorded}\`) and re-run; an unresolvable base is not a passing one.`
13687
+ };
13688
+ }
13689
+ return contained ? {
13690
+ name: "base-branch",
13691
+ status: "pass",
13692
+ detail: `${mergedCommit} is contained in origin/${recorded}, the base this flow recorded`
13693
+ } : {
13694
+ name: "base-branch",
13695
+ status: "fail",
13696
+ detail: `violated: this flow recorded base ${recorded}, but ${mergedCommit} is not contained in origin/${recorded}. The merge landed somewhere else.`
13697
+ };
13698
+ }
13699
+ if (prBase === undefined || prBase === null || prBase === "") {
13700
+ return {
13701
+ name: "base-branch",
13702
+ status: "fail",
13703
+ detail: `unobserved: this flow recorded base ${recorded}, but the tracker did not report the pull request's base. An unread base is not a matching one.`
13704
+ };
13705
+ }
13706
+ return prBase === recorded ? {
13707
+ name: "base-branch",
13708
+ status: "pass",
13709
+ detail: `the pull request targets ${prBase}, the base this flow recorded`
13710
+ } : {
13711
+ name: "base-branch",
13712
+ status: "fail",
13713
+ detail: `violated: this flow recorded base ${recorded}, but the pull request now targets ${prBase}. It was retargeted after the base was recorded.`
13714
+ };
13715
+ }
13716
+ async function commitContainedIn(cwd, commit, ref) {
13717
+ if (!/^[0-9a-f]{7,64}$/i.test(commit)) {
13718
+ return null;
13719
+ }
13720
+ const resolved = Bun.spawn(["git", "rev-parse", "--verify", `${ref}^{commit}`], {
13721
+ cwd,
13722
+ stdout: "ignore",
13723
+ stderr: "ignore"
13724
+ });
13725
+ if (await resolved.exited !== 0) {
13726
+ return null;
13727
+ }
13728
+ const process2 = Bun.spawn(["git", "merge-base", "--is-ancestor", commit, ref], {
13729
+ cwd,
13730
+ stdout: "ignore",
13731
+ stderr: "ignore"
13732
+ });
13733
+ return await process2.exited === 0;
13734
+ }
13664
13735
  async function verifyCommitOnMain(cwd, commit) {
13665
13736
  if (!/^[0-9a-f]{7,64}$/i.test(commit)) {
13666
13737
  return { status: "fail", detail: "merged commit must be a hexadecimal Git commit id" };
@@ -18423,63 +18494,112 @@ var CONTRACTS = [
18423
18494
  {
18424
18495
  name: "agent-event",
18425
18496
  fileName: "agent-event.schema.json",
18426
- description: "Append-only lifecycle event emitted by orchestrators and subagents."
18497
+ description: "Append-only lifecycle event emitted by orchestrators and subagents.",
18498
+ enforcement: {
18499
+ kind: "none",
18500
+ reason: "Events are appended by whichever agent is running, in that agent's own runtime. No keryx process sits between the emitter and the log, so there is no point at which a malformed event could be refused rather than written."
18501
+ }
18427
18502
  },
18428
18503
  {
18429
18504
  name: "flow-orchestrator-input",
18430
18505
  fileName: "flow-orchestrator-input-contract.schema.json",
18431
18506
  description: "Dispatch payload handed to flow-orchestrator (request + base branch + completion outcome + constraints).",
18432
- sourcePath: "src/gdskills/bundled/skills/orchestration/flow-orchestrator/input-contract.schema.json"
18507
+ sourcePath: "src/gdskills/bundled/skills/orchestration/flow-orchestrator/input-contract.schema.json",
18508
+ enforcement: {
18509
+ kind: "none",
18510
+ reason: "This payload is handed from one AGENT to another. In a session driven by a host agent's own dispatch tool, no keryx process is on that path, so nothing can refuse a malformed dispatch. Same structural position as reviewer-input, recorded for the same reason. Registering it made `keryx skills contracts validate --schema flow-orchestrator-input` possible, which is a validator an agent must remember to run, not an enforcement."
18511
+ }
18433
18512
  },
18434
18513
  {
18435
18514
  name: "review-pr-feedback-input",
18436
18515
  fileName: "review-pr-feedback-input-contract.schema.json",
18437
18516
  description: "Request handed to review-pr-feedback (PR reference, --fix, and the operator confirmation --fix requires).",
18438
- sourcePath: "src/gdskills/bundled/skills/review/review-pr-feedback/input-contract.schema.json"
18517
+ sourcePath: "src/gdskills/bundled/skills/review/review-pr-feedback/input-contract.schema.json",
18518
+ enforcement: {
18519
+ kind: "none",
18520
+ reason: "The request is what an operator or a host agent hands the skill before any keryx command runs, so keryx is not yet in the path when the payload would have to be refused. This is the one where that hurts most: `--fix` merges third-party review comments into somebody else's pull request, and `operator_confirmed` is the fence. The conditional is expressed in the schema and `keryx skills contracts validate --schema review-pr-feedback-input` will apply it \u2014 but only when something invokes it."
18521
+ }
18439
18522
  },
18440
18523
  {
18441
18524
  name: "review-pr-feedback-output",
18442
18525
  fileName: "review-pr-feedback-output-contract.schema.json",
18443
18526
  description: "Result review-pr-feedback returns: verdict counts, the injection-screen record, and what the fix run merged.",
18444
- sourcePath: "src/gdskills/bundled/skills/review/review-pr-feedback/output-contract.schema.json"
18527
+ sourcePath: "src/gdskills/bundled/skills/review/review-pr-feedback/output-contract.schema.json",
18528
+ enforcement: {
18529
+ kind: "opt-in",
18530
+ module: "src/commands/review.ts",
18531
+ switchedOnBy: "keryx review comments reply --result <file>",
18532
+ refuses: "A result that contradicts itself, before the pass is built and before any network call: an analyze-mode run reporting a branch and a merge, or a record saying the injection screen never ran while claiming it excluded comments."
18533
+ }
18445
18534
  },
18446
18535
  {
18447
18536
  name: "job-orchestrator-state",
18448
18537
  fileName: "job-orchestrator-state.schema.json",
18449
18538
  description: "Persisted job package state (.metaproject/jobs/<name>/state.json), written by `keryx job`.",
18450
- sourcePath: "src/gdskills/bundled/skills/orchestration/job-orchestrator/state.schema.json"
18539
+ sourcePath: "src/gdskills/bundled/skills/orchestration/job-orchestrator/state.schema.json",
18540
+ enforcement: {
18541
+ kind: "production",
18542
+ module: "src/job/store.ts",
18543
+ refuses: "`writeJob` refuses to write a state file that does not satisfy the schema, so an invalid job package cannot reach disk."
18544
+ }
18451
18545
  },
18452
18546
  {
18453
18547
  name: "orchestrator-state",
18454
18548
  fileName: "orchestrator-state.schema.json",
18455
- description: "Persisted resumable orchestrator state."
18549
+ description: "Persisted resumable orchestrator state.",
18550
+ enforcement: {
18551
+ kind: "none",
18552
+ reason: "No keryx command reads or writes this file. It describes state an orchestrator agent persists for itself, in its own runtime, so there is no keryx-owned write path to refuse at. Contrast job-orchestrator-state, which looks similar and IS enforced for exactly one reason: `keryx job` writes that one."
18553
+ }
18456
18554
  },
18457
18555
  {
18458
18556
  name: "review-finding",
18459
18557
  fileName: "review-finding.schema.json",
18460
- description: "Normalized reviewer finding consumed by review-orchestrator and learning flows."
18558
+ description: "Normalized reviewer finding consumed by review-orchestrator and learning flows.",
18559
+ enforcement: {
18560
+ kind: "production",
18561
+ module: "src/review/managed.ts",
18562
+ refuses: "`createManagedReviewPackage` refuses to record findings that do not satisfy the schema, and the disposition sub-schema is applied the same way, so a review round cannot be written with a malformed finding or an unreadable outcome."
18563
+ }
18461
18564
  },
18462
18565
  {
18463
18566
  name: "subagent-dispatch",
18464
18567
  fileName: "subagent-dispatch.schema.json",
18465
- description: "Orchestrator-to-subagent dispatch payload."
18568
+ description: "Orchestrator-to-subagent dispatch payload.",
18569
+ enforcement: {
18570
+ kind: "none",
18571
+ reason: "The dispatch is built and consumed inside the harness without the canonical schema being loaded: `subagent-dispatch` appears only as a label on the extension metadata. Its sibling `subagent-result` IS validated, because the harness parses a child's reply back and has to decide whether it is well-formed; nothing performs the equivalent act on the way out."
18572
+ }
18466
18573
  },
18467
18574
  {
18468
18575
  name: "subagent-result",
18469
18576
  fileName: "subagent-result.schema.json",
18470
- description: "Subagent-to-orchestrator result payload."
18577
+ description: "Subagent-to-orchestrator result payload.",
18578
+ enforcement: {
18579
+ kind: "production",
18580
+ module: "src/harness/external/runtime.ts",
18581
+ refuses: "A child's reply that does not parse as a well-formed result is rejected when the harness reads it back, so a malformed result cannot be persisted as if the child had succeeded."
18582
+ }
18471
18583
  },
18472
18584
  {
18473
18585
  name: "task-implementer-input",
18474
18586
  fileName: "task-implementer-input-contract.schema.json",
18475
18587
  description: "Task request handed to task-implementer (task + workspace + automation), validated before dispatch.",
18476
- sourcePath: "src/gdskills/bundled/skills/orchestration/task-implementer/input-contract.schema.json"
18588
+ sourcePath: "src/gdskills/bundled/skills/orchestration/task-implementer/input-contract.schema.json",
18589
+ enforcement: {
18590
+ kind: "none",
18591
+ reason: "Dispatched agent-to-agent, like flow-orchestrator-input. The skill's Phase 1.4 lists five `ASSERT \u2026 \u2192 ABORT(\u2026)` refusals; registering the contract is what lets `keryx skills contracts validate` perform them, and nothing forces that call. The comment beside this registration has said so since it was written."
18592
+ }
18477
18593
  },
18478
18594
  {
18479
18595
  name: "task-implementer-output",
18480
18596
  fileName: "task-implementer-output-contract.schema.json",
18481
18597
  description: "JSON result task-implementer writes in Phase 6.1 before it emits its STATUS line.",
18482
- sourcePath: "src/gdskills/bundled/skills/orchestration/task-implementer/output-contract.schema.json"
18598
+ sourcePath: "src/gdskills/bundled/skills/orchestration/task-implementer/output-contract.schema.json",
18599
+ enforcement: {
18600
+ kind: "none",
18601
+ reason: "The skill writes this file itself, in its own runtime, and no keryx command reads it back. Contrast subagent-result, which is the same shape of thing and IS enforced for one reason: the harness reads that one back and must decide whether it is well-formed."
18602
+ }
18483
18603
  }
18484
18604
  ];
18485
18605
  async function loadSchema(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.84",
3
+ "version": "0.2.88",
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": {
@@ -54,6 +54,7 @@
54
54
  "check": "bun run lint && bun run typecheck && bun run typecheck:scripts && bun test",
55
55
  "check:core": "bun run lint && bun run typecheck && bun run typecheck:scripts && bun run test:core",
56
56
  "check:doc-links": "bun scripts/check-doc-links.ts",
57
+ "check:retired-spellings": "bun scripts/check-retired-cli-spellings.ts",
57
58
  "baseline:review-precision": "bun scripts/review-precision-baseline.ts",
58
59
  "test:guards": "bun test src/lib/config-dir.ast.test.ts src/lib/config-dir.readers.test.ts src/lib/production-graph.test.ts src/harness/policy/profiles.test.ts src/lib/serve-server.test.ts src/gdskills/agent-catalogue-xref.test.ts src/gdskills/enforcement-claims.test.ts",
59
60
  "lint": "eslint ."
@@ -198,13 +198,15 @@ keryx flow init --issue <url>
198
198
  or:
199
199
 
200
200
  ```bash
201
- keryx flow init --title "<short formalized problem>"
201
+ keryx flow init --title "<short formalized problem>" --base "<branch the work must land on>"
202
202
  ```
203
203
 
204
204
  5. Run `keryx flow status <id>` and read the flow package.
205
- 6. Record the git base branch from which the flow branch was created in
206
- `context.md` and `journal.md`; the PR must later be merged into this exact
207
- branch.
205
+ 6. `--base` records the branch in the flow record itself, where the completion
206
+ gate reads it. Note it in `context.md` and `journal.md` as well if it helps a
207
+ reader, but prose is not what the gate checks: before this flag the base
208
+ survived only as something an agent had written down, which is detectable at
209
+ dispatch and undetectable at completion.
208
210
 
209
211
  ## Phase 1: Initialize The Flow Package
210
212
 
@@ -442,9 +444,16 @@ How should this flow end?
442
444
 
443
445
  3. Follow the selected outcome:
444
446
 
445
- - **A - Create PR and merge:** create or confirm a PR in the author's name.
446
- Preserve the base branch recorded during initialization. Do not mark the
447
- flow implemented or complete before the PR is merged into that branch.
447
+ - **A - Create PR and merge:** create or confirm a PR in the author's name,
448
+ opened against the base recorded at initialization. Do not mark the flow
449
+ implemented or complete before the PR is merged into that branch.
450
+
451
+ `keryx flow complete` now checks this rather than asking you to confirm it by
452
+ eye: its `base-branch` condition compares where the merge landed against the
453
+ base in the record, and refuses when they differ. It reports three states,
454
+ and only one of them is a pass — a flow that recorded no base gets
455
+ `not recorded`, which is not a pass either. So the useful thing to do here is
456
+ make sure the base WAS recorded, not to re-verify the merge yourself.
448
457
 
449
458
  ### A dispatched run answers the question from its input
450
459
 
@@ -591,6 +591,7 @@ merge, with every reviewer unanswered.
591
591
 
592
592
  ```bash
593
593
  keryx review comments reply --repo <owner/repo> --pr <n> --outcomes <file|-> \
594
+ --result <this-run's-output.json> \
594
595
  --sha <mergedHeadSha> --final [--dry-run] [--flow-link <url>]
595
596
  ```
596
597
 
@@ -598,6 +599,17 @@ Run it **after** the merge, never during the loop: a reply written mid-round sta
598
599
  an intention, and by the time the reviewer reads it the intention has changed.
599
600
  `--final` is required by the command; it is not a reminder that can be skipped.
600
601
 
602
+ `--result` hands this run's own output to the one keryx-owned point that can
603
+ refuse it. Write the result described in Step 11 to a file first and pass it
604
+ here: the command validates it against `review-pr-feedback-output` BEFORE
605
+ posting anything, so a result that contradicts itself — analyze mode reporting a
606
+ merge, or a screen recorded as never having run while excluding comments — stops
607
+ here instead of being published under your name.
608
+
609
+ Optional in the CLI, and the registry records this contract as `opt-in` rather
610
+ than enforced for exactly that reason: omit the flag and nothing is checked.
611
+ Passing it is the whole of the enforcement.
612
+
601
613
  The judgement is yours; the command owns the mechanics. It routes an inline comment
602
614
  to its thread (`pulls/{n}/comments/{id}/replies`) and a review-submission body or
603
615
  PR-level comment to one top-level comment that names what it answers — GitHub