@mrciphersmith/keryx 0.3.58 → 0.3.59

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 (3) hide show
  1. package/dist/cli.js +537 -24
  2. package/dist/core.js +240 -9
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20104,7 +20104,7 @@ var package_default;
20104
20104
  var init_package = __esm(() => {
20105
20105
  package_default = {
20106
20106
  name: "@mrciphersmith/keryx",
20107
- version: "0.3.58",
20107
+ version: "0.3.59",
20108
20108
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20109
20109
  private: false,
20110
20110
  publishConfig: {
@@ -56553,6 +56553,125 @@ var init_outcome_author = __esm(() => {
56553
56553
  OUTCOME_AUTHOR_READINGS = [...OUTCOME_AUTHORS, "unknown"];
56554
56554
  });
56555
56555
 
56556
+ // src/flow/origin.ts
56557
+ function isOriginKind(value) {
56558
+ return typeof value === "string" && ORIGIN_KINDS.includes(value);
56559
+ }
56560
+ function nonBlank(value) {
56561
+ return typeof value === "string" && value.trim().length > 0;
56562
+ }
56563
+ function readOrigin(value) {
56564
+ if (value === null || typeof value !== "object" || Array.isArray(value))
56565
+ return;
56566
+ const raw = value;
56567
+ if (!isOriginKind(raw.kind))
56568
+ return;
56569
+ const origin = { kind: raw.kind };
56570
+ if (typeof raw.quote === "string" && raw.quote.length > 0)
56571
+ origin.quote = raw.quote;
56572
+ if (typeof raw.source === "string" && raw.source.length > 0)
56573
+ origin.source = raw.source;
56574
+ return origin;
56575
+ }
56576
+ function readOriginKind(value) {
56577
+ return readOrigin(value)?.kind ?? "unknown";
56578
+ }
56579
+ function resolveOrigin(input) {
56580
+ if (!isOriginKind(input.kind)) {
56581
+ return {
56582
+ note: `origin kind "${input.kind}" is not one of: ${ORIGIN_KINDS.join(", ")}; origin stays unknown.`
56583
+ };
56584
+ }
56585
+ const hasQuote = nonBlank(input.quote);
56586
+ const hasSource = nonBlank(input.source);
56587
+ if (input.kind === "human-request") {
56588
+ if (!hasQuote && !hasSource) {
56589
+ return { note: "origin human-request needs a verbatim --quote and a --source; neither was given, origin stays unknown." };
56590
+ }
56591
+ if (!hasQuote) {
56592
+ return { note: "origin human-request needs a verbatim --quote of the human's first message; none was given, origin stays unknown." };
56593
+ }
56594
+ if (!hasSource) {
56595
+ return { note: "origin human-request needs a --source (channel, message id or time); none was given, origin stays unknown." };
56596
+ }
56597
+ } else if (!hasSource) {
56598
+ return { note: `origin ${input.kind} needs a --source (the check, review or test that produced it); none was given, origin stays unknown.` };
56599
+ }
56600
+ const origin = { kind: input.kind };
56601
+ if (hasQuote)
56602
+ origin.quote = input.quote;
56603
+ if (hasSource)
56604
+ origin.source = input.source;
56605
+ return { origin };
56606
+ }
56607
+ function effectiveOutcomeAuthor(flow) {
56608
+ const explicit = readOutcomeAuthor(flow.outcomeAuthor);
56609
+ if (explicit !== "unknown")
56610
+ return explicit;
56611
+ return readOrigin(flow.origin) === undefined ? "unknown" : "agent";
56612
+ }
56613
+ function originSummary(origin) {
56614
+ if (origin === undefined)
56615
+ return "unknown";
56616
+ const parts = [origin.kind];
56617
+ if (origin.quote !== undefined)
56618
+ parts.push(`quote: ${JSON.stringify(origin.quote)}`);
56619
+ if (origin.source !== undefined)
56620
+ parts.push(`source: ${origin.source}`);
56621
+ return parts.join("; ");
56622
+ }
56623
+ function originDetailLines(origin, indent = "") {
56624
+ if (origin === undefined)
56625
+ return [`${indent}origin: unknown`];
56626
+ const lines = [`${indent}origin: ${origin.kind}`];
56627
+ if (origin.quote !== undefined) {
56628
+ const [first = "", ...rest] = origin.quote.split(`
56629
+ `);
56630
+ lines.push(`${indent} quote: \xAB${first}`, ...rest.map((line) => `${indent} ${line}`));
56631
+ lines[lines.length - 1] += "\xBB";
56632
+ }
56633
+ if (origin.source !== undefined)
56634
+ lines.push(`${indent} source: ${origin.source}`);
56635
+ return lines;
56636
+ }
56637
+ function renderOriginBullets(origin) {
56638
+ const first = [];
56639
+ if (origin.kind === "human-request") {
56640
+ first.push(...bulletWithText(REQUEST_LABEL, origin.quote === undefined ? undefined : `\xAB${origin.quote}\xBB`, REQUEST_PLACEHOLDER));
56641
+ if (origin.source !== undefined)
56642
+ first[first.length - 1] += ` (source: ${origin.source})`;
56643
+ } else {
56644
+ first.push(...bulletWithText(SOURCE_LABEL, origin.source, SOURCE_PLACEHOLDER));
56645
+ if (origin.quote !== undefined)
56646
+ first.push(...bulletWithText(REQUEST_LABEL, `\xAB${origin.quote}\xBB`, REQUEST_PLACEHOLDER));
56647
+ }
56648
+ return [
56649
+ ...first,
56650
+ `- ${FORMALIZATION_LABEL}: ${FORMALIZATION_PLACEHOLDER}`,
56651
+ `- ${OBSERVE_LABEL}: ${OBSERVE_PLACEHOLDER}`
56652
+ ];
56653
+ }
56654
+ function bulletWithText(label, text, placeholder) {
56655
+ if (text === undefined)
56656
+ return [`- ${label}: ${placeholder}`];
56657
+ const lines = text.split(`
56658
+ `);
56659
+ const head = `- ${label}: ${lines[0] ?? ""}`;
56660
+ return [head, ...lines.slice(1).map((line) => ` ${line}`)];
56661
+ }
56662
+ var ORIGIN_KINDS, ORIGIN_READINGS, REQUEST_LABEL = "\u0417\u0430\u043F\u0440\u043E\u0441 (\u0434\u043E\u0441\u043B\u043E\u0432\u043D\u043E)", SOURCE_LABEL = "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", 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)", 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)", REQUEST_PLACEHOLDER = "<the human's request, verbatim, no translation or paraphrase>", SOURCE_PLACEHOLDER = "<where the finding or proposal came from: check, review, test or discussion>", FORMALIZATION_PLACEHOLDER = "<the effect that has to be true when this flow is done, in your own words>", OBSERVE_PLACEHOLDER = "<how the effect can be observed once the flow is done>", ORIGIN_PLACEHOLDER_BULLETS;
56663
+ var init_origin = __esm(() => {
56664
+ init_outcome_author();
56665
+ ORIGIN_KINDS = ["human-request", "agent-finding", "agent-proposal"];
56666
+ ORIGIN_READINGS = [...ORIGIN_KINDS, "unknown"];
56667
+ ORIGIN_PLACEHOLDER_BULLETS = [
56668
+ `- ${REQUEST_LABEL}: ${REQUEST_PLACEHOLDER}`,
56669
+ `- ${SOURCE_LABEL}: ${SOURCE_PLACEHOLDER}`,
56670
+ `- ${FORMALIZATION_LABEL}: ${FORMALIZATION_PLACEHOLDER}`,
56671
+ `- ${OBSERVE_LABEL}: ${OBSERVE_PLACEHOLDER}`
56672
+ ];
56673
+ });
56674
+
56556
56675
  // src/flow/identity.ts
56557
56676
  function resolveSignerIdentity(input) {
56558
56677
  const stated = input.stated?.trim();
@@ -56999,6 +57118,23 @@ function flowStateSchema() {
56999
57118
  enum: ["agent", "human"],
57000
57119
  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."
57001
57120
  },
57121
+ origin: {
57122
+ type: "object",
57123
+ additionalProperties: false,
57124
+ required: ["kind"],
57125
+ 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.",
57126
+ properties: {
57127
+ kind: { type: "string", enum: ["human-request", "agent-finding", "agent-proposal"] },
57128
+ quote: {
57129
+ type: "string",
57130
+ description: "The human's request, verbatim and byte for byte: no translation, no paraphrase."
57131
+ },
57132
+ source: {
57133
+ type: "string",
57134
+ description: "Where the request or finding came from: channel, message id, time, or the check that produced it."
57135
+ }
57136
+ }
57137
+ },
57002
57138
  signatures: {
57003
57139
  type: "array",
57004
57140
  description: "Append-only signing record for `flow ac confirm` and a passing `flow complete`. Absent means never signed under this field's existence.",
@@ -57897,17 +58033,28 @@ function outcomeBulletsFrom(markdown) {
57897
58033
  if (body === null)
57898
58034
  return null;
57899
58035
  const bullets = [];
58036
+ let skipping = false;
57900
58037
  for (const line of body.split(/\r?\n/)) {
57901
58038
  const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
57902
58039
  if ((bullet?.[1] ?? line).trim() === OUTCOME_HINT)
57903
58040
  continue;
57904
- if (bullet !== null)
57905
- bullets.push((bullet[1] ?? "").trim());
57906
- else if (line.trim().length > 0 && bullets.length > 0)
58041
+ if (bullet !== null) {
58042
+ skipping = isOriginTemplateBullet(bullet[1] ?? "");
58043
+ if (!skipping)
58044
+ bullets.push((bullet[1] ?? "").trim());
58045
+ } else if (line.trim().length > 0 && !skipping && bullets.length > 0)
57907
58046
  bullets[bullets.length - 1] += ` ${line.trim()}`;
57908
58047
  }
57909
58048
  return bullets.filter((bullet) => bullet.length > 0);
57910
58049
  }
58050
+ function isOriginTemplateBullet(text) {
58051
+ const trimmed = text.trim();
58052
+ return trimmed.startsWith(`${REQUEST_LABEL}:`) || trimmed.startsWith(`${SOURCE_LABEL}:`) || ORIGIN_PLACEHOLDER_BULLETS.some((placeholder) => placeholder.slice(2) === trimmed);
58053
+ }
58054
+ function isOriginPlaceholderBullet(text) {
58055
+ const trimmed = text.trim();
58056
+ return ORIGIN_PLACEHOLDER_BULLETS.some((placeholder) => placeholder.slice(2) === trimmed);
58057
+ }
57911
58058
  function initIntentNote(description) {
57912
58059
  return flowStatementFrom(description) === null ? INIT_INTENT_NOTE : null;
57913
58060
  }
@@ -57920,6 +58067,7 @@ async function intentNoteForNewFlow(cwd, dir) {
57920
58067
  }
57921
58068
  var OUTCOME_HINT = 'State an outcome criterion: what you would look at afterwards to see this helped, as a bullet. Or write "- not measured \u2014 <reason>".', PLACEHOLDERS2, FLOW_STATEMENT_HEADINGS, STATEMENT_LIMIT = 300, INIT_INTENT_NOTE = "note: no intent statement extractable from description.md \u2014 the product index will hold no statement for this flow";
57922
58069
  var init_description_intent = __esm(() => {
58070
+ init_origin();
57923
58071
  PLACEHOLDERS2 = ["Describe the problem precisely", "What must be true when this flow is done"];
57924
58072
  FLOW_STATEMENT_HEADINGS = [/^problem$/i, /^expected outcome$/i];
57925
58073
  });
@@ -58508,7 +58656,9 @@ var init_folder_committed = __esm(() => {
58508
58656
  });
58509
58657
 
58510
58658
  // src/flow/templates.ts
58511
- function renderDescription(title, source) {
58659
+ function renderDescription(title, source, origin) {
58660
+ const outcome = origin === undefined ? OUTCOME_HINT : renderOriginBullets(origin).join(`
58661
+ `);
58512
58662
  return `# ${title}
58513
58663
 
58514
58664
  Status: draft (flow-init skill formalizes this)
@@ -58524,7 +58674,7 @@ What must be true when this flow is done.
58524
58674
 
58525
58675
  ## Outcome criteria
58526
58676
 
58527
- ${OUTCOME_HINT}
58677
+ ${outcome}
58528
58678
 
58529
58679
  ## Out of Scope
58530
58680
 
@@ -58903,6 +59053,7 @@ whose status is \`implemented\`.
58903
59053
  var init_templates4 = __esm(() => {
58904
59054
  init_default_tasks();
58905
59055
  init_description_intent();
59056
+ init_origin();
58906
59057
  });
58907
59058
 
58908
59059
  // src/flow/service.ts
@@ -58910,6 +59061,8 @@ var exports_service2 = {};
58910
59061
  __export(exports_service2, {
58911
59062
  AC_CHECK_TOKEN_BUDGET: () => AC_CHECK_TOKEN_BUDGET,
58912
59063
  CONFIRMATION_CAVEAT: () => CONFIRMATION_CAVEAT,
59064
+ ORIGIN_KINDS: () => ORIGIN_KINDS,
59065
+ ORIGIN_READINGS: () => ORIGIN_READINGS,
58913
59066
  OUTCOME_AUTHOR_READINGS: () => OUTCOME_AUTHOR_READINGS,
58914
59067
  REVIEW_GATE_CONFIG_PATH: () => REVIEW_GATE_CONFIG_PATH,
58915
59068
  acCheckCacheKey: () => acCheckCacheKey,
@@ -58924,6 +59077,7 @@ __export(exports_service2, {
58924
59077
  confirmPreconditionError: () => confirmPreconditionError,
58925
59078
  createFlowService: () => createFlowService,
58926
59079
  describeAcKind: () => describeAcKind,
59080
+ effectiveOutcomeAuthor: () => effectiveOutcomeAuthor,
58927
59081
  evaluatedVerdict: () => evaluatedVerdict,
58928
59082
  extractCriterionTokens: () => extractCriterionTokens,
58929
59083
  factsOnlyVerdict: () => factsOnlyVerdict,
@@ -58933,6 +59087,7 @@ __export(exports_service2, {
58933
59087
  intentNoteForNewFlow: () => intentNoteForNewFlow,
58934
59088
  isFrozen: () => isFrozen,
58935
59089
  notCheckableVerdict: () => notCheckableVerdict,
59090
+ originDetailLines: () => originDetailLines,
58936
59091
  outcomeBulletsFrom: () => outcomeBulletsFrom,
58937
59092
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
58938
59093
  proseOutsideFences: () => proseOutsideFences,
@@ -58940,7 +59095,8 @@ __export(exports_service2, {
58940
59095
  readAcCheckEnabled: () => readAcCheckEnabled,
58941
59096
  readAcKindRecords: () => readAcKindRecords,
58942
59097
  readFlow: () => readFlow,
58943
- readOutcomeAuthor: () => readOutcomeAuthor,
59098
+ readOrigin: () => readOrigin,
59099
+ readOriginKind: () => readOriginKind,
58944
59100
  renderAcCheckAdvisoryNotice: () => renderAcCheckAdvisoryNotice,
58945
59101
  renderAcCheckReport: () => renderAcCheckReport,
58946
59102
  renderAcKindDistribution: () => renderAcKindDistribution,
@@ -59203,7 +59359,24 @@ function createFlowService(deps) {
59203
59359
  if (input.owner !== undefined && !input.owner.trim()) {
59204
59360
  throw new Error(BLANK_OWNER_MESSAGE);
59205
59361
  }
59206
- const outcomeAuthor = input.outcomeAuthor === undefined ? DEFAULT_OUTCOME_AUTHOR : parseOutcomeAuthor(input.outcomeAuthor);
59362
+ const outcomeAuthor = input.outcomeAuthor === undefined ? undefined : parseOutcomeAuthor(input.outcomeAuthor);
59363
+ let originNote;
59364
+ let origin;
59365
+ let originDraft;
59366
+ if (input.origin !== undefined) {
59367
+ const resolution = resolveOrigin({ kind: input.origin, quote: input.originQuote, source: input.originSource });
59368
+ origin = resolution.origin;
59369
+ originNote = resolution.note;
59370
+ if (isOriginKind(input.origin)) {
59371
+ originDraft = {
59372
+ kind: input.origin,
59373
+ ...input.originQuote?.trim() ? { quote: input.originQuote } : {},
59374
+ ...input.originSource?.trim() ? { source: input.originSource } : {}
59375
+ };
59376
+ }
59377
+ } else if (input.originQuote !== undefined || input.originSource !== undefined) {
59378
+ originNote = "--quote/--source were given without --origin; origin stays unknown.";
59379
+ }
59207
59380
  const trackerReady = deps.tracker ? await deps.tracker.detect() : false;
59208
59381
  const tracker = trackerReady ? deps.tracker : null;
59209
59382
  const issueRef = input.issue && deps.tracker ? deps.tracker.parseRef(input.issue) : null;
@@ -59259,12 +59432,13 @@ function createFlowService(deps) {
59259
59432
  pr: { url: null },
59260
59433
  ...input.baseBranch === undefined ? {} : { baseBranch: input.baseBranch },
59261
59434
  ...input.owner?.trim() ? { owner: ownerIdentity(input.owner.trim(), "`--owner` flag on `flow init`") } : {},
59262
- outcomeAuthor,
59435
+ ...outcomeAuthor !== undefined ? { outcomeAuthor } : origin === undefined ? { outcomeAuthor: DEFAULT_OUTCOME_AUTHOR } : {},
59436
+ ...origin === undefined ? {} : { origin },
59263
59437
  tasks: DEFAULT_TASKS.map((task) => ({ ...task, status: "todo" })),
59264
59438
  history: [{ at: createdAt, event: "created" }]
59265
59439
  };
59266
59440
  const sourceLabel = input.issue ?? "user description";
59267
- await writeFileAtomic(path143.join(absolute, "description.md"), renderDescription(title, sourceLabel));
59441
+ await writeFileAtomic(path143.join(absolute, "description.md"), renderDescription(title, originDraft === undefined ? sourceLabel : `${sourceLabel} (origin: ${originDraft.kind})`, originDraft));
59268
59442
  await writeFileAtomic(path143.join(absolute, "context.md"), context.markdown);
59269
59443
  await writeFileAtomic(path143.join(absolute, "plan.md"), renderPlan());
59270
59444
  await writeFileAtomic(path143.join(absolute, "tasks.md"), renderTasksDoc());
@@ -59272,7 +59446,12 @@ function createFlowService(deps) {
59272
59446
  await writeFileAtomic(path143.join(absolute, "journal.md"), renderJournal(createdAt));
59273
59447
  await writeFlow(input.cwd, dir, flow);
59274
59448
  await recordAllocation(scope, { id, dir, at: createdAt, project: scope.project });
59275
- return { flow, dir: path143.relative(input.cwd, absolute), contextNotes: context.notes };
59449
+ return {
59450
+ flow,
59451
+ dir: path143.relative(input.cwd, absolute),
59452
+ contextNotes: context.notes,
59453
+ ...originNote === undefined ? {} : { originNote }
59454
+ };
59276
59455
  });
59277
59456
  },
59278
59457
  async list({ cwd }) {
@@ -59336,6 +59515,50 @@ function createFlowService(deps) {
59336
59515
  });
59337
59516
  return { flow, previous, changed };
59338
59517
  },
59518
+ async originSet({ cwd, id, kind, reason, quote, source }) {
59519
+ if (!reason?.trim()) {
59520
+ throw new Error('flow origin set requires --reason "<why>"');
59521
+ }
59522
+ validateSingleLineReason(reason);
59523
+ let previous = "unknown";
59524
+ let next = "unknown";
59525
+ let changed = false;
59526
+ let note;
59527
+ const flow = await mutate(cwd, id, async ({ dir, flow: current }) => {
59528
+ const before = readOrigin(current.origin);
59529
+ previous = before?.kind ?? "unknown";
59530
+ let after;
59531
+ if (kind !== "unknown" && !isOriginKind(kind)) {
59532
+ note = `origin kind must be one of: human-request, agent-finding, agent-proposal, unknown (got "${kind}"); the origin is unchanged.`;
59533
+ next = previous;
59534
+ return current;
59535
+ }
59536
+ if (kind !== "unknown") {
59537
+ const resolution = resolveOrigin({
59538
+ kind,
59539
+ quote: quote !== undefined && quote.length > 0 ? quote : before?.quote,
59540
+ source: source !== undefined && source.length > 0 ? source : before?.source
59541
+ });
59542
+ if (resolution.origin === undefined) {
59543
+ note = resolution.note;
59544
+ next = previous;
59545
+ return current;
59546
+ }
59547
+ after = resolution.origin;
59548
+ }
59549
+ next = after?.kind ?? "unknown";
59550
+ if (JSON.stringify(before ?? null) === JSON.stringify(after ?? null)) {
59551
+ return current;
59552
+ }
59553
+ changed = true;
59554
+ if (after === undefined)
59555
+ delete current.origin;
59556
+ else
59557
+ current.origin = after;
59558
+ return save(cwd, dir, current, "origin-set", `${originSummary(before)} -> ${originSummary(after)} (${reason.trim()})`);
59559
+ });
59560
+ return { flow, previous, next, changed, ...note === undefined ? {} : { note } };
59561
+ },
59339
59562
  async freeze({ cwd, id }) {
59340
59563
  return mutate(cwd, id, async ({ dir, flow }) => {
59341
59564
  assertTransition(flow.status, "ready");
@@ -60176,7 +60399,9 @@ var init_service9 = __esm(() => {
60176
60399
  init_machine();
60177
60400
  init_review_gate();
60178
60401
  init_outcome_author();
60402
+ init_origin();
60179
60403
  init_outcome_author();
60404
+ init_origin();
60180
60405
  init_flow_move();
60181
60406
  init_ac_reseal();
60182
60407
  init_schema3();
@@ -100970,6 +101195,12 @@ var init_help_groups = __esm(() => {
100970
101195
  group: "Managed work",
100971
101196
  summary: "List doc sections that likely went stale because of the working diff, with Jev."
100972
101197
  },
101198
+ {
101199
+ kind: "slash",
101200
+ name: "/flow",
101201
+ group: "Managed work",
101202
+ summary: `Show a flow's origin, or set it with a reason \u2014 /flow origin [<id>] | /flow origin <id> <kind> --reason "...".`
101203
+ },
100973
101204
  {
100974
101205
  kind: "slash",
100975
101206
  name: "/opencomments",
@@ -105351,6 +105582,8 @@ async function flowCommand(args) {
105351
105582
  return await runOwner(args.slice(1));
105352
105583
  case "outcome":
105353
105584
  return await runOutcome(args.slice(1));
105585
+ case "origin":
105586
+ return await runOrigin(args.slice(1));
105354
105587
  case "implemented":
105355
105588
  return await runImplemented(args.slice(1));
105356
105589
  case "complete":
@@ -105394,7 +105627,10 @@ async function runInit(args) {
105394
105627
  baseBranch: optionValue(args, "--base"),
105395
105628
  owner: optionValue(args, "--owner"),
105396
105629
  requireConfirmation: args.includes("--require-confirmation"),
105397
- outcomeAuthor: outcomeAuthorFlag(args)
105630
+ outcomeAuthor: outcomeAuthorFlag(args),
105631
+ origin: optionValue(args, "--origin"),
105632
+ originQuote: textFlag(args, "--quote"),
105633
+ originSource: textFlag(args, "--source")
105398
105634
  });
105399
105635
  banner("flow init", `Created flow ${result.flow.id}`);
105400
105636
  console.log(` ${style.green(symbols.ok)} ${style.bold(result.flow.title)}`);
@@ -105404,7 +105640,12 @@ async function runInit(args) {
105404
105640
  console.log(` base: ${result.flow.baseBranch}`);
105405
105641
  }
105406
105642
  console.log(` owner: ${result.flow.owner?.value ?? style.dim("not set")}`);
105407
- console.log(` outcome author: ${readOutcomeAuthor(result.flow.outcomeAuthor)}`);
105643
+ console.log(` outcome author: ${outcomeAuthorLine(result.flow)}`);
105644
+ for (const line of originDetailLines(readOrigin(result.flow.origin), " "))
105645
+ console.log(line);
105646
+ if (result.originNote !== undefined) {
105647
+ console.log(` ${style.yellow(WARN)} ${result.originNote}`);
105648
+ }
105408
105649
  if (result.flow.gates?.confirmation) {
105409
105650
  console.log(` confirmation: required ${style.dim("(a terminal-minted token: `keryx flow confirm <id>`)")}`);
105410
105651
  }
@@ -105423,6 +105664,18 @@ async function runInit(args) {
105423
105664
  `Freeze and start: ${style.cyan(`keryx flow freeze ${result.flow.id}`)} then ${style.cyan(`flow start ${result.flow.id}`)}.`
105424
105665
  ]);
105425
105666
  }
105667
+ function textFlag(args, name) {
105668
+ const index = args.indexOf(name);
105669
+ if (index >= 0) {
105670
+ const next = args[index + 1];
105671
+ return next !== undefined && !/^--[A-Za-z][\w-]*(=|$)/.test(next) ? next : undefined;
105672
+ }
105673
+ return args.find((argument) => argument.startsWith(`${name}=`))?.slice(name.length + 1);
105674
+ }
105675
+ function outcomeAuthorLine(flow) {
105676
+ const reading = effectiveOutcomeAuthor(flow);
105677
+ return flow.outcomeAuthor === undefined && reading !== "unknown" ? `${reading} (derived from origin)` : reading;
105678
+ }
105426
105679
  function outcomeAuthorFlag(args) {
105427
105680
  const given = args.filter((arg) => arg === "--outcome-author" || arg.startsWith("--outcome-author=")).length;
105428
105681
  if (given > 1) {
@@ -105545,7 +105798,9 @@ async function runStatus(args) {
105545
105798
  const acLabel = flow.acChecksum ? style.green("frozen") : style.yellow("not frozen");
105546
105799
  console.log(` AC: ${acLabel}, ${Object.keys(flow.acConfirmed).length} confirmed`);
105547
105800
  console.log(` PR: ${flow.pr.url ? style.cyan(flow.pr.url) : style.dim("none")}`);
105548
- console.log(` outcome author: ${readOutcomeAuthor(flow.outcomeAuthor)}`);
105801
+ console.log(` outcome author: ${outcomeAuthorLine(flow)}`);
105802
+ for (const line of originDetailLines(readOrigin(flow.origin), " "))
105803
+ console.log(line);
105549
105804
  console.log(` owner: ${flow.owner?.value ? `${flow.owner.value} ${style.dim(`[${flow.owner.basis}]`)}` : style.dim("not set")}`);
105550
105805
  const latestSignature = flow.signatures?.at(-1);
105551
105806
  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")}`);
@@ -105814,9 +106069,43 @@ async function runOutcome(args) {
105814
106069
  author,
105815
106070
  reason: optionValue(rest, "--reason") ?? ""
105816
106071
  });
105817
- const after = readOutcomeAuthor(result.flow.outcomeAuthor);
106072
+ const after = effectiveOutcomeAuthor(result.flow);
105818
106073
  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`);
105819
106074
  }
106075
+ async function runOrigin(args) {
106076
+ if (args[0] !== "set") {
106077
+ throw new Error(ORIGIN_USAGE);
106078
+ }
106079
+ const rest = args.slice(1);
106080
+ const positionals = [];
106081
+ for (let i = 0;i < rest.length; i += 1) {
106082
+ const arg = rest[i] ?? "";
106083
+ if (arg === "--reason" || arg === "--quote" || arg === "--source") {
106084
+ i += 1;
106085
+ } else if (!arg.startsWith("--")) {
106086
+ positionals.push(arg);
106087
+ }
106088
+ }
106089
+ const id = positionals[0];
106090
+ const kind = positionals[1];
106091
+ if (!id || kind === undefined || positionals.length > 2) {
106092
+ throw new Error(ORIGIN_USAGE);
106093
+ }
106094
+ const result = await getService().originSet({
106095
+ cwd: process.cwd(),
106096
+ id,
106097
+ kind,
106098
+ reason: optionValue(rest, "--reason") ?? "",
106099
+ quote: textFlag(rest, "--quote"),
106100
+ source: textFlag(rest, "--source")
106101
+ });
106102
+ if (result.note !== undefined) {
106103
+ console.log(` ${style.yellow(WARN)} ${result.note}`);
106104
+ console.log(` ${style.dim(symbols.bullet)} Origin stays ${style.bold(result.previous)}; nothing written`);
106105
+ return;
106106
+ }
106107
+ console.log(result.changed ? ` ${style.green(symbols.ok)} Origin ${result.previous} ${style.cyan(symbols.arrow)} ${style.bold(result.next)}` : ` ${style.dim(symbols.bullet)} Origin already ${style.bold(result.next)}; nothing written`);
106108
+ }
105820
106109
  async function runAc(args) {
105821
106110
  const sub = args[0];
105822
106111
  if (sub === "confirm") {
@@ -106204,6 +106493,7 @@ function printHelp2() {
106204
106493
  'keryx flow task depends <id> <taskId> --on T1,T2|none --reason "<why>" (repair an unsatisfiable dependsOn)',
106205
106494
  'keryx flow owner set <id> --owner "<name>" --reason "<why>" (the human accountable; never inferred)',
106206
106495
  'keryx flow outcome author <id> agent|human --reason "<why>" (who wrote the outcome criterion; journaled, gates nothing)',
106496
+ 'keryx flow origin set <id> human-request|agent-finding|agent-proposal|unknown --reason "<why>" [--quote "<verbatim>"] [--source "<ref>"] (where the flow came from; journaled, gates nothing)',
106207
106497
  'keryx flow ac confirm <id> <ACn> [--note "<evidence>"] [--signed-by "<name>"]',
106208
106498
  'keryx flow ac update <id> --reason "<why>" (re-freeze the file as already edited; VOIDS prior confirmations)',
106209
106499
  'keryx flow ac update <id> --criterion ACn --text "<criterion>" --reason "<why>" (rewrite/append that one criterion, then re-freeze; VOIDS prior confirmations)',
@@ -106225,9 +106515,10 @@ function printHelp2() {
106225
106515
  ]);
106226
106516
  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/.");
106227
106517
  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.");
106518
+ note('`flow init --origin human-request|agent-finding|agent-proposal --quote "<verbatim>" --source "<ref>"` records where the flow came from. Evidence, not assertion: `human-request` is recorded only with a verbatim `--quote` of the human\'s first message with the idea AND a `--source` (channel, message id or time); `agent-finding` and `agent-proposal` need a `--source`. Without the evidence, or with an invalid kind, the origin stays `unknown`, the command still succeeds and says why. The Outcome criteria template then holds the request (or the source), the agent\'s formalization and how to observe it. `flow origin set` changes it later, with a reason. The origin labels a sample and gates nothing.');
106228
106519
  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.");
106229
106520
  }
106230
- var VALID_TASK_KINDS, WARN = "!", AC_CONFIRM_FLAGS, AC_UPDATE_FLAGS, AC_RESEAL_FLAGS, VALID_DISPOSITIONS, service4 = null, OUTCOME_AUTHOR_USAGE = 'Usage: keryx flow outcome author <id> agent|human --reason "<why>"', CHALLENGE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
106521
+ var VALID_TASK_KINDS, WARN = "!", AC_CONFIRM_FLAGS, AC_UPDATE_FLAGS, AC_RESEAL_FLAGS, VALID_DISPOSITIONS, service4 = null, OUTCOME_AUTHOR_USAGE = 'Usage: keryx flow outcome author <id> agent|human --reason "<why>"', ORIGIN_USAGE = 'Usage: keryx flow origin set <id> human-request|agent-finding|agent-proposal|unknown --reason "<why>" [--quote "<verbatim>"] [--source "<ref>"]', CHALLENGE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
106231
106522
  var init_flow = __esm(() => {
106232
106523
  init_args();
106233
106524
  init_fs();
@@ -106876,6 +107167,10 @@ function flowsInSession(flows, opts) {
106876
107167
  return explicitFlowMention(flow, text);
106877
107168
  });
106878
107169
  }
107170
+ function originField(raw) {
107171
+ const origin = readOrigin(raw);
107172
+ return origin === undefined ? {} : { origin };
107173
+ }
106879
107174
  function flowItemFromState(flow, dir) {
106880
107175
  const sessionIds = [
106881
107176
  ...new Set(flow.tasks.map((task) => task.runLink?.sessionId).filter((id) => typeof id === "string" && id.length > 0))
@@ -106894,7 +107189,8 @@ function flowItemFromState(flow, dir) {
106894
107189
  updatedAt: flow.updatedAt,
106895
107190
  source: flow.source.ref ?? flow.source.type,
106896
107191
  tasks: flow.tasks.map((task) => ({ id: task.id, title: task.title, status: task.status })),
106897
- outcomeAuthor: readOutcomeAuthor(flow.outcomeAuthor)
107192
+ outcomeAuthor: effectiveOutcomeAuthor(flow),
107193
+ ...originField(flow.origin)
106898
107194
  };
106899
107195
  }
106900
107196
  function localSacGuard() {
@@ -107123,6 +107419,7 @@ function formatFlowDetailLines(item) {
107123
107419
  `${item.id} ${item.title}`,
107124
107420
  `Status ${item.status}`,
107125
107421
  `Outcome author ${item.outcomeAuthor ?? "unknown"}`,
107422
+ ...originDetailLines(item.origin),
107126
107423
  ...item.interrupted ? [` ${item.interrupted}`] : [],
107127
107424
  ...item.uncommitted ? [` note: ${item.uncommitted}`] : [],
107128
107425
  ...(item.hygiene?.notes ?? []).map((note) => ` ${note}`),
@@ -107381,6 +107678,7 @@ function openFlows(otui, chrome, options) {
107381
107678
  }
107382
107679
  var FLOWS_COMMAND = "/flows", AC_COMMAND = "/ac", FLOWS_FOOTER, MARKER_GLYPH;
107383
107680
  var init_flow_inspector = __esm(() => {
107681
+ init_service9();
107384
107682
  init_modal_host();
107385
107683
  init_ac_kinds_surface();
107386
107684
  init_inspector_sources();
@@ -107490,6 +107788,7 @@ function extractFlowIntent(source, repoPath) {
107490
107788
  const closed = status === "done";
107491
107789
  const title = text(flow.title) ?? /^#\s+(.+)$/m.exec(description)?.[1]?.trim() ?? repoPath.split("/").at(-1) ?? repoPath;
107492
107790
  const criterion = outcomeCriterionFrom(description) ?? outcomeCriterionFrom(source.criteria ?? "");
107791
+ const origin = readOrigin(flow.origin);
107493
107792
  return {
107494
107793
  id: text(flow.id) ?? repoPath.split("/").at(-1)?.slice(0, 3) ?? repoPath,
107495
107794
  source: "flow",
@@ -107501,7 +107800,8 @@ function extractFlowIntent(source, repoPath) {
107501
107800
  flowStatus: status,
107502
107801
  closedAt: closed ? closedAtOf(flow) : null,
107503
107802
  outcome: { criterion, ...observationFrom(source.journal) },
107504
- outcomeAuthor: readOutcomeAuthor(flow.outcomeAuthor)
107803
+ outcomeAuthor: effectiveOutcomeAuthor(flow),
107804
+ ...origin === undefined ? {} : { origin }
107505
107805
  };
107506
107806
  }
107507
107807
  function extractDocpackIntent(source, repoPath) {
@@ -107717,6 +108017,32 @@ var init_corpus = __esm(() => {
107717
108017
  };
107718
108018
  });
107719
108019
 
108020
+ // src/product/by-origin.ts
108021
+ function g1aByOrigin(index) {
108022
+ const criterion = new Map;
108023
+ const notMeasured = new Map;
108024
+ for (const intent of index.intents) {
108025
+ if (intent.source !== "flow")
108026
+ continue;
108027
+ const reading = readOriginKind(intent.origin);
108028
+ const bucket = hasInstrument(intent.outcome.criterion) ? criterion : notMeasured;
108029
+ bucket.set(reading, (bucket.get(reading) ?? 0) + 1);
108030
+ }
108031
+ return ORIGIN_READINGS.map((origin) => ({ origin, criterion: criterion.get(origin) ?? 0, notMeasured: notMeasured.get(origin) ?? 0 }));
108032
+ }
108033
+ function g1aLines(rows) {
108034
+ if (rows === undefined || rows.every((row) => row.criterion + row.notMeasured === 0))
108035
+ return [];
108036
+ return [
108037
+ "G1a by origin (flows with a real criterion / not measured):",
108038
+ ...rows.map((row) => ` ${row.origin}: ${row.criterion} real criterion, ${row.notMeasured} not measured`)
108039
+ ];
108040
+ }
108041
+ var init_by_origin = __esm(() => {
108042
+ init_service9();
108043
+ init_extract3();
108044
+ });
108045
+
107720
108046
  // src/product/store.ts
107721
108047
  import { readFile as readFile114 } from "fs/promises";
107722
108048
  import path242 from "path";
@@ -107775,6 +108101,13 @@ function indexProblem(value) {
107775
108101
  return "index.json holds an observed intent without a verdict";
107776
108102
  if (intent.outcomeAuthor !== undefined && !OUTCOME_AUTHOR_READINGS.includes(intent.outcomeAuthor))
107777
108103
  return "index.json holds an intent with an unknown outcome author";
108104
+ if (intent.origin !== undefined) {
108105
+ const origin = intent.origin;
108106
+ if (!isRecord10(origin) || !ORIGIN_KINDS.includes(origin.kind))
108107
+ return "index.json holds an intent with an unknown origin";
108108
+ if (origin.quote !== undefined && typeof origin.quote !== "string" || origin.source !== undefined && typeof origin.source !== "string")
108109
+ return "index.json holds an intent with an unusable origin";
108110
+ }
107778
108111
  }
107779
108112
  return null;
107780
108113
  }
@@ -107826,7 +108159,9 @@ function buildOpenReport(index) {
107826
108159
  closedAt: intent.closedAt,
107827
108160
  outcome: hasInstrument(intent.outcome.criterion) ? intent.outcome.criterion : intent.outcome.criterion ?? NO_INSTRUMENT,
107828
108161
  hasCriterion: hasInstrument(intent.outcome.criterion),
107829
- outcomeAuthor: readOutcomeAuthor(intent.outcomeAuthor)
108162
+ outcomeAuthor: effectiveOutcomeAuthor({ outcomeAuthor: intent.outcomeAuthor, origin: intent.origin }),
108163
+ origin: readOriginKind(intent.origin),
108164
+ ...originFields(intent.origin)
107830
108165
  })).sort((a, b) => (b.closedAt ?? "").localeCompare(a.closedAt ?? "") || Number.parseInt(b.id, 10) - Number.parseInt(a.id, 10) || (a.path < b.path ? -1 : 1));
107831
108166
  const { counts } = index;
107832
108167
  return {
@@ -107840,7 +108175,15 @@ function buildOpenReport(index) {
107840
108175
  harmed: counts.harmed,
107841
108176
  inconclusive: counts.inconclusive,
107842
108177
  failures: index.failures.length,
107843
- entries
108178
+ entries,
108179
+ g1a: g1aByOrigin(index)
108180
+ };
108181
+ }
108182
+ function originFields(raw) {
108183
+ const origin = readOrigin(raw);
108184
+ return {
108185
+ ...origin?.quote === undefined ? {} : { originQuote: origin.quote },
108186
+ ...origin?.source === undefined ? {} : { originSource: origin.source }
107844
108187
  };
107845
108188
  }
107846
108189
  function openHeaderLines(report) {
@@ -107862,12 +108205,14 @@ function openEntryLines(entry) {
107862
108205
  `${label} ${entry.title}`,
107863
108206
  `${pad}outcome: ${entry.outcome}`,
107864
108207
  `${pad}outcome author: ${entry.outcomeAuthor}`,
108208
+ ...originDetailLines(entry.origin === undefined || entry.origin === "unknown" ? undefined : { kind: entry.origin, quote: entry.originQuote, source: entry.originSource }, pad),
107865
108209
  `${pad}closed ${entry.closedAt === null ? "at an unrecorded time" : entry.closedAt.slice(0, 10)}`
107866
108210
  ];
107867
108211
  }
107868
108212
  function renderOpen(report) {
107869
108213
  const rows = report.entries.flatMap((entry) => ["", ...openEntryLines(entry).map((line) => ` ${line}`)]);
107870
- return [...openHeaderLines(report), ...rows].join(`
108214
+ const g1a = g1aLines(report.g1a);
108215
+ return [...openHeaderLines(report), ...rows, ...g1a.length > 0 ? ["", ...g1a] : []].join(`
107871
108216
  `);
107872
108217
  }
107873
108218
  async function loadOpenReport(cwd) {
@@ -107900,6 +108245,7 @@ function renderIndexSummary(index, file) {
107900
108245
  var REBUILD = "Run `keryx product index`.";
107901
108246
  var init_open = __esm(() => {
107902
108247
  init_service9();
108248
+ init_by_origin();
107903
108249
  init_extract3();
107904
108250
  init_store7();
107905
108251
  });
@@ -107908,6 +108254,7 @@ var init_open = __esm(() => {
107908
108254
  var init_service14 = __esm(() => {
107909
108255
  init_corpus();
107910
108256
  init_extract3();
108257
+ init_by_origin();
107911
108258
  init_open();
107912
108259
  init_store7();
107913
108260
  });
@@ -115426,6 +115773,11 @@ var init_agent_commands = __esm(() => {
115426
115773
  description: "List doc sections that likely went stale because of the working diff, with Jev",
115427
115774
  modes: AGENT_ONLY
115428
115775
  },
115776
+ {
115777
+ name: "/flow",
115778
+ description: `Show a flow's origin, or set it \u2014 /flow origin [<id>] | /flow origin <id> <kind> --reason "..."`,
115779
+ modes: AGENT_ONLY
115780
+ },
115429
115781
  {
115430
115782
  name: "/opencomments",
115431
115783
  description: "List open PR review comments with a Jev resolved/still-open/escalation label \u2014 /opencomments <owner/repo> <pr>",
@@ -115852,12 +116204,25 @@ function proseEntries(body) {
115852
116204
  const entries = [];
115853
116205
  let sawHint = false;
115854
116206
  let open = false;
116207
+ let skippingOrigin = false;
115855
116208
  for (const raw of body.split(/\r?\n/)) {
115856
116209
  const line = raw.trim();
115857
116210
  if (line.length === 0) {
115858
116211
  open = false;
115859
116212
  continue;
115860
116213
  }
116214
+ if (/^[-*]\s+/.test(line)) {
116215
+ const text = line.replace(/^[-*]\s+/, "");
116216
+ skippingOrigin = isOriginTemplateBullet(text);
116217
+ if (skippingOrigin) {
116218
+ if (isOriginPlaceholderBullet(text))
116219
+ sawHint = true;
116220
+ open = false;
116221
+ continue;
116222
+ }
116223
+ } else if (skippingOrigin && /^\s+\S/.test(raw)) {
116224
+ continue;
116225
+ }
115861
116226
  const unquoted = line.replace(/^>\s*/, "").replace(/^[-*]\s+/, "");
115862
116227
  if (unquoted === OUTCOME_HINT) {
115863
116228
  sawHint = true;
@@ -129625,6 +129990,115 @@ var init_jev_scenarios_command = __esm(() => {
129625
129990
  init_scope();
129626
129991
  });
129627
129992
 
129993
+ // src/tui/flow-origin-command.ts
129994
+ function isFlowOriginCommand(name) {
129995
+ return name === FLOW_ORIGIN_COMMAND;
129996
+ }
129997
+ function splitShellWords(line) {
129998
+ const words = [];
129999
+ let current = "";
130000
+ let quote;
130001
+ let started = false;
130002
+ for (const char of line) {
130003
+ if (quote !== undefined) {
130004
+ if (char === quote)
130005
+ quote = undefined;
130006
+ else
130007
+ current += char;
130008
+ } else if (char === '"' || char === "'") {
130009
+ quote = char;
130010
+ started = true;
130011
+ } else if (/\s/.test(char)) {
130012
+ if (started || current.length > 0)
130013
+ words.push(current);
130014
+ current = "";
130015
+ started = false;
130016
+ } else {
130017
+ current += char;
130018
+ started = true;
130019
+ }
130020
+ }
130021
+ if (started || current.length > 0)
130022
+ words.push(current);
130023
+ return words;
130024
+ }
130025
+ function parseOriginArgs(words) {
130026
+ const parsed = { positionals: [] };
130027
+ for (let i = 0;i < words.length; i += 1) {
130028
+ const word = words[i] ?? "";
130029
+ if (word === "--reason" || word === "--quote" || word === "--source") {
130030
+ const value = words[i + 1];
130031
+ i += 1;
130032
+ if (word === "--reason")
130033
+ parsed.reason = value;
130034
+ else if (word === "--quote")
130035
+ parsed.quote = value;
130036
+ else
130037
+ parsed.source = value;
130038
+ } else {
130039
+ parsed.positionals.push(word);
130040
+ }
130041
+ }
130042
+ return parsed;
130043
+ }
130044
+ function renderFlowOrigin(id, origin) {
130045
+ return [`${id}`, ...originDetailLines(readOrigin(origin), " ")].join(`
130046
+ `);
130047
+ }
130048
+ async function runFlowOriginForShell(cwd, line, service = createFlowService(flowServiceDeps())) {
130049
+ const words = splitShellWords(line.trim());
130050
+ const [, sub, ...rest] = words;
130051
+ if (sub !== "origin") {
130052
+ return `Usage: ${FLOW_ORIGIN_USAGE}`;
130053
+ }
130054
+ const args = parseOriginArgs(rest);
130055
+ const [id, kind, ...extra] = args.positionals;
130056
+ if (extra.length > 0) {
130057
+ return `Usage: ${FLOW_ORIGIN_USAGE}`;
130058
+ }
130059
+ if (id === undefined) {
130060
+ const flows = await service.list({ cwd });
130061
+ if (flows.length === 0)
130062
+ return "No flows.";
130063
+ const blocks = [];
130064
+ for (const summary of flows) {
130065
+ const flow = await service.get({ cwd, id: summary.id });
130066
+ blocks.push(renderFlowOrigin(flow.id, flow.origin));
130067
+ }
130068
+ return blocks.join(`
130069
+ `);
130070
+ }
130071
+ if (kind === undefined) {
130072
+ const flow = await service.get({ cwd, id });
130073
+ return renderFlowOrigin(flow.id, flow.origin);
130074
+ }
130075
+ if (args.reason === undefined || args.reason.trim().length === 0) {
130076
+ return `/flow origin ${id} ${kind} needs --reason "<why>".
130077
+ Usage: ${FLOW_ORIGIN_USAGE}`;
130078
+ }
130079
+ const result = await service.originSet({
130080
+ cwd,
130081
+ id,
130082
+ kind,
130083
+ reason: args.reason,
130084
+ quote: args.quote,
130085
+ source: args.source
130086
+ });
130087
+ if (result.note !== undefined) {
130088
+ return `${result.note}
130089
+ Nothing written; origin stays ${result.previous}.
130090
+ ${renderFlowOrigin(result.flow.id, result.flow.origin)}`;
130091
+ }
130092
+ const head = result.changed ? `origin ${result.previous} -> ${result.next}` : `origin already ${result.next}; nothing written`;
130093
+ return `${head}
130094
+ ${renderFlowOrigin(result.flow.id, result.flow.origin)}`;
130095
+ }
130096
+ var FLOW_ORIGIN_COMMAND = "/flow", FLOW_ORIGIN_USAGE = '/flow origin [<id>] | /flow origin <id> <kind> --reason "<why>" [--quote "<verbatim>"] [--source "<ref>"] (kind: human-request, agent-finding, agent-proposal, unknown)';
130097
+ var init_flow_origin_command = __esm(() => {
130098
+ init_flow();
130099
+ init_service9();
130100
+ });
130101
+
129628
130102
  // src/review/jev-docs-config.ts
129629
130103
  import { readFile as readFile138 } from "fs/promises";
129630
130104
  import path277 from "path";
@@ -150959,6 +151433,19 @@ ${formatThemeList(getThemeId())}`);
150959
151433
  `);
150960
151434
  } catch (error) {
150961
151435
  io.onSystem?.(`review-jev-docs: ${error instanceof Error ? error.message : String(error)}
151436
+ `);
151437
+ }
151438
+ });
151439
+ return;
151440
+ }
151441
+ if (isFlowOriginCommand(command.name)) {
151442
+ const cwd = inspectorCwd();
151443
+ trackCommandWork(async () => {
151444
+ try {
151445
+ io.onSystem?.(`${await runFlowOriginForShell(cwd, line)}
151446
+ `);
151447
+ } catch (error) {
151448
+ io.onSystem?.(`flow origin: ${error instanceof Error ? error.message : String(error)}
150962
151449
  `);
150963
151450
  }
150964
151451
  });
@@ -151888,6 +152375,7 @@ var init_tui_shell = __esm(() => {
151888
152375
  init_jev_contract_command();
151889
152376
  init_jev_triage_command();
151890
152377
  init_jev_scenarios_command();
152378
+ init_flow_origin_command();
151891
152379
  init_jev_docs_command();
151892
152380
  init_jev_comments_command();
151893
152381
  init_ci_triage_source();
@@ -177285,7 +177773,7 @@ var init_command_registry = __esm(() => {
177285
177773
  {
177286
177774
  module: "tasks",
177287
177775
  command: "flow status",
177288
- 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.",
177776
+ summary: "One flow's full status: lifecycle state, outcome author (agent, human or unknown), origin (kind, quote, source), AC freeze/confirmation count, PR, owner, latest signature, task list, and recent history.",
177289
177777
  intent: ["\u0441\u0442\u0430\u0442\u0443\u0441 \u0444\u043B\u043E\u0443", "flow status", "flow details", "show one flow"],
177290
177778
  args: [{ name: "<id>", type: "string", required: true, desc: "flow id" }],
177291
177779
  json: false,
@@ -177294,7 +177782,7 @@ var init_command_registry = __esm(() => {
177294
177782
  {
177295
177783
  module: "tasks",
177296
177784
  command: "flow init",
177297
- 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).",
177785
+ 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). `--origin` records where the flow came from, with the " + "evidence its kind needs (`human-request`: a verbatim `--quote` and a `--source`; `agent-finding` and " + "`agent-proposal`: a `--source`); without it the origin stays `unknown`.",
177298
177786
  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"],
177299
177787
  args: [
177300
177788
  { name: "title", type: "string", required: false, desc: "work title; required unless --issue is given" },
@@ -177313,7 +177801,15 @@ var init_command_registry = __esm(() => {
177313
177801
  type: "string",
177314
177802
  required: false,
177315
177803
  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"
177316
- }
177804
+ },
177805
+ {
177806
+ name: "origin",
177807
+ type: "string",
177808
+ required: false,
177809
+ desc: "where the flow came from: human-request, agent-finding or agent-proposal; recorded only with its evidence, otherwise it stays unknown and the command still succeeds; an invalid kind is reported and ignored; gates nothing"
177810
+ },
177811
+ { name: "quote", type: "string", required: false, desc: "the human's request, verbatim and byte for byte (required with --origin human-request)" },
177812
+ { name: "source", type: "string", required: false, desc: "where the request or finding came from: channel, message id, time or check (required with every --origin kind)" }
177317
177813
  ],
177318
177814
  json: false,
177319
177815
  read: false,
@@ -177350,6 +177846,22 @@ var init_command_registry = __esm(() => {
177350
177846
  read: false,
177351
177847
  sideEffects: ["writes flow.json's outcomeAuthor field and appends one journal.md line and a history entry"]
177352
177848
  },
177849
+ {
177850
+ module: "tasks",
177851
+ command: "flow origin set",
177852
+ summary: "Change where a flow came from (human-request, agent-finding, agent-proposal, or unknown to clear it). Requires a reason; " + "the evidence rule applies (a verbatim quote and a source for human-request, a source for the agent kinds), and without it " + "nothing is written and the reason is printed. Appends one journal line naming the old origin, the new one and the reason. " + "Works on any flow, never changes the outcome author, gates nothing.",
177853
+ intent: ["\u043E\u0442\u043A\u0443\u0434\u0430 \u043F\u0440\u0438\u0448\u0451\u043B \u0444\u043B\u043E\u0443", "flow origin set", "set flow origin", "mark flow as human request", "\u043F\u0440\u043E\u0438\u0441\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0444\u043B\u043E\u0443"],
177854
+ args: [
177855
+ { name: "<id>", type: "string", required: true, desc: "flow id" },
177856
+ { name: "<kind>", type: "string", required: true, desc: "human-request, agent-finding, agent-proposal or unknown" },
177857
+ { name: "reason", type: "string", required: true, desc: "why the origin is being set or changed; one line" },
177858
+ { name: "quote", type: "string", required: false, desc: "the human's request, verbatim; kept from the flow when omitted" },
177859
+ { name: "source", type: "string", required: false, desc: "channel, message id, time or check; kept from the flow when omitted" }
177860
+ ],
177861
+ json: false,
177862
+ read: false,
177863
+ sideEffects: ["writes flow.json's origin field and appends one journal.md line and a history entry"]
177864
+ },
177353
177865
  {
177354
177866
  module: "tasks",
177355
177867
  command: "flow ac confirm",
@@ -205542,6 +206054,7 @@ var init_group_subcommands = __esm(() => {
205542
206054
  "check-ac",
205543
206055
  "owner",
205544
206056
  "outcome",
206057
+ "origin",
205545
206058
  "implemented",
205546
206059
  "complete",
205547
206060
  "check-complete",
package/dist/core.js CHANGED
@@ -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,
@@ -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.59",
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": {