@mrciphersmith/keryx 0.3.32 → 0.3.34

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/README.md CHANGED
@@ -646,7 +646,10 @@ Grouped by what you are trying to do, not by internal module layout.
646
646
  at, each with its outcome criterion or `not measured — no instrument stated`,
647
647
  and splits the count three ways: no outcome criterion stated, criterion
648
648
  stated but never observed, observed (split by verdict). A criterion goes in
649
- the `## Outcome criteria` slot `flow init` leaves in `description.md`. An
649
+ the `## Outcome criteria` slot `flow init` leaves in `description.md`; who
650
+ wrote it is recorded as `outcomeAuthor` (`flow init --outcome-author
651
+ agent|human`, `flow outcome author <id> agent|human --reason "<why>"`), shown
652
+ by `product open`, and gates nothing. An
650
653
  observation is a line starting `outcome-observed: <verdict> — <note>` in the
651
654
  flow's `journal.md`, with the verdict one of `helped`, `no-effect`, `harmed`,
652
655
  `inconclusive`; a requirements package uses an `## Outcome observations`
package/dist/cli.js CHANGED
@@ -13447,7 +13447,7 @@ var package_default;
13447
13447
  var init_package = __esm(() => {
13448
13448
  package_default = {
13449
13449
  name: "@mrciphersmith/keryx",
13450
- version: "0.3.32",
13450
+ version: "0.3.34",
13451
13451
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
13452
13452
  private: false,
13453
13453
  publishConfig: {
@@ -48615,6 +48615,22 @@ var init_review_gate = __esm(() => {
48615
48615
  REPLY_EVIDENCE_PATTERN = /(https?:\/\/\S+|#(issue|discussion)comment[-_]?\d+|\breplied\b|\breply\b|\banswered\b)/i;
48616
48616
  });
48617
48617
 
48618
+ // src/flow/outcome-author.ts
48619
+ function readOutcomeAuthor(value) {
48620
+ return value === "agent" || value === "human" ? value : "unknown";
48621
+ }
48622
+ function parseOutcomeAuthor(raw, flag = "--outcome-author") {
48623
+ if (raw === "agent" || raw === "human")
48624
+ return raw;
48625
+ const shown = typeof raw === "string" ? `"${raw}"` : "nothing";
48626
+ throw new Error(`${flag} must be one of: ${OUTCOME_AUTHORS.join(", ")} (got ${shown}).`);
48627
+ }
48628
+ var OUTCOME_AUTHORS, OUTCOME_AUTHOR_READINGS, DEFAULT_OUTCOME_AUTHOR = "agent";
48629
+ var init_outcome_author = __esm(() => {
48630
+ OUTCOME_AUTHORS = ["agent", "human"];
48631
+ OUTCOME_AUTHOR_READINGS = [...OUTCOME_AUTHORS, "unknown"];
48632
+ });
48633
+
48618
48634
  // src/flow/identity.ts
48619
48635
  function resolveSignerIdentity(input) {
48620
48636
  const stated = input.stated?.trim();
@@ -49052,6 +49068,11 @@ function flowStateSchema() {
49052
49068
  $ref: "#/definitions/identity",
49053
49069
  description: "The human accountable for this flow, or absent if one was never set. Never inferred: only ever written by an explicit `--owner` on `flow init` or `flow owner set`, so `basis` is always `stated`."
49054
49070
  },
49071
+ outcomeAuthor: {
49072
+ type: "string",
49073
+ enum: ["agent", "human"],
49074
+ 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."
49075
+ },
49055
49076
  signatures: {
49056
49077
  type: "array",
49057
49078
  description: "Append-only signing record for `flow ac confirm` and a passing `flow complete`. Absent means never signed under this field's existence.",
@@ -50830,6 +50851,7 @@ function createFlowService(deps) {
50830
50851
  if (input.owner !== undefined && !input.owner.trim()) {
50831
50852
  throw new Error(BLANK_OWNER_MESSAGE);
50832
50853
  }
50854
+ const outcomeAuthor = input.outcomeAuthor === undefined ? DEFAULT_OUTCOME_AUTHOR : parseOutcomeAuthor(input.outcomeAuthor);
50833
50855
  const trackerReady = deps.tracker ? await deps.tracker.detect() : false;
50834
50856
  const tracker = trackerReady ? deps.tracker : null;
50835
50857
  const issueRef = input.issue && deps.tracker ? deps.tracker.parseRef(input.issue) : null;
@@ -50881,6 +50903,7 @@ function createFlowService(deps) {
50881
50903
  pr: { url: null },
50882
50904
  ...input.baseBranch === undefined ? {} : { baseBranch: input.baseBranch },
50883
50905
  ...input.owner?.trim() ? { owner: ownerIdentity(input.owner.trim(), "`--owner` flag on `flow init`") } : {},
50906
+ outcomeAuthor,
50884
50907
  tasks: DEFAULT_TASKS.map((task) => ({ ...task, status: "todo" })),
50885
50908
  history: [{ at: createdAt, event: "created" }]
50886
50909
  };
@@ -50938,6 +50961,25 @@ function createFlowService(deps) {
50938
50961
  return save(cwd, dir, flow, previous === null ? "owner-set" : "owner-changed", detail);
50939
50962
  });
50940
50963
  },
50964
+ async outcomeAuthorSet({ cwd, id, author, reason }) {
50965
+ const next = parseOutcomeAuthor(author, "outcome author");
50966
+ if (!reason?.trim()) {
50967
+ throw new Error('flow outcome author requires --reason "<why>"');
50968
+ }
50969
+ validateSingleLineReason(reason);
50970
+ let previous = "unknown";
50971
+ let changed = false;
50972
+ const flow = await mutate(cwd, id, async ({ dir, flow: current }) => {
50973
+ previous = readOutcomeAuthor(current.outcomeAuthor);
50974
+ if (previous === next) {
50975
+ return current;
50976
+ }
50977
+ changed = true;
50978
+ current.outcomeAuthor = next;
50979
+ return save(cwd, dir, current, "outcome-author-set", `${previous} -> ${next} (${reason.trim()})`);
50980
+ });
50981
+ return { flow, previous, changed };
50982
+ },
50941
50983
  async freeze({ cwd, id }) {
50942
50984
  return mutate(cwd, id, async ({ dir, flow }) => {
50943
50985
  assertTransition(flow.status, "ready");
@@ -51785,6 +51827,8 @@ var init_service9 = __esm(() => {
51785
51827
  init_validator();
51786
51828
  init_machine();
51787
51829
  init_review_gate();
51830
+ init_outcome_author();
51831
+ init_outcome_author();
51788
51832
  init_flow_move();
51789
51833
  init_ac_reseal();
51790
51834
  init_schema3();
@@ -96328,7 +96372,8 @@ function flowItemFromState(flow, dir) {
96328
96372
  createdAt: flow.createdAt,
96329
96373
  updatedAt: flow.updatedAt,
96330
96374
  source: flow.source.ref ?? flow.source.type,
96331
- tasks: flow.tasks.map((task) => ({ id: task.id, title: task.title, status: task.status }))
96375
+ tasks: flow.tasks.map((task) => ({ id: task.id, title: task.title, status: task.status })),
96376
+ outcomeAuthor: readOutcomeAuthor(flow.outcomeAuthor)
96332
96377
  };
96333
96378
  }
96334
96379
  function localSacGuard() {
@@ -96548,6 +96593,7 @@ function formatFlowDetailLines(item) {
96548
96593
  return [
96549
96594
  `${item.id} ${item.title}`,
96550
96595
  `Status ${item.status}`,
96596
+ `Outcome author ${item.outcomeAuthor ?? "unknown"}`,
96551
96597
  ...item.interrupted ? [` ${item.interrupted}`] : [],
96552
96598
  ...item.uncommitted ? [` note: ${item.uncommitted}`] : [],
96553
96599
  `Dir ${item.dir}`,
@@ -96937,7 +96983,8 @@ function extractFlowIntent(source, repoPath) {
96937
96983
  status: closed ? "closed" : "open",
96938
96984
  flowStatus: status,
96939
96985
  closedAt: closed ? closedAtOf(flow) : null,
96940
- outcome: { criterion, ...observationFrom(source.journal) }
96986
+ outcome: { criterion, ...observationFrom(source.journal) },
96987
+ outcomeAuthor: readOutcomeAuthor(flow.outcomeAuthor)
96941
96988
  };
96942
96989
  }
96943
96990
  function extractDocpackIntent(source, repoPath) {
@@ -97209,6 +97256,8 @@ function indexProblem(value) {
97209
97256
  return "index.json holds an intent with an unusable note";
97210
97257
  if (outcome.observed === true && outcome.verdict === null)
97211
97258
  return "index.json holds an observed intent without a verdict";
97259
+ if (intent.outcomeAuthor !== undefined && !OUTCOME_AUTHOR_READINGS.includes(intent.outcomeAuthor))
97260
+ return "index.json holds an intent with an unknown outcome author";
97212
97261
  }
97213
97262
  return null;
97214
97263
  }
@@ -97244,6 +97293,7 @@ async function checkStaleness(cwd, index) {
97244
97293
  }
97245
97294
  var COUNT_KEYS;
97246
97295
  var init_store7 = __esm(() => {
97296
+ init_service9();
97247
97297
  init_fs();
97248
97298
  init_corpus();
97249
97299
  init_types8();
@@ -97258,7 +97308,8 @@ function buildOpenReport(index) {
97258
97308
  path: intent.path,
97259
97309
  closedAt: intent.closedAt,
97260
97310
  outcome: hasInstrument(intent.outcome.criterion) ? intent.outcome.criterion : intent.outcome.criterion ?? NO_INSTRUMENT,
97261
- hasCriterion: hasInstrument(intent.outcome.criterion)
97311
+ hasCriterion: hasInstrument(intent.outcome.criterion),
97312
+ outcomeAuthor: readOutcomeAuthor(intent.outcomeAuthor)
97262
97313
  })).sort((a, b) => (b.closedAt ?? "").localeCompare(a.closedAt ?? "") || Number.parseInt(b.id, 10) - Number.parseInt(a.id, 10) || (a.path < b.path ? -1 : 1));
97263
97314
  const { counts } = index;
97264
97315
  return {
@@ -97293,6 +97344,7 @@ function openEntryLines(entry) {
97293
97344
  return [
97294
97345
  `${label} ${entry.title}`,
97295
97346
  `${pad}outcome: ${entry.outcome}`,
97347
+ `${pad}outcome author: ${entry.outcomeAuthor}`,
97296
97348
  `${pad}closed ${entry.closedAt === null ? "at an unrecorded time" : entry.closedAt.slice(0, 10)}`
97297
97349
  ];
97298
97350
  }
@@ -97330,6 +97382,7 @@ function renderIndexSummary(index, file) {
97330
97382
  }
97331
97383
  var REBUILD = "Run `keryx product index`.";
97332
97384
  var init_open = __esm(() => {
97385
+ init_service9();
97333
97386
  init_extract3();
97334
97387
  init_store7();
97335
97388
  });
@@ -122434,6 +122487,8 @@ async function flowCommand(args) {
122434
122487
  return await runCheckAcCommand(args.slice(1));
122435
122488
  case "owner":
122436
122489
  return await runOwner(args.slice(1));
122490
+ case "outcome":
122491
+ return await runOutcome(args.slice(1));
122437
122492
  case "implemented":
122438
122493
  return await runImplemented(args.slice(1));
122439
122494
  case "complete":
@@ -122474,7 +122529,8 @@ async function runInit(args) {
122474
122529
  slug: optionValue(args, "--slug"),
122475
122530
  baseBranch: optionValue(args, "--base"),
122476
122531
  owner: optionValue(args, "--owner"),
122477
- requireConfirmation: args.includes("--require-confirmation")
122532
+ requireConfirmation: args.includes("--require-confirmation"),
122533
+ outcomeAuthor: outcomeAuthorFlag(args)
122478
122534
  });
122479
122535
  banner("flow init", `Created flow ${result.flow.id}`);
122480
122536
  console.log(` ${style.green(symbols.ok)} ${style.bold(result.flow.title)}`);
@@ -122484,6 +122540,7 @@ async function runInit(args) {
122484
122540
  console.log(` base: ${result.flow.baseBranch}`);
122485
122541
  }
122486
122542
  console.log(` owner: ${result.flow.owner?.value ?? style.dim("not set")}`);
122543
+ console.log(` outcome author: ${readOutcomeAuthor(result.flow.outcomeAuthor)}`);
122487
122544
  if (result.flow.gates?.confirmation) {
122488
122545
  console.log(` confirmation: required ${style.dim("(a terminal-minted token: `keryx flow confirm <id>`)")}`);
122489
122546
  }
@@ -122502,6 +122559,13 @@ async function runInit(args) {
122502
122559
  `Freeze and start: ${style.cyan(`keryx flow freeze ${result.flow.id}`)} then ${style.cyan(`flow start ${result.flow.id}`)}.`
122503
122560
  ]);
122504
122561
  }
122562
+ function outcomeAuthorFlag(args) {
122563
+ const given = args.filter((arg) => arg === "--outcome-author" || arg.startsWith("--outcome-author=")).length;
122564
+ if (given > 1) {
122565
+ throw new Error("--outcome-author was given more than once; pass it once, as agent or human.");
122566
+ }
122567
+ return given === 1 ? optionValue(args, "--outcome-author") ?? "" : undefined;
122568
+ }
122505
122569
  async function runPlan(args) {
122506
122570
  const id = requireId(args);
122507
122571
  const cwd = process.cwd();
@@ -122598,6 +122662,7 @@ async function runStatus(args) {
122598
122662
  const acLabel = flow.acChecksum ? style.green("frozen") : style.yellow("not frozen");
122599
122663
  console.log(` AC: ${acLabel}, ${Object.keys(flow.acConfirmed).length} confirmed`);
122600
122664
  console.log(` PR: ${flow.pr.url ? style.cyan(flow.pr.url) : style.dim("none")}`);
122665
+ console.log(` outcome author: ${readOutcomeAuthor(flow.outcomeAuthor)}`);
122601
122666
  console.log(` owner: ${flow.owner?.value ? `${flow.owner.value} ${style.dim(`[${flow.owner.basis}]`)}` : style.dim("not set")}`);
122602
122667
  const latestSignature = flow.signatures?.at(-1);
122603
122668
  console.log(` signed: ${latestSignature ? `${latestSignature.identity.value ?? "unknown"} ${style.dim(`[${latestSignature.identity.basis}]`)} ${style.dim(`(${latestSignature.kind}, ${latestSignature.at})`)}${latestSignature.confirmation ? ` ${style.dim(`+ terminal token ${latestSignature.confirmation.tokenRef}`)}` : ""}` : style.dim("no signatures yet")}`);
@@ -122841,6 +122906,34 @@ async function runOwner(args) {
122841
122906
  }
122842
122907
  throw new Error('Usage: keryx flow owner set <id> --owner "<name>" --reason "<why>"');
122843
122908
  }
122909
+ async function runOutcome(args) {
122910
+ if (args[0] !== "author") {
122911
+ throw new Error(OUTCOME_AUTHOR_USAGE);
122912
+ }
122913
+ const rest = args.slice(1);
122914
+ const positionals = [];
122915
+ for (let i = 0;i < rest.length; i += 1) {
122916
+ const arg = rest[i] ?? "";
122917
+ if (arg === "--reason") {
122918
+ i += 1;
122919
+ } else if (!arg.startsWith("--")) {
122920
+ positionals.push(arg);
122921
+ }
122922
+ }
122923
+ const id = positionals[0];
122924
+ const author = positionals[1];
122925
+ if (!id || author === undefined || positionals.length > 2) {
122926
+ throw new Error(OUTCOME_AUTHOR_USAGE);
122927
+ }
122928
+ const result = await getService2().outcomeAuthorSet({
122929
+ cwd: process.cwd(),
122930
+ id,
122931
+ author,
122932
+ reason: optionValue(rest, "--reason") ?? ""
122933
+ });
122934
+ const after = readOutcomeAuthor(result.flow.outcomeAuthor);
122935
+ console.log(result.changed ? ` ${style.green(symbols.ok)} Outcome author ${result.previous} ${style.cyan(symbols.arrow)} ${style.bold(after)}` : ` ${style.dim(symbols.bullet)} Outcome author already ${style.bold(after)}; nothing written`);
122936
+ }
122844
122937
  async function runAc(args) {
122845
122938
  const sub = args[0];
122846
122939
  if (sub === "confirm") {
@@ -123188,6 +123281,7 @@ function printHelp5() {
123188
123281
  'keryx flow task attempt <id> <taskId> --outcome started|failed|blocked [--detail "<what happened>"]',
123189
123282
  'keryx flow task depends <id> <taskId> --on T1,T2|none --reason "<why>" (repair an unsatisfiable dependsOn)',
123190
123283
  'keryx flow owner set <id> --owner "<name>" --reason "<why>" (the human accountable; never inferred)',
123284
+ 'keryx flow outcome author <id> agent|human --reason "<why>" (who wrote the outcome criterion; journaled, gates nothing)',
123191
123285
  'keryx flow ac confirm <id> <ACn> [--note "<evidence>"] [--signed-by "<name>"]',
123192
123286
  'keryx flow ac update <id> --reason "<why>" (re-freeze the file as already edited; VOIDS prior confirmations)',
123193
123287
  'keryx flow ac update <id> --criterion ACn --text "<criterion>" --reason "<why>" (rewrite/append that one criterion, then re-freeze; VOIDS prior confirmations)',
@@ -123207,9 +123301,10 @@ function printHelp5() {
123207
123301
  "keryx flow schema [--out <path>]"
123208
123302
  ]);
123209
123303
  note("`--signed-by` names the signer explicitly (stated). Falls back to KERYX_ACTOR (also " + "stated), then to `git config user.email` in this checkout (derived \u2014 the person who " + "RAN the command, not necessarily who signed), then to `unknown`. None of these is proof a human signed: a flag, an environment variable, and a local git identity can all be set " + "by an agent. `--owner` is never inferred at all \u2014 see docs/decisions/keryx-harness/.");
123304
+ note("`flow init --outcome-author agent|human` records who wrote the outcome criterion: `agent` (the default when the flag " + "is absent) or `human`, and `human` only when the flag says so \u2014 never inferred from a git " + "identity, an owner or the environment. A flow without the field reads `unknown`. The flag labels a sample and gates nothing.");
123210
123305
  note("`flow confirm` mints a short-lived, single-use token only for a flow created with `--require-confirmation` (or `completion.require_confirmation: true`). " + CONFIRMATION_CAVEAT + " See docs/decisions/keryx-harness/TM-03-terminal-confirmation-token.md.");
123211
123306
  }
123212
- var VALID_TASK_KINDS, WARN = "!", AC_CONFIRM_FLAGS, AC_UPDATE_FLAGS, AC_RESEAL_FLAGS, VALID_DISPOSITIONS, service5 = null, CHALLENGE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
123307
+ var VALID_TASK_KINDS, WARN = "!", AC_CONFIRM_FLAGS, AC_UPDATE_FLAGS, AC_RESEAL_FLAGS, VALID_DISPOSITIONS, service5 = null, OUTCOME_AUTHOR_USAGE = 'Usage: keryx flow outcome author <id> agent|human --reason "<why>"', CHALLENGE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
123213
123308
  var init_flow = __esm(() => {
123214
123309
  init_args();
123215
123310
  init_fs();
@@ -159494,7 +159589,7 @@ var init_command_registry = __esm(() => {
159494
159589
  {
159495
159590
  module: "tasks",
159496
159591
  command: "flow status",
159497
- summary: "One flow's full status: lifecycle state, AC freeze/confirmation count, PR, owner, latest signature, task list, and recent history.",
159592
+ summary: "One flow's full status: lifecycle state, outcome author (agent, human or unknown), AC freeze/confirmation count, PR, owner, latest signature, task list, and recent history.",
159498
159593
  intent: ["\u0441\u0442\u0430\u0442\u0443\u0441 \u0444\u043B\u043E\u0443", "flow status", "flow details", "show one flow"],
159499
159594
  args: [{ name: "<id>", type: "string", required: true, desc: "flow id" }],
159500
159595
  json: false,
@@ -159503,7 +159598,7 @@ var init_command_registry = __esm(() => {
159503
159598
  {
159504
159599
  module: "tasks",
159505
159600
  command: "flow init",
159506
- summary: "Create a new flow (managed work item): allocates an id, scaffolds its directory, and collects initial " + "context from the source issue if one is given. `--owner` is NEVER inferred \u2014 only an explicit value " + "on this command populates it.",
159601
+ summary: "Create a new flow (managed work item): allocates an id, scaffolds its directory, and collects initial " + "context from the source issue if one is given. `--owner` is NEVER inferred \u2014 only an explicit value " + "on this command populates it. `--outcome-author` records who wrote the outcome criterion (default " + "`agent`; `human` only when the flag says so).",
159507
159602
  intent: ["\u0441\u043E\u0437\u0434\u0430\u0439 \u0444\u043B\u043E\u0443", "flow init", "start a new flow", "new managed work item", "\u0437\u0430\u0432\u0435\u0434\u0438 \u0444\u043B\u043E\u0443"],
159508
159603
  args: [
159509
159604
  { name: "title", type: "string", required: false, desc: "work title; required unless --issue is given" },
@@ -159516,6 +159611,12 @@ var init_command_registry = __esm(() => {
159516
159611
  type: "bool",
159517
159612
  required: false,
159518
159613
  desc: "opt this flow into the confirmation gate: `flow complete` then needs a token minted by `flow confirm` (flow 299)"
159614
+ },
159615
+ {
159616
+ name: "outcome-author",
159617
+ type: "string",
159618
+ required: false,
159619
+ desc: "who wrote the outcome criterion: agent (the default when omitted) or human; never inferred, any other value is refused before the flow is created; gates nothing"
159519
159620
  }
159520
159621
  ],
159521
159622
  json: false,
@@ -159539,6 +159640,20 @@ var init_command_registry = __esm(() => {
159539
159640
  read: false,
159540
159641
  sideEffects: ["writes flow.json's owner field (basis: stated) and appends a history entry"]
159541
159642
  },
159643
+ {
159644
+ module: "tasks",
159645
+ command: "flow outcome author",
159646
+ summary: "Change who wrote a flow's outcome criterion (agent or human). Requires a reason; appends one journal line " + "naming the old value (or `unknown`), the new value and the reason, and writes nothing when the value is unchanged. Gates nothing.",
159647
+ intent: ["\u043A\u0442\u043E \u043D\u0430\u043F\u0438\u0441\u0430\u043B \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0438\u0441\u0445\u043E\u0434\u0430", "flow outcome author", "set outcome author", "mark outcome criterion as human", "\u0430\u0432\u0442\u043E\u0440 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u044F \u0438\u0441\u0445\u043E\u0434\u0430"],
159648
+ args: [
159649
+ { name: "<id>", type: "string", required: true, desc: "flow id" },
159650
+ { name: "<agent|human>", type: "string", required: true, desc: "who wrote the outcome criterion" },
159651
+ { name: "reason", type: "string", required: true, desc: "why the author is being set or changed; one line" }
159652
+ ],
159653
+ json: false,
159654
+ read: false,
159655
+ sideEffects: ["writes flow.json's outcomeAuthor field and appends one journal.md line and a history entry"]
159656
+ },
159542
159657
  {
159543
159658
  module: "tasks",
159544
159659
  command: "flow ac confirm",
@@ -183491,6 +183606,7 @@ var init_group_subcommands = __esm(() => {
183491
183606
  "ac",
183492
183607
  "check-ac",
183493
183608
  "owner",
183609
+ "outcome",
183494
183610
  "implemented",
183495
183611
  "complete",
183496
183612
  "confirm",
package/dist/core.js CHANGED
@@ -21629,6 +21629,9 @@ __export(exports_service2, {
21629
21629
  AC_CHECK_TOKEN_BUDGET: () => AC_CHECK_TOKEN_BUDGET,
21630
21630
  AC_KINDS: () => AC_KINDS,
21631
21631
  CONFIRMATION_CAVEAT: () => CONFIRMATION_CAVEAT,
21632
+ DEFAULT_OUTCOME_AUTHOR: () => DEFAULT_OUTCOME_AUTHOR,
21633
+ OUTCOME_AUTHORS: () => OUTCOME_AUTHORS,
21634
+ OUTCOME_AUTHOR_READINGS: () => OUTCOME_AUTHOR_READINGS,
21632
21635
  OUTCOME_HINT: () => OUTCOME_HINT,
21633
21636
  REVIEW_GATE_CONFIG_PATH: () => REVIEW_GATE_CONFIG_PATH,
21634
21637
  acCheckCacheKey: () => acCheckCacheKey,
@@ -21655,11 +21658,13 @@ __export(exports_service2, {
21655
21658
  isFrozen: () => isFrozen,
21656
21659
  notCheckableVerdict: () => notCheckableVerdict,
21657
21660
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
21661
+ parseOutcomeAuthor: () => parseOutcomeAuthor,
21658
21662
  proseOutsideFences: () => proseOutsideFences,
21659
21663
  readAcCheckCache: () => readAcCheckCache,
21660
21664
  readAcCheckEnabled: () => readAcCheckEnabled,
21661
21665
  readAcKindRecords: () => readAcKindRecords,
21662
21666
  readFlow: () => readFlow,
21667
+ readOutcomeAuthor: () => readOutcomeAuthor,
21663
21668
  renderAcCheckAdvisoryNotice: () => renderAcCheckAdvisoryNotice,
21664
21669
  renderAcCheckReport: () => renderAcCheckReport,
21665
21670
  renderAcKindDistribution: () => renderAcKindDistribution,
@@ -23122,6 +23127,19 @@ async function reviewGate(input) {
23122
23127
  return { name: "review", status: verdict.status, detail: verdict.detail };
23123
23128
  }
23124
23129
 
23130
+ // src/flow/outcome-author.ts
23131
+ var OUTCOME_AUTHORS = ["agent", "human"];
23132
+ var OUTCOME_AUTHOR_READINGS = [...OUTCOME_AUTHORS, "unknown"];
23133
+ var DEFAULT_OUTCOME_AUTHOR = "agent";
23134
+ function readOutcomeAuthor(value) {
23135
+ return value === "agent" || value === "human" ? value : "unknown";
23136
+ }
23137
+ function parseOutcomeAuthor(raw, flag = "--outcome-author") {
23138
+ if (raw === "agent" || raw === "human")
23139
+ return raw;
23140
+ const shown = typeof raw === "string" ? `"${raw}"` : "nothing";
23141
+ throw new Error(`${flag} must be one of: ${OUTCOME_AUTHORS.join(", ")} (got ${shown}).`);
23142
+ }
23125
23143
  // src/flow/identity.ts
23126
23144
  function resolveSignerIdentity(input) {
23127
23145
  const stated = input.stated?.trim();
@@ -23408,6 +23426,11 @@ function flowStateSchema() {
23408
23426
  $ref: "#/definitions/identity",
23409
23427
  description: "The human accountable for this flow, or absent if one was never set. Never inferred: only ever written by an explicit `--owner` on `flow init` or `flow owner set`, so `basis` is always `stated`."
23410
23428
  },
23429
+ outcomeAuthor: {
23430
+ type: "string",
23431
+ enum: ["agent", "human"],
23432
+ 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."
23433
+ },
23411
23434
  signatures: {
23412
23435
  type: "array",
23413
23436
  description: "Append-only signing record for `flow ac confirm` and a passing `flow complete`. Absent means never signed under this field's existence.",
@@ -24890,6 +24913,7 @@ function createFlowService(deps) {
24890
24913
  if (input.owner !== undefined && !input.owner.trim()) {
24891
24914
  throw new Error(BLANK_OWNER_MESSAGE);
24892
24915
  }
24916
+ const outcomeAuthor = input.outcomeAuthor === undefined ? DEFAULT_OUTCOME_AUTHOR : parseOutcomeAuthor(input.outcomeAuthor);
24893
24917
  const trackerReady = deps.tracker ? await deps.tracker.detect() : false;
24894
24918
  const tracker = trackerReady ? deps.tracker : null;
24895
24919
  const issueRef = input.issue && deps.tracker ? deps.tracker.parseRef(input.issue) : null;
@@ -24941,6 +24965,7 @@ function createFlowService(deps) {
24941
24965
  pr: { url: null },
24942
24966
  ...input.baseBranch === undefined ? {} : { baseBranch: input.baseBranch },
24943
24967
  ...input.owner?.trim() ? { owner: ownerIdentity(input.owner.trim(), "`--owner` flag on `flow init`") } : {},
24968
+ outcomeAuthor,
24944
24969
  tasks: DEFAULT_TASKS.map((task) => ({ ...task, status: "todo" })),
24945
24970
  history: [{ at: createdAt, event: "created" }]
24946
24971
  };
@@ -24998,6 +25023,25 @@ function createFlowService(deps) {
24998
25023
  return save(cwd, dir, flow, previous === null ? "owner-set" : "owner-changed", detail);
24999
25024
  });
25000
25025
  },
25026
+ async outcomeAuthorSet({ cwd, id, author, reason }) {
25027
+ const next = parseOutcomeAuthor(author, "outcome author");
25028
+ if (!reason?.trim()) {
25029
+ throw new Error('flow outcome author requires --reason "<why>"');
25030
+ }
25031
+ validateSingleLineReason(reason);
25032
+ let previous = "unknown";
25033
+ let changed = false;
25034
+ const flow = await mutate(cwd, id, async ({ dir, flow: current }) => {
25035
+ previous = readOutcomeAuthor(current.outcomeAuthor);
25036
+ if (previous === next) {
25037
+ return current;
25038
+ }
25039
+ changed = true;
25040
+ current.outcomeAuthor = next;
25041
+ return save(cwd, dir, current, "outcome-author-set", `${previous} -> ${next} (${reason.trim()})`);
25042
+ });
25043
+ return { flow, previous, changed };
25044
+ },
25001
25045
  async freeze({ cwd, id }) {
25002
25046
  return mutate(cwd, id, async ({ dir, flow }) => {
25003
25047
  assertTransition(flow.status, "ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
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": {